Skip to content

Commit

Permalink
feat: Game time logging
Browse files Browse the repository at this point in the history
fix: Proper screenshot sizing on sidebar
  • Loading branch information
colin969 committed Jun 26, 2023
1 parent 1f09906 commit 3c366f1
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 29 deletions.
8 changes: 8 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,14 @@
"noTitle": "No Title",
"by": "by",
"play": "Play",
"lastPlayed": "Last Played",
"playtime": "Playtime",
"playCount": "Play Count",
"never": "Never",
"today": "Today",
"yesterday": "Yesterday",
"daysAgo": "{0} Days Ago",
"weeksAgo": "{0} Weeks Ago",
"stop": "Stop",
"noDeveloper": "No Developer",
"alternateTitles": "Alternate Titles",
Expand Down
1 change: 0 additions & 1 deletion src/back/GameLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,6 @@ async function handleGameDataParams(opts: LaunchBaseOpts, serverOverride?: strin
export async function checkAndInstallPlatform(platforms: Platform[], state: BackState, openMessageBox: ShowMessageBoxFunc) {
const compsToInstall: ComponentStatus[] = [];
for (const platform of platforms) {
console.log(JSON.stringify(state.componentStatuses.map(c => c.name), undefined, 2));
const compIdx = state.componentStatuses.findIndex(c => c.name.toLowerCase() === platform.primaryAlias.name.toLowerCase());
if (compIdx > -1) {
if (state.componentStatuses[compIdx].state === ComponentState.UNINSTALLED) {
Expand Down
17 changes: 17 additions & 0 deletions src/back/game/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,20 @@ async function getGameQuery(

return query;
}

export async function logGameStart(gameId: string) {
const game = await findGame(gameId);
if (game) {
game.lastPlayed = (new Date()).toISOString();
game.playCounter += 1;
await save(game);
}
}

export async function addGamePlaytime(gameId: string, time: number) {
const game = await findGame(gameId);
if (game) {
game.playtime = game.playtime + Math.ceil(time / 1000);
await save(game);
}
}
47 changes: 24 additions & 23 deletions src/back/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ import { SourceFileURL1612435692266 } from '@database/migration/1612435692266-So
import { SourceFileCount1612436426353 } from '@database/migration/1612436426353-SourceFileCount';
import { GameTagsStr1613571078561 } from '@database/migration/1613571078561-GameTagsStr';
import { GameDataParams1619885915109 } from '@database/migration/1619885915109-GameDataParams';
import { BackIn, BackInit, BackInitArgs, BackOut, BackResParams, ComponentState, ComponentStatus, DownloadDetails } from '@shared/back/types';
import { LoadedCuration } from '@shared/curate/types';
import { getContentFolderByKey } from '@shared/curate/util';
import { ILogoSet, LogoSet } from '@shared/extensions/interfaces';
import { IBackProcessInfo, RecursivePartial } from '@shared/interfaces';
import { getDefaultLocalization, LangFileContent } from '@shared/lang';
import { ILogEntry, LogLevel } from '@shared/Log/interface';
import { PreferencesFile } from '@shared/preferences/PreferencesFile';
import { defaultPreferencesData } from '@shared/preferences/util';
import { Theme } from '@shared/ThemeFile';
import {
createErrorProxy, deepCopy,
removeFileExtension,
stringifyArray
} from '@shared/Util';
import { BackIn, BackInit, BackInitArgs, BackOut, BackResParams, ComponentState, ComponentStatus, DownloadDetails } from '@shared/back/types';
import { LoadedCuration } from '@shared/curate/types';
import { getContentFolderByKey } from '@shared/curate/util';
import { ILogoSet, LogoSet } from '@shared/extensions/interfaces';
import { IBackProcessInfo, RecursivePartial } from '@shared/interfaces';
import { LangFileContent, getDefaultLocalization } from '@shared/lang';
import { PreferencesFile } from '@shared/preferences/PreferencesFile';
import { defaultPreferencesData } from '@shared/preferences/util';
import { validateSemiUUID } from '@shared/utils/uuid';
import * as child_process from 'child_process';
import { EventEmitter } from 'events';
Expand All @@ -36,7 +36,7 @@ import { http as httpFollow, https as httpsFollow } from 'follow-redirects';
import * as fs from 'fs-extra';
import * as http from 'http';
import * as mime from 'mime';
import { extractFull, Progress } from 'node-7z';
import { Progress, extractFull } from 'node-7z';
import * as path from 'path';
import 'reflect-metadata';
import { genCurationWarnings, loadCurationFolder } from './curate/util';
Expand All @@ -47,6 +47,10 @@ import { RemoveSources1676712700000 } from '@database/migration/1676712700000-Re
import { RemovePlaylist1676713895000 } from '@database/migration/1676713895000-RemovePlaylist';
import { TagifyPlatform1677943090621 } from '@database/migration/1677943090621-TagifyPlatform';
import { AddPlatformsRedundancyFieldToGame1677951346785 } from '@database/migration/1677951346785-AddPlatformsRedundancyFieldToGame';
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 {
CURATIONS_FOLDER_EXPORTED,
CURATIONS_FOLDER_EXTRACTING,
Expand All @@ -56,32 +60,32 @@ import {
import { Tail } from 'tail';
import { DataSource, DataSourceOptions } from 'typeorm';
import { ConfigFile } from './ConfigFile';
import { loadExecMappingsFile } from './Execs';
import { ExtConfigFile } from './ExtConfigFile';
import { InstancedAbortController } from './InstancedAbortController';
import { ManagedChildProcess, onServiceChange } from './ManagedChildProcess';
import { PlaylistFile } from './PlaylistFile';
import { ServicesFile } from './ServicesFile';
import { SocketServer } from './SocketServer';
import { newThemeWatcher } from './Themes';
import { CONFIG_FILENAME, EXT_CONFIG_FILENAME, PREFERENCES_FILENAME, SERVICES_SOURCE } from './constants';
import { loadCurationIndexImage } from './curate/parse';
import { readCurationMeta } from './curate/read';
import { onFileServerRequestCurationFileFactory, onFileServerRequestPostCuration } from './curate/util';
import { loadExecMappingsFile } from './Execs';
import { ExtConfigFile } from './ExtConfigFile';
import { ApiEmitter } from './extensions/ApiEmitter';
import { ExtensionService } from './extensions/ExtensionService';
import {
FPLNodeModuleFactory,
INodeModuleFactory,
SqliteInterceptorFactory,
installNodeInterceptor,
registerInterceptor,
SqliteInterceptorFactory
registerInterceptor
} from './extensions/NodeInterceptor';
import { Command } from './extensions/types';
import * as GameManager from './game/GameManager';
import { onWillImportCuration } from './importGame';
import { InstancedAbortController } from './InstancedAbortController';
import { ManagedChildProcess, onServiceChange } from './ManagedChildProcess';
import { PlaylistFile } from './PlaylistFile';
import { registerRequestCallbacks } from './responses';
import { genContentTree } from './rust';
import { ServicesFile } from './ServicesFile';
import { SocketServer } from './SocketServer';
import { newThemeWatcher } from './Themes';
import { BackState, ImageDownloadItem } from './types';
import { EventQueue } from './util/EventQueue';
import { FileServer, serveFile } from './util/FileServer';
Expand All @@ -90,9 +94,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 { GDIndex1680813346696 } from '@database/migration/1680813346696-GDIndex';
import { MoveLaunchPath1681561150000 } from '@database/migration/1681561150000-MoveLaunchPath';
import { PrimaryPlatform1684673859425 } from '@database/migration/1684673859425-PrimaryPlatform';

const dataSourceOptions: DataSourceOptions = {
type: 'better-sqlite3',
Expand All @@ -101,7 +102,7 @@ const dataSourceOptions: DataSourceOptions = {
migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266,
SourceFileCount1612436426353, GameTagsStr1613571078561, GameDataParams1619885915109, RemoveSources1676712700000, RemovePlaylist1676713895000,
TagifyPlatform1677943090621, AddPlatformsRedundancyFieldToGame1677951346785, GDIndex1680813346696, MoveLaunchPath1681561150000,
PrimaryPlatform1684673859425
PrimaryPlatform1684673859425, PlayTime1687807237714
]
};
export let AppDataSource: DataSource = new DataSource(dataSourceOptions);
Expand Down
2 changes: 1 addition & 1 deletion src/back/playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export async function importPlaylist(state: BackState, filePath: string, library
state.socketServer.send(event.client, BackOut.IMPORT_PLAYLIST, newPlaylist);
}
} catch (e) {
console.log(e);
log.error('Launcher', `Error importing playlist: ${e}`);
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/back/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2363,12 +2363,26 @@ function runGameFactory(state: BackState) {
kill: true
}
);
if (proc.getState() === ProcessState.RUNNING) {
// Update game last played
GameManager.logGameStart(gameLaunchInfo.game.id);
} else {
proc.on('change', async () => {
if (proc.getState() === ProcessState.RUNNING) {
// Update game last played
await GameManager.logGameStart(gameLaunchInfo.game.id);
}
});
}
// Remove game service when it exits
proc.on('change', () => {
if (proc.getState() === ProcessState.STOPPED) {
// Update game playtime counter
GameManager.addGamePlaytime(gameLaunchInfo.game.id, Date.now() - proc.getStartTime());
removeService(state, proc.id);
}
});

return proc;
};
}
Expand Down
12 changes: 12 additions & 0 deletions src/database/entity/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ export class Game {
@OneToMany(() => GameData, datas => datas.game)
data?: GameData[];

/** Last Played Date */
@Column({ type: 'datetime' })
lastPlayed?: string;

/** Total Playtime (seconds) */
@Column({ default: 0 })
playtime: number;

/** Number of plays */
@Column({ default: 0 })
playCounter: number;

// This doesn't run... sometimes.
@BeforeUpdate()
updateTagsStr() {
Expand Down
16 changes: 16 additions & 0 deletions src/database/migration/1687807237714-PlayTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm"

export class PlayTime1687807237714 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "game" ADD COLUMN "lastPlayed" datetime`);
await queryRunner.query(`ALTER TABLE "game" ADD COLUMN "playtime" integer DEFAULT 0`);
await queryRunner.query(`ALTER TABLE "game" ADD COLUMN "playCounter" integer DEFAULT 0`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "game" DROP COLUMN "lastPlayed"`);
await queryRunner.query(`ALTER TABLE "game" DROP COLUMN "playtime"`);
await queryRunner.query(`ALTER TABLE "game" DROP COLUMN "playCounter"`);
}

}
15 changes: 15 additions & 0 deletions src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,21 @@ export class App extends React.Component<AppProps> {

window.Shared.back.register(BackOut.SERVICE_CHANGE, (event, data) => {
if (data.id) {
// Check if game just stopped, update to reflect time played changes if so
if (this.props.main.currentGame && data.state === ProcessState.STOPPED) {
if (data.id.startsWith('game.') && data.id.length > 5) {
const id = data.id.slice(5);
if (id === this.props.main.currentGame.id) {
// Reload game in sidebar
window.Shared.back.request(BackIn.GET_GAME, this.props.main.currentGame.id)
.then((game) => {
if (game && this.props.main.selectedGameId === game.id) {
this.props.setMainState({ currentGame: game });
}
});
}
}
}
const newServices = [...this.props.main.services];
const service = newServices.find(item => item.id === data.id);
if (service) {
Expand Down
89 changes: 88 additions & 1 deletion src/renderer/components/RightBrowseSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,27 @@ export class RightBrowseSidebar extends React.Component<RightBrowseSidebarProps,
</div>
)
}
{/** Gameplay Statistics */}
{ isPlaceholder || this.props.fpfssEditMode ? undefined : (
<div className='browse-right-sidebar__stats'>
<div className='browse-right-sidebar__stats-row'>
<div className='browse-right-sidebar__stats-row-left'>
{strings.lastPlayed}
</div>
<div className='browse-right-sidebar__stats-row-right'>
{game.lastPlayed ? formatLastPlayed(game.lastPlayed, strings) : strings.never}
</div>
</div>
<div className='browse-right-sidebar__stats-row'>
<div className='browse-right-sidebar__stats-row-left'>
{strings.playtime}
</div>
<div className='browse-browser-right-sidebarright-sidebar__stats-row-right'>
{formatPlaytime(game.playtime)}
</div>
</div>
</div>
)}
</div>
<div
ref={this.state.middleScrollRef}
Expand Down Expand Up @@ -691,7 +712,7 @@ export class RightBrowseSidebar extends React.Component<RightBrowseSidebarProps,
{/* -- Screenshot -- */}
<div className='browse-right-sidebar__section browse-right-sidebar__section--below-gap'>
<div className='browse-right-sidebar__row browse-right-sidebar__row__spacer' />
<div className='browse-right-sidebar__row'>
<div className='browse-right-sidebar__row browse-right-sidebar__row__screenshot-container'>
<div
className='browse-right-sidebar__row__screenshot'
onContextMenu={this.onScreenshotContextMenu}>
Expand Down Expand Up @@ -743,6 +764,10 @@ export class RightBrowseSidebar extends React.Component<RightBrowseSidebarProps,
src={screenshotSrc}
onCancel={this.onScreenshotPreviewClick} />
) : undefined }
</div>
)}
{ !this.props.fpfssEditMode && (
<div className='browse-right-sidebar__super-bottom'>
<SimpleButton
value={strings.openGameDataBrowser}
onClick={() => this.setState({ gameDataBrowserOpen: !this.state.gameDataBrowserOpen })}/>
Expand Down Expand Up @@ -1291,3 +1316,65 @@ function openContextMenu(template: MenuItemConstructorOptions[]): Menu {
menu.popup({ window: remote.getCurrentWindow() });
return menu;
}

/**
* Get a formatted last played string (Rounded to the nearest useful amount)
*
* @param lastPlayed Last Played Date
* @param strings localized strings
*/
function formatLastPlayed(lastPlayed: string, strings: any): string {
const secondsInDay = 60 * 60 * 24;
const lpdate = new Date(lastPlayed);
const diff = Math.ceil((Date.now() - lpdate.getTime()) / 1000);

if (diff < (secondsInDay * 2)) {
if ((new Date()).getDate() === lpdate.getDate()) {
return strings.today;
} else {
return strings.yesterday;
}
} else if (diff < (secondsInDay * 8)) {
return formatString(strings.daysAgo, Math.floor(diff / secondsInDay).toString()) as string;
} else if (diff < (secondsInDay * 7 * 4)) {
return formatString(strings.weeksAgo, Math.floor(diff / (secondsInDay * 7)).toString()) as string;
} else {
const ordinal = ordinalSuffixOf(lpdate.getDate());
const month = lpdate.toLocaleString('default', { month: 'long' });
return `${ordinal} ${month} ${lpdate.getFullYear()}`;
}
}

/**
* Get a formatted playtime string (Rounded to the nearest useful amount)
*
* @param playtime Seconds of playtime
*/
function formatPlaytime(playtime: number): string {
// Less than 1 minute
if (playtime <= 60) {
return `${playtime} Seconds`;
}
// Less than 2 hours
if (playtime <= (60 * 120)) {
return `${Math.floor(playtime / 60)} Minutes`;
} else {
return `${(playtime / (60 * 60)).toFixed(1)} Hours`;
}
}

// https://stackoverflow.com/questions/13627308/add-st-nd-rd-and-th-ordinal-suffix-to-a-number
function ordinalSuffixOf(i: number) {
const j = i % 10,
k = i % 100;
if (j == 1 && k != 11) {
return i + 'st';
}
if (j == 2 && k != 12) {
return i + 'nd';
}
if (j == 3 && k != 13) {
return i + 'rd';
}
return i + 'th';
}
10 changes: 10 additions & 0 deletions src/shared/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ const langTemplate = {
'noTitle',
'by',
'play',
'lastPlayed',
'playtime',
'playCount',
'never',
'today',
'yesterday',
'daysAgo',
'weeksAgo',
'stop',
'noDeveloper',
'alternateTitles',
Expand Down Expand Up @@ -694,6 +702,8 @@ export function getDefaultLocalization(): LangContainer {
lang.browse.dropGameOnLeft += ' {0}';
lang.browse.setFlashpointPathQuestion += ' {0} {1}';
lang.browse.noteSaveAndRestart += ' {0}';
lang.browse.daysAgo += ' {0}';
lang.browse.weeksAgo += ' {0}';
lang.misc.noBlankFound = ' {0} ' + lang.misc.noBlankFound;
lang.misc.addBlank += ' {0}';
lang.misc.deleteAllBlankImages += ' {0}';
Expand Down
Loading

0 comments on commit 3c366f1

Please sign in to comment.