diff --git a/README.md b/README.md index 30491cc..41d1e44 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,19 @@ | Game version | Launcher version | Patch version | | :---: | :---: | :---: | -| 2.2.0 | 2.2.0-beta1 | 2.2.0-testing | +| 2.2.0 | 2.2.0-beta2 | 2.2.0-testing | Download from [Releases](https://notabug.org/nobody/an-anime-game-launcher/releases) +# Requirements + +To work this launcher requires + +* wine +* winecfg +* winetricks +* unzip + # Development ## Build from source @@ -28,6 +37,17 @@ npm run build:linux npm start ``` +# Roadmap + +To 2.2.0-release1 + +* Fix AppImage builds +* Make Proton-GE default compatibility tool and fix game input issues +* Add additional telemetry checking +* Add preferences menu +* Add background banners for different languages +* Add launcher updates notifications +
### Please, try to use "An Anime Game" phrase instead of the real game name to avoid search engines parsing \ No newline at end of file diff --git a/package.json b/package.json index fcbabd2..3103add 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "an-anime-game-linux-launcher", - "version": "2.2.0-beta1", + "version": "2.2.0-beta2", "description": "An Anime Game Linux Launcher", "author": "Nikita Podvirnyy", "license": "GPL-3.0", diff --git a/public/patch/patch_anti_logincrash.sh b/public/patch/patch_anti_logincrash.sh index 78ed72f..adb6757 100644 --- a/public/patch/patch_anti_logincrash.sh +++ b/public/patch/patch_anti_logincrash.sh @@ -1,10 +1,5 @@ #!/usr/bin/env bash -echo "[NOTE] This patch is not required as of 2021-10-13. However, it might become" -echo " necessary afterwards (Friday?). If that's the case, comment the line below." -exit 0 - - # MacOS and *BSD do not have md5sum: use md5 instead if [[ $(uname) == "Darwin" || $(uname) == *"BSD" ]]; then md5sum() { @@ -41,13 +36,6 @@ echo "[INFO] Patch to fix a login and runtime crash" echo "" # =========================================================== -echo "[WARNING] Hereby you are violating the game's Terms of Service!" -echo " Do you accept the risk and possible consequences?" -read -p "Accept? [y/n] " choice - -if [[ ! "$choice" == [JjSsYy]* ]]; then - exit 1 -fi echo echo "--- Applying xLua patch" diff --git a/src/ts/Genshinlib.ts b/src/ts/Genshinlib.ts index 08d46dd..d454bc3 100644 --- a/src/ts/Genshinlib.ts +++ b/src/ts/Genshinlib.ts @@ -21,6 +21,7 @@ export class Genshinlib public static readonly patchDir: string = path.join(path.dirname(__dirname), 'patch'); public static readonly patchJson: string = path.join(this.patchDir, 'patch.json'); public static readonly patchSh = path.join(this.patchDir, 'patch.sh'); + public static readonly patchAntiCrashSh = path.join(this.patchDir, 'patch_anti_logincrash.sh'); public static readonly launcherDir: string = path.join(os.homedir(), 'genshin-impact-launcher'); public static readonly launcherJson: string = path.join(this.launcherDir, 'launcher.json'); @@ -186,13 +187,14 @@ export class Genshinlib 'Executing load_times', 'Executing load_trebuchet', 'Executing load_verdana', - 'Executing load_webdings' + 'Executing load_webdings', + 'Executing load_usetakefocus n' ]; return new Promise((resolve) => { let installationProgress = 0; - let installerProcess = spawn('winetricks', ['corefonts'], { + let installerProcess = spawn('winetricks', ['corefonts', 'usetakefocus=n'], { env: { ...process.env, WINEPREFIX: path @@ -221,4 +223,38 @@ export class Genshinlib { return fs.existsSync(path.join(prefixPath, 'drive_c')); } + + public static applyPatch (onFinish: () => void, onData: (data: string) => void) + { + let patcherProcess = spawn('bash', [Genshinlib.patchSh], { + cwd: Genshinlib.gameDir, + env: { + ...process.env, + WINEPREFIX: Genshinlib.prefixDir + } + }); + + patcherProcess.stdout.on('data', (data: string) => onData(data)); + + patcherProcess.on('close', () => { + let patcherAntiCrashProcess = spawn('bash', [Genshinlib.patchAntiCrashSh], { + cwd: Genshinlib.gameDir, + env: { + ...process.env, + WINEPREFIX: Genshinlib.prefixDir + } + }); + + patcherAntiCrashProcess.stdout.on('data', (data: string) => onData(data)); + + patcherAntiCrashProcess.on('close', () => { + Genshinlib.setConfig({ + ...Genshinlib.getConfig(), + patch: Genshinlib.getPatchInfo() + }); + + onFinish(); + }); + }); + } } \ No newline at end of file diff --git a/src/ts/LauncherUI.ts b/src/ts/LauncherUI.ts new file mode 100644 index 0000000..43a7131 --- /dev/null +++ b/src/ts/LauncherUI.ts @@ -0,0 +1,150 @@ +import $ from 'cash-dom'; + +type LauncherState = + 'patch-unavailable' | + 'test-patch-available' | + 'patch-applying' | + 'game-update-available' | + 'game-installation-available' | + 'game-launch-available'; + +export class LauncherUI +{ + protected static _launcherState: LauncherState = 'game-launch-available'; + + public static get launcherState(): LauncherState + { + return this._launcherState; + } + + public static setState (state: LauncherState) + { + $('#downloader-panel').css('display', 'none'); + $('#launch').css('display', 'block'); + + switch (state) + { + case 'patch-unavailable': + $('#launch').text('Patch required'); + $('#launch').attr('disabled', 'disabled'); + + $('#launch').addClass('hint--top') + .addClass('hint--medium'); + + $('#launch').attr('data-hint', 'This game version doesn\'t have the anti-cheat patch. Please, wait a few days before it will be created'); + + break; + + case 'test-patch-available': + $('#launch').text('Apply test patch'); + + $('#launch').addClass('button-blue') + .addClass('hint--top') + .addClass('hint--large'); + + $('#launch').attr('data-hint', 'This game version has the anti-cheat patch, but it is in the test phase. You can wait a few days until it will become stable or apply it on your own risc'); + + break; + + case 'patch-applying': + $('#launch').text('Applying patch'); + $('#launch').attr('disabled', 'disabled'); + + break; + + case 'game-update-available': + $('#launch').text('Update'); + + break; + + case 'game-installation-available': + $('#launch').text('Install'); + + break; + + case 'game-launch-available': + $('#launch').removeAttr('disabled') + .removeAttr('data-hint'); + + $('#launch').removeClass('button-blue') + .removeClass('hint--top') + .removeClass('hint--medium') + .removeClass('hint--large'); + + $('#launch').text('Launch'); + + break; + } + + this._launcherState = state; + } + + protected static progressBar = { + beganAt: 0, + prevTime: 0, + temp: 0 + }; + + public static initProgressBar (): void + { + this.progressBar = { + beganAt: Date.now(), + prevTime: Date.now(), + temp: 0 + }; + + $('#downloaded').text(''); + $('#speed').text(''); + $('#eta').text(''); + + $('#downloader .progress').css('width', '0'); + + $('#downloader-panel').css('display', 'block'); + $('#launch').css('display', 'none'); + } + + public static updateProgressBar (prefix: string, current: number, total: number, difference: number): void + { + $('#downloaded').text(`${prefix}: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`); + + this.progressBar.temp += difference; + + if (Date.now() - this.progressBar.prevTime > 1000) + { + let elapsed = (Date.now() - this.progressBar.beganAt) / 1000; + let eta = Math.round(total * elapsed / current - elapsed); + + let etaHours = Math.floor(eta / 3600), + etaMinutes = Math.floor((eta - etaHours * 3600) / 60), + etaSeconds = eta - etaHours * 3600 - etaMinutes * 60; + + if (etaHours < 10) // @ts-expect-error + etaHours = '0' + etaHours.toString(); + + if (etaMinutes < 10) // @ts-expect-error + etaMinutes = '0' + etaMinutes.toString(); + + if (etaSeconds < 10) // @ts-expect-error + etaSeconds = '0' + etaSeconds.toString(); + + $('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`); + $('#speed').text(`${ (this.progressBar.temp / (Date.now() - this.progressBar.prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`); + $('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`); + + this.progressBar.prevTime = Date.now(); + this.progressBar.temp = 0; + } + } + + public static clearProgressBar(): void + { + $('#downloader-panel').css('display', 'none'); + $('#launch').css('display', 'block'); + + $('#downloaded').text(''); + $('#speed').text(''); + $('#eta').text(''); + + $('#downloader .progress').css('width', '0'); + } +} \ No newline at end of file diff --git a/src/ts/index.ts b/src/ts/index.ts index a917173..cb81c35 100644 --- a/src/ts/index.ts +++ b/src/ts/index.ts @@ -1,11 +1,12 @@ const path = require('path'); const fs = require('fs'); -const { exec, spawn } = require('child_process'); +const { exec } = require('child_process'); const { ipcRenderer } = require('electron'); import $ from 'cash-dom'; import { Genshinlib } from './Genshinlib'; +import { LauncherUI } from './LauncherUI'; if (!fs.existsSync(Genshinlib.prefixDir)) fs.mkdirSync(Genshinlib.prefixDir, { recursive: true }); @@ -16,28 +17,17 @@ $(() => { $('body').css('background-image', `url(${ Genshinlib.getBackgroundUri() })`); - // TODO: create LauncherUI class and move a lot of code there - // because it becomes a fucking unfunny joke - Genshinlib.getData().then(data => { // Update available if (Genshinlib.version != data.game.latest.version) - $('#launch').text(Genshinlib.version === null ? 'Install' : 'Update'); + LauncherUI.setState(Genshinlib.version === null ? 'game-installation-available' : 'game-update-available'); // Patch version is incorrect else if (Genshinlib.getConfig().patch.version != Genshinlib.getPatchInfo().version) { // Patch is not available if (Genshinlib.getPatchInfo().version !== data.game.latest.version) - { - $('#launch').attr('disabled', 'disabled'); - $('#launch').text('Patch required'); - - $('#launch').addClass('hint--top'); - $('#launch').addClass('hint--medium'); - - $('#launch').attr('data-hint', 'This game version doesn\'t have the anti-cheat patch. Please, wait a few days before it will be created'); - } + LauncherUI.setState('patch-unavailable'); // Patch available else if (Genshinlib.getPatchInfo().version === data.game.latest.version) @@ -47,41 +37,15 @@ $(() => { { console.log(`%c> Applying patch...`, 'font-size: 16px'); - $('#launch').attr('disabled', 'disabled'); - $('#launch').text('Applying patch...'); + LauncherUI.setState('patch-applying'); - let patcherProcess = spawn('bash', [Genshinlib.patchSh], { - cwd: Genshinlib.gameDir, - env: { - ...process.env, - WINEPREFIX: Genshinlib.prefixDir - } - }); - - patcherProcess.stdout.on('data', (data: string) => console.log(data.toString())); - - patcherProcess.on('close', () => { - Genshinlib.setConfig({ - ...Genshinlib.getConfig(), - patch: Genshinlib.getPatchInfo() - }); - - $('#launch').removeAttr('disabled'); - $('#launch').text('Launch'); - }); + Genshinlib.applyPatch(() => { + LauncherUI.setState('game-launch-available'); + }, (data) => console.log(data.toString())); } // Patch is in testing phase - else - { - $('#launch').text('Apply test patch'); - - $('#launch').addClass('button-blue'); - $('#launch').addClass('hint--top'); - $('#launch').addClass('hint--large'); - - $('#launch').attr('data-hint', 'This game version has the anti-cheat patch, but it is in the test phase. You can wait a few days until it will become stable or apply it on your own risc'); - } + else LauncherUI.setState('test-patch-available'); } } @@ -91,28 +55,11 @@ $(() => { { console.log(`%c> Applying patch...`, 'font-size: 16px'); - $('#launch').attr('disabled', 'disabled'); - $('#launch').text('Applying patch...'); + LauncherUI.setState('patch-applying'); - let patcherProcess = spawn('bash', [Genshinlib.patchSh], { - cwd: Genshinlib.gameDir, - env: { - ...process.env, - WINEPREFIX: Genshinlib.prefixDir - } - }); - - patcherProcess.stdout.on('data', (data: string) => console.log(data.toString())); - - patcherProcess.on('close', () => { - Genshinlib.setConfig({ - ...Genshinlib.getConfig(), - patch: Genshinlib.getPatchInfo() - }); - - $('#launch').removeAttr('disabled'); - $('#launch').text('Launch'); - }); + Genshinlib.applyPatch(() => { + LauncherUI.setState('game-launch-available'); + }, (data) => console.log(data.toString())); } $('#launch').on('click', async () => { @@ -168,34 +115,11 @@ $(() => { { console.log(`%c> Applying patch...`, 'font-size: 16px'); - $('#launch').attr('disabled', 'disabled'); - $('#launch').text('Applying patch...'); + LauncherUI.setState('patch-applying'); - let patcherProcess = spawn('bash', [Genshinlib.patchSh], { - cwd: Genshinlib.gameDir, - env: { - ...process.env, - WINEPREFIX: Genshinlib.prefixDir - } - }); - - patcherProcess.stdout.on('data', (data: string) => console.log(data.toString())); - - patcherProcess.on('close', () => { - Genshinlib.setConfig({ - ...Genshinlib.getConfig(), - patch: Genshinlib.getPatchInfo() - }); - - $('#launch').removeClass('button-blue'); - $('#launch').removeClass('hint--top'); - $('#launch').removeClass('hint--large'); - - $('#launch').removeAttr('disabled'); - $('#launch').removeAttr('data-hint'); - - $('#launch').text('Launch'); - }); + Genshinlib.applyPatch(() => { + LauncherUI.setState('game-launch-available'); + }, (data) => console.log(data.toString())); } // Installing game @@ -203,9 +127,6 @@ $(() => { { console.log(`%c> Downloading game data...`, 'font-size: 16px'); - $('#launch').css('display', 'none'); - $('#downloader-panel').css('display', 'block'); - let diff = { path: data.game.latest.path, name: `latest-${data.game.latest.version}.zip`, @@ -223,41 +144,14 @@ $(() => { if (fs.existsSync(path.join(Genshinlib.gameDir, diff.name))) fs.unlinkSync(path.join(Genshinlib.gameDir, diff.name)); - let beganAt = Date.now(), prevTime = Date.now(), downloaded = 0; - /** * Downloading game */ + LauncherUI.initProgressBar(); + Genshinlib.downloadFile(diff.path, path.join(Genshinlib.launcherDir, diff.name), (current: number, total: number, difference: number) => { - $('#downloaded').text(`Downloaded: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`); - - downloaded += difference; - - if (Date.now() - prevTime > 1000) - { - let eta = Math.round(total / current * (Date.now() - beganAt) / 1000); // seconds - - let etaHours = Math.floor(eta / 3600), - etaMinutes = Math.floor((eta - etaHours * 3600) / 60), - etaSeconds = eta - etaHours * 3600 - etaMinutes * 60; - - if (etaHours < 10) // @ts-expect-error - etaHours = '0' + etaHours.toString(); - - if (etaMinutes < 10) // @ts-expect-error - etaMinutes = '0' + etaMinutes.toString(); - - if (etaSeconds < 10) // @ts-expect-error - etaSeconds = '0' + etaSeconds.toString(); - - $('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`); - $('#speed').text(`${ (downloaded / (Date.now() - prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`); - $('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`); - - prevTime = Date.now(); - downloaded = 0; - } + LauncherUI.updateProgressBar('Downloaded', current, total, difference); }).then(() => { /** * Unpacking downloaded game @@ -265,44 +159,18 @@ $(() => { console.log(`%c> Unpacking game data...`, 'font-size: 16px'); - $('#speed').text(''); - $('#eta').text(''); - if (!fs.existsSync(Genshinlib.gameDir)) fs.mkdirSync(Genshinlib.gameDir, { recursive: true }); - - let beganAt = Date.now(), prevTime = Date.now(), unpacked = 0; + + LauncherUI.initProgressBar(); Genshinlib.unzip(path.join(Genshinlib.launcherDir, diff.name), Genshinlib.gameDir, (current: number, total: number, difference: number) => { - $('#downloaded').text(`Unpacking: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`); - - unpacked += difference; - - if (Date.now() - prevTime > 1000) - { - let eta = Math.round(total / current * (Date.now() - beganAt) / 1000); // seconds - - let etaHours = Math.floor(eta / 3600), - etaMinutes = Math.floor((eta - etaHours * 3600) / 60), - etaSeconds = eta - etaHours * 3600 - etaMinutes * 60; - - if (etaHours < 10) // @ts-expect-error - etaHours = '0' + etaHours.toString(); - - if (etaMinutes < 10) // @ts-expect-error - etaMinutes = '0' + etaMinutes.toString(); - - if (etaSeconds < 10) // @ts-expect-error - etaSeconds = '0' + etaSeconds.toString(); - - $('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`); - $('#speed').text(`${ (unpacked / (Date.now() - prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`); - $('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`); - - prevTime = Date.now(); - unpacked = 0; - } + LauncherUI.updateProgressBar('Unpacking', current, total, difference); }).then(() => { + /** + * Downloading voice data + */ + console.log(`%c> Downloading voice data...`, 'font-size: 16px'); fs.unlinkSync(path.join(Genshinlib.launcherDir, diff.name)); @@ -317,82 +185,21 @@ $(() => { break; } - let beganAt = Date.now(), prevTime = Date.now(), downloaded = 0; - - /** - * Downloading voice data - */ + LauncherUI.initProgressBar(); Genshinlib.downloadFile(voicePack.path, path.join(Genshinlib.launcherDir, voicePack.name), (current: number, total: number, difference: number) => { - $('#downloaded').text(`Downloaded: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`); - - downloaded += difference; - - if (Date.now() - prevTime > 1000) - { - let eta = Math.round(total / current * (Date.now() - beganAt) / 1000); // seconds - - let etaHours = Math.floor(eta / 3600), - etaMinutes = Math.floor((eta - etaHours * 3600) / 60), - etaSeconds = eta - etaHours * 3600 - etaMinutes * 60; - - if (etaHours < 10) // @ts-expect-error - etaHours = '0' + etaHours.toString(); - - if (etaMinutes < 10) // @ts-expect-error - etaMinutes = '0' + etaMinutes.toString(); - - if (etaSeconds < 10) // @ts-expect-error - etaSeconds = '0' + etaSeconds.toString(); - - $('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`); - $('#speed').text(`${ (downloaded / (Date.now() - prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`); - $('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`); - - prevTime = Date.now(); - downloaded = 0; - } + LauncherUI.updateProgressBar('Downloaded', current, total, difference); }).then(() => { /** * Unpacking downloaded game */ - console.log(`%c> Unpacking voice data...`, 'font-size: 16px'); - - $('#speed').text(''); - $('#eta').text(''); + console.log(`%c> Unpacking voice data...`, 'font-size: 16px'); - let beganAt = Date.now(), prevTime = Date.now(), unpacked = 0; + LauncherUI.initProgressBar(); Genshinlib.unzip(path.join(Genshinlib.launcherDir, voicePack.name), Genshinlib.gameDir, (current: number, total: number, difference: number) => { - $('#downloaded').text(`Unpacking: ${ Math.round(current / total * 100) }% (${ (current / 1024 / 1024 / 1024).toFixed(2) } GB / ${ Math.round(total / 1024 / 1024 / 1024).toFixed(2) } GB)`); - - unpacked += difference; - - if (Date.now() - prevTime > 1000) - { - let eta = Math.round(total / current * (Date.now() - beganAt) / 1000); // seconds - - let etaHours = Math.floor(eta / 3600), - etaMinutes = Math.floor((eta - etaHours * 3600) / 60), - etaSeconds = eta - etaHours * 3600 - etaMinutes * 60; - - if (etaHours < 10) // @ts-expect-error - etaHours = '0' + etaHours.toString(); - - if (etaMinutes < 10) // @ts-expect-error - etaMinutes = '0' + etaMinutes.toString(); - - if (etaSeconds < 10) // @ts-expect-error - etaSeconds = '0' + etaSeconds.toString(); - - $('#downloader .progress').css('width', `${ Math.round(current / total * 100) }%`); - $('#speed').text(`${ (unpacked / (Date.now() - prevTime) * 1000 / 1024 / 1024).toFixed(2) } MB/s`); - $('#eta').text(`ETA: ${etaHours}:${etaMinutes}:${etaSeconds}`); - - prevTime = Date.now(); - unpacked = 0; - } + LauncherUI.updateProgressBar('Unpacking', current, total, difference); }).then(() => { fs.unlinkSync(path.join(Genshinlib.launcherDir, voicePack.name)); @@ -408,45 +215,16 @@ $(() => { console.log(`%c> Applying patch...`, 'font-size: 16px'); + // patch-applying state changes only button text $('#downloaded').text('Applying patch...'); - let patcherProcess = spawn('bash', [Genshinlib.patchSh], { - cwd: Genshinlib.gameDir, - env: { - ...process.env, - WINEPREFIX: Genshinlib.prefixDir - } - }); - - patcherProcess.stdout.on('data', (data: string) => console.log(data.toString())); - - patcherProcess.on('close', () => { - Genshinlib.setConfig({ - ...Genshinlib.getConfig(), - patch: Genshinlib.getPatchInfo() - }); - - $('#launch').css('display', 'block'); - $('#downloader-panel').css('display', 'none'); - - $('#launch').text('Launch'); - }); + Genshinlib.applyPatch(() => { + LauncherUI.setState('game-launch-available'); + }, (data) => console.log(data.toString())); } // Patch is not available - else - { - $('#launch').css('display', 'block'); - $('#downloader-panel').css('display', 'none'); - - $('#launch').attr('disabled', 'disabled'); - $('#launch').text('Patch required'); - - $('#launch').addClass('hint--top'); - $('#launch').addClass('hint--medium'); - - $('#launch').attr('data-hint', 'This game version doesn\'t have the anti-cheat patch. Please, wait a few days before it will be created'); - } + else LauncherUI.setState('patch-unavailable'); }); }).catch(err => console.log(err)); }).catch(err => console.log(err));