diff --git a/README.md b/README.md
index 49b12ed..acf5a26 100644
--- a/README.md
+++ b/README.md
@@ -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
* Downloading progress
* Launcher state functionality
+ * Game launch available
+ * Game update (installation) required
+ * Voice data update (installation) required
+ * Patch unavailable
+ * Test patch available
* Make Vue components
* Checkbox
* Selection
diff --git a/index.html b/index.html
index a596c26..cb9386c 100644
--- a/index.html
+++ b/index.html
@@ -24,7 +24,7 @@
diff --git a/package.json b/package.json
index 55db141..ab79825 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/pages/index.ts b/src/pages/index.ts
index d177ef4..6db259e 100644
--- a/src/pages/index.ts
+++ b/src/pages/index.ts
@@ -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: () => ({
diff --git a/src/ts/Configs.ts b/src/ts/Configs.ts
index 71f9f96..32e8f37 100644
--- a/src/ts/Configs.ts
+++ b/src/ts/Configs.ts
@@ -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
indicates if the default settings were applied
*/
- public static defaults(configs: scalar): Promise
+ public static defaults(configs: object): Promise
{
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({}));
});
}
diff --git a/src/ts/Constants.ts b/src/ts/Constants.ts
index 21af4a0..9eda6fc 100644
--- a/src/ts/Constants.ts
+++ b/src/ts/Constants.ts
@@ -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
{
- return new Promise(async (resolve) => resolve(`${await this.launcherDir}/config.json`));
+ return new Promise(async (resolve) => resolve(`${await this.launcherDir}/config.yaml`));
}
/**
diff --git a/src/ts/Launcher.ts b/src/ts/Launcher.ts
index 2cfd9a2..c5bbd85 100644
--- a/src/ts/Launcher.ts
+++ b/src/ts/Launcher.ts
@@ -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
{
return new Promise(async (resolve) => {
diff --git a/src/ts/core/Runners.ts b/src/ts/core/Runners.ts
index ab6d4a9..1929add 100644
--- a/src/ts/core/Runners.ts
+++ b/src/ts/core/Runners.ts
@@ -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
+ {
+ 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
+ public static list(): Promise
{
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
+ {
+ 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
{
- 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
diff --git a/src/ts/core/promisify.ts b/src/ts/core/promisify.ts
new file mode 100644
index 0000000..7785e10
--- /dev/null
+++ b/src/ts/core/promisify.ts
@@ -0,0 +1,9 @@
+/**
+ * Make a promise from a synchronous function and run it
+ */
+export default function promisify(callback: () => any): Promise
+{
+ return new Promise((resolve) => {
+ resolve(callback());
+ });
+};
diff --git a/src/ts/launcher/State.ts b/src/ts/launcher/State.ts
new file mode 100644
index 0000000..26cd523
--- /dev/null
+++ b/src/ts/launcher/State.ts
@@ -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 = 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;
+ }
+ }
+};
diff --git a/src/ts/launcher/states/Launch.ts b/src/ts/launcher/states/Launch.ts
new file mode 100644
index 0000000..14dfd1a
--- /dev/null
+++ b/src/ts/launcher/states/Launch.ts
@@ -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 => {
+ 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();
+ });
+ });
+};
diff --git a/src/ts/neutralino/Process.ts b/src/ts/neutralino/Process.ts
new file mode 100644
index 0000000..5da65e9
--- /dev/null
+++ b/src/ts/neutralino/Process.ts
@@ -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
+ {
+ // 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;
diff --git a/src/ts/types/Launcher.ts b/src/ts/types/Launcher.ts
new file mode 100644
index 0000000..ba60adc
--- /dev/null
+++ b/src/ts/types/Launcher.ts
@@ -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 };
\ No newline at end of file