├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── launch.json ├── LICENSE ├── README.md ├── build.mjs ├── config.ts ├── package.json ├── resources └── icon.png ├── src ├── common │ ├── channels.ts │ └── types.ts ├── main │ ├── api │ │ ├── APIManager.ts │ │ └── AuthorizationService.ts │ ├── core │ │ ├── IHandleable.ts │ │ ├── Launcher.ts │ │ ├── LauncherWindow.ts │ │ └── System.ts │ ├── game │ │ ├── AuthlibInjector.ts │ │ ├── GameService.ts │ │ ├── GameWindow.ts │ │ ├── JavaManager.ts │ │ ├── LibrariesMatcher.ts │ │ ├── Starter.ts │ │ ├── Updater.ts │ │ └── Watcher.ts │ ├── helpers │ │ ├── LogHelper.ts │ │ ├── PlatformHelper.ts │ │ └── StorageHelper.ts │ ├── index.ts │ └── scenes │ │ ├── Login.ts │ │ ├── ServerPanel.ts │ │ └── ServersList.ts ├── preload │ ├── components │ │ ├── LoginScene.ts │ │ ├── ServerPanelScene.ts │ │ ├── ServersListScene.ts │ │ └── Window.ts │ └── index.ts └── renderer │ ├── App.tsx │ ├── index.html │ ├── index.tsx │ ├── renderer.d.ts │ ├── runtime │ ├── assets │ │ ├── images │ │ │ ├── background.png │ │ │ ├── logo.png │ │ │ └── steve.png │ │ └── sass │ │ │ └── index.sass │ ├── components │ │ ├── If.tsx │ │ ├── Layout.tsx │ │ ├── Modal │ │ │ ├── hooks.ts │ │ │ ├── index.module.sass │ │ │ ├── index.tsx │ │ │ └── states.ts │ │ ├── ServerButton │ │ │ ├── index.module.sass │ │ │ └── index.tsx │ │ ├── SkinView.tsx │ │ └── TitleBar │ │ │ ├── hooks.ts │ │ │ ├── index.module.sass │ │ │ ├── index.tsx │ │ │ └── states.ts │ ├── hooks │ │ └── pingServer.ts │ └── scenes │ │ ├── Login │ │ ├── index.module.sass │ │ └── index.tsx │ │ ├── ServerPanel │ │ ├── index.module.sass │ │ └── index.tsx │ │ └── ServersList │ │ ├── index.module.sass │ │ └── index.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:react/recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:react/jsx-runtime', 12 | ], 13 | overrides: [], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react', '@typescript-eslint'], 20 | rules: { 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | }, 23 | globals: { 24 | launcherAPI: 'readonly', 25 | }, 26 | settings: { 27 | react: { 28 | version: 'detect', 29 | }, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Releases Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | include: 14 | - os: ubuntu-latest 15 | - os: windows-latest 16 | - os: macos-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup Node.js environment 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 18 25 | 26 | - name: Get npm cache directory 27 | id: npm-cache-dir 28 | shell: bash 29 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} 30 | 31 | - name: Cache NPM dependencies 32 | uses: actions/cache@v4 33 | with: 34 | path: ${{ steps.npm-cache-dir.outputs.dir }} 35 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 36 | restore-keys: | 37 | ${{ runner.os }}-node- 38 | 39 | - name: Download dependencies 40 | run: npm i 41 | 42 | - name: Build Launcher 43 | run: npm run build 44 | 45 | - name: Pack and publish Launcher 46 | run: npm run release 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Заменить на кастомный токен (например GH_TOKEN) 49 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 50 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Aurora files ignore 2 | build 3 | out 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | pnpm-debug.log* 12 | lerna-debug.log* 13 | 14 | node_modules 15 | dist 16 | dist-ssr 17 | *.local 18 | 19 | # Editor directories and files 20 | .vscode/* 21 | !.vscode/extensions.json 22 | !.vscode/launch.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | package-lock.json 32 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "syler.sass-indented", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": ["run-script", "dev"], 9 | "runtimeExecutable": "npm", 10 | "skipFiles": ["/**"], 11 | "console": "integratedTerminal" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 AuroraTeam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

AuroraLauncher (Client)

