├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.YML ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── other-problem-question-proposal.md ├── button-boosty.png ├── button-counters.png ├── button-discord.png ├── button-download.png ├── logo.png ├── sponsors │ └── signpath.png └── workflows │ ├── deploy.yml │ └── pr_lint.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .versionrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── package.json ├── packages ├── common │ ├── enums │ │ ├── country.ts │ │ ├── osu.ts │ │ └── tosu.ts │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── utils │ │ ├── arguments.ts │ │ ├── config.ts │ │ ├── directories.ts │ │ ├── downloader.ts │ │ ├── ingame.ts │ │ ├── json.ts │ │ ├── logger.ts │ │ ├── manipulation.ts │ │ ├── platforms.ts │ │ ├── sleep.ts │ │ └── unzip.ts ├── ingame-overlay-updater │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── ingame-overlay │ ├── .gitignore │ ├── README.md │ ├── asset │ │ └── tosu.ico │ ├── electron.vite.config.mjs │ ├── main │ │ ├── index.ts │ │ ├── keybind.ts │ │ ├── overlay │ │ │ ├── manager.ts │ │ │ └── process │ │ │ │ ├── index.ts │ │ │ │ └── input.ts │ │ └── types │ │ │ ├── files.d.ts │ │ │ └── wql-process-monitor.d.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── preload │ │ └── index.ts │ └── tsconfig.json ├── server │ ├── assets │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-BoldItalic.ttf │ │ │ ├── Roboto-Italic.ttf │ │ │ ├── Roboto-Regular.ttf │ │ │ └── style.css │ │ ├── homepage.html │ │ ├── homepage.js │ │ ├── homepage.min.css │ │ ├── icons │ │ │ ├── fonts │ │ │ │ ├── icomoon.eot │ │ │ │ ├── icomoon.svg │ │ │ │ ├── icomoon.ttf │ │ │ │ └── icomoon.woff │ │ │ └── style.css │ │ ├── images │ │ │ ├── 39979.png │ │ │ ├── discord-5865f2.svg │ │ │ ├── github-000000.svg │ │ │ └── twitter-1DA1F2.svg │ │ ├── ingame.css │ │ ├── ingame.html │ │ ├── ingame.js │ │ ├── overlayDisplay.html │ │ └── vue.js │ ├── index.ts │ ├── package.json │ ├── router │ │ ├── assets.ts │ │ ├── index.ts │ │ ├── scApi.ts │ │ ├── socket.ts │ │ ├── v1.ts │ │ └── v2.ts │ ├── scripts │ │ └── beatmapFile.ts │ ├── tsconfig.json │ └── utils │ │ ├── commands.ts │ │ ├── counters.ts │ │ ├── counters.types.ts │ │ ├── directories.ts │ │ ├── hashing.ts │ │ ├── homepage.ts │ │ ├── htmls.ts │ │ ├── http.ts │ │ ├── index.ts │ │ ├── parseSettings.ts │ │ └── socket.ts ├── tosu │ ├── package.json │ ├── pkg.linux.json │ ├── pkg.win.json │ ├── src │ │ ├── api │ │ │ ├── types │ │ │ │ ├── sc.ts │ │ │ │ ├── v1.ts │ │ │ │ └── v2.ts │ │ │ └── utils │ │ │ │ ├── buildResult.ts │ │ │ │ ├── buildResultSC.ts │ │ │ │ ├── buildResultV2.ts │ │ │ │ └── buildResultV2Precise.ts │ │ ├── assets │ │ │ └── icon.ico │ │ ├── index.ts │ │ ├── instances │ │ │ ├── index.ts │ │ │ ├── lazerInstance.ts │ │ │ ├── manager.ts │ │ │ └── osuInstance.ts │ │ ├── memory │ │ │ ├── index.ts │ │ │ ├── lazer.ts │ │ │ ├── stable.ts │ │ │ └── types.ts │ │ ├── postBuild.ts │ │ ├── states │ │ │ ├── bassDensity.ts │ │ │ ├── beatmap.ts │ │ │ ├── gameplay.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ ├── lazerMultiSpectating.ts │ │ │ ├── menu.ts │ │ │ ├── resultScreen.ts │ │ │ ├── settings.ts │ │ │ ├── tourney.ts │ │ │ ├── types.ts │ │ │ └── user.ts │ │ └── utils │ │ │ ├── bindings.ts │ │ │ ├── calculators.ts │ │ │ ├── converters.ts │ │ │ ├── multiplayer.types.ts │ │ │ ├── osuMods.ts │ │ │ ├── osuMods.types.ts │ │ │ └── settings.types.ts │ └── tsconfig.json ├── tsprocess │ ├── .gitignore │ ├── binding.gyp │ ├── lib │ │ ├── .clang-format │ │ ├── .clangd │ │ ├── functions.cc │ │ ├── logger.h │ │ └── memory │ │ │ ├── memory.h │ │ │ ├── memory_linux.cc │ │ │ └── memory_windows.cc │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── process.ts │ └── tsconfig.json └── updater │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.eslintignore: -------------------------------------------------------------------------------- 1 | **/static/**/* 2 | 3 | packages/server/assets/homepage.js 4 | packages/server/assets/ingame.js 5 | packages/server/assets/vue.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "standard", 5 | "plugin:prettier/recommended" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "semi": ["error", "always"], 11 | "no-var": "error", 12 | "no-unused-vars": "off", 13 | "guard-for-in": "error", 14 | "@typescript-eslint/no-unused-vars": ["error", { 15 | "vars": "all", 16 | "args": "after-used", 17 | "ignoreRestSiblings": true, 18 | "argsIgnorePattern": "^_" 19 | }], 20 | "prefer-const": ["error", { 21 | "destructuring": "all" 22 | }], 23 | "standard/no-callback-literal": "off" 24 | }, 25 | "parserOptions": { 26 | "ecmaVersion": 6, 27 | "sourceType": "module" 28 | }, 29 | "overrides": [ 30 | { 31 | "files": "*.ts", 32 | "rules": { 33 | "no-undef": "off", 34 | "no-redeclare": "off", 35 | "@typescript-eslint/no-redeclare": ["error"], 36 | "no-use-before-define": "off", 37 | "guard-for-in": "off" 38 | } 39 | }, 40 | { 41 | "files": "*.d.ts", 42 | "rules": { 43 | "no-useless-constructor": "off", 44 | "@typescript-eslint/no-unused-vars": "off" 45 | } 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /.github/FUNDING.YML: -------------------------------------------------------------------------------- 1 | custom: "https://boosty.to/kotrik/donate" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 1. Describe your issue as detailed as possible 11 | 2. Where does **tosu.exe** located (Folder path, Example: D:\tosu\tosu.exe) 12 | 3. Record video showing: 13 | - tosu.env & config.ini 14 | - Enable debug in settings 15 | - Close tosu, then start it again 16 | - Open osu and show what not working 17 | 4. Windows or Linux 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the feature request here. It would be good if you describe why you need this functionality, e.g. is there any idea behind this functionality etc. 15 | 16 | **Describe alternatives you've considered (optional)** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other-problem-question-proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other problem/question/proposal 3 | about: Write in that template anything other related to tosu! 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/button-boosty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/.github/button-boosty.png -------------------------------------------------------------------------------- /.github/button-counters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/.github/button-counters.png -------------------------------------------------------------------------------- /.github/button-discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/.github/button-discord.png -------------------------------------------------------------------------------- /.github/button-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/.github/button-download.png -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/.github/logo.png -------------------------------------------------------------------------------- /.github/sponsors/signpath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/.github/sponsors/signpath.png -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: build & deploy 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | tags: 7 | - "v*" 8 | paths: 9 | - ".github/workflows/**/*" 10 | - "packages/**/*" 11 | - "package.json" 12 | - "pnpm-lock.yaml" 13 | pull_request: 14 | paths: 15 | - "packages/**/*" 16 | - "package.json" 17 | - "pnpm-lock.yaml" 18 | 19 | concurrency: 20 | group: deploy-${{ github.ref_name }} 21 | cancel-in-progress: true 22 | 23 | permissions: 24 | contents: write 25 | 26 | jobs: 27 | build: 28 | strategy: 29 | matrix: 30 | os: [ windows-latest, ubuntu-latest ] 31 | 32 | runs-on: ${{ matrix.os }} 33 | 34 | name: "build for ${{ matrix.os }}" 35 | steps: 36 | - name: 🛎️ - Checkout 37 | uses: actions/checkout@v3 38 | 39 | - name: 📐 - Define variables (1) 40 | if: github.event_name == 'pull_request' 41 | id: set-pr-sha 42 | shell: bash 43 | run: echo "SHORT_PR_SHA=$(git rev-parse HEAD | cut -c 1-7)" >> "$GITHUB_OUTPUT" 44 | 45 | - name: 🛠️ - Install Node 46 | uses: actions/setup-node@v3 47 | with: { node-version: 20.11.1 } 48 | 49 | - name: 🛠️ - Install Deps 50 | run: npm install -g pnpm@10.10.0 && pnpm install --frozen-lockfile 51 | 52 | - name: 📦 - Build (windows) 53 | if: ${{ matrix.os == 'windows-latest' }} 54 | run: pnpm build:win 55 | 56 | - name: 📦 - Build (linux) 57 | if: ${{ matrix.os == 'ubuntu-latest' }} 58 | run: pnpm build:linux 59 | 60 | - name: 📦 - Build ingame-overlay (windows) 61 | if: ${{ matrix.os == 'windows-latest' }} 62 | run: pnpm build:overlay 63 | 64 | - name: 📦 - Prepare overlay for upload 65 | if: ${{ matrix.os == 'windows-latest' }} 66 | shell: bash 67 | run: sed -nE "s/.*'([0-9]+\.[0-9]+\.[0-9]+)'.*/\1/p" packages/tosu/_version.js > packages/ingame-overlay/pack/win-unpacked/version 68 | 69 | - name: 🚀 - Upload artifacts for windows 70 | id: upload-artifact-windows 71 | if: ${{ matrix.os == 'windows-latest' }} 72 | uses: actions/upload-artifact@v4 73 | with: { 74 | name: "tosu-windows-${{ steps.set-pr-sha.outputs.SHORT_PR_SHA || github.ref_name }}", 75 | path: packages/tosu/dist/tosu.exe 76 | } 77 | 78 | - name: 🚀 - Upload overlay artifacts for windows 79 | if: ${{ matrix.os == 'windows-latest' }} 80 | uses: actions/upload-artifact@v4 81 | with: { 82 | name: "tosu-overlay-${{ steps.set-pr-sha.outputs.SHORT_PR_SHA || github.ref_name }}", 83 | path: packages/ingame-overlay/pack/win-unpacked/* 84 | } 85 | 86 | - name: 🚀 - Upload artifacts for linux 87 | if: ${{ matrix.os == 'ubuntu-latest' }} 88 | uses: actions/upload-artifact@v4 89 | with: { 90 | name: "tosu-linux-${{ steps.set-pr-sha.outputs.SHORT_PR_SHA || github.ref_name }}", 91 | path: packages/tosu/dist/tosu 92 | } 93 | 94 | - name: ✍️ - Creating sign request to SignPath 95 | if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' 96 | uses: signpath/github-action-submit-signing-request@v1.1 97 | with: 98 | api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' 99 | organization-id: '5b025426-9c79-4f04-87e8-64cf64131d0b' 100 | project-slug: 'tosu' 101 | signing-policy-slug: 'release-signing' 102 | github-artifact-id: '${{ steps.upload-artifact-windows.outputs.artifact-id }}' 103 | 104 | # - name: 🚀 - Upload artifacts for mac 105 | # if: matrix.os == 'macos-latest' 106 | # uses: actions/upload-artifact@v3 107 | # with: { 108 | # name: "${{ steps.set-artifact-name.outputs.ARTIFACT_NAME }}", 109 | # path: packages/tosu/dist/tosu.exe 110 | # } 111 | -------------------------------------------------------------------------------- /.github/workflows/pr_lint.yml: -------------------------------------------------------------------------------- 1 | name: PR-check 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: lint-${{ github.ref_name }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | #runs-on: windows-latest 14 | steps: 15 | - name: Checkout 🛎️ 16 | uses: actions/checkout@v3 17 | 18 | - name: Install Node.js 🔧 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20.11.1 22 | 23 | - name: Install Deps 🔧 24 | run: | 25 | npm install -g pnpm@10.10.0 26 | pnpm install --frozen-lockfile 27 | 28 | - name: Lint PR (prettier) 29 | run: | 30 | pnpm run prettier:ci 31 | 32 | - name: Lint PR (eslint) 33 | run: | 34 | pnpm run lint:ci -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | tosu.env 3 | tsosu.env 4 | dist/ 5 | node_modules 6 | yarn-error.log 7 | static/ 8 | package-lock.json 9 | config.ini 10 | tosu.exe 11 | _version.js 12 | **/tosu/gameOverlay/ 13 | **/tosu/game-overlay/ 14 | logs/ 15 | settings/ 16 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11.1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules/ 3 | dist/ 4 | static/ 5 | packages/server/assets/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "quoteProps": "as-needed", 9 | "bracketSpacing": true, 10 | "endOfLine": "auto", 11 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 12 | "importOrderSeparation": true, 13 | "importOrderSortSpecifiers": true, 14 | "importOrder": ["^@/(.*)$", "^[./]"] 15 | } 16 | -------------------------------------------------------------------------------- /.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "bumpFiles": [ 3 | { 4 | "filename": "package.json", 5 | "type": "json" 6 | }, 7 | { 8 | "filename": "packages/tosu/package.json", 9 | "type": "json" 10 | }, 11 | { 12 | "filename": "packages/ingame-overlay/package.json", 13 | "type": "json" 14 | } 15 | ], 16 | "packageFiles": [ 17 | { 18 | "filename": "package.json", 19 | "type": "json" 20 | }, 21 | { 22 | "filename": "packages/tosu/package.json", 23 | "type": "json" 24 | }, 25 | { 26 | "filename": "packages/ingame-overlay/package.json", 27 | "type": "json" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "run tosu", 10 | "request": "launch", 11 | "args": ["src/index.ts"], 12 | "outputCapture": "std", 13 | "runtimeArgs": ["--nolazy", "--inspect", "--expose-gc", "--inspect", "-r", "ts-node/register", "-r", "tsconfig-paths/register"], 14 | "cwd": "${workspaceRoot}/packages/tosu", 15 | }, 16 | { 17 | "type": "node", 18 | "name": "Current TS File", 19 | "request": "launch", 20 | "args": ["${relativeFile}"], 21 | "outputCapture": "std", 22 | "runtimeArgs": ["--nolazy", "--inspect", "--expose-gc", "-r", "ts-node/register", "-r", "tsconfig-paths/register"], 23 | "cwd": "${workspaceRoot}/packages/tosu", 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "discord.enabled": true, 3 | "files.associations": { 4 | "xlocale": "cpp" 5 | }, 6 | "editor.tabSize": 4, 7 | "editor.insertSpaces": true, 8 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to tosu 2 | I love your input! I want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the code 6 | - Submitting a fix 7 | - Proposing new features 8 | - Becoming a maintainer 9 | 10 | ## Develop with Github 11 | Use github to host code, to track issues and feature requests, as well as accept pull requests. 12 | 13 | ## We Use Github Flow, So All Code Changes Happen Through Pull Requests 14 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. Make sure your code lints. (`pnpm run prettier:fix`) 18 | 3. Issue that pull request! 19 | 20 | ## Any contributions you make will be under the GNU Lesser General Public License v3.0 21 | In short, when you submit code changes, your submissions are understood to be under the same [GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern. 22 | 23 | ## Report bugs using Github's [issues](https://github.com/tosuapp/tosu/issues) 24 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/tosuapp/tosu/issues/new); it's that easy! 25 | 26 | ## Write bug reports with detail, background, and sample code 27 | 28 | **Great Bug Reports** tend to have: 29 | 30 | - A quick summary and/or background 31 | - Steps to reproduce 32 | - Be specific! 33 | - What you expected would happen 34 | - What actually happens 35 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 36 | 37 | People *love* thorough bug reports. I'm not even kidding. 38 | 39 | ## References 40 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebookarchive/draft-js/blob/main/CONTRIBUTING.md) 41 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Requirements 4 | 5 | - `Typescript` >=5.3.3 6 | - `Node.js` >=20.11.1 7 | - `Rust` >= any 8 | - `pnpm` >= 10.10.0 9 | 10 | 11 | 12 | 1. Clone repository 13 | 14 | ``` 15 | git clone https://github.com/tosuapp/tosu.git 16 | ``` 17 | 18 | 2. Go to project folder 19 | ``` 20 | cd tosu 21 | ``` 22 | 23 | 3. Install pnpm 24 | ``` 25 | npm install -g pnpm 26 | ``` 27 | 28 | 4. Install dependencies 29 | ``` 30 | pnpm install 31 | ``` 32 | 33 | 5. To run tosu in dev mode 34 | ``` 35 | pnpm run start 36 | ``` 37 | 38 | 39 | 6. Compile tosu 40 | 41 | For Windows: 42 | ``` 43 | pnpm install && pnpm build:win 44 | ``` 45 | 46 | For Linux 47 | ``` 48 | pnpm install && pnpm build:linux 49 | ``` 50 | 51 | 7. Go to `/tosu/packages/tosu/dist`, and there is your's tosu build 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Mikhail Babynichev", 3 | "license": "GPL-3.0", 4 | "version": "4.9.0", 5 | "packageManager": "pnpm@10.10.0", 6 | "scripts": { 7 | "prepare": "husky install", 8 | "start": "pnpm run -C packages/tosu run:dev", 9 | "build:win": "pnpm run -C packages/tosu compile:win", 10 | "build:linux": "pnpm run -C packages/tosu compile:linux", 11 | "build:overlay": "pnpm run -C \"packages/ingame-overlay\" dist", 12 | "release": "standard-version", 13 | "prettier:fix": "prettier --write \"**/*.{js,jsx,ts,tsx,css}\"", 14 | "prettier:ci": "prettier --check \"**/*.{js,jsx,ts,tsx,css}\"", 15 | "lint:ci": "eslint --ext .ts,.d.ts .", 16 | "lint:fix": "eslint --fix --ext .ts,.d.ts ." 17 | }, 18 | "devDependencies": { 19 | "@trivago/prettier-plugin-sort-imports": "^5.2.2", 20 | "@types/node": "^20.11.1", 21 | "@typescript-eslint/eslint-plugin": "^8.27.0", 22 | "@typescript-eslint/parser": "^8.27.0", 23 | "eslint": "^8", 24 | "eslint-config-prettier": "^10.1.1", 25 | "eslint-plugin-prettier": "^5.2.3", 26 | "husky": "^9.1.7", 27 | "lint-staged": "^15.5.0", 28 | "prettier": "^3.5.3", 29 | "standard": "^17.1.0", 30 | "standard-version": "^9.5.0", 31 | "tsconfig-paths": "^4.2.0", 32 | "typescript": "^5.8.2" 33 | }, 34 | "lint-staged": { 35 | "**/*.{js,ts}": [ 36 | "pnpm run prettier:fix", 37 | "pnpm run lint:fix" 38 | ] 39 | }, 40 | "homepage": "https://github.com/tosuapp/tosu#readme", 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/tosuapp/tosu.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/tosuapp/tosu/issues" 47 | }, 48 | "engines": { 49 | "node": ">=20.11.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/common/enums/country.ts: -------------------------------------------------------------------------------- 1 | export enum CountryCodes { 2 | 'oc' = 1, 3 | 'eu' = 2, 4 | 'ad' = 3, 5 | 'ae' = 4, 6 | 'af' = 5, 7 | 'ag' = 6, 8 | 'ai' = 7, 9 | 'al' = 8, 10 | 'am' = 9, 11 | 'an' = 10, 12 | 'ao' = 11, 13 | 'aq' = 12, 14 | 'ar' = 13, 15 | 'as' = 14, 16 | 'at' = 15, 17 | 'au' = 16, 18 | 'aw' = 17, 19 | 'az' = 18, 20 | 'ba' = 19, 21 | 'bb' = 20, 22 | 'bd' = 21, 23 | 'be' = 22, 24 | 'bf' = 23, 25 | 'bg' = 24, 26 | 'bh' = 25, 27 | 'bi' = 26, 28 | 'bj' = 27, 29 | 'bm' = 28, 30 | 'bn' = 29, 31 | 'bo' = 30, 32 | 'br' = 31, 33 | 'bs' = 32, 34 | 'bt' = 33, 35 | 'bv' = 34, 36 | 'bw' = 35, 37 | 'by' = 36, 38 | 'bz' = 37, 39 | 'ca' = 38, 40 | 'cc' = 39, 41 | 'cd' = 40, 42 | 'cf' = 41, 43 | 'cg' = 42, 44 | 'ch' = 43, 45 | 'ci' = 44, 46 | 'ck' = 45, 47 | 'cl' = 46, 48 | 'cm' = 47, 49 | 'cn' = 48, 50 | 'co' = 49, 51 | 'cr' = 50, 52 | 'cu' = 51, 53 | 'cv' = 52, 54 | 'cx' = 53, 55 | 'cy' = 54, 56 | 'cz' = 55, 57 | 'de' = 56, 58 | 'dj' = 57, 59 | 'dk' = 58, 60 | 'dm' = 59, 61 | 'do' = 60, 62 | 'dz' = 61, 63 | 'ec' = 62, 64 | 'ee' = 63, 65 | 'eg' = 64, 66 | 'eh' = 65, 67 | 'er' = 66, 68 | 'es' = 67, 69 | 'et' = 68, 70 | 'fi' = 69, 71 | 'fj' = 70, 72 | 'fk' = 71, 73 | 'fm' = 72, 74 | 'fo' = 73, 75 | 'fr' = 74, 76 | 'fx' = 75, 77 | 'ga' = 76, 78 | 'gb' = 77, 79 | 'gd' = 78, 80 | 'ge' = 79, 81 | 'gf' = 80, 82 | 'gh' = 81, 83 | 'gi' = 82, 84 | 'gl' = 83, 85 | 'gm' = 84, 86 | 'gn' = 85, 87 | 'gp' = 86, 88 | 'gq' = 87, 89 | 'gr' = 88, 90 | 'gs' = 89, 91 | 'gt' = 90, 92 | 'gu' = 91, 93 | 'gw' = 92, 94 | 'gy' = 93, 95 | 'hk' = 94, 96 | 'hm' = 95, 97 | 'hn' = 96, 98 | 'hr' = 97, 99 | 'ht' = 98, 100 | 'hu' = 99, 101 | 'id' = 100, 102 | 'ie' = 101, 103 | 'il' = 102, 104 | 'in' = 103, 105 | 'io' = 104, 106 | 'iq' = 105, 107 | 'ir' = 106, 108 | 'is' = 107, 109 | 'it' = 108, 110 | 'jm' = 109, 111 | 'jo' = 110, 112 | 'jp' = 111, 113 | 'ke' = 112, 114 | 'kg' = 113, 115 | 'kh' = 114, 116 | 'ki' = 115, 117 | 'km' = 116, 118 | 'kn' = 117, 119 | 'kp' = 118, 120 | 'kr' = 119, 121 | 'kw' = 120, 122 | 'ky' = 121, 123 | 'kz' = 122, 124 | 'la' = 123, 125 | 'lb' = 124, 126 | 'lc' = 125, 127 | 'li' = 126, 128 | 'lk' = 127, 129 | 'lr' = 128, 130 | 'ls' = 129, 131 | 'lt' = 130, 132 | 'lu' = 131, 133 | 'lv' = 132, 134 | 'ly' = 133, 135 | 'ma' = 134, 136 | 'mc' = 135, 137 | 'md' = 136, 138 | 'mg' = 137, 139 | 'mh' = 138, 140 | 'mk' = 139, 141 | 'ml' = 140, 142 | 'mm' = 141, 143 | 'mn' = 142, 144 | 'mo' = 143, 145 | 'mp' = 144, 146 | 'mq' = 145, 147 | 'mr' = 146, 148 | 'ms' = 147, 149 | 'mt' = 148, 150 | 'mu' = 149, 151 | 'mv' = 150, 152 | 'mw' = 151, 153 | 'mx' = 152, 154 | 'my' = 153, 155 | 'mz' = 154, 156 | 'na' = 155, 157 | 'nc' = 156, 158 | 'ne' = 157, 159 | 'nf' = 158, 160 | 'ng' = 159, 161 | 'ni' = 160, 162 | 'nl' = 161, 163 | 'no' = 162, 164 | 'np' = 163, 165 | 'nr' = 164, 166 | 'nu' = 165, 167 | 'nz' = 166, 168 | 'om' = 167, 169 | 'pa' = 168, 170 | 'pe' = 169, 171 | 'pf' = 170, 172 | 'pg' = 171, 173 | 'ph' = 172, 174 | 'pk' = 173, 175 | 'pl' = 174, 176 | 'pm' = 175, 177 | 'pn' = 176, 178 | 'pr' = 177, 179 | 'ps' = 178, 180 | 'pt' = 179, 181 | 'pw' = 180, 182 | 'py' = 181, 183 | 'qa' = 182, 184 | 're' = 183, 185 | 'ro' = 184, 186 | 'ru' = 185, 187 | 'rw' = 186, 188 | 'sa' = 187, 189 | 'sb' = 188, 190 | 'sc' = 189, 191 | 'sd' = 190, 192 | 'se' = 191, 193 | 'sg' = 192, 194 | 'sh' = 193, 195 | 'si' = 194, 196 | 'sj' = 195, 197 | 'sk' = 196, 198 | 'sl' = 197, 199 | 'sm' = 198, 200 | 'sn' = 199, 201 | 'so' = 200, 202 | 'sr' = 201, 203 | 'st' = 202, 204 | 'sv' = 203, 205 | 'sy' = 204, 206 | 'sz' = 205, 207 | 'tc' = 206, 208 | 'td' = 207, 209 | 'tf' = 208, 210 | 'tg' = 209, 211 | 'th' = 210, 212 | 'tj' = 211, 213 | 'tk' = 212, 214 | 'tm' = 213, 215 | 'tn' = 214, 216 | 'to' = 215, 217 | 'tl' = 216, 218 | 'tr' = 217, 219 | 'tt' = 218, 220 | 'tv' = 219, 221 | 'tw' = 220, 222 | 'tz' = 221, 223 | 'ua' = 222, 224 | 'ug' = 223, 225 | 'um' = 224, 226 | 'us' = 225, 227 | 'uy' = 226, 228 | 'uz' = 227, 229 | 'va' = 228, 230 | 'vc' = 229, 231 | 've' = 230, 232 | 'vg' = 231, 233 | 'vi' = 232, 234 | 'vn' = 233, 235 | 'vu' = 234, 236 | 'wf' = 235, 237 | 'ws' = 236, 238 | 'ye' = 237, 239 | 'yt' = 238, 240 | 'rs' = 239, 241 | 'za' = 240, 242 | 'zm' = 241, 243 | 'me' = 242, 244 | 'zw' = 243, 245 | 'xx' = 244, 246 | 'a2' = 245, 247 | 'o1' = 246, 248 | 'ax' = 247, 249 | 'gg' = 248, 250 | 'im' = 249, 251 | 'je' = 250, 252 | 'bl' = 251, 253 | 'mf' = 252 254 | } 255 | -------------------------------------------------------------------------------- /packages/common/enums/osu.ts: -------------------------------------------------------------------------------- 1 | export enum GradeEnum { 2 | XH, 3 | X, 4 | SH, 5 | S, 6 | A, 7 | B, 8 | C, 9 | D, 10 | None 11 | } 12 | 13 | export enum GameState { 14 | menu, 15 | edit, 16 | play, 17 | exit, 18 | selectEdit, 19 | selectPlay, 20 | selectDrawings, 21 | resultScreen, 22 | update, 23 | busy, 24 | unknown, 25 | lobby, 26 | matchSetup, 27 | selectMulti, 28 | rankingVs, 29 | onlineSelection, 30 | optionsOffsetWizard, 31 | rankingTagCoop, 32 | rankingTeam, 33 | beatmapImport, 34 | packageUpdater, 35 | benchmark, 36 | tourney, 37 | charts 38 | } 39 | 40 | export enum LobbyStatus { 41 | notJoined, 42 | idle, 43 | pendingJoin, 44 | pendingCreate, 45 | setup, 46 | play, 47 | results 48 | } 49 | 50 | export enum StableBeatmapStatuses { 51 | unknown, 52 | notSubmitted = 1, 53 | pending = 2, 54 | ranked = 4, 55 | approved = 5, 56 | qualified = 6, 57 | loved = 7 58 | } 59 | 60 | export enum Rulesets { 61 | osu = 0, 62 | taiko = 1, 63 | fruits = 2, 64 | mania = 3 65 | } 66 | 67 | export enum BanchoStatus { 68 | idle, 69 | afk, 70 | playing, 71 | editing, 72 | modding, 73 | multiplayer, 74 | watching, 75 | unknown, 76 | testing, 77 | submitting, 78 | paused, 79 | lobby, 80 | multiplaying, 81 | osuDirect 82 | } 83 | 84 | export enum UserLoginStatus { 85 | reconnecting = 0, 86 | guest = 256, 87 | recieving_data = 257, 88 | disconnected = 65537, 89 | connected = 65793 90 | } 91 | 92 | export enum ReleaseStream { 93 | cuttingEdge, 94 | stable, 95 | beta, 96 | fallback 97 | } 98 | 99 | export enum ScoreMeterType { 100 | none, 101 | colour, 102 | error 103 | } 104 | 105 | export enum LeaderboardType { 106 | local, 107 | global, 108 | selectedmods, 109 | friends, 110 | country 111 | } 112 | 113 | export enum GroupType { 114 | none, 115 | artist, 116 | bPM, 117 | creator, 118 | date, 119 | difficulty, 120 | length, 121 | rank, 122 | myMaps, 123 | search = 12, 124 | show_All = 12, 125 | title, 126 | lastPlayed, 127 | onlineFavourites, 128 | maniaKeys, 129 | mode, 130 | collection, 131 | rankedStatus 132 | } 133 | 134 | export enum SortType { 135 | artist, 136 | bpm, 137 | creator, 138 | date, 139 | difficulty, 140 | length, 141 | rank, 142 | title 143 | } 144 | 145 | export enum ChatStatus { 146 | hidden, 147 | visible, 148 | visibleWithFriendsList 149 | } 150 | 151 | export enum ProgressBarType { 152 | off, 153 | pie, 154 | topRight, 155 | bottomRight, 156 | bottom 157 | } 158 | 159 | export enum ScoringMode { 160 | standardised, 161 | classic 162 | } 163 | 164 | export enum LazerHitResults { 165 | none = 0, 166 | miss = 1, 167 | meh = 2, 168 | ok = 3, 169 | good = 4, 170 | great = 5, 171 | perfect = 6, 172 | smallTickMiss = 7, 173 | smallTickHit = 8, 174 | largeTickMiss = 9, 175 | largeTickHit = 10, 176 | smallBonus = 11, 177 | largeBonus = 12, 178 | ignoreMiss = 13, 179 | ignoreHit = 14, 180 | comboBreak = 15, 181 | sliderTailHit = 16, 182 | legacyComboIncrease = 99 183 | } 184 | 185 | export enum LazerSettings { 186 | Ruleset, 187 | Token, 188 | MenuCursorSize, 189 | GameplayCursorSize, 190 | AutoCursorSize, 191 | GameplayCursorDuringTouch, 192 | DimLevel, 193 | BlurLevel, 194 | EditorDim, 195 | LightenDuringBreaks, 196 | ShowStoryboard, 197 | KeyOverlay, 198 | GameplayLeaderboard, 199 | PositionalHitsoundsLevel, 200 | AlwaysPlayFirstComboBreak, 201 | FloatingComments, 202 | HUDVisibilityMode, 203 | ShowHealthDisplayWhenCantFail, 204 | FadePlayfieldWhenHealthLow, 205 | MouseDisableButtons, 206 | MouseDisableWheel, 207 | ConfineMouseMode, 208 | AudioOffset, 209 | VolumeInactive, 210 | MenuMusic, 211 | MenuVoice, 212 | MenuTips, 213 | CursorRotation, 214 | MenuParallax, 215 | Prefer24HourTime, 216 | BeatmapDetailTab, 217 | BeatmapDetailModsFilter, 218 | Username, 219 | ReleaseStream, 220 | SavePassword, 221 | SaveUsername, 222 | DisplayStarsMinimum, 223 | DisplayStarsMaximum, 224 | SongSelectGroupingMode, 225 | SongSelectSortingMode, 226 | RandomSelectAlgorithm, 227 | ModSelectHotkeyStyle, 228 | ShowFpsDisplay, 229 | ChatDisplayHeight, 230 | BeatmapListingCardSize, 231 | ToolbarClockDisplayMode, 232 | SongSelectBackgroundBlur, 233 | Version, 234 | ShowFirstRunSetup, 235 | ShowConvertedBeatmaps, 236 | Skin, 237 | ScreenshotFormat, 238 | ScreenshotCaptureMenuCursor, 239 | BeatmapSkins, 240 | BeatmapColours, 241 | BeatmapHitsounds, 242 | IncreaseFirstObjectVisibility, 243 | ScoreDisplayMode, 244 | ExternalLinkWarning, 245 | PreferNoVideo, 246 | Scaling, 247 | ScalingPositionX, 248 | ScalingPositionY, 249 | ScalingSizeX, 250 | ScalingSizeY, 251 | ScalingBackgroundDim, 252 | UIScale, 253 | IntroSequence, 254 | NotifyOnUsernameMentioned, 255 | NotifyOnPrivateMessage, 256 | NotifyOnFriendPresenceChange, 257 | UIHoldActivationDelay, 258 | HitLighting, 259 | StarFountains, 260 | MenuBackgroundSource, 261 | GameplayDisableWinKey, 262 | SeasonalBackgroundMode, 263 | EditorWaveformOpacity, 264 | EditorShowHitMarkers, 265 | EditorAutoSeekOnPlacement, 266 | DiscordRichPresence, 267 | ShowOnlineExplicitContent, 268 | LastProcessedMetadataId, 269 | SafeAreaConsiderations, 270 | ComboColourNormalisationAmount, 271 | ProfileCoverExpanded, 272 | EditorLimitedDistanceSnap, 273 | ReplaySettingsOverlay, 274 | ReplayPlaybackControlsExpanded, 275 | AutomaticallyDownloadMissingBeatmaps, 276 | EditorShowSpeedChanges, 277 | TouchDisableGameplayTaps, 278 | ModSelectTextSearchStartsActive, 279 | UserOnlineStatus, 280 | MultiplayerRoomFilter, 281 | HideCountryFlags, 282 | EditorTimelineShowTimingChanges, 283 | EditorTimelineShowTicks, 284 | AlwaysShowHoldForMenuButton, 285 | EditorContractSidebars, 286 | EditorScaleOrigin, 287 | EditorRotationOrigin, 288 | EditorTimelineShowBreaks, 289 | EditorAdjustExistingObjectsOnTimingChanges, 290 | AlwaysRequireHoldingForPause, 291 | MultiplayerShowInProgressFilter, 292 | BeatmapListingFeaturedArtistFilter, 293 | ShowMobileDisclaimer, 294 | EditorShowStoryboard, 295 | EditorSubmissionNotifyOnDiscussionReplies, 296 | EditorSubmissionLoadInBrowserAfterSubmission 297 | } 298 | -------------------------------------------------------------------------------- /packages/common/enums/tosu.ts: -------------------------------------------------------------------------------- 1 | export enum ClientType { 2 | stable, 3 | lazer 4 | } 5 | 6 | export enum Bitness { 7 | x86 = 32, 8 | x64 = 64 9 | } 10 | -------------------------------------------------------------------------------- /packages/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils/downloader'; 2 | export * from './utils/logger'; 3 | export * from './utils/platforms'; 4 | export * from './utils/arguments'; 5 | export * from './utils/sleep'; 6 | export * from './utils/config'; 7 | export * from './utils/unzip'; 8 | export * from './utils/directories'; 9 | export * from './utils/json'; 10 | export * from './utils/ingame'; 11 | export * from './utils/manipulation'; 12 | 13 | export * from './enums/osu'; 14 | export * from './enums/tosu'; 15 | export * from './enums/country'; 16 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tosu/common", 3 | "private": "true", 4 | "version": "0.0.1", 5 | "description": "", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "prepare": "npm run build", 10 | "build": "tsc" 11 | }, 12 | "dependencies": { 13 | "adm-zip": "^0.5.16", 14 | "dotenv": "^16.4.7" 15 | }, 16 | "devDependencies": { 17 | "@types/adm-zip": "^0.5.7" 18 | } 19 | } -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ES2020" 5 | ], 6 | "module": "commonjs", 7 | "moduleResolution": "Node", 8 | "allowJs": true, 9 | "esModuleInterop": true, 10 | "outDir": "dist", 11 | "rootDir": "./", 12 | "sourceMap": false, 13 | "declaration": false, 14 | "strict": true, 15 | "noImplicitAny": false, 16 | "target": "ES2020", 17 | "strictPropertyInitialization": false, 18 | "baseUrl": ".", 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "dist", 23 | ], 24 | "include": [ 25 | "**/*" 26 | ], 27 | } -------------------------------------------------------------------------------- /packages/common/utils/arguments.ts: -------------------------------------------------------------------------------- 1 | export const argumentsParser = (argumentsString: string[] | string) => { 2 | const args: { [key: string]: any } = {}; 3 | 4 | if (typeof argumentsString === 'string') { 5 | const regex = /(?:--|-)(?[^=\s]+)(?:[= ](?(?!--|-)\S*))?/g; 6 | const matches = [...argumentsString.matchAll(regex)]; 7 | matches.forEach((match) => { 8 | const name = match?.groups?.name || ''; 9 | const value: any = match?.groups?.value || ''; 10 | 11 | if (!isNaN(parseFloat(value))) args[name] = parseFloat(value); 12 | else if (value === 'true') args[name] = true; 13 | else if (value === 'false') args[name] = false; 14 | else args[name] = value; 15 | }); 16 | 17 | return args; 18 | } 19 | 20 | const regex = /(?:--|-)(?[^=\s]+)(?:[= ](?(?!--|-)\S*))?/; 21 | for (let i = 0; i < argumentsString.length; i++) { 22 | const arg = argumentsString[i]; 23 | 24 | if (!regex.test(arg)) continue; 25 | const groups = regex.exec(arg)?.groups; 26 | 27 | const name = groups?.name || ''; 28 | const value: any = groups?.value || ''; 29 | 30 | if (!isNaN(parseFloat(value))) args[name] = parseFloat(value); 31 | else if (value === 'true') args[name] = true; 32 | else if (value === 'false') args[name] = false; 33 | else args[name] = value; 34 | } 35 | 36 | return args; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/common/utils/directories.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { config } from './config'; 5 | 6 | export function recursiveFilesSearch({ 7 | _ignoreFileName, 8 | dir, 9 | filename, 10 | fileList 11 | }: { 12 | _ignoreFileName?: string; 13 | dir: string; 14 | filename: string; 15 | fileList: { filePath: string; created: number }[]; 16 | }) { 17 | const files = fs.readdirSync(dir); 18 | files.forEach((file) => { 19 | if (file.startsWith('.')) return; 20 | 21 | const filePath = path.join(dir, file); 22 | const stats = fs.statSync(filePath); 23 | if (stats.isDirectory()) { 24 | if (_ignoreFileName) { 25 | const ignoreFilePath = path.join(filePath, _ignoreFileName); 26 | if (fs.existsSync(ignoreFilePath)) { 27 | return; 28 | } 29 | } 30 | 31 | recursiveFilesSearch({ dir: filePath, filename, fileList }); 32 | } else if (filePath.includes(filename)) { 33 | const stats = fs.statSync(filePath); 34 | 35 | fileList.push({ filePath, created: stats.mtimeMs }); 36 | } 37 | }); 38 | 39 | fileList.sort((a, b) => a.created - b.created); 40 | 41 | return fileList.map((r) => r.filePath); 42 | } 43 | 44 | export function getStaticPath() { 45 | let staticPath = 46 | config.staticFolderPath || path.join(getProgramPath(), 'static'); 47 | 48 | // replace ./static with normal path to the static with program path 49 | if ( 50 | staticPath.toLowerCase() === './static' || 51 | staticPath.toLowerCase() === '.\\static' 52 | ) 53 | staticPath = path.join(getProgramPath(), 'static'); 54 | 55 | return path.resolve(staticPath); 56 | } 57 | 58 | export function getCachePath() { 59 | return path.join(getProgramPath(), '.cache'); 60 | } 61 | 62 | export function getProgramPath() { 63 | if ('pkg' in process) return path.dirname(process.execPath); 64 | return process.cwd(); 65 | } 66 | 67 | export function getSettingsPath(folderName: string) { 68 | if (!folderName) return ''; 69 | 70 | const settingsFolderPath = path.join(getProgramPath(), 'settings'); 71 | if (!fs.existsSync(settingsFolderPath)) 72 | fs.mkdirSync(settingsFolderPath, { recursive: true }); 73 | 74 | const folderPath = path.join( 75 | settingsFolderPath, 76 | `${folderName}.values.json` 77 | ); 78 | return folderPath; 79 | } 80 | -------------------------------------------------------------------------------- /packages/common/utils/downloader.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import https from 'https'; 3 | 4 | import { colorText } from './logger'; 5 | 6 | const progressBarWidth = 40; 7 | 8 | export const updateProgressBar = ( 9 | title: string, 10 | progress: number, 11 | message: string = '' 12 | ): void => { 13 | const coloredText = colorText('info'); 14 | if (message) message = ` - ${message}`; 15 | 16 | const filledWidth = Math.round(progressBarWidth * progress); 17 | const emptyWidth = progressBarWidth - filledWidth; 18 | const progressBar = '█'.repeat(filledWidth) + '░'.repeat(emptyWidth); 19 | 20 | process.stdout.write( 21 | `${coloredText} ${title}: [${progressBar}] ${(progress * 100).toFixed(2)}%${message}\r` 22 | ); 23 | 24 | if (progress === 1) { 25 | if ( 26 | typeof process.stdout.clearLine === 'function' && 27 | typeof process.stdout.cursorTo === 'function' 28 | ) { 29 | process.stdout.clearLine(0); 30 | process.stdout.cursorTo(0); 31 | } else { 32 | process.stdout.write('\n'); 33 | } 34 | } 35 | }; 36 | 37 | /** 38 | * A cyperdark's downloadFile implmentation based on pure node api 39 | * @param url {string} 40 | * @param destination {string} 41 | * @returns {Promise} 42 | */ 43 | export const downloadFile = ( 44 | url: string, 45 | destination: string 46 | ): Promise => 47 | new Promise((resolve, reject) => { 48 | const options = { 49 | headers: { 50 | Accept: 'application/octet-stream', 51 | 'User-Agent': '@tosuapp/tosu' 52 | }, 53 | agent: new https.Agent({ 54 | secureOptions: require('node:crypto').constants.SSL_OP_ALL 55 | }) 56 | }; 57 | 58 | // find url 59 | https 60 | .get(url, options, (response) => { 61 | if (response.headers.location) { 62 | downloadFile(response.headers.location, destination) 63 | .then(resolve) 64 | .catch(reject); 65 | return; 66 | } 67 | 68 | const file = fs.createWriteStream(destination); 69 | 70 | file.on('error', (err) => { 71 | fs.unlinkSync(destination); 72 | reject(err); 73 | }); 74 | 75 | file.on('finish', () => { 76 | file.close(); 77 | resolve(destination); 78 | }); 79 | 80 | const totalSize = parseInt( 81 | response.headers['content-length']!, 82 | 10 83 | ); 84 | let downloadedSize = 0; 85 | 86 | response.on('data', (data) => { 87 | downloadedSize += data.length; 88 | const progress = downloadedSize / totalSize; 89 | updateProgressBar('Downloading', progress); 90 | }); 91 | 92 | response.pipe(file); 93 | }) 94 | .on('error', reject); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/common/utils/ingame.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { getSettingsPath } from './directories'; 4 | 5 | export const checkGameOverlayConfig = () => { 6 | const newestConfigPath = getSettingsPath('__ingame__'); 7 | if (fs.existsSync(newestConfigPath)) return; 8 | 9 | fs.writeFileSync(newestConfigPath, '{}', 'utf8'); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/common/utils/json.ts: -------------------------------------------------------------------------------- 1 | export const JsonSafeParse = (str: string, errorReturn: any) => { 2 | try { 3 | return JSON.parse(str); 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | } catch (e) { 6 | return errorReturn; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /packages/common/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import fsp from 'fs/promises'; 3 | import path from 'path'; 4 | 5 | import { config } from './config'; 6 | import { getProgramPath } from './directories'; 7 | 8 | const colors = { 9 | info: '\x1b[1m\x1b[40m\x1b[42m', 10 | error: '\x1b[1m\x1b[37m\x1b[41m', 11 | debug: '\x1b[1m\x1b[37m\x1b[44m', 12 | debugError: '\x1b[1m\x1b[37m\x1b[45m', 13 | warn: '\x1b[1m\x1b[40m\x1b[43m', 14 | reset: '\x1b[0m', 15 | grey: '\x1b[90m' 16 | }; 17 | 18 | export function colorText(status: string) { 19 | const colorCode = colors[status] || colors.reset; 20 | const timestamp = new Date().toISOString().split('T')[1].replace('Z', ''); 21 | 22 | const time = `${colors.grey}${timestamp}${colors.reset}`; 23 | const version = `${colors.grey}v${config.currentVersion}${colors.reset}`; 24 | return `${time} ${version} ${colorCode} ${status.toUpperCase()} ${colors.reset}`; 25 | } 26 | 27 | export const wLogger = { 28 | info: (...args: any) => { 29 | const coloredText = colorText('info'); 30 | console.log(coloredText, ...args); 31 | 32 | writeLog('info', args); 33 | }, 34 | debug: (...args: any) => { 35 | writeLog('debug', args); 36 | 37 | if (config.debugLogging !== true) return; 38 | 39 | const coloredText = colorText('debug'); 40 | console.log(coloredText, ...args); 41 | }, 42 | debugError: (...args: any) => { 43 | if (config.debugLogging !== true) return; 44 | 45 | const coloredText = colorText('debugError'); 46 | console.log(coloredText, ...args); 47 | 48 | writeLog('debugError', args); 49 | }, 50 | error: (...args: any) => { 51 | const coloredText = colorText('error'); 52 | console.log(coloredText, ...args); 53 | 54 | writeLog('error', args); 55 | }, 56 | warn: (...args: any) => { 57 | const coloredText = colorText('warn'); 58 | console.log(coloredText, ...args); 59 | 60 | writeLog('warn', args); 61 | } 62 | }; 63 | 64 | function writeLog(type: string, ...args: any[]) { 65 | if (config.logsPath === '') { 66 | const logsPath = path.join(getProgramPath(), 'logs'); 67 | if (!fs.existsSync(logsPath)) 68 | fs.mkdirSync(logsPath, { recursive: true }); 69 | 70 | config.logsPath = path.join(logsPath, `${Date.now()}.txt`); 71 | } 72 | 73 | fsp.appendFile( 74 | config.logsPath, 75 | `${new Date().toISOString()} ${type} ${args.join(' ')}\n`, 76 | 'utf8' 77 | ).catch((reason) => console.log(`writeLog`, reason)); 78 | } 79 | -------------------------------------------------------------------------------- /packages/common/utils/manipulation.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export function formatMilliseconds(ms: number) { 4 | const hours = Math.floor(ms / 3600000); 5 | const minutes = Math.floor((ms % 3600000) / 60000); 6 | const seconds = Math.floor((ms % 60000) / 1000); 7 | const milliseconds = ms % 1000; 8 | 9 | const hoursStr = String(hours).padStart(2, '0'); 10 | const minutesStr = String(minutes).padStart(2, '0'); 11 | const secondsStr = String(seconds).padStart(2, '0'); 12 | 13 | return `${hoursStr}:${minutesStr}:${secondsStr}.${milliseconds}`; 14 | } 15 | 16 | export function textMD5(text: string) { 17 | return crypto.createHash('md5').update(text).digest('hex'); 18 | } 19 | -------------------------------------------------------------------------------- /packages/common/utils/platforms.ts: -------------------------------------------------------------------------------- 1 | export type Platform = 'windows' | 'linux' | 'macos' | 'unknown'; 2 | 3 | interface IPlatform { 4 | type: Platform; 5 | fileType: string; 6 | command: string; 7 | } 8 | 9 | export function platformResolver(platform: string): IPlatform { 10 | let type: Platform = 'unknown'; 11 | let fileType = ''; 12 | let command = ''; 13 | 14 | switch (platform) { 15 | case 'win32': 16 | type = 'windows'; 17 | fileType = '.exe'; 18 | command = 'start ""'; 19 | break; 20 | 21 | case 'linux': 22 | type = 'linux'; 23 | command = 'xdg-open'; 24 | break; 25 | 26 | case 'darwin': 27 | type = 'macos'; 28 | command = 'open -R'; 29 | break; 30 | } 31 | 32 | return { type, fileType, command }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/common/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (ms: number) => { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/common/utils/unzip.ts: -------------------------------------------------------------------------------- 1 | import AdmZip from 'adm-zip'; 2 | import fs from 'fs'; 3 | 4 | import { wLogger } from './logger'; 5 | 6 | export const unzip = (zipPath: string, extractPath: string): Promise => 7 | new Promise((resolve, reject) => { 8 | const zip = new AdmZip(zipPath); 9 | 10 | try { 11 | if (!fs.existsSync(extractPath)) fs.mkdirSync(extractPath); 12 | 13 | zip.extractAllTo(extractPath, true); 14 | resolve(extractPath); 15 | } catch (error) { 16 | wLogger.error('[unzip]', (error as any).message); 17 | wLogger.debug('[unzip]', error); 18 | reject(error); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /packages/ingame-overlay-updater/README.md: -------------------------------------------------------------------------------- 1 | @tosu/ingame-overlay-updater 2 | --- 3 | Package to control updates of storycraft overlay -------------------------------------------------------------------------------- /packages/ingame-overlay-updater/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tosu/ingame-overlay-updater", 3 | "private": "true", 4 | "version": "0.0.1", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "prepare": "npm run build", 9 | "build": "tsc" 10 | }, 11 | "dependencies": { 12 | "tsprocess": "workspace:*", 13 | "@tosu/common": "workspace:*" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/ingame-overlay-updater/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | checkGameOverlayConfig, 3 | downloadFile, 4 | getProgramPath, 5 | platformResolver, 6 | unzip, 7 | wLogger 8 | } from '@tosu/common'; 9 | import { ChildProcess, spawn } from 'node:child_process'; 10 | import { existsSync, readFileSync } from 'node:fs'; 11 | import { mkdir, rm } from 'node:fs/promises'; 12 | import path from 'node:path'; 13 | 14 | // NOTE: _version.js packs with pkg support in tosu build 15 | const currentVersion = require(process.cwd() + '/_version.js'); 16 | 17 | const platform = platformResolver(process.platform); 18 | 19 | export async function runOverlay(): Promise { 20 | if (process.platform !== 'win32') { 21 | throw new Error( 22 | 'This feature is currently only available on the Windows platform' 23 | ); 24 | } 25 | 26 | checkGameOverlayConfig(); 27 | 28 | const gameOverlayPath = path.join(getProgramPath(), 'game-overlay'); 29 | if ( 30 | existsSync(path.join(gameOverlayPath)) && 31 | !existsSync(path.join(gameOverlayPath, 'version')) 32 | ) { 33 | // old overlay detected, removing it 34 | wLogger.warn( 35 | '[ingame-overlay] Old version of the ingame overlay detected. Removing...' 36 | ); 37 | await rm(gameOverlayPath, { recursive: true, force: true }); 38 | } 39 | 40 | if (existsSync(path.join(gameOverlayPath, 'version'))) { 41 | const overlayVersion = readFileSync( 42 | path.join(gameOverlayPath, 'version'), 43 | 'utf8' 44 | ); 45 | if (overlayVersion.trimEnd() !== currentVersion) { 46 | await rm(gameOverlayPath, { recursive: true, force: true }); 47 | 48 | wLogger.warn( 49 | '[ingame-overlay] A newer version of the ingame overlay is available. Updating...' 50 | ); 51 | } 52 | } 53 | 54 | if (!existsSync(gameOverlayPath)) { 55 | const archivePath = path.join(gameOverlayPath, 'tosu-gameoverlay.zip'); 56 | 57 | await mkdir(gameOverlayPath, { recursive: true }); 58 | 59 | const request = await fetch( 60 | `https://api.github.com/repos/tosuapp/tosu/releases/tags/v${currentVersion}` 61 | ); 62 | const json = (await request.json()) as any; 63 | const { 64 | assets 65 | }: { 66 | assets: { name: string; browser_download_url: string }[]; 67 | } = json; 68 | 69 | const findAsset = assets.find( 70 | (r) => r.name.includes('tosu-overlay') && r.name.endsWith('.zip') 71 | ); 72 | if (!findAsset) { 73 | throw new Error( 74 | `Could not find downloadable files for your operating system. (${platform.type})` 75 | ); 76 | } 77 | 78 | await downloadFile(findAsset.browser_download_url, archivePath); 79 | 80 | await unzip(archivePath, gameOverlayPath); 81 | await rm(archivePath); 82 | 83 | wLogger.info('[ingame-overlay] Ingame overlay downloaded'); 84 | } 85 | 86 | wLogger.warn(`[ingame-overlay] Starting...`); 87 | 88 | const child = spawn( 89 | path.join(gameOverlayPath, 'tosu-ingame-overlay.exe'), 90 | [], 91 | { 92 | detached: false, 93 | stdio: ['ignore', 'overlapped', 'overlapped'], 94 | windowsHide: true, 95 | shell: false, 96 | env: { 97 | // Force nvidia optimus to prefer dedicated gpu 98 | SHIM_MCCOMPAT: '0x800000001', 99 | ...process.env 100 | } 101 | } 102 | ); 103 | 104 | child.stdout.setEncoding('utf-8').on('data', (data: string) => { 105 | // overlay logs are a bit verbose, so redirect them to debug log 106 | wLogger.debug('[ingame-overlay]', data.trim()); 107 | }); 108 | 109 | child.stderr.setEncoding('utf-8').on('data', (data: string) => { 110 | // redirect overlay error backtraces to debug error log 111 | wLogger.debugError('[ingame-overlay]', data.trim()); 112 | }); 113 | 114 | return child; 115 | } 116 | -------------------------------------------------------------------------------- /packages/ingame-overlay-updater/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2020"], 4 | "module": "commonjs", 5 | "moduleResolution": "Node", 6 | "allowJs": true, 7 | "esModuleInterop": true, 8 | "outDir": "dist", 9 | "rootDir": "src", 10 | "sourceMap": false, 11 | "declaration": false, 12 | "strict": true, 13 | "noImplicitAny": false, 14 | "target": "ES2020", 15 | "strictPropertyInitialization": false, 16 | "baseUrl": ".", 17 | }, 18 | "exclude": ["node_modules"], 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/ingame-overlay/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | pack/ 4 | -------------------------------------------------------------------------------- /packages/ingame-overlay/README.md: -------------------------------------------------------------------------------- 1 | # `tosu-ingame-overlay` 2 | High performance [tosu](https://github.com/tosuapp/tosu) fullscreen ingame overlay 3 | 4 | Supports OpenGL(stable, lazer), dx9(stable compat), dx11(lazer) 5 | 6 | Disable stock ingame overlay before using it. 7 | 8 | Ingame config is not working yet, go to `http://localhost:24050/api/ingame` for configuration. 9 | Exit or reload overlay using tray icon. 10 | 11 | Overlay library used under this project: [asdf-overlay](https://github.com/storycraft/asdf-overlay) 12 | 13 | ## Performance 14 | This projects uses shared gpu surface for rendering overlay, so no cpu work is involved. 15 | It have same or less performance overhead than adding a OBS browser source. 16 | Which is very small and also have no noticeable input latency. 17 | -------------------------------------------------------------------------------- /packages/ingame-overlay/asset/tosu.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/ingame-overlay/asset/tosu.ico -------------------------------------------------------------------------------- /packages/ingame-overlay/electron.vite.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { bytecodePlugin } from 'electron-vite'; 4 | import { defineConfig } from 'electron-vite'; 5 | import path from 'node:path'; 6 | 7 | export default defineConfig({ 8 | main: { 9 | build: { 10 | lib: { 11 | entry: './main/index.ts', 12 | formats: ['cjs'], 13 | }, 14 | outDir: 'dist/main', 15 | minify: true, 16 | rollupOptions: { 17 | external: [ 18 | 'asdf-overlay-node', 19 | '@jellybrick/wql-process-monitor', 20 | 'tsprocess' 21 | ], 22 | }, 23 | }, 24 | plugins: [bytecodePlugin()], 25 | resolve: { 26 | alias: { 27 | '@asset': path.resolve('./asset'), 28 | }, 29 | }, 30 | assetsInclude: ['./asset/*'], 31 | }, 32 | preload: { 33 | build: { 34 | lib: { 35 | entry: './preload/index.ts', 36 | formats: ['cjs'], 37 | }, 38 | outDir: 'dist/preload', 39 | minify: true, 40 | }, 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /packages/ingame-overlay/main/index.ts: -------------------------------------------------------------------------------- 1 | import tosuIcon from '@asset/tosu.ico?no-inline'; 2 | import { Menu, Tray, app } from 'electron'; 3 | import path from 'path'; 4 | 5 | import packageJSON from '../package.json'; 6 | import { OverlayManager } from './overlay/manager'; 7 | 8 | (async () => { 9 | try { 10 | await main(); 11 | } finally { 12 | app.quit(); 13 | } 14 | })(); 15 | 16 | async function main() { 17 | if (!app.requestSingleInstanceLock()) { 18 | console.error( 19 | 'Ingame overlay is already running. Please check tray icon' 20 | ); 21 | return; 22 | } 23 | 24 | // prefer discrete gpu on laptop 25 | app.commandLine.appendSwitch('force_high_performance_gpu'); 26 | // disable view scaling on hidpi 27 | app.commandLine.appendSwitch('high-dpi-support', '1'); 28 | app.commandLine.appendSwitch('force-device-scale-factor', '1'); 29 | 30 | // prevent main process from exiting when all windows are closed 31 | app.on('window-all-closed', () => {}); 32 | 33 | await app.whenReady(); 34 | 35 | const manager = new OverlayManager(); 36 | const tray = new Tray(path.join(__dirname, tosuIcon)); 37 | const contextMenu = Menu.buildFromTemplate([ 38 | { 39 | label: `${packageJSON.name} v${packageJSON.version} by ${packageJSON.author}`, 40 | enabled: false 41 | }, 42 | { 43 | type: 'separator' 44 | }, 45 | { 46 | label: 'Reload overlays', 47 | click: () => { 48 | manager.reloadAll(); 49 | } 50 | }, 51 | { 52 | type: 'separator' 53 | }, 54 | { 55 | label: 'Exit', 56 | role: 'quit' 57 | } 58 | ]); 59 | tray.setToolTip(packageJSON.name); 60 | tray.setContextMenu(contextMenu); 61 | 62 | await manager.run(); 63 | } 64 | -------------------------------------------------------------------------------- /packages/ingame-overlay/main/keybind.ts: -------------------------------------------------------------------------------- 1 | import { Key } from 'asdf-overlay-node'; 2 | import { InputState } from 'asdf-overlay-node/input'; 3 | 4 | export class Keybind { 5 | private state = 0xffffffff; 6 | 7 | /** 8 | * @param keys array of keybind key up to 32 keys 9 | */ 10 | constructor(private readonly keys: Key[]) { 11 | if (keys.length > 32) { 12 | throw new Error('Keybind keys cannot be more than 32 keys'); 13 | } 14 | } 15 | 16 | update(key: Key, state: InputState): boolean { 17 | const index = this.keys.findIndex((keybindKey) => { 18 | return ( 19 | key.code === keybindKey.code && 20 | key.extended === keybindKey.extended 21 | ); 22 | }); 23 | if (index === -1) { 24 | return false; 25 | } 26 | 27 | if (state === 'Pressed') { 28 | // unset index bit 29 | this.state &= ~(1 << index); 30 | 31 | // check if all settable bits are 0 (all keybind keys are pressed) 32 | return !(this.state << (32 - this.keys.length)); 33 | } else { 34 | // set index bit 35 | this.state |= 1 << index; 36 | return false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/ingame-overlay/main/overlay/manager.ts: -------------------------------------------------------------------------------- 1 | import { promises as wql } from '@jellybrick/wql-process-monitor'; 2 | import EventEmitter from 'node:events'; 3 | import { Process } from 'tsprocess'; 4 | 5 | import { OverlayProcess } from './process'; 6 | 7 | type ManagerEventEmitter = EventEmitter<{ 8 | added: [pid: number, overlay: OverlayProcess]; 9 | removed: [pid: number]; 10 | }>; 11 | 12 | export class OverlayManager { 13 | readonly event: ManagerEventEmitter = new EventEmitter(); 14 | 15 | private readonly map: Map = new Map(); 16 | private readonly abortController: AbortController = new AbortController(); 17 | 18 | private async addOverlay(pid: number) { 19 | try { 20 | console.debug('initializing ingame overlay pid:', pid); 21 | const overlay = await OverlayProcess.initialize(pid); 22 | try { 23 | await overlay.window.loadURL( 24 | 'http://localhost:24050/api/ingame' 25 | ); 26 | } catch (e) { 27 | console.warn('cannot connect to ingame overlay. err: ', e); 28 | } 29 | 30 | this.map.set(pid, overlay); 31 | this.event.emit('added', pid, overlay); 32 | overlay.event.once('destroyed', () => { 33 | this.map.delete(pid); 34 | this.event.emit('removed', pid); 35 | }); 36 | } catch (e) { 37 | console.warn('overlay injection failed err:', e); 38 | } 39 | } 40 | 41 | async run() { 42 | const signal = this.abortController.signal; 43 | 44 | const emitter = await wql.subscribe({ 45 | creation: true 46 | }); 47 | emitter.on('creation', ([name, pid]) => { 48 | if (name === 'osu!.exe' || name === 'osulazer.exe') { 49 | const id = Number.parseInt(pid); 50 | if (isNaN(id)) { 51 | return; 52 | } 53 | 54 | this.addOverlay(id); 55 | } 56 | }); 57 | 58 | const osuProcesses = Process.findProcesses([ 59 | 'osu!.exe', 60 | 'osulazer.exe' 61 | ]); 62 | for (const osuGamePid of osuProcesses) { 63 | await this.addOverlay(osuGamePid); 64 | } 65 | 66 | await new Promise((resolve) => { 67 | signal.addEventListener('abort', resolve, { once: true }); 68 | }); 69 | } 70 | 71 | reloadAll() { 72 | for (const overlay of this.map.values()) { 73 | overlay.window.reload(); 74 | } 75 | } 76 | 77 | destroy() { 78 | this.abortController.abort(); 79 | } 80 | } 81 | 82 | // fix wql 83 | wql.createEventSink(); 84 | -------------------------------------------------------------------------------- /packages/ingame-overlay/main/overlay/process/index.ts: -------------------------------------------------------------------------------- 1 | import { Overlay, defaultDllDir, key, length } from 'asdf-overlay-node'; 2 | import { BrowserWindow, TextureInfo } from 'electron'; 3 | import EventEmitter from 'node:events'; 4 | import path from 'node:path'; 5 | 6 | import { Keybind } from '../../keybind'; 7 | import { toCursor, toKeyboardEvent, toMouseEvent } from './input'; 8 | 9 | export type OverlayEventEmitter = EventEmitter<{ 10 | destroyed: []; 11 | }>; 12 | 13 | export class OverlayProcess { 14 | readonly event: OverlayEventEmitter = new EventEmitter(); 15 | 16 | private constructor( 17 | readonly pid: number, 18 | private readonly hwnd: number, 19 | readonly overlay: Overlay, 20 | readonly window: BrowserWindow 21 | ) { 22 | overlay.event.once('disconnected', () => { 23 | this.window.destroy(); 24 | this.event.emit('destroyed'); 25 | }); 26 | 27 | overlay.event.on('resized', (hwnd, width, height) => { 28 | if (hwnd !== this.hwnd) { 29 | return; 30 | } 31 | 32 | console.debug( 33 | 'window resized hwnd:', 34 | hwnd, 35 | 'width:', 36 | width, 37 | 'height:', 38 | height 39 | ); 40 | this.window.setSize(width, height); 41 | }); 42 | 43 | overlay.event.on('cursor_input', (_, input) => { 44 | const event = toMouseEvent(input); 45 | if (event) { 46 | window.webContents.sendInputEvent(event); 47 | } 48 | }); 49 | 50 | window.webContents.on('cursor-changed', (_, type) => { 51 | overlay.setBlockingCursor(hwnd, toCursor(type)); 52 | }); 53 | 54 | // TODO:: configurable input key bind 55 | let configurationEnabled = false; 56 | const keybind = new Keybind([ 57 | key(0x11), // Left Control 58 | key(0x10), // Left Shift 59 | key(0x20) // Space 60 | ]); 61 | 62 | overlay.event.on('input_blocking_ended', () => { 63 | this.closeConfiguration(); 64 | configurationEnabled = false; 65 | }); 66 | 67 | overlay.event.on('keyboard_input', (_, input) => { 68 | if ( 69 | input.kind === 'Key' && 70 | keybind.update(input.key, input.state) 71 | ) { 72 | configurationEnabled = !configurationEnabled; 73 | 74 | overlay.blockInput(hwnd, configurationEnabled); 75 | if (configurationEnabled) { 76 | this.openConfiguration(); 77 | } 78 | } 79 | 80 | if (configurationEnabled) { 81 | const event = toKeyboardEvent(input); 82 | if (event) { 83 | window.webContents.sendInputEvent(event); 84 | } 85 | } 86 | }); 87 | 88 | window.webContents.on('paint', async (e) => { 89 | if (!e.texture) { 90 | return; 91 | } 92 | 93 | try { 94 | await this.updateSurface(e.texture.textureInfo); 95 | } catch (e) { 96 | console.error( 97 | `error while updating overlay pid: ${pid.toString()}, err:`, 98 | e 99 | ); 100 | this.destroy(); 101 | } finally { 102 | e.texture.release(); 103 | } 104 | }); 105 | } 106 | 107 | private openConfiguration() { 108 | this.window.webContents.send('inputCaptureStart'); 109 | this.window.focusOnWebView(); 110 | } 111 | 112 | private closeConfiguration() { 113 | this.window.webContents.send('inputCaptureEnd'); 114 | this.window.blurWebView(); 115 | } 116 | 117 | private async updateSurface(info: TextureInfo) { 118 | const rect = info.metadata.captureUpdateRect ?? info.contentRect; 119 | await this.overlay.updateShtex( 120 | info.codedSize.width, 121 | info.codedSize.height, 122 | info.sharedTextureHandle, 123 | { 124 | dstX: rect.x, 125 | dstY: rect.y, 126 | src: rect 127 | } 128 | ); 129 | } 130 | 131 | destroy() { 132 | this.overlay.destroy(); 133 | } 134 | 135 | static async initialize(pid: number): Promise { 136 | const overlay = await Overlay.attach( 137 | 'tosu-ingame-overlay', 138 | defaultDllDir().replaceAll('app.asar', 'app.asar.unpacked'), 139 | pid, 140 | 5000 141 | ); 142 | const [hwnd, width, height] = await new Promise< 143 | [number, number, number] 144 | >((resolve) => 145 | overlay.event.once('added', (hwnd, width, height) => 146 | resolve([hwnd, width, height]) 147 | ) 148 | ); 149 | console.debug('found hwnd:', hwnd, 'for pid:', pid); 150 | 151 | await overlay.setPosition(length(0), length(0)); 152 | await overlay.setAnchor(length(0), length(0)); 153 | await overlay.setMargin(length(0), length(0), length(0), length(0)); 154 | // Listen for keyboard events 155 | await overlay.listenInput(hwnd, false, true); 156 | 157 | const window = new BrowserWindow({ 158 | webPreferences: { 159 | offscreen: { 160 | useSharedTexture: true 161 | }, 162 | transparent: true, 163 | preload: path.join(__dirname, '../preload/index.js') 164 | }, 165 | show: false 166 | }); 167 | window.setSize(width, height, false); 168 | 169 | return new OverlayProcess(pid, hwnd, overlay, window); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /packages/ingame-overlay/main/types/files.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@asset/*' { 2 | const path: string; 3 | export default path; 4 | } 5 | -------------------------------------------------------------------------------- /packages/ingame-overlay/main/types/wql-process-monitor.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@jellybrick/wql-process-monitor' { 2 | import Emittery from 'emittery'; 3 | 4 | type Options = { 5 | /** 6 | * Subscribe to the creation event 7 | * @default true 8 | */ 9 | creation?: boolean; 10 | /** 11 | * Subscribe to the deletion event 12 | * @default true 13 | */ 14 | deletion?: boolean; 15 | /** 16 | * Exclude events originating from System32 and SysWOW64 Windows folder as well as integrated OneDrive FileCoAuth.exe. 17 | * e.g. cmd.exe, powershell.exe, svchost.exe, RuntimeBroker.exe, and others Windows processes. 18 | * 19 | * NB: Using this will prevent you to catch any elevated process event. 20 | * Unless you are also elevated. This is a permission issue (See #2). 21 | * You can implement your own filter on top of the event emitter result instead. 22 | * @default false 23 | */ 24 | filterWindowsNoise?: boolean; 25 | /** 26 | * Exclude events originating from Program Files, Program Files (x86), AppData local and AppData Roaming. 27 | * 28 | * NB: Using this will prevent you to catch any elevated process event. 29 | * Unless you are also elevated. This is a permission issue (See #2). 30 | * You can implement your own filter on top of the event emitter result instead. 31 | * @default false 32 | */ 33 | filterUsualProgramLocations?: boolean; 34 | /** 35 | * Custom list of process to exclude. 36 | * eg: ["firefox.exe","chrome.exe",...] 37 | * 38 | * NB: There are limits to the number of AND and OR keywords that can be used in WQL queries. Large numbers of WQL keywords used in a complex query can cause WMI to return the WBEM_E_QUOTA_VIOLATION error code as an HRESULT value. The limit of WQL keywords depends on how complex the query is 39 | * cf: https://docs.microsoft.com/en-us/windows/win32/wmisdk/querying-with-wql 40 | * If you have a huge list consider implementing your own filter on top of the event emitter result instead. 41 | * @default [] 42 | */ 43 | filter?: string[]; 44 | /** 45 | * Use `filter` option as a whitelist. 46 | * `filterWindowsNoise` / `filterUsualProgramLocations` can still be used. 47 | * Previously mentioned limitation(s) still apply. 48 | * 49 | * @default false 50 | */ 51 | whitelist?: boolean; 52 | }; 53 | 54 | interface Promises { 55 | /** 56 | * Subscribe to process creation and deletion events. 57 | * @param option 58 | * @returns {Promise>} 59 | */ 60 | subscribe(option?: Options): Promise< 61 | Emittery<{ 62 | /** 63 | * Process creation event 64 | * @param {string} processName process name 65 | * @param {string} processId process identifier (Process id should be number...) 66 | * @param {string} filepath file location path (if available*) 67 | */ 68 | creation: [string, string, string?]; 69 | /** 70 | * Process deletion event 71 | * @param {string} processName process name 72 | * @param {string} processId process identifier 73 | */ 74 | deletion: [string, string]; 75 | }> 76 | >; 77 | 78 | /** 79 | * @deprecated Since version >= 2.0 this is automatically done for you when you call subscribe(). Method was merely kept for backward compatibility. 80 | * @returns {Promise} 81 | */ 82 | createEventSink(): Promise; 83 | 84 | /** 85 | * Properly close the event sink. 86 | * There is no 'un-subscribe' thing to do prior to closing the sink. Just close it. 87 | * It is recommended to properly close the event sink when you are done if you intend to re-open it later on. 88 | * Most of the time you wouldn't have to bother with this, but it's here in case you need it. 89 | * @returns {Promise} 90 | */ 91 | closeEventSink(): Promise; 92 | } 93 | 94 | interface WQL { 95 | /** 96 | * Subscribe to process creation and deletion events. 97 | * @param option 98 | * @returns {Emittery<{creation: [string, string, string?], deletion: [string, string]}>} 99 | */ 100 | subscribe(option?: Options): Emittery<{ 101 | /** 102 | * Process creation event 103 | * @param {string} processName process name 104 | * @param {string} processId process identifier (Process id should be number...) 105 | * @param {string} filepath file location path (if available*) 106 | */ 107 | creation: [string, string, string?]; 108 | /** 109 | * Process deletion event 110 | * @param {string} processName process name 111 | * @param {string} processId process identifier 112 | */ 113 | deletion: [string, string]; 114 | }>; 115 | 116 | /** 117 | * @deprecated Since version >= 2.0 this is automatically done for you when you call subscribe(). Method was merely kept for backward compatibility. 118 | * @returns {void} 119 | */ 120 | createEventSink(): void; 121 | 122 | /** 123 | * Properly close the event sink. 124 | * There is no 'un-subscribe' thing to do prior to closing the sink. Just close it. 125 | * It is recommended to properly close the event sink when you are done if you intend to re-open it later on. 126 | * Most of the time you wouldn't have to bother with this, but it's here in case you need it. 127 | * @returns {void} 128 | */ 129 | closeEventSink(): void; 130 | } 131 | 132 | /** 133 | * WQL Process Monitor 134 | * 135 | * Usage of promise instead of sync is recommended so that you will not block Node's event loop. 136 | */ 137 | const wql: WQL; 138 | /** 139 | * Promisified version of wql 140 | */ 141 | export const promises: Promises; 142 | 143 | export default wql; 144 | } 145 | -------------------------------------------------------------------------------- /packages/ingame-overlay/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tosu/ingame-overlay", 3 | "version": "4.9.0", 4 | "description": "High performance tosu ingame overlay", 5 | "private": true, 6 | "main": "./dist/main/index.js", 7 | "scripts": { 8 | "build": "electron-vite build", 9 | "dev": "electron-vite dev", 10 | "dist": "npm run build && electron-builder --windows" 11 | }, 12 | "keywords": [], 13 | "author": "storycraft ", 14 | "dependencies": { 15 | "@jellybrick/wql-process-monitor": "^1.4.8", 16 | "asdf-overlay-node": "^0.6.3", 17 | "tsprocess": "workspace:*" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.26.0", 21 | "@types/node": "^22.15.14", 22 | "electron": "^36.1.0", 23 | "electron-builder": "^26.0.12", 24 | "electron-vite": "^3.1.0", 25 | "emittery": "^1.1.0", 26 | "node-abi": "^4.8.0", 27 | "typescript": "^5.8.3" 28 | }, 29 | "build": { 30 | "productName": "tosu-ingame-overlay", 31 | "asar": true, 32 | "files": [ 33 | "dist" 34 | ], 35 | "electronLanguages": "en-US", 36 | "win": { 37 | "target": [ 38 | { 39 | "target": "dir", 40 | "arch": [ 41 | "x64" 42 | ] 43 | } 44 | ], 45 | "icon": "./asset/tosu.ico" 46 | }, 47 | "directories": { 48 | "output": "./pack/", 49 | "app": "." 50 | }, 51 | "publish": { 52 | "provider": "github" 53 | } 54 | }, 55 | "os": [ 56 | "win32" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /packages/ingame-overlay/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | ipcRenderer.on('inputCaptureStart', () => { 4 | window.postMessage('editingStarted'); 5 | }); 6 | 7 | ipcRenderer.on('inputCaptureEnd', () => { 8 | window.postMessage('editingEnded'); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ingame-overlay/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "Preserve", 5 | "allowSyntheticDefaultImports": true, 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "moduleResolution": "bundler", 9 | "allowArbitraryExtensions": true, 10 | "resolveJsonModule": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictFunctionTypes": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "@asset/*": ["./asset/*"] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /packages/server/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/favicon.ico -------------------------------------------------------------------------------- /packages/server/assets/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /packages/server/assets/fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /packages/server/assets/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /packages/server/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /packages/server/assets/fonts/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | src: url('Roboto-Regular.ttf'); 4 | src: local('Roboto'), local('Roboto-Regular'), 5 | url('Roboto-Regular.ttf') format('truetype'); 6 | font-weight: 400; 7 | font-style: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: 'Roboto'; 12 | src: url('Roboto-Bold.ttf'); 13 | src: local('Roboto Bold'), local('Roboto-Bold'), 14 | url('Roboto-Bold.ttf') format('truetype'); 15 | font-weight: 700; 16 | font-style: normal; 17 | } 18 | 19 | @font-face { 20 | font-family: 'Roboto'; 21 | src: url('Roboto-Italic.ttf'); 22 | src: local('Roboto Italic'), local('Roboto-Italic'), 23 | url('Roboto-Italic.ttf') format('truetype'); 24 | font-weight: 400; 25 | font-style: italic; 26 | } 27 | 28 | @font-face { 29 | font-family: 'Roboto'; 30 | src: url('Roboto-BoldItalic.ttf'); 31 | src: local('Roboto Bold Italic'), local('Roboto-Bold-Italic'), 32 | url('Roboto-BoldItalic.ttf') format('truetype'); 33 | font-weight: 700; 34 | font-style: italic; 35 | } -------------------------------------------------------------------------------- /packages/server/assets/homepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tosu dashboard by ck v1.5 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | website 16 | 17 | ‧ 18 | 19 | 20 | github 21 | 22 | ‧ 23 | 24 | 25 | discord 26 | 27 | 28 | 29 | DevMode 30 | 31 | 32 | 33 | 34 | 35 | New version available. {OLD} => {NEW} 36 | click here to run update 37 | 38 | 39 | 40 | 41 | wiki 42 | 43 | 44 | 45 | 46 | open tosu folder 47 | 48 | 49 | How to install local counter 50 | 51 | 52 | 53 | 54 | 55 | Settings 56 | 57 | 58 | 59 | Overlay settings 60 | 61 | Installed{{LOCAL_AMOUNT}} 62 | Available{{AVAILABLE_AMOUNT}} 63 | 64 | 65 | 66 | 67 | 68 | {{LIST}} 69 | 70 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /packages/server/assets/icons/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/icons/fonts/icomoon.eot -------------------------------------------------------------------------------- /packages/server/assets/icons/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/icons/fonts/icomoon.ttf -------------------------------------------------------------------------------- /packages/server/assets/icons/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/icons/fonts/icomoon.woff -------------------------------------------------------------------------------- /packages/server/assets/icons/style.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:icomoon;src:url(fonts/icomoon.eot?yo4fz7);src:url(fonts/icomoon.eot?yo4fz7#iefix) format('embedded-opentype'),url(fonts/icomoon.ttf?yo4fz7) format('truetype'),url(fonts/icomoon.woff?yo4fz7) format('woff'),url(fonts/icomoon.svg?yo4fz7#icomoon) format('svg');font-weight:400;font-style:normal;font-display:block}[class*=" icon-"],[class^=icon-]{font-family:icomoon!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-plus:before{content:"\e92a"}.icon-delete:before{content:"\e92d"}.icon-folder:before{content:"\e900"}.icon-settings:before{content:"\e901"}.icon-link:before{content:"\e902"}.icon-github:before{content:"\e903"}.icon-discord:before{content:"\e904"}.icon-builder:before{content:"\e905"} -------------------------------------------------------------------------------- /packages/server/assets/images/39979.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/server/assets/images/39979.png -------------------------------------------------------------------------------- /packages/server/assets/images/discord-5865f2.svg: -------------------------------------------------------------------------------- 1 | DISCORDDISCORD -------------------------------------------------------------------------------- /packages/server/assets/images/github-000000.svg: -------------------------------------------------------------------------------- 1 | GITHUBGITHUB -------------------------------------------------------------------------------- /packages/server/assets/images/twitter-1DA1F2.svg: -------------------------------------------------------------------------------- 1 | TWITTERTWITTER -------------------------------------------------------------------------------- /packages/server/assets/overlayDisplay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/server/index.ts: -------------------------------------------------------------------------------- 1 | import { config, wLogger } from '@tosu/common'; 2 | 3 | import buildAssetsApi from './router/assets'; 4 | import buildBaseApi from './router/index'; 5 | import buildSCApi from './router/scApi'; 6 | import buildSocket from './router/socket'; 7 | import buildV1Api from './router/v1'; 8 | import buildV2Api from './router/v2'; 9 | import { handleSocketCommands } from './utils/commands'; 10 | import { HttpServer } from './utils/http'; 11 | import { isRequestAllowed } from './utils/index'; 12 | import { Websocket } from './utils/socket'; 13 | 14 | export class Server { 15 | instanceManager: any; 16 | app = new HttpServer(); 17 | 18 | WS_V1: Websocket; 19 | WS_SC: Websocket; 20 | WS_V2: Websocket; 21 | WS_V2_PRECISE: Websocket; 22 | WS_COMMANDS: Websocket; 23 | 24 | constructor({ instanceManager }: { instanceManager: any }) { 25 | this.instanceManager = instanceManager; 26 | 27 | this.middlewares(); 28 | } 29 | 30 | start() { 31 | this.WS_V1 = new Websocket({ 32 | instanceManager: this.instanceManager, 33 | pollRateFieldName: 'pollRate', 34 | stateFunctionName: 'getState', 35 | onMessageCallback: handleSocketCommands 36 | }); 37 | this.WS_SC = new Websocket({ 38 | instanceManager: this.instanceManager, 39 | pollRateFieldName: 'pollRate', 40 | stateFunctionName: 'getStateSC', 41 | onMessageCallback: handleSocketCommands 42 | }); 43 | 44 | this.WS_V2 = new Websocket({ 45 | instanceManager: this.instanceManager, 46 | pollRateFieldName: 'pollRate', 47 | stateFunctionName: 'getStateV2', 48 | onMessageCallback: handleSocketCommands 49 | }); 50 | this.WS_V2_PRECISE = new Websocket({ 51 | instanceManager: this.instanceManager, 52 | pollRateFieldName: 'preciseDataPollRate', 53 | stateFunctionName: 'getPreciseData', 54 | onMessageCallback: handleSocketCommands 55 | }); 56 | this.WS_COMMANDS = new Websocket({ 57 | instanceManager: '', 58 | pollRateFieldName: '', 59 | stateFunctionName: '', 60 | onMessageCallback: handleSocketCommands, 61 | onConnectionCallback: (_, url) => { 62 | if (url !== '/websocket/commands?l=__ingame__') return; 63 | 64 | const ip = 65 | config.serverIP === '0.0.0.0' 66 | ? 'localhost' 67 | : config.serverIP; 68 | wLogger.warn( 69 | `[ingame-overlay] initialized successfully, setup it here: http://${ip}:${config.serverPort}/?tab=4` 70 | ); 71 | } 72 | }); 73 | 74 | buildAssetsApi(this); 75 | buildV1Api(this.app); 76 | buildSCApi(this.app); 77 | 78 | buildV2Api(this.app); 79 | 80 | buildSocket({ 81 | app: this.app, 82 | 83 | WS_V1: this.WS_V1, 84 | WS_SC: this.WS_SC, 85 | WS_V2: this.WS_V2, 86 | WS_V2_PRECISE: this.WS_V2_PRECISE, 87 | WS_COMMANDS: this.WS_COMMANDS 88 | }); 89 | 90 | buildBaseApi(this); 91 | 92 | this.app.listen(config.serverPort, config.serverIP); 93 | } 94 | 95 | restart() { 96 | this.app.server.close(); 97 | this.app.listen(config.serverPort, config.serverIP); 98 | } 99 | 100 | restartWS() { 101 | if (this.WS_V1) this.WS_V1.stopLoop(); 102 | if (this.WS_SC) this.WS_SC.stopLoop(); 103 | if (this.WS_V2) this.WS_V2.stopLoop(); 104 | if (this.WS_V2_PRECISE) this.WS_V2_PRECISE.stopLoop(); 105 | 106 | if (this.WS_V1) this.WS_V1.startLoop(); 107 | if (this.WS_SC) this.WS_SC.startLoop(); 108 | if (this.WS_V2) this.WS_V2.startLoop(); 109 | if (this.WS_V2_PRECISE) this.WS_V2_PRECISE.startLoop(); 110 | } 111 | 112 | middlewares() { 113 | const instanceManager = this.instanceManager; 114 | 115 | this.app.use((_, res, next) => { 116 | res.setHeader('Access-Control-Allow-Origin', '*'); 117 | res.setHeader( 118 | 'Access-Control-Allow-Headers', 119 | 'Origin, X-Requested-With, Content-Type, Accept' 120 | ); 121 | res.setHeader( 122 | 'Access-Control-Allow-Methods', 123 | 'POST, GET, PUT, DELETE, OPTIONS' 124 | ); 125 | next(); 126 | }); 127 | 128 | this.app.use((req, res, next) => { 129 | const allowed = isRequestAllowed(req); 130 | if (allowed) { 131 | return next(); 132 | } 133 | 134 | wLogger.warn('[request]', 'Unallowed request', req.url, { 135 | origin: req.headers.origin, 136 | referer: req.headers.referer 137 | }); 138 | 139 | res.statusCode = 403; 140 | res.end('Not Found'); 141 | }); 142 | 143 | this.app.use((req, _, next) => { 144 | req.instanceManager = instanceManager; 145 | next(); 146 | }); 147 | } 148 | } 149 | 150 | export * from './utils/http'; 151 | export * from './utils/socket'; 152 | export * from './utils/index'; 153 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tosu/server", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "cyperdark", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "prepare": "npm run build", 10 | "build": "tsc" 11 | }, 12 | "dependencies": { 13 | "@tosu/common": "workspace:*", 14 | "@tosu/updater": "workspace:*", 15 | "@kotrikd/rosu-pp": "3.0.1", 16 | "semver": "^7.7.1", 17 | "ws": "^8.18.0" 18 | }, 19 | "devDependencies": { 20 | "@types/ws": "^8.18.0" 21 | } 22 | } -------------------------------------------------------------------------------- /packages/server/router/assets.ts: -------------------------------------------------------------------------------- 1 | import { wLogger } from '@tosu/common'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | import { Server, getContentType } from '../index'; 6 | 7 | const pkgAssetsPath = 8 | 'pkg' in process 9 | ? path.join(__dirname, 'assets') 10 | : path.join(__filename, '../../../assets'); 11 | 12 | export default function buildAssetsApi(server: Server) { 13 | server.app.route(/^\/assets\/(?.*)/, 'GET', (req, res) => { 14 | fs.readFile( 15 | path.join(pkgAssetsPath, req.params.filePath), 16 | (err, content) => { 17 | if (err) { 18 | wLogger.debug(`/assets/${req.params.filePath}`, err); 19 | res.writeHead(404, { 'Content-Type': 'text/html' }); 20 | 21 | res.end('page not found'); 22 | return; 23 | } 24 | 25 | res.writeHead(200, { 26 | 'Content-Type': getContentType(req.params.filePath) 27 | }); 28 | 29 | res.end(content); 30 | } 31 | ); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/router/scApi.ts: -------------------------------------------------------------------------------- 1 | import { HttpServer, sendJson } from '../index'; 2 | import { beatmapFileShortcut } from '../scripts/beatmapFile'; 3 | 4 | export default function buildSCApi(app: HttpServer) { 5 | app.route('/json/sc', 'GET', (req, res) => { 6 | const osuInstance: any = req.instanceManager.getInstance( 7 | req.instanceManager.focusedClient 8 | ); 9 | if (!osuInstance) { 10 | throw new Error('osu is not ready/running'); 11 | } 12 | 13 | const json = osuInstance.getStateSC(req.instanceManager); 14 | return sendJson(res, json); 15 | }); 16 | 17 | app.route('/backgroundImage', 'GET', (req, res) => 18 | beatmapFileShortcut(req, res, 'background') 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/router/socket.ts: -------------------------------------------------------------------------------- 1 | import { wLogger } from '@tosu/common'; 2 | 3 | import { HttpServer, Websocket, isRequestAllowed } from '../index'; 4 | 5 | export default function buildSocket({ 6 | app, 7 | 8 | WS_V1, 9 | WS_SC, 10 | WS_V2, 11 | WS_V2_PRECISE, 12 | WS_COMMANDS 13 | }: { 14 | app: HttpServer; 15 | WS_V1: Websocket; 16 | WS_SC: Websocket; 17 | WS_V2: Websocket; 18 | WS_V2_PRECISE: Websocket; 19 | WS_COMMANDS: Websocket; 20 | }) { 21 | app.server.on('upgrade', function (request, socket, head) { 22 | const allowed = isRequestAllowed(request); 23 | if (!allowed) { 24 | wLogger.warn('[ws]', 'External request detected', request.url, { 25 | address: request.socket.remoteAddress, 26 | origin: request.headers.origin, 27 | referer: request.headers.referer 28 | }); 29 | 30 | socket.write('HTTP/1.1 403 Not Found\r\n\r\n'); 31 | socket.destroy(); 32 | return; 33 | } 34 | 35 | try { 36 | const hostname = request.headers.host; 37 | const parsedURL = new URL(`http://${hostname}${request.url}`); 38 | (request as any).query = {}; 39 | 40 | parsedURL.searchParams.forEach( 41 | (value, key) => ((request as any).query[key] = value) 42 | ); 43 | 44 | if (parsedURL.pathname === '/ws') { 45 | WS_V1.socket.handleUpgrade( 46 | request, 47 | socket, 48 | head, 49 | function (ws) { 50 | WS_V1.socket.emit('connection', ws, request); 51 | } 52 | ); 53 | } 54 | 55 | if (parsedURL.pathname === '/tokens') { 56 | WS_SC.socket.handleUpgrade( 57 | request, 58 | socket, 59 | head, 60 | function (ws) { 61 | WS_SC.socket.emit('connection', ws, request); 62 | } 63 | ); 64 | } 65 | 66 | if (parsedURL.pathname === '/websocket/v2') { 67 | WS_V2.socket.handleUpgrade( 68 | request, 69 | socket, 70 | head, 71 | function (ws) { 72 | WS_V2.socket.emit('connection', ws, request); 73 | } 74 | ); 75 | } 76 | 77 | if (parsedURL.pathname === '/websocket/v2/precise') { 78 | WS_V2_PRECISE.socket.handleUpgrade( 79 | request, 80 | socket, 81 | head, 82 | function (ws) { 83 | WS_V2_PRECISE.socket.emit('connection', ws, request); 84 | } 85 | ); 86 | } 87 | 88 | if (parsedURL.pathname === '/websocket/commands') { 89 | WS_COMMANDS.socket.handleUpgrade( 90 | request, 91 | socket, 92 | head, 93 | function (ws) { 94 | WS_COMMANDS.socket.emit('connection', ws, request); 95 | } 96 | ); 97 | } 98 | } catch (exc) { 99 | wLogger.error('[ws]', request.url, (exc as any).message); 100 | wLogger.debug('[ws]', request.url, exc); 101 | } 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /packages/server/router/v1.ts: -------------------------------------------------------------------------------- 1 | import { HttpServer } from '../index'; 2 | import { directoryWalker } from '../utils/directories'; 3 | 4 | export default function buildV1Api(app: HttpServer) { 5 | app.route(/^\/Songs\/(?.*)/, 'GET', (req, res) => { 6 | const url = req.pathname || '/'; 7 | const osuInstance: any = req.instanceManager.getInstance( 8 | req.instanceManager.focusedClient 9 | ); 10 | if (!osuInstance) { 11 | throw new Error('osu is not ready/running'); 12 | } 13 | 14 | const global = osuInstance.get('global'); 15 | if (global.songsFolder === '') { 16 | throw new Error('osu is not ready/running'); 17 | } 18 | 19 | directoryWalker({ 20 | res, 21 | baseUrl: url, 22 | pathname: req.params.filePath, 23 | folderPath: global.songsFolder 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /packages/server/router/v2.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { HttpServer, sendJson } from '../index'; 4 | import { beatmapFileShortcut } from '../scripts/beatmapFile'; 5 | import { directoryWalker } from '../utils/directories'; 6 | 7 | export default function buildV2Api(app: HttpServer) { 8 | app.route('/json/v2', 'GET', (req, res) => { 9 | const osuInstance: any = req.instanceManager.getInstance( 10 | req.instanceManager.focusedClient 11 | ); 12 | if (!osuInstance) { 13 | throw new Error('osu is not ready/running'); 14 | } 15 | 16 | const json = osuInstance.getStateV2(req.instanceManager); 17 | return sendJson(res, json); 18 | }); 19 | 20 | app.route('/json/v2/precise', 'GET', (req, res) => { 21 | const osuInstance: any = req.instanceManager.getInstance( 22 | req.instanceManager.focusedClient 23 | ); 24 | if (!osuInstance) { 25 | throw new Error('osu is not ready/running'); 26 | } 27 | 28 | const json = osuInstance.getPreciseData(req.instanceManager); 29 | return sendJson(res, json); 30 | }); 31 | 32 | app.route( 33 | /\/files\/beatmap\/(?background|audio|file)/, 34 | 'GET', 35 | (req, res) => beatmapFileShortcut(req, res, req.params.type as any) 36 | ); 37 | 38 | app.route(/^\/files\/beatmap\/(?.*)/, 'GET', (req, res) => { 39 | const url = req.pathname || '/'; 40 | const osuInstance: any = req.instanceManager.getInstance( 41 | req.instanceManager.focusedClient 42 | ); 43 | if (!osuInstance) { 44 | throw new Error('osu is not ready/running'); 45 | } 46 | const global = osuInstance.get('global'); 47 | if (global.songsFolder === '') { 48 | throw new Error('osu is not ready/running'); 49 | } 50 | 51 | directoryWalker({ 52 | res, 53 | baseUrl: url, 54 | pathname: req.params.filePath, 55 | folderPath: global.songsFolder 56 | }); 57 | }); 58 | 59 | app.route(/^\/files\/skin\/(?.*)/, 'GET', (req, res) => { 60 | const url = req.pathname || '/'; 61 | 62 | const osuInstance: any = req.instanceManager.getInstance( 63 | req.instanceManager.focusedClient 64 | ); 65 | if (!osuInstance) { 66 | throw new Error('osu is not ready/running'); 67 | } 68 | 69 | const global = osuInstance.get('global'); 70 | if ( 71 | (global.gameFolder === '' && global.skinFolder === '') || 72 | (global.gameFolder == null && global.skinFolder == null) 73 | ) { 74 | throw new Error('osu is not ready/running'); 75 | } 76 | 77 | const folder = path.join(global.gameFolder, 'Skins', global.skinFolder); 78 | directoryWalker({ 79 | res, 80 | baseUrl: url, 81 | pathname: req.params.filePath, 82 | folderPath: folder 83 | }); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /packages/server/scripts/beatmapFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import type { ServerResponse } from 'http'; 3 | import path from 'path'; 4 | 5 | import type { ExtendedIncomingMessage } from '../utils/http'; 6 | import { getContentType, sendJson } from '../utils/index'; 7 | 8 | export function beatmapFileShortcut( 9 | req: ExtendedIncomingMessage, 10 | res: ServerResponse, 11 | beatmapFileType: 'audio' | 'background' | 'file' 12 | ) { 13 | const osuInstance: any = req.instanceManager.getInstance( 14 | req.instanceManager.focusedClient 15 | ); 16 | if (!osuInstance) { 17 | throw new Error('osu is not ready/running'); 18 | } 19 | 20 | const { global, menu } = osuInstance.getServices(['global', 'menu']); 21 | if ( 22 | (global.gameFolder === '' && global.skinFolder === '') || 23 | (global.gameFolder == null && global.skinFolder == null) 24 | ) { 25 | throw new Error('osu is not ready/running'); 26 | } 27 | 28 | const folder = path.join(global.songsFolder, menu.folder || ''); 29 | let fileName = ''; 30 | 31 | if (beatmapFileType === 'audio') fileName = menu.audioFilename; 32 | else if (beatmapFileType === 'background') 33 | fileName = menu.backgroundFilename; 34 | else if (beatmapFileType === 'file') fileName = menu.filename; 35 | else { 36 | return sendJson(res, { 37 | error: 'Unknown file type' 38 | }); 39 | } 40 | 41 | const filePath = path.join(folder, fileName); 42 | const fileStat = fs.statSync(filePath); 43 | if (!fileStat.isFile() || !fs.existsSync(filePath)) { 44 | res.writeHead(404, { 45 | 'Content-Type': getContentType(fileName) 46 | }); 47 | return res.end(); 48 | } 49 | 50 | if (req.headers.range) { 51 | const range = req.headers.range.replace('bytes=', '').split('-'); 52 | const start = parseInt(range[0]); 53 | const end = range[1] ? parseInt(range[1]) : fileStat.size - 1; 54 | 55 | if (start >= fileStat.size || end >= fileStat.size) { 56 | res.writeHead(416, { 57 | 'Content-Range': `bytes */${fileStat.size}` 58 | }); 59 | return res.end(); 60 | } 61 | 62 | res.writeHead(206, { 63 | 'Accept-Ranges': 'bytes', 64 | 'Content-Type': getContentType(fileName), 65 | 'Content-Range': `bytes ${start}-${end}/${fileStat.size}`, 66 | 'Content-Length': end - start + 1 67 | }); 68 | 69 | fs.createReadStream(filePath, { start, end }).pipe(res); 70 | return; 71 | } 72 | 73 | res.writeHead(200, { 74 | 'Content-Type': getContentType(fileName), 75 | 'Content-Length': fileStat.size 76 | }); 77 | 78 | fs.createReadStream(filePath).pipe(res); 79 | } 80 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ES2020" 5 | ], 6 | "module": "commonjs", 7 | "moduleResolution": "Node", 8 | "allowJs": true, 9 | "esModuleInterop": true, 10 | "outDir": "dist", 11 | "rootDir": "./", 12 | "sourceMap": false, 13 | "declaration": false, 14 | "strict": true, 15 | "noImplicitAny": false, 16 | "target": "ES2020", 17 | "strictPropertyInitialization": false, 18 | "baseUrl": ".", 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "dist", 23 | ], 24 | "include": [ 25 | "**/*" 26 | ], 27 | } -------------------------------------------------------------------------------- /packages/server/utils/commands.ts: -------------------------------------------------------------------------------- 1 | import { JsonSafeParse, wLogger } from '@tosu/common'; 2 | 3 | import { getLocalCounters } from './counters'; 4 | import { parseCounterSettings } from './parseSettings'; 5 | import { ModifiedWebsocket } from './socket'; 6 | 7 | export function handleSocketCommands(data: string, socket: ModifiedWebsocket) { 8 | wLogger.debug('[ws]', `commands`, data); 9 | if (!data.includes(':')) { 10 | return; 11 | } 12 | 13 | const index = data.indexOf(':'); 14 | const command = data.substring(0, index); 15 | const payload = data.substring(index + 1); 16 | 17 | let message: any; 18 | 19 | const requestedFrom = decodeURI(socket.query?.l || ''); 20 | const requestedName = decodeURI(payload || ''); 21 | switch (command) { 22 | case 'getCounters': { 23 | if ( 24 | requestedFrom !== '__ingame__' || 25 | requestedName !== requestedFrom 26 | ) { 27 | message = { 28 | error: 'Wrong overlay' 29 | }; 30 | break; 31 | } 32 | 33 | message = getLocalCounters(); 34 | break; 35 | } 36 | 37 | case 'getSettings': { 38 | if (requestedName !== requestedFrom) { 39 | message = { 40 | error: 'Wrong overlay' 41 | }; 42 | break; 43 | } 44 | 45 | try { 46 | const result = parseCounterSettings( 47 | requestedName, 48 | 'counter/get' 49 | ); 50 | if (result instanceof Error) { 51 | message = { 52 | error: result.message 53 | }; 54 | break; 55 | } 56 | 57 | message = result.values; 58 | } catch (exc) { 59 | wLogger.error( 60 | '[ws]', 61 | `commands`, 62 | command, 63 | (exc as Error).message 64 | ); 65 | wLogger.debug('[ws]', `commands`, command, exc); 66 | } 67 | 68 | break; 69 | } 70 | 71 | case 'applyFilters': { 72 | const json = JsonSafeParse(payload, new Error('Broken json')); 73 | if (json instanceof Error) { 74 | wLogger.error( 75 | '[ws]', 76 | `commands`, 77 | command, 78 | (json as Error).message 79 | ); 80 | wLogger.debug('[ws]', `commands`, command, json); 81 | return; 82 | } 83 | 84 | try { 85 | if (!Array.isArray(json)) { 86 | wLogger.error( 87 | `applyFilter(${socket.id})[${socket.pathname}] >>>`, 88 | `Filters should be array of strings (${json})` 89 | ); 90 | return; 91 | } 92 | 93 | socket.filters = json; 94 | return; 95 | } catch (exc) { 96 | wLogger.error( 97 | '[ws]', 98 | `commands`, 99 | command, 100 | (exc as Error).message 101 | ); 102 | wLogger.debug('[ws]', `commands`, command, exc); 103 | } 104 | } 105 | } 106 | 107 | try { 108 | socket.send( 109 | JSON.stringify({ 110 | command, 111 | message 112 | }) 113 | ); 114 | } catch (exc) { 115 | wLogger.error('[ws]', `commands-send`, (exc as Error).message); 116 | wLogger.debug('[ws]', `commands-send`, exc); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/server/utils/counters.types.ts: -------------------------------------------------------------------------------- 1 | export interface ISettings { 2 | uniqueID: string; 3 | uniqueCheck?: string; 4 | type: ISettingsType; 5 | title: string; 6 | options?: 7 | | string[] 8 | | { 9 | required: boolean; 10 | type: 'text' | 'number' | 'checkbox' | 'options'; 11 | name: string; 12 | title: string; 13 | description: string; 14 | values: string[]; 15 | value: any; 16 | }[]; 17 | description: string; 18 | value: any; 19 | } 20 | 21 | export type ISettingsCompact = { [key: string]: any }; 22 | 23 | export type ISettingsType = 24 | | 'text' 25 | | 'color' 26 | | 'number' 27 | | 'checkbox' 28 | | 'button' 29 | | 'options' 30 | | 'commands' 31 | | 'textarea' 32 | | 'password' 33 | | 'header'; 34 | 35 | export interface ICounter { 36 | _downloaded?: boolean; 37 | _updatable?: boolean; 38 | _settings?: boolean; 39 | folderName: string; 40 | name: string; 41 | author: string; 42 | version: string; 43 | resolution: number[]; 44 | authorlinks: string[]; 45 | settings: ISettings[]; 46 | 47 | usecase?: string; 48 | compatiblewith?: string[]; 49 | assets?: { 50 | type: string; 51 | url: string; 52 | }[]; 53 | downloadLink?: string; 54 | } 55 | 56 | export interface bodyPayload { 57 | uniqueID: string; 58 | value: any; 59 | } 60 | -------------------------------------------------------------------------------- /packages/server/utils/directories.ts: -------------------------------------------------------------------------------- 1 | import { getStaticPath, wLogger } from '@tosu/common'; 2 | import fs from 'fs'; 3 | import http from 'http'; 4 | import path from 'path'; 5 | 6 | import { getContentType } from '../index'; 7 | import { OVERLAYS_STATIC } from './homepage'; 8 | 9 | function isPathDirectory(path) { 10 | const stat = fs.statSync(path); 11 | return Boolean(stat && stat.isDirectory()); 12 | } 13 | 14 | export function directoryWalker({ 15 | _htmlRedirect, 16 | res, 17 | baseUrl, 18 | folderPath, 19 | pathname 20 | }: { 21 | _htmlRedirect?: boolean; 22 | 23 | res: http.ServerResponse; 24 | baseUrl: string; 25 | 26 | pathname: string; 27 | folderPath: string; 28 | }) { 29 | let cleanedUrl; 30 | try { 31 | cleanedUrl = decodeURIComponent(pathname); 32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 33 | } catch (error) { 34 | res.writeHead(404, { 35 | 'Content-Type': getContentType('file.txt') 36 | }); 37 | res.end(''); 38 | return; 39 | } 40 | 41 | const contentType = getContentType(cleanedUrl); 42 | const filePath = path.join(folderPath, cleanedUrl); 43 | 44 | const isDirectory = isPathDirectory(filePath); 45 | const isHTML = filePath.endsWith('.html'); 46 | 47 | if (isDirectory) { 48 | return readDirectory(filePath, baseUrl, (html: Error | string) => { 49 | if (html instanceof Error) { 50 | res.writeHead(404, { 'Content-Type': 'text/html' }); 51 | res.end('404 Not Found'); 52 | return; 53 | } 54 | 55 | res.writeHead(200, { 56 | 'Content-Type': getContentType('file.html') 57 | }); 58 | res.end(html); 59 | }); 60 | } 61 | 62 | return fs.readFile( 63 | filePath, 64 | isHTML === true ? 'utf8' : null, 65 | (err, content) => { 66 | if (err?.code === 'ENOENT' && _htmlRedirect === true) { 67 | return readDirectory( 68 | filePath.replace('index.html', ''), 69 | baseUrl, 70 | (html: Error | string) => { 71 | if (html instanceof Error) { 72 | res.writeHead(404, { 'Content-Type': 'text/html' }); 73 | res.end('404 Not Found'); 74 | return; 75 | } 76 | 77 | if (isHTML === true) { 78 | html = addCounterMetadata(html, filePath); 79 | } 80 | 81 | res.writeHead(200, { 82 | 'Content-Type': getContentType('file.html') 83 | }); 84 | res.end(html); 85 | } 86 | ); 87 | } 88 | 89 | if (err?.code === 'ENOENT') { 90 | res.writeHead(404, { 'Content-Type': 'text/html' }); 91 | res.end('404 Not Found'); 92 | return; 93 | } 94 | 95 | if (err) { 96 | res.writeHead(500); 97 | res.end(`Server Error: ${err.code}`); 98 | return; 99 | } 100 | 101 | if (isHTML === true) { 102 | content = addCounterMetadata(content.toString(), filePath); 103 | } 104 | 105 | res.writeHead(200, { 'Content-Type': contentType }); 106 | res.end(content, 'utf-8'); 107 | } 108 | ); 109 | } 110 | 111 | export function readDirectory( 112 | folderPath: string, 113 | url: string, 114 | callback: Function 115 | ) { 116 | fs.readdir(folderPath, (err, folders) => { 117 | if (err) { 118 | return callback(new Error(`Files not found: ${folderPath}`)); 119 | } 120 | 121 | const html = folders.map((r) => { 122 | const slashAtTheEnd = getContentType(r) === '' ? '/' : ''; 123 | 124 | return `${r}`; 125 | }); 126 | 127 | return callback( 128 | OVERLAYS_STATIC.replace('{OVERLAYS_LIST}', html.join('\n')).replace( 129 | '{PAGE_URL}', 130 | `tosu - ${url}` 131 | ) 132 | ); 133 | }); 134 | } 135 | 136 | export function addCounterMetadata(html: string, filePath: string) { 137 | try { 138 | const staticPath = getStaticPath(); 139 | 140 | const counterPath = path 141 | .dirname(filePath.replace(staticPath, '')) 142 | .replace(/^(\\\\\\|\\\\|\\|\/|\/\/)/, '') 143 | .replace(/\\/gm, '/'); 144 | 145 | html += `\n\n\n\n`; 146 | 147 | return html; 148 | } catch (error) { 149 | wLogger.error('addCounterMetadata', (error as any).message); 150 | wLogger.debug('addCounterMetadata', error); 151 | 152 | return ''; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/server/utils/hashing.ts: -------------------------------------------------------------------------------- 1 | export function getUniqueID() { 2 | const s4 = () => 3 | Math.floor((1 + Math.random()) * 0x10000) 4 | .toString(16) 5 | .substring(1); 6 | return s4() + s4() + '-' + s4(); 7 | } 8 | -------------------------------------------------------------------------------- /packages/server/utils/homepage.ts: -------------------------------------------------------------------------------- 1 | export const OVERLAYS_STATIC = ` 2 | 3 | {PAGE_URL} 4 | 5 | 6 | 7 | 51 | 52 | 53 | 54 | 55 | {OVERLAYS_LIST} 56 | 57 | 58 | `; 59 | -------------------------------------------------------------------------------- /packages/server/utils/htmls.ts: -------------------------------------------------------------------------------- 1 | export const iconsImages = { 2 | 'github.com': '/assets/images/github-000000.svg', 3 | 'twitter.com': '/assets/images/twitter-1DA1F2.svg', 4 | 'discord.gg': '/assets/images/discord-5865f2.svg', 5 | 'discord.com': '/assets/images/discord-5865f2.svg' 6 | }; 7 | 8 | export const emptyNotice = 9 | 'No countersGo here to get one 👉'; 10 | export const emptyCounters = 11 | 'No countersChange your search phrase'; 12 | export const noMoreCounters = 13 | 'Nice job!You downloaded all available pp counters'; 14 | 15 | export const iframeHTML = 16 | ''; 17 | 18 | export const metadataHTML = ` 19 | URL: {TEXT_URL} 20 | Resolution: {X} x {Y} 21 | `; 22 | 23 | export const nameHTML = '{NAME}'; 24 | export const authorHTML = 'by {AUTHOR}'; 25 | export const authorLinksHTML = 26 | ''; 27 | 28 | export const galleryImageHTML = ''; 29 | 30 | export const resultItemHTML = ` 31 | 32 | 33 | 34 | {NAME} 35 | {AUTHOR}{AUTHOR_LINKS} 36 | 37 | {BUTTONS} 38 | 39 | 40 | {GALLERY} 41 | {FOOTER} 42 | `; 43 | 44 | export const settingsItemHTML = ` 45 | 46 | 47 | {NAME} 48 | {DESCRIPTION} 49 | 50 | {INPUT} 51 | `; 52 | 53 | export const checkboxHTML = ` 54 | 55 | 56 | 57 | 58 | `; 59 | 60 | export const inputHTML = 61 | ''; 62 | 63 | export const textareaHTML = '{VALUE}'; 64 | 65 | export const settingsGroupHTML = ` 66 | 67 | {header} 68 | 69 | {items} 70 | 71 | 72 | `; 73 | 74 | export const settingsItemHTMLv2 = ` 75 | 76 | 77 | {input-1} 78 | {name} 79 | {input-2} 80 | 81 | {input-3} 82 | {description} 83 | 84 | `; 85 | 86 | export const settingsSwitchHTML = ` 87 | 88 | 89 | 90 | 91 | `; 92 | export const settingsNumberInputHTML = ` 93 | 94 | - 95 | 96 | + 97 | 98 | `; 99 | 100 | export const settingsTextInputHTML = ` 101 | 102 | 103 | 104 | `; 105 | 106 | export const settingsTextareaInputHTML = ` 107 | 108 | {value} 109 | 110 | `; 111 | 112 | export const settingsSaveButtonHTMLv2 = ` 113 | 114 | Save Settings 115 | 116 | `; 117 | 118 | export const saveSettingsButtonHTML = 119 | 'Save settings'; 120 | 121 | export const submitCounterHTML = ` 122 | 123 | Submit your pp counter here 124 | `; 125 | -------------------------------------------------------------------------------- /packages/server/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@tosu/common'; 2 | import http from 'http'; 3 | import path from 'path'; 4 | 5 | const contentTypes = { 6 | '.html': 'text/html; charset=utf-8', 7 | '.js': 'text/javascript; charset=utf-8', 8 | '.css': 'text/css; charset=utf-8', 9 | '.json': 'application/json; charset=utf-8', 10 | '.png': 'image/png; charset=utf-8', 11 | '.jpg': 'image/jpg; charset=utf-8', 12 | '.gif': 'image/gif; charset=utf-8', 13 | '.svg': 'image/svg+xml; charset=utf-8', 14 | '.wav': 'audio/wav; charset=utf-8', 15 | '.mp4': 'video/mp4; charset=utf-8', 16 | '.woff': 'application/font-woff; charset=utf-8', 17 | '.ttf': 'application/font-ttf; charset=utf-8', 18 | '.eot': 'application/vnd.ms-fontobject; charset=utf-8', 19 | '.otf': 'application/font-otf; charset=utf-8', 20 | '.wasm': 'application/wasm; charset=utf-8', 21 | '.aac': 'audio/aac; charset=utf-8', 22 | '.abw': 'application/x-abiword; charset=utf-8', 23 | '.apng': 'image/apng; charset=utf-8', 24 | '.arc': 'application/x-freearc; charset=utf-8', 25 | '.avif': 'image/avif; charset=utf-8', 26 | '.avi': 'video/x-msvideo; charset=utf-8', 27 | '.bin': 'application/octet-stream; charset=utf-8', 28 | '.bmp': 'image/bmp; charset=utf-8', 29 | '.bz': 'application/x-bzip; charset=utf-8', 30 | '.bz2': 'application/x-bzip2; charset=utf-8', 31 | '.cda': 'application/x-cdf; charset=utf-8', 32 | '.csv': 'text/csv; charset=utf-8', 33 | '.gz': 'application/gzip; charset=utf-8', 34 | '.htm': 'text/html; charset=utf-8', 35 | '.ico': 'image/vnd.microsoft.icon; charset=utf-8', 36 | '.jpeg': 'image/jpeg; charset=utf-8', 37 | '.jsonld': 'application/ld+json; charset=utf-8', 38 | '.mid': 'audio/x-midi; charset=utf-8', 39 | '.mjs': 'text/javascript; charset=utf-8', 40 | '.mp3': 'audio/mpeg; charset=utf-8', 41 | '.mpeg': 'video/mpeg; charset=utf-8', 42 | '.mpkg': 'application/vnd.apple.installer+xml; charset=utf-8', 43 | '.ogg': 'audio/ogg; charset=utf-8', 44 | '.oga': 'audio/ogg; charset=utf-8', 45 | '.ogv': 'video/ogg; charset=utf-8', 46 | '.ogx': 'application/ogg; charset=utf-8', 47 | '.opus': 'audio/opus; charset=utf-8', 48 | '.rar': 'application/vnd.rar; charset=utf-8', 49 | '.sh': 'application/x-sh; charset=utf-8', 50 | '.tar': 'application/x-tar; charset=utf-8', 51 | '.tif': 'image/tiff; charset=utf-8', 52 | '.ts': 'video/mp2t; charset=utf-8', 53 | '.txt': 'text/plain; charset=utf-8', 54 | '.weba': 'audio/webm; charset=utf-8', 55 | '.webm': 'video/webm; charset=utf-8', 56 | '.webp': 'image/webp; charset=utf-8', 57 | '.woff2': 'font/woff2; charset=utf-8', 58 | '.xhtml': 'application/xhtml+xml; charset=utf-8', 59 | '.xml': 'application/xmlz; charset=utf-8', 60 | '.xul': 'application/vnd.mozilla.xul+xml; charset=utf-8', 61 | '.zip': 'application/zip; charset=utf-8', 62 | '.3gp': 'video/3gpp;; charset=utf-8', 63 | '.3g2': 'video/3gpp2;; charset=utf-8', 64 | '.7z': 'application/x-7z-compressed; charset=utf-8', 65 | '.osu': 'text/plain; charset=utf-8', 66 | 67 | '.midi': 'audio/x-midi; charset=utf-8', 68 | '.swf': 'application/x-shockwave-flash; charset=utf-8', 69 | '.tiff': 'image/tiff; charset=utf-8' 70 | }; 71 | 72 | export function getContentType(text: string) { 73 | const extension = path.extname(text); 74 | 75 | const contentType = contentTypes[extension] || ''; 76 | return contentType; 77 | } 78 | 79 | export function sendJson(response: http.ServerResponse, json: object | any[]) { 80 | response.setHeader('Content-Type', 'application/json'); 81 | 82 | try { 83 | return response.end(JSON.stringify(json)); 84 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 85 | } catch (error) { 86 | return response.end(JSON.stringify({ error: 'Json parsing error' })); 87 | } 88 | } 89 | 90 | export function isRequestAllowed(req: http.IncomingMessage) { 91 | const remoteAddress = req.socket.remoteAddress; 92 | 93 | const origin = req.headers.origin; 94 | const referer = req.headers.referer; 95 | 96 | const isOriginOrRefererAllowed = 97 | isAllowedIP(origin) || isAllowedIP(referer); 98 | 99 | // NOT SURE 100 | if (origin === undefined && referer === undefined) { 101 | return true; 102 | } 103 | 104 | if (isOriginOrRefererAllowed) { 105 | return true; 106 | } 107 | 108 | if (isOriginOrRefererAllowed && isAllowedIP(remoteAddress)) { 109 | return false; 110 | } 111 | 112 | return false; 113 | } 114 | 115 | function isAllowedIP(url: string | undefined) { 116 | if (!url) return false; 117 | 118 | const allowedIPs = config.allowedIPs.split(','); 119 | allowedIPs.push(config.serverIP); 120 | 121 | try { 122 | const hostname = new URL(url).hostname.toLowerCase().trim(); 123 | return allowedIPs.some((pattern) => { 124 | // compare IP's length and match wildcard like comparision 125 | if (pattern.includes('*') && pattern.includes('.')) { 126 | const patternLength = pattern.match(/\./g)?.length || 0; 127 | const hostnameLength = hostname.match(/\./g)?.length || 0; 128 | 129 | if (patternLength !== 3 || hostnameLength !== 3) return false; 130 | 131 | const patternParts = pattern.split('.'); 132 | const hostnameParts = hostname.split('.'); 133 | 134 | const matches = hostnameParts.filter((r, index) => { 135 | if (patternParts[index] === '*') return true; 136 | 137 | return patternParts[index] === r; 138 | }); 139 | 140 | return matches.length === 4; 141 | } 142 | 143 | return pattern.toLowerCase().trim() === hostname; 144 | }); 145 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 146 | } catch (error) { 147 | return allowedIPs.some( 148 | (r) => r.toLowerCase().trim() === url.toLowerCase().trim() 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /packages/tosu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tosu", 3 | "version": "4.9.0", 4 | "main": "dist/index.js", 5 | "bin": "dist/index.js", 6 | "scripts": { 7 | "genver": "npx genversion _version.js", 8 | "ts:run": "cross-env NODE_ENV=development ts-node --transpile-only -r tsconfig-paths/register --project tsconfig.json", 9 | "ts:compile": "ncc build src/index.ts -o dist -m -d", 10 | "run:dev": "pnpm run genver && pnpm run ts:run src/index.ts", 11 | "compile:prepare-htmls": "cp -rf node_modules/@tosu/server/assets ./dist", 12 | "compile:win": "pnpm run genver && pnpm run ts:compile && pnpm run compile:prepare-htmls && pkg --output dist/tosu.exe --debug --config pkg.win.json --compress brotli dist/index.js && pnpm run ts:run src/postBuild.ts", 13 | "compile:linux": "pnpm run genver && pnpm run ts:compile && pnpm run compile:prepare-htmls && pkg --output dist/tosu --debug --config pkg.linux.json --compress brotli dist/index.js" 14 | }, 15 | "dependencies": { 16 | "@tosu/common": "workspace:*", 17 | "@tosu/ingame-overlay-updater": "workspace:*", 18 | "@tosu/server": "workspace:*", 19 | "@tosu/updater": "workspace:*", 20 | "osu-catch-stable": "^4.0.0", 21 | "osu-classes": "^3.1.0", 22 | "osu-mania-stable": "^5.0.0", 23 | "osu-parsers": "^4.1.7", 24 | "osu-standard-stable": "^5.0.0", 25 | "osu-taiko-stable": "^5.0.0", 26 | "resedit": "^2.0.3", 27 | "@kotrikd/rosu-pp": "3.0.1", 28 | "semver": "^7.7.1", 29 | "tsprocess": "workspace:*" 30 | }, 31 | "devDependencies": { 32 | "@vercel/ncc": "^0.38.3", 33 | "@yao-pkg/pkg": "^6.3.2", 34 | "cross-env": "^7.0.3", 35 | "genversion": "^3.2.0", 36 | "ts-node": "^10.9.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/tosu/pkg.linux.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": "dist/**/*.js", 3 | "assets": [ 4 | "dist/**/*.node", 5 | "dist/**/*.wasm", 6 | "dist/target/**/*", 7 | "dist/_version.js", 8 | "dist/assets/**/*" 9 | ], 10 | "targets": [ 11 | "node20-linux-x64" 12 | ], 13 | "outputPath": "dist" 14 | } 15 | -------------------------------------------------------------------------------- /packages/tosu/pkg.win.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": "dist/**/*.js", 3 | "assets": [ 4 | "dist/**/*.node", 5 | "dist/**/*.wasm", 6 | "dist/target/**/*", 7 | "dist/_version.js", 8 | "dist/assets/**/*" 9 | ], 10 | "targets": [ 11 | "node20-win-x64" 12 | ], 13 | "outputPath": "dist" 14 | } 15 | -------------------------------------------------------------------------------- /packages/tosu/src/api/types/sc.ts: -------------------------------------------------------------------------------- 1 | export type ApiAnswer = scAPI | { error?: string }; 2 | 3 | export interface scAPI { 4 | osuIsRunning: number; 5 | chatIsEnabled: number; 6 | ingameInterfaceIsEnabled: number; 7 | isBreakTime: number; 8 | 9 | banchoUsername: string; 10 | banchoId: number; 11 | banchoIsConnected: number; 12 | banchoCountry: string; 13 | banchoStatus: number; 14 | 15 | artistUnicode: string; 16 | artistRoman: string; 17 | 18 | titleRoman: string; 19 | titleUnicode: string; 20 | 21 | mapArtistTitleUnicode: string; 22 | mapArtistTitle: string; 23 | 24 | diffName: string; 25 | mapDiff: string; 26 | 27 | creator: string; 28 | 29 | liveStarRating: number; 30 | mStars: number; 31 | 32 | mAR: number; 33 | ar: number; 34 | 35 | mOD: number; 36 | od: number; 37 | 38 | cs: number; 39 | mCS: number; 40 | 41 | hp: number; 42 | mHP: number; 43 | 44 | currentBpm: number; 45 | bpm: number; 46 | 47 | mainBpm: number; 48 | maxBpm: number; 49 | minBpm: number; 50 | 51 | mMainBpm: number; 52 | mMaxBpm: number; 53 | mMinBpm: number; 54 | 55 | mBpm: string; 56 | 57 | md5: string; 58 | 59 | gameMode: string; 60 | mode: number; 61 | 62 | time: number; 63 | previewtime: number; 64 | totaltime: number; 65 | timeLeft: string; 66 | drainingtime: number; 67 | totalAudioTime: number; 68 | firstHitObjectTime: number; 69 | 70 | mapid: number; 71 | mapsetid: number; 72 | mapStrains: { [key: string]: number }; 73 | mapBreaks: { 74 | startTime: number; 75 | endTime: number; 76 | hasEffect: boolean; 77 | }[]; 78 | mapKiaiPoints: { 79 | startTime: number; 80 | duration: number; 81 | }[]; 82 | mapPosition: string; 83 | mapTimingPoints: { 84 | startTime: number; 85 | bpm?: number; 86 | beatLength: number; 87 | }[]; 88 | 89 | sliders: number; 90 | circles: number; 91 | spinners: number; 92 | maxCombo: number; 93 | 94 | mp3Name: string; 95 | osuFileName: string; 96 | backgroundImageFileName: string; 97 | 98 | osuFileLocation: string; 99 | backgroundImageLocation: string; 100 | 101 | retries: number; 102 | 103 | username: string; 104 | 105 | score: number; 106 | 107 | playerHp: number; 108 | playerHpSmooth: number; 109 | 110 | combo: number; 111 | currentMaxCombo: number; 112 | 113 | keyOverlay: string; 114 | 115 | geki: number; 116 | c300: number; 117 | katsu: number; 118 | c100: number; 119 | c50: number; 120 | miss: number; 121 | sliderBreaks: number; 122 | 123 | acc: number; 124 | unstableRate: number; 125 | convertedUnstableRate: number; 126 | 127 | grade: number; 128 | maxGrade: number; 129 | 130 | hitErrors: number[]; 131 | 132 | modsEnum: number; 133 | mods: string; 134 | 135 | ppIfMapEndsNow: number; 136 | ppIfRestFced: number; 137 | 138 | leaderBoardMainPlayer: string; 139 | leaderBoardPlayers: string; 140 | 141 | rankedStatus: number; 142 | songSelectionRankingType: number; 143 | rawStatus: number; 144 | 145 | dir: string; 146 | dl: string; 147 | 148 | osu_90PP: number; 149 | osu_95PP: number; 150 | osu_96PP: number; 151 | osu_97PP: number; 152 | osu_98PP: number; 153 | osu_99PP: number; 154 | osu_99_9PP: number; 155 | osu_SSPP: number; 156 | 157 | osu_m90PP: number; 158 | osu_m95PP: number; 159 | osu_m96PP: number; 160 | osu_m97PP: number; 161 | osu_m98PP: number; 162 | osu_m99PP: number; 163 | osu_m99_9PP: number; 164 | osu_mSSPP: number; 165 | 166 | accPpIfMapEndsNow: number; 167 | aimPpIfMapEndsNow: number; 168 | speedPpIfMapEndsNow: number; 169 | strainPpIfMapEndsNow: number; 170 | noChokePp: number; 171 | 172 | skinPath: string; 173 | skin: string; 174 | 175 | songSelectionScores: string; 176 | songSelectionMainPlayerScore: string; 177 | 178 | songSelectionTotalScores: number; 179 | status: number; 180 | 181 | mania_m1_000_000PP: number; 182 | mania_1_000_000PP: number; 183 | 184 | simulatedPp: number; 185 | 186 | plays: number; 187 | tags: string; 188 | source: string; 189 | 190 | starsNomod: number; 191 | comboLeft: number; 192 | 193 | localTime: number; 194 | localTimeISO: string; 195 | 196 | sl: number; 197 | sv: number; 198 | threadid: number; 199 | test: string; 200 | } 201 | -------------------------------------------------------------------------------- /packages/tosu/src/api/utils/buildResultV2Precise.ts: -------------------------------------------------------------------------------- 1 | import { ApiAnswerPrecise as ApiAnswer, PreciseTourney } from '@/api/types/v2'; 2 | import { InstanceManager } from '@/instances/manager'; 3 | 4 | const buildTourneyData = ( 5 | instanceManager: InstanceManager 6 | ): PreciseTourney[] => { 7 | const osuTourneyManager = Object.values( 8 | instanceManager.osuInstances 9 | ).filter((instance) => instance.isTourneyManager); 10 | if (osuTourneyManager.length < 1) { 11 | return []; 12 | } 13 | 14 | const osuTourneyClients = Object.values( 15 | instanceManager.osuInstances 16 | ).filter((instance) => instance.isTourneySpectator); 17 | 18 | const mappedOsuTourneyClients = osuTourneyClients 19 | .sort((a, b) => a.ipcId - b.ipcId) 20 | .map((instance): PreciseTourney => { 21 | const { gameplay } = instance.getServices(['gameplay']); 22 | 23 | return { 24 | ipcId: instance.ipcId, 25 | keys: { 26 | k1: { 27 | isPressed: gameplay.keyOverlay.K1Pressed, 28 | count: gameplay.keyOverlay.K1Count 29 | }, 30 | k2: { 31 | isPressed: gameplay.keyOverlay.K2Pressed, 32 | count: gameplay.keyOverlay.K2Count 33 | }, 34 | m1: { 35 | isPressed: gameplay.keyOverlay.M1Pressed, 36 | count: gameplay.keyOverlay.M1Count 37 | }, 38 | m2: { 39 | isPressed: gameplay.keyOverlay.M2Pressed, 40 | count: gameplay.keyOverlay.M2Count 41 | } 42 | }, 43 | hitErrors: gameplay.hitErrors 44 | }; 45 | }); 46 | 47 | return mappedOsuTourneyClients; 48 | }; 49 | 50 | export const buildResult = (instanceManager: InstanceManager): ApiAnswer => { 51 | const osuInstance = instanceManager.getInstance( 52 | instanceManager.focusedClient 53 | ); 54 | if (!osuInstance) { 55 | return { error: 'not_ready' }; 56 | } 57 | 58 | const { global, gameplay } = osuInstance.getServices([ 59 | 'gameplay', 60 | 'global' 61 | ]); 62 | 63 | return { 64 | currentTime: global.playTime, 65 | keys: { 66 | k1: { 67 | isPressed: gameplay.keyOverlay.K1Pressed, 68 | count: gameplay.keyOverlay.K1Count 69 | }, 70 | k2: { 71 | isPressed: gameplay.keyOverlay.K2Pressed, 72 | count: gameplay.keyOverlay.K2Count 73 | }, 74 | m1: { 75 | isPressed: gameplay.keyOverlay.M1Pressed, 76 | count: gameplay.keyOverlay.M1Count 77 | }, 78 | m2: { 79 | isPressed: gameplay.keyOverlay.M2Pressed, 80 | count: gameplay.keyOverlay.M2Count 81 | } 82 | }, 83 | hitErrors: gameplay.hitErrors, 84 | tourney: buildTourneyData(instanceManager) 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /packages/tosu/src/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tosuapp/tosu/33af090bba8ea0c51c267ecf2cba34e0f80775d7/packages/tosu/src/assets/icon.ico -------------------------------------------------------------------------------- /packages/tosu/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | argumentsParser, 3 | config, 4 | getProgramPath, 5 | wLogger, 6 | watchConfigFile 7 | } from '@tosu/common'; 8 | import { Server } from '@tosu/server'; 9 | import { autoUpdater, checkUpdates } from '@tosu/updater'; 10 | import { Process } from 'tsprocess'; 11 | 12 | import { InstanceManager } from '@/instances/manager'; 13 | 14 | // NOTE: _version.js packs with pkg support in tosu build 15 | const currentVersion = require(process.cwd() + '/_version.js'); 16 | 17 | (async () => { 18 | config.currentVersion = currentVersion; 19 | wLogger.info(`Starting tosu`); 20 | 21 | Process.disablePowerThrottling(); 22 | 23 | const instanceManager = new InstanceManager(); 24 | const httpServer = new Server({ instanceManager }); 25 | 26 | watchConfigFile({ httpServer, initial: true }); 27 | 28 | const { update, onedrive: onedriveBypass } = argumentsParser(process.argv); 29 | 30 | const isDev = process.env.NODE_ENV === 'development'; 31 | const isConfigUpdate = config.enableAutoUpdate === true; 32 | if (update !== null && update !== undefined) { 33 | if (update === true) { 34 | await autoUpdater(); 35 | } else { 36 | await checkUpdates(); 37 | } 38 | } else { 39 | if (isDev === false && isConfigUpdate) { 40 | await autoUpdater(); 41 | } else { 42 | await checkUpdates(); 43 | } 44 | } 45 | 46 | if (process.platform === 'win32') { 47 | const currentPath = getProgramPath(); 48 | if (process.env.TEMP && currentPath.startsWith(process.env.TEMP)) { 49 | wLogger.warn( 50 | 'Incase if you running tosu from archive, please extract it to a folder' 51 | ); 52 | return; 53 | } 54 | 55 | if ( 56 | onedriveBypass !== true && 57 | process.env.OneDrive && 58 | currentPath.startsWith(process.env.OneDrive) 59 | ) { 60 | wLogger.warn( 61 | 'tosu cannot run from a OneDrive folder due to potential sync conflicts and performance issues.' 62 | ); 63 | wLogger.warn('Please move tosu to different folder'); 64 | return; 65 | } 66 | } 67 | 68 | wLogger.info('Searching for osu!'); 69 | 70 | httpServer.start(); 71 | instanceManager.runWatcher(); 72 | instanceManager.runDetemination(); 73 | })(); 74 | -------------------------------------------------------------------------------- /packages/tosu/src/memory/index.ts: -------------------------------------------------------------------------------- 1 | import { ClientType, wLogger } from '@tosu/common'; 2 | import { platform } from 'process'; 3 | import { Process } from 'tsprocess'; 4 | 5 | import type { AbstractInstance } from '@/instances'; 6 | import type { 7 | IAudioVelocityBase, 8 | IBindingValue, 9 | IConfigValue, 10 | IGameplay, 11 | IGlobal, 12 | IGlobalPrecise, 13 | IHitErrors, 14 | IKeyOverlay, 15 | ILeaderboard, 16 | IMP3Length, 17 | IMenu, 18 | IOffsets, 19 | IResultScreen, 20 | ISettingsPointers, 21 | ITourney, 22 | ITourneyChat, 23 | ITourneyUser, 24 | IUser, 25 | ScanPatterns 26 | } from '@/memory/types'; 27 | import type { ITourneyManagerChatItem } from '@/states/tourney'; 28 | import type { BindingsList, ConfigList } from '@/utils/settings.types'; 29 | 30 | export abstract class AbstractMemory> { 31 | abstract patterns: M; 32 | 33 | pid: number; 34 | process: Process; 35 | path: string = ''; 36 | 37 | game: AbstractInstance; 38 | 39 | private leaderStart: number = platform !== 'win32' ? 0xc : 0x8; 40 | 41 | constructor(process: Process, instance: AbstractInstance) { 42 | this.process = process; 43 | 44 | this.pid = process.id; 45 | this.path = process.path; 46 | 47 | this.game = instance; 48 | } 49 | 50 | abstract getScanPatterns(): ScanPatterns; 51 | abstract audioVelocityBase(): IAudioVelocityBase; 52 | abstract user(): IUser; 53 | abstract settingsPointers(): ISettingsPointers; 54 | abstract configOffsets(address: number, list: ConfigList): IOffsets; 55 | abstract bindingsOffsets(address: number, list: BindingsList): IOffsets; 56 | abstract configValue( 57 | address: number, 58 | position: number, 59 | list: ConfigList 60 | ): IConfigValue; 61 | 62 | abstract bindingValue(address: number, position: number): IBindingValue; 63 | abstract resultScreen(): IResultScreen; 64 | abstract gameplay(): IGameplay; 65 | abstract keyOverlay(mode: number): IKeyOverlay; 66 | abstract hitErrors(): IHitErrors; 67 | abstract global(): IGlobal; 68 | abstract globalPrecise(): IGlobalPrecise; 69 | abstract menu(previousChecksum: string): IMenu; 70 | abstract mp3Length(): IMP3Length; 71 | abstract tourney(): ITourney; 72 | abstract tourneyChat(messages: ITourneyManagerChatItem[]): ITourneyChat; 73 | abstract tourneyUser(): ITourneyUser; 74 | abstract leaderboard(): ILeaderboard; 75 | 76 | checkIsBasesValid(): boolean { 77 | Object.entries(this.patterns).map((entry) => 78 | wLogger.debug( 79 | ClientType[this.game.client], 80 | this.pid, 81 | 'checkIsBasesValid', 82 | `${entry[0]}: ${entry[1].toString(16).toUpperCase()}` 83 | ) 84 | ); 85 | return !Object.values(this.patterns).some((base) => base === 0); 86 | } 87 | 88 | setPattern(key: keyof M, val: M[keyof M]): boolean { 89 | this.patterns[key] = val; 90 | return true; 91 | } 92 | 93 | getPattern(key: keyof M) { 94 | return this.patterns[key]; 95 | } 96 | 97 | getPatterns( 98 | patterns: T 99 | ): Pick | never { 100 | return patterns.reduce( 101 | (acc, item: keyof Pick) => { 102 | acc[item] = this.patterns[item]; 103 | return acc; 104 | }, 105 | {} as Pick 106 | ); 107 | } 108 | 109 | getLeaderStart() { 110 | return this.leaderStart; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/tosu/src/memory/types.ts: -------------------------------------------------------------------------------- 1 | import { ITourneyManagerChatItem } from '@/states/tourney'; 2 | import { KeyOverlay, LeaderboardPlayer } from '@/states/types'; 3 | import { MultiplayerTeamType } from '@/utils/multiplayer.types'; 4 | import { CalculateMods } from '@/utils/osuMods.types'; 5 | 6 | export type ScanPatterns = { 7 | [k in keyof any]: { 8 | pattern: string; 9 | offset?: number; 10 | isTourneyOnly?: boolean; 11 | }; 12 | }; 13 | 14 | export type IAudioVelocityBase = number[] | string; 15 | 16 | export interface IUserProtected { 17 | name: string; 18 | accuracy: number; 19 | rankedScore: number; 20 | id: number; 21 | level: number; 22 | playCount: number; 23 | playMode: number; 24 | rank: number; 25 | countryCode: number; 26 | performancePoints: number; 27 | rawBanchoStatus: number; 28 | backgroundColour: number; 29 | rawLoginStatus: number; 30 | } 31 | 32 | export type IUser = Error | IUserProtected; 33 | 34 | export type ISettingsPointers = { config: number; binding: number } | Error; 35 | export type IOffsets = number[] | Error; 36 | 37 | export type IConfigValue = 38 | | { 39 | key: string; 40 | value: any; 41 | } 42 | | null 43 | | Error; 44 | 45 | export type IBindingValue = 46 | | { 47 | key: number; 48 | value: number; 49 | } 50 | | Error; 51 | 52 | export type IResultScreen = 53 | | { 54 | onlineId: number; 55 | playerName: string; 56 | mods: CalculateMods; 57 | mode: number; 58 | maxCombo: number; 59 | score: number; 60 | hit100: number; 61 | hit300: number; 62 | hit50: number; 63 | hitGeki: number; 64 | hitKatu: number; 65 | hitMiss: number; 66 | sliderEndHits: number; 67 | smallTickHits: number; 68 | largeTickHits: number; 69 | date: string; 70 | } 71 | | string 72 | | Error; 73 | 74 | export type IScore = { 75 | retries: number; 76 | playerName: string; 77 | mods: CalculateMods; 78 | mode: number; 79 | score: number; 80 | playerHPSmooth: number; 81 | playerHP: number; 82 | accuracy: number; 83 | hit100: number; 84 | hit300: number; 85 | hit50: number; 86 | hitGeki: number; 87 | hitKatu: number; 88 | hitMiss: number; 89 | sliderEndHits: number; 90 | smallTickHits: number; 91 | largeTickHits: number; 92 | combo: number; 93 | maxCombo: number; 94 | pp?: number; 95 | }; 96 | 97 | export type IGameplay = IScore | string | Error; 98 | 99 | export type IKeyOverlay = KeyOverlay | string | Error; 100 | export type IHitErrors = number[] | string | Error; 101 | 102 | export type IGlobal = 103 | | { 104 | isWatchingReplay: boolean; 105 | isReplayUiHidden: boolean; 106 | isMultiSpectating: boolean; 107 | 108 | showInterface: boolean; 109 | chatStatus: number; 110 | status: number; 111 | 112 | gameTime: number; 113 | menuMods: CalculateMods; 114 | 115 | skinFolder: string; 116 | memorySongsFolder: string; 117 | } 118 | | string 119 | | Error; 120 | 121 | export type IGlobalPrecise = { time: number } | Error; 122 | 123 | export type IMenu = 124 | | { 125 | type: 'update'; 126 | gamemode: number; 127 | checksum: string; 128 | filename: string; 129 | plays: number; 130 | artist: string; 131 | artistOriginal: string; 132 | title: string; 133 | titleOriginal: string; 134 | ar: number; 135 | cs: number; 136 | hp: number; 137 | od: number; 138 | audioFilename: string; 139 | backgroundFilename: string; 140 | folder: string; 141 | creator: string; 142 | difficulty: string; 143 | mapID: number; 144 | setID: number; 145 | rankedStatus: number; 146 | objectCount: number; 147 | } 148 | | { 149 | type: 'checksum'; 150 | gamemode: number; 151 | rankedStatus: number; 152 | } 153 | | string 154 | | Error; 155 | 156 | export type IMP3Length = number | Error; 157 | 158 | export type ITourney = 159 | | { 160 | ipcState: number; 161 | leftStars: number; 162 | rightStars: number; 163 | bestOf: number; 164 | starsVisible: boolean; 165 | scoreVisible: boolean; 166 | firstTeamName: string; 167 | secondTeamName: string; 168 | firstTeamScore: number; 169 | secondTeamScore: number; 170 | } 171 | | string 172 | | Error; 173 | 174 | export type ITourneyChat = ITourneyManagerChatItem[] | Error | boolean; 175 | 176 | export type ITourneyUser = 177 | | { 178 | id: number; 179 | name: string; 180 | country: string; 181 | accuracy: number; 182 | playcount: number; 183 | rankedScore: number; 184 | globalRank: number; 185 | pp: number; 186 | } 187 | | string 188 | | Error; 189 | 190 | export type ILeaderboard = 191 | | [boolean, LeaderboardPlayer | undefined, LeaderboardPlayer[]] 192 | | Error; 193 | 194 | export interface ILazerSpectatorEntry { 195 | team: MultiplayerTeamType; 196 | user: IUser; 197 | resultScreen: IResultScreen | undefined; 198 | score: IScore | undefined; 199 | } 200 | 201 | export type ILazerSpectator = 202 | | { 203 | chat: ITourneyManagerChatItem[]; 204 | spectatingClients: ILazerSpectatorEntry[]; 205 | } 206 | | undefined; 207 | -------------------------------------------------------------------------------- /packages/tosu/src/postBuild.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { load } from 'resedit/cjs'; 4 | import semverParse from 'semver/functions/parse'; 5 | 6 | async function windowsPostBuild(output) { 7 | const packageVersion = require(path.join(process.cwd(), '_version.js')); 8 | 9 | const ResEdit = await load(); 10 | const exe = ResEdit.NtExecutable.from(fs.readFileSync(output)); 11 | const res = ResEdit.NtExecutableResource.from(exe); 12 | const iconFile = ResEdit.Data.IconFile.from( 13 | fs.readFileSync(path.join(__dirname, 'assets', 'icon.ico')) 14 | ); 15 | 16 | ResEdit.Resource.IconGroupEntry.replaceIconsForResource( 17 | res.entries, 18 | 1, 19 | 1033, 20 | iconFile.icons.map((item) => item.data) 21 | ); 22 | 23 | const vi = ResEdit.Resource.VersionInfo.fromEntries(res.entries)[0]; 24 | const semanticTosu = semverParse(packageVersion); 25 | 26 | const tosuVersion = { 27 | major: semanticTosu?.major || 0, 28 | minor: semanticTosu?.minor || 0, 29 | patch: semanticTosu?.patch || 0 30 | }; 31 | 32 | vi.setStringValues( 33 | { lang: 1033, codepage: 1200 }, 34 | { 35 | ProductName: 'tosu', 36 | FileDescription: 'osu! memory reader, built in typescript', 37 | CompanyName: 'KotRik', 38 | LegalCopyright: '© KotRik. All rights reserved.' 39 | } 40 | ); 41 | vi.setFileVersion( 42 | tosuVersion.major, 43 | tosuVersion.minor, 44 | tosuVersion.patch, 45 | 0, 46 | 1033 47 | ); 48 | vi.setProductVersion( 49 | tosuVersion.major, 50 | tosuVersion.minor, 51 | tosuVersion.patch, 52 | 0, 53 | 1033 54 | ); 55 | vi.outputToResourceEntries(res.entries); 56 | res.outputResource(exe); 57 | fs.writeFileSync(output, Buffer.from(exe.generate())); 58 | } 59 | 60 | if (process.platform === 'win32') { 61 | windowsPostBuild(path.join(__dirname, '../', './dist/tosu.exe')); 62 | } 63 | -------------------------------------------------------------------------------- /packages/tosu/src/states/bassDensity.ts: -------------------------------------------------------------------------------- 1 | import { ClientType, wLogger } from '@tosu/common'; 2 | 3 | import { AbstractState } from '@/states'; 4 | 5 | // yep each dto should have class! 6 | export class BassDensity extends AbstractState { 7 | currentAudioVelocity: number = 0.0; 8 | density: number = 0.0; 9 | 10 | updateState() { 11 | try { 12 | const audioVelocityBase = this.game.memory.audioVelocityBase(); 13 | if (typeof audioVelocityBase === 'string') { 14 | wLogger.debug( 15 | ClientType[this.game.client], 16 | this.game.pid, 17 | 'resolvePatterns', 18 | audioVelocityBase 19 | ); 20 | return; 21 | } 22 | 23 | let bass = 0.0; 24 | let currentAudioVelocity = this.currentAudioVelocity; 25 | for (let i = 0; i < 40; i++) { 26 | const value = audioVelocityBase[i]; 27 | if (value < 0) { 28 | this.density = 0.5; 29 | return; 30 | } 31 | bass += (2 * value * (40 - i)) / 40; 32 | } 33 | 34 | if (isNaN(currentAudioVelocity) || isNaN(bass)) { 35 | this.currentAudioVelocity = 0; 36 | this.density = 0.5; 37 | return; 38 | } 39 | currentAudioVelocity = Math.max( 40 | currentAudioVelocity, 41 | Math.min(bass * 1.5, 6) 42 | ); 43 | currentAudioVelocity *= 0.95; 44 | 45 | this.currentAudioVelocity = currentAudioVelocity; 46 | this.density = (1 + currentAudioVelocity) * 0.5; 47 | 48 | this.resetReportCount('BassDensity updateState'); 49 | } catch (exc) { 50 | this.reportError( 51 | 'BassDensity updateState', 52 | 10, 53 | ClientType[this.game.client], 54 | this.game.pid, 55 | 'BassDensity updateState', 56 | (exc as Error).message 57 | ); 58 | wLogger.debug( 59 | ClientType[this.game.client], 60 | this.game.pid, 61 | 'BassDensity updateState', 62 | exc 63 | ); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/tosu/src/states/global.ts: -------------------------------------------------------------------------------- 1 | import { ClientType, wLogger } from '@tosu/common'; 2 | 3 | import { AbstractState } from '@/states'; 4 | import { cleanPath } from '@/utils/converters'; 5 | import { defaultCalculatedMods } from '@/utils/osuMods'; 6 | import { CalculateMods } from '@/utils/osuMods.types'; 7 | 8 | export class Global extends AbstractState { 9 | isWatchingReplay: boolean = false; 10 | isReplayUiHidden: boolean = false; 11 | isMultiSpectating: boolean = false; 12 | showInterface: boolean = false; 13 | 14 | chatStatus: number = 0; 15 | status: number = 0; 16 | 17 | gameTime: number = 0; 18 | playTime: number = 0; 19 | menuMods: CalculateMods = Object.assign({}, defaultCalculatedMods); 20 | 21 | gameFolder: string = ''; 22 | skinFolder: string = ''; 23 | songsFolder: string = ''; 24 | memorySongsFolder: string = ''; 25 | 26 | setGameFolder(value: string) { 27 | if (typeof value !== 'string') return; 28 | 29 | this.gameFolder = value; 30 | } 31 | 32 | setSongsFolder(value: string) { 33 | if (typeof value !== 'string') return; 34 | 35 | this.songsFolder = value; 36 | } 37 | 38 | updateState() { 39 | try { 40 | const result = this.game.memory.global(); 41 | if (result instanceof Error) throw result; 42 | if (typeof result === 'string') { 43 | if (result === '') return; 44 | 45 | wLogger.debug( 46 | ClientType[this.game.client], 47 | this.game.pid, 48 | `global updateState`, 49 | result 50 | ); 51 | 52 | return 'not-ready'; 53 | } 54 | 55 | this.isWatchingReplay = result.isWatchingReplay; 56 | this.isReplayUiHidden = result.isReplayUiHidden; 57 | this.isMultiSpectating = result.isMultiSpectating; 58 | 59 | this.showInterface = result.showInterface; 60 | this.chatStatus = result.chatStatus; 61 | this.status = result.status; 62 | 63 | this.gameTime = result.gameTime; 64 | this.menuMods = result.menuMods; 65 | 66 | this.skinFolder = cleanPath(result.skinFolder); 67 | this.memorySongsFolder = cleanPath(result.memorySongsFolder); 68 | 69 | this.resetReportCount('global updateState'); 70 | } catch (exc) { 71 | this.reportError( 72 | 'global updateState', 73 | 10, 74 | ClientType[this.game.client], 75 | this.game.pid, 76 | `global updateState`, 77 | (exc as any).message 78 | ); 79 | wLogger.debug( 80 | ClientType[this.game.client], 81 | this.game.pid, 82 | `global updateState`, 83 | exc 84 | ); 85 | } 86 | } 87 | 88 | updatePreciseState() { 89 | try { 90 | const result = this.game.memory.globalPrecise(); 91 | if (result instanceof Error) throw result; 92 | 93 | this.playTime = result.time; 94 | 95 | this.resetReportCount('global updatePreciseState'); 96 | } catch (exc) { 97 | this.reportError( 98 | 'global updatePreciseState', 99 | 10, 100 | ClientType[this.game.client], 101 | this.game.pid, 102 | `global updatePreciseState`, 103 | (exc as any).message 104 | ); 105 | wLogger.debug( 106 | ClientType[this.game.client], 107 | this.game.pid, 108 | `global updatePreciseState`, 109 | exc 110 | ); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/tosu/src/states/index.ts: -------------------------------------------------------------------------------- 1 | import { wLogger } from '@tosu/common'; 2 | 3 | import { AbstractInstance } from '@/instances'; 4 | 5 | export type ReportError = ( 6 | id: string | number, 7 | maxAmount: number, 8 | ...args: any[] 9 | ) => void; 10 | export type ResetReportCount = (id: string | number) => void; 11 | 12 | export abstract class AbstractState { 13 | errorsCount: { [key: string | number]: number } = {}; 14 | game: AbstractInstance; 15 | 16 | constructor(game: AbstractInstance) { 17 | this.game = game; 18 | } 19 | 20 | updateState() { 21 | throw Error('Error: updateState not implemented'); 22 | } 23 | 24 | reportError(id: string | number, maxAmount: number, ...args: any[]) { 25 | this.errorsCount[id] = (this.errorsCount[id] || 0) + 1; 26 | 27 | if (this.errorsCount[id] <= maxAmount) { 28 | wLogger.debugError(...args); 29 | return; 30 | } 31 | 32 | wLogger.error(...args); 33 | } 34 | 35 | resetReportCount(id: string | number) { 36 | this.errorsCount[id] = 0; 37 | } 38 | 39 | preventThrow(callback) { 40 | try { 41 | const result = callback(); 42 | return result; 43 | } catch (error) { 44 | return error as Error; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/tosu/src/states/lazerMultiSpectating.ts: -------------------------------------------------------------------------------- 1 | import { ClientType, wLogger } from '@tosu/common'; 2 | 3 | import { type LazerInstance } from '@/instances/lazerInstance'; 4 | import { ILazerSpectator } from '@/memory/types'; 5 | import { AbstractState } from '@/states'; 6 | 7 | export class LazerMultiSpectating extends AbstractState { 8 | lazerSpectatingData: ILazerSpectator; 9 | 10 | updateState() { 11 | try { 12 | if (this.game.client !== ClientType.lazer) { 13 | throw new Error( 14 | 'lazer multi spectating is not available for stable' 15 | ); 16 | } 17 | 18 | this.lazerSpectatingData = ( 19 | this.game as LazerInstance 20 | ).memory.readSpectatingData(); 21 | 22 | this.resetReportCount('lazerMultiSpectating updateState'); 23 | } catch (exc) { 24 | this.reportError( 25 | 'lazerMultiSpectating updateState', 26 | 10, 27 | ClientType[this.game.client], 28 | this.game.pid, 29 | `lazerMultiSpectating updateState`, 30 | (exc as any).message 31 | ); 32 | wLogger.debug( 33 | ClientType[this.game.client], 34 | this.game.pid, 35 | `lazerMultiSpectating updateState`, 36 | exc 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/tosu/src/states/menu.ts: -------------------------------------------------------------------------------- 1 | import { ClientType, wLogger } from '@tosu/common'; 2 | 3 | import { AbstractState } from '@/states'; 4 | import { cleanPath } from '@/utils/converters'; 5 | 6 | export class Menu extends AbstractState { 7 | gamemode: number; 8 | plays: number; 9 | artist: string; 10 | artistOriginal: string; 11 | title: string; 12 | titleOriginal: string; 13 | ar: number; 14 | cs: number; 15 | hp: number; 16 | od: number; 17 | audioFilename: string; 18 | backgroundFilename: string; 19 | folder: string; 20 | creator: string; 21 | filename: string; 22 | difficulty: string; 23 | mapID: number; 24 | setID: number; 25 | rankedStatus: number; 26 | checksum: string; 27 | objectCount: number; 28 | mp3Length: number; 29 | 30 | previousMD5: string = ''; 31 | pendingChecksum: string = ''; 32 | mapChangeTime: number = 0; 33 | 34 | updateState() { 35 | try { 36 | const result = this.game.memory.menu(this.previousMD5); 37 | if (result instanceof Error) throw result; 38 | if (typeof result === 'string') { 39 | if (result === '') return; 40 | 41 | wLogger.debug( 42 | ClientType[this.game.client], 43 | this.game.pid, 44 | `menu updateState`, 45 | result 46 | ); 47 | return 'not-ready'; 48 | } 49 | if (result.type === 'checksum') { 50 | // update gamemoe in menu, even if beatmap is the same 51 | this.gamemode = result.gamemode; 52 | this.rankedStatus = result.rankedStatus; 53 | return; 54 | } 55 | 56 | if ( 57 | this.pendingChecksum !== result.checksum && 58 | (this.game.isTourneySpectator || this.game.isTourneyManager) 59 | ) { 60 | this.mapChangeTime = performance.now(); 61 | this.pendingChecksum = result.checksum; 62 | 63 | return; 64 | } 65 | 66 | // delay in milliseconds. 500ms has been enough to eliminate spurious map ID changes in the tournament client 67 | // over two weeks of testing at 4WC 68 | if ( 69 | performance.now() - this.mapChangeTime < 500 && 70 | (this.game.isTourneySpectator || this.game.isTourneyManager) 71 | ) { 72 | return; 73 | } 74 | 75 | this.gamemode = result.gamemode; 76 | 77 | // MD5 hasn't changed in over NEW_MAP_COMMIT_DELAY, commit to new map 78 | this.checksum = result.checksum; 79 | this.filename = cleanPath(result.filename); 80 | 81 | this.plays = result.plays; 82 | this.artist = result.artist; 83 | this.artistOriginal = result.artistOriginal; 84 | this.title = result.title; 85 | this.titleOriginal = result.titleOriginal; 86 | 87 | this.ar = result.ar; 88 | this.cs = result.cs; 89 | this.hp = result.hp; 90 | this.od = result.od; 91 | this.audioFilename = cleanPath(result.audioFilename); 92 | this.backgroundFilename = cleanPath(result.backgroundFilename); 93 | this.folder = cleanPath(result.folder); 94 | this.creator = result.creator; 95 | this.difficulty = result.difficulty; 96 | this.mapID = result.mapID; 97 | this.setID = result.setID; 98 | this.rankedStatus = result.rankedStatus; 99 | this.objectCount = result.objectCount; 100 | 101 | this.previousMD5 = this.checksum; 102 | 103 | this.resetReportCount('menu updateState'); 104 | } catch (exc) { 105 | this.reportError( 106 | 'menu updateState', 107 | 10, 108 | ClientType[this.game.client], 109 | this.game.pid, 110 | `menu updateState`, 111 | (exc as any).message 112 | ); 113 | wLogger.debug( 114 | ClientType[this.game.client], 115 | this.game.pid, 116 | `menu updateState`, 117 | exc 118 | ); 119 | } 120 | } 121 | 122 | updateMP3Length() { 123 | try { 124 | const result = this.game.memory.mp3Length(); 125 | if (result instanceof Error) throw result; 126 | if (typeof result === 'string') { 127 | if (result === '') return; 128 | 129 | wLogger.debug( 130 | ClientType[this.game.client], 131 | this.game.pid, 132 | `menu updateMP3Length`, 133 | result 134 | ); 135 | 136 | return 'not-ready'; 137 | } 138 | 139 | this.mp3Length = result; 140 | this.resetReportCount('menu updateMP3Length'); 141 | } catch (exc) { 142 | this.reportError( 143 | 'menu updateMP3Length', 144 | 10, 145 | ClientType[this.game.client], 146 | this.game.pid, 147 | `menu updateMP3Length`, 148 | (exc as any).message 149 | ); 150 | wLogger.debug( 151 | ClientType[this.game.client], 152 | this.game.pid, 153 | `menu updateMP3Length`, 154 | exc 155 | ); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /packages/tosu/src/states/tourney.ts: -------------------------------------------------------------------------------- 1 | import { ClientType, wLogger } from '@tosu/common'; 2 | 3 | import { AbstractState } from '@/states'; 4 | 5 | export type ITourneyManagerChatItem = { 6 | time: string; 7 | name: string; 8 | content: string; 9 | }; 10 | 11 | export class TourneyManager extends AbstractState { 12 | isDefaultState: boolean = true; 13 | 14 | ChatAreaAddr: number = 0; 15 | 16 | ipcState: number = 0; 17 | leftStars: number = 0; 18 | rightStars: number = 0; 19 | bestOf: number = 0; 20 | starsVisible: boolean = false; 21 | scoreVisible: boolean = false; 22 | firstTeamName: string = ''; 23 | secondTeamName: string = ''; 24 | firstTeamScore: number = 0; 25 | secondTeamScore: number = 0; 26 | 27 | messages: ITourneyManagerChatItem[] = []; 28 | 29 | userAccuracy: number = 0.0; 30 | userRankedScore: number = 0; 31 | userPlayCount: number = 0; 32 | userGlobalRank: number = 0; 33 | userPP: number = 0; 34 | userName: string = ''; 35 | userCountry: string = ''; 36 | userID: number = 0; 37 | 38 | reset() { 39 | if (this.isDefaultState) { 40 | return; 41 | } 42 | 43 | this.isDefaultState = true; 44 | this.userAccuracy = 0.0; 45 | this.userRankedScore = 0; 46 | this.userPlayCount = 0; 47 | this.userGlobalRank = 0; 48 | this.userPP = 0; 49 | this.userName = ''; 50 | this.userCountry = ''; 51 | this.userID = 0; 52 | } 53 | 54 | updateState() { 55 | try { 56 | wLogger.debug( 57 | ClientType[this.game.client], 58 | this.game.pid, 59 | `tourney updateState starting` 60 | ); 61 | 62 | const result = this.game.memory.tourney(); 63 | if (result instanceof Error) throw result; 64 | if (typeof result === 'string') { 65 | wLogger.debug( 66 | ClientType[this.game.client], 67 | this.game.pid, 68 | `tourney updateState`, 69 | result 70 | ); 71 | return 'not-ready'; 72 | } 73 | 74 | this.ipcState = result.ipcState; 75 | this.leftStars = result.leftStars; 76 | this.rightStars = result.rightStars; 77 | this.bestOf = result.bestOf; 78 | this.starsVisible = result.starsVisible; 79 | this.scoreVisible = result.scoreVisible; 80 | this.firstTeamName = result.firstTeamName; 81 | this.secondTeamName = result.secondTeamName; 82 | this.firstTeamScore = result.firstTeamScore; 83 | this.secondTeamScore = result.secondTeamScore; 84 | 85 | const messages = this.game.memory.tourneyChat(this.messages); 86 | if (messages instanceof Error) throw Error; 87 | if (!Array.isArray(messages)) return; 88 | 89 | this.messages = messages; 90 | 91 | this.resetReportCount('tourney updateState'); 92 | } catch (exc) { 93 | this.reportError( 94 | 'tourney updateState', 95 | 10, 96 | ClientType[this.game.client], 97 | this.game.pid, 98 | `tourney updateState`, 99 | (exc as any).message 100 | ); 101 | wLogger.debug( 102 | ClientType[this.game.client], 103 | this.game.pid, 104 | `tourney updateState`, 105 | exc 106 | ); 107 | } 108 | } 109 | 110 | updateUser() { 111 | try { 112 | const gameplay = this.game.get('gameplay'); 113 | if (gameplay === null) { 114 | return; 115 | } 116 | 117 | const result = this.game.memory.tourneyUser(); 118 | if (result instanceof Error) throw result; 119 | if (typeof result === 'string') { 120 | wLogger.debug( 121 | ClientType[this.game.client], 122 | this.game.pid, 123 | `tourney updateUser`, 124 | result 125 | ); 126 | this.reset(); 127 | 128 | if (gameplay.isDefaultState === true) return; 129 | gameplay.init(undefined, 'tourney'); 130 | return; 131 | } 132 | 133 | this.userAccuracy = result.accuracy; 134 | this.userRankedScore = result.rankedScore; 135 | this.userPlayCount = result.playcount; 136 | this.userPP = result.pp; 137 | this.userName = result.name; 138 | this.userCountry = result.country; 139 | this.userID = result.id; 140 | 141 | this.isDefaultState = false; 142 | 143 | this.resetReportCount('tourney updateUser'); 144 | } catch (exc) { 145 | this.reportError( 146 | 'tourney updateUser', 147 | 10, 148 | ClientType[this.game.client], 149 | this.game.pid, 150 | `tourney updateUser`, 151 | (exc as any).message 152 | ); 153 | wLogger.debug( 154 | ClientType[this.game.client], 155 | this.game.pid, 156 | `tourney updateUser`, 157 | exc 158 | ); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /packages/tosu/src/states/types.ts: -------------------------------------------------------------------------------- 1 | import { CalculateMods } from '@/utils/osuMods.types'; 2 | 3 | export interface Statistics { 4 | miss: number; 5 | meh: number; 6 | ok: number; 7 | good: number; 8 | great: number; 9 | perfect: number; 10 | smallTickMiss: number; 11 | smallTickHit: number; 12 | largeTickMiss: number; 13 | largeTickHit: number; 14 | smallBonus: number; 15 | largeBonus: number; 16 | ignoreMiss: number; 17 | ignoreHit: number; 18 | comboBreak: number; 19 | sliderTailHit: number; 20 | legacyComboIncrease: number; 21 | } 22 | 23 | export interface KeyOverlay { 24 | K1Pressed: boolean; 25 | K1Count: number; 26 | K2Pressed: boolean; 27 | K2Count: number; 28 | M1Pressed: boolean; 29 | M1Count: number; 30 | M2Pressed: boolean; 31 | M2Count: number; 32 | } 33 | 34 | export interface LeaderboardPlayer { 35 | userId: number; 36 | name: string; 37 | score: number; 38 | combo: number; 39 | maxCombo: number; 40 | mods: CalculateMods; 41 | h300: number; 42 | h100: number; 43 | h50: number; 44 | h0: number; 45 | team: number; 46 | position: number; 47 | isPassing: boolean; 48 | } 49 | -------------------------------------------------------------------------------- /packages/tosu/src/states/user.ts: -------------------------------------------------------------------------------- 1 | import { ClientType, wLogger } from '@tosu/common'; 2 | 3 | import { AbstractState } from '@/states'; 4 | 5 | export class User extends AbstractState { 6 | name: string; 7 | accuracy: number; 8 | rankedScore: number; 9 | id: number; 10 | level: number; 11 | playCount: number; 12 | playMode: number; 13 | rank: number; 14 | countryCode: number; 15 | performancePoints: number; 16 | rawBanchoStatus: number; 17 | backgroundColour: number; 18 | rawLoginStatus: number; 19 | 20 | updateState() { 21 | try { 22 | const profile = this.game.memory.user(); 23 | if (profile instanceof Error) throw profile; 24 | 25 | this.name = profile.name; 26 | this.accuracy = profile.accuracy; 27 | this.rankedScore = profile.rankedScore; 28 | this.id = profile.id; 29 | this.level = profile.level; 30 | this.playCount = profile.playCount; 31 | this.playMode = profile.playMode; 32 | this.rank = profile.rank; 33 | this.countryCode = profile.countryCode; 34 | this.performancePoints = profile.performancePoints; 35 | this.rawBanchoStatus = profile.rawBanchoStatus; 36 | this.backgroundColour = profile.backgroundColour; 37 | this.rawLoginStatus = profile.rawLoginStatus; 38 | 39 | this.resetReportCount('user updateState'); 40 | } catch (exc) { 41 | this.reportError( 42 | 'user updateState', 43 | 10, 44 | ClientType[this.game.client], 45 | this.game.pid, 46 | `user updateState`, 47 | (exc as any).message 48 | ); 49 | wLogger.debug( 50 | ClientType[this.game.client], 51 | this.game.pid, 52 | `user updateState`, 53 | exc 54 | ); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/tosu/src/utils/bindings.ts: -------------------------------------------------------------------------------- 1 | export enum Bindings { 2 | None, 3 | _Standard, 4 | OsuLeft, 5 | OsuRight, 6 | OsuSmoke, 7 | _TaikoMod, 8 | TaikoInnerLeft, 9 | TaikoInnerRight, 10 | TaikoOuterLeft, 11 | TaikoOuterRight, 12 | _CatchTheBeatMod, 13 | FruitsLeft, 14 | FruitsRight, 15 | FruitsDash, 16 | _ManiaMod, 17 | IncreaseSpeed, 18 | DecreaseSpeed, 19 | _InGame, 20 | Pause, 21 | Skip, 22 | ToggleScoreboard, 23 | IncreaseAudioOffset, 24 | DecreaseAudioOffset, 25 | QuickRetry, 26 | _General, 27 | ToggleFrameLimiter, 28 | ToggleChat, 29 | ToggleExtendedChat, 30 | Screenshot, 31 | VolumeIncrease, 32 | VolumeDecrease, 33 | DisableMouseButtons, 34 | BossKey, 35 | _Editor, 36 | SelectTool, 37 | NormalTool, 38 | SliderTool, 39 | SpinnerTool, 40 | NewComboToggle, 41 | WhistleToggle, 42 | FinishToggle, 43 | ClapToggle, 44 | GridSnapToggle, 45 | DistSnapToggle, 46 | NoteLockToggle, 47 | NudgeLeft, 48 | NudgeRight, 49 | HelpToggle, 50 | JumpToBegin, 51 | PlayFromBegin, 52 | AudioPause, 53 | JumpToEnd, 54 | GridChange, 55 | TimingSection, 56 | InheritingSection, 57 | RemoveSection, 58 | _ModSelect, 59 | Easy, 60 | NoFail, 61 | HalfTime, 62 | HardRock, 63 | SuddenDeath, 64 | DoubleTime, 65 | Hidden, 66 | Flashlight, 67 | Relax, 68 | Autopilot, 69 | SpunOut, 70 | Auto, 71 | ScoreV2 72 | } 73 | 74 | export enum VirtualKeyCode { 75 | A = 65, 76 | Add = 107, 77 | Apps = 93, 78 | Attn = 246, 79 | B = 66, 80 | Back = 8, 81 | BrowserBack = 166, 82 | BrowserFavorites = 171, 83 | BrowserForward = 167, 84 | BrowserHome = 172, 85 | BrowserRefresh = 168, 86 | BrowserSearch = 170, 87 | BrowserStop = 169, 88 | C = 67, 89 | CapsLock = 20, 90 | Crsel = 247, 91 | D = 68, 92 | D0 = 48, 93 | D1, 94 | D2, 95 | D3, 96 | D4, 97 | D5, 98 | D6, 99 | D7, 100 | D8, 101 | D9, 102 | '.' = 110, 103 | Delete = 46, 104 | '/' = 111, 105 | Down = 40, 106 | E = 69, 107 | End = 35, 108 | Enter = 13, 109 | EraseEof = 249, 110 | Escape = 27, 111 | Execute = 43, 112 | Exsel = 248, 113 | F = 70, 114 | F1 = 112, 115 | F10 = 121, 116 | F11, 117 | F12, 118 | F13, 119 | F14, 120 | F15, 121 | F16, 122 | F17, 123 | F18, 124 | F19, 125 | F2 = 113, 126 | F20 = 131, 127 | F21, 128 | F22, 129 | F23, 130 | F24, 131 | F3 = 114, 132 | F4, 133 | F5, 134 | F6, 135 | F7, 136 | F8, 137 | F9, 138 | G = 71, 139 | H, 140 | Help = 47, 141 | Home = 36, 142 | I = 73, 143 | Insert = 45, 144 | J = 74, 145 | K, 146 | L, 147 | LaunchApplication1 = 182, 148 | LaunchApplication2, 149 | LaunchMail = 180, 150 | LeftControl = 162, 151 | Left = 37, 152 | LeftAlt = 164, 153 | LeftShift = 160, 154 | LeftWindows = 91, 155 | M = 77, 156 | MediaNextTrack = 176, 157 | MediaPlayPause = 179, 158 | MediaPreviousTrack = 177, 159 | MediaStop, 160 | Multiply = 106, 161 | N = 78, 162 | None = 0, 163 | NumLock = 144, 164 | NumPad0 = 96, 165 | NumPad1, 166 | NumPad2, 167 | NumPad3, 168 | NumPad4, 169 | NumPad5, 170 | NumPad6, 171 | NumPad7, 172 | NumPad8, 173 | NumPad9, 174 | O = 79, 175 | ';' = 186, 176 | '\\' = 226, 177 | '?' = 191, 178 | '~', 179 | '[' = 219, 180 | '|', 181 | ']', 182 | '"', 183 | Oem8, 184 | OemClear = 254, 185 | ',' = 188, 186 | '-', 187 | '. ', 188 | '+' = 187, 189 | P = 80, 190 | Pa1 = 253, 191 | PageDown = 34, 192 | PageUp = 33, 193 | Play = 250, 194 | Print = 42, 195 | PrintScreen = 44, 196 | ProcessKey = 229, 197 | Q = 81, 198 | R, 199 | RightControl = 163, 200 | Right = 39, 201 | RightAlt = 165, 202 | RightShift = 161, 203 | RightWindows = 92, 204 | S = 83, 205 | Scroll = 145, 206 | Select = 41, 207 | SelectMedia = 181, 208 | Separator = 108, 209 | Sleep = 95, 210 | Space = 32, 211 | Subtract = 109, 212 | T = 84, 213 | Tab = 9, 214 | U = 85, 215 | Up = 38, 216 | V = 86, 217 | VolumeDown = 174, 218 | VolumeMute = 173, 219 | VolumeUp = 175, 220 | W = 87, 221 | X, 222 | Y, 223 | Z, 224 | Zoom = 251 225 | } 226 | -------------------------------------------------------------------------------- /packages/tosu/src/utils/converters.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | const DOUBLE_POWERS_10 = [ 4 | 1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 5 | 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, 1e23, 1e24, 1e25, 6 | 1e26, 1e27, 1e28, 1e29, 1e30, 1e31, 1e32, 1e33, 1e34, 1e35, 1e36, 1e37, 7 | 1e38, 1e39, 1e40, 1e41, 1e42, 1e43, 1e44, 1e45, 1e46, 1e47, 1e48, 1e49, 8 | 1e50, 1e51, 1e52, 1e53, 1e54, 1e55, 1e56, 1e57, 1e58, 1e59, 1e60, 1e61, 9 | 1e62, 1e63, 1e64, 1e65, 1e66, 1e67, 1e68, 1e69, 1e70, 1e71, 1e72, 1e73, 10 | 1e74, 1e75, 1e76, 1e77, 1e78, 1e79, 1e80 11 | ]; 12 | 13 | /** 14 | * Simple function to convert 4.123123132131 -> 4.13 15 | * 16 | * @param decimalNumber number with big amount of numbers after dot 17 | * @returns float number with two digits after 18 | */ 19 | export const fixDecimals = (decimalNumber: number) => 20 | parseFloat((decimalNumber || 0).toFixed(2)); 21 | 22 | /** 23 | * Converts raw .NET's dateData to Date object 24 | * 25 | * @param {number} dateDataHi Raw .NET internal ticks high-order number 26 | * @param {number} dateDataLo Raw .NET internal ticks low-order number 27 | * @returns {Date} Local timezone Date 28 | */ 29 | export const netDateBinaryToDate = ( 30 | dateDataHi: number, 31 | dateDataLo: number 32 | ): Date => { 33 | const buffer = Buffer.alloc(8); 34 | 35 | const ticksMask = 0x3fffffff; 36 | 37 | dateDataHi &= ticksMask; 38 | 39 | buffer.writeInt32LE(dateDataLo); 40 | buffer.writeInt32LE(dateDataHi, 4); 41 | 42 | const dateData = buffer.readBigInt64LE(); 43 | 44 | const ticksPerMillisecond = 10000n; 45 | const epochTicks = 621355968000000000n; 46 | const milliseconds = (dateData - epochTicks) / ticksPerMillisecond; 47 | 48 | return new Date(Number(milliseconds)); 49 | }; 50 | 51 | /** 52 | * Converts .NET's decimal to Number 53 | * 54 | * @param {number} lo64 64-bit low-order number of decimal 55 | * @param {number} hi32 32-bit high-order number of decimal 56 | * @param {number} flags Flags indicating scale and sign of decimal 57 | * @returns {number} double-precision number 58 | */ 59 | export const numberFromDecimal = ( 60 | lo64: number, 61 | hi32: number, 62 | flags: number 63 | ): number => { 64 | if (lo64 === 0 && hi32 === 0 && flags === 0) { 65 | return 0; 66 | } 67 | 68 | const isNegative = (flags >> 24) & 0xff; 69 | const scale = (flags >> 16) & 0xff; 70 | 71 | const ds2to64 = 1.8446744073709552e19; 72 | 73 | let value = (lo64 + hi32 * ds2to64) / DOUBLE_POWERS_10[scale]; 74 | 75 | if (isNegative) { 76 | value = -value; 77 | } 78 | 79 | return value; 80 | }; 81 | 82 | /** 83 | * Joins multiple paths, sanitizing each parameter (like invalid windows characters, trailing spaces, etc.) before joining. 84 | * 85 | * @param {...string[]} paths Paths to sanitize and join. 86 | * @returns {string} The joined & sanitized path. 87 | */ 88 | export const cleanPath = (...paths: string[]): string => { 89 | paths.map((p) => { 90 | // Ensure UTF-8 encoding and trim whitespace 91 | let cleaned = Buffer.from(p.trim(), 'utf-8').toString('utf-8'); 92 | 93 | // Replace invalid OS-specific characters 94 | cleaned = cleaned.replace( 95 | process.platform === 'win32' ? /[<>:"/\\|?*]/g : /\//g, 96 | '' 97 | ); 98 | 99 | // On Windows, trim trailing dots and spaces 100 | if (process.platform === 'win32') 101 | cleaned = cleaned.replace(/[ .]+$/, ''); 102 | 103 | return cleaned; 104 | }); 105 | 106 | return join(...paths); 107 | }; 108 | -------------------------------------------------------------------------------- /packages/tosu/src/utils/multiplayer.types.ts: -------------------------------------------------------------------------------- 1 | export enum MultiplayerUserState { 2 | Idle, 3 | Ready, 4 | WaitingForLoad, 5 | Loaded, 6 | ReadyForGameplay, 7 | Playing, 8 | FinishedPlay, 9 | Results, 10 | Spectating 11 | } 12 | 13 | export type MultiplayerTeamType = 'red' | 'blue' | 'none'; 14 | -------------------------------------------------------------------------------- /packages/tosu/src/utils/settings.types.ts: -------------------------------------------------------------------------------- 1 | export type ConfigList = Record; 2 | export type BindingsList = Record; 3 | 4 | export interface IBindable { 5 | setValue: (value: any) => void; 6 | } 7 | 8 | export interface IConfigBindable extends IBindable { 9 | type: 'bool' | 'byte' | 'int' | 'double' | 'string' | 'bstring' | 'enum'; 10 | } 11 | 12 | export interface Keybinds { 13 | osu: KeybindsOsu; 14 | fruits: KeybindsFruits; 15 | taiko: KeybindsTaiko; 16 | quickRetry: string; 17 | } 18 | 19 | export interface KeybindsOsu { 20 | k1: string; 21 | k2: string; 22 | smokeKey: string; 23 | } 24 | 25 | export interface KeybindsFruits { 26 | k1: string; 27 | k2: string; 28 | Dash: string; 29 | } 30 | 31 | export interface KeybindsTaiko { 32 | innerLeft: string; 33 | innerRight: string; 34 | outerLeft: string; 35 | outerRight: string; 36 | } 37 | 38 | export interface Volume { 39 | master: number; 40 | music: number; 41 | effect: number; 42 | } 43 | 44 | export interface Audio { 45 | ignoreBeatmapSounds: boolean; 46 | useSkinSamples: boolean; 47 | volume: Volume; 48 | offset: Offset; 49 | } 50 | 51 | export interface Background { 52 | storyboard: boolean; 53 | video: boolean; 54 | dim: number; 55 | } 56 | 57 | export interface Client { 58 | updateAvailable: boolean; 59 | branch: number; 60 | version: string; 61 | } 62 | 63 | export interface Resolution { 64 | fullscreen: boolean; 65 | width: number; 66 | height: number; 67 | widthFullscreen: number; 68 | heightFullscreen: number; 69 | } 70 | 71 | export interface ScoreMeter { 72 | type: number; 73 | size: number; 74 | } 75 | 76 | export interface Offset { 77 | universal: number; 78 | } 79 | 80 | export interface Cursor { 81 | useSkinCursor: boolean; 82 | autoSize: boolean; 83 | size: number; 84 | } 85 | 86 | export interface Mouse { 87 | disableButtons: boolean; 88 | disableWheel: boolean; 89 | rawInput: boolean; 90 | sensitivity: number; 91 | } 92 | 93 | export interface Mania { 94 | speedBPMScale: boolean; 95 | usePerBeatmapSpeedScale: boolean; 96 | } 97 | -------------------------------------------------------------------------------- /packages/tosu/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "files": true 4 | }, 5 | "compilerOptions": { 6 | "lib": ["ES2020"], 7 | "module": "commonjs", 8 | "moduleResolution": "Node", 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | "outDir": "dist", 12 | "rootDir": "src", 13 | "sourceMap": false, 14 | "declaration": false, 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "target": "ES2020", 18 | "emitDecoratorMetadata": true, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization": false, 21 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": ["src/*"] 24 | }, 25 | "typeRoots": [ 26 | "./src/@types" 27 | ] 28 | }, 29 | "exclude": ["node_modules", "src/postBuild.ts"], 30 | "include": ["src"] 31 | } -------------------------------------------------------------------------------- /packages/tsprocess/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ -------------------------------------------------------------------------------- /packages/tsprocess/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'tsprocess', 5 | 'sources': [ 'lib/functions.cc', 'lib/memory/memory_linux.cc', 'lib/memory/memory_windows.cc' ], 6 | 'include_dirs': [" 2 | #include 3 | 4 | namespace logger { 5 | 6 | template 7 | std::string format(const std::string_view format, Args... args) { 8 | const auto size = std::snprintf(nullptr, 0, format.data(), args...) + 1; 9 | if (size <= 0) { 10 | return {}; 11 | } 12 | auto buffer = std::make_unique(size); 13 | std::snprintf(buffer.get(), size, format.data(), args...); 14 | return std::string(buffer.get(), size - 1); 15 | } 16 | 17 | template 18 | void println(const std::string_view format, Args... args) { 19 | std::printf(logger::format(format, args...).c_str()); 20 | std::printf("\n"); 21 | } 22 | 23 | } // namespace logger 24 | -------------------------------------------------------------------------------- /packages/tsprocess/lib/memory/memory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct MemoryRegion { 12 | uintptr_t address; 13 | std::size_t size; 14 | }; 15 | 16 | struct Pattern { 17 | int index; 18 | std::span signature; 19 | std::span mask; 20 | bool found; 21 | }; 22 | 23 | struct PatternResult { 24 | int index; 25 | uintptr_t address; 26 | }; 27 | 28 | namespace memory { 29 | 30 | std::vector query_regions(void *process); 31 | 32 | std::vector find_processes(const std::vector &process_names); 33 | 34 | void *open_process(uint32_t id); 35 | void close_handle(void *handle); 36 | bool is_process_64bit(uint32_t id); 37 | bool is_process_exist(void *process); 38 | std::string get_process_path(void *process); 39 | std::string get_process_command_line(void *process); 40 | std::string get_process_cwd(void *process); 41 | void *get_foreground_window_process(); 42 | 43 | bool read_buffer(void *process, uintptr_t address, std::size_t size, uint8_t *buffer); 44 | 45 | template 46 | std::tuple read(void *process, uintptr_t address) { 47 | T data; 48 | const auto success = read_buffer(process, address, sizeof(T), reinterpret_cast(&data)); 49 | return std::make_tuple(data, success); 50 | } 51 | 52 | inline bool scan(std::vector buffer, std::span signature, std::span mask, size_t &offset) { 53 | offset = 0; 54 | 55 | for (size_t i = 0; i + signature.size() <= buffer.size(); ++i) { 56 | bool found = true; 57 | for (size_t j = 0; j < signature.size(); ++j) { 58 | if (buffer[i + j] == signature[j] || mask[j] == 0) 59 | continue; 60 | 61 | found = false; 62 | break; 63 | } 64 | 65 | if (!found) { 66 | continue; 67 | } 68 | 69 | offset = static_cast(i); 70 | return true; 71 | } 72 | 73 | return false; 74 | } 75 | 76 | inline bool scan(std::vector buffer, std::vector signature, std::vector mask, size_t &offset) { 77 | offset = 0; 78 | 79 | for (size_t i = 0; i + signature.size() <= buffer.size(); ++i) { 80 | bool found = true; 81 | for (size_t j = 0; j < signature.size(); ++j) { 82 | if (buffer[i + j] == signature[j] || mask[j] == 0) 83 | continue; 84 | 85 | found = false; 86 | break; 87 | } 88 | 89 | if (!found) { 90 | continue; 91 | } 92 | 93 | offset = static_cast(i); 94 | return true; 95 | } 96 | 97 | return false; 98 | } 99 | 100 | inline uintptr_t find_pattern(void *process, const std::vector signature, const std::vector mask) { 101 | const auto regions = query_regions(process); 102 | 103 | for (auto ®ion : regions) { 104 | auto buffer = std::vector(region.size); 105 | if (!read_buffer(process, region.address, region.size, buffer.data())) { 106 | continue; 107 | } 108 | 109 | size_t offset; 110 | if (!scan(buffer, signature, mask, offset)) { 111 | continue; 112 | } 113 | 114 | return region.address + offset; 115 | } 116 | 117 | return 0; 118 | } 119 | 120 | inline std::vector batch_find_pattern(void *process, std::vector patterns) { 121 | const auto regions = query_regions(process); 122 | 123 | auto results = std::vector(); 124 | 125 | for (auto ®ion : regions) { 126 | auto buffer = std::vector(region.size); 127 | if (!read_buffer(process, region.address, region.size, buffer.data())) { 128 | continue; 129 | } 130 | 131 | for (auto &pattern : patterns) { 132 | if (pattern.found) { 133 | continue; 134 | } 135 | 136 | size_t offset; 137 | if (!scan(buffer, pattern.signature, pattern.mask, offset)) { 138 | continue; 139 | } 140 | 141 | PatternResult result; 142 | result.index = pattern.index; 143 | result.address = region.address + offset; 144 | 145 | results.push_back(result); 146 | 147 | pattern.found = true; 148 | 149 | if (patterns.size() == results.size()) { 150 | return results; 151 | } 152 | } 153 | } 154 | 155 | return results; 156 | } 157 | 158 | } // namespace memory 159 | -------------------------------------------------------------------------------- /packages/tsprocess/lib/memory/memory_linux.cc: -------------------------------------------------------------------------------- 1 | #ifdef __unix__ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "../logger.h" 13 | #include "memory.h" 14 | 15 | namespace { 16 | 17 | std::string read_file(const std::string &path) { 18 | std::ifstream file(path, std::ios::binary); 19 | std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); 20 | 21 | return content; 22 | } 23 | 24 | } // namespace 25 | 26 | std::vector memory::find_processes(const std::vector &process_names) { 27 | std::vector process_ids; 28 | const auto dir = opendir("/proc"); 29 | if (dir) { 30 | dirent *entry; 31 | while ((entry = readdir(dir)) != nullptr) { 32 | if (entry->d_type == DT_DIR) { 33 | std::string pid_str(entry->d_name); 34 | if (std::all_of(pid_str.begin(), pid_str.end(), isdigit)) { 35 | const auto pid = std::stoi(pid_str); 36 | const auto cmdline_path = "/proc/" + pid_str + "/comm"; 37 | const auto cmdline = read_file(cmdline_path); 38 | 39 | // Check if the process name matches any in the provided list 40 | for (const auto &process_name : process_names) { 41 | if (cmdline.find(process_name) != std::string::npos) { 42 | process_ids.push_back(pid); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | closedir(dir); 49 | } 50 | return process_ids; 51 | } 52 | 53 | void *memory::open_process(uint32_t id) { 54 | return reinterpret_cast(id); 55 | } 56 | 57 | void memory::close_handle(void *handle) { 58 | // do nothing 59 | } 60 | 61 | bool memory::is_process_exist(void *process) { 62 | const auto pid = reinterpret_cast(process); 63 | struct stat sts; 64 | const auto proc_path = "/proc/" + std::to_string(pid); 65 | if (stat(proc_path.c_str(), &sts) == -1 && errno == ENOENT) { 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | bool memory::is_process_64bit(uint32_t id) { 72 | const auto exe_path = "/proc/" + std::to_string(id) + "/exe"; 73 | 74 | std::ifstream file(exe_path, std::ios::binary); 75 | if (!file.is_open()) { 76 | return false; 77 | } 78 | 79 | unsigned char magic[4]; 80 | file.read(reinterpret_cast(magic), sizeof(magic)); 81 | 82 | if (magic[0] != 0x7F || magic[1] != 'E' || magic[2] != 'L' || magic[3] != 'F') { 83 | return false; 84 | } 85 | 86 | unsigned char elf_class; 87 | file.read(reinterpret_cast(&elf_class), sizeof(elf_class)); 88 | 89 | return elf_class == 2; 90 | } 91 | 92 | std::string memory::get_process_path(void *process) { 93 | const auto pid = reinterpret_cast(process); 94 | const auto path = "/proc/" + std::to_string(pid) + "/exe"; 95 | char buf[PATH_MAX]; 96 | const auto len = readlink(path.c_str(), buf, sizeof(buf) - 1); 97 | if (len != -1) { 98 | buf[len] = '\0'; 99 | return std::string(buf); 100 | } 101 | return ""; 102 | } 103 | 104 | std::string memory::get_process_command_line(void *process) { 105 | const auto pid = reinterpret_cast(process); 106 | return read_file("/proc/" + std::to_string(pid) + "/cmdline"); 107 | } 108 | 109 | std::string memory::get_process_cwd(void *process) { 110 | const auto pid = reinterpret_cast(process); 111 | const auto path = "/proc/" + std::to_string(pid) + "/cwd"; 112 | char buf[PATH_MAX]; 113 | const auto len = readlink(path.c_str(), buf, sizeof(buf) - 1); 114 | if (len != -1) { 115 | buf[len] = '\0'; 116 | return std::string(buf); 117 | } 118 | return ""; 119 | } 120 | 121 | bool memory::read_buffer(void *process, uintptr_t address, std::size_t size, uint8_t *buffer) { 122 | const auto pid = reinterpret_cast(process); 123 | 124 | iovec local_iov{buffer, size}; 125 | iovec remote_iov{reinterpret_cast(address), size}; 126 | 127 | const auto result_size = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); 128 | 129 | const auto success = result_size == size; 130 | 131 | if (!success && errno == EPERM) { 132 | logger::println("failed to read address %x of size %x (consider trying to run with sudo)", address, size); 133 | } 134 | 135 | return success; 136 | } 137 | 138 | std::vector memory::query_regions(void *process) { 139 | std::vector regions; 140 | 141 | const auto pid = reinterpret_cast(process); 142 | const auto maps_path = "/proc/" + std::to_string(pid) + "/maps"; 143 | std::ifstream maps_file(maps_path); 144 | if (!maps_file.is_open()) { 145 | return regions; 146 | } 147 | 148 | std::string line; 149 | while (std::getline(maps_file, line)) { 150 | MemoryRegion region; 151 | 152 | const auto first_space_pos = line.find(' '); 153 | const auto address_range = line.substr(0, first_space_pos); 154 | 155 | const auto dash_pos = address_range.find('-'); 156 | region.address = std::stoull(address_range.substr(0, dash_pos), nullptr, 16); 157 | const auto end_address = std::stoull(address_range.substr(dash_pos + 1), nullptr, 16); 158 | region.size = end_address - region.address; 159 | const auto protections = line.substr(first_space_pos + 1, 5); 160 | 161 | if (protections[0] == 'r' && protections[1] == 'w') { 162 | regions.push_back(region); 163 | } 164 | } 165 | 166 | return regions; 167 | } 168 | 169 | void *memory::get_foreground_window_process() { 170 | return 0; 171 | } 172 | 173 | #endif 174 | -------------------------------------------------------------------------------- /packages/tsprocess/lib/memory/memory_windows.cc: -------------------------------------------------------------------------------- 1 | #if defined(WIN32) || defined(_WIN32) 2 | 3 | // clang-format off 4 | #include 5 | #include 6 | // clang-format on 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "memory.h" 15 | 16 | #pragma comment(lib, "Psapi.lib") 17 | #pragma comment(lib, "ntdll.lib") 18 | 19 | bool memory::read_buffer(void *process, uintptr_t address, std::size_t size, uint8_t *buffer) { 20 | return ReadProcessMemory(process, reinterpret_cast(address), buffer, size, 0) == 1; 21 | } 22 | 23 | std::vector memory::query_regions(void *process) { 24 | std::vector regions; 25 | 26 | MEMORY_BASIC_INFORMATION info; 27 | for (uint8_t *address = 0; VirtualQueryEx(process, address, &info, sizeof(info)) != 0; address += info.RegionSize) { 28 | if ((info.State & MEM_COMMIT) == 0 || (info.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE)) == 0) { 29 | continue; 30 | } 31 | 32 | regions.push_back(MemoryRegion{reinterpret_cast(info.BaseAddress), info.RegionSize}); 33 | } 34 | 35 | return regions; 36 | } 37 | 38 | std::vector memory::find_processes(const std::vector &process_names) { 39 | PROCESSENTRY32 processEntry; 40 | processEntry.dwSize = sizeof(PROCESSENTRY32); 41 | 42 | std::vector processes; 43 | 44 | HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); 45 | if (Process32First(snapshot, &processEntry)) { 46 | do { 47 | for (const auto &process_name : process_names) { 48 | if (process_name == processEntry.szExeFile) { 49 | processes.push_back(processEntry.th32ProcessID); 50 | } 51 | } 52 | } while (Process32Next(snapshot, &processEntry)); 53 | } 54 | 55 | CloseHandle(snapshot); 56 | 57 | return processes; 58 | } 59 | 60 | void *memory::open_process(uint32_t id) { 61 | return OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, id); 62 | } 63 | 64 | void memory::close_handle(void *handle) { 65 | CloseHandle(handle); 66 | } 67 | 68 | bool memory::is_process_exist(void *handle) { 69 | DWORD returnCode{}; 70 | if (GetExitCodeProcess(handle, &returnCode)) { 71 | return returnCode == STILL_ACTIVE; 72 | } 73 | return false; 74 | } 75 | 76 | bool memory::is_process_64bit(uint32_t id) { 77 | HANDLE process_handle = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, id); 78 | BOOL is_wow64 = FALSE; 79 | 80 | if (!IsWow64Process(process_handle, &is_wow64)) { 81 | DWORD error = GetLastError(); 82 | std::cerr << "Failed to determine process bitness, error: " << error << std::endl; 83 | return false; 84 | } 85 | 86 | return !is_wow64; 87 | } 88 | 89 | std::string memory::get_process_path(void *handle) { 90 | char filePath[MAX_PATH]; 91 | GetModuleFileNameExA(handle, NULL, filePath, MAX_PATH); 92 | 93 | return filePath; 94 | } 95 | 96 | std::string memory::get_process_cwd(void *process) { 97 | return ""; 98 | } 99 | 100 | std::string memory::get_process_command_line(void *process) { 101 | std::string commandLine; 102 | 103 | PROCESS_BASIC_INFORMATION pbi = {}; 104 | NTSTATUS status = NtQueryInformationProcess(process, ProcessBasicInformation, &pbi, sizeof(pbi), NULL); 105 | if (status != 0) { 106 | std::cerr << "failed to query the process, error: " << status << std::endl; 107 | } else { 108 | PEB peb = {}; 109 | if (!ReadProcessMemory(process, pbi.PebBaseAddress, &peb, sizeof(peb), NULL)) { 110 | DWORD err = GetLastError(); 111 | std::cerr << "failed to read the process PEB, error: " << err << std::endl; 112 | } else { 113 | RTL_USER_PROCESS_PARAMETERS params = {}; 114 | if (!ReadProcessMemory(process, peb.ProcessParameters, ¶ms, sizeof(params), NULL)) { 115 | DWORD err = GetLastError(); 116 | std::cerr << "failed to read the process params, error: " << err << std::endl; 117 | } else { 118 | UNICODE_STRING &commandLineArgs = params.CommandLine; 119 | std::vector buffer(commandLineArgs.Length / sizeof(WCHAR)); 120 | if (!ReadProcessMemory(process, commandLineArgs.Buffer, buffer.data(), commandLineArgs.Length, NULL)) { 121 | DWORD err = GetLastError(); 122 | std::cerr << "failed to read the process command line, error: " << err << std::endl; 123 | } else { 124 | commandLine = std::string(buffer.begin(), buffer.end()); 125 | } 126 | } 127 | } 128 | } 129 | 130 | return commandLine; 131 | } 132 | 133 | void *memory::get_foreground_window_process() { 134 | DWORD process_id; 135 | 136 | GetWindowThreadProcessId(GetForegroundWindow(), &process_id); 137 | 138 | return reinterpret_cast(process_id); 139 | } 140 | 141 | #endif 142 | -------------------------------------------------------------------------------- /packages/tsprocess/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsprocess", 3 | "version": "1.0.1", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "build": "tsc", 9 | "postinstall": "node-gyp clean configure build" 10 | }, 11 | "dependencies": { 12 | "node-addon-api": "^8.3.1", 13 | "node-gyp": "^11.1.0" 14 | }, 15 | "files": [ 16 | "lib", 17 | "dist", 18 | "binding.gyp" 19 | ], 20 | "devDependencies": { 21 | "node-api-headers": "^1.5.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/tsprocess/src/index.ts: -------------------------------------------------------------------------------- 1 | export default require('./lib/tsprocess.node'); 2 | export * from './process'; 3 | -------------------------------------------------------------------------------- /packages/tsprocess/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module":"CommonJS", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "allowSyntheticDefaultImports": true, 8 | "declarationDir": "dist", 9 | "outDir": "dist", 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } -------------------------------------------------------------------------------- /packages/updater/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | config, 3 | downloadFile, 4 | getProgramPath, 5 | platformResolver, 6 | sleep, 7 | unzip, 8 | wLogger 9 | } from '@tosu/common'; 10 | import { spawn } from 'child_process'; 11 | import fs from 'fs'; 12 | import path from 'path'; 13 | 14 | // NOTE: _version.js packs with pkg support in tosu build 15 | const currentVersion = require(process.cwd() + '/_version.js'); 16 | 17 | const platform = platformResolver(process.platform); 18 | 19 | const fileDestination = path.join(getProgramPath(), 'update.zip'); 20 | const backupExecutablePath = path.join( 21 | getProgramPath(), 22 | `tosu_old${platform.fileType}` 23 | ); 24 | 25 | const deleteNotLocked = async (filePath: string) => { 26 | try { 27 | await fs.promises.unlink(filePath); 28 | } catch (err) { 29 | if ((err as any).code === 'EPERM') { 30 | await sleep(1000); 31 | deleteNotLocked(filePath); 32 | return; 33 | } 34 | 35 | wLogger.error('[updater]', 'deleteNotLocked', (err as any).message); 36 | wLogger.debug('[updater]', 'deleteNotLocked', err); 37 | } 38 | }; 39 | 40 | export const checkUpdates = async () => { 41 | wLogger.info('[updater]', 'Checking updates'); 42 | 43 | try { 44 | if (platform.type === 'unknown') { 45 | wLogger.warn( 46 | '[updater]', 47 | `Unsupported platform (${process.platform}). Unable to run updater` 48 | ); 49 | 50 | return new Error( 51 | `Unsupported platform (${process.platform}). Unable to run updater` 52 | ); 53 | } 54 | 55 | const request = await fetch( 56 | `https://api.github.com/repos/tosuapp/tosu/releases/latest` 57 | ); 58 | const json = (await request.json()) as any; 59 | const { 60 | assets, 61 | name: versionName 62 | }: { 63 | name: string; 64 | assets: { name: string; browser_download_url: string }[]; 65 | } = json; 66 | 67 | config.currentVersion = currentVersion; 68 | config.updateVersion = versionName || currentVersion; 69 | 70 | if (versionName === null || versionName === undefined) { 71 | wLogger.info( 72 | '[updater]', 73 | `Failed to check updates v${currentVersion}` 74 | ); 75 | 76 | return new Error('Version the same'); 77 | } 78 | 79 | return { assets, versionName }; 80 | } catch (exc) { 81 | wLogger.error('[updater]', `checkUpdates`, (exc as any).message); 82 | wLogger.debug('[updater]', `checkUpdates`, exc); 83 | 84 | config.currentVersion = currentVersion; 85 | config.updateVersion = currentVersion; 86 | 87 | return exc as Error; 88 | } 89 | }; 90 | 91 | export const autoUpdater = async () => { 92 | try { 93 | const check = await checkUpdates(); 94 | if (check instanceof Error) { 95 | return check; 96 | } 97 | 98 | const { assets, versionName } = check; 99 | if ( 100 | versionName.includes(currentVersion) || 101 | currentVersion.includes('-forced') 102 | ) { 103 | wLogger.info( 104 | '[updater]', 105 | `You're using latest version v${currentVersion}` 106 | ); 107 | 108 | if (fs.existsSync(fileDestination)) { 109 | await deleteNotLocked(fileDestination); 110 | } 111 | 112 | if (fs.existsSync(backupExecutablePath)) { 113 | await deleteNotLocked(backupExecutablePath); 114 | } 115 | 116 | return; 117 | } 118 | 119 | const findAsset = assets.find( 120 | (r) => r.name.includes(platform.type) && r.name.endsWith('.zip') 121 | ); 122 | if (!findAsset) { 123 | wLogger.info( 124 | '[updater]', 125 | `Files to update not found (${platform.type})` 126 | ); 127 | return 'noFiles'; 128 | } 129 | 130 | const downloadAsset = await downloadFile( 131 | findAsset.browser_download_url, 132 | fileDestination 133 | ); 134 | 135 | const currentExecutablePath = process.argv[0]; // Path to the current executable 136 | 137 | await fs.promises.rename(currentExecutablePath, backupExecutablePath); 138 | await unzip(downloadAsset, getProgramPath()); 139 | 140 | wLogger.info('[updater]', 'Restarting program'); 141 | 142 | spawn(`"${process.argv[0]}"`, process.argv.slice(1), { 143 | detached: true, 144 | shell: true, 145 | stdio: 'ignore' 146 | }).unref(); 147 | 148 | wLogger.info('[updater]', 'Closing program'); 149 | 150 | await sleep(1000); 151 | 152 | process.exit(); 153 | } catch (exc) { 154 | wLogger.error('[updater]', 'autoUpdater', (exc as any).message); 155 | wLogger.debug('[updater]', 'autoUpdater', exc); 156 | 157 | return exc; 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /packages/updater/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tosu/updater", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "cyperdark", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "prepare": "npm run build", 10 | "build": "tsc" 11 | }, 12 | "dependencies": { 13 | "@tosu/common": "workspace:*" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/updater/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ES2020" 5 | ], 6 | "module": "commonjs", 7 | "moduleResolution": "Node", 8 | "allowJs": true, 9 | "esModuleInterop": true, 10 | "outDir": "dist", 11 | "rootDir": "./", 12 | "sourceMap": false, 13 | "declaration": false, 14 | "strict": true, 15 | "noImplicitAny": false, 16 | "target": "ES2020", 17 | "strictPropertyInitialization": false, 18 | "baseUrl": ".", 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "dist", 23 | ], 24 | "include": [ 25 | "**/*" 26 | ], 27 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in direct subdirs of packages/ 3 | - 'packages/*' 4 | 5 | onlyBuiltDependencies: 6 | - '@jellybrick/ffi-napi' 7 | - '@jellybrick/ref-napi' 8 | - electron 9 | - esbuild --------------------------------------------------------------------------------
{DESCRIPTION}
{header}
{name}
{description}