mirror of
https://github.com/an-anime-team/an-anime-game-launcher.git
synced 2024-12-18 07:51:47 +03:00
API improvements
- made `promisify()` function to run some code asynchronously - added some default configs setting at start - now settings are stored in yaml format - fixed `Configs.defaults()` null values overwriting - added launcher state system - added `Runners.current` field to get the current selected runner according to the config file - `Configs.get()` method was renamed to `Configs.list()` - added `Configs.get()` method to get a `Runner` object by the specified runner name - added `Process` class to have a better experience of working with processes - updated readme
This commit is contained in:
parent
6e38611900
commit
d1474f643a
13 changed files with 402 additions and 35 deletions
|
@ -142,6 +142,11 @@ This is our current roadmap goals. You can find older ones [here](ROADMAP.md)
|
|||
* Make `Launcher` class to manage launcher-related features
|
||||
* <s>Downloading progress</s>
|
||||
* Launcher state functionality
|
||||
* <s>Game launch available</s>
|
||||
* Game update (installation) required
|
||||
* Voice data update (installation) required
|
||||
* Patch unavailable
|
||||
* Test patch available
|
||||
* Make Vue components
|
||||
* Checkbox
|
||||
* Selection
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
|
||||
<div id="launcher-content">
|
||||
<iframe id="launcher-content-frame" :src="uri.social" scrolling="no" style="position: absolute; border: 0; top: 0; left: 0;" width="100%" height="100%"></iframe>
|
||||
<iframe :src="uri.social" scrolling="no" style="position: absolute; border: 0; top: 0; left: 0;" width="100%" height="100%"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="settings">
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"js-md5": "^0.7.3",
|
||||
"vue": "^3.2.25"
|
||||
"vue": "^3.2.25",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@neutralinojs/neu": "^8.0.0",
|
||||
|
|
|
@ -5,16 +5,39 @@ import Window from '../ts/neutralino/Window';
|
|||
import Launcher from '../ts/Launcher';
|
||||
import Configs from '../ts/Configs';
|
||||
import constants from '../ts/Constants';
|
||||
import promisify from '../ts/core/promisify';
|
||||
import Process from '../ts/neutralino/Process';
|
||||
|
||||
(async () => {
|
||||
promisify(async () => {
|
||||
Configs.defaults({
|
||||
lang: {
|
||||
launcher: 'en-us',
|
||||
voice: 'en-us'
|
||||
},
|
||||
prefix: await constants.paths.prefix.default
|
||||
|
||||
// Path to wine prefix
|
||||
prefix: await constants.paths.prefix.default,
|
||||
|
||||
// runner name to use, or null if runner is not specified
|
||||
runner: null,
|
||||
|
||||
/**
|
||||
* HUD
|
||||
*
|
||||
* null if don't use
|
||||
* otherwise should be "dxvk" or "mangohud"
|
||||
*/
|
||||
hud: null,
|
||||
|
||||
/**
|
||||
* vkBasalt preset to use
|
||||
*
|
||||
* null if don't use
|
||||
* otherwise should be some folder name from the "shaders" folder
|
||||
*/
|
||||
shaders: null
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
let app = createApp({
|
||||
data: () => ({
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import YAML from 'yaml';
|
||||
|
||||
import constants from './Constants';
|
||||
|
||||
declare const Neutralino;
|
||||
|
@ -19,7 +21,7 @@ export default class Configs
|
|||
{
|
||||
return new Promise(async (resolve) => {
|
||||
Neutralino.filesystem.readFile(await constants.paths.config).then((config) => {
|
||||
config = JSON.parse(config);
|
||||
config = YAML.parse(config);
|
||||
|
||||
if (name !== '')
|
||||
{
|
||||
|
@ -54,12 +56,12 @@ export default class Configs
|
|||
value = await Promise.resolve(value);
|
||||
|
||||
Neutralino.filesystem.readFile(await constants.paths.config).then(async (config) => {
|
||||
config = JSON.stringify(getUpdatedArray(name.split('.'), JSON.parse(config), value), null, 4);
|
||||
config = YAML.stringify(getUpdatedArray(name.split('.'), YAML.parse(config), value));
|
||||
|
||||
Neutralino.filesystem.writeFile(await constants.paths.config, config)
|
||||
.then(() => resolve());
|
||||
}).catch(async () => {
|
||||
let config = JSON.stringify(getUpdatedArray(name.split('.'), {}, value), null, 4);
|
||||
let config = YAML.stringify(getUpdatedArray(name.split('.'), {}, value));
|
||||
|
||||
Neutralino.filesystem.writeFile(await constants.paths.config, config)
|
||||
.then(() => resolve());
|
||||
|
@ -74,32 +76,32 @@ export default class Configs
|
|||
*
|
||||
* @returns Promise<void> indicates if the default settings were applied
|
||||
*/
|
||||
public static defaults(configs: scalar): Promise<void>
|
||||
public static defaults(configs: object): Promise<void>
|
||||
{
|
||||
return new Promise(async (resolve) => {
|
||||
const setDefaults = async (current: scalar) => {
|
||||
const updateDefaults = (current: scalar, defaults: scalar) => {
|
||||
Object.keys(defaults!).forEach((key) => {
|
||||
const setDefaults = async (current: object) => {
|
||||
const updateDefaults = (current: object, defaults: object) => {
|
||||
Object.keys(defaults).forEach((key) => {
|
||||
// If the field exists in defaults and doesn't exist in current
|
||||
if (current![key] === undefined)
|
||||
current![key] = defaults![key];
|
||||
if (current[key] === undefined)
|
||||
current[key] = defaults[key];
|
||||
|
||||
// If both of default and current are objects
|
||||
else if (typeof current![key] == 'object' && typeof defaults![key] == 'object')
|
||||
current![key] = updateDefaults(current![key], defaults![key]);
|
||||
// and we also should check if they're not nulls
|
||||
// because JS thinks that [typeof null === 'object']
|
||||
else if (typeof current[key] == 'object' && typeof defaults[key] == 'object' && current[key] !== null && defaults[key] !== null)
|
||||
current[key] = updateDefaults(current[key], defaults![key]);
|
||||
});
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
current = JSON.stringify(updateDefaults(current, configs), null, 4);
|
||||
|
||||
Neutralino.filesystem.writeFile(await constants.paths.config, current)
|
||||
Neutralino.filesystem.writeFile(await constants.paths.config, YAML.stringify(updateDefaults(current, configs)))
|
||||
.then(() => resolve());
|
||||
};
|
||||
|
||||
Neutralino.filesystem.readFile(await constants.paths.config)
|
||||
.then((config) => setDefaults(JSON.parse(config)))
|
||||
.then((config) => setDefaults(YAML.parse(config)))
|
||||
.catch(() => setDefaults({}));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -83,11 +83,11 @@ class Paths
|
|||
/**
|
||||
* Config file
|
||||
*
|
||||
* @default "~/.local/share/anime-game-launcher/config.json"
|
||||
* @default "~/.local/share/anime-game-launcher/config.yaml"
|
||||
*/
|
||||
public static get config(): Promise<string>
|
||||
{
|
||||
return new Promise(async (resolve) => resolve(`${await this.launcherDir}/config.json`));
|
||||
return new Promise(async (resolve) => resolve(`${await this.launcherDir}/config.yaml`));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import constants from './Constants';
|
||||
import Configs from './Configs';
|
||||
|
||||
import Background from './launcher/Background';
|
||||
import ProgressBar from './launcher/ProgressBar';
|
||||
|
||||
declare const Neutralino;
|
||||
import State from './launcher/State';
|
||||
|
||||
export default class Launcher
|
||||
{
|
||||
public app;
|
||||
|
||||
public state: State;
|
||||
public progressBar: ProgressBar;
|
||||
|
||||
public constructor(app)
|
||||
{
|
||||
this.app = app;
|
||||
|
||||
this.state = new State(this);
|
||||
this.progressBar = new ProgressBar(this);
|
||||
|
||||
// Progress bar test
|
||||
|
@ -40,6 +44,9 @@ export default class Launcher
|
|||
t(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update launcher background picture
|
||||
*/
|
||||
public updateBackground(): Promise<void>
|
||||
{
|
||||
return new Promise(async (resolve) => {
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
} from '../types/Runners';
|
||||
|
||||
import constants from '../Constants';
|
||||
import Configs from '../Configs';
|
||||
import AbstractInstaller from './AbstractInstaller';
|
||||
|
||||
declare const Neutralino;
|
||||
|
@ -18,10 +19,25 @@ class Stream extends AbstractInstaller
|
|||
|
||||
class Runners
|
||||
{
|
||||
/**
|
||||
* Get the current using runner according to the config file
|
||||
*/
|
||||
public static get current(): Promise<Runner|null>
|
||||
{
|
||||
return new Promise((resolve) => {
|
||||
Configs.get('runner').then((runner) => {
|
||||
if (typeof runner === 'string')
|
||||
Runners.get(runner).then((runner) => resolve(runner));
|
||||
|
||||
else resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get runners list
|
||||
*/
|
||||
public static get(): Promise<RunnerFamily[]>
|
||||
public static list(): Promise<RunnerFamily[]>
|
||||
{
|
||||
return new Promise((resolve) => {
|
||||
constants.paths.runnersDir.then(async (runnersDir: string) => {
|
||||
|
@ -58,6 +74,29 @@ class Runners
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the runner with a specified name
|
||||
*
|
||||
* @returns null if the runner with this name is not found
|
||||
*/
|
||||
public static get(name: string): Promise<Runner|null>
|
||||
{
|
||||
return new Promise((resolve) => {
|
||||
this.list().then((list) => {
|
||||
for (const family of list)
|
||||
for (const runner of family.runners)
|
||||
if (runner.name == name)
|
||||
{
|
||||
resolve(runner);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download runner to the [constants.paths.runners] directory
|
||||
*
|
||||
|
@ -66,21 +105,14 @@ class Runners
|
|||
*/
|
||||
public static download(runner: Runner|Runner['name']): Promise<null|Stream>
|
||||
{
|
||||
return new Promise(async (resolve) => {
|
||||
return new Promise((resolve) => {
|
||||
// If we provided runner parameter with a name of a runner
|
||||
// then we should find this runner and call this method for it
|
||||
if (typeof runner == 'string')
|
||||
{
|
||||
let foundRunner;
|
||||
|
||||
(await this.get()).forEach((family) => {
|
||||
family.runners.forEach((familyRunner) => {
|
||||
if (familyRunner.name == runner)
|
||||
foundRunner = familyRunner;
|
||||
});
|
||||
this.get(runner).then((foundRunner) => {
|
||||
resolve(foundRunner === null ? null : new Stream(foundRunner));
|
||||
});
|
||||
|
||||
resolve(foundRunner === null ? null : new Stream(foundRunner));
|
||||
}
|
||||
|
||||
// Otherwise we can use runner.uri and so on to download runner
|
||||
|
|
9
src/ts/core/promisify.ts
Normal file
9
src/ts/core/promisify.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Make a promise from a synchronous function and run it
|
||||
*/
|
||||
export default function promisify(callback: () => any): Promise<any>
|
||||
{
|
||||
return new Promise((resolve) => {
|
||||
resolve(callback());
|
||||
});
|
||||
};
|
54
src/ts/launcher/State.ts
Normal file
54
src/ts/launcher/State.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import Launcher from '../Launcher';
|
||||
|
||||
import type { LauncherState } from '../types/Launcher';
|
||||
|
||||
export default class State
|
||||
{
|
||||
public launcher: Launcher;
|
||||
|
||||
public launchButton: HTMLElement;
|
||||
|
||||
protected _state: LauncherState = 'game-launch-available';
|
||||
|
||||
protected events = {
|
||||
'game-launch-available': import('./states/Launch')
|
||||
};
|
||||
|
||||
public constructor(launcher: Launcher)
|
||||
{
|
||||
this.launcher = launcher;
|
||||
|
||||
this.launchButton = <HTMLElement>document.getElementById('launch');
|
||||
|
||||
this.launchButton.onclick = () => {
|
||||
if (this.events[this._state])
|
||||
this.events[this._state].then((event) => event.default());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current launcher state
|
||||
*/
|
||||
public get(): LauncherState
|
||||
{
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set launcher state
|
||||
*/
|
||||
public set(state: LauncherState): void
|
||||
{
|
||||
this._state = state;
|
||||
|
||||
switch(state)
|
||||
{
|
||||
case 'game-launch-available':
|
||||
this.launcher.progressBar.hide();
|
||||
|
||||
this.launchButton.textContent = 'Launch';
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
123
src/ts/launcher/states/Launch.ts
Normal file
123
src/ts/launcher/states/Launch.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import Configs from '../../Configs';
|
||||
import constants from '../../Constants';
|
||||
import Runners from '../../core/Runners';
|
||||
import Launcher from '../../Launcher';
|
||||
import Process from '../../neutralino/Process';
|
||||
|
||||
declare const Neutralino;
|
||||
|
||||
export default (launcher: Launcher): Promise<void> => {
|
||||
return new Promise(async (resolve) => {
|
||||
/**
|
||||
* Selecting wine executable
|
||||
*/
|
||||
let wineExeutable = 'wine';
|
||||
|
||||
const runner = await Runners.current;
|
||||
|
||||
console.log(runner);
|
||||
|
||||
if (runner !== null)
|
||||
{
|
||||
wineExeutable = `${constants.paths.runnersDir}/${runner.name}/${runner.files.wine}`;
|
||||
|
||||
try
|
||||
{
|
||||
Neutralino.filesystem.getStats(wineExeutable);
|
||||
}
|
||||
|
||||
catch
|
||||
{
|
||||
wineExeutable = 'wine';
|
||||
|
||||
await Configs.set('runner', null);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Wine executable: ${wineExeutable}`);
|
||||
|
||||
// Some special variables
|
||||
let env: any = {};
|
||||
|
||||
/**
|
||||
* HUD
|
||||
*/
|
||||
switch (await Configs.get('hud'))
|
||||
{
|
||||
case 'dxvk':
|
||||
env['DXVK_HUD'] = 'fps,frametimes';
|
||||
|
||||
break;
|
||||
|
||||
case 'mangohud':
|
||||
env['MANGOHUD'] = 1;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shaders
|
||||
*/
|
||||
const shaders = await Configs.get('shaders');
|
||||
|
||||
if (shaders !== null)
|
||||
{
|
||||
const userShadersFile = `${constants.paths.shadersDir}/${shaders}/vkBasalt.conf`;
|
||||
const launcherShadersFile = `${await constants.paths.launcherDir}/vkBasalt.conf`;
|
||||
|
||||
env['ENABLE_VKBASALT'] = 1;
|
||||
env['VKBASALT_CONFIG_FILE'] = launcherShadersFile;
|
||||
|
||||
await Neutralino.filesystem.writeFile(launcherShadersFile, await Neutralino.filesystem.readFile(userShadersFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* GPU selection
|
||||
*/
|
||||
/*if (LauncherLib.getConfig('gpu') != 'default')
|
||||
{
|
||||
const gpu = await SwitcherooControl.getGpuByName(LauncherLib.getConfig('gpu'));
|
||||
|
||||
if (gpu)
|
||||
{
|
||||
env = {
|
||||
...env,
|
||||
...SwitcherooControl.getEnvAsObject(gpu)
|
||||
};
|
||||
}
|
||||
|
||||
else console.warn(`GPU ${LauncherLib.getConfig('gpu')} not found. Launching on the default GPU`);
|
||||
}*/
|
||||
|
||||
// let command = `${wineExeutable} ${LauncherLib.getConfig('fpsunlock') ? 'fpsunlock.bat' : 'launcher.bat'}`;
|
||||
|
||||
/**
|
||||
* Gamemode integration
|
||||
*/
|
||||
/*if (LauncherLib.getConfig('gamemode'))
|
||||
command = `gamemoderun ${command}`;*/
|
||||
|
||||
const command = `${wineExeutable} launcher.bat`;
|
||||
|
||||
console.log(`Execution command: ${command}`);
|
||||
|
||||
/**
|
||||
* Starting the game
|
||||
*/
|
||||
const startTime = Date.now();
|
||||
|
||||
const process = await Process.run(command, {
|
||||
env: env,
|
||||
cwd: await constants.paths.gameDir
|
||||
});
|
||||
|
||||
// Game closed event
|
||||
process.finish(() => {
|
||||
const stopTime = Date.now();
|
||||
|
||||
// todo
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
100
src/ts/neutralino/Process.ts
Normal file
100
src/ts/neutralino/Process.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
declare const Neutralino;
|
||||
|
||||
type ProcessOptions = {
|
||||
input?: string;
|
||||
env?: object;
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
class Process
|
||||
{
|
||||
public readonly id: number;
|
||||
|
||||
/**
|
||||
* Interval between process status update
|
||||
*/
|
||||
public interval: number = 200;
|
||||
|
||||
protected _finished: boolean = false;
|
||||
|
||||
/**
|
||||
* Whether the process was finished
|
||||
*/
|
||||
public get finished(): boolean
|
||||
{
|
||||
return this._finished;
|
||||
};
|
||||
|
||||
protected onFinish?: (process: Process) => void;
|
||||
|
||||
public constructor(pid: number)
|
||||
{
|
||||
this.id = pid;
|
||||
|
||||
const updateStatus = async () => {
|
||||
Neutralino.os.execCommand(`ps -p ${this.id}`).then((output) => {
|
||||
// The process is still running
|
||||
if (output.stdOut.includes(this.id))
|
||||
setTimeout(updateStatus, this.interval);
|
||||
|
||||
// Otherwise the process was stopped
|
||||
else
|
||||
{
|
||||
this._finished = true;
|
||||
|
||||
if (this.onFinish)
|
||||
this.onFinish(this);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(updateStatus, this.interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify callback to run when the process will be finished
|
||||
*/
|
||||
public finish(callback: (process: Process) => void)
|
||||
{
|
||||
this.onFinish = callback;
|
||||
|
||||
if (this._finished)
|
||||
callback(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run shell command
|
||||
*/
|
||||
public static run(command: string, options: ProcessOptions = {}): Promise<Process>
|
||||
{
|
||||
// Replace '\a\b' to '\\a\\b'
|
||||
// And replace ''' to '\''
|
||||
const addSlashes = (str: string) => str.replaceAll('\\', '\\\\').replaceAll('\'', '\\\'');
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
// Set env variables
|
||||
if (options.env)
|
||||
{
|
||||
Object.keys(options.env).forEach((key) => {
|
||||
command = `${key}='${addSlashes(options.env![key])}' ${command}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Set current working directory
|
||||
if (options.cwd)
|
||||
command = `cd '${addSlashes(options.cwd)}' && ${command} && cd -`;
|
||||
|
||||
// And run the command
|
||||
const process = await Neutralino.os.execCommand(command, {
|
||||
background: true,
|
||||
stdin: options.input ?? ''
|
||||
});
|
||||
|
||||
resolve(new Process(process.pid));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type { ProcessOptions };
|
||||
|
||||
export default Process;
|
11
src/ts/types/Launcher.ts
Normal file
11
src/ts/types/Launcher.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
type LauncherState =
|
||||
| 'patch-unavailable'
|
||||
| 'test-patch-available'
|
||||
| 'patch-applying'
|
||||
| 'game-update-available'
|
||||
| 'game-installation-available'
|
||||
| 'game-voice-update-required'
|
||||
| 'resume-download-available'
|
||||
| 'game-launch-available';
|
||||
|
||||
export type { LauncherState };
|
Loading…
Reference in a new issue