From 7f4f14d76ba2708beedc1c6840c0f306deb7b988 Mon Sep 17 00:00:00 2001 From: Observer KRypt0n_ Date: Mon, 27 Dec 2021 16:08:06 +0200 Subject: [PATCH] Severl changes - disabled neutralino window hot reloading - fixed `Game.current` version gathering from the game files - fixed `Patch.getPatchInfo()` method working, so now this function and the `Patch.current` field will return correct information whether the patch was applied or not - `Patch.install()` method now can return null if the patch state is "preparation" - added `Voice.selected` field that will represent the `lang.voice` config - fixed `Voice.getDiff()` method errors - improved `Archive`'s stream unpacked files listing - made `Prefix` class, some `Runners` methods from the previous update were moved there - fixed `ProgressBar.init()` method work which also fixes ETA calculation - added bunch of new launcher states and events for them - added `State.update()` method to automatically update launcher's state - `Process` object now can get the output of the process that was started by the `Process.run()` method and can be accesed by the `output` event - also was removed `input` field for the `Process.run()` method options --- package.json | 2 +- public/locales/en-us.yaml | 2 +- public/locales/ru-ru.yaml | 2 +- src/ts/Game.ts | 7 +- src/ts/Launcher.ts | 29 +----- src/ts/Patch.ts | 26 ++++- src/ts/Voice.ts | 13 ++- src/ts/core/Archive.ts | 29 ++++-- src/ts/core/Prefix.ts | 125 +++++++++++++++++++++++++ src/ts/core/Runners.ts | 91 ------------------ src/ts/launcher/ProgressBar.ts | 12 ++- src/ts/launcher/State.ts | 85 ++++++++++++++++- src/ts/launcher/states/ApplyPatch.ts | 39 ++++++++ src/ts/launcher/states/CreatePrefix.ts | 51 ++++++++++ src/ts/launcher/states/Install.ts | 40 ++++---- src/ts/launcher/states/InstallVoice.ts | 43 +++++++++ src/ts/neutralino/Process.ts | 73 ++++++++++++--- src/ts/types/Launcher.ts | 20 +++- 18 files changed, 506 insertions(+), 183 deletions(-) create mode 100644 src/ts/core/Prefix.ts create mode 100644 src/ts/launcher/states/ApplyPatch.ts create mode 100644 src/ts/launcher/states/CreatePrefix.ts create mode 100644 src/ts/launcher/states/InstallVoice.ts diff --git a/package.json b/package.json index 95bb4ea..abbca2b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "neu": "neu", - "dev": "vite build && neu run", + "dev": "vite build && neu run --disable-auto-reload", "build": "vite build && neu build --release", "check": "svelte-check --tsconfig ./tsconfig.json" }, diff --git a/public/locales/en-us.yaml b/public/locales/en-us.yaml index 3716653..5460d01 100644 --- a/public/locales/en-us.yaml +++ b/public/locales/en-us.yaml @@ -46,7 +46,7 @@ settings: gamemode: GameMode fps_unlocker: Unlock FPS - purge_dxvk_logs: Auto-delete DXVK logs + purge_dxvk_logs: Delete DXVK logs # DXVKs dxvks: diff --git a/public/locales/ru-ru.yaml b/public/locales/ru-ru.yaml index 4937145..bdf5c38 100644 --- a/public/locales/ru-ru.yaml +++ b/public/locales/ru-ru.yaml @@ -47,7 +47,7 @@ settings: # TODO: add hints to the components so I could describe what these options means gamemode: GameMode fps_unlocker: Разблокировать FPS - purge_dxvk_logs: Автоматически удалять логи DXVK + purge_dxvk_logs: Удалять логи DXVK # DXVKs dxvks: diff --git a/src/ts/Game.ts b/src/ts/Game.ts index b3548fa..8055aa5 100644 --- a/src/ts/Game.ts +++ b/src/ts/Game.ts @@ -35,9 +35,10 @@ export default class Game Neutralino.filesystem.readFile(persistentPath) .then((version) => resolve(version)) .catch(() => { - Neutralino.filesystem.readFile(globalGameManagersPath) - .then((config) => { - const version = /([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+/.exec(config); + Neutralino.filesystem.readBinaryFile(globalGameManagersPath) + .then((config: ArrayBuffer) => { + const buffer = new TextDecoder('ascii').decode(new Uint8Array(config)); + const version = /([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+/.exec(buffer); resolve(version !== null ? version[1] : null); }) diff --git a/src/ts/Launcher.ts b/src/ts/Launcher.ts index 94580da..9984a50 100644 --- a/src/ts/Launcher.ts +++ b/src/ts/Launcher.ts @@ -17,32 +17,8 @@ export default class Launcher public constructor(onMount) { onMount(() => { - this.state = new State(this); this.progressBar = new ProgressBar(this); - - // Progress bar test - /*this.progressBar.init({ - label: 'Abobus', - showSpeed: true, - showEta: true, - showPercents: true, - showTotals: true, - - finish: () => this.progressBar!.hide() - }); - - this.progressBar.show(); - - const t = (curr) => { - if (curr <= 3000) - { - this.progressBar!.update(curr, 3000, 1); - - setTimeout(() => t(curr + 1), 10); - } - }; - - t(0);*/ + this.state = new State(this); }); } @@ -65,7 +41,8 @@ export default class Launcher if (window.status) { - this.settingsMenu = new Process(window.data!.pid, null); + this.settingsMenu = new Process(window.data!.pid); + this.settingsMenu.runningInterval = null; /*this.settingsMenu.finish(() => { Window.current.show(); diff --git a/src/ts/Patch.ts b/src/ts/Patch.ts index 1669f5b..1dd3600 100644 --- a/src/ts/Patch.ts +++ b/src/ts/Patch.ts @@ -241,9 +241,19 @@ export default class Patch // compare it with actual UnityPlayer.dll hash and say whether the patch // was applied or not if (originalPlayer !== null) - patchInfo.applied = md5(`${constants.paths.gameDir}/UnityPlayer.dll`) != originalPlayer[1]; + { + constants.paths.gameDir.then((gameDir) => { + Neutralino.filesystem.readBinaryFile(`${gameDir}/UnityPlayer.dll`) + .then((currPlayer: ArrayBuffer) => { + patchInfo.applied = md5(currPlayer) != originalPlayer[1]; - resolve(patchInfo); + resolve(patchInfo); + }) + .catch(() => resolve(patchInfo)); + }); + } + + else resolve(patchInfo); } }); } @@ -255,12 +265,20 @@ export default class Patch /** * Get patch installation stream + * + * @returns null if the latest available patch in preparation state + * @returns rejects Error object if the patch's repositories are unreachable or they responded with an error */ - public static install(): Promise + public static install(): Promise { return new Promise((resolve, reject) => { this.latest - .then((patch) => resolve(new Stream(constants.getPatchUri(patch.source ?? 'origin'), patch.version))) + .then((patch) => { + if (patch.state === 'preparation') + resolve(null); + + else resolve(new Stream(constants.getPatchUri(patch.source ?? 'origin'), patch.version)); + }) .catch((err) => reject(err)); }); } diff --git a/src/ts/Voice.ts b/src/ts/Voice.ts index 65ed6c9..5d453c9 100644 --- a/src/ts/Voice.ts +++ b/src/ts/Voice.ts @@ -1,9 +1,10 @@ import type { VoicePack } from './types/GameData'; -import type { InstalledVoice } from './types/Voice'; +import type { InstalledVoice, VoiceLang } from './types/Voice'; import constants from './Constants'; import Game from './Game'; import AbstractInstaller from './core/AbstractInstaller'; +import Configs from './Configs'; declare const Neutralino; @@ -60,6 +61,14 @@ export default class Voice }); } + /** + * Get currently selected voice package language accotring to the config file + */ + public static get selected(): Promise + { + return Configs.get('lang.voice') as Promise; + } + /** * Get latest voice data info * @@ -84,7 +93,7 @@ export default class Voice { return new Promise((resolve, reject) => { Game.getDiff(version) - .then((data) => resolve(data!.voice_packs ?? null)) + .then((data) => resolve(data?.voice_packs ?? null)) .catch((error) => reject(error)); }); } diff --git a/src/ts/core/Archive.ts b/src/ts/core/Archive.ts index 2f14611..2db3e7f 100644 --- a/src/ts/core/Archive.ts +++ b/src/ts/core/Archive.ts @@ -5,6 +5,8 @@ import type { ArchiveInfo } from '../types/Archive'; +import promisify from './promisify'; + declare const Neutralino; declare const NL_CWD; @@ -83,21 +85,34 @@ class Stream const updateProgress = async () => { let difference: number = 0; + let pool: any[] = []; remainedFiles.forEach((file) => { if (file.path != '#unpacked#') { - Neutralino.filesystem.getStats(`${baseDir}/${file.path}`) - .then(() => { - this.unpacked += file.size.uncompressed!; - difference += file.size.uncompressed!; + pool.push((): Promise => { + return new Promise((resolve) => { + Neutralino.filesystem.getStats(`${baseDir}/${file.path}`) + .then(() => { + this.unpacked += file.size.uncompressed!; + difference += file.size.uncompressed!; - file.path = '#unpacked#'; - }) - .catch(() => {}); + file.path = '#unpacked#'; + + resolve(); + }) + .catch(() => resolve()) + }); + }); } }); + await promisify({ + callbacks: pool, + callAtOnce: true, + interval: 200 + }); + remainedFiles = remainedFiles.filter((file) => file.path != '#unpacked#'); if (this.onProgress) diff --git a/src/ts/core/Prefix.ts b/src/ts/core/Prefix.ts new file mode 100644 index 0000000..2368c79 --- /dev/null +++ b/src/ts/core/Prefix.ts @@ -0,0 +1,125 @@ +import constants from '../Constants'; +import Process from '../neutralino/Process'; +import Downloader from './Downloader'; +import Runners from './Runners'; + +declare const Neutralino; + +export default class Prefix +{ + /** + * Check if the wine prefix is created in the specified path + */ + public static exists(path: string|null = null): Promise + { + return new Promise(async (resolve) => { + path ??= await constants.paths.prefix.current; + + Neutralino.filesystem.getStats(`${path}/drive_c`) + .then(() => resolve(true)) + .catch(() => resolve(false)); + }); + } + + /** + * Get path to the winetricks.sh file + * + * If this file is not downloaded - then this method will download it + * and return the path after it + */ + public static getWinetricks(): Promise + { + return new Promise(async (resolve) => { + const winetricksPath = `${await constants.paths.launcherDir}/winetricks.sh`; + + Neutralino.filesystem.getStats(winetricksPath) + .then(() => resolve(winetricksPath)) + .catch(() => { + Downloader.download(constants.uri.winetricks, winetricksPath).then((stream) => { + stream.finish(() => resolve(winetricksPath)); + }); + }); + }); + } + + /** + * Create wine prefix using the current selected wine + * + * @param path folder to create prefix in + * @param progress function that will be called with every creation step + * + * @returns false if there's no selected wine version. Otherwise true + */ + public static create(path: string, progress?: (output: string, current: number, total: number) => void): Promise + { + const installationSteps = [ + // corefonts + 'Executing w_do_call corefonts', + 'Executing load_corefonts', + 'Executing load_andale', + 'Executing load_arial', + 'Executing load_comicsans', + 'Executing load_courier', + 'Executing load_georgia', + 'Executing load_impact', + 'Executing load_times', + 'Executing load_trebuchet', + 'Executing load_verdana', + 'Executing load_webdings', + + // usetakefocus=n (fullscreen input issues fix) + 'Executing load_usetakefocus n' + ]; + + return new Promise((resolve) => { + Runners.current.then((runner) => { + if (runner === null) + resolve(false); + + else + { + this.getWinetricks().then(async (winetricks) => { + let installationProgress = 0; + + const process = await Process.run(`bash '${Process.addSlashes(winetricks)}' corefonts usetakefocus=n`, { + env: { + WINE: `${await constants.paths.runnersDir}/${runner.name}/${runner.files.wine}`, + WINESERVER: `${await constants.paths.runnersDir}/${runner.name}/${runner.files.wineserver}`, + WINEPREFIX: path + } + }); + + process.outputInterval = null; + + if (progress) + { + process.outputInterval = 1500; + + process.output((output) => { + for (let i = 0; i < installationSteps.length; ++i) + if (output.includes(installationSteps[i])) + { + installationProgress = i + 1; + + break; + } + + if (output != '') + { + const lastLine = output.split(/\r\n|\r|\n/gm) + .filter((line) => line.length > 0) + .pop()?.trim(); + + if (lastLine && !lastLine.includes('------')) + progress(lastLine, installationProgress, installationSteps.length); + } + }); + } + + process.finish(() => resolve(true)); + }); + } + }); + }); + } +}; diff --git a/src/ts/core/Runners.ts b/src/ts/core/Runners.ts index 4a99e35..9306e29 100644 --- a/src/ts/core/Runners.ts +++ b/src/ts/core/Runners.ts @@ -121,97 +121,6 @@ class Runners else resolve(new Stream(runner)); }); } - - /** - * Get path to the winetricks.sh file - * - * If this file is not downloaded - then this method will download it - * and return the path after it - */ - public static getWinetricks(): Promise - { - return new Promise(async (resolve) => { - const winetricksPath = `${await constants.paths.launcherDir}/winetricks.sh`; - - Neutralino.filesystem.getStats(winetricksPath) - .then(() => resolve(winetricksPath)) - .catch(() => { - Downloader.download(constants.uri.winetricks, winetricksPath).then((stream) => { - stream.finish(() => resolve(winetricksPath)); - }); - }); - }); - } - - /** - * Create wine prefix using the current selected wine - * - * @param path folder to create prefix in - * @param progress function that will be called with every creation step - * - * @returns false if there's no selected wine version. Otherwise true - */ - public static createPrefix(path: string, progress?: (output: string, current: number, total: number) => void): Promise - { - const installationSteps = [ - // corefonts - 'Executing w_do_call corefonts', - 'Executing load_corefonts', - 'Executing load_andale', - 'Executing load_arial', - 'Executing load_comicsans', - 'Executing load_courier', - 'Executing load_georgia', - 'Executing load_impact', - 'Executing load_times', - 'Executing load_trebuchet', - 'Executing load_verdana', - 'Executing load_webdings', - - // usetakefocus=n (fullscreen input issues fix) - 'Executing load_usetakefocus n' - ]; - - return new Promise((resolve) => { - this.current.then((runner) => { - if (runner === null) - resolve(false); - - else - { - this.getWinetricks().then(async (winetricks) => { - // let installationProgress = 0; - - const process = await Process.run(`bash '${Process.addSlashes(winetricks)}' corefonts usetakefocus=n`, { - env: { - WINE: `${await constants.paths.runnersDir}/${runner.name}/${runner.files.wine}`, - WINESERVER: `${await constants.paths.runnersDir}/${runner.name}/${runner.files.wineserver}`, - WINEPREFIX: path - } - }); - - // todo: add process output reading - - process.finish(() => resolve(true)); - - /*installerProcess.stdout.on('data', (data: string) => { - let str = data.toString(); - - for (let i = 0; i < installationSteps.length; ++i) - if (str.includes(installationSteps[i])) - { - installationProgress = i + 1; - - break; - } - - progress(str, installationProgress, installationSteps.length); - });*/ - }); - } - }); - }); - } } export default Runners; diff --git a/src/ts/launcher/ProgressBar.ts b/src/ts/launcher/ProgressBar.ts index 54e428e..7a8a8cf 100644 --- a/src/ts/launcher/ProgressBar.ts +++ b/src/ts/launcher/ProgressBar.ts @@ -75,10 +75,14 @@ export default class ProgressBar this.speedLabelElement.textContent = ''; this.etaLabelElement.textContent = ''; - if (typeof options.label === 'string') - this.downloadedLabelElement.textContent = options.label; + this.downloadedLabelElement.textContent = typeof options.label === 'string' ? + options.label : ''; - this.progress.beganAt = Date.now(); + this.progress = { + beganAt: Date.now(), + prevTime: Date.now(), + temp: 0 + }; } /** @@ -93,7 +97,7 @@ export default class ProgressBar // Otherwise update percents and totals if we should else if (this.options!.showPercents || this.options!.showPercents) { - this.downloadedLabelElement.textContent = `${this.options!.label}:`; + this.downloadedLabelElement.textContent = this.options!.label; if (this.options!.showPercents) this.downloadedLabelElement.textContent += ` ${Math.round(current / total * 100)}%`; diff --git a/src/ts/launcher/State.ts b/src/ts/launcher/State.ts index b72efe8..f079270 100644 --- a/src/ts/launcher/State.ts +++ b/src/ts/launcher/State.ts @@ -1,4 +1,6 @@ +import Game from '../Game'; import type Launcher from '../Launcher'; +import Patch from '../Patch'; import type { LauncherState } from '../types/Launcher'; @@ -8,13 +10,18 @@ export default class State public launchButton: HTMLElement; - protected _state: LauncherState = 'game-installation-available'; + protected _state: LauncherState = 'game-launch-available'; protected events = { 'game-launch-available': import('./states/Launch'), 'game-installation-available': import('./states/Install'), - 'game-update-available': import('./states/Install') + 'game-update-available': import('./states/Install'), + + 'game-voice-update-required': import('./states/InstallVoice'), + + 'test-patch-available': import('./states/ApplyPatch'), + 'patch-available': import('./states/ApplyPatch') }; public constructor(launcher: Launcher) @@ -27,6 +34,8 @@ export default class State if (this.events[this._state]) this.events[this._state].then((event) => event.default(this.launcher)); }; + + this.update(); } /** @@ -44,14 +53,82 @@ export default class State { this._state = state; + this.launcher.progressBar!.hide(); + switch(state) { case 'game-launch-available': - this.launcher.progressBar!.hide(); - this.launchButton.textContent = 'Launch'; + break; + + case 'game-installation-available': + this.launchButton.textContent = 'Install'; + + break; + + case 'game-update-available': + case 'game-voice-update-required': + this.launchButton.textContent = 'Update'; + + break; + + case 'patch-available': + this.launchButton.textContent = 'Apply patch'; + + break; + + case 'test-patch-available': + // todo some warning message + this.launchButton.textContent = 'Apply test patch'; + + break; + + case 'patch-unavailable': + // todo some warning message + this.launchButton.textContent = 'Patch unavailable'; + break; } } + + /** + * Update launcher state + * + * @returns new launcher state + * + * This state will be automatically applied to the launcher + * so you don't need to do it manually + */ + public update(): Promise + { + return new Promise(async (resolve) => { + let state: LauncherState; + + const gameCurrent = await Game.current; + const gameLatest = (await Game.latest).version; + const patch = await Patch.latest; + + console.log(patch); + + if (gameCurrent === null) + state = 'game-installation-available'; + + else if (gameCurrent != gameLatest) + state = 'game-update-available'; + + else if (!patch.applied) + { + state = patch.state == 'preparation' ? + 'patch-unavailable' : (patch.state == 'testing' ? + 'test-patch-available' : 'patch-available'); + } + + else state = 'game-launch-available'; + + this.set(state); + + resolve(state); + }); + } }; diff --git a/src/ts/launcher/states/ApplyPatch.ts b/src/ts/launcher/states/ApplyPatch.ts new file mode 100644 index 0000000..665ab9c --- /dev/null +++ b/src/ts/launcher/states/ApplyPatch.ts @@ -0,0 +1,39 @@ +import type Launcher from '../../Launcher'; + +import Patch from '../../Patch'; + +export default (launcher: Launcher): Promise => { + return new Promise(async (resolve) => { + Patch.latest.then((patch) => { + if (patch.applied) + resolve(); + + else + { + launcher.progressBar?.init({ + label: 'Applying patch...', + showSpeed: false, + showEta: false, + showPercents: false, + showTotals: false + }); + + Patch.install().then((stream) => { + if (stream === null) + resolve(); + + else + { + stream.downloadStart(() => launcher.progressBar?.show()); + + stream.patchFinish(() => { + launcher.progressBar?.hide(); + + resolve(); + }); + } + }); + } + }); + }); +}; diff --git a/src/ts/launcher/states/CreatePrefix.ts b/src/ts/launcher/states/CreatePrefix.ts new file mode 100644 index 0000000..eec3f42 --- /dev/null +++ b/src/ts/launcher/states/CreatePrefix.ts @@ -0,0 +1,51 @@ +import type Launcher from '../../Launcher'; + +import constants from '../../Constants'; +import Prefix from '../../core/Prefix'; + +export default (launcher: Launcher): Promise => { + return new Promise(async (resolve) => { + const prefixDir = await constants.paths.prefix.current; + + Prefix.exists().then((exists) => { + if (exists) + resolve(); + + else + { + let progressLabel = 'Creating prefix...'; + + launcher.progressBar!.init({ + label: () => progressLabel, + showSpeed: false, + showEta: false, + showPercents: false, + showTotals: false + }); + + launcher.progressBar!.show(); + + Prefix.create(prefixDir, (output, current, total) => { + progressLabel = output; + + if (progressLabel.length > 70) + progressLabel = progressLabel.substring(0, 70) + '...'; + + launcher.progressBar!.update(current, total, 1); + }) + .then((result) => { + if (result === true) + resolve(); + + else + { + // TODO + console.error('There\'s no wine version installed to use to create the prefix'); + + resolve(); + } + }); + } + }); + }); +}; diff --git a/src/ts/launcher/states/Install.ts b/src/ts/launcher/states/Install.ts index 9c0e6d3..3d7586e 100644 --- a/src/ts/launcher/states/Install.ts +++ b/src/ts/launcher/states/Install.ts @@ -1,34 +1,23 @@ import type Launcher from '../../Launcher'; import Game from '../../Game'; -import constants from '../../Constants'; -import Runners from '../../core/Runners'; - -declare const Neutralino; +import Prefix from '../../core/Prefix'; export default (launcher: Launcher): Promise => { return new Promise(async (resolve) => { - const prefixDir = await constants.paths.prefix.current; - - Neutralino.filesystem.getStats(prefixDir) - .then(() => updateGame()) - .catch(() => { - Runners.createPrefix(prefixDir).then((result) => { - if (result === true) - updateGame(); - - else - { - // TODO - console.error('There\'s no wine version installed to use to create the prefix'); - - resolve(); - } + Prefix.exists().then((exists) => { + if (!exists) + { + import('./CreatePrefix').then((module) => { + module.default(launcher).then(() => updateGame()); }); - }); + } + }); const updateGame = async () => { - Game.update(await Game.current).then((stream) => { + const prevGameVersion = await Game.current; + + Game.update(prevGameVersion).then((stream) => { launcher.progressBar?.init({ label: 'Downloading game...', showSpeed: true, @@ -57,7 +46,12 @@ export default (launcher: Launcher): Promise => { launcher.progressBar?.update(current, total, difference); }); - stream?.unpackFinish(() => resolve()); + stream?.unpackFinish(() => { + // Download voice package when the game itself was installed + import('./InstallVoice').then((module) => { + module.default(launcher, prevGameVersion).then(() => resolve()); + }); + }); }); }; }); diff --git a/src/ts/launcher/states/InstallVoice.ts b/src/ts/launcher/states/InstallVoice.ts new file mode 100644 index 0000000..e5f5ff3 --- /dev/null +++ b/src/ts/launcher/states/InstallVoice.ts @@ -0,0 +1,43 @@ +import type Launcher from '../../Launcher'; + +import Voice from '../../Voice'; + +export default (launcher: Launcher, prevGameVersion: string|null = null): Promise => { + return new Promise(async (resolve) => { + Voice.update(await Voice.selected, prevGameVersion).then((stream) => { + launcher.progressBar?.init({ + label: 'Downloading voice package...', + showSpeed: true, + showEta: true, + showPercents: true, + showTotals: true + }); + + stream?.downloadStart(() => launcher.progressBar?.show()); + + stream?.downloadProgress((current: number, total: number, difference: number) => { + launcher.progressBar?.update(current, total, difference); + }); + + stream?.unpackStart(() => { + launcher.progressBar?.init({ + label: 'Unpacking voice package...', + showSpeed: true, + showEta: true, + showPercents: true, + showTotals: true + }); + }); + + stream?.unpackProgress((current: number, total: number, difference: number) => { + launcher.progressBar?.update(current, total, difference); + }); + + stream?.unpackFinish(() => { + launcher.progressBar?.hide(); + + resolve(); + }); + }); + }); +}; diff --git a/src/ts/neutralino/Process.ts b/src/ts/neutralino/Process.ts index f024e46..b336a60 100644 --- a/src/ts/neutralino/Process.ts +++ b/src/ts/neutralino/Process.ts @@ -1,7 +1,7 @@ declare const Neutralino; +declare const NL_CWD; type ProcessOptions = { - input?: string; env?: object; cwd?: string; }; @@ -20,7 +20,19 @@ class Process * * @default 200 */ - public interval: number|null; + public runningInterval: number|null = 200; + + /** + * Interval in ms between process output update + * + * null if you don't want to update process output + * + * @default 500 + */ + public outputInterval: number|null = 500; + + protected outputFile: string|null; + protected outputOffset: number = 0; protected _finished: boolean = false; @@ -32,20 +44,21 @@ class Process return this._finished; }; + protected onOutput?: (output: string, process: Process) => void; protected onFinish?: (process: Process) => void; - public constructor(pid: number, interval: number|null = 200) + public constructor(pid: number, outputFile: string|null = null) { this.id = pid; - this.interval = interval; + this.outputFile = outputFile; const updateStatus = () => { this.running().then((running) => { // The process is still running if (running) { - if (this.interval) - setTimeout(updateStatus, this.interval); + if (this.runningInterval) + setTimeout(updateStatus, this.runningInterval); } // Otherwise the process was stopped @@ -59,8 +72,34 @@ class Process }); }; - if (this.interval) - setTimeout(updateStatus, this.interval); + if (this.runningInterval) + setTimeout(updateStatus, this.runningInterval); + + if (this.outputFile) + { + const updateOutput = () => { + Neutralino.filesystem.readFile(this.outputFile) + .then((output: string) => { + if (this.onOutput) + this.onOutput(output.substring(this.outputOffset), this); + + this.outputOffset = output.length; + + if (this._finished) + Neutralino.filesystem.removeFile(this.outputFile); + + else if (this.outputInterval) + setTimeout(updateOutput, this.outputInterval); + }) + .catch(() => { + if (this.outputInterval && !this._finished) + setTimeout(updateOutput, this.outputInterval); + }); + }; + + if (this.outputInterval) + setTimeout(updateOutput, this.outputInterval); + } } /** @@ -75,7 +114,7 @@ class Process // If user stopped process status auto-checking // then we should check it manually when this method was called - else if (this.interval === null) + else if (this.runningInterval === null) { this.running().then((running) => { if (!running) @@ -88,6 +127,11 @@ class Process } } + public output(callback: (output: string, process: Process) => void) + { + this.onOutput = callback; + } + /** * Kill process */ @@ -117,8 +161,9 @@ class Process */ public static run(command: string, options: ProcessOptions = {}): Promise { - return new Promise(async (resolve) => { + const tmpFile = `${NL_CWD}/${10000 + Math.round(Math.random() * 89999)}.tmp`; + // Set env variables if (options.env) { @@ -127,17 +172,19 @@ class Process }); } + // Set output redirection to the temp file + command = `${command} > '${this.addSlashes(tmpFile)}' 2>&1`; + // Set current working directory if (options.cwd) command = `cd '${this.addSlashes(options.cwd)}' && ${command} && cd -`; // And run the command const process = await Neutralino.os.execCommand(command, { - background: true, - stdin: options.input ?? '' + background: true }); - resolve(new Process(process.pid)); + resolve(new Process(process.pid, tmpFile)); }); } diff --git a/src/ts/types/Launcher.ts b/src/ts/types/Launcher.ts index ba60adc..5dc41f0 100644 --- a/src/ts/types/Launcher.ts +++ b/src/ts/types/Launcher.ts @@ -1,11 +1,25 @@ +/** + * With a first run the game will not be installed + * and the launcher will have "game-installation-available" state + * + * With it, launcher will create wine prefix if it is required, + * download the game, voice data and unpack them + * + * Then, with game's updates launcher will have "game-update-available" state + * and with it it will download and unpack game and voice updates + * + * When the game is installed and updated - then launcher will have either + * "patch-unavailable", "test-patch-available", "patch-available", or "game-launch-available" + * So it will either download and apply patch, launch the game or notify user that the patch is not available + */ + type LauncherState = | 'patch-unavailable' | 'test-patch-available' - | 'patch-applying' - | 'game-update-available' + | 'patch-available' | 'game-installation-available' + | 'game-update-available' | 'game-voice-update-required' - | 'resume-download-available' | 'game-launch-available'; export type { LauncherState }; \ No newline at end of file