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:
Observer KRypt0n_ 2021-12-24 14:23:45 +02:00
parent 6e38611900
commit d1474f643a
No known key found for this signature in database
GPG key ID: DC5D4EC1303465DA
13 changed files with 402 additions and 35 deletions

View file

@ -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 * Make `Launcher` class to manage launcher-related features
* <s>Downloading progress</s> * <s>Downloading progress</s>
* Launcher state functionality * 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 * Make Vue components
* Checkbox * Checkbox
* Selection * Selection

View file

@ -24,7 +24,7 @@
</div> </div>
<div id="launcher-content"> <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>
<div id="settings"> <div id="settings">

View file

@ -9,7 +9,8 @@
}, },
"dependencies": { "dependencies": {
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"vue": "^3.2.25" "vue": "^3.2.25",
"yaml": "^1.10.2"
}, },
"devDependencies": { "devDependencies": {
"@neutralinojs/neu": "^8.0.0", "@neutralinojs/neu": "^8.0.0",

View file

@ -5,16 +5,39 @@ import Window from '../ts/neutralino/Window';
import Launcher from '../ts/Launcher'; import Launcher from '../ts/Launcher';
import Configs from '../ts/Configs'; import Configs from '../ts/Configs';
import constants from '../ts/Constants'; import constants from '../ts/Constants';
import promisify from '../ts/core/promisify';
import Process from '../ts/neutralino/Process';
(async () => { promisify(async () => {
Configs.defaults({ Configs.defaults({
lang: { lang: {
launcher: 'en-us', launcher: 'en-us',
voice: '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({ let app = createApp({
data: () => ({ data: () => ({

View file

@ -1,3 +1,5 @@
import YAML from 'yaml';
import constants from './Constants'; import constants from './Constants';
declare const Neutralino; declare const Neutralino;
@ -19,7 +21,7 @@ export default class Configs
{ {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
Neutralino.filesystem.readFile(await constants.paths.config).then((config) => { Neutralino.filesystem.readFile(await constants.paths.config).then((config) => {
config = JSON.parse(config); config = YAML.parse(config);
if (name !== '') if (name !== '')
{ {
@ -54,12 +56,12 @@ export default class Configs
value = await Promise.resolve(value); value = await Promise.resolve(value);
Neutralino.filesystem.readFile(await constants.paths.config).then(async (config) => { 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) Neutralino.filesystem.writeFile(await constants.paths.config, config)
.then(() => resolve()); .then(() => resolve());
}).catch(async () => { }).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) Neutralino.filesystem.writeFile(await constants.paths.config, config)
.then(() => resolve()); .then(() => resolve());
@ -74,32 +76,32 @@ export default class Configs
* *
* @returns Promise<void> indicates if the default settings were applied * @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) => { return new Promise(async (resolve) => {
const setDefaults = async (current: scalar) => { const setDefaults = async (current: object) => {
const updateDefaults = (current: scalar, defaults: scalar) => { const updateDefaults = (current: object, defaults: object) => {
Object.keys(defaults!).forEach((key) => { Object.keys(defaults).forEach((key) => {
// If the field exists in defaults and doesn't exist in current // If the field exists in defaults and doesn't exist in current
if (current![key] === undefined) if (current[key] === undefined)
current![key] = defaults![key]; current[key] = defaults[key];
// If both of default and current are objects // If both of default and current are objects
else if (typeof current![key] == 'object' && typeof defaults![key] == 'object') // and we also should check if they're not nulls
current![key] = updateDefaults(current![key], defaults![key]); // 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; return current;
}; };
current = JSON.stringify(updateDefaults(current, configs), null, 4); Neutralino.filesystem.writeFile(await constants.paths.config, YAML.stringify(updateDefaults(current, configs)))
Neutralino.filesystem.writeFile(await constants.paths.config, current)
.then(() => resolve()); .then(() => resolve());
}; };
Neutralino.filesystem.readFile(await constants.paths.config) Neutralino.filesystem.readFile(await constants.paths.config)
.then((config) => setDefaults(JSON.parse(config))) .then((config) => setDefaults(YAML.parse(config)))
.catch(() => setDefaults({})); .catch(() => setDefaults({}));
}); });
} }

View file

@ -83,11 +83,11 @@ class Paths
/** /**
* Config file * Config file
* *
* @default "~/.local/share/anime-game-launcher/config.json" * @default "~/.local/share/anime-game-launcher/config.yaml"
*/ */
public static get config(): Promise<string> 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`));
} }
/** /**

View file

@ -1,18 +1,22 @@
import constants from './Constants'; import constants from './Constants';
import Configs from './Configs'; import Configs from './Configs';
import Background from './launcher/Background'; import Background from './launcher/Background';
import ProgressBar from './launcher/ProgressBar'; import ProgressBar from './launcher/ProgressBar';
import State from './launcher/State';
declare const Neutralino;
export default class Launcher export default class Launcher
{ {
public app; public app;
public state: State;
public progressBar: ProgressBar; public progressBar: ProgressBar;
public constructor(app) public constructor(app)
{ {
this.app = app; this.app = app;
this.state = new State(this);
this.progressBar = new ProgressBar(this); this.progressBar = new ProgressBar(this);
// Progress bar test // Progress bar test
@ -40,6 +44,9 @@ export default class Launcher
t(0); t(0);
} }
/**
* Update launcher background picture
*/
public updateBackground(): Promise<void> public updateBackground(): Promise<void>
{ {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {

View file

@ -4,6 +4,7 @@ import {
} from '../types/Runners'; } from '../types/Runners';
import constants from '../Constants'; import constants from '../Constants';
import Configs from '../Configs';
import AbstractInstaller from './AbstractInstaller'; import AbstractInstaller from './AbstractInstaller';
declare const Neutralino; declare const Neutralino;
@ -18,10 +19,25 @@ class Stream extends AbstractInstaller
class Runners 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 * Get runners list
*/ */
public static get(): Promise<RunnerFamily[]> public static list(): Promise<RunnerFamily[]>
{ {
return new Promise((resolve) => { return new Promise((resolve) => {
constants.paths.runnersDir.then(async (runnersDir: string) => { 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 * Download runner to the [constants.paths.runners] directory
* *
@ -66,21 +105,14 @@ class Runners
*/ */
public static download(runner: Runner|Runner['name']): Promise<null|Stream> 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 // If we provided runner parameter with a name of a runner
// then we should find this runner and call this method for it // then we should find this runner and call this method for it
if (typeof runner == 'string') if (typeof runner == 'string')
{ {
let foundRunner; this.get(runner).then((foundRunner) => {
resolve(foundRunner === null ? null : new Stream(foundRunner));
(await this.get()).forEach((family) => {
family.runners.forEach((familyRunner) => {
if (familyRunner.name == runner)
foundRunner = familyRunner;
});
}); });
resolve(foundRunner === null ? null : new Stream(foundRunner));
} }
// Otherwise we can use runner.uri and so on to download runner // Otherwise we can use runner.uri and so on to download runner

9
src/ts/core/promisify.ts Normal file
View 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
View 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;
}
}
};

View 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();
});
});
};

View 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
View 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 };