From f85b5150af209eef4e952d8f2a4b643f5c894558 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 18 Jul 2023 17:54:29 +0100 Subject: [PATCH 01/11] feat: Archive State for Games --- .github/workflows/nexus.yml | 1 + lang/en.json | 5 +++- package.json | 2 +- src/back/index.ts | 3 +- src/back/sync.ts | 5 +++- src/database/entity/Game.ts | 4 +++ .../migration/1689423335642-ArchiveState.ts | 11 +++++++ .../components/RightBrowseSidebar.tsx | 29 +++++++++++++++++-- src/shared/back/types.ts | 6 ++++ src/shared/lang.ts | 3 ++ typings/flashpoint-launcher.d.ts | 6 ++++ 11 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/database/migration/1689423335642-ArchiveState.ts diff --git a/.github/workflows/nexus.yml b/.github/workflows/nexus.yml index e3ca7c1b5..c8f4f5373 100644 --- a/.github/workflows/nexus.yml +++ b/.github/workflows/nexus.yml @@ -6,6 +6,7 @@ on: - master - develop - 'release/**' + - 'hotfix/**' jobs: build: diff --git a/lang/en.json b/lang/en.json index d3c21556f..7b6755e75 100644 --- a/lang/en.json +++ b/lang/en.json @@ -362,7 +362,10 @@ "noMountParameters": "No Mount Parameters", "showExtremeScreenshot": "Show Extreme Screenshot", "busy": "Please Wait...", - "openGameDataBrowser": "Open Game Data Browser" + "openGameDataBrowser": "Open Game Data Browser", + "notArchived": "Not Archived", + "archived": "Archived", + "playOnline": "Play Online" }, "tags": { "name": "Name", diff --git a/package.json b/package.json index 3848ae34d..384d65ada 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flashpoint-launcher", - "version": "12.0.0", + "version": "12.1.0", "description": "A desktop application used to browse, manage and play games from Flashpoint Archive", "main": "build/main/index.js", "config": { diff --git a/src/back/index.ts b/src/back/index.ts index 7b131d484..dc4c102b9 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -95,6 +95,7 @@ import { createContainer, exit, getMacPATH, runService } from './util/misc'; import { uuid } from './util/uuid'; import { PlayTimeIndices1687847922729 } from '@database/migration/1687847922729-PlayTimeIndices'; import axios from 'axios'; +import { ArchiveState1689423335642 } from '@database/migration/1689423335642-ArchiveState'; const dataSourceOptions: DataSourceOptions = { type: 'better-sqlite3', @@ -103,7 +104,7 @@ const dataSourceOptions: DataSourceOptions = { migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, SourceFileCount1612436426353, GameTagsStr1613571078561, GameDataParams1619885915109, RemoveSources1676712700000, RemovePlaylist1676713895000, TagifyPlatform1677943090621, AddPlatformsRedundancyFieldToGame1677951346785, GDIndex1680813346696, MoveLaunchPath1681561150000, - PrimaryPlatform1684673859425, PlayTime1687807237714, PlayTimeIndices1687847922729 + PrimaryPlatform1684673859425, PlayTime1687807237714, PlayTimeIndices1687847922729, ArchiveState1689423335642 ] }; export let AppDataSource: DataSource = new DataSource(dataSourceOptions); diff --git a/src/back/sync.ts b/src/back/sync.ts index 919229912..225685b39 100644 --- a/src/back/sync.ts +++ b/src/back/sync.ts @@ -353,7 +353,8 @@ export async function syncGames(tx: EntityManager, source: GameMetadataSource, d language: changedGame.language, library: changedGame.library, activeDataId: -1, - platformName: changedGame.platform_name + platformName: changedGame.platform_name, + archiveState: changedGame.archive_state }).where({ id: changedGame.id }).execute(); } @@ -387,6 +388,7 @@ export async function syncGames(tx: EntityManager, source: GameMetadataSource, d activeDataId: -1, activeDataOnDisk: false, platformName: newGame.platform_name, + archiveState: newGame.archive_state, }); await gamesRepo.save(g); } @@ -533,6 +535,7 @@ type RemoteGame = { language: string; library: string; platform_name: string; + archive_state: number; } type RemoteTagRaw = { diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 7561cdb05..9d2b48278 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -3,6 +3,7 @@ import { AdditionalApp } from './AdditionalApp'; import { GameData } from './GameData'; import { Platform } from './Platform'; import { Tag } from './Tag'; +import { ArchiveState } from '@shared/back/types'; @Index('IDX_lookup_title', ['library', 'title']) @Index('IDX_lookup_dateAdded', ['library', 'dateAdded']) @@ -164,6 +165,9 @@ export class Game { @Column({ default: 0 }) playCounter: number; + @Column({ default: ArchiveState.Available }) + archiveState: ArchiveState; + // This doesn't run... sometimes. @BeforeUpdate() updateTagsStr() { diff --git a/src/database/migration/1689423335642-ArchiveState.ts b/src/database/migration/1689423335642-ArchiveState.ts new file mode 100644 index 000000000..7fb8e2abb --- /dev/null +++ b/src/database/migration/1689423335642-ArchiveState.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class ArchiveState1689423335642 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "game" ADD COLUMN "archiveState" integer DEFAULT 2`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "game" DROP COLUMN "archiveState"`); + } +} diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index ee06dcab9..9e4391af9 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -317,9 +317,11 @@ export class RightBrowseSidebar extends React.Component
{ this.props.fpfssEditMode ? strings.fpfssGame : - this.state.activeData ? (this.state.activeData.presentOnDisk ? strings.installed : strings.notInstalled): strings.legacyGame} + this.props.currentGame?.archiveState === 0 ? strings.notArchived : + this.props.currentGame?.archiveState === 1 ? strings.archived : + this.state.activeData ? (this.state.activeData.presentOnDisk ? strings.installed : strings.notInstalled): strings.legacyGame}
- { this.state.activeData && ( + { this.props.currentGame?.archiveState === 2 && this.state.activeData && (
{`${sizeToString(this.state.activeData.size)}`}
@@ -342,6 +344,29 @@ export class RightBrowseSidebar extends React.Component {strings.stop} + ) : (this.props.currentGame?.archiveState == 0) ? ( +
+ {strings.notArchived} +
+ ) : (this.props.currentGame?.archiveState == 1) ? ( +
{ + if (this.props.currentGame) { + let url = this.props.currentGame.source; + if (!url.startsWith('http://') && !url.startsWith('https://')) { + alert('Cannot open, not a valid source url.'); + } + if (url.endsWith(')') && url.toLowerCase().includes('wayback')) { + // Cut off after space + url = url.split(' ')[0]; + } + remote.shell.openExternal(url); + } + }}> + {strings.playOnline} +
) : (this.state.activeData && !this.state.activeData.presentOnDisk) ? (
void; }; From c08851b4495af9a80da89c53fb82020bb2b64a24 Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 19 Jul 2023 16:44:25 +0100 Subject: [PATCH 02/11] feat: Add dialog in front of FPFSS login --- src/renderer/app.tsx | 6 ++++-- src/renderer/fpfss.ts | 30 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 9cd04c358..090978d6c 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -1902,9 +1902,11 @@ export class App extends React.Component { let user = this.props.main.fpfss.user; if (!user) { // Logged out, try login - user = await fpfssLogin() + user = await fpfssLogin(this.props.dispatchMain, this.props.main.dialogResEvent) .catch((err) => { - alert(err); + if (err !== 'User Cancelled') { + alert(err); + } }) as FpfssUser | null; // Weird void from inferred typing? if (user) { // Store in main state diff --git a/src/renderer/fpfss.ts b/src/renderer/fpfss.ts index 1df8e857c..9c71fdad3 100644 --- a/src/renderer/fpfss.ts +++ b/src/renderer/fpfss.ts @@ -1,8 +1,14 @@ import { FpfssUser } from '@shared/back/types'; import axios from 'axios'; import * as remote from '@electron/remote'; +import { uuid } from '@shared/utils/uuid'; +import { DialogState } from 'flashpoint-launcher'; +import { MainActionType } from './store/main/enums'; +import { Dispatch } from 'redux'; +import { MainAction } from './store/main/types'; +import EventEmitter = require('events'); -export async function fpfssLogin(): Promise { +export async function fpfssLogin(dispatchMain: Dispatch, dialogResEvent: EventEmitter): Promise { const fpfssBaseUrl = window.Shared.preferences.data.fpfssBaseUrl; // Get device auth token from FPFSS const tokenUrl = `${fpfssBaseUrl}/auth/token`; @@ -20,6 +26,17 @@ export async function fpfssLogin(): Promise { const profileUrl = `${fpfssBaseUrl}/api/profile`; await remote.shell.openExternal(verifyUrl); + const dialog: DialogState = { + largeMessage: true, + message: 'Please login in your browser to continue', + buttons: ['Cancel'], + id: uuid() + }; + dispatchMain({ + type: MainActionType.NEW_DIALOG, + dialog + }); + // Start loop until an end state occurs return new Promise((resolve, reject) => { const interval = setInterval(async () => { @@ -70,5 +87,16 @@ export async function fpfssLogin(): Promise { reject('Failed to contact FPFSS while polling'); }); }, token.interval * 1000); + // Listen for dialog response + dialogResEvent.once(dialog.id, (d: DialogState, res: number) => { + clearInterval(interval); + reject('User Cancelled'); + }); + }) + .finally(() => { + dispatchMain({ + type: MainActionType.CANCEL_DIALOG, + dialogId: dialog.id + }); }); } From e5177cd569f6a5232ab0a70377df56c93919cb8d Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 22 Jul 2023 19:21:47 +0000 Subject: [PATCH 03/11] fix: Game data download saving --- src/back/game/GameDataManager.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/back/game/GameDataManager.ts b/src/back/game/GameDataManager.ts index 1b5fe315b..75efafe8d 100644 --- a/src/back/game/GameDataManager.ts +++ b/src/back/game/GameDataManager.ts @@ -36,9 +36,13 @@ export async function downloadGameData(gameDataId: number, dataPacksFolderPath: if (sha256.toLowerCase() !== gameData.sha256.toLowerCase()) { reject('Hash of download does not match! Download aborted.\n (It may be a corrupted download, try again)'); } else { - await importGameDataSkipHash(gameData.gameId, tempPath, dataPacksFolderPath, sha256); - await fs.promises.unlink(tempPath); - resolve(); + try { + await importGameDataSkipHash(gameData.gameId, tempPath, dataPacksFolderPath, sha256); + await fs.promises.unlink(tempPath); + resolve(); + } catch (err) { + reject(err); + } } }); stream.pipe(hash); @@ -92,11 +96,9 @@ export async function importGameDataSkipHash(gameId: string, filePath: string, d const newPath = path.join(dataPacksFolderPath, newFilename); await fs.promises.copyFile(filePath, newPath); if (existingGameData) { - if (existingGameData.presentOnDisk === false) { - existingGameData.path = newFilename; - existingGameData.presentOnDisk = true; - return save(existingGameData); - } + existingGameData.path = newFilename; + existingGameData.presentOnDisk = true; + return save(existingGameData); } else { const newGameData = new GameData(); newGameData.title = 'Data Pack'; From 5b6094295caa6851d616566408bd14c1d1933611 Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 22 Jul 2023 19:28:37 +0000 Subject: [PATCH 04/11] fix: Kill main when back dies --- src/main/Main.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/Main.ts b/src/main/Main.ts index 2db4577d6..fd2a88428 100644 --- a/src/main/Main.ts +++ b/src/main/Main.ts @@ -173,6 +173,18 @@ export function main(init: Init): void { // Increase memory limit in dev instance (mostly for developer page functions) const env = Util.isDev ? Object.assign({ 'NODE_OPTIONS' : '--max-old-space-size=6144' }, process.env ) : process.env; state.backProc = fork(path.join(__dirname, '../back/index.js'), [init.rest], { detached: true, env, stdio: 'pipe' }); + state.backProc.on('exit', (code) => { + if (!code || code === 0) { + console.log('Back proc exited cleanly, killing self.'); + process.exit(process.pid); + } else { + console.log(`Back proc exited unclean (${code}), killing self after 60 seconds to allow time to view message.`); + setTimeout(() => { + process.exit(process.pid); + + }, 60000); + } + }); if (state.backProc.stdout) { state.backProc.stdout.on('data', (chunk) => { process.stdout.write(chunk); From f42b434e28f6ba2cd49a074074f341872977a7fa Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 22 Jul 2023 20:16:40 +0000 Subject: [PATCH 05/11] gitfix: Better import game data error reporting try 2 --- src/back/game/GameDataManager.ts | 6 +++++- src/main/Main.ts | 1 + src/shared/back/SocketClient.ts | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/back/game/GameDataManager.ts b/src/back/game/GameDataManager.ts index 75efafe8d..fa7254f4f 100644 --- a/src/back/game/GameDataManager.ts +++ b/src/back/game/GameDataManager.ts @@ -37,7 +37,11 @@ export async function downloadGameData(gameDataId: number, dataPacksFolderPath: reject('Hash of download does not match! Download aborted.\n (It may be a corrupted download, try again)'); } else { try { - await importGameDataSkipHash(gameData.gameId, tempPath, dataPacksFolderPath, sha256); + await importGameDataSkipHash(gameData.gameId, tempPath, dataPacksFolderPath, sha256) + .catch((err) => { + log.error('Launcher', 'Error importing game data ' + err); + throw err; + }); await fs.promises.unlink(tempPath); resolve(); } catch (err) { diff --git a/src/main/Main.ts b/src/main/Main.ts index fd2a88428..a81adc7cd 100644 --- a/src/main/Main.ts +++ b/src/main/Main.ts @@ -268,6 +268,7 @@ export function main(init: Init): void { }; }), TIMEOUT_DELAY); state.socket.setSocket(ws); + state.socket.killOnDisconnect = true; // Start frontend if (opts.frontend) { diff --git a/src/shared/back/SocketClient.ts b/src/shared/back/SocketClient.ts index 13a5ced4d..e71056bd4 100644 --- a/src/shared/back/SocketClient.ts +++ b/src/shared/back/SocketClient.ts @@ -26,6 +26,9 @@ export class SocketClient { /** If true, do not attempt to reconnect */ abortReconnects = false; + /** Kill self if connection ever fully dies (can't reconnect) */ + killOnDisconnect = false; + client: SocketServerClient = { id: -1, // Unused (only used by servers) next_id: 0, @@ -250,6 +253,12 @@ export class SocketClient { protected onClose(event: CloseEvent): void { if (this.abortReconnects) { console.log('Socket Client - Connection closed.'); + if (this.killOnDisconnect) { + process.exit(process.pid); + } + if (this.onStateChange) { + this.onStateChange(false); + } } else { console.log(`SharedSocket Closed (Code: ${event.code}, Clean: ${event.wasClean}, Reason: "${event.reason}", URL: "${this.url}").`); if (this.onStateChange) { From 66fc34697df18f0c430d9a9418791c02211281a7 Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 23 Jul 2023 09:28:30 +0100 Subject: [PATCH 06/11] fix: Make sure proxy dies on exit --- src/back/ManagedChildProcess.ts | 4 ++-- src/back/index.ts | 8 ++++---- src/back/util/misc.ts | 35 +++++++++++++++++++++++++-------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/back/ManagedChildProcess.ts b/src/back/ManagedChildProcess.ts index 16c34efd6..5d10b04a0 100644 --- a/src/back/ManagedChildProcess.ts +++ b/src/back/ManagedChildProcess.ts @@ -1,5 +1,5 @@ -import { IBackProcessInfo, INamedBackProcessInfo, ProcessState } from '@shared/interfaces'; import { ILogPreEntry } from '@shared/Log/interface'; +import { IBackProcessInfo, INamedBackProcessInfo, ProcessState } from '@shared/interfaces'; import * as Coerce from '@shared/utils/Coerce'; import { ChildProcess, spawn } from 'child_process'; import { EventEmitter } from 'events'; @@ -112,7 +112,7 @@ export class ManagedChildProcess extends EventEmitter { } // Spawn process log.debug('Server', `Arguments: ${this.info.arguments.join('; ')}`); - this.process = spawn(this.info.filename, this.info.arguments, { cwd: this.cwd, detached: this.detached, shell: this.shell , env: this.env}); + this.process = spawn(this.info.filename, this.info.arguments, { cwd: this.cwd, detached: this.detached, shell: this.shell, env: this.env}); // Set start timestamp this.startTime = Date.now(); // Log diff --git a/src/back/index.ts b/src/back/index.ts index dc4c102b9..306b4fdb6 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -50,12 +50,15 @@ import { GDIndex1680813346696 } from '@database/migration/1680813346696-GDIndex' import { MoveLaunchPath1681561150000 } from '@database/migration/1681561150000-MoveLaunchPath'; import { PrimaryPlatform1684673859425 } from '@database/migration/1684673859425-PrimaryPlatform'; import { PlayTime1687807237714 } from '@database/migration/1687807237714-PlayTime'; +import { PlayTimeIndices1687847922729 } from '@database/migration/1687847922729-PlayTimeIndices'; +import { ArchiveState1689423335642 } from '@database/migration/1689423335642-ArchiveState'; import { CURATIONS_FOLDER_EXPORTED, CURATIONS_FOLDER_EXTRACTING, CURATIONS_FOLDER_TEMP, CURATIONS_FOLDER_WORKING, CURATION_META_FILENAMES } from '@shared/constants'; +import axios from 'axios'; import { Tail } from 'tail'; import { DataSource, DataSourceOptions } from 'typeorm'; import { ConfigFile } from './ConfigFile'; @@ -93,9 +96,6 @@ import { LogFile } from './util/LogFile'; import { logFactory } from './util/logging'; import { createContainer, exit, getMacPATH, runService } from './util/misc'; import { uuid } from './util/uuid'; -import { PlayTimeIndices1687847922729 } from '@database/migration/1687847922729-PlayTimeIndices'; -import axios from 'axios'; -import { ArchiveState1689423335642 } from '@database/migration/1689423335642-ArchiveState'; const dataSourceOptions: DataSourceOptions = { type: 'better-sqlite3', @@ -993,7 +993,7 @@ async function initialize() { for (let i = 0; i < state.serviceInfo.daemon.length; i++) { const service = state.serviceInfo.daemon[i]; const id = 'daemon_' + i; - runService(state, id, service.name || id, state.config.flashpointPath, {}, service); + runService(state, id, service.name || id, state.config.flashpointPath, { detached: !service.kill, noshell: !!service.kill }, service); } // Start file watchers for (let i = 0; i < state.serviceInfo.watch.length; i++) { diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index ef688a7f1..0dc7929e3 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -1,27 +1,27 @@ +import { ManagedChildProcess, ProcessOpts } from '@back/ManagedChildProcess'; +import { SocketServer } from '@back/SocketServer'; import { SERVICES_SOURCE } from '@back/constants'; import { createTagsFromLegacy } from '@back/importGame'; -import { ManagedChildProcess, ProcessOpts } from '@back/ManagedChildProcess'; import { exitApp } from '@back/responses'; -import { SocketServer } from '@back/SocketServer'; import { BackState, ShowMessageBoxFunc, ShowOpenDialogFunc, ShowSaveDialogFunc, StatusState } from '@back/types'; import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { Tag } from '@database/entity/Tag'; +import { deepCopy, recursiveReplace, stringifyArray } from '@shared/Util'; import { BackOut, ComponentState } from '@shared/back/types'; import { getCurationFolder } from '@shared/curate/util'; import { BrowserApplicationOpts } from '@shared/extensions/interfaces'; import { IBackProcessInfo, INamedBackProcessInfo, IService, ProcessState } from '@shared/interfaces'; -import { autoCode, getDefaultLocalization, LangContainer, LangFile } from '@shared/lang'; +import { LangContainer, LangFile, autoCode, getDefaultLocalization } from '@shared/lang'; import { Legacy_IAdditionalApplicationInfo, Legacy_IGameInfo } from '@shared/legacy/interfaces'; -import { deepCopy, recursiveReplace, stringifyArray } from '@shared/Util'; import * as child_process from 'child_process'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; +import * as psTree from 'ps-tree'; import { promisify } from 'util'; -import { uuid } from './uuid'; import { AppDataSource } from '..'; -import terminate from 'terminate'; +import { uuid } from './uuid'; const unlink = promisify(fs.unlink); @@ -129,9 +129,14 @@ export async function exit(state: BackState, beforeProcessExit?: () => void | Pr await Promise.race([ service.kill(), new Promise(resolve => { - setTimeout(resolve, 10000); + setTimeout((resolve), 10000); }) ]); + if (!('name' in service.info)) { + console.log(` - Killed '${service.info.filename}' Service`); + } else { + console.log(` - Killed '${service.info.name}' Service`); + } } } console.log(' - Managed Services Killed'); @@ -187,7 +192,21 @@ export async function exit(state: BackState, beforeProcessExit?: () => void | Pr state.socketServer.close() .catch(e => { console.error(e); }); - terminate(process.pid); + await new Promise((resolve, reject) => { + psTree(process.pid, async (error, children) => { + if (error) { + reject(error); + } + // Kill each child process. + for (const child of children) { + process.kill(Number(child.PID)); + } + resolve(); + }); + }); + // Kill the parent process. + process.kill(process.pid); + process.exit(0); }); } } From cdd2d9a1966c614c5199d6ae4ed69ea2718cf5cb Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 23 Jul 2023 10:06:05 +0100 Subject: [PATCH 07/11] fix: Allow game data with clashing hashes to work --- src/back/GameLauncher.ts | 9 +++++++-- src/back/game/GameDataManager.ts | 13 ++++++++----- src/back/responses.ts | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/back/GameLauncher.ts b/src/back/GameLauncher.ts index b929f2b1c..9cf4a6c65 100644 --- a/src/back/GameLauncher.ts +++ b/src/back/GameLauncher.ts @@ -525,13 +525,18 @@ async function handleGameDataParams(opts: LaunchBaseOpts, serverOverride?: strin alreadyExtracted = fs.existsSync(filePath); } if (!alreadyExtracted) { - // Extra game data to htdocs folder + // Extract game data to htdocs folder const gameDataPath = path.join(opts.fpPath, opts.dataPacksFolderPath, gameData.path || ''); const tempPath = path.join(opts.fpPath, '.temp', 'extract'); await fs.ensureDir(tempPath); const destPath = path.join(opts.fpPath, opts.htdocsPath); log.debug('Launcher', `Extracting game data from "${gameDataPath}" to "${tempPath}"`); - await extractFullPromise([gameDataPath, tempPath, { $bin: opts.sevenZipPath }]); + await extractFullPromise([gameDataPath, tempPath, { $bin: opts.sevenZipPath }]) + .catch((err) => { + console.log(err); + log.error('Launcher', `Failed to extract game data: ${err}`); + throw err; + }); const contentFolder = path.join(tempPath, 'content'); // Move contents of contentFolder to destPath log.debug('Launcher', `Moving extracted game data from "${contentFolder}" to "${destPath}"`); diff --git a/src/back/game/GameDataManager.ts b/src/back/game/GameDataManager.ts index fa7254f4f..ba19402bc 100644 --- a/src/back/game/GameDataManager.ts +++ b/src/back/game/GameDataManager.ts @@ -18,7 +18,6 @@ export async function downloadGameData(gameDataId: number, dataPacksFolderPath: const sourceErrors: string[] = []; log.debug('Game Launcher', `Checking ${sources.length} Sources for this GameData...`); if (gameData) { - if (gameData.presentOnDisk) { return; } // GameData real, find an available source for (const source of sources) { try { @@ -37,8 +36,10 @@ export async function downloadGameData(gameDataId: number, dataPacksFolderPath: reject('Hash of download does not match! Download aborted.\n (It may be a corrupted download, try again)'); } else { try { - await importGameDataSkipHash(gameData.gameId, tempPath, dataPacksFolderPath, sha256) + log.debug('Game Launcher', 'Validated game data, importing to games folder'); + await importGameDataSkipHash(gameData.gameId, tempPath, dataPacksFolderPath, sha256, gameData) .catch((err) => { + console.log(`Error importing game data ${err}`); log.error('Launcher', 'Error importing game data ' + err); throw err; }); @@ -88,12 +89,14 @@ export async function remove(gameDataId: number): Promise { await gameDataRepository.delete({ id: gameDataId }); } -export async function importGameDataSkipHash(gameId: string, filePath: string, dataPacksFolderPath: string, sha256: string): Promise { +export async function importGameDataSkipHash(gameId: string, filePath: string, dataPacksFolderPath: string, sha256: string, existingGameData?: GameData): Promise { await fs.promises.access(filePath, fs.constants.F_OK); // Gather basic info const stats = await fs.promises.stat(filePath); - const gameData = await findGameData(gameId); - const existingGameData = gameData.find(g => g.sha256.toLowerCase() === sha256.toLowerCase()); + if (!existingGameData) { + const gameData = await findGameData(gameId); + existingGameData = gameData.find(g => g.sha256.toLowerCase() === sha256.toLowerCase()); + } // Copy file const dateAdded = new Date(); const newFilename = existingGameData ? `${gameId}-${existingGameData.dateAdded.getTime()}.zip` : `${gameId}-${dateAdded.getTime()}.zip`; diff --git a/src/back/responses.ts b/src/back/responses.ts index 6367c8b44..a37b41ad3 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -548,6 +548,7 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise Date: Sun, 30 Jul 2023 10:28:27 +0100 Subject: [PATCH 08/11] css: Change playtime stats display --- .../components/RightBrowseSidebar.tsx | 20 +++++++++---------- static/window/styles/core.css | 13 ++++++------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 9e4391af9..d2e47e9ae 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -3,7 +3,7 @@ import { Tag } from '@database/entity/Tag'; import { TagCategory } from '@database/entity/TagCategory'; import * as remote from '@electron/remote'; import { WithConfirmDialogProps } from '@renderer/containers/withConfirmDialog'; -import { BackIn, BackOut, BackOutTemplate, TagSuggestion } from '@shared/back/types'; +import { ArchiveState, BackIn, BackOut, BackOutTemplate, TagSuggestion } from '@shared/back/types'; import { LOGOS, SCREENSHOTS } from '@shared/constants'; import { wrapSearchTerm } from '@shared/game/GameFilter'; import { ModelUtils } from '@shared/game/util'; @@ -412,21 +412,21 @@ export class RightBrowseSidebar extends React.Component -
-
+
+
{strings.lastPlayed}
-
- {game.lastPlayed ? formatLastPlayed(game.lastPlayed, strings) : strings.never} +
+ {strings.playtime}
-
-
- {strings.playtime} +
+
+ {game.lastPlayed ? formatLastPlayed(game.lastPlayed, strings) : strings.never}
-
+
{formatPlaytime(game.playtime, strings)}
diff --git a/static/window/styles/core.css b/static/window/styles/core.css index 72f5607f7..b9365e867 100644 --- a/static/window/styles/core.css +++ b/static/window/styles/core.css @@ -1574,16 +1574,17 @@ body { flex-direction: column; margin-bottom: 0.5rem; } +.browse-right-sidebar__stats-row-top, +.browse-right-sidebar__stats-row-bottom, .browse-right-sidebar__stats-row { - display: flex; - flex-direction: row; - justify-content: space-between; + display: inline-flex; } -.browse-right-sidebar__stats-row-left { +.browse-right-sidebar__stats-row-top { font-weight: bold; } -.browse-right-sidebar__stats-row-right { - text-align: left; +.browse-right-sidebar__stats-cell { + width: 50%; + text-align: center; } From ebd7b39b528551044c5da5b6ad4be5d0e58a6eb2 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 1 Aug 2023 21:42:59 +0100 Subject: [PATCH 09/11] fix: Allow fpfss edits for legacy fields --- lang/en.json | 1 + src/renderer/app.tsx | 8 ++++++++ src/shared/Util.ts | 4 ++++ src/shared/lang.ts | 1 + 4 files changed, 14 insertions(+) diff --git a/lang/en.json b/lang/en.json index 7b6755e75..3eb1da509 100644 --- a/lang/en.json +++ b/lang/en.json @@ -342,6 +342,7 @@ "saveChanges": "Save Changes", "discardChanges": "Discard Changes", "editFpfssGame": "Edit Game (FPFSS)", + "showOnFpfss": "Show Game on FPFSS", "editGame": "Edit Game", "allGames": "All Games", "newPlaylist": "New Playlist", diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 090978d6c..c8507eea1 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -1102,6 +1102,14 @@ export class App extends React.Component { click: () => { this.onFpfssEditGame(gameId); } + }, + { + /* Show on FPFSS */ + label: strings.browse.showOnFpfss, + enabled: this.props.preferencesData.enableEditing, + click: () => { + remote.shell.openExternal(`${this.props.preferencesData.fpfssBaseUrl}/web/game/${gameId}`); + } } ] : []; diff --git a/src/shared/Util.ts b/src/shared/Util.ts index 24033f6dd..ed841667e 100644 --- a/src/shared/Util.ts +++ b/src/shared/Util.ts @@ -548,6 +548,8 @@ export function mapFpfssGameToLocal(data: any, categories: TagCategory[]): Game if (!game.data) { game.data = []; } if (!game.platforms) { game.platforms = []; } if (!game.tags) { game.tags = []; } + game.legacyApplicationPath = (game as any).applicationPath; + game.legacyLaunchCommand = (game as any).launchCommand; // Tags for (const tag of game.tags) { tag.primaryAlias = { @@ -600,6 +602,8 @@ export function mapLocalToFpfssGame(game: Game, categories: TagCategory[], userI userId: userId }); } + (game as any).launchCommand = game.legacyLaunchCommand; + (game as any).applicationPath = game.legacyApplicationPath; return snekify({ ...game, tags: newTags, diff --git a/src/shared/lang.ts b/src/shared/lang.ts index 1effa7e3a..1c7db95bf 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -345,6 +345,7 @@ const langTemplate = { 'saveChanges', 'discardChanges', 'editFpfssGame', + 'showOnFpfss', 'editGame', 'allGames', 'newPlaylist', From 2c43d5578bbd859da4df296e383b488716ad5e42 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 1 Aug 2023 22:42:45 +0100 Subject: [PATCH 10/11] refactor: Sidebar stat changes --- lang/en.json | 1 + package-lock.json | 30 +---- .../components/RightBrowseSidebar.tsx | 120 ++++++++++++------ src/shared/lang.ts | 1 + static/window/styles/core.css | 21 ++- 5 files changed, 108 insertions(+), 65 deletions(-) diff --git a/lang/en.json b/lang/en.json index 3eb1da509..4461dbb2e 100644 --- a/lang/en.json +++ b/lang/en.json @@ -301,6 +301,7 @@ "noVersion": "No Version", "releaseDate": "Release Date", "noReleaseDate": "No Release Date", + "noneFound": "None Found", "language": "Language", "noLanguage": "No Language", "dateAdded": "Date Added", diff --git a/package-lock.json b/package-lock.json index 2e7a8caab..ca30f08ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "flashpoint-launcher", - "version": "12.0.0", + "version": "12.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "flashpoint-launcher", - "version": "12.0.0", + "version": "12.1.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -24,7 +24,6 @@ "follow-redirects": "1.14.8", "fs-extra": "8.1.0", "lodash": "^4.17.21", - "longjohn": "^0.2.12", "mime": "2.4.4", "minimist": "^1.2.7", "node-7z": "1.1.1", @@ -14173,17 +14172,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/longjohn": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/longjohn/-/longjohn-0.2.12.tgz", - "integrity": "sha512-36GDBamIwXTdCikEO29GWsE3GjtNGBGKNqSwWbwXczVC3KHNA1OzzIBKc9uocjQVYAPVGf/TWn9b0aPnraPjiQ==", - "dependencies": { - "source-map-support": "0.3.2 - 1.0.0" - }, - "engines": { - "node": ">= 0.9.3" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -18112,6 +18100,7 @@ }, "node_modules/source-map": { "version": "0.6.1", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -18131,6 +18120,7 @@ }, "node_modules/source-map-support": { "version": "0.5.21", + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -30837,14 +30827,6 @@ "longest-streak": { "version": "3.0.0" }, - "longjohn": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/longjohn/-/longjohn-0.2.12.tgz", - "integrity": "sha512-36GDBamIwXTdCikEO29GWsE3GjtNGBGKNqSwWbwXczVC3KHNA1OzzIBKc9uocjQVYAPVGf/TWn9b0aPnraPjiQ==", - "requires": { - "source-map-support": "0.3.2 - 1.0.0" - } - }, "loose-envify": { "version": "1.4.0", "requires": { @@ -33367,7 +33349,8 @@ } }, "source-map": { - "version": "0.6.1" + "version": "0.6.1", + "dev": true }, "source-map-resolve": { "version": "0.5.3", @@ -33382,6 +33365,7 @@ }, "source-map-support": { "version": "0.5.21", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index d2e47e9ae..9f548b60b 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -126,7 +126,6 @@ export class RightBrowseSidebar extends React.Component = React.createRef(); @@ -212,8 +211,6 @@ export class RightBrowseSidebar extends React.Component -
-
- {strings.lastPlayed} +
+
+
+ {strings.lastPlayed} +
-
- {strings.playtime} +
+
+ {game.lastPlayed ? formatLastPlayed(game.lastPlayed, strings) : strings.never} +
-
-
- {game.lastPlayed ? formatLastPlayed(game.lastPlayed, strings) : strings.never} +
+
+
+ {strings.playtime} +
-
- {formatPlaytime(game.playtime, strings)} +
+
+ {formatPlaytime(game.playtime, strings)} +
@@ -578,16 +583,17 @@ export class RightBrowseSidebar extends React.Component
-
-

{strings.releaseDate}:

- -
+ { editable && ( +
+

{strings.releaseDate}:

+ +
+ )}

{strings.language}:

-
-

{strings.dateAdded}:

-

- {(new Date(dateAdded)).toUTCString()} -

-
-
-

{strings.dateModified}:

-

- {(new Date(dateModified)).toUTCString()} -

-
{ game.broken || editable ? (
) } + {/* -- Date Display -- */} + { !editable && ( +
+
+
+
+
+ {strings.dateAdded} +
+
+
+
+ {formatSidebarDate(game.dateAdded)} +
+
+
+
+
+
+ {strings.dateModified} +
+
+
+
+ {formatSidebarDate(game.dateModified)} +
+
+
+
+
+
+ {strings.releaseDate} +
+
+
+
+ {game.releaseDate ? game.releaseDate : strings.noneFound} +
+
+
+
+
+ )} {/* -- Playlist Game Entry Notes -- */} { currentPlaylistEntry ? (
@@ -1407,3 +1440,12 @@ function ordinalSuffixOf(i: number) { } return i + 'th'; } + +function formatSidebarDate(dStr: string): string { + try { + const d = new Date(dStr); + return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; + } catch { + return 'Invalid Date'; + } +} diff --git a/src/shared/lang.ts b/src/shared/lang.ts index 1c7db95bf..861a24186 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -310,6 +310,7 @@ const langTemplate = { 'noVersion', 'releaseDate', 'noReleaseDate', + 'noneFound', 'language', 'noLanguage', 'dateAdded', diff --git a/static/window/styles/core.css b/static/window/styles/core.css index b9365e867..914c11b30 100644 --- a/static/window/styles/core.css +++ b/static/window/styles/core.css @@ -1513,7 +1513,7 @@ body { } /* Browse-Right-Sidebar Section */ .browse-right-sidebar__section { - margin-bottom: 1.1em; + margin-bottom: 1em; } .browse-right-sidebar__section:last-child { margin-bottom: 0; @@ -1571,21 +1571,36 @@ body { /* Browse game stats */ .browse-right-sidebar__stats { display: flex; - flex-direction: column; + flex-flow: wrap; + flex-direction: row; margin-bottom: 0.5rem; } .browse-right-sidebar__stats-row-top, .browse-right-sidebar__stats-row-bottom, .browse-right-sidebar__stats-row { + width: 100%; display: inline-flex; } .browse-right-sidebar__stats-row-top { + width: 100%; font-weight: bold; } .browse-right-sidebar__stats-cell { - width: 50%; + width: 100%; + white-space: nowrap; text-align: center; } +.browse-right-sidebar__stats-box { + margin-left: 0.3rem; + margin-right: 0.3rem; + flex-direction: column; + flex-grow: 1; + display: flex; +} +/** @TODO: Remove awful workaround */ +.browse-right-sidebar__stats-cell-placeholder { + font-style: unset !important; +} From a921a2836701d80299f763d520a7b323d619e294 Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 2 Aug 2023 17:00:44 +0100 Subject: [PATCH 11/11] fix: Open FPM button --- src/back/responses.ts | 2 +- src/back/util/misc.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/back/responses.ts b/src/back/responses.ts index a37b41ad3..e57bb8fc2 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -1706,7 +1706,7 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise c.state === ComponentState.NEEDS_UPDATE).length > 0; exitApp(state, async () => { const args = updatesReady ? ['/update', '/launcher'] : ['/launcher']; - const child = child_process.spawn(fpmPath, args, { detached: true, cwd, stdio: ['ignore', 'ignore', 'ignore'] }); + const child = child_process.spawn(fpmPath, args, { detached: true, shell: true, cwd, stdio: ['ignore', 'ignore', 'ignore'] }); child.unref(); }); }); diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index 0dc7929e3..b4e0ccf1d 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -184,7 +184,7 @@ export async function exit(state: BackState, beforeProcessExit?: () => void | Pr console.log(' - Cleanup Complete, Exiting Process...'); if (beforeProcessExit) { console.log(' - Executing callback before process exit...'); - await beforeProcessExit(); + await Promise.resolve(beforeProcessExit()); } await state.socketServer.broadcast(BackOut.QUIT); console.log(' - Quit Broadcast Sent');