mirror of
https://github.com/an-anime-team/an-anime-game-launcher.git
synced 2024-12-22 09:44:24 +03:00
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
This commit is contained in:
parent
68d766da58
commit
7f4f14d76b
18 changed files with 506 additions and 183 deletions
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<Stream>
|
||||
public static install(): Promise<Stream|null>
|
||||
{
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<VoiceLang>
|
||||
{
|
||||
return Configs.get('lang.voice') as Promise<VoiceLang>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<void> => {
|
||||
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)
|
||||
|
|
125
src/ts/core/Prefix.ts
Normal file
125
src/ts/core/Prefix.ts
Normal file
|
@ -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<boolean>
|
||||
{
|
||||
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<string>
|
||||
{
|
||||
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<boolean>
|
||||
{
|
||||
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));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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<string>
|
||||
{
|
||||
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<boolean>
|
||||
{
|
||||
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;
|
||||
|
|
|
@ -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)}%`;
|
||||
|
|
|
@ -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<string>
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
39
src/ts/launcher/states/ApplyPatch.ts
Normal file
39
src/ts/launcher/states/ApplyPatch.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type Launcher from '../../Launcher';
|
||||
|
||||
import Patch from '../../Patch';
|
||||
|
||||
export default (launcher: Launcher): Promise<void> => {
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
51
src/ts/launcher/states/CreatePrefix.ts
Normal file
51
src/ts/launcher/states/CreatePrefix.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import type Launcher from '../../Launcher';
|
||||
|
||||
import constants from '../../Constants';
|
||||
import Prefix from '../../core/Prefix';
|
||||
|
||||
export default (launcher: Launcher): Promise<void> => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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<void> => {
|
||||
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<void> => {
|
|||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
|
43
src/ts/launcher/states/InstallVoice.ts
Normal file
43
src/ts/launcher/states/InstallVoice.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type Launcher from '../../Launcher';
|
||||
|
||||
import Voice from '../../Voice';
|
||||
|
||||
export default (launcher: Launcher, prevGameVersion: string|null = null): Promise<void> => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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<Process>
|
||||
{
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 };
|
Loading…
Reference in a new issue