From d2d690a114d1e5327743125da03677674c248850 Mon Sep 17 00:00:00 2001 From: Observer KRypt0n_ Date: Wed, 29 Dec 2021 18:04:30 +0200 Subject: [PATCH] Added game pre-downloading feature - decreased splash window size - added `Game.getLatestData()` result caching, so basically every `Game` and `Voice` method and field now caches most of the data - Made `Game.predownloadUpdate()` method - Made `Game.isUpdatePredownloaded()` method - Made `Voice.predownloadUpdate()` method - Made `Voice.isUpdatePredownloaded()` method - fixed `Cache.get()` method work - now launcher window hides when you launch the game - added `pre_download_game` API field type definition - added game pre-downloading button --- README.md | 2 +- src/assets/images/cloud-download.png | Bin 0 -> 2522 bytes src/index.svelte | 15 ++-- src/sass/index.sass | 24 +++++- src/sass/splash.sass | 8 +- src/ts/Game.ts | 89 ++++++++++++++++++--- src/ts/Voice.ts | 75 +++++++++++++++-- src/ts/core/Cache.ts | 85 ++++++++++++++++---- src/ts/launcher/State.ts | 43 ++++++++-- src/ts/launcher/states/Launch.ts | 5 ++ src/ts/launcher/states/Predownload.ts | 44 ++++++++++ src/ts/launcher/states/PredownloadVoice.ts | 29 +++++++ src/ts/types/GameData.d.ts | 7 +- src/ts/types/Launcher.ts | 2 + 14 files changed, 376 insertions(+), 52 deletions(-) create mode 100644 src/assets/images/cloud-download.png create mode 100644 src/ts/launcher/states/Predownload.ts create mode 100644 src/ts/launcher/states/PredownloadVoice.ts diff --git a/README.md b/README.md index edebc12..acd76f1 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ This is our current roadmap goals. You can find older ones [here](ROADMAP.md) * Debugger * Splash screen * Theming system -* Game pre-installation +* Game pre-installation * Launcher auto-updates * Statistics window * Chengelog window diff --git a/src/assets/images/cloud-download.png b/src/assets/images/cloud-download.png new file mode 100644 index 0000000000000000000000000000000000000000..4998ce5178dfe27c5ae45adc8a2131ce0ec68932 GIT binary patch literal 2522 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 import { onMount } from 'svelte'; - import Gear from '/src/assets/images/gear.png'; - import GearActive from '/src/assets/images/gear-active.png'; - import Window from './ts/neutralino/Window'; import Launcher from './ts/Launcher'; @@ -15,11 +12,15 @@ import Game from './ts/Game'; import Background from './ts/launcher/Background'; + import Gear from './assets/images/gear.png'; + import GearActive from './assets/images/gear-active.png'; + import Download from './assets/images/cloud-download.png'; + Neutralino.events.on('ready', () => { Window.open('splash', { title: 'Splash', - width: 400, - height: 500, + width: 300, + height: 400, borderless: true, exitProcessOnClose: false }); @@ -82,5 +83,9 @@ Settings + + diff --git a/src/sass/index.sass b/src/sass/index.sass index 7c71eec..d2384ab 100644 --- a/src/sass/index.sass +++ b/src/sass/index.sass @@ -35,16 +35,32 @@ background-color: #7284b6 #launch + position: absolute + width: 238px height: 64px - - font-size: 22px - - position: absolute right: 128px bottom: 64px + font-size: 22px + +#predownload + position: absolute + display: none + + width: 52px + height: 52px + + right: 386px + bottom: 70px + + border-radius: 32px + + img + width: 60% + margin: auto + #settings width: 76px height: 76px diff --git a/src/sass/splash.sass b/src/sass/splash.sass index 63ecb42..ebaedba 100644 --- a/src/sass/splash.sass +++ b/src/sass/splash.sass @@ -7,11 +7,13 @@ background-color: map.get($theme-map, "background") div - width: 60% - margin: 56px auto 0 auto + width: 80% + margin: 42px auto 0 img - width: 100% + width: 80% + display: block + margin: 0 auto 16px auto image-rendering: optimizeQuality h2, p diff --git a/src/ts/Game.ts b/src/ts/Game.ts index d04ab16..04477ae 100644 --- a/src/ts/Game.ts +++ b/src/ts/Game.ts @@ -11,6 +11,8 @@ 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; @@ -67,12 +69,24 @@ export default class Game if (response.ok) { - const json: ServerResponse = JSON.parse(await response.body()); + const cache = await Cache.get('Game.getLatestData.ServerResponse'); - if (json.message == 'OK') - resolve(json.data); + if (cache && !cache.expired) + resolve(cache.value as Data); - else reject(new Error(`${constants.placeholders.uppercase.company}'s versions server responds with an error: [${json.retcode}] ${json.message}`)); + else + { + const json: ServerResponse = JSON.parse(await response.body()); + + if (json.message == 'OK') + { + Cache.set('Game.getLatestData.ServerResponse', json.data, 24 * 3600); + + resolve(json.data); + } + + else reject(new Error(`${constants.placeholders.uppercase.company}'s versions server responds with an error: [${json.retcode}] ${json.message}`)); + } } else reject(new Error(`${constants.placeholders.uppercase.company}'s versions server is unreachable`)); @@ -141,16 +155,19 @@ export default class Game /** * Get the game installation stream * + * @param version current game version to download difference from + * * @returns null if the version can't be found * @returns Error if company's servers are unreachable or they responded with an error */ public static update(version: string|null = null): Promise { - Debug.log( - version !== null ? - `Updating the game from the ${version} version` : - 'Installing the game' - ); + Debug.log({ + function: 'Game.update', + message: version !== null ? + `Updating the game from the ${version} version` : + 'Installing the game' + }); return new Promise((resolve, reject) => { (version === null ? this.latest : this.getDiff(version)) @@ -159,6 +176,60 @@ export default class Game }); } + /** + * Pre-download the game update + * + * @param version current game version to download difference from + * + * @returns null if the game pre-downloading is not available. Otherwise - downloading stream + * @returns Error if company's servers are unreachable or they responded with an error + */ + public static predownloadUpdate(version: string|null = null): Promise + { + const debugThread = new DebugThread('Game.predownloadUpdate', 'Predownloading game data...') + + return new Promise((resolve) => { + this.getLatestData() + .then((data) => { + if (data.pre_download_game) + { + let path = data.pre_download_game.latest.path; + + if (version !== null) + for (const diff of data.pre_download_game.diffs) + if (diff.version == version) + { + path = diff.path; + + break; + } + + debugThread.log(`Downloading update from the path: ${path}`); + + constants.paths.launcherDir.then((dir) => { + Downloader.download(path, `${dir}/game-predownloaded.zip`) + .then((stream) => resolve(stream)); + }); + } + + else resolve(null); + }) + .catch((error) => resolve(error)); + }); + } + + /** + * Checks whether the update was downloaded or not + */ + public static isUpdatePredownloaded(): Promise + { + return new Promise(async (resolve) => { + Neutralino.filesystem.getStats(`${await constants.paths.launcherDir}/game-predownloaded.zip`) + .then(() => resolve(true)) + .catch(() => resolve(false)); + }); + } + /** * Check if the telemetry servers are disabled */ diff --git a/src/ts/Voice.ts b/src/ts/Voice.ts index ae511f5..9f40304 100644 --- a/src/ts/Voice.ts +++ b/src/ts/Voice.ts @@ -5,7 +5,8 @@ import constants from './Constants'; import Game from './Game'; import AbstractInstaller from './core/AbstractInstaller'; import Configs from './Configs'; -import Debug from './core/Debug'; +import Debug, { DebugThread } from './core/Debug'; +import Downloader, { Stream as DownloadingStream } from './core/Downloader'; declare const Neutralino; @@ -118,13 +119,14 @@ export default class Voice * @returns null if the language or the version can't be found * @returns rejects Error object if company's servers are unreachable or they responded with an error */ - public static update(lang: string|null = null, version: string|null = null): Promise + public static update(lang: string, version: string|null = null): Promise { - Debug.log( - version !== null ? - `Updating the voice package from the ${version} version` : - 'Installing the voice package' - ); + Debug.log({ + function: 'Voice.update', + message: version !== null ? + `Updating the voice package from the ${version} version` : + 'Installing the voice package' + }); return new Promise((resolve, reject) => { (version === null ? this.latest : this.getDiff(version)) @@ -142,6 +144,65 @@ export default class Voice .catch((error) => reject(error)); }); } + + /** + * Pre-download the game's voice update + * + * @param version current game version to download difference from + * + * @returns null if the game pre-downloading is not available or the language wasn't found. Otherwise - downloading stream + * @returns Error if company's servers are unreachable or they responded with an error + */ + public static predownloadUpdate(lang: string, version: string|null = null): Promise + { + const debugThread = new DebugThread('Voice.predownloadUpdate', 'Predownloading game voice data...') + + return new Promise((resolve) => { + Game.getLatestData() + .then((data) => { + if (data.pre_download_game) + { + let voicePack = data.pre_download_game.latest.voice_packs.filter(voice => voice.language === lang); + + if (version !== null) + for (const diff of data.pre_download_game.diffs) + if (diff.version == version) + { + voicePack = diff.voice_packs.filter(voice => voice.language === lang); + + break; + } + + if (voicePack.length === 1) + { + debugThread.log(`Downloading update from the path: ${voicePack[0].path}`); + + constants.paths.launcherDir.then((dir) => { + Downloader.download(voicePack[0].path, `${dir}/voice-${lang}-predownloaded.zip`) + .then((stream) => resolve(stream)); + }); + } + + else resolve(null); + } + + else resolve(null); + }) + .catch((error) => resolve(error)); + }); + } + + /** + * Checks whether the update was downloaded or not + */ + public static isUpdatePredownloaded(lang: string): Promise + { + return new Promise(async (resolve) => { + Neutralino.filesystem.getStats(`${await constants.paths.launcherDir}/voice-${lang}-predownloaded.zip`) + .then(() => resolve(true)) + .catch(() => resolve(false)); + }); + } } export { Stream }; diff --git a/src/ts/core/Cache.ts b/src/ts/core/Cache.ts index 16b08fb..a5db8c6 100644 --- a/src/ts/core/Cache.ts +++ b/src/ts/core/Cache.ts @@ -1,4 +1,5 @@ import constants from '../Constants'; +import Debug from './Debug'; type Record = { expired: boolean; @@ -9,6 +10,10 @@ 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 * @@ -17,19 +22,44 @@ export default class Cache public static get(name: string): Promise { return new Promise(async (resolve) => { - Neutralino.filesystem.readFile(await constants.paths.cache) - .then((cache) => { - cache = JSON.parse(cache); + if (this.cache !== null && this.cache[name] !== undefined) + { + Debug.log({ + function: 'Cache.get', + message: [ + `Resolved ${this.cache[name].expired ? 'expired' : 'unexpired'} hot cache record`, + `[name] ${name}`, + `[value]: ${this.cache[name].value}` + ] + }); - if (cache[name] === undefined) + resolve(this.cache[name]); + } + + else Neutralino.filesystem.readFile(await constants.paths.cache) + .then((cache) => { + this.cache = JSON.parse(cache); + + if (this.cache![name] === undefined) resolve(null); else { - resolve({ - expired: cache[name].ttl !== null ? Date.now() > cache[name].ttl * 1000 : false, - value: JSON.parse(atob(cache[name].value)) + 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)); @@ -49,19 +79,40 @@ export default class Cache { return new Promise((resolve) => { constants.paths.cache.then((cacheFile) => { - let cache = {}; + if (this.cache === null) + { + Neutralino.filesystem.readFile(cacheFile) + .then((cacheRaw) => + { + this.cache = JSON.parse(cacheRaw); - Neutralino.filesystem.readFile(cacheFile) - .then((cacheRaw) => cache = JSON.parse(cacheRaw)) - .catch(() => {}); + writeCache(); + }) + .catch(() => { + this.cache = {}; - cache[name] = { - ttl: ttl !== null ? Math.round(Date.now() / 1000) + ttl : null, - value: btoa(JSON.stringify(value)) + writeCache(); + }); + } + + 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()); }; - - Neutralino.filesystem.writeFile(cacheFile, JSON.stringify(cache)) - .then(() => resolve()); }); }); } diff --git a/src/ts/launcher/State.ts b/src/ts/launcher/State.ts index 03ab1b1..1a8bbe4 100644 --- a/src/ts/launcher/State.ts +++ b/src/ts/launcher/State.ts @@ -14,6 +14,7 @@ export default class State public launcher: Launcher; public launchButton: HTMLElement; + public predownloadButton: HTMLElement; protected _state: LauncherState = 'game-launch-available'; @@ -34,6 +35,7 @@ export default class State this.launcher = launcher; this.launchButton = document.getElementById('launch'); + this.predownloadButton = document.getElementById('predownload'); this.launchButton.onclick = () => { if (this.events[this._state]) @@ -42,14 +44,30 @@ export default class State this.events[this._state].then((event) => { event.default(this.launcher).then(() => { - this.launchButton.style['display'] = 'block'; - - this.update(); + this.update().then(() => { + this.launchButton.style['display'] = 'block'; + }); }); }); } }; + this.predownloadButton.onclick = () => { + this.launchButton.style['display'] = 'none'; + this.predownloadButton.style['display'] = 'none'; + + const module = this._state === 'game-pre-installation-available' ? + 'Predownload' : 'PredownloadVoice'; + + import(`./states/${module}`).then((module) => { + module.default(this.launcher).then(() => { + this.update().then(() => { + this.launchButton.style['display'] = 'block'; + }); + }); + }); + }; + this.update().then(() => { Neutralino.storage.setData('launcherLoaded', 'aboba'); @@ -73,6 +91,7 @@ export default class State this._state = state; this.launcher.progressBar!.hide(); + this.predownloadButton.style['display'] = 'none'; switch(state) { @@ -81,6 +100,14 @@ export default class State break; + case 'game-pre-installation-available': + case 'game-voice-pre-installation-available': + this.launchButton.textContent = 'Launch'; + + this.predownloadButton.style['display'] = 'block'; + + break; + case 'game-installation-available': this.launchButton.textContent = 'Install'; @@ -125,14 +152,14 @@ export default class State let state: LauncherState; const gameCurrent = await Game.current; - const gameLatest = (await Game.latest).version; + const gameLatest = await Game.getLatestData(); const patch = await Patch.latest; const voiceData = await Voice.current; if (gameCurrent === null) state = 'game-installation-available'; - else if (gameCurrent != gameLatest) + else if (gameCurrent != gameLatest.game.latest.version) state = 'game-update-available'; // TODO: update this thing if the user selected another voice language @@ -146,6 +173,12 @@ export default class State 'test-patch-available' : 'patch-available'); } + else if (gameLatest.pre_download_game && !await Game.isUpdatePredownloaded()) + state = 'game-pre-installation-available'; + + else if (gameLatest.pre_download_game && !await Voice.isUpdatePredownloaded(await Voice.selected)) + state = 'game-voice-pre-installation-available'; + else state = 'game-launch-available'; this.set(state); diff --git a/src/ts/launcher/states/Launch.ts b/src/ts/launcher/states/Launch.ts index d06c1a9..52bb7bd 100644 --- a/src/ts/launcher/states/Launch.ts +++ b/src/ts/launcher/states/Launch.ts @@ -5,6 +5,7 @@ import Notifications from '../../core/Notifications'; import Runners from '../../core/Runners'; import Game from '../../Game'; import Process from '../../neutralino/Process'; +import Window from '../../neutralino/Window'; declare const Neutralino; @@ -32,6 +33,8 @@ export default (): Promise => { // Otherwise run the game else { + Window.current.hide(); + /** * Selecting wine executable */ @@ -139,6 +142,8 @@ export default (): Promise => { process.finish(() => { const stopTime = Date.now(); + Window.current.show(); + // todo resolve(); diff --git a/src/ts/launcher/states/Predownload.ts b/src/ts/launcher/states/Predownload.ts new file mode 100644 index 0000000..2811752 --- /dev/null +++ b/src/ts/launcher/states/Predownload.ts @@ -0,0 +1,44 @@ +import type Launcher from '../../Launcher'; + +import Game from '../../Game'; +import Prefix from '../../core/Prefix'; + +export default (launcher: Launcher): Promise => { + return new Promise(async (resolve) => { + Prefix.exists().then((exists) => { + if (!exists) + { + import('./CreatePrefix').then((module) => { + module.default(launcher).then(() => updateGame()); + }); + } + }); + + const updateGame = async () => { + const prevGameVersion = await Game.current; + + Game.predownloadUpdate(prevGameVersion).then((stream) => { + launcher.progressBar?.init({ + label: 'Downloading game...', + showSpeed: true, + showEta: true, + showPercents: true, + showTotals: true + }); + + stream?.start(() => launcher.progressBar?.show()); + + stream?.progress((current: number, total: number, difference: number) => { + launcher.progressBar?.update(current, total, difference); + }); + + stream?.finish(() => { + // Predownload voice package when the game itself has been downloaded + import('./PredownloadVoice').then((module) => { + module.default(launcher, prevGameVersion).then(() => resolve()); + }); + }); + }); + }; + }); +}; diff --git a/src/ts/launcher/states/PredownloadVoice.ts b/src/ts/launcher/states/PredownloadVoice.ts new file mode 100644 index 0000000..10da460 --- /dev/null +++ b/src/ts/launcher/states/PredownloadVoice.ts @@ -0,0 +1,29 @@ +import type Launcher from '../../Launcher'; + +import Voice from '../../Voice'; + +export default (launcher: Launcher, prevGameVersion: string|null = null): Promise => { + return new Promise(async (resolve) => { + Voice.predownloadUpdate(await Voice.selected, prevGameVersion).then((stream) => { + launcher.progressBar?.init({ + label: 'Downloading voice package...', + showSpeed: true, + showEta: true, + showPercents: true, + showTotals: true + }); + + stream?.start(() => launcher.progressBar?.show()); + + stream?.progress((current: number, total: number, difference: number) => { + launcher.progressBar?.update(current, total, difference); + }); + + stream?.finish(() => { + launcher.progressBar?.hide(); + + resolve(); + }); + }); + }); +}; diff --git a/src/ts/types/GameData.d.ts b/src/ts/types/GameData.d.ts index 6a5b8c6..3220df0 100644 --- a/src/ts/types/GameData.d.ts +++ b/src/ts/types/GameData.d.ts @@ -52,12 +52,17 @@ type DeprecatedPackage = { md5: string; } +type PreDownloadGame = { + latest: Latest; + diffs: Diff[]; +}; + type Data = { game: Game; plugin: Plugin; web_url: string; force_update?: any; - pre_download_game?: any; + pre_download_game?: PreDownloadGame; deprecated_packages: DeprecatedPackage[]; sdk?: any; } diff --git a/src/ts/types/Launcher.ts b/src/ts/types/Launcher.ts index 38817c3..1597759 100644 --- a/src/ts/types/Launcher.ts +++ b/src/ts/types/Launcher.ts @@ -19,6 +19,8 @@ type LauncherState = | 'game-installation-available' | 'game-update-available' | 'game-voice-update-required' + | 'game-pre-installation-available' + | 'game-voice-pre-installation-available' | 'game-launch-available'; export type { LauncherState }; \ No newline at end of file