mirror of
https://github.com/an-anime-team/an-anime-game-launcher.git
synced 2024-12-23 02:00:17 +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",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"neu": "neu",
|
"neu": "neu",
|
||||||
"dev": "vite build && neu run",
|
"dev": "vite build && neu run --disable-auto-reload",
|
||||||
"build": "vite build && neu build --release",
|
"build": "vite build && neu build --release",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,7 +46,7 @@ settings:
|
||||||
|
|
||||||
gamemode: GameMode
|
gamemode: GameMode
|
||||||
fps_unlocker: Unlock FPS
|
fps_unlocker: Unlock FPS
|
||||||
purge_dxvk_logs: Auto-delete DXVK logs
|
purge_dxvk_logs: Delete DXVK logs
|
||||||
|
|
||||||
# DXVKs
|
# DXVKs
|
||||||
dxvks:
|
dxvks:
|
||||||
|
|
|
@ -47,7 +47,7 @@ settings:
|
||||||
# TODO: add hints to the components so I could describe what these options means
|
# TODO: add hints to the components so I could describe what these options means
|
||||||
gamemode: GameMode
|
gamemode: GameMode
|
||||||
fps_unlocker: Разблокировать FPS
|
fps_unlocker: Разблокировать FPS
|
||||||
purge_dxvk_logs: Автоматически удалять логи DXVK
|
purge_dxvk_logs: Удалять логи DXVK
|
||||||
|
|
||||||
# DXVKs
|
# DXVKs
|
||||||
dxvks:
|
dxvks:
|
||||||
|
|
|
@ -35,9 +35,10 @@ export default class Game
|
||||||
Neutralino.filesystem.readFile(persistentPath)
|
Neutralino.filesystem.readFile(persistentPath)
|
||||||
.then((version) => resolve(version))
|
.then((version) => resolve(version))
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
Neutralino.filesystem.readFile(globalGameManagersPath)
|
Neutralino.filesystem.readBinaryFile(globalGameManagersPath)
|
||||||
.then((config) => {
|
.then((config: ArrayBuffer) => {
|
||||||
const version = /([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+/.exec(config);
|
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);
|
resolve(version !== null ? version[1] : null);
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,32 +17,8 @@ export default class Launcher
|
||||||
public constructor(onMount)
|
public constructor(onMount)
|
||||||
{
|
{
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
this.state = new State(this);
|
|
||||||
this.progressBar = new ProgressBar(this);
|
this.progressBar = new ProgressBar(this);
|
||||||
|
this.state = new State(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);*/
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +41,8 @@ export default class Launcher
|
||||||
|
|
||||||
if (window.status)
|
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(() => {
|
/*this.settingsMenu.finish(() => {
|
||||||
Window.current.show();
|
Window.current.show();
|
||||||
|
|
|
@ -241,9 +241,19 @@ export default class Patch
|
||||||
// compare it with actual UnityPlayer.dll hash and say whether the patch
|
// compare it with actual UnityPlayer.dll hash and say whether the patch
|
||||||
// was applied or not
|
// was applied or not
|
||||||
if (originalPlayer !== null)
|
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
|
* 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.latest
|
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));
|
.catch((err) => reject(err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import type { VoicePack } from './types/GameData';
|
import type { VoicePack } from './types/GameData';
|
||||||
import type { InstalledVoice } from './types/Voice';
|
import type { InstalledVoice, VoiceLang } from './types/Voice';
|
||||||
|
|
||||||
import constants from './Constants';
|
import constants from './Constants';
|
||||||
import Game from './Game';
|
import Game from './Game';
|
||||||
import AbstractInstaller from './core/AbstractInstaller';
|
import AbstractInstaller from './core/AbstractInstaller';
|
||||||
|
import Configs from './Configs';
|
||||||
|
|
||||||
declare const Neutralino;
|
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
|
* Get latest voice data info
|
||||||
*
|
*
|
||||||
|
@ -84,7 +93,7 @@ export default class Voice
|
||||||
{
|
{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Game.getDiff(version)
|
Game.getDiff(version)
|
||||||
.then((data) => resolve(data!.voice_packs ?? null))
|
.then((data) => resolve(data?.voice_packs ?? null))
|
||||||
.catch((error) => reject(error));
|
.catch((error) => reject(error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import type {
|
||||||
ArchiveInfo
|
ArchiveInfo
|
||||||
} from '../types/Archive';
|
} from '../types/Archive';
|
||||||
|
|
||||||
|
import promisify from './promisify';
|
||||||
|
|
||||||
declare const Neutralino;
|
declare const Neutralino;
|
||||||
declare const NL_CWD;
|
declare const NL_CWD;
|
||||||
|
|
||||||
|
@ -83,21 +85,34 @@ class Stream
|
||||||
|
|
||||||
const updateProgress = async () => {
|
const updateProgress = async () => {
|
||||||
let difference: number = 0;
|
let difference: number = 0;
|
||||||
|
let pool: any[] = [];
|
||||||
|
|
||||||
remainedFiles.forEach((file) => {
|
remainedFiles.forEach((file) => {
|
||||||
if (file.path != '#unpacked#')
|
if (file.path != '#unpacked#')
|
||||||
{
|
{
|
||||||
|
pool.push((): Promise<void> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
Neutralino.filesystem.getStats(`${baseDir}/${file.path}`)
|
Neutralino.filesystem.getStats(`${baseDir}/${file.path}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.unpacked += file.size.uncompressed!;
|
this.unpacked += file.size.uncompressed!;
|
||||||
difference += file.size.uncompressed!;
|
difference += file.size.uncompressed!;
|
||||||
|
|
||||||
file.path = '#unpacked#';
|
file.path = '#unpacked#';
|
||||||
|
|
||||||
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => resolve())
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await promisify({
|
||||||
|
callbacks: pool,
|
||||||
|
callAtOnce: true,
|
||||||
|
interval: 200
|
||||||
|
});
|
||||||
|
|
||||||
remainedFiles = remainedFiles.filter((file) => file.path != '#unpacked#');
|
remainedFiles = remainedFiles.filter((file) => file.path != '#unpacked#');
|
||||||
|
|
||||||
if (this.onProgress)
|
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));
|
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;
|
export default Runners;
|
||||||
|
|
|
@ -75,10 +75,14 @@ export default class ProgressBar
|
||||||
this.speedLabelElement.textContent = '';
|
this.speedLabelElement.textContent = '';
|
||||||
this.etaLabelElement.textContent = '';
|
this.etaLabelElement.textContent = '';
|
||||||
|
|
||||||
if (typeof options.label === 'string')
|
this.downloadedLabelElement.textContent = typeof options.label === 'string' ?
|
||||||
this.downloadedLabelElement.textContent = options.label;
|
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
|
// Otherwise update percents and totals if we should
|
||||||
else if (this.options!.showPercents || this.options!.showPercents)
|
else if (this.options!.showPercents || this.options!.showPercents)
|
||||||
{
|
{
|
||||||
this.downloadedLabelElement.textContent = `${this.options!.label}:`;
|
this.downloadedLabelElement.textContent = this.options!.label;
|
||||||
|
|
||||||
if (this.options!.showPercents)
|
if (this.options!.showPercents)
|
||||||
this.downloadedLabelElement.textContent += ` ${Math.round(current / total * 100)}%`;
|
this.downloadedLabelElement.textContent += ` ${Math.round(current / total * 100)}%`;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import Game from '../Game';
|
||||||
import type Launcher from '../Launcher';
|
import type Launcher from '../Launcher';
|
||||||
|
import Patch from '../Patch';
|
||||||
|
|
||||||
import type { LauncherState } from '../types/Launcher';
|
import type { LauncherState } from '../types/Launcher';
|
||||||
|
|
||||||
|
@ -8,13 +10,18 @@ export default class State
|
||||||
|
|
||||||
public launchButton: HTMLElement;
|
public launchButton: HTMLElement;
|
||||||
|
|
||||||
protected _state: LauncherState = 'game-installation-available';
|
protected _state: LauncherState = 'game-launch-available';
|
||||||
|
|
||||||
protected events = {
|
protected events = {
|
||||||
'game-launch-available': import('./states/Launch'),
|
'game-launch-available': import('./states/Launch'),
|
||||||
|
|
||||||
'game-installation-available': import('./states/Install'),
|
'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)
|
public constructor(launcher: Launcher)
|
||||||
|
@ -27,6 +34,8 @@ export default class State
|
||||||
if (this.events[this._state])
|
if (this.events[this._state])
|
||||||
this.events[this._state].then((event) => event.default(this.launcher));
|
this.events[this._state].then((event) => event.default(this.launcher));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,14 +53,82 @@ export default class State
|
||||||
{
|
{
|
||||||
this._state = state;
|
this._state = state;
|
||||||
|
|
||||||
|
this.launcher.progressBar!.hide();
|
||||||
|
|
||||||
switch(state)
|
switch(state)
|
||||||
{
|
{
|
||||||
case 'game-launch-available':
|
case 'game-launch-available':
|
||||||
this.launcher.progressBar!.hide();
|
|
||||||
|
|
||||||
this.launchButton.textContent = 'Launch';
|
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;
|
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 type Launcher from '../../Launcher';
|
||||||
|
|
||||||
import Game from '../../Game';
|
import Game from '../../Game';
|
||||||
import constants from '../../Constants';
|
import Prefix from '../../core/Prefix';
|
||||||
import Runners from '../../core/Runners';
|
|
||||||
|
|
||||||
declare const Neutralino;
|
|
||||||
|
|
||||||
export default (launcher: Launcher): Promise<void> => {
|
export default (launcher: Launcher): Promise<void> => {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const prefixDir = await constants.paths.prefix.current;
|
Prefix.exists().then((exists) => {
|
||||||
|
if (!exists)
|
||||||
Neutralino.filesystem.getStats(prefixDir)
|
|
||||||
.then(() => updateGame())
|
|
||||||
.catch(() => {
|
|
||||||
Runners.createPrefix(prefixDir).then((result) => {
|
|
||||||
if (result === true)
|
|
||||||
updateGame();
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// TODO
|
import('./CreatePrefix').then((module) => {
|
||||||
console.error('There\'s no wine version installed to use to create the prefix');
|
module.default(launcher).then(() => updateGame());
|
||||||
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateGame = async () => {
|
const updateGame = async () => {
|
||||||
Game.update(await Game.current).then((stream) => {
|
const prevGameVersion = await Game.current;
|
||||||
|
|
||||||
|
Game.update(prevGameVersion).then((stream) => {
|
||||||
launcher.progressBar?.init({
|
launcher.progressBar?.init({
|
||||||
label: 'Downloading game...',
|
label: 'Downloading game...',
|
||||||
showSpeed: true,
|
showSpeed: true,
|
||||||
|
@ -57,7 +46,12 @@ export default (launcher: Launcher): Promise<void> => {
|
||||||
launcher.progressBar?.update(current, total, difference);
|
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 Neutralino;
|
||||||
|
declare const NL_CWD;
|
||||||
|
|
||||||
type ProcessOptions = {
|
type ProcessOptions = {
|
||||||
input?: string;
|
|
||||||
env?: object;
|
env?: object;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,19 @@ class Process
|
||||||
*
|
*
|
||||||
* @default 200
|
* @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;
|
protected _finished: boolean = false;
|
||||||
|
|
||||||
|
@ -32,20 +44,21 @@ class Process
|
||||||
return this._finished;
|
return this._finished;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
protected onOutput?: (output: string, process: Process) => void;
|
||||||
protected onFinish?: (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.id = pid;
|
||||||
this.interval = interval;
|
this.outputFile = outputFile;
|
||||||
|
|
||||||
const updateStatus = () => {
|
const updateStatus = () => {
|
||||||
this.running().then((running) => {
|
this.running().then((running) => {
|
||||||
// The process is still running
|
// The process is still running
|
||||||
if (running)
|
if (running)
|
||||||
{
|
{
|
||||||
if (this.interval)
|
if (this.runningInterval)
|
||||||
setTimeout(updateStatus, this.interval);
|
setTimeout(updateStatus, this.runningInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise the process was stopped
|
// Otherwise the process was stopped
|
||||||
|
@ -59,8 +72,34 @@ class Process
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.interval)
|
if (this.runningInterval)
|
||||||
setTimeout(updateStatus, this.interval);
|
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
|
// If user stopped process status auto-checking
|
||||||
// then we should check it manually when this method was called
|
// 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) => {
|
this.running().then((running) => {
|
||||||
if (!running)
|
if (!running)
|
||||||
|
@ -88,6 +127,11 @@ class Process
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public output(callback: (output: string, process: Process) => void)
|
||||||
|
{
|
||||||
|
this.onOutput = callback;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kill process
|
* Kill process
|
||||||
*/
|
*/
|
||||||
|
@ -117,8 +161,9 @@ class Process
|
||||||
*/
|
*/
|
||||||
public static run(command: string, options: ProcessOptions = {}): Promise<Process>
|
public static run(command: string, options: ProcessOptions = {}): Promise<Process>
|
||||||
{
|
{
|
||||||
|
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
|
const tmpFile = `${NL_CWD}/${10000 + Math.round(Math.random() * 89999)}.tmp`;
|
||||||
|
|
||||||
// Set env variables
|
// Set env variables
|
||||||
if (options.env)
|
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
|
// Set current working directory
|
||||||
if (options.cwd)
|
if (options.cwd)
|
||||||
command = `cd '${this.addSlashes(options.cwd)}' && ${command} && cd -`;
|
command = `cd '${this.addSlashes(options.cwd)}' && ${command} && cd -`;
|
||||||
|
|
||||||
// And run the command
|
// And run the command
|
||||||
const process = await Neutralino.os.execCommand(command, {
|
const process = await Neutralino.os.execCommand(command, {
|
||||||
background: true,
|
background: true
|
||||||
stdin: options.input ?? ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 =
|
type LauncherState =
|
||||||
| 'patch-unavailable'
|
| 'patch-unavailable'
|
||||||
| 'test-patch-available'
|
| 'test-patch-available'
|
||||||
| 'patch-applying'
|
| 'patch-available'
|
||||||
| 'game-update-available'
|
|
||||||
| 'game-installation-available'
|
| 'game-installation-available'
|
||||||
|
| 'game-update-available'
|
||||||
| 'game-voice-update-required'
|
| 'game-voice-update-required'
|
||||||
| 'resume-download-available'
|
|
||||||
| 'game-launch-available';
|
| 'game-launch-available';
|
||||||
|
|
||||||
export type { LauncherState };
|
export type { LauncherState };
|
Loading…
Reference in a new issue