3 | 4 | Здесь содержится код программы лаунчера (клиентского интерфейса). Код лаунчсервера и других библиотек находится в [этом репозитории](https://github.com/AuroraTeam/AuroraLauncher). 5 | -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- 1 | import { esbuildDecorators } from '@aurora-launcher/esbuild-decorators'; 2 | import { context } from 'esbuild'; 3 | import minimist from 'minimist'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | const { _, watch, ...args } = minimist(process.argv.slice(2)); 7 | 8 | if (!watch) { 9 | console.log('Build...'); 10 | console.time('Build successfully'); 11 | } 12 | 13 | const ctx = await context({ 14 | entryPoints: ['src/main/index.ts'], 15 | bundle: true, 16 | sourcemap: true, 17 | platform: 'node', 18 | target: 'node20', 19 | format: 'cjs', 20 | outdir: 'build/main', 21 | external: ['electron'], 22 | keepNames: true, 23 | loader: { 24 | '.png': 'file', 25 | //TODO Secure auth 26 | '.pem': 'base64', 27 | }, 28 | plugins: [esbuildDecorators()], 29 | ...args, 30 | }).catch(() => process.exit(1)); 31 | 32 | if (watch) { 33 | console.log('Watching...'); 34 | await ctx.watch(); 35 | } else { 36 | await ctx.rebuild(); 37 | await ctx.dispose(); 38 | console.timeEnd('Build successfully'); 39 | } 40 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | // import token from './public.pem'; 2 | 3 | export const window = { 4 | width: 900, 5 | height: 550, 6 | frame: false, 7 | resizable: false, 8 | maximizable: false, 9 | fullscreenable: false, 10 | title: 'Aurora Launcher', 11 | }; 12 | 13 | export const api = { 14 | ws: 'ws://127.0.0.1:1370/ws', 15 | web: 'http://127.0.0.1:1370', 16 | // extraToken: token, 17 | }; 18 | 19 | export const appPath = '.aurora-launcher'; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurora-launcher-app", 3 | "version": "0.0.4", 4 | "description": "Launcher for Minecraft", 5 | "main": "build/main/index.js", 6 | "private": true, 7 | "scripts": { 8 | "dev": "concurrently -i -n electron,main,preload,renderer -c cyan.bold,blue.bold,yellow.bold,magenta.bold \"npm:start:dev\" \"npm:build:main -- --watch --sourcemap=inline\" \"npm:build:preload -- --watch --sourcemap=inline\" \"vite\"", 9 | "build": "concurrently -n main,preload,renderer -c blue.bold,yellow.bold,magenta.bold \"npm:build:main\" \"npm:build:preload\" \"npm:build:renderer\" --kill-others-on-fail", 10 | "build:main": "node build.mjs", 11 | "build:preload": "esbuild src/preload/index.ts --platform=node --bundle --outdir=build/preload --external:electron --format=iife", 12 | "build:renderer": "tsc && vite build", 13 | "start:dev": "cross-env DEV=true nodemon --watch src --ext ts,tsx --ignore 'src/renderer' --exec npm run start:prod", 14 | "start:prod": "electron .", 15 | "clean": "rimraf build out", 16 | "prettier": "prettier --config .prettierrc.json --write src", 17 | "typecheck": "tsc --noEmit", 18 | "lint": "eslint src --ext .ts,.tsx", 19 | "lint:fix": "npm run lint -- --fix", 20 | "pack": "electron-builder --dir", 21 | "release": "electron-builder", 22 | "obf": "javascript-obfuscator build/main/index.js --output build/main/index-obf.js --split-strings-chunk-length 8", 23 | "bytenode": "bytenode -c build/main/index-obf.js -e" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/AuroraTeam/Launcher.git" 28 | }, 29 | "author": "AuroraTeam", 30 | "contributors": [ 31 | "JoCat (https://github.com/JoCat)" 32 | ], 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/AuroraTeam/Launcher/issues" 36 | }, 37 | "homepage": "https://github.com/AuroraTeam/Launcher#readme", 38 | "devDependencies": { 39 | "@aurora-launcher/esbuild-decorators": "^0.0.1", 40 | "@types/node": "^18.17.15", 41 | "@types/react": "^18.0.18", 42 | "@types/react-dom": "^18.0.6", 43 | "@types/semver": "^7.3.8", 44 | "@types/tar": "^6.1.6", 45 | "@types/ws": "^8.5.3", 46 | "@typescript-eslint/eslint-plugin": "^6.5.0", 47 | "@typescript-eslint/parser": "^6.5.0", 48 | "@vitejs/plugin-react": "^4.0.0", 49 | "bytenode": "^1.5.3", 50 | "concurrently": "^8.0.1", 51 | "cross-env": "^7.0.3", 52 | "electron": "^26.1.0", 53 | "electron-builder": "^24.6.4", 54 | "electron-extension-installer": "^1.1.3", 55 | "esbuild": "^0.19.2", 56 | "eslint": "^8.13.0", 57 | "eslint-plugin-react": "^7.31.1", 58 | "import-sort-style-module": "^6.0.0", 59 | "javascript-obfuscator": "^4.1.0", 60 | "minimist": "^1.2.8", 61 | "nodemon": "^3.0.1", 62 | "prettier": "^3.0.3", 63 | "prettier-plugin-import-sort": "^0.0.7", 64 | "rimraf": "^5.0.0", 65 | "sass": "^1.55.0", 66 | "source-map-support": "^0.5.21", 67 | "typescript": "^5.0.4", 68 | "vite": "^5.0.9" 69 | }, 70 | "dependencies": { 71 | "@aurora-launcher/api": "^0.3.0", 72 | "@aurora-launcher/core": "^0.18.0", 73 | "electron-updater": "^6.1.4", 74 | "react": "^18.2.0", 75 | "react-dom": "^18.2.0", 76 | "react-router-dom": "^6.3.0", 77 | "recoil": "^0.7.5", 78 | "reflect-metadata": "^0.1.13", 79 | "semver": "^7.3.5", 80 | "skinview3d": "^2.2.1", 81 | "tar": "^6.2.0", 82 | "typedi": "^0.10.0" 83 | }, 84 | "importSort": { 85 | ".tsx, .ts, .mjs, .js": { 86 | "style": "module", 87 | "parser": "typescript" 88 | } 89 | }, 90 | "build": { 91 | "appId": "ru.aurora.launcher", 92 | "productName": "Aurora Launcher", 93 | "publish": [ 94 | { 95 | "provider": "github", 96 | "releaseType": "release", 97 | "owner": "AuroraTeam", 98 | "repo": "Launcher" 99 | } 100 | ], 101 | "directories": { 102 | "buildResources": "resources" 103 | }, 104 | "files": [ 105 | "build/**/*", 106 | "!node_modules/**/*" 107 | ], 108 | "nsis": { 109 | "artifactName": "${name}-Setup-${version}.${ext}" 110 | }, 111 | "mac": { 112 | "category": "public.app-category.games" 113 | }, 114 | "linux": { 115 | "target": [ 116 | "deb", 117 | "rpm", 118 | "AppImage" 119 | ], 120 | "category": "Game", 121 | "maintainer": "AuroraTeam " 122 | } 123 | }, 124 | "optionalDependencies": { 125 | "@swc/core-linux-x64-gnu": "^1.3.96", 126 | "@swc/core-linux-x64-musl": "^1.3.96" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraTeam/Launcher/9462126b73bcaf68dae50be9befd237a08696924/resources/icon.png -------------------------------------------------------------------------------- /src/common/channels.ts: -------------------------------------------------------------------------------- 1 | export const EVENTS = { 2 | WINDOW: { 3 | HIDE: 'window:hide', 4 | CLOSE: 'window:close', 5 | }, 6 | SCENES: { 7 | LOGIN: { 8 | AUTH: 'scenes:login:auth', 9 | }, 10 | SERVERS_LIST: { 11 | GET_SERVERS: 'scenes:serversList:getServers', 12 | SELECT_SERVER: 'scenes:serversList:selectServer', 13 | }, 14 | SERVER_PANEL: { 15 | GET_PROFILE: 'scenes:serverPanel:getProfile', 16 | GET_SERVER: 'scenes:serverPanel:getServer', 17 | START_GAME: 'scenes:serverPanel:startGame', 18 | TEXT_TO_CONSOLE: 'scenes:serverPanel:textToConsole', 19 | LOAD_PROGRESS: 'scenes:serverPanel:loadProgress', 20 | STOP_GAME: 'scenes:serverPanel:stopGame', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { AuthResponseData } from '@aurora-launcher/core'; 2 | 3 | export type UserData = Omit; 4 | 5 | export type Session = AuthResponseData; 6 | 7 | export interface LoadProgress { 8 | total: number; 9 | loaded: number; 10 | type: 'count' | 'size'; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/api/APIManager.ts: -------------------------------------------------------------------------------- 1 | import { AuroraAPI } from '@aurora-launcher/api'; 2 | import { api as apiConfig } from '@config'; 3 | import { Service } from 'typedi'; 4 | 5 | import { LogHelper } from '../helpers/LogHelper'; 6 | 7 | @Service() 8 | export class APIManager { 9 | private api = new AuroraAPI(apiConfig.ws || 'ws://localhost:1370', { 10 | onClose: () => setTimeout(() => this.initConnection(), 5000), 11 | }); 12 | 13 | async initConnection() { 14 | try { 15 | await this.api.connect(); 16 | } catch (error) { 17 | LogHelper.error(error); 18 | } 19 | } 20 | 21 | public auth(login: string, password: string) { 22 | return this.api.auth(login, password); 23 | } 24 | 25 | public getServers() { 26 | return this.api.getServers(); 27 | } 28 | 29 | public getProfile(uuid: string) { 30 | return this.api.getProfile(uuid); 31 | } 32 | 33 | public getUpdates(dir: string) { 34 | return this.api.getUpdates(dir); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/api/AuthorizationService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | 3 | import { Session, UserData } from '../../common/types'; 4 | import { APIManager } from './APIManager'; 5 | 6 | @Service() 7 | export class AuthorizationService { 8 | private currentSession?: Session; 9 | 10 | constructor(private apiService: APIManager) {} 11 | 12 | async authorize(login: string, password: string): Promise { 13 | this.currentSession = await this.apiService.auth(login, password); 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | const { accessToken, ...publicData } = this.currentSession; 16 | return publicData; 17 | } 18 | 19 | getCurrentSession() { 20 | return this.currentSession; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/core/IHandleable.ts: -------------------------------------------------------------------------------- 1 | export interface IHandleable { 2 | initHandlers(): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/main/core/Launcher.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | 3 | import { APIManager } from '../api/APIManager'; 4 | import { LogHelper } from '../helpers/LogHelper'; 5 | import { StorageHelper } from '../helpers/StorageHelper'; 6 | import { LoginScene } from '../scenes/Login'; 7 | import { ServerPanelScene } from '../scenes/ServerPanel'; 8 | import { ServersListScene } from '../scenes/ServersList'; 9 | import { LauncherWindow } from './LauncherWindow'; 10 | 11 | @Service() 12 | export class Launcher { 13 | constructor( 14 | private window: LauncherWindow, 15 | private apiManager: APIManager, 16 | 17 | private loginScene: LoginScene, 18 | private serversListScene: ServersListScene, 19 | private serverPanelScene: ServerPanelScene, 20 | ) { 21 | this.init(); 22 | } 23 | 24 | async init() { 25 | StorageHelper.createMissing(); 26 | 27 | await this.apiManager.initConnection(); 28 | 29 | this.loginScene.initHandlers(); 30 | this.serversListScene.initHandlers(); 31 | this.serverPanelScene.initHandlers(); 32 | 33 | this.window.createWindow(); 34 | LogHelper.info('Launcher started'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/core/LauncherWindow.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { window as windowConfig } from '@config'; 4 | import { BrowserWindow, app, ipcMain } from 'electron'; 5 | import installExtension, { 6 | REACT_DEVELOPER_TOOLS, 7 | } from 'electron-extension-installer'; 8 | import { Service } from 'typedi'; 9 | import { autoUpdater } from 'electron-updater'; 10 | 11 | import { EVENTS } from '../../common/channels'; 12 | import logo from '../../renderer/runtime/assets/images/logo.png'; 13 | import { PlatformHelper } from '../helpers/PlatformHelper'; 14 | 15 | const isDev = process.env.DEV === 'true' && !app.isPackaged; 16 | 17 | @Service() 18 | export class LauncherWindow { 19 | private mainWindow?: BrowserWindow; 20 | 21 | /** 22 | * Launcher initialization 23 | */ 24 | createWindow() { 25 | autoUpdater.checkForUpdatesAndNotify(); 26 | // This method will be called when Electron has finished 27 | // initialization and is ready to create browser windows. 28 | // Some APIs can only be used after this event occurs. 29 | app.whenReady().then(() => { 30 | this.mainWindow = this.createMainWindow(); 31 | if (isDev) { 32 | installExtension(REACT_DEVELOPER_TOOLS, { 33 | loadExtensionOptions: { 34 | allowFileAccess: true, 35 | }, 36 | }) 37 | .then((name: any) => 38 | console.log(`Added Extension: ${name}`), 39 | ) 40 | .catch((err: any) => 41 | console.error('An error occurred: ', err), 42 | ); 43 | } 44 | 45 | app.on('activate', () => { 46 | // On macOS it's common to re-create a window in the app when the 47 | // dock icon is clicked and there are no other windows open. 48 | if (!this.mainWindow) this.mainWindow = this.createMainWindow(); 49 | }); 50 | }); 51 | 52 | // Quit when all windows are closed, except on macOS. There, it's common 53 | // for applications and their menu bar to stay active until the user quits 54 | // explicitly with Cmd + Q. 55 | app.on('window-all-closed', () => { 56 | if (!PlatformHelper.isMac) app.quit(); 57 | }); 58 | 59 | // hide the main window when the minimize button is pressed 60 | ipcMain.on(EVENTS.WINDOW.HIDE, () => { 61 | this.mainWindow?.minimize(); 62 | }); 63 | 64 | // close the main window when the close button is pressed 65 | ipcMain.on(EVENTS.WINDOW.CLOSE, () => { 66 | this.mainWindow?.close(); 67 | }); 68 | } 69 | 70 | /** 71 | * Create launcher window 72 | */ 73 | private createMainWindow(): BrowserWindow { 74 | // creating and configuring a window 75 | const mainWindow = new BrowserWindow({ 76 | show: false, // Use 'ready-to-show' event to show window 77 | width: windowConfig.width || 900, 78 | height: windowConfig.height || 550, 79 | frame: windowConfig.frame || false, 80 | resizable: windowConfig.resizable || false, 81 | maximizable: windowConfig.maximizable || false, 82 | fullscreenable: windowConfig.fullscreenable || false, 83 | title: windowConfig.title || 'Aurora Launcher', 84 | icon: join(__dirname, logo), // TODO Check no img (maybe use mainWindow.setIcon()) 85 | webPreferences: { 86 | preload: join(__dirname, '../preload/index.js'), 87 | devTools: isDev, 88 | }, 89 | }); 90 | 91 | // loading renderer code (runtime) 92 | if (isDev) mainWindow.loadURL('http://localhost:3000'); 93 | else mainWindow.loadFile(join(__dirname, '../renderer/index.html')); 94 | 95 | mainWindow.on('closed', () => { 96 | this.mainWindow = undefined; 97 | }); 98 | 99 | /** 100 | * If you install `show: true` then it can cause issues when trying to close the window. 101 | * Use `show: false` and listener events `ready-to-show` to fix these issues. 102 | * 103 | * @see https://github.com/electron/electron/issues/25012 104 | */ 105 | mainWindow.on('ready-to-show', () => { 106 | mainWindow?.show(); 107 | 108 | // open developer tools when using development mode 109 | if (isDev) mainWindow.webContents.openDevTools(); 110 | }); 111 | 112 | return mainWindow; 113 | } 114 | 115 | public sendEvent(channel: string, ...args: any[]): void { 116 | this.mainWindow?.webContents.send(channel, ...args); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/core/System.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * enum for NodeJS.Platform 3 | */ 4 | export enum Platform { 5 | WINDOWS = 'win32', 6 | LINUX = 'linux', 7 | MACOS = 'darwin', 8 | } 9 | 10 | /** 11 | * enum for NodeJS.Architecture 12 | */ 13 | export enum Architecture { 14 | ARM = 'arm', 15 | ARM64 = 'arm64', 16 | X32 = 'ia32', 17 | X64 = 'x64', 18 | } 19 | -------------------------------------------------------------------------------- /src/main/game/AuthlibInjector.ts: -------------------------------------------------------------------------------- 1 | import { HashHelper, HttpHelper } from '@aurora-launcher/core'; 2 | import { existsSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { StorageHelper } from '../helpers/StorageHelper'; 5 | import { LogHelper } from '../helpers/LogHelper'; 6 | import { Service } from 'typedi'; 7 | 8 | @Service() 9 | export class AuthlibInjector { 10 | readonly authlibFilePath = join(StorageHelper.storageDir, 'authlib.jar'); 11 | 12 | async verify() { 13 | if (!existsSync(this.authlibFilePath)) { 14 | await this.#downloadAuthlib(); 15 | } 16 | LogHelper.info('Authlib loaded successfully'); 17 | } 18 | 19 | async #downloadAuthlib() { 20 | const apiUrl = 21 | 'https://authlib-injector.yushi.moe/artifact/latest.json'; 22 | 23 | let authlibData: AuthlibData; 24 | try { 25 | authlibData = await HttpHelper.getResourceFromJson(apiUrl); 26 | } catch (error) { 27 | LogHelper.error('Failed to check Authlib Injector API'); 28 | LogHelper.debug(error); 29 | return; 30 | } 31 | 32 | try { 33 | await HttpHelper.downloadFile( 34 | authlibData.download_url, 35 | this.authlibFilePath, 36 | ); 37 | } catch (error) { 38 | LogHelper.error('Failed to download Authlib Injector'); 39 | LogHelper.debug(error); 40 | return; 41 | } 42 | 43 | const fileHash = await HashHelper.getHashFromFile( 44 | this.authlibFilePath, 45 | 'sha256', 46 | ); 47 | if (fileHash !== authlibData.checksums.sha256) { 48 | LogHelper.error('Authlib checksum mismatch'); 49 | return; 50 | } 51 | 52 | LogHelper.info('Authlib downloaded successfully'); 53 | } 54 | } 55 | 56 | interface AuthlibData { 57 | build_number: number; 58 | version: string; 59 | download_url: string; 60 | checksums: { 61 | sha256: string; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/main/game/GameService.ts: -------------------------------------------------------------------------------- 1 | import { Profile, Server } from '@aurora-launcher/core'; 2 | import { Service } from 'typedi'; 3 | 4 | import { APIManager } from '../api/APIManager'; 5 | import { Starter } from './Starter'; 6 | import { Updater } from './Updater'; 7 | import { Watcher } from './Watcher'; 8 | import { GameWindow } from './GameWindow'; 9 | 10 | @Service() 11 | export class GameService { 12 | private selectedServer?: Server; 13 | private selectedProfile?: Profile; 14 | 15 | constructor( 16 | private apiService: APIManager, 17 | private gameUpdater: Updater, 18 | private gameWatcher: Watcher, 19 | private gameStarter: Starter, 20 | private gameWindow: GameWindow, 21 | ) {} 22 | 23 | async setServer(server: Server) { 24 | this.selectedServer = server; 25 | this.selectedProfile = await this.apiService.getProfile( 26 | server.profileUUID, 27 | ); 28 | } 29 | 30 | getServer() { 31 | return this.selectedServer; 32 | } 33 | 34 | getProfile() { 35 | return this.selectedProfile; 36 | } 37 | 38 | async startGame() { 39 | const profile = this.selectedProfile; 40 | const server = this.selectedServer; 41 | 42 | if (!profile || !server) { 43 | this.gameWindow.sendToConsole('Error: Profile or server not set'); 44 | this.gameWindow.stopGame(); 45 | return; 46 | } 47 | 48 | try { 49 | await this.gameUpdater.validateClient(profile); 50 | await this.gameStarter.start(profile); 51 | await this.gameWatcher.watch(); 52 | } catch (error) { 53 | this.gameWindow.sendToConsole(`${error}`); 54 | this.gameWindow.stopGame(); 55 | throw error; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/game/GameWindow.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | 3 | import { EVENTS } from '../../common/channels'; 4 | import { LauncherWindow } from '../core/LauncherWindow'; 5 | import { LoadProgress } from '../../common/types'; 6 | 7 | @Service() 8 | export class GameWindow { 9 | constructor(private window: LauncherWindow) {} 10 | 11 | sendToConsole(text: string) { 12 | this.window.sendEvent( 13 | EVENTS.SCENES.SERVER_PANEL.TEXT_TO_CONSOLE, 14 | `${text}\n`, 15 | ); 16 | } 17 | 18 | sendProgress(progress: LoadProgress) { 19 | this.window.sendEvent( 20 | EVENTS.SCENES.SERVER_PANEL.LOAD_PROGRESS, 21 | progress, 22 | ); 23 | } 24 | 25 | stopGame() { 26 | this.window.sendEvent(EVENTS.SCENES.SERVER_PANEL.STOP_GAME); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/game/JavaManager.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { StorageHelper } from '../helpers/StorageHelper'; 3 | import { existsSync } from 'fs'; 4 | import { Service } from 'typedi'; 5 | import { HttpHelper, ZipHelper } from '@aurora-launcher/core'; 6 | import tar from 'tar'; 7 | import { mkdir, readdir } from 'fs/promises'; 8 | import { Architecture, Platform } from '../core/System'; 9 | import { PlatformHelper } from '../helpers/PlatformHelper'; 10 | import { GameWindow } from './GameWindow'; 11 | 12 | @Service() 13 | export class JavaManager { 14 | // TODO Лишнее связывание, придумать как сделать лучше 15 | constructor(private gameWindow: GameWindow) {} 16 | 17 | async checkAndDownloadJava(majorVersion: number) { 18 | const javaDir = this.#getJavaDir(majorVersion); 19 | if (existsSync(javaDir)) return true; 20 | 21 | const javaLink = 22 | 'https://api.adoptium.net/v3/binary/latest/{version}/ga/{os}/{arch}/jre/hotspot/normal/eclipse'; 23 | 24 | this.gameWindow.sendToConsole('Download Java'); 25 | const javaFile = await HttpHelper.downloadFile( 26 | javaLink 27 | .replace('{version}', majorVersion.toString()) 28 | .replace('{os}', this.#getOs()) 29 | .replace('{arch}', this.#getArch()), 30 | null, 31 | { 32 | saveToTempFile: true, 33 | onProgress: (progress) => { 34 | this.gameWindow.sendProgress({ 35 | total: progress.total, 36 | loaded: progress.transferred, 37 | type: 'size', 38 | }); 39 | }, 40 | }, 41 | ); 42 | 43 | if (PlatformHelper.isWindows) { 44 | ZipHelper.unzip(javaFile, javaDir); 45 | } else { 46 | await mkdir(javaDir, { recursive: true }); 47 | await tar.x({ file: javaFile, cwd: javaDir }); 48 | } 49 | } 50 | 51 | async getJavaPath(majorVersion: number) { 52 | const path = ['bin', 'java']; 53 | if (PlatformHelper.isMac) { 54 | path.unshift('Contents', 'Home'); 55 | } 56 | 57 | const javaVerPath = this.#getJavaDir(majorVersion); 58 | const firstDir = (await readdir(javaVerPath))[0]; 59 | 60 | return join(javaVerPath, firstDir, ...path); 61 | } 62 | 63 | #getJavaDir(majorVersion: number) { 64 | return join(StorageHelper.javaDir, majorVersion.toString()); 65 | } 66 | 67 | #getOs() { 68 | const PlatformToJavaOS = { 69 | [Platform.WINDOWS]: JavaOs.WINDOWS, 70 | [Platform.MACOS]: JavaOs.MAC, 71 | [Platform.LINUX]: JavaOs.LINUX, 72 | }; 73 | return PlatformToJavaOS[process.platform] || process.platform; 74 | } 75 | 76 | #getArch() { 77 | const ArchitectureToJavaOS = { 78 | [Architecture.X32]: JavaArchitecture.X32, 79 | [Architecture.X64]: JavaArchitecture.X64, 80 | [Architecture.ARM]: JavaArchitecture.ARM, 81 | [Architecture.ARM64]: JavaArchitecture.ARM64, 82 | }; 83 | return ArchitectureToJavaOS[process.arch] || process.arch; 84 | } 85 | } 86 | 87 | enum JavaOs { 88 | WINDOWS = 'windows', 89 | MAC = 'mac', 90 | LINUX = 'linux', 91 | } 92 | 93 | enum JavaArchitecture { 94 | ARM = 'arm', 95 | ARM64 = 'aarch64', 96 | X32 = 'x86', 97 | X64 = 'x64', 98 | } 99 | -------------------------------------------------------------------------------- /src/main/game/LibrariesMatcher.ts: -------------------------------------------------------------------------------- 1 | import { Action, LibraryRule, Name } from '@aurora-launcher/core'; 2 | import { Architecture, Platform } from '../core/System'; 3 | 4 | export class LibrariesMatcher { 5 | static match(rules?: LibraryRule[]) { 6 | if (!rules || rules.length === 0) return true; 7 | 8 | let result; 9 | 10 | rules.forEach((rule) => { 11 | if (rule.action === Action.Allow) { 12 | result = true; 13 | if (rule.os) { 14 | result = 15 | this.isMatchedOs(rule.os.name) && 16 | this.isMatchedArch(rule.os.arch) && 17 | this.isMatchedVersion(rule.os.version); 18 | } 19 | } else { 20 | // Disallow 21 | result = false; 22 | if (rule.os) { 23 | result = 24 | !this.isMatchedOs(rule.os.name) || 25 | !this.isMatchedArch(rule.os.arch) || 26 | !this.isMatchedVersion(rule.os.version); 27 | } 28 | } 29 | }); 30 | 31 | return result; 32 | } 33 | 34 | private static isMatchedOs(os: Name) { 35 | return os ? this.mapOsToPlatform(os) === process.platform : true; 36 | } 37 | 38 | private static isMatchedArch(arch?: string) { 39 | // Не нашёл используется ли это где-то, но пожалуй оставлю, всё равно работает 40 | return arch ? this.mapArch(arch) === process.arch : true; 41 | } 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 44 | private static isMatchedVersion(version?: string) { 45 | // эта тема встречается только на старых версиях (например на 1.6.4) с macOS 46 | // поэтому думаю мы с этим даже не столкнёмся, но на всякий случай оставлю этот обработчик 47 | return true; 48 | } 49 | 50 | private static mapOsToPlatform(os: Name) { 51 | const osToPlatform = { 52 | [Name.Osx]: Platform.MACOS, 53 | [Name.Linux]: Platform.LINUX, 54 | [Name.Windows]: Platform.WINDOWS, 55 | }; 56 | return osToPlatform[os]; 57 | } 58 | 59 | private static mapArch(arch: string) { 60 | if (['x32', 'x86'].includes(arch)) { 61 | return Architecture.X32; 62 | } 63 | return arch; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/game/Starter.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import { delimiter, join } from 'path'; 3 | 4 | import { Profile, ZipHelper } from '@aurora-launcher/core'; 5 | import { LogHelper } from 'main/helpers/LogHelper'; 6 | import { StorageHelper } from 'main/helpers/StorageHelper'; 7 | import { coerce, gte, lte } from 'semver'; 8 | import { Service } from 'typedi'; 9 | 10 | import { AuthorizationService } from '../api/AuthorizationService'; 11 | import { LibrariesMatcher } from './LibrariesMatcher'; 12 | import { GameWindow } from './GameWindow'; 13 | import { JavaManager } from './JavaManager'; 14 | import { AuthlibInjector } from './AuthlibInjector'; 15 | 16 | import { api as apiConfig } from '@config'; 17 | import { PlatformHelper } from '../helpers/PlatformHelper'; 18 | import { Session } from '../../common/types'; 19 | 20 | @Service() 21 | export class Starter { 22 | constructor( 23 | private authorizationService: AuthorizationService, 24 | private gameWindow: GameWindow, 25 | private javaManager: JavaManager, 26 | private authlibInjector: AuthlibInjector, 27 | ) {} 28 | 29 | async start(clientArgs: Profile): Promise { 30 | const clientDir = join(StorageHelper.clientsDir, clientArgs.clientDir); 31 | 32 | const clientVersion = coerce(clientArgs.version); 33 | if (clientVersion === null) { 34 | throw new Error('Invalig client version'); 35 | } 36 | 37 | const userArgs = this.authorizationService.getCurrentSession(); 38 | if (!userArgs) { 39 | throw new Error('Auth requierd'); 40 | } 41 | 42 | const gameArgs: string[] = []; 43 | 44 | gameArgs.push('--version', clientArgs.version); 45 | gameArgs.push('--gameDir', clientDir); 46 | gameArgs.push('--assetsDir', StorageHelper.assetsDir); 47 | 48 | // TODO: add support legacy assets 49 | 50 | if (gte(clientVersion, '1.6.0')) { 51 | this.gameLauncher( 52 | gameArgs, 53 | clientArgs, 54 | clientVersion.version, 55 | userArgs, 56 | ); 57 | } else { 58 | gameArgs.push(userArgs.username); 59 | gameArgs.push(userArgs.accessToken); 60 | } 61 | 62 | const classPath = clientArgs.libraries 63 | .filter( 64 | (library) => 65 | library.type === 'library' && 66 | LibrariesMatcher.match(library.rules), 67 | ) 68 | .map(({ path }) => { 69 | return join(StorageHelper.librariesDir, path); 70 | }); 71 | classPath.push(join(clientDir, clientArgs.gameJar)); 72 | 73 | const jvmArgs = []; 74 | 75 | await this.authlibInjector.verify(); 76 | jvmArgs.push( 77 | `-javaagent:${this.authlibInjector.authlibFilePath}=${apiConfig.web}`, 78 | ); 79 | 80 | const nativesDirectory = this.prepareNatives(clientArgs); 81 | jvmArgs.push(`-Djava.library.path=${nativesDirectory}`); 82 | 83 | if (gte(clientVersion, '1.20.0')) { 84 | jvmArgs.push( 85 | `-Djna.tmpdir=${nativesDirectory}`, 86 | `-Dorg.lwjgl.system.SharedLibraryExtractPath=${nativesDirectory}`, 87 | `-Dio.netty.native.workdir=${nativesDirectory}`, 88 | ); 89 | } 90 | 91 | jvmArgs.push( 92 | ...clientArgs.jvmArgs.map((arg) => 93 | arg 94 | .replaceAll( 95 | '${library_directory}', 96 | StorageHelper.librariesDir, 97 | ) 98 | .replaceAll('${classpath_separator}', delimiter), 99 | ), 100 | ); 101 | 102 | if (PlatformHelper.isMac) { 103 | jvmArgs.push('-XstartOnFirstThread'); 104 | } 105 | 106 | jvmArgs.push('-cp', classPath.join(delimiter)); 107 | jvmArgs.push(clientArgs.mainClass); 108 | 109 | jvmArgs.push(...gameArgs); 110 | jvmArgs.push(...clientArgs.clientArgs); 111 | 112 | await this.javaManager.checkAndDownloadJava(clientArgs.javaVersion); 113 | 114 | const gameProccess = spawn( 115 | await this.javaManager.getJavaPath(clientArgs.javaVersion), 116 | jvmArgs, 117 | { cwd: clientDir }, 118 | ); 119 | 120 | gameProccess.stdout.on('data', (data: Buffer) => { 121 | const log = data.toString().trim(); 122 | this.gameWindow.sendToConsole(log); 123 | LogHelper.info(log); 124 | }); 125 | 126 | gameProccess.stderr.on('data', (data: Buffer) => { 127 | const log = data.toString().trim(); 128 | this.gameWindow.sendToConsole(log); 129 | LogHelper.error(log); 130 | }); 131 | 132 | gameProccess.on('close', () => { 133 | this.gameWindow.stopGame(); 134 | LogHelper.info('Game stop'); 135 | }); 136 | } 137 | private gameLauncher( 138 | gameArgs: string[], 139 | clientArgs: Profile, 140 | clientVersion: string, 141 | userArgs: Session, 142 | ): void { 143 | gameArgs.push('--username', userArgs.username); 144 | 145 | if (gte(clientVersion, '1.7.2')) { 146 | gameArgs.push('--uuid', userArgs.userUUID); 147 | gameArgs.push('--accessToken', userArgs.accessToken); 148 | 149 | if (gte(clientVersion, '1.7.3')) { 150 | gameArgs.push('--assetIndex', clientArgs.assetIndex); 151 | 152 | if (lte(clientVersion, '1.9.0')) { 153 | gameArgs.push('--userProperties', '{}'); 154 | } 155 | } 156 | 157 | if (gte(clientVersion, '1.7.4')) { 158 | gameArgs.push('--userType', 'mojang'); 159 | } 160 | 161 | if (gte(clientVersion, '1.9.0')) { 162 | gameArgs.push('--versionType', 'AuroraLauncher v0.0.9'); 163 | } 164 | } else { 165 | gameArgs.push('--session', userArgs.accessToken); 166 | } 167 | } 168 | 169 | prepareNatives(clientArgs: Profile) { 170 | const nativesDir = join( 171 | StorageHelper.clientsDir, 172 | clientArgs.clientDir, 173 | 'natives', 174 | ); 175 | 176 | clientArgs.libraries 177 | .filter( 178 | (library) => 179 | library.type === 'native' && 180 | LibrariesMatcher.match(library.rules), 181 | ) 182 | .forEach(({ path }) => { 183 | try { 184 | ZipHelper.unzip( 185 | join(StorageHelper.librariesDir, path), 186 | nativesDir, 187 | ['.so', '.dylib', '.jnilib', '.dll'], 188 | ); 189 | } catch (error) { 190 | LogHelper.error(error); 191 | } 192 | }); 193 | 194 | return nativesDir; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/game/Updater.ts: -------------------------------------------------------------------------------- 1 | import { mkdirSync } from 'fs'; 2 | import { writeFile } from 'fs/promises'; 3 | import { dirname, join } from 'path'; 4 | 5 | import { 6 | HashHelper, 7 | HttpHelper, 8 | JsonHelper, 9 | Profile, 10 | } from '@aurora-launcher/core'; 11 | import { api as apiConfig } from '@config'; 12 | import { StorageHelper } from 'main/helpers/StorageHelper'; 13 | import pMap from 'p-map'; 14 | import { Service } from 'typedi'; 15 | 16 | import { APIManager } from '../api/APIManager'; 17 | import { GameWindow } from './GameWindow'; 18 | import { LibrariesMatcher } from './LibrariesMatcher'; 19 | 20 | @Service() 21 | export class Updater { 22 | constructor( 23 | private api: APIManager, 24 | private gameWindow: GameWindow, 25 | ) {} 26 | 27 | async validateClient(clientArgs: Profile): Promise { 28 | await this.validateAssets(clientArgs); 29 | await this.validateLibraries(clientArgs); 30 | await this.validateGameFiles(clientArgs); 31 | } 32 | 33 | async validateAssets(clientArgs: Profile): Promise { 34 | this.gameWindow.sendToConsole('Load assets files'); 35 | 36 | const assetIndexPath = `indexes/${clientArgs.assetIndex}.json`; 37 | const filePath = join(StorageHelper.assetsDir, assetIndexPath); 38 | mkdirSync(dirname(filePath), { recursive: true }); 39 | 40 | const assetIndexUrl = this.getFileUrl(assetIndexPath, 'assets'); 41 | const assetFile = await HttpHelper.getResource(assetIndexUrl); 42 | await writeFile(filePath, assetFile); 43 | 44 | const { objects } = JsonHelper.fromJson(assetFile); 45 | 46 | const assetsHashes = Object.values(objects) 47 | .sort((a, b) => b.size - a.size) 48 | .map((hash) => ({ 49 | ...hash, 50 | path: `objects/${hash.hash.slice(0, 2)}/${hash.hash}`, 51 | })); 52 | 53 | const totalSize = assetsHashes.reduce( 54 | (prev, cur) => prev + cur.size, 55 | 0, 56 | ); 57 | let loaded = 0; 58 | 59 | await pMap( 60 | assetsHashes, 61 | async (hash) => { 62 | await this.validateAndDownloadFile( 63 | hash.path, 64 | hash.hash, 65 | StorageHelper.assetsDir, 66 | 'assets', 67 | ); 68 | 69 | this.gameWindow.sendProgress({ 70 | total: totalSize, 71 | loaded: (loaded += hash.size), 72 | type: 'size', 73 | }); 74 | }, 75 | { concurrency: 4 }, 76 | ); 77 | } 78 | 79 | async validateLibraries(clientArgs: Profile): Promise { 80 | this.gameWindow.sendToConsole('Load libraries files'); 81 | 82 | const usedLibraries = clientArgs.libraries.filter((library) => 83 | LibrariesMatcher.match(library.rules), 84 | ); 85 | 86 | let loaded = 0; 87 | 88 | await pMap( 89 | usedLibraries, 90 | async (library) => { 91 | await this.validateAndDownloadFile( 92 | library.path, 93 | library.sha1, 94 | StorageHelper.librariesDir, 95 | 'libraries', 96 | ); 97 | 98 | this.gameWindow.sendProgress({ 99 | total: usedLibraries.length, 100 | loaded: (loaded += 1), 101 | type: 'count', 102 | }); 103 | }, 104 | { concurrency: 4 }, 105 | ); 106 | } 107 | 108 | async validateGameFiles(clientArgs: Profile): Promise { 109 | this.gameWindow.sendToConsole('Load client files'); 110 | 111 | const hashes = await this.api.getUpdates(clientArgs.clientDir); 112 | if (!hashes) { 113 | throw new Error('Client not found'); 114 | } 115 | 116 | hashes.sort( 117 | (a: { size: number }, b: { size: number }) => b.size - a.size, 118 | ); 119 | const totalSize = hashes.reduce( 120 | (prev: any, cur: { size: any }) => prev + cur.size, 121 | 0, 122 | ); 123 | let loaded = 0; 124 | 125 | await pMap( 126 | hashes, 127 | async (hash: any) => { 128 | await this.validateAndDownloadFile( 129 | hash.path, 130 | hash.sha1, 131 | StorageHelper.clientsDir, 132 | 'clients', 133 | ); 134 | 135 | this.gameWindow.sendProgress({ 136 | total: totalSize, 137 | loaded: (loaded += hash.size), 138 | type: 'size', 139 | }); 140 | }, 141 | { concurrency: 4 }, 142 | ); 143 | } 144 | 145 | private getFileUrl( 146 | path: string, 147 | type: 'clients' | 'libraries' | 'assets', 148 | ): URL { 149 | return new URL( 150 | `files/${type}/${path.replace('\\', '/')}`, 151 | apiConfig.web, 152 | ); 153 | } 154 | 155 | async validateAndDownloadFile( 156 | path: string, 157 | sha1: string, 158 | rootDir: string, 159 | type: 'clients' | 'libraries' | 'assets', 160 | ): Promise { 161 | const filePath = join(rootDir, path); 162 | mkdirSync(dirname(filePath), { recursive: true }); 163 | 164 | const fileUrl = this.getFileUrl(path, type); 165 | 166 | try { 167 | const fileHash = await HashHelper.getHashFromFile(filePath, 'sha1'); 168 | if (fileHash === sha1) return; 169 | } catch (error) { 170 | // ignore not found file 171 | } 172 | 173 | try { 174 | await HttpHelper.downloadFile(fileUrl, filePath); 175 | } catch (error) { 176 | throw new Error(`file ${fileUrl} not found`); 177 | } 178 | } 179 | } 180 | 181 | // TODO: Move to @aurora-launcher/core 182 | /** 183 | * For assets 184 | */ 185 | export interface Assets { 186 | /** 187 | * Найдено в https://launchermeta.mojang.com/v1/packages/3d8e55480977e32acd9844e545177e69a52f594b/pre-1.6.json \ 188 | * до версии 1.6 (если точнее до снапшота 13w23b) 189 | */ 190 | map_to_resources?: boolean; 191 | /** 192 | * Найдено в https://launchermeta.mojang.com/v1/packages/770572e819335b6c0a053f8378ad88eda189fc14/legacy.json \ 193 | * начиная с версии версии 1.6 (если точнее с снапшота 13w24a) и до 1.7.2 (13w48b) 194 | */ 195 | virtual?: boolean; 196 | objects: { [key: string]: Asset }; 197 | } 198 | 199 | export interface Asset { 200 | hash: string; 201 | size: number; 202 | } 203 | -------------------------------------------------------------------------------- /src/main/game/Watcher.ts: -------------------------------------------------------------------------------- 1 | import { LogHelper } from 'main/helpers/LogHelper'; 2 | import { Service } from 'typedi'; 3 | 4 | @Service() 5 | export class Watcher { 6 | async watch() { 7 | LogHelper.info('Watcher not implemented in current version'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/helpers/LogHelper.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs'; 2 | import { EOL } from 'os'; 3 | import { format } from 'util'; 4 | 5 | import { StorageHelper } from './StorageHelper'; 6 | 7 | export class LogHelper { 8 | static readonly isDevEnabled = process.argv.includes('--dev'); 9 | static readonly isDebugEnabled = 10 | process.argv.includes('--debug') || process.argv.includes('--dev'); 11 | 12 | private static readonly logFileStream = createWriteStream( 13 | StorageHelper.logFile, 14 | { flags: 'a' }, 15 | ); 16 | 17 | static debug(msg: any, ...args: any): void { 18 | if (!this.isDebugEnabled) return; 19 | this.log(LogLevel.DEBUG, msg, ...args); 20 | } 21 | 22 | static dev(msg: any, ...args: any): void { 23 | if (!this.isDevEnabled) return; 24 | this.log(LogLevel.DEV, msg, ...args); 25 | } 26 | 27 | static error(msg: any, ...args: any): void { 28 | this.log(LogLevel.ERROR, msg, ...args); 29 | } 30 | 31 | static fatal(msg: any, ...args: any): void { 32 | this.log(LogLevel.FATAL, msg, ...args); 33 | process.exit(1); 34 | } 35 | 36 | static info(msg: any, ...args: any): void { 37 | this.log(LogLevel.INFO, msg, ...args); 38 | } 39 | 40 | static raw(msg: any, ...args: any): void { 41 | this.log(LogLevel.RAW, msg, ...args); 42 | } 43 | 44 | static warn(msg: any, ...args: any): void { 45 | this.log(LogLevel.WARN, msg, ...args); 46 | } 47 | 48 | private static log(level: LogLevel, msg: any, ...args: any) { 49 | if (level === LogLevel.RAW) return this.rawLog(msg, ...args); 50 | const date = new Date().toLocaleString(); 51 | this.rawLog(`${date} [${level.toUpperCase()}] ${msg}`, ...args); 52 | } 53 | 54 | private static rawLog(msg: any, ...args: any) { 55 | const message = format(msg, ...args) + EOL; 56 | process.stdout.write(message); 57 | this.logFileStream.write(message); 58 | } 59 | } 60 | 61 | enum LogLevel { 62 | DEBUG = 'debug', 63 | DEV = 'dev', 64 | ERROR = 'error', 65 | FATAL = 'fatal', 66 | INFO = 'info', 67 | RAW = 'raw', 68 | WARN = 'warn', 69 | } 70 | -------------------------------------------------------------------------------- /src/main/helpers/PlatformHelper.ts: -------------------------------------------------------------------------------- 1 | import process from 'process'; 2 | import { Platform } from '../core/System'; 3 | 4 | export class PlatformHelper { 5 | public static isMac = process.platform == Platform.MACOS; 6 | public static isLinux = process.platform == Platform.LINUX; 7 | public static isUnix = this.isMac || this.isLinux 8 | public static isWindows = process.platform == Platform.WINDOWS; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/helpers/StorageHelper.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs'; 2 | import { homedir } from 'os'; 3 | import { resolve } from 'path'; 4 | import { app } from 'electron'; 5 | import { PlatformHelper } from './PlatformHelper'; 6 | 7 | export class StorageHelper { 8 | static storageDir: string = this.getPlatformStorageDir(); 9 | static assetsDir: string = resolve(StorageHelper.storageDir, 'assets'); 10 | static clientsDir: string = resolve(StorageHelper.storageDir, 'clients'); 11 | static librariesDir: string = resolve( 12 | StorageHelper.storageDir, 13 | 'libraries', 14 | ); 15 | static javaDir: string = resolve(StorageHelper.storageDir, 'java'); 16 | static logFile: string = resolve(StorageHelper.storageDir, 'Launcher.log'); 17 | 18 | static createMissing(): void { 19 | if (!existsSync(this.storageDir)) mkdirSync(this.storageDir); 20 | if (!existsSync(this.assetsDir)) mkdirSync(this.assetsDir); 21 | if (!existsSync(this.clientsDir)) mkdirSync(this.clientsDir); 22 | if (!existsSync(this.librariesDir)) mkdirSync(this.librariesDir); 23 | if (!existsSync(this.javaDir)) mkdirSync(this.javaDir); 24 | } 25 | 26 | private static getPlatformStorageDir() { 27 | if (PlatformHelper.isMac) { 28 | return resolve(app.getPath('userData'), "../", "aurora-launcher"); 29 | } 30 | return resolve(homedir(), '.aurora-launcher'); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | // Без этой либы почему-то в лаунчере сурсмапы не работают 2 | import 'source-map-support/register'; 3 | import 'reflect-metadata'; 4 | 5 | import Container from 'typedi'; 6 | 7 | import { Launcher } from './core/Launcher'; 8 | 9 | Container.get(Launcher); 10 | -------------------------------------------------------------------------------- /src/main/scenes/Login.ts: -------------------------------------------------------------------------------- 1 | import { EVENTS } from 'common/channels'; 2 | import { ipcMain } from 'electron'; 3 | import { Service } from 'typedi'; 4 | 5 | import { AuthorizationService } from '../api/AuthorizationService'; 6 | import { IHandleable } from '../core/IHandleable'; 7 | 8 | @Service() 9 | export class LoginScene implements IHandleable { 10 | constructor(private authorizationService: AuthorizationService) {} 11 | 12 | initHandlers() { 13 | ipcMain.handle( 14 | EVENTS.SCENES.LOGIN.AUTH, 15 | (_, login: string, password: string) => 16 | this.authorizationService.authorize(login, password), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scenes/ServerPanel.ts: -------------------------------------------------------------------------------- 1 | import { EVENTS } from 'common/channels'; 2 | import { ipcMain } from 'electron'; 3 | import { Service } from 'typedi'; 4 | 5 | import { IHandleable } from '../core/IHandleable'; 6 | import { GameService } from '../game/GameService'; 7 | 8 | @Service() 9 | export class ServerPanelScene implements IHandleable { 10 | constructor(private gameService: GameService) {} 11 | 12 | initHandlers() { 13 | ipcMain.handle(EVENTS.SCENES.SERVER_PANEL.GET_PROFILE, () => 14 | this.gameService.getProfile(), 15 | ); 16 | ipcMain.handle(EVENTS.SCENES.SERVER_PANEL.GET_SERVER, () => 17 | this.gameService.getServer(), 18 | ); 19 | ipcMain.on(EVENTS.SCENES.SERVER_PANEL.START_GAME, () => 20 | this.gameService.startGame(), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scenes/ServersList.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@aurora-launcher/core'; 2 | import { EVENTS } from 'common/channels'; 3 | import { ipcMain } from 'electron'; 4 | import { Service } from 'typedi'; 5 | 6 | import { APIManager } from '../api/APIManager'; 7 | import { IHandleable } from '../core/IHandleable'; 8 | import { GameService } from '../game/GameService'; 9 | 10 | @Service() 11 | export class ServersListScene implements IHandleable { 12 | constructor( 13 | private apiService: APIManager, 14 | private gameService: GameService, 15 | ) {} 16 | 17 | initHandlers() { 18 | ipcMain.handle(EVENTS.SCENES.SERVERS_LIST.GET_SERVERS, () => 19 | this.apiService.getServers(), 20 | ); 21 | ipcMain.handle( 22 | EVENTS.SCENES.SERVERS_LIST.SELECT_SERVER, 23 | (_, server: Server) => this.gameService.setServer(server), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/preload/components/LoginScene.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | import { EVENTS } from '../../common/channels'; 4 | import { UserData } from '../../common/types'; 5 | 6 | export default class LoginScene { 7 | static auth(login: string, password: string): Promise { 8 | return ipcRenderer.invoke(EVENTS.SCENES.LOGIN.AUTH, login, password); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/preload/components/ServerPanelScene.ts: -------------------------------------------------------------------------------- 1 | import { Profile, Server } from '@aurora-launcher/core'; 2 | import { ipcRenderer } from 'electron'; 3 | 4 | import { EVENTS } from '../../common/channels'; 5 | import { LoadProgress } from '../../common/types'; 6 | 7 | export default class ServerPanelScene { 8 | static getProfile(): Promise { 9 | return ipcRenderer.invoke(EVENTS.SCENES.SERVER_PANEL.GET_PROFILE); 10 | } 11 | 12 | static getServer(): Promise { 13 | return ipcRenderer.invoke(EVENTS.SCENES.SERVER_PANEL.GET_SERVER); 14 | } 15 | 16 | static startGame( 17 | consoleListener: (string: string) => void, 18 | progressListener: (data: LoadProgress) => void, 19 | stopGameListener: () => void, 20 | ) { 21 | ipcRenderer.send(EVENTS.SCENES.SERVER_PANEL.START_GAME); 22 | 23 | ipcRenderer.on( 24 | EVENTS.SCENES.SERVER_PANEL.TEXT_TO_CONSOLE, 25 | (_, string) => { 26 | consoleListener(string); 27 | }, 28 | ); 29 | 30 | ipcRenderer.on(EVENTS.SCENES.SERVER_PANEL.LOAD_PROGRESS, (_, data) => { 31 | progressListener(data); 32 | }); 33 | 34 | ipcRenderer.once(EVENTS.SCENES.SERVER_PANEL.STOP_GAME, () => { 35 | ipcRenderer.removeAllListeners( 36 | EVENTS.SCENES.SERVER_PANEL.TEXT_TO_CONSOLE, 37 | ); 38 | ipcRenderer.removeAllListeners( 39 | EVENTS.SCENES.SERVER_PANEL.LOAD_PROGRESS, 40 | ); 41 | stopGameListener(); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/preload/components/ServersListScene.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@aurora-launcher/core'; 2 | import { ipcRenderer } from 'electron'; 3 | 4 | import { EVENTS } from '../../common/channels'; 5 | 6 | export default class ServersListScene { 7 | static getServers(): Promise { 8 | return ipcRenderer.invoke(EVENTS.SCENES.SERVERS_LIST.GET_SERVERS); 9 | } 10 | 11 | static selectServer(server: Server) { 12 | return ipcRenderer.invoke( 13 | EVENTS.SCENES.SERVERS_LIST.SELECT_SERVER, 14 | server, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/preload/components/Window.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | import { EVENTS } from '../../common/channels'; 4 | 5 | export default class Window { 6 | /** 7 | * Hide window 8 | */ 9 | static hide() { 10 | ipcRenderer.send(EVENTS.WINDOW.HIDE); 11 | } 12 | 13 | /** 14 | * Close window 15 | */ 16 | static close() { 17 | ipcRenderer.send(EVENTS.WINDOW.CLOSE); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron'; 2 | 3 | import LoginScene from './components/LoginScene'; 4 | import ServerPanel from './components/ServerPanelScene'; 5 | import ServersList from './components/ServersListScene'; 6 | import Window from './components/Window'; 7 | 8 | // export для типизации 9 | export const API = { 10 | window: { 11 | hide: Window.hide, 12 | close: Window.close, 13 | }, 14 | scenes: { 15 | login: { 16 | auth: LoginScene.auth, 17 | }, 18 | serversList: { 19 | getServers: ServersList.getServers, 20 | selectServer: ServersList.selectServer, 21 | }, 22 | serverPanel: { 23 | getProfile: ServerPanel.getProfile, 24 | getServer: ServerPanel.getServer, 25 | startGame: ServerPanel.startGame, 26 | }, 27 | }, 28 | }; 29 | 30 | contextBridge.exposeInMainWorld('launcherAPI', API); 31 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import { HashRouter, Route, Routes } from 'react-router-dom'; 2 | import { RecoilRoot } from 'recoil'; 3 | 4 | import Layout from './runtime/components/Layout'; 5 | import Login from './runtime/scenes/Login'; 6 | import ServerPanel from './runtime/scenes/ServerPanel'; 7 | import ServersList from './runtime/scenes/ServersList'; 8 | 9 | export default function App() { 10 | return ( 11 | 12 | 13 | 14 | }> 15 | } /> 16 | } /> 17 | } /> 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import './runtime/assets/sass/index.sass'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | 6 | import App from './App'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | , 12 | ); 13 | -------------------------------------------------------------------------------- /src/renderer/renderer.d.ts: -------------------------------------------------------------------------------- 1 | declare type LauncherAPI = typeof import('../preload/index').API; 2 | 3 | // Для использования window.launcherAPI 4 | declare interface Window { 5 | launcherAPI: LauncherAPI; 6 | } 7 | 8 | // Но можно использовать и просто launcherAPI 9 | declare const launcherAPI: LauncherAPI; 10 | -------------------------------------------------------------------------------- /src/renderer/runtime/assets/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraTeam/Launcher/9462126b73bcaf68dae50be9befd237a08696924/src/renderer/runtime/assets/images/background.png -------------------------------------------------------------------------------- /src/renderer/runtime/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraTeam/Launcher/9462126b73bcaf68dae50be9befd237a08696924/src/renderer/runtime/assets/images/logo.png -------------------------------------------------------------------------------- /src/renderer/runtime/assets/images/steve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraTeam/Launcher/9462126b73bcaf68dae50be9befd237a08696924/src/renderer/runtime/assets/images/steve.png -------------------------------------------------------------------------------- /src/renderer/runtime/assets/sass/index.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@400;700&display=swap') 2 | 3 | :root 4 | --primary-color: rgb(0, 128, 255) 5 | --primary-color-hover: rgb(0, 96, 192) 6 | --background-color: rgba(128, 128, 128, 0.1) 7 | --default-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25) 8 | 9 | body 10 | background-image: url('../images/background.png') 11 | margin: 0 12 | 13 | main 14 | height: calc(100vh - 40px) 15 | 16 | * 17 | font-family: 'Comfortaa', cursive 18 | transition-duration: .15s 19 | box-sizing: border-box 20 | user-select: none 21 | color: #fff 22 | 23 | button 24 | background-color: transparent 25 | padding: 0 26 | border: 0 27 | outline: 0 28 | cursor: pointer 29 | 30 | /* 31 | * Кастомизация скролла 32 | * Пример кода взят отсюда 33 | * https://www.cssscript.com/best-custom-scrollbar-javascript-libraries/ 34 | */ 35 | 36 | /* width */ 37 | ::-webkit-scrollbar 38 | width: 5px 39 | height: 5px 40 | 41 | /* Handle */ 42 | ::-webkit-scrollbar-thumb 43 | background: rgba(255, 255, 255, 0.2) 44 | border-radius: 3px 45 | 46 | ::-webkit-scrollbar-corner 47 | display: none 48 | 49 | [disabled] 50 | opacity: 0.75 51 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/If.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | interface IfProps { 4 | state: boolean; 5 | children: ReactElement; 6 | } 7 | 8 | export default function If({ state = false, children }: IfProps) { 9 | return state ? children : null; 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | 3 | import Modal from './Modal'; 4 | import TitleBar from './TitleBar'; 5 | 6 | export default function Layout() { 7 | return ( 8 | <> 9 | 10 |
11 | 12 |
13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/Modal/hooks.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { useSetRecoilState } from 'recoil'; 3 | 4 | import { modalContent, modalShow, modalTitle } from './states'; 5 | 6 | export function useModal() { 7 | const setShow = useSetRecoilState(modalShow); 8 | const setContent = useSetRecoilState(modalContent); 9 | const setTitle = useSetRecoilState(modalTitle); 10 | 11 | return { 12 | showModal: (title: string, content: ReactNode) => { 13 | setTitle(title); 14 | setContent(content); 15 | setShow(true); 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/Modal/index.module.sass: -------------------------------------------------------------------------------- 1 | .modalOverlay 2 | position: fixed 3 | top: 0 4 | left: 0 5 | right: 0 6 | bottom: 0 7 | display: flex 8 | align-items: center 9 | justify-content: center 10 | background-color: rgba(0, 0, 0, 0.5) 11 | opacity: 0 12 | pointer-events: none 13 | transition-duration: .3s 14 | &.show 15 | opacity: 1 16 | pointer-events: visible 17 | .modal 18 | transform: translateY(0) 19 | 20 | .modal 21 | background: var(--background-color) 22 | backdrop-filter: blur(10px) 23 | border-radius: 10px 24 | padding: 25px 25 | transform: translateY(-200px) 26 | transition-duration: .3s 27 | text-align: center 28 | max-width: 50% 29 | 30 | .button 31 | background-color: var(--primary-color) 32 | border-radius: 10px 33 | display: block 34 | margin: auto 35 | margin-top: 40px 36 | padding: 12px 40px 37 | color: #fff 38 | &:hover 39 | background-color: var(--primary-color-hover) 40 | 41 | .title 42 | font-size: 1.5rem 43 | 44 | .content 45 | margin-top: 20px 46 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useRecoilState, useRecoilValue } from 'recoil'; 3 | 4 | import classes from './index.module.sass'; 5 | import { modalContent, modalShow, modalTitle } from './states'; 6 | 7 | export default function Modal() { 8 | const [show, setShow] = useRecoilState(modalShow); 9 | const content = useRecoilValue(modalContent); 10 | const title = useRecoilValue(modalTitle); 11 | 12 | function closeModal() { 13 | setShow(false); 14 | } 15 | 16 | const closeOnEscapeKeyDown = (event: KeyboardEvent) => { 17 | if (event.code === 'Escape' || event.key === 'Escape') closeModal(); 18 | }; 19 | 20 | useEffect(() => { 21 | document.addEventListener('keydown', closeOnEscapeKeyDown); 22 | return () => 23 | document.removeEventListener('keydown', closeOnEscapeKeyDown); 24 | }, []); 25 | 26 | return ( 27 |
28 |
29 |
{title}
30 |
{content}
31 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/Modal/states.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { atom } from 'recoil'; 3 | 4 | export const modalShow = atom({ 5 | key: 'modal.show', 6 | default: false, 7 | }); 8 | 9 | export const modalTitle = atom({ 10 | key: 'modal.title', 11 | default: 'Modal title', 12 | }); 13 | 14 | export const modalContent = atom({ 15 | key: 'modal.content', 16 | default: 'Modal content', 17 | }); 18 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/ServerButton/index.module.sass: -------------------------------------------------------------------------------- 1 | .button 2 | background: rgba(21, 21, 21, 0.9) 3 | filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.4)) 4 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.4) 5 | border-radius: 5px 6 | border: 0 7 | color: #fff 8 | width: 350px 9 | height: 60px 10 | margin: 0 12px 25px auto 11 | padding: 16px 12 | display: flex 13 | align-items: center 14 | position: relative 15 | overflow: hidden 16 | &:last-child 17 | margin-bottom: 0 18 | .title 19 | font-weight: bold 20 | font-size: 24px 21 | margin-right: auto 22 | max-width: 200px 23 | white-space: nowrap 24 | text-overflow: ellipsis 25 | overflow: hidden 26 | .online 27 | font-size: 18px 28 | margin: 0 10px 29 | width: 74px 30 | .next 31 | width: 24px 32 | height: 24px 33 | display: flex 34 | align-items: center 35 | justify-content: center 36 | .progress 37 | position: absolute 38 | bottom: 0 39 | left: 0 40 | width: 100% 41 | height: 5px 42 | border: 0 43 | background-color: rgba(85, 85, 85, 0.3) 44 | .progressLine 45 | height: 100% 46 | background-color: var(--primary-color) 47 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/ServerButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Server } from '@aurora-launcher/core'; 2 | import classes from './index.module.sass'; 3 | import { usePingServer } from '../../hooks/pingServer'; 4 | 5 | interface ServerButtonProps { 6 | onClick: () => void; 7 | server: Server; 8 | } 9 | 10 | export function ServerButton({ onClick, server }: ServerButtonProps) { 11 | const { online, max } = usePingServer(server); 12 | 13 | return ( 14 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/SkinView.tsx: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useRef } from 'react'; 2 | import { IdleAnimation, SkinViewer, createOrbitControls } from 'skinview3d'; 3 | 4 | import defaultSkin from '../assets/images/steve.png'; 5 | 6 | export default function SkinView() { 7 | const skinCanvas = useRef() as MutableRefObject; 8 | 9 | useEffect(() => { 10 | const skinViewer = new SkinViewer({ 11 | canvas: skinCanvas.current, 12 | width: 220, 13 | height: 440, 14 | }); 15 | 16 | skinViewer.camera.position.x = -20; 17 | skinViewer.camera.position.y = 20; 18 | skinViewer.zoom = 0.8; 19 | 20 | skinViewer.animations.add(IdleAnimation); 21 | 22 | const control = createOrbitControls(skinViewer); 23 | control.enableZoom = false; 24 | 25 | // Поддержка загрузки и отображения скина 26 | const { skinUrl, capeUrl, isAlex } = JSON.parse( 27 | localStorage.getItem('userData') || '{}', 28 | ); 29 | if (skinUrl) { 30 | skinViewer.loadSkin(skinUrl); 31 | } else { 32 | // Fuck skinview (race condition moment) 33 | skinViewer.loadSkin(defaultSkin); 34 | } 35 | if (capeUrl) skinViewer.loadCape(capeUrl); 36 | if (isAlex) skinViewer.playerObject.skin.modelType = 'slim'; 37 | }, []); 38 | 39 | return ; 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/TitleBar/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useRecoilState, useSetRecoilState } from 'recoil'; 2 | 3 | import { titlebarBackBtn, titlebarTitle, titlebarUser } from './states'; 4 | 5 | export function useTitlebar() { 6 | const [titlebarBackBtnState, setTitlebarBackBtnState] = 7 | useRecoilState(titlebarBackBtn); 8 | 9 | function showTitlebarBackBtn() { 10 | if (!titlebarBackBtnState.show) { 11 | setTitlebarBackBtnState({ show: true }); 12 | } 13 | } 14 | 15 | function hideTitlebarBackBtn() { 16 | if (titlebarBackBtnState.show) { 17 | setTitlebarBackBtnState({ show: false }); 18 | } 19 | } 20 | 21 | const setTitlebarTitleState = useSetRecoilState(titlebarTitle); 22 | 23 | function showTitlebarTitle() { 24 | setTitlebarTitleState((state) => ({ ...state, show: true })); 25 | } 26 | 27 | function hideTitlebarTitle() { 28 | setTitlebarTitleState((state) => ({ ...state, show: false })); 29 | } 30 | 31 | function setTitlebarTitleText(text: string) { 32 | setTitlebarTitleState((state) => ({ ...state, text })); 33 | } 34 | 35 | const setTitlebarUserState = useSetRecoilState(titlebarUser); 36 | 37 | function showTitlebarUser() { 38 | setTitlebarUserState((state) => ({ ...state, show: true })); 39 | } 40 | 41 | function hideTitlebarUser() { 42 | setTitlebarUserState((state) => ({ ...state, show: false })); 43 | } 44 | 45 | function setTitlebarUserText(username: string) { 46 | setTitlebarUserState((state) => ({ ...state, username })); 47 | } 48 | 49 | return { 50 | showTitlebarBackBtn, 51 | hideTitlebarBackBtn, 52 | showTitlebarTitle, 53 | hideTitlebarTitle, 54 | setTitlebarTitleText, 55 | showTitlebarUser, 56 | hideTitlebarUser, 57 | setTitlebarUserText, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/TitleBar/index.module.sass: -------------------------------------------------------------------------------- 1 | .titlebar, 2 | .titlebar > div, 3 | .user 4 | display: flex 5 | align-items: center 6 | 7 | .titlebar 8 | padding: 8px 9 | -webkit-app-region: drag 10 | justify-content: space-between 11 | background: var(--background-color) 12 | box-shadow: var(--default-shadow) 13 | button 14 | -webkit-app-region: no-drag 15 | cursor: pointer 16 | height: 24px 17 | filter: drop-shadow(var(--default-shadow)) 18 | .back 19 | margin-right: 13px 20 | &:hover path 21 | fill: #ddd 22 | .hide 23 | &:hover path 24 | fill: #2f80ed 25 | .close 26 | margin-left: 7px 27 | &:hover path 28 | fill: #eb5757 29 | .user 30 | margin-right: 27px 31 | .username 32 | margin-left: 5px 33 | .text 34 | margin-top: 2px 35 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/TitleBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useRecoilValue } from 'recoil'; 3 | 4 | import If from '../If'; 5 | import classes from './index.module.sass'; 6 | import { titlebarBackBtn, titlebarTitle, titlebarUser } from './states'; 7 | 8 | export default function TitleBar() { 9 | const backBtn = useRecoilValue(titlebarBackBtn); 10 | const title = useRecoilValue(titlebarTitle); 11 | const user = useRecoilValue(titlebarUser); 12 | const navigate = useNavigate(); 13 | 14 | function hide() { 15 | launcherAPI.window.hide(); 16 | } 17 | function close() { 18 | launcherAPI.window.close(); 19 | } 20 | function historyBack() { 21 | navigate(-1); 22 | } 23 | 24 | return ( 25 |
26 |
27 | 28 | 36 | 37 | 38 | {title.text} 39 | 40 |
41 |
42 | 43 |
44 | 50 | 57 | 64 | 70 | 71 |
76 | {user.username} 77 |
78 |
79 |
80 | 85 | 93 |
94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/renderer/runtime/components/TitleBar/states.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | export const titlebarBackBtn = atom({ 4 | key: 'titlebar.backBtn', 5 | default: { 6 | show: false, 7 | }, 8 | }); 9 | 10 | export const titlebarTitle = atom({ 11 | key: 'titlebar.title', 12 | default: { 13 | show: true, 14 | text: 'AuroraLauncher v0.0.4', 15 | }, 16 | }); 17 | 18 | export const titlebarUser = atom({ 19 | key: 'titlebar.user', 20 | default: { 21 | show: false, 22 | username: 'Test', 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/renderer/runtime/hooks/pingServer.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@aurora-launcher/core'; 2 | import { useState, useEffect } from 'react'; 3 | 4 | export function usePingServer(server: Server) { 5 | const [players, setPlayers] = useState({ online: 0, max: 0 }); 6 | 7 | useEffect(() => { 8 | if (!server.ip) { 9 | return; 10 | } 11 | 12 | fetch( 13 | `https://mcapi.us/server/status?ip=${server.ip}&port=${server.port || 25565}`, 14 | ) 15 | .then((res) => res.json()) 16 | .then((res) => { 17 | if (!res.online) return; 18 | const { max, now } = res.players; 19 | setPlayers({ max, online: now }); 20 | }); 21 | }, [server]); 22 | 23 | return players; 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/runtime/scenes/Login/index.module.sass: -------------------------------------------------------------------------------- 1 | h1 2 | text-align: center 3 | .block 4 | width: 300px 5 | height: 450px 6 | background-color: var(--background-color) 7 | border-radius: 10px 8 | position: absolute 9 | top: 70px 10 | left: 0 11 | right: 0 12 | margin: auto 13 | box-shadow: var(--default-shadow) 14 | backdrop-filter: blur(10px) 15 | display: flex 16 | flex-direction: column 17 | align-items: center 18 | img 19 | height: 100px 20 | margin-top: 30px 21 | border-radius: 50% 22 | div 23 | font-size: 24px 24 | margin-top: 20px 25 | p 26 | margin: 20px 0 30px 27 | font-size: 14px 28 | font-weight: 300 29 | text-align: center 30 | letter-spacing: 0.02em 31 | form 32 | display: flex 33 | flex-direction: column 34 | align-items: center 35 | input 36 | width: 194px 37 | height: 35px 38 | margin-bottom: 15px 39 | border: 2px solid var(--primary-color) 40 | border-radius: 17px 41 | background-color: transparent 42 | text-align: center 43 | font-size: 13px 44 | font-weight: 300 45 | outline: 0 46 | &::placeholder 47 | color: #fff 48 | &:hover, &:focus 49 | border-color: var(--primary-color-hover) 50 | &::placeholder 51 | color: #ddd 52 | button 53 | width: 194px 54 | height: 35px 55 | margin-top: 15px 56 | border: 0 57 | border-radius: 17px 58 | background: linear-gradient(135deg, #7F47DD 0%, #2575FC 100%) 59 | font-size: 16px 60 | outline: 0 61 | color: #fff 62 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25) 63 | box-shadow: 0px 0px 7px rgba(0, 0, 0, 0.25) 64 | &:hover 65 | box-shadow: var(--default-shadow) 66 | filter: saturate(150%) 67 | -------------------------------------------------------------------------------- /src/renderer/runtime/scenes/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import logo from '../../assets/images/logo.png'; 5 | import { useModal } from '../../components/Modal/hooks'; 6 | import { useTitlebar } from '../../components/TitleBar/hooks'; 7 | import classes from './index.module.sass'; 8 | 9 | interface AuthData { 10 | [k: string]: string; 11 | login: string; 12 | password: string; 13 | } 14 | 15 | export default function Login() { 16 | const { showModal } = useModal(); 17 | const { setTitlebarUserText, showTitlebarUser } = useTitlebar(); 18 | const navigate = useNavigate(); 19 | 20 | const auth = async (event: FormEvent) => { 21 | event.preventDefault(); 22 | 23 | const formData = new FormData(event.currentTarget); 24 | const { login, password } = Object.fromEntries(formData) as AuthData; 25 | 26 | // Пример валидации 27 | if (login.length < 3) { 28 | return showModal( 29 | 'Ошибка ввода', 30 | 'Логин должен быть не менее 3-ёх символов', 31 | ); 32 | } 33 | // if (password.length < 8) { 34 | // return showModal( 35 | // 'Ошибка ввода', 36 | // 'Пароль должен быть не менее 8-ми символов' 37 | // ); 38 | // } 39 | 40 | let userData; 41 | try { 42 | userData = await launcherAPI.scenes.login.auth(login, password); 43 | } catch (error) { 44 | console.error(error); 45 | return showModal('Ошибка авторизации', (error as Error).message); 46 | } 47 | 48 | // Поддержка загрузки и отображения скина 49 | localStorage.setItem('userData', JSON.stringify(userData)); 50 | 51 | setTitlebarUserText(userData.username); 52 | showTitlebarUser(); 53 | navigate('ServersList'); 54 | }; 55 | 56 | return ( 57 |
58 | 59 |
Aurora Launcher
60 |

61 | Введите логин и пароль, 62 |
63 | чтобы продолжить 64 |

65 |
66 | 67 | 68 | 69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/renderer/runtime/scenes/ServerPanel/index.module.sass: -------------------------------------------------------------------------------- 1 | .window 2 | height: 100% 3 | padding: 20px 4 | background-color: rgba(0,0,0,.2) 5 | .info 6 | display: flex 7 | align-items: flex-start 8 | justify-content: space-between 9 | margin: 20px 0 10 | gap: 20px 11 | .title 12 | font-size: 60px 13 | white-space: nowrap 14 | text-overflow: ellipsis 15 | overflow: hidden 16 | .status 17 | display: flex 18 | align-items: center 19 | text-align: right 20 | font-weight: bold 21 | flex-shrink: 0 22 | .gamers 23 | font-size: 30px 24 | .line 25 | margin: 0 16px 26 | height: 60px 27 | width: 1px 28 | background-color: #E8E9F3 29 | .count 30 | font-size: 48px 31 | .total 32 | font-size: 18px 33 | color: #bdbdbd 34 | 35 | .content 36 | height: calc( 100% - 163px ) 37 | 38 | .buttons 39 | display: flex 40 | justify-content: flex-end 41 | gap: 10px 42 | margin-top: auto 43 | button 44 | cursor: pointer 45 | width: 180px 46 | height: 50px 47 | padding: 7px 19px 48 | border: 2px solid var(--primary-color) 49 | border-radius: 15px 50 | font-size: 20px 51 | outline: 0 52 | color: #fff 53 | box-shadow: var(--default-shadow) 54 | &:hover, &:disabled 55 | background-color: var(--primary-color) 56 | pre 57 | overflow: auto 58 | margin-top: 20px 59 | height: 220px 60 | padding: 10px 61 | border-radius: 3px 62 | font-family: monospace 63 | background-color: rgba(8, 8, 8, 0.8) 64 | .progress 65 | width: 500px 66 | height: 15px 67 | background-color: rgba(8, 8, 8, 0.5) 68 | border-radius: 7px 69 | overflow: hidden 70 | margin: 10px auto 71 | .progress-line 72 | width: 0 73 | height: 100% 74 | background-color: var(--primary-color) 75 | transition-duration: .5s 76 | .progress-info 77 | text-align: center 78 | margin-bottom: 10px 79 | -------------------------------------------------------------------------------- /src/renderer/runtime/scenes/ServerPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Profile, Server } from '@aurora-launcher/core'; 2 | import { MutableRefObject, useEffect, useRef, useState } from 'react'; 3 | 4 | import If from '../../components/If'; 5 | import { useTitlebar } from '../../components/TitleBar/hooks'; 6 | import classes from './index.module.sass'; 7 | import { LoadProgress } from '../../../../common/types'; 8 | import { usePingServer } from '../../hooks/pingServer'; 9 | 10 | // TODO Refactoring scene 11 | export default function ServerPanel() { 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | const [selectedProfile, setSelectedProfile] = useState({} as Profile); 14 | const [selectedServer, setSelectedServer] = useState({} as Server); 15 | const players = usePingServer(selectedServer); 16 | 17 | const [showConsole, setShowConsole] = useState(false); 18 | const [showProgress, setShowProgress] = useState(false); 19 | const [gameStarted, setGameStarted] = useState(false); 20 | 21 | const consoleRef = useRef() as MutableRefObject; 22 | const progressLine = useRef() as MutableRefObject; 23 | const progressInfo = useRef() as MutableRefObject; 24 | 25 | const { showTitlebarBackBtn, hideTitlebarBackBtn } = useTitlebar(); 26 | 27 | useEffect(() => { 28 | launcherAPI.scenes.serverPanel.getProfile().then(setSelectedProfile); 29 | launcherAPI.scenes.serverPanel.getServer().then(setSelectedServer); 30 | 31 | showTitlebarBackBtn(); 32 | }, []); 33 | 34 | const startGame = () => { 35 | hideTitlebarBackBtn(); 36 | setShowConsole(true); 37 | consoleRef.current?.replaceChildren(); 38 | setGameStarted(true); 39 | launcherAPI.scenes.serverPanel.startGame( 40 | textToConsole, 41 | progress, 42 | stopGame, 43 | ); 44 | }; 45 | 46 | const stopGame = () => { 47 | setGameStarted(false); 48 | showTitlebarBackBtn(); 49 | }; 50 | 51 | const textToConsole = (string: string) => { 52 | const consoleEl = consoleRef.current; 53 | if (!consoleEl) return; 54 | 55 | consoleEl.appendChild(document.createTextNode(string)); 56 | consoleEl.scrollTop = consoleEl.scrollHeight; 57 | }; 58 | 59 | const progress = ({ total, loaded, type }: LoadProgress) => { 60 | if (loaded < total) setShowProgress(true); 61 | 62 | const percent = (loaded / total) * 100; 63 | 64 | if (progressLine.current) { 65 | progressLine.current.style.width = percent.toFixed(2) + '%'; 66 | } 67 | setShowProgress(percent < 100); 68 | 69 | if (!progressInfo.current) return; 70 | 71 | if (type === 'count') { 72 | progressInfo.current.innerHTML = `Загружено ${loaded} из ${total}`; 73 | } else { 74 | progressInfo.current.innerHTML = `Загружено ${bytesToSize( 75 | loaded, 76 | )} из ${bytesToSize(total)}`; 77 | } 78 | }; 79 | 80 | return ( 81 |
82 |
83 |
{selectedServer.title}
84 |
85 |
86 | Игроков 87 |
88 | онлайн 89 |
90 |
91 |
92 | {players.online} 93 |
из {players.max}
94 |
95 |
96 |
97 |
98 | 99 | <> 100 |
101 |
105 |
106 |
110 | 111 |
112 | 113 |

114 |                 
115 |
116 |
117 | 120 |
121 |
122 | ); 123 | } 124 | 125 | function bytesToSize(bytes: number): string { 126 | const sizes = ['Bytes', 'KB', 'MB']; 127 | if (bytes === 0) return 'n/a'; 128 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 129 | if (i === 0) return `${bytes} ${sizes[i]}`; 130 | return `${(bytes / 1024 ** i).toFixed(2)} ${sizes[i]}`; 131 | } 132 | -------------------------------------------------------------------------------- /src/renderer/runtime/scenes/ServersList/index.module.sass: -------------------------------------------------------------------------------- 1 | .window 2 | display: flex 3 | align-items: center 4 | &>div 5 | width: 50% 6 | .skinView 7 | display: flex 8 | justify-content: center 9 | .serverList 10 | height: 400px 11 | margin: 36px 48px 74px 0 12 | overflow: auto 13 | -------------------------------------------------------------------------------- /src/renderer/runtime/scenes/ServersList/index.tsx: -------------------------------------------------------------------------------- 1 | import { Server } from '@aurora-launcher/core'; 2 | import { useEffect, useState } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import { ServerButton } from '../../components/ServerButton'; 6 | import SkinView from '../../components/SkinView'; 7 | import classes from './index.module.sass'; 8 | import { useTitlebar } from '../../components/TitleBar/hooks'; 9 | 10 | export default function ServersList() { 11 | const { hideTitlebarBackBtn } = useTitlebar(); 12 | hideTitlebarBackBtn(); 13 | 14 | const [servers, setServers] = useState([]); 15 | const navigate = useNavigate(); 16 | 17 | useEffect(() => { 18 | launcherAPI.scenes.serversList.getServers().then(setServers); 19 | }, []); 20 | 21 | const selectServer = async (server: Server) => { 22 | await launcherAPI.scenes.serversList.selectServer(server); 23 | navigate('/ServerPanel'); 24 | }; 25 | 26 | return ( 27 |
28 |
29 | 30 |
31 |
32 | {servers.map((server, i) => ( 33 | selectServer(server)} 37 | /> 38 | ))} 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "useDefineForClassFields": true, 9 | "allowJs": false, 10 | "skipLibCheck": true, 11 | "esModuleInterop": false, 12 | "resolveJsonModule": true, 13 | "allowSyntheticDefaultImports": true, 14 | "emitDecoratorMetadata": true, 15 | "experimentalDecorators": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "baseUrl": "src", 20 | "paths": { 21 | "@config": ["../config.ts"] 22 | }, 23 | "jsx": "react-jsx" 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import react from '@vitejs/plugin-react'; 4 | import { defineConfig } from 'vite'; 5 | 6 | const toDir = (dir: string) => join(__dirname, dir); 7 | 8 | export default defineConfig({ 9 | root: toDir('src/renderer'), 10 | cacheDir: toDir('node_modules/.vite'), 11 | base: '', 12 | build: { 13 | sourcemap: true, 14 | outDir: toDir('build/renderer'), 15 | assetsDir: '.', 16 | emptyOutDir: true, 17 | }, 18 | plugins: [react()], 19 | server: { port: 3000 }, 20 | resolve: { 21 | alias: [ 22 | { 23 | find: /@runtime\/(.*)/, 24 | replacement: toDir('src/renderer/runtime/$1.ts'), 25 | }, 26 | { 27 | find: /@scripts\/(.*)/, 28 | replacement: toDir('src/renderer/scripts/$1.ts'), 29 | }, 30 | ], 31 | }, 32 | }); 33 | --------------------------------------------------------------------------------