Merge branch 'empathize' into 'main'

Moved launcher on empathize

See merge request KRypt0n_/an-anime-game-launcher!24
This commit is contained in:
Observer KRypt0n_ 2022-01-28 17:58:53 +00:00
commit 236113bd15
50 changed files with 246 additions and 2097 deletions

View file

@ -11,6 +11,7 @@
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"dependencies": {
"@empathize/framework": "^1.3.5",
"js-md5": "^0.7.3",
"semver": "^7.3.5",
"svelte-i18n": "^3.3.13",
@ -18,12 +19,12 @@
},
"devDependencies": {
"@neutralinojs/neu": "^9.1.1",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.35",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.36",
"@tsconfig/svelte": "^3.0.0",
"@types/js-md5": "^0.4.3",
"neutralino-appimage-bundler": "^1.3.2",
"sass": "^1.49.0",
"svelte": "^3.46.2",
"svelte": "^3.46.3",
"svelte-check": "^2.3.0",
"svelte-preprocess": "^4.10.2",
"tslib": "^2.3.1",

View file

@ -1,6 +1,8 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { Configs } from '../empathize';
export let active: boolean = false;
export let disabled: boolean = false;
@ -12,8 +14,6 @@
import Checkmark from '../assets/svgs/checkmark.svg';
import Configs from '../ts/Configs';
Configs.get(prop).then((value) => active = value as boolean);
function updateCheckbox()

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import Configs from '../ts/Configs';
import { Configs } from '../empathize';
export let visible: boolean = false;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import Configs from '../ts/Configs';
import { Configs } from '../empathize';
export let prop: string = '';
export let lang: string = '';

View file

@ -1,13 +1,15 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import Configs from '../ts/Configs';
import { Configs } from '../empathize';
import Button from './Button.svelte';
let last_id = 0, variables = {}, selected;
Configs.get('env').then((env) => {
if (env)
{
for (const key of Object.keys(env as object))
{
variables[last_id++] = {
@ -15,6 +17,7 @@
value: env![key]
};
}
}
});
const updateEnv = () => {
@ -29,6 +32,7 @@
</script>
<div>
{#if Object.keys(variables).length > 0}
<table class="table properties-table" style="margin-top: 16px">
<tr>
<th>{$_('settings.environment.items.table.name')}</th>
@ -49,6 +53,9 @@
</tr>
{/each}
</table>
{:else}
<p>There're no variables here</p>
{/if}
<div style="margin-top: 16px">
<Button lang="settings.environment.items.buttons.add" click={() => variables[last_id++] = { key: '', value: '' }} />

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import Configs from '../ts/Configs';
import { Configs } from '../empathize';
export let prop: string = '';
export let lang: string = '';

View file

@ -1,6 +1,6 @@
import Configs from './ts/Configs';
import { Configs, promisify } from './empathize';
import constants from './ts/Constants';
import promisify from './ts/core/promisify';
promisify(async () => {
Configs.defaults({

55
src/empathize.ts Normal file
View file

@ -0,0 +1,55 @@
import {
// Paths API
path, dir,
// Filesystem API
fs,
// Windows API
Windows,
// OS API
Process, Tray, IPC, Notification, Archive,
// Network API
fetch, Domain, Downloader,
// Async API
promisify,
// Meta classes
Cache, Configs, Debug
} from '@empathize/framework';
import YAML from 'yaml';
import constants from './ts/Constants';
Configs.file = constants.paths.config;
Cache.file = constants.paths.cache;
Configs.serialize = YAML.stringify;
Configs.unserialize = YAML.parse;
export {
// Paths API
path, dir,
// Filesystem API
fs,
// Windows API
Windows,
// OS API
Process, Tray, IPC, Notification, Archive,
// Network API
fetch, Domain, Downloader,
// Async API
promisify,
// Meta classes
Cache, Configs, Debug
};

View file

@ -6,18 +6,12 @@
import { onMount } from 'svelte';
import { _, locale } from 'svelte-i18n';
import Window from './ts/neutralino/Window';
import Process from './ts/neutralino/Process';
import { Windows, path, Archive, Debug, Downloader, IPC, Configs } from './empathize';
import Launcher from './ts/Launcher';
import constants from './ts/Constants';
import Game from './ts/Game';
import Background from './ts/launcher/Background';
import Archive from './ts/core/Archive';
import Debug from './ts/core/Debug';
import Downloader from './ts/core/Downloader';
import IPC from './ts/core/IPC';
import Configs from './ts/Configs';
import Gear from './assets/images/gear.png';
import GearActive from './assets/images/gear-active.png';
@ -36,7 +30,7 @@
const launcher = new Launcher(onMount);
Neutralino.events.on('ready', () => {
Window.open('splash', {
Windows.open('splash', {
title: 'Splash',
width: 300,
height: 400,
@ -60,13 +54,13 @@
await launcher.rpc.stop(true);
// Remove .tmp files from the temp folder
await Neutralino.os.execCommand(`find "${Process.addSlashes(tempDir)}" -maxdepth 1 -type f -name "*.tmp" -delete`);
await Neutralino.os.execCommand(`find "${path.addSlashes(tempDir)}" -maxdepth 1 -type f -name "*.tmp" -delete`);
// Remove old launcher's log files
const purge_logs = await Configs.get('purge_logs.launcher') as string|null;
if (purge_logs !== null && purge_logs[purge_logs.length - 1] == 'd')
await Neutralino.os.execCommand(`find "${Process.addSlashes(launcherDir)}/logs" -maxdepth 1 -mtime ${purge_logs.substring(0, purge_logs.length - 1)} -delete`);
await Neutralino.os.execCommand(`find "${path.addSlashes(launcherDir)}/logs" -maxdepth 1 -mtime ${purge_logs.substring(0, purge_logs.length - 1)} -delete`);
// Save logs
const log = Debug.get().join('\r\n');
@ -103,7 +97,7 @@
* Update launcher's title
*/
Game.latest.then((game) => {
Window.current.setTitle(`${constants.placeholders.uppercase.full} Linux Launcher - ${game.version}`);
Windows.current.setTitle(`${constants.placeholders.uppercase.full} Linux Launcher - ${game.version}`);
});
/**

View file

@ -6,16 +6,11 @@
import { onMount } from 'svelte';
import { _, locale, locales } from 'svelte-i18n';
import Window from './ts/neutralino/Window';
import Process from './ts/neutralino/Process';
import { Windows, Configs, Debug, IPC, Process, path } from './empathize';
import constants from './ts/Constants';
import Configs from './ts/Configs';
import Launcher from './ts/Launcher';
import FPSUnlock from './ts/FPSUnlock';
import Debug from './ts/core/Debug';
import IPC from './ts/core/IPC';
import Runners from './ts/core/Runners';
import Button from './components/Button.svelte';
@ -41,72 +36,6 @@
launcherLocales = launcherLocales;
/**
* Game voice packs languages
*/
const voiceLocales = {
'en-us': 'settings.general.items.lang.voice.items.en-us',
'ja-jp': 'settings.general.items.lang.voice.items.ja-jp',
'ko-kr': 'settings.general.items.lang.voice.items.ko-kr',
'zn-cn': 'settings.general.items.lang.voice.items.zn-cn'
};
/**
* Themes
*/
const themes = {
'system': 'settings.general.items.theme.items.system',
'light': 'settings.general.items.theme.items.light',
'dark': 'settings.general.items.theme.items.dark'
};
/**
* HUD options
*/
const huds = {
'none': 'settings.enhancements.items.hud.items.none',
'dxvk': 'settings.enhancements.items.hud.items.dxvk',
'mangohud': 'settings.enhancements.items.hud.items.mangohud'
};
/**
* Wine synchronizations
*/
const winesyncs = {
'none': 'settings.enhancements.items.winesync.items.none',
'esync': 'settings.enhancements.items.winesync.items.esync',
'fsync': 'settings.enhancements.items.winesync.items.fsync'
};
/**
* Delete launcher logs options
*/
const purgeLauncherLogs = {
'1d': 'settings.enhancements.items.purge_logs.launcher.items.1d',
'3d': 'settings.enhancements.items.purge_logs.launcher.items.3d',
'5d': 'settings.enhancements.items.purge_logs.launcher.items.5d',
'7d': 'settings.enhancements.items.purge_logs.launcher.items.7d',
'14d': 'settings.enhancements.items.purge_logs.launcher.items.14d',
'never': 'settings.enhancements.items.purge_logs.launcher.items.never'
};
/**
* Menu items
*/
const menuItems = [
'general',
'enhancements',
'runners',
'dxvks',
'shaders',
'environment'
];
/**
* Some components stuff
*/
@ -203,8 +132,8 @@
// Do some stuff when all the content will be loaded
onMount(async () => {
await Window.current.show();
await Window.current.center(900, 600);
await Windows.current.show();
// FIXME: await Windows.current.center(900, 600);
// This thing will fix window resizing
// in several cases (wayland + gnome + custom theme)
@ -214,7 +143,7 @@
else
{
Window.current.setSize({
Windows.current.setSize({
width: 900 + (900 - window.innerWidth),
height: 600 + (600 - window.innerHeight),
resizable: false
@ -241,8 +170,13 @@
{#if typeof $locale === 'string'}
<main>
<div class="menu">
{#each menuItems as item}
<div class="menu-item" on:click={changeItem} class:menu-item-active={selectedItem === item} data-anchor={item}>{ $_(`settings.${item}.title`) }</div>
{#each ['general', 'enhancements', 'runners', 'dxvks', 'shaders', 'environment'] as item}
<div
class="menu-item"
class:menu-item-active={selectedItem === item}
data-anchor={item}
on:click={changeItem}
>{$_(`settings.${item}.title`)}</div>
{/each}
</div>
@ -269,14 +203,23 @@
tooltip="settings.general.items.lang.voice.tooltip"
prop="lang.voice"
selected={undefined}
items={voiceLocales}
items={{
'en-us': 'settings.general.items.lang.voice.items.en-us',
'ja-jp': 'settings.general.items.lang.voice.items.ja-jp',
'ko-kr': 'settings.general.items.lang.voice.items.ko-kr',
'zn-cn': 'settings.general.items.lang.voice.items.zn-cn'
}}
selectionUpdated={() => voiceUpdateRequired = true}
/>
<SelectionBox
lang="settings.general.items.theme.title"
prop="theme"
items={themes}
items={{
'system': 'settings.general.items.theme.items.system',
'light': 'settings.general.items.theme.items.light',
'dark': 'settings.general.items.theme.items.dark'
}}
valueChanged={switchTheme}
/>
@ -294,7 +237,7 @@
const runnersDir = await constants.paths.runnersDir;
Process.run(`"${Process.addSlashes(await constants.paths.launcherDir)}/winetricks.sh"`, {
Process.run(`"${path.addSlashes(await constants.paths.launcherDir)}/winetricks.sh"`, {
env: {
WINE: runner ? `${runnersDir}/${runner.name}/${runner.files.wine}` : 'wine',
WINESERVER: runner ? `${runnersDir}/${runner.name}/${runner.files.wineserver}` : 'wineserver',
@ -308,7 +251,7 @@
const runnerDir = runner ? `${await constants.paths.runnersDir}/${runner.name}` : '';
Process.run(runner ? `"${Process.addSlashes(`${runnerDir}/${runner.files.wine}`)}" "${Process.addSlashes(`${runnerDir}/${runner.files.winecfg}`)}"` : 'winecfg', {
Process.run(runner ? `"${path.addSlashes(`${runnerDir}/${runner.files.wine}`)}" "${path.addSlashes(`${runnerDir}/${runner.files.winecfg}`)}"` : 'winecfg', {
env: {
WINE: runner ? `${runnerDir}/${runner.files.wine}` : 'wine',
WINESERVER: runner ? `${runnerDir}/${runner.files.wineserver}` : 'wineserver',
@ -319,14 +262,14 @@
<!-- svelte-ignore missing-declaration -->
<Button lang="settings.general.items.buttons.launcher" click={async () => {
Neutralino.os.execCommand(`xdg-open "${Process.addSlashes(await constants.paths.launcherDir)}"`, {
Neutralino.os.execCommand(`xdg-open "${path.addSlashes(await constants.paths.launcherDir)}"`, {
background: true
});
}} />
<!-- svelte-ignore missing-declaration -->
<Button lang="settings.general.items.buttons.game" click={async () => {
Neutralino.os.execCommand(`xdg-open "${Process.addSlashes(await constants.paths.gameDir)}"`, {
Neutralino.os.execCommand(`xdg-open "${path.addSlashes(await constants.paths.gameDir)}"`, {
background: true
});
}} />
@ -339,7 +282,11 @@
<SelectionBox
lang="settings.enhancements.items.hud.title"
prop="hud"
items={huds}
items={{
'none': 'settings.enhancements.items.hud.items.none',
'dxvk': 'settings.enhancements.items.hud.items.dxvk',
'mangohud': 'settings.enhancements.items.hud.items.mangohud'
}}
/>
<SelectionBox
@ -347,7 +294,11 @@
prop="winesync"
tooltip="settings.enhancements.items.winesync.tooltip"
tooltip_size="large"
items={winesyncs}
items={{
'none': 'settings.enhancements.items.winesync.items.none',
'esync': 'settings.enhancements.items.winesync.items.esync',
'fsync': 'settings.enhancements.items.winesync.items.fsync'
}}
/>
<Checkbox
@ -388,7 +339,14 @@
lang="settings.enhancements.items.purge_logs.launcher.title"
tooltip="settings.enhancements.items.purge_logs.launcher.tooltip"
prop="purge_logs.launcher"
items={purgeLauncherLogs}
items={{
'1d': 'settings.enhancements.items.purge_logs.launcher.items.1d',
'3d': 'settings.enhancements.items.purge_logs.launcher.items.3d',
'5d': 'settings.enhancements.items.purge_logs.launcher.items.5d',
'7d': 'settings.enhancements.items.purge_logs.launcher.items.7d',
'14d': 'settings.enhancements.items.purge_logs.launcher.items.14d',
'never': 'settings.enhancements.items.purge_logs.launcher.items.never'
}}
/>
</div>

View file

@ -6,10 +6,7 @@
import { onMount } from 'svelte';
import { _, locale } from 'svelte-i18n';
import Configs from './ts/Configs';
import IPC from './ts/core/IPC';
import Window from './ts/neutralino/Window';
import { Configs, IPC, Windows } from './empathize';
import Splash from './assets/gifs/running-qiqi.gif';
import SplashSecret from './assets/gifs/loading-marie-please.gif';
@ -19,8 +16,8 @@
let phrase = Math.round(Math.random() * 8);
onMount(() => {
Window.current.show();
Window.current.center(300, 400);
Windows.current.show();
// FIXME: Windows.current.center(300, 400);
});
const isLauncherLoaded = () => {
@ -32,7 +29,7 @@
for (const record of launcherLoaded)
await record.pop();
Window.current.hide();
Windows.current.hide();
Neutralino.app.exit();
}

View file

@ -1,110 +0,0 @@
import YAML from 'yaml';
import constants from './Constants';
declare const Neutralino;
// Ok yea, null, object and boolean aren't scalars
// but I don't care
type scalar = null | string | number | boolean | object;
export default class Configs
{
/**
* Get config value
*
* @param name config name, e.g. "lang.launcher"
*
* @returns undefined if config doesn't exist. Otherwise - config value
*/
public static get(name: string = ''): Promise<undefined|scalar|scalar[]>
{
return new Promise(async (resolve) => {
Neutralino.filesystem.readFile(await constants.paths.config).then((config) => {
config = YAML.parse(config);
if (name !== '')
{
name.split('.').forEach((value) => {
config = config[value];
});
}
resolve(config);
}).catch(() => {
setTimeout(() => resolve(this.get(name)), 100);
});
});
}
/**
* Set config value
*
* @param name config name, e.g. "lang.launcher"
* @param value config value, e.g. "en-us"
*
* @returns Promise<void> indicates if the settings were updated
*/
public static set(name: string, value: scalar|scalar[]|Promise<scalar|scalar[]>): Promise<void>
{
const getUpdatedArray = (path: string[], array: scalar|scalar[], value: scalar|scalar[]): scalar|scalar[] => {
array![path[0]] = path.length > 1 ?
getUpdatedArray(path.slice(1), array![path[0]] ?? {}, value) : value;
return array;
};
return new Promise(async (resolve) => {
value = await Promise.resolve(value);
Neutralino.filesystem.readFile(await constants.paths.config).then(async (config) => {
config = YAML.stringify(getUpdatedArray(name.split('.'), YAML.parse(config), value));
Neutralino.filesystem.writeFile(await constants.paths.config, config)
.then(() => resolve());
}).catch(async () => {
let config = YAML.stringify(getUpdatedArray(name.split('.'), {}, value));
Neutralino.filesystem.writeFile(await constants.paths.config, config)
.then(() => resolve());
});
});
}
/**
* Set default values
*
* @param configs object of default values
*
* @returns Promise<void> indicates if the default settings were applied
*/
public static defaults(configs: object): Promise<void>
{
return new Promise(async (resolve) => {
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 both default and current are objects
// 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;
};
Neutralino.filesystem.writeFile(await constants.paths.config, YAML.stringify(updateDefaults(current, configs)))
.then(() => resolve());
};
Neutralino.filesystem.readFile(await constants.paths.config)
.then((config) => setDefaults(YAML.parse(config)))
.catch(() => setDefaults({}));
});
}
}

View file

@ -1,4 +1,4 @@
import Configs from './Configs';
import { Configs } from '../empathize';
declare const Neutralino;
declare const NL_CWD;

View file

@ -1,6 +1,6 @@
import { Downloader, path } from '../empathize';
import constants from './Constants';
import Downloader from './core/Downloader';
import Process from './neutralino/Process';
declare const Neutralino;
@ -35,7 +35,7 @@ export default class FPSUnlock
Downloader.download(constants.uri.fpsunlock.bat, fpsunlockBat).then((stream) => {
stream.finish(async () => {
// sed -i 's/start ..\/GI_FPSUnlocker\/unlockfps.exe \%\*/start ..\/fpsunlock\/unlockfps.exe \%\*/g' unlockfps.bat
Neutralino.os.execCommand(`sed -i 's/start ..\\/GI_FPSUnlocker\\/unlockfps.exe \\%\\*/start ..\\/fpsunlock\\/unlockfps.exe \\%\\*/g' "${Process.addSlashes(fpsunlockBat)}"`)
Neutralino.os.execCommand(`sed -i 's/start ..\\/GI_FPSUnlocker\\/unlockfps.exe \\%\\*/start ..\\/fpsunlock\\/unlockfps.exe \\%\\*/g' "${path.addSlashes(fpsunlockBat)}"`)
.then(() => resolve());
});
});

View file

@ -5,14 +5,13 @@ import type {
Diff
} from './types/GameData';
import type { Stream as DownloadingStream } from '@empathize/framework/dist/network/Downloader';
import { fetch, Domain, promisify, Downloader, Cache, Debug } from '../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import constants from './Constants';
import fetch from './core/Fetch';
import AbstractInstaller from './core/AbstractInstaller';
import Domain from './core/Domain';
import promisify from './core/promisify';
import Debug, { DebugThread } from './core/Debug';
import Downloader, { Stream as DownloadingStream } from './core/Downloader';
import Cache from './core/Cache';
declare const Neutralino;

View file

@ -1,13 +1,11 @@
import { locale } from 'svelte-i18n';
import Window from './neutralino/Window';
import Process from './neutralino/Process';
import Tray from './neutralino/Tray';
import {
Windows, Process, Tray,
Configs, Debug, IPC
} from '../empathize';
import constants from './Constants';
import Configs from './Configs';
import Debug from './core/Debug';
import IPC from './core/IPC';
import DiscordRPC from './core/DiscordRPC';
import Locales from './launcher/Locales';
@ -56,7 +54,7 @@ export default class Launcher
{
this.settingsMenu = undefined;
const window = await Window.open('settings', {
const window = await Windows.open('settings', {
title: 'Settings',
width: 900,
height: 600,
@ -96,11 +94,11 @@ export default class Launcher
});
});
Window.current.show();
Window.current.center(1280, 700);
Windows.current.show();
// TODO: Windows.current.center(1280, 700);
})
Window.current.hide();
Windows.current.hide();
}
resolve(window.status);

View file

@ -2,14 +2,12 @@ import type { PatchInfo } from './types/Patch';
import md5 from 'js-md5';
import { fetch, promisify, Debug, Cache, path } from '../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import constants from './Constants';
import Game from './Game';
import fetch from './core/Fetch';
import AbstractInstaller from './core/AbstractInstaller';
import promisify from './core/promisify';
import Process from './neutralino/Process';
import Debug, { DebugThread } from './core/Debug';
import Cache from './core/Cache';
declare const Neutralino;
@ -50,37 +48,37 @@ class Stream extends AbstractInstaller
/**
* Remove test version restrictions from the main patch
*/
() => Neutralino.os.execCommand(`cd "${Process.addSlashes(patchDir)}" && sed -i '/^echo "If you would like to test this patch, modify this script and remove the line below this one."/,+5d' patch.sh`),
() => Neutralino.os.execCommand(`cd "${path.addSlashes(patchDir)}" && sed -i '/^echo "If you would like to test this patch, modify this script and remove the line below this one."/,+5d' patch.sh`),
/**
* Remove /etc/hosts editing due to sudo permissions
*/
() => Neutralino.os.execCommand(`cd "${Process.addSlashes(patchDir)}" && sed -i '/^# ===========================================================/,+68d' patch.sh`),
() => Neutralino.os.execCommand(`cd "${path.addSlashes(patchDir)}" && sed -i '/^# ===========================================================/,+68d' patch.sh`),
/**
* Remove test version restrictions from the anti-login crash patch
*/
() => Neutralino.os.execCommand(`cd "${Process.addSlashes(patchDir)}" && sed -i '/^echo " necessary afterwards (Friday?). If that's the case, comment the line below."/,+2d' patch_anti_logincrash.sh`),
() => Neutralino.os.execCommand(`cd "${path.addSlashes(patchDir)}" && sed -i '/^echo " necessary afterwards (Friday?). If that's the case, comment the line below."/,+2d' patch_anti_logincrash.sh`),
/**
* Make the main patch executable
*/
() => Neutralino.os.execCommand(`chmod +x "${Process.addSlashes(patchDir)}/patch.sh"`),
() => Neutralino.os.execCommand(`chmod +x "${path.addSlashes(patchDir)}/patch.sh"`),
/**
* Make the anti-login crash patch executable
*/
() => Neutralino.os.execCommand(`chmod +x "${Process.addSlashes(patchDir)}/patch_anti_logincrash.sh"`),
() => Neutralino.os.execCommand(`chmod +x "${path.addSlashes(patchDir)}/patch_anti_logincrash.sh"`),
/**
* Execute the main patch installation script
*/
() => Neutralino.os.execCommand(`cd "${Process.addSlashes(gameDir)}" && yes yes | bash "${Process.addSlashes(patchDir)}/patch.sh"`),
() => Neutralino.os.execCommand(`cd "${path.addSlashes(gameDir)}" && yes yes | bash "${path.addSlashes(patchDir)}/patch.sh"`),
/**
* Execute the anti-login crash patch installation script
*/
() => Neutralino.os.execCommand(`cd "${Process.addSlashes(gameDir)}" && yes | bash "${Process.addSlashes(patchDir)}/patch_anti_logincrash.sh"`)
() => Neutralino.os.execCommand(`cd "${path.addSlashes(gameDir)}" && yes | bash "${path.addSlashes(patchDir)}/patch_anti_logincrash.sh"`)
]
});

View file

@ -1,14 +1,14 @@
import type { VoicePack } from './types/GameData';
import type { VoiceLang, InstalledVoice } from './types/Voice';
import type { Stream as DownloadingStream } from '@empathize/framework/dist/network/Downloader';
import { Configs, Debug, Downloader, promisify, path } from '../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import constants from './Constants';
import Game from './Game';
import AbstractInstaller from './core/AbstractInstaller';
import Configs from './Configs';
import Debug, { DebugThread } from './core/Debug';
import Downloader, { Stream as DownloadingStream } from './core/Downloader';
import Process from './neutralino/Process';
import promisify from './core/promisify';
declare const Neutralino;
@ -170,15 +170,15 @@ export default class Voice
const pipeline = promisify({
callbacks: [
() => Neutralino.os.execCommand(`rm -rf "${Process.addSlashes(`${voiceDir}/${this.langs[lang]}`)}"`),
() => Neutralino.os.execCommand(`rm -rf "${path.addSlashes(`${voiceDir}/${this.langs[lang]}`)}"`),
(): Promise<void> => new Promise(async (resolve) => {
Neutralino.os.execCommand(`rm -f "${Process.addSlashes(`${await constants.paths.gameDir}/Audio_${this.langs[lang]}_pkg_version`)}"`)
Neutralino.os.execCommand(`rm -f "${path.addSlashes(`${await constants.paths.gameDir}/Audio_${this.langs[lang]}_pkg_version`)}"`)
.then(() => resolve());
}),
(): Promise<void> => new Promise(async (resolve) => {
Neutralino.os.execCommand(`sed -i '/${this.langs[lang]}/d' "${Process.addSlashes(`${await constants.paths.gameDataDir}/Persistent/audio_lang_14`)}"`)
Neutralino.os.execCommand(`sed -i '/${this.langs[lang]}/d' "${path.addSlashes(`${await constants.paths.gameDataDir}/Persistent/audio_lang_14`)}"`)
.then(() => resolve());
})
],

View file

@ -1,7 +1,9 @@
import type { Stream as DownloadStream } from '@empathize/framework/dist/network/Downloader';
import { Downloader, Archive } from '../../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import constants from '../Constants';
import Downloader, { Stream as DownloadStream } from './Downloader';
import Archive from './Archive';
import { DebugThread } from './Debug';
declare const Neutralino;
@ -68,7 +70,7 @@ export default abstract class Installer
if (shouldResolve)
debugThread.log(`Resolved unpack dir: ${unpackDir}`);
Archive.unpack(archivePath, unpackDir).then((stream) => {
Archive.extract(archivePath, unpackDir).then((stream) => {
stream.progressInterval = this.unpackProgressInterval;
stream.start(() => {

View file

@ -1,364 +0,0 @@
import type {
ArchiveType,
Size,
File,
ArchiveInfo
} from '../types/Archive';
import { DebugThread } from './Debug';
import promisify from './promisify';
import Process from '../neutralino/Process';
declare const Neutralino;
declare const NL_CWD;
class Stream
{
protected _id: number = -1;
/**
* ID of the archive unpacker process
*/
public get id(): number
{
return this._id;
}
/**
* The interval in ms between progress event calls
*/
public progressInterval: number = 500;
protected path: string;
protected unpackDir: string|null;
protected unpacked: number = 0;
protected archive?: ArchiveInfo;
protected onStart?: () => void;
protected onProgress?: (current: number, total: number, difference: number) => void;
protected onFinish?: () => void;
protected onError?: () => void;
protected started: boolean = false;
protected finished: boolean = false;
protected throwedError: boolean = false;
/**
* @param path path to archive
* @param unpackDir directory to extract the files to
*/
public constructor(path: string, unpackDir: string|null = null)
{
this.path = path;
this.unpackDir = unpackDir;
this.started = true;
const debugThread = new DebugThread('Archive/Stream', {
message: {
'path': path,
'unpack dir': unpackDir
}
});
if (this.onStart)
this.onStart();
Archive.getInfo(path).then((info) => {
if (info === null)
{
this.throwedError = true;
if (this.onError)
this.onError();
}
else
{
this.archive = info;
let command = {
tar: `tar -xvf "${Process.addSlashes(path)}"${unpackDir ? ` -C "${Process.addSlashes(unpackDir)}"` : ''}`,
zip: `unzip -o "${Process.addSlashes(path)}"${unpackDir ? ` -d "${Process.addSlashes(unpackDir)}"` : ''}`
}[this.archive.type!];
if (unpackDir)
command = `mkdir -p "${Process.addSlashes(unpackDir)}" && ${command}`;
let remainedFiles = this.archive.files;
const baseDir = unpackDir ?? NL_CWD;
Neutralino.os.execCommand(command, {
background: true
}).then((result) => {
this._id = result.pid;
});
debugThread.log(`Unpacking started with command: ${command}`);
const updateProgress = async () => {
let difference: number = 0;
let pool: any[] = [];
remainedFiles.forEach((file) => {
if (file.path != '#unpacked#')
{
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#';
resolve();
})
.catch(() => resolve())
});
});
}
});
await promisify({
callbacks: pool,
callAtOnce: true,
interval: 200
});
remainedFiles = remainedFiles.filter((file) => file.path != '#unpacked#');
if (this.onProgress)
this.onProgress(this.unpacked, this.archive!.size.uncompressed!, difference);
if (this.unpacked >= this.archive!.size.uncompressed!)
{
this.finished = true;
debugThread.log('Unpacking finished');
if (this.onFinish)
this.onFinish();
}
if (!this.finished)
setTimeout(updateProgress, this.progressInterval);
};
setTimeout(updateProgress, this.progressInterval);
}
});
}
/**
* Specify event that will be called when the extraction has started
*
* @param callback
*/
public start(callback: () => void)
{
this.onStart = callback;
if (this.started)
callback();
}
/**
* Specify event that will be called every [this.progressInterval] ms while extracting the archive
*
* @param callback
*/
public progress(callback: (current: number, total: number, difference: number) => void)
{
this.onProgress = callback;
}
/**
* Specify event that will be called after the archive has been extracted
*
* @param callback
*/
public finish(callback: () => void)
{
this.onFinish = callback;
if (this.finished)
callback();
}
/**
* Specify event that will be called if archive can't be extracted
*
* @param callback
*/
public error(callback: () => void)
{
this.onError = callback;
if (this.throwedError)
callback();
}
/**
* Close unpacking stream
*/
public close(forced: boolean = false)
{
Neutralino.os.execCommand(`kill ${forced ? '-9' : '-15'} ${this._id}`);
}
}
export default class Archive
{
protected static streams: Stream[] = [];
/**
* Get type of archive
*
* @param path path to archive
* @returns supported archive type or null
*/
public static getType(path: string): ArchiveType|null
{
if (path.substring(path.length - 4) == '.zip')
return 'zip';
else if (path.substring(path.length - 7, path.length - 2) == '.tar.')
return 'tar';
else return null;
}
/**
* Get archive info
*
* @param path path to archive
* @returns null if the archive has unsupported type. Otherwise - archive info
*/
public static getInfo(path: string): Promise<ArchiveInfo|null>
{
const debugThread = new DebugThread('Archive.getInfo', `Getting info about archive: ${path}`);
return new Promise(async (resolve) => {
let archive: ArchiveInfo = {
size: {
compressed: null,
uncompressed: null
},
type: this.getType(path),
files: []
};
switch (archive.type)
{
case 'tar':
const tarOutput = await Neutralino.os.execCommand(`tar -tvf "${path}"`);
for (const match of tarOutput.stdOut.matchAll(/^[dwxr\-]+ [\w/]+[ ]+(\d+) [0-9\-]+ [0-9\:]+ (.+)/gm))
{
let fileSize = parseInt(match[1]);
archive.size.uncompressed! += fileSize;
archive.files.push({
path: match[2],
size: {
compressed: null,
uncompressed: fileSize
}
});
}
debugThread.log({
message: {
'type': archive.type,
'compressed size': archive.size.compressed,
'uncompressed size': archive.size.uncompressed,
'files amount': archive.files.length
}
});
resolve(archive);
break;
case 'zip':
const zipOutput = await Neutralino.os.execCommand(`unzip -v "${path}"`);
for (const match of zipOutput.stdOut.matchAll(/^(\d+) [a-zA-Z\:]+[ ]+(\d+)[ ]+[0-9\-]+% [0-9\-]+ [0-9\:]+ [a-f0-9]{8} (.+)/gm))
{
let uncompressedSize = parseInt(match[1]),
compressedSize = parseInt(match[2]);
archive.size.compressed! += compressedSize;
archive.size.uncompressed! += uncompressedSize;
archive.files.push({
path: match[3],
size: {
compressed: compressedSize,
uncompressed: uncompressedSize
}
});
}
debugThread.log({
message: {
'type': archive.type,
'compressed size': archive.size.compressed,
'uncompressed size': archive.size.uncompressed,
'files amount': archive.files.length
}
});
resolve(archive);
break;
default:
debugThread.log(`Unsupported archive type: ${archive.type}`);
resolve(null);
break;
}
});
}
/**
* Extract Archive
*
* @param path path to archive
* @param unpackDir directory to extract the files to
*/
public static unpack(path: string, unpackDir: string|null = null): Promise<Stream>
{
return new Promise((resolve) => {
const stream = new Stream(path, unpackDir);
this.streams.push(stream);
resolve(stream);
});
}
/**
* Close every open archive extracting stream
*/
public static closeStreams(forced: boolean = false)
{
this.streams.forEach((stream) => {
stream.close(forced);
});
}
};
export { Stream };
export type {
ArchiveType,
File,
Size,
ArchiveInfo
};

View file

@ -1,128 +0,0 @@
import constants from '../Constants';
import Debug from './Debug';
type Record = {
expired: boolean;
value: object|object[];
};
declare const Neutralino;
export default class Cache
{
// Locally stored cache to not to access
// cache.json file every time we want to find something
protected static cache: object|null = null;
/**
* Get cached value
*
* @returns null if this value is not cached
*/
public static get(name: string): Promise<Record|null>
{
return new Promise(async (resolve) => {
if (this.cache !== null && this.cache[name] !== undefined)
{
const expired = this.cache[name].ttl !== null ? Date.now() > this.cache[name].ttl * 1000 : false;
Debug.log({
function: 'Cache.get',
message: [
`Resolved ${expired ? 'expired' : 'unexpired'} hot cache record`,
`[name] ${name}`,
`[value]: ${JSON.stringify(this.cache[name].value)}`
]
});
resolve({
expired: expired,
value: this.cache[name].value
});
}
else Neutralino.filesystem.readFile(await constants.paths.cache)
.then((cache) => {
this.cache = JSON.parse(cache);
if (this.cache![name] === undefined)
resolve(null);
else
{
const output = {
expired: this.cache![name].ttl !== null ? Date.now() > this.cache![name].ttl * 1000 : false,
value: this.cache![name].value
};
Debug.log({
function: 'Cache.get',
message: [
`Resolved ${output.expired ? 'expired' : 'unexpired'} cache`,
`[name] ${name}`,
`[value]: ${JSON.stringify(output.value)}`
]
});
resolve(output);
}
})
.catch(() => resolve(null));
});
}
/**
* Cache value
*
* @param name name of the value to cache
* @param value value to cache
* @param ttl number of seconds to cache
*
* @returns promise that indicates when the value will be cached
*/
public static set(name: string, value: object|object[], ttl: number|null = null): Promise<void>
{
return new Promise((resolve) => {
constants.paths.cache.then((cacheFile) => {
const writeCache = () => {
Debug.log({
function: 'Cache.set',
message: [
'Caching data:',
`[ttl] ${ttl}`,
`[value] ${JSON.stringify(value)}`
]
});
this.cache![name] = {
ttl: ttl !== null ? Math.round(Date.now() / 1000) + ttl : null,
value: value
};
Neutralino.filesystem.writeFile(cacheFile, JSON.stringify(this.cache))
.then(() => resolve());
};
if (this.cache === null)
{
Neutralino.filesystem.readFile(cacheFile)
.then((cacheRaw) =>
{
this.cache = JSON.parse(cacheRaw);
writeCache();
})
.catch(() => {
this.cache = {};
writeCache();
});
}
else writeCache();
});
});
}
};
export type { Record };

View file

@ -1,12 +1,11 @@
import type { DXVK as TDXVK } from '../types/DXVK';
import { Configs, Process, promisify, path } from '../../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import constants from '../Constants';
import Configs from '../Configs';
import AbstractInstaller from './AbstractInstaller';
import Process from '../neutralino/Process';
import promisify from './promisify';
import Runners from './Runners';
import { DebugThread } from './Debug';
declare const Neutralino;
@ -143,7 +142,7 @@ export default class DXVK
const version = typeof dxvk !== 'string' ?
dxvk.version : dxvk;
await Neutralino.os.execCommand(`rm -rf "${Process.addSlashes(await constants.paths.dxvksDir)}/dxvk-${version}"`);
await Neutralino.os.execCommand(`rm -rf "${path.addSlashes(await constants.paths.dxvksDir)}/dxvk-${version}"`);
debugThread.log('Deletion completed');

View file

@ -1,133 +0,0 @@
import type { DebugOptions, LogRecord } from '../types/Debug';
class DebugThread
{
protected thread: number;
protected funcName: string|null;
public constructor(funcName: string|null = null, options: DebugOptions|string|null = null)
{
// Generate some random thread id
this.thread = 1000 + Math.round(Math.random() * 8999);
this.funcName = funcName;
if (options !== null)
this.log(options);
}
public log(options: DebugOptions|string)
{
Debug.log({
thread: this.thread,
function: this.funcName ?? '',
...(typeof options === 'string' ? { message: options } : options)
});
}
}
class Debug
{
public static readonly startedAt = new Date;
protected static logOutput: LogRecord[] = [];
protected static onLogHandler?: (record: LogRecord) => void;
protected static formatTime(time: number): string
{
const prefixTime = (time: number): string => {
return time < 10 ? `0${time}` : time.toString();
};
const date = new Date(time);
return `${prefixTime(date.getHours())}:${prefixTime(date.getMinutes())}:${prefixTime(date.getSeconds())}.${date.getMilliseconds()}`;
}
public static log(options: DebugOptions|string)
{
const time = Date.now();
let output: LogRecord = {
time: time,
log: [
`[${this.formatTime(time)}]`
]
};
if (typeof options === 'string')
output.log[0] += ` ${options}`;
else
{
// Add thread id
if (options.thread)
output.log[0] += `[thread: ${options.thread}]`;
// Add function name
if (options.function)
output.log[0] += `[${options.function}]`;
// Add log message if it is a single line
if (typeof options.message === 'string')
output.log[0] += ` ${options.message}`;
// message: [a, b, c, d]
else if (Array.isArray(options.message))
options.message.forEach((line) => {
if (line !== '')
output.log.push(` - ${line}`);
});
// message: { a: b, c: d }
else Object.keys(options.message).forEach((key) => {
output.log.push(` - [${key}] ${options.message[key]}`);
});
}
console.log(output.log.join('\r\n'));
this.logOutput.push(output);
if (this.onLogHandler)
this.onLogHandler(output);
}
public static merge(records: LogRecord[])
{
this.logOutput.unshift(...records);
this.logOutput.sort((a, b) => a.time - b.time);
}
public static getRecords(): LogRecord[]
{
return this.logOutput;
}
public static get(): string[]
{
let output: string[] = [];
this.logOutput.forEach((record) => {
record.log.forEach((line) => output.push(line));
});
return output;
}
public static handler(handler: (record: LogRecord) => void)
{
this.onLogHandler = handler;
}
}
export default Debug;
export { DebugThread };
export type {
DebugOptions,
LogRecord
};

View file

@ -1,6 +1,6 @@
import type { Params } from '../types/DiscordRPC';
import Process from '../neutralino/Process';
import { Process, path } from '../../empathize';
declare const NL_CWD;
@ -19,10 +19,10 @@ export default class DiscordRPC
];
if (params.details)
exec = [...exec, `-d "${Process.addSlashes(params.details)}"`];
exec = [...exec, `-d "${path.addSlashes(params.details)}"`];
if (params.state)
exec = [...exec, `-s "${Process.addSlashes(params.state)}"`];
exec = [...exec, `-s "${path.addSlashes(params.state)}"`];
if (params.icon)
{

View file

@ -1,48 +0,0 @@
import type { DomainInfo } from '../types/Domain';
import Process from '../neutralino/Process';
import { DebugThread } from './Debug';
declare const Neutralino;
export default class Domain
{
public static getInfo(uri: string): Promise<DomainInfo>
{
const debugThread = new DebugThread('Domain.getInfo', `Getting info about uri: ${uri}`);
return new Promise(async (resolve) => {
const process = await Neutralino.os.execCommand(`ping -n -4 -w 1 -B "${Process.addSlashes(uri)}"`);
const output = process.stdOut || process.stdErr;
const resolveInfo = (info: DomainInfo) => {
debugThread.log({ message: info });
resolve(info);
};
if (output.includes('Name or service not known'))
{
resolveInfo({
uri: uri,
available: false
});
}
else
{
const regex = /PING (.*) \(([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\) .* ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) : [\d]+\([\d]+\)/gm.exec(output);
if (regex !== null)
{
resolveInfo({
uri: regex[1],
remoteIp: regex[2],
localIp: regex[3],
available: regex[2] !== regex[3]
});
}
}
});
}
};

View file

@ -1,230 +0,0 @@
import Process from '../neutralino/Process';
import { DebugThread } from './Debug';
import fetch from './Fetch';
declare const Neutralino;
class Stream
{
protected _id: number = -1;
/**
* ID of the curl process
*/
public get id(): number
{
return this._id;
}
/**
* The interval in ms between progress event calls
*/
public progressInterval: number = 200;
/**
* The interval in ms between checking was downloading resumed after pausing
*/
public pauseInterval: number = 500;
protected uri: string;
protected output: string;
protected total: number;
protected previous: number = 0;
protected onStart?: () => void;
protected onProgress?: (current: number, total: number, difference: number) => void;
protected onFinish?: () => void;
protected started: boolean = false;
protected paused: boolean = true; // true because we call .resume() method at start
protected finished: boolean = false;
protected debugThread: DebugThread;
public constructor(uri: string, output: string, total: number)
{
this.uri = uri;
this.output = output;
this.total = total;
this.started = true;
this.debugThread = new DebugThread('Downloader/Stream', {
message: {
'uri': uri,
'output file': output,
'total size': total
}
});
if (this.onStart)
this.onStart();
this.resume();
const updateProgress = () => {
if (!this.paused)
{
Neutralino.filesystem.getStats(output).then((stats) => {
if (this.onProgress)
this.onProgress(stats.size, this.total, stats.size - this.previous);
this.previous = stats.size;
if (stats.size >= this.total)
{
this.finished = true;
this.debugThread.log('Downloading finished');
if (this.onFinish)
this.onFinish();
}
if (!this.finished)
setTimeout(updateProgress, this.progressInterval);
}).catch(() => {
if (!this.finished)
setTimeout(updateProgress, this.progressInterval);
});
}
else setTimeout(updateProgress, this.pauseInterval);
};
setTimeout(updateProgress, this.progressInterval);
}
/**
* Specify event that will be called when the download gets started
*
* @param callback
*/
public start(callback: () => void)
{
this.onStart = callback;
if (this.started)
callback();
}
/**
* Specify event that will be called every [this.progressInterval] ms while the file is downloading
*
* @param callback
*/
public progress(callback: (current: number, total: number, difference: number) => void)
{
this.onProgress = callback;
}
/**
* Specify event that will be called after the file is downloaded
*
* @param callback
*/
public finish(callback: () => void)
{
this.onFinish = callback;
if (this.finished)
callback();
}
/**
* Pause downloading
*/
public pause()
{
if (!this.paused)
{
this.debugThread.log('Downloading paused');
this.close(true);
this.paused = true;
}
}
/**
* Resume downloading
*/
public resume()
{
if (this.paused)
{
const command = `curl -s -L -N -C - -o "${Process.addSlashes(this.output)}" "${this.uri}"`;
this.debugThread.log(`Downloading started with command: ${command}`);
Neutralino.os.execCommand(command, {
background: true
}).then((result) => {
this._id = result.pid;
});
this.paused = false;
}
}
/**
* Close downloading stream
*/
public close(forced: boolean = false)
{
Neutralino.os.execCommand(`kill ${forced ? '-9' : '-15'} ${this._id}`);
}
}
export default class Downloader
{
protected static streams: Stream[] = [];
/**
* Download file
*
* @param uri file's uri to download
* @param output relative or absolute path to the file to save it as
*
* @returns downloading stream
*/
public static async download(uri: string, output: string|null = null): Promise<Stream>
{
return new Promise(async (resolve) => {
fetch(uri).then((response) => {
const stream = new Stream(uri, output ?? this.fileFromUri(uri), response.length!);
this.streams.push(stream);
resolve(stream);
});
});
}
/**
* Close every open downloading stream
*/
public static closeStreams(forced: boolean = false)
{
this.streams.forEach((stream) => {
stream.close(forced);
});
}
/**
* Get a file name from the URI
*/
public static fileFromUri(uri: string): string
{
const file = uri.split('/').pop()!.split('#')[0].split('?')[0];
if (file === '')
return 'index.html';
else if (`https://${file}` != uri && `http://${file}` != uri)
return file;
else return 'index.html';
}
};
export { Stream };

View file

@ -1,79 +0,0 @@
declare const Neutralino;
class Response
{
/**
* Requested url
*/
public readonly url: string;
/**
* HTTP status code
*/
public readonly status: number|null;
/**
* Content length
*/
public readonly length: number|null;
/**
* Represents whether the response was successful (status in the range 200-299) or not
*/
public readonly ok: boolean;
public constructor(url: string, status: number|null, length: number|null)
{
this.url = url;
this.status = status;
this.length = length;
// https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
this.ok = status! >= 200 && status! <= 299;
}
/**
* Get request's body
*
* @param delay maximal request delay in milliseconds
*/
public body(delay: number|null = null): Promise<string>
{
return new Promise((resolve) => {
Neutralino.os.execCommand(`curl -s -L ${delay !== null ? `-m ${delay / 1000}` : ''} "${this.url}"`)
.then((output) => resolve(output.stdOut));
});
}
}
/**
* Fetch data from the URL
*
* @param delay maximal request delay in milliseconds
*/
export default function fetch(url: string, delay: number|null = null): Promise<Response>
{
return new Promise(async (resolve) => {
let header = await Neutralino.os.execCommand(`curl -s -I -L ${delay !== null ? `-m ${delay / 1000}` : ''} "${url}"`);
if (header.stdOut == '')
header = header.stdErr;
else header = header.stdOut;
header = header.split(/^HTTP\/[\d]+ /mi).pop();
let status: any = /^([\d]+)[\s]+$/m.exec(header);
let length: any = /^content-length: ([\d]+)/mi.exec(header);
if (status !== null)
status = parseInt(status[1]);
if (length !== null)
length = parseInt(length[1]);
resolve(new Response(url, status, length));
});
};
export { Response };

View file

@ -1,4 +1,4 @@
import Process from '../neutralino/Process';
import { path } from '../../empathize';
type Tag = {
tag: string,
@ -17,7 +17,7 @@ export default class Git
public static getTags(repository: string): Promise<Tag[]>
{
return new Promise(async (resolve) => {
const output = await Neutralino.os.execCommand(`git ls-remote --tags "${Process.addSlashes(repository)}"`);
const output = await Neutralino.os.execCommand(`git ls-remote --tags "${path.addSlashes(repository)}"`);
let tags: Tag[] = [];

View file

@ -1,101 +0,0 @@
import constants from '../Constants';
declare const Neutralino;
class IPCRecord
{
public readonly id: number;
public readonly time: number;
public readonly data: any;
public constructor(id: number, time: number, data: any)
{
this.id = id;
this.time = time;
this.data = data;
}
/**
* Remove the record from the storage
*/
public pop(): IPCRecord
{
IPC.remove(this);
return this;
}
public get(): { id: number; time: number; data: any}
{
return {
id: this.id,
time: this.time,
data: this.data
};
}
}
export default class IPC
{
/**
* Read records from the "shared inter-process storage"
*/
public static read(): Promise<IPCRecord[]>
{
return new Promise(async (resolve) => {
Neutralino.filesystem.readFile(`${await constants.paths.launcherDir}/.ipc.json`)
.then((data) => resolve(JSON.parse(data).map((record) => new IPCRecord(record.id, record.time, record.data))))
.catch(() => resolve([]));
});
}
/**
* Write some data to the "shared inter-process storage"
*/
public static write(data: any): Promise<void>
{
return new Promise(async (resolve) => {
const records = await this.read();
records.push({
id: Math.round(Math.random() * 100000),
time: Date.now(),
data: data
} as IPCRecord);
await Neutralino.filesystem.writeFile(`${await constants.paths.launcherDir}/.ipc.json`, JSON.stringify(records));
resolve();
});
}
/**
* Remove record from the "shared inter-process storage"
*/
public static remove(record: IPCRecord): Promise<void>
{
return new Promise(async (resolve) => {
let records = await this.read();
records = records.filter((item) => item.id !== record.id || item.time !== record.time);
await Neutralino.filesystem.writeFile(`${await constants.paths.launcherDir}/.ipc.json`, JSON.stringify(records));
resolve();
});
}
/**
* Remove all the record from the "shared inter-process storage"
*/
public static purge(): Promise<void>
{
return new Promise(async (resolve) => {
Neutralino.filesystem.removeFile(`${await constants.paths.launcherDir}/.ipc.json`)
.then(() => resolve())
.catch(() => resolve());
});
}
};
export { IPCRecord };

View file

@ -1,32 +0,0 @@
import type { NotificationsOptions } from '../types/Notifications';
import Process from '../neutralino/Process';
declare const Neutralino;
export default class Notifications
{
/**
* Show notification
*/
public static show(options: NotificationsOptions)
{
let command = `notify-send "${Process.addSlashes(options.title)}" "${Process.addSlashes(options.body)}"`;
// Specify notification icon
if (options.icon)
command += ` -i "${Process.addSlashes(options.icon)}"`;
// Specify notification duration
if (options.duration)
command += ` -d ${options.duration}`;
// Specify notification importance
if (options.importance)
command += ` -u ${options.importance}`;
Neutralino.os.execCommand(command, {
background: true
});
}
};

View file

@ -1,7 +1,7 @@
import { Process, Downloader, Debug, path } from '../../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import constants from '../Constants';
import Process from '../neutralino/Process';
import Debug, { DebugThread } from './Debug';
import Downloader from './Downloader';
import Runners from './Runners';
declare const Neutralino;
@ -52,7 +52,7 @@ export default class Prefix
.catch(() => {
Downloader.download(constants.uri.winetricks, winetricksPath).then((stream) => {
stream.finish(async () => {
await Neutralino.os.execCommand(`chmod +x "${Process.addSlashes(winetricksPath)}"`);
await Neutralino.os.execCommand(`chmod +x "${path.addSlashes(winetricksPath)}"`);
resolve(winetricksPath);
});
@ -64,12 +64,12 @@ export default class Prefix
/**
* Create wine prefix using the current selected wine
*
* @param path folder to create prefix in
* @param folder 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>
public static create(folder: string, progress?: (output: string, current: number, total: number) => void): Promise<boolean>
{
const debugThread = new DebugThread('Prefix.create', 'Creating wine prefix');
@ -108,11 +108,11 @@ export default class Prefix
this.getWinetricks().then(async (winetricks) => {
let installationProgress = 0;
const process = await Process.run(`"${Process.addSlashes(winetricks)}" corefonts usetakefocus=n`, {
const process = await Process.run(`"${path.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
WINEPREFIX: folder
}
});

View file

@ -3,11 +3,11 @@ import type {
RunnerFamily
} from '../types/Runners';
import { Configs, Process, path } from '../../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import constants from '../Constants';
import Configs from '../Configs';
import AbstractInstaller from './AbstractInstaller';
import Process from '../neutralino/Process';
import { DebugThread } from './Debug';
declare const Neutralino;
@ -149,7 +149,7 @@ class Runners
const name = typeof runner !== 'string' ?
runner.name : runner;
Process.run(`rm -rf "${Process.addSlashes(await constants.paths.runnersDir + '/' + name)}"`)
Process.run(`rm -rf "${path.addSlashes(await constants.paths.runnersDir + '/' + name)}"`)
.then((process) => {
process.finish(() => {
debugThread.log('Runner deleted');

View file

@ -1,83 +0,0 @@
type callback = () => any;
type PromiseOptions = {
callbacks: callback[]|Promise<any>[];
/**
* If true, then all the callbacks will be called
* at the same time and promisify will be resolved
* when all of them have finished
*
* Otherwise, callbacks will be called one after the other
* and promisify will be resolved with the last one
*/
callAtOnce?: boolean;
/**
* [callAtOnce: true] updates interval in ms
*
* @default 100
*/
interval?: number;
};
/**
* Make a promise from the provided function(s) and run it(them)
*/
export default function promisify(callback: callback|Promise<any>|PromiseOptions): Promise<any>
{
return new Promise(async (resolve) => {
// promisify(() => { ... })
if (typeof callback === 'function')
resolve(await Promise.resolve(callback()));
// promisify(new Promise(...))
else if (typeof callback['then'] === 'function')
resolve(await callback);
// promisify({ callbacks: [ ... ] })
else
{
let outputs = {};
// @ts-expect-error
if (callback.callAtOnce)
{
// @ts-expect-error
let remained = callback.callbacks.length;
// @ts-expect-error
for (let i = 0; i < callback.callbacks.length; ++i) // @ts-expect-error
promisify(callback.callbacks[i]).then((output) => {
outputs[i] = output;
--remained;
});
const updater = () => {
if (remained > 0) // @ts-expect-error
setTimeout(updater, callback.interval ?? 100);
else resolve(outputs);
};
// @ts-expect-error
setTimeout(updater, callback.interval ?? 100);
}
else
{
// @ts-expect-error
for (let i = 0; i < callback.callbacks.length; ++i) // @ts-expect-error
outputs[i] = await promisify(callback.callbacks[i]);
resolve(outputs);
}
}
});
};
export type {
PromiseOptions,
callback
};

View file

@ -1,5 +1,6 @@
import { fetch } from '../../empathize';
import constants from '../Constants';
import fetch from '../core/Fetch';
import Locales from './Locales';
export default class Background

View file

@ -2,9 +2,9 @@ import { dictionary, locale } from 'svelte-i18n';
import YAML from 'yaml';
import { promisify, Configs } from '../../empathize';
import constants from '../Constants';
import promisify from '../core/promisify';
import Configs from '../Configs';
type AvailableLocales =
| 'en-us'

View file

@ -2,9 +2,9 @@ import YAML from 'yaml';
import type { Shader } from '../types/Shaders';
import Configs from '../Configs';
import { Configs, promisify } from '../../empathize';
import constants from '../Constants';
import promisify from '../core/promisify';
declare const Neutralino;

View file

@ -3,22 +3,20 @@ import { dictionary, locale } from 'svelte-i18n';
import semver from 'semver';
import type { LauncherState } from '../types/Launcher';
import { Windows, Debug, IPC, Notification } from '../../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import Window from '../neutralino/Window';
import type { LauncherState } from '../types/Launcher';
import Launcher from '../Launcher';
import Game from '../Game';
import Patch from '../Patch';
import Voice from '../Voice';
import Runners from '../core/Runners';
import Debug, { DebugThread } from '../core/Debug';
import DXVK from '../core/DXVK';
import IPC from '../core/IPC';
import Locales from './Locales';
import Git from '../core/Git';
import constants from '../Constants';
import Notifications from '../core/Notifications';
export default class State
{
@ -97,8 +95,8 @@ export default class State
this.update().then(async () => {
IPC.write('launcher-loaded');
await Window.current.show();
await Window.current.center(1280, 700);
await Windows.current.show();
// FIXME: await Windows.current.center(1280, 700);
// Check for new versions of the launcher
Git.getTags(constants.uri.launcher).then((tags) => {
@ -110,7 +108,7 @@ export default class State
const locales = (currentDictionary[currentLocale ?? 'en-us'] ?? currentDictionary['en-us'])['launcher']!['update'] as object;
Notifications.show({
Notification.show({
title: locales['title'].replace('{from}', Launcher.version).replace('{to}', tag.tag),
body: locales['body'].replace('{repository}', constants.uri.launcher),
icon: `${constants.paths.appDir}/public/images/baal64-transparent.png`
@ -128,7 +126,7 @@ export default class State
else
{
Window.current.setSize({
Windows.current.setSize({
width: 1280 + (1280 - window.innerWidth),
height: 700 + (700 - window.innerHeight),
resizable: false
@ -400,7 +398,7 @@ export default class State
{
state = 'game-launch-available';
Notifications.show({
Notification.show({
title: 'An Anime Game Launcher',
body: 'All the patch repositories are not available. You\'ll be able to run the game, but launcher can\'t be sure is it patched properly',
icon: `${constants.paths.appDir}/public/images/baal64-transparent.png`,

View file

@ -1,6 +1,7 @@
import { Notification } from '../../../empathize';
import Launcher from '../../Launcher';
import Patch from '../../Patch';
import Notifications from '../../core/Notifications';
import constants from '../../Constants';
export default (launcher: Launcher): Promise<void> => {
@ -8,7 +9,7 @@ export default (launcher: Launcher): Promise<void> => {
// Show an error notification if xdelta3 package is not installed
if (!await Launcher.isPackageAvailable('xdelta3'))
{
Notifications.show({
Notification.show({
title: 'An Anime Game Launcher',
body: 'You must download xdelta3 package to apply the patch',
icon: `${constants.paths.appDir}/public/images/baal64-transparent.png`,
@ -76,7 +77,7 @@ export default (launcher: Launcher): Promise<void> => {
// If for some reasons patch wasn't applied successfully
if (!result)
{
Notifications.show({
Notification.show({
title: 'An Anime Game Launcher',
body: 'Patch wasn\'t applied successfully. Please, check your log file to find a reason of it, or ask someone in our discord server',
icon: `${constants.paths.appDir}/public/images/baal64-transparent.png`
@ -89,7 +90,7 @@ export default (launcher: Launcher): Promise<void> => {
});
}
}).catch(() => {
Notifications.show({
Notification.show({
title: 'An Anime Game Launcher',
body: 'All the patch repositories are not available. You\'ll be able to run the game, but launcher can\'t be sure is it patched properly',
icon: `${constants.paths.appDir}/public/images/baal64-transparent.png`,

View file

@ -3,10 +3,11 @@ import { _ } from 'svelte-i18n';
import type Launcher from '../../Launcher';
import { Debug } from '../../../empathize';
import Game from '../../Game';
import Prefix from '../../core/Prefix';
import constants from '../../Constants';
import Debug from '../../core/Debug';
declare const Neutralino;

View file

@ -4,8 +4,9 @@ import { _ } from 'svelte-i18n';
import type Launcher from '../../Launcher';
import type { VoiceLang } from '../../types/Voice';
import { promisify } from '../../../empathize';
import Voice from '../../Voice';
import promisify from '../../core/promisify';
import Game from '../../Game';
export default (launcher: Launcher): Promise<void> => {

View file

@ -1,11 +1,8 @@
import Process from '../../neutralino/Process';
import Window from '../../neutralino/Window';
import { Process, Windows, Configs, Notification, path } from '../../../empathize';
import { DebugThread } from '@empathize/framework/dist/meta/Debug';
import Launcher from '../../Launcher';
import Configs from '../../Configs';
import constants from '../../Constants';
import { DebugThread } from '../../core/Debug';
import Notifications from '../../core/Notifications';
import Runners from '../../core/Runners';
import Game from '../../Game';
@ -20,7 +17,7 @@ export default (launcher: Launcher): Promise<void> => {
// If telemetry servers are not disabled
if (!telemetry)
{
Notifications.show({
Notification.show({
title: 'An Anime Game Launcher',
body: 'Telemetry servers are not disabled',
icon: `${constants.paths.appDir}/public/images/baal64-transparent.png`,
@ -33,7 +30,7 @@ export default (launcher: Launcher): Promise<void> => {
// Otherwise run the game
else
{
Window.current.hide();
Windows.current.hide();
launcher.updateDiscordRPC('in-game');
@ -139,7 +136,7 @@ export default (launcher: Launcher): Promise<void> => {
else console.warn(`GPU ${LauncherLib.getConfig('gpu')} not found. Launching on the default GPU`);
}*/
let command = `"${Process.addSlashes(wineExeutable)}" ${await Configs.get('fps_unlocker') ? 'unlockfps.bat' : 'launcher.bat'}`;
let command = `"${path.addSlashes(wineExeutable)}" ${await Configs.get('fps_unlocker') ? 'unlockfps.bat' : 'launcher.bat'}`;
/**
* Gamemode integration
@ -208,8 +205,8 @@ export default (launcher: Launcher): Promise<void> => {
{
const stopTime = Date.now();
Window.current.show();
Window.current.center(1280, 700);
Windows.current.show();
// FIXME: Windows.current.center(1280, 700);
launcher.updateDiscordRPC('in-launcher');
launcher.tray.hide();
@ -218,7 +215,7 @@ export default (launcher: Launcher): Promise<void> => {
Configs.get('purge_logs.game').then(async (purge_logs) => {
if (purge_logs)
{
const gameDir = Process.addSlashes(await constants.paths.gameDir);
const gameDir = path.addSlashes(await constants.paths.gameDir);
// Delete .log files (e.g. "ZFGameBrowser_xxxx.log")
Neutralino.os.execCommand(`find "${gameDir}" -maxdepth 1 -type f -name "*.log" -delete`);

View file

@ -1,8 +1,9 @@
import type Launcher from '../../Launcher';
import type { VoiceLang } from '../../types/Voice';
import { promisify } from '../../../empathize';
import Voice from '../../Voice';
import promisify from '../../core/promisify';
export default (launcher: Launcher): Promise<void> => {
return new Promise(async (resolve) => {

View file

@ -1,267 +0,0 @@
import constants from '../Constants';
import Debug, { DebugThread } from "../core/Debug";
declare const Neutralino;
declare const NL_CWD;
type ProcessOptions = {
/**
* Environment variables
*/
env?: object;
/**
* Current working directory for the running process
*/
cwd?: string;
/**
* Interval between tries to find started process id
*
* @default 50
*/
childInterval?: number;
};
class Process
{
/**
* Process ID
*/
public readonly id: number;
/**
* Interval in ms between process status update
*
* null if you don't want to update process status
*
* @default 200
*/
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;
/**
* Whether the process was finished
*/
public get finished(): boolean
{
return this._finished;
};
protected onOutput?: (output: string, process: Process) => void;
protected onFinish?: (process: Process) => void;
public constructor(pid: number, outputFile: string|null = null)
{
const debugThread = new DebugThread('Process/Stream', `Opened process ${pid} stream`);
this.id = pid;
this.outputFile = outputFile;
const updateStatus = () => {
this.running().then((running) => {
// The process is still running
if (running)
{
if (this.runningInterval)
setTimeout(updateStatus, this.runningInterval);
}
// Otherwise the process was stopped
else
{
this._finished = true;
debugThread.log('Process stopped');
if (this.onFinish)
this.onFinish(this);
}
});
};
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)
{
if (output !== '')
{
debugThread.log({
message: [
'Process output:',
...output.split(/\r\n|\r|\n/)
]
});
}
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);
}
}
/**
* Specify callback to run when the process will be finished
*/
public finish(callback: (process: Process) => void)
{
this.onFinish = callback;
if (this._finished)
callback(this);
// If user stopped process status auto-checking
// then we should check it manually when this method was called
else if (this.runningInterval === null)
{
this.running().then((running) => {
if (!running)
{
this._finished = true;
callback(this);
}
});
}
}
public output(callback: (output: string, process: Process) => void)
{
this.onOutput = callback;
}
/**
* Kill process
*/
public kill(forced: boolean = false): Promise<void>
{
Neutralino.filesystem.removeFile(this.outputFile);
return Process.kill(this.id, forced);
}
/**
* Returns whether the process is running
*
* This method doesn't call onFinish event
*/
public running(): Promise<boolean>
{
return new Promise((resolve) => {
Neutralino.os.execCommand(`ps -p ${this.id} -S`).then((output) => {
resolve(output.stdOut.includes(this.id) && !output.stdOut.includes('Z '));
});
});
}
/**
* Run shell command
*/
public static run(command: string, options: ProcessOptions = {}): Promise<Process>
{
return new Promise(async (resolve) => {
const tmpFile = `${await constants.paths.tempDir}/${10000 + Math.round(Math.random() * 89999)}.tmp`;
// Set env variables
if (options.env)
for (const key of Object.keys(options.env))
command = `${key}="${this.addSlashes(options.env![key].toString())}" ${command}`;
// 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}`;
// And run the command
const process = await Neutralino.os.execCommand(command, {
background: true
});
const childFinder = async () => {
const childProcess = await Neutralino.os.execCommand(`pgrep -P ${process.pid}`);
// Child wasn't found
if (childProcess.stdOut == '')
setTimeout(childFinder, options.childInterval ?? 50);
// Otherwise return its id
else
{
const processId = parseInt(childProcess.stdOut.substring(0, childProcess.stdOut.length - 1));
Debug.log({
function: 'Process.run',
message: {
'running command': command,
'cwd': options.cwd,
'initial process id': process.pid,
'real process id': processId,
...options.env
}
});
resolve(new Process(processId, tmpFile));
}
};
setTimeout(childFinder, options.childInterval ?? 50);
});
}
public static kill(id: number, forced: boolean = false): Promise<void>
{
return new Promise((resolve) => {
Neutralino.os.execCommand(`kill ${forced ? '-9' : '-15'} ${id}`).then(() => resolve());
});
}
/**
* Replace '\a\b' to '\\a\\b'
* And replace ''' to '\''
*/
public static addSlashes(str: string): string
{
return str.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
}
}
export type { ProcessOptions };
export default Process;

View file

@ -1,120 +0,0 @@
declare const Neutralino;
type Item = {
/**
* Item text
*/
text: string;
/**
* Item id
*/
id?: string;
/**
* Whether the item disabled or not
*
* If yes, then it will be a string
*/
disabled?: boolean;
/**
* Is this item a checkbox or not
*/
checked?: boolean;
/**
* Event on click
*
* If specified, then will generate random
* item id if it is not specified
*/
click?: (item: Item) => void;
};
Neutralino.events.on('trayMenuItemClicked', (item) => {
for (const tray of Tray.trays)
for (const trayItem of tray.items)
if (trayItem.id === item.detail.id)
{
if (trayItem.click)
{
trayItem.click({
id: item.detail.id,
text: item.detail.text,
disabled: item.detail['isDisabled'],
checked: item.detail['isChecked'],
click: trayItem.click
});
}
return;
}
});
export default class Tray
{
public static trays: Tray[] = [];
public icon: string;
protected _items: Item[] = [];
public get items(): Item[]
{
return this._items.map((item) => {
return {
id: item.id,
text: item.text,
disabled: item['isDisabled'],
checked: item['isChecked'],
click: item.click
};
});
}
public set items(items: Item[])
{
this._items = items.map((item) => {
if (item.id === undefined && item.click !== undefined)
item.id = 'click:' + Math.random().toString().substring(2);
return {
id: item.id,
text: item.text,
isDisabled: item.disabled,
isChecked: item.checked,
click: item.click
};
});
}
public constructor(icon: string, items: Item[] = [])
{
this.icon = icon;
this.items = items;
Tray.trays.push(this);
}
public update(items: Item[]|null = null): Promise<void>
{
if (items !== null)
this.items = items;
return Neutralino.os.setTray({
icon: this.icon,
menuItems: this._items
});
}
public hide(): Promise<void>
{
return Neutralino.os.setTray({
icon: this.icon,
menuItems: []
});
}
};
export type { Item };

View file

@ -1,82 +0,0 @@
type WindowSize = {
width?: number;
height?: number;
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
resizable?: boolean;
};
type WindowOptions = WindowSize & {
title?: string;
icon?: string;
fullScreen?: boolean;
alwaysOnTop?: boolean;
enableInspector?: boolean;
borderless?: boolean;
maximize?: boolean;
hidden?: boolean;
maximizable?: boolean;
exitProcessOnClose?: boolean;
processArgs?: string;
};
type WindowOpenResult = {
status: boolean;
data?: {
pid: number;
stdOut: string;
stdErr: string;
exitCode: number;
};
};
declare const Neutralino;
class Window
{
public static get current(): any
{
return {
...Neutralino.window,
center(windowWidth: number, windowHeight: number)
{
Neutralino.window.move(Math.round((window.screen.width - windowWidth) / 2), Math.round((window.screen.height - windowHeight) / 2));
}
};
}
public static open(name: string, options: WindowOptions = {}): Promise<WindowOpenResult>
{
return new Promise(async (resolve) => {
const status = await Neutralino.window.create(`/${name}.html`, {
width: 600,
height: 400,
enableInspector: false,
exitProcessOnClose: true,
...options,
// So basically you should display the window manually
// with Window.current.show() when your app will load
// all its content there
hidden: true
});
resolve({
status: status !== undefined,
data: status
});
});
}
}
export type {
WindowSize,
WindowOptions,
WindowOpenResult
};
export default Window;

View file

@ -1,27 +0,0 @@
type ArchiveType =
| 'tar'
| 'zip'
| null;
type Size = {
compressed?: number | null;
uncompressed?: number | null;
};
type File = {
path: string;
size: Size;
};
type ArchiveInfo = {
size: Size;
type: ArchiveType;
files: File[];
};
export type {
ArchiveType,
Size,
File,
ArchiveInfo
};

View file

@ -1,23 +0,0 @@
type DebugOptions = {
/**
* Some random-generated thread id
*/
thread?: number;
/**
* Some function name
*/
function?: string;
/**
* Some log message
*/
message: string|string[]|object;
};
type LogRecord = {
time: number;
log: string[];
};
export type { DebugOptions, LogRecord };

View file

@ -1,8 +0,0 @@
type DomainInfo = {
uri: string;
remoteIp?: string;
localIp?: string;
available: boolean;
};
export type { DomainInfo };

View file

@ -1,24 +0,0 @@
type NotificationsOptions = {
title: string;
body: string;
/**
* Icon name or path
*/
icon?: string;
/**
* Number of seconds this notification
* will be visible
*/
duration?: number;
/**
* Importance of the notification
*
* @default "normal"
*/
importance?: 'low' | 'normal' | 'critical';
};
export type { NotificationsOptions };

View file

@ -2,12 +2,12 @@
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"strict": true,
"noImplicitAny": false,
"removeComments": true,
"resolveJsonModule": true,
"useDefineForClassFields": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.