Skip to content

Commit

Permalink
add user game activity page (#1280)
Browse files Browse the repository at this point in the history
* add usergameactivity.phhp

* address analysis warnings
  • Loading branch information
Jamiras authored Jan 6, 2023
1 parent 6efc070 commit 924b072
Show file tree
Hide file tree
Showing 4 changed files with 329 additions and 0 deletions.
208 changes: 208 additions & 0 deletions lib/database/user-activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Legacy\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use RA\AchievementType;
use RA\ActivityType;
use RA\ArticleType;
use RA\Permissions;
Expand Down Expand Up @@ -711,3 +712,210 @@ function GetMostPopularTitles($daysRange = 7, $offset = 0, $count = 10): array

return $data;
}

function getUserGameActivity(string $user, int $gameID): array
{
sanitize_sql_inputs($user);

$query = "SELECT a.timestamp, a.lastupdate, a.data
FROM Activity a
WHERE a.User='$user' AND a.data=$gameID
AND a.activitytype=" . ActivityType::StartedPlaying;
$dbResult = s_mysql_query($query);
if ($dbResult === false) {
log_sql_fail();

return [];
}

$sessions = [];
while ($row = mysqli_fetch_assoc($dbResult)) {
$sessions[] = [
'StartTime' => strtotime($row['timestamp']),
];

if ($row['lastupdate'] != $row['timestamp']) {
$sessions[] = [
'StartTime' => strtotime($row['lastupdate']),
];
}
}

// create a dummy placeholder session for any achievements unlocked before the first session
$sessions[] = [
'StartTime' => 0,
'IsGenerated' => true,
];

// reverse sort by date so we can update the appropriate session when we find it
usort($sessions, function ($a, $b) { return $b['StartTime'] - $a['StartTime']; });

$query = "SELECT a.timestamp, a.data, a.data2, ach.Title, ach.Points, ach.BadgeName, ach.Flags
FROM Activity a
LEFT JOIN Achievements ach ON ach.ID = a.data
WHERE ach.GameID=$gameID AND a.User='$user'
AND a.activitytype=" . ActivityType::EarnedAchievement;
$dbResult = s_mysql_query($query);
if ($dbResult === false) {
log_sql_fail();

return [];
}

$achievements = [];
$unofficialAchievements = [];
while ($row = mysqli_fetch_assoc($dbResult)) {
$when = strtotime($row['timestamp']);
$achievements[$row['data']] = $when;

if ($row['Flags'] != AchievementType::OfficialCore) {
$unofficialAchievements[$row['data']] = 1;
}

foreach ($sessions as &$session) {
if ($session['StartTime'] < $when) {
$session['Achievements'][] = [
'When' => $when,
'AchievementID' => $row['data'],
'Title' => $row['Title'],
'Points' => $row['Points'],
'BadgeName' => $row['BadgeName'],
'Flags' => $row['Flags'],
'HardcoreMode' => $row['data2'],
];
break;
}
}
}

// calculate the duration of each session
$totalTime = _updateUserGameSessionDurations($sessions, $achievements);

// sort everything and find the first and last achievement timestamps
usort($sessions, function ($a, $b) { return $a['StartTime'] - $b['StartTime']; });

$unlockSessionCount = 0;
$firstAchievementTime = null;
$lastAchievementTime = null;
foreach ($sessions as &$session) {
if (!empty($session['Achievements'])) {
$unlockSessionCount++;
foreach ($session['Achievements'] as &$achievement) {
if ($firstAchievementTime === null) {
$firstAchievementTime = $achievement['When'];
}
$lastAchievementTime = $achievement['When'];
}
}
}

// assume every achievement took roughly the same amount of time to earn. divide the
// user's total known playtime by the number of achievements they've earned to get the
// approximate time per achievement earned. add this value to each session to account
// for time played after getting the last achievement of the session.
$achievementsUnlocked = count($achievements);
if ($achievementsUnlocked > 0 && $unlockSessionCount > 1) {
$sessionAdjustment = $totalTime / $achievementsUnlocked;
$totalTime += $sessionAdjustment * $unlockSessionCount;
} else {
$sessionAdjustment = 0;
}

$activity = [
'Sessions' => $sessions,
'TotalTime' => $totalTime,
'PerSessionAdjustment' => $sessionAdjustment,
'AchievementsUnlocked' => count($achievements) - count($unofficialAchievements),
'UnlockSessionCount' => $unlockSessionCount,
'FirstUnlockTime' => $firstAchievementTime,
'LastUnlockTime' => $lastAchievementTime,
'TotalUnlockTime' => ($lastAchievementTime != null) ? $lastAchievementTime - $firstAchievementTime : 0,
];

// Count num possible achievements
$query = "SELECT COUNT(*) as Count FROM Achievements ach
WHERE ach.Flags=" . AchievementType::OfficialCore . " AND ach.GameID=$gameID";
$dbResult = s_mysql_query($query);
if ($dbResult) {
$activity['CoreAchievementCount'] = mysqli_fetch_assoc($dbResult)['Count'];
}

return $activity;
}

function _updateUserGameSessionDurations(array &$sessions, array &$achievements): int
{
$totalTime = 0;
$newSessions = [];
foreach ($sessions as &$session) {
if (!array_key_exists('Achievements', $session)) {
if ($session['StartTime'] > 0) {
$session['Achievements'] = [];
$session['EndTime'] = $session['StartTime'];
$newSessions[] = $session;
}
} else {
usort($session['Achievements'], function ($a, $b) { return $a['When'] - $b['When']; });

if ($session['StartTime'] === 0) {
$session['StartTime'] = $session['Achievements'][0]['When'];
}

foreach ($session['Achievements'] as &$achievement) {
if ($achievement['When'] != $achievements[$achievement['AchievementID']]) {
$achievement['UnlockedLater'] = true;
}
}

// if there are any gaps in the achievements earned within a session that
// are more than four hours apart, split into separate sessions
$split = [];
$prevTime = $session['StartTime'];
for ($i = 0; $i < count($session['Achievements']); $i++) {
$distance = $session['Achievements'][$i]['When'] - $prevTime;
if ($distance > 4 * 60 * 60) {
$split[] = $i;
}
$prevTime = $session['Achievements'][$i]['When'];
}

if (empty($split)) {
$session['EndTime'] = end($session['Achievements'])['When'];
$totalTime += ($session['EndTime'] - $session['StartTime']);
$newSessions[] = $session;
} else {
$split[] = count($session['Achievements']);
$firstIndex = 0;
$isGenerated = false;
foreach ($split as $i) {
if ($i === 0) {
$newSession = [
'StartTime' => $session['StartTime'],
'EndTime' => $session['StartTime'],
'Achievements' => [],
];
} else {
$newSession = [
'StartTime' => !$isGenerated ? $session['StartTime'] :
$session['Achievements'][$firstIndex]['When'],
'EndTime' => $session['Achievements'][$i - 1]['When'],
'Achievements' => array_slice($session['Achievements'], $firstIndex, $i - $firstIndex),
];
}

$newSession['IsGenerated'] = $isGenerated;
$isGenerated = true;

$totalTime += ($newSession['EndTime'] - $newSession['StartTime']);
$newSessions[] = $newSession;

$firstIndex = $i;
}
}
}
}

$sessions = $newSessions;

return $totalTime;
}
5 changes: 5 additions & 0 deletions lib/util/date.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

function formatHMS(int $seconds): string
{
return sprintf("%d:%02d:%02d", $seconds / 3600, ($seconds / 60) % 60, $seconds % 60);
}

function getNiceTime($timestamp, $locale = 'EN-GB'): string
{
setlocale(LC_ALL, $locale);
Expand Down
10 changes: 10 additions & 0 deletions public/gamecompare.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@
echo "<br>";
}

if ($permissions >= Permissions::Admin) {
echo "<div class='devbox mb-3'>";
echo "<span onclick=\"$('#devboxcontent').toggle(); return false;\">Admin ▼</span>";
echo "<div id='devboxcontent' style='display: none'>";

echo "<div><a class='btn btn-link' href='/usergameactivity.php?ID=$gameID&f=$user2'>View User Game Activity</a></div>";

echo "</div></div>";
}

echo "There are <b>$numAchievements</b> achievements worth <b>$totalPossible</b> points.<br>";

$iconSize = 48;
Expand Down
106 changes: 106 additions & 0 deletions public/usergameactivity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

use RA\AchievementType;
use RA\Permissions;

if (!authenticateFromCookie($user, $permissions, $userDetails, Permissions::Admin)) {
abort(401);
}

$gameID = requestInputSanitized('ID', null, 'integer');
$user2 = requestInputSanitized('f');

$gameData = getGameData($gameID);
$gameTitle = $gameData['Title'];
$consoleID = $gameData['ConsoleID'];
$consoleName = $gameData['ConsoleName'];

$activity = getUserGameActivity($user2, $gameID);
$estimated = ($activity['PerSessionAdjustment'] !== 0) ? " (estimated)" : "";

$unlockSessionCount = $activity['UnlockSessionCount'];
$sessionInfo = "$unlockSessionCount session";
if ($unlockSessionCount != 1) {
$sessionInfo .= 's';

if ($unlockSessionCount > 1) {
$elapsedAchievementDays = ceil($activity['TotalUnlockTime'] / (24 * 60 * 60));
if ($elapsedAchievementDays > 2) {
$sessionInfo .= " over $elapsedAchievementDays days";
} else {
$sessionInfo .= " over " . ceil($activity['TotalUnlockTime'] / (60 * 60)) . " hours";
}
}
}

$gameAchievementCount = $activity['CoreAchievementCount'] ?? 0;
$userProgress = ($gameAchievementCount > 0) ? sprintf("/%d (%01.2f%%)",
$gameAchievementCount, $activity['AchievementsUnlocked'] * 100 / $gameAchievementCount) : "n/a";

RenderContentStart("$user2's activity for $gameTitle");
?>
<div id="mainpage">
<div id="leftcontainer">
<div id="gamecompare">
<?php
echo "<div class='navpath'>";
echo "<a href='/gameList.php'>All Games</a>";
echo " &raquo; <a href='/gameList.php?c=$consoleID'>$consoleName</a>";
echo " &raquo; <a href='/game/$gameID'>$gameTitle</a>";
echo " &raquo; <b>$user2</b>";
echo "</div>";

echo "<h3>$gameTitle</h3>";

$pageTitleAttr = attributeEscape($gameTitle);
$imageIcon = media_asset($gameData['ImageIcon']);

echo "<div class='sm:flex justify-between items-start gap-3 mb-3'>";
echo "<img class='aspect-1 object-cover' src='$imageIcon' width='96' height='96' alt='$pageTitleAttr'>";
echo "<table><colgroup><col class='w-48'></colgroup><tbody>";
echo "<tr><td>User:</td><td>" . userAvatar($user2, icon: false) . "</td></tr>";
echo "<tr><td>Total Playtime:</td><td>" . formatHMS($activity['TotalTime']) . "$estimated</td></tr>";
echo "<tr><td>Achievement Sessions:</td><td>$sessionInfo</td></tr>";
echo "<tr><td>Achievements Unlocked:</td><td>" . $activity['AchievementsUnlocked'] . "$userProgress</td></tr>";
echo "</tbody></table>";
echo "</div>";

echo "<div id='activity'>";
echo "<table>";
echo "<tr><th style='width: 20'></th><th style='width: 250'></th><th></th></tr>";

foreach ($activity['Sessions'] as $session) {
$startDate = getNiceDate($session['StartTime']);
if ($session['IsGenerated'] ?? false) {
echo "<tr><td colspan=2>$startDate</td><td>Generated Session</td></tr>";
} else {
echo "<tr><td colspan=2>$startDate</td><td>Started Playing</td></tr>";
}

$prevWhen = $session['StartTime'];
foreach ($session['Achievements'] as $achievement) {
$when = getNiceDate($achievement['When']);
$formatted = formatHMS($achievement['When'] - $prevWhen);
$prevWhen = $achievement['When'];

echo "<tr><td>&nbsp;</td><td>$when<span class='smalltext text-muted'> (+$formatted)</span></td><td>";
echo achievementAvatar($achievement);

if ($achievement['Flags'] != AchievementType::OfficialCore) {
echo " (Unofficial)";
}

if ($achievement['UnlockedLater'] ?? false) {
echo " (unlocked again later)";
}

echo "</td></tr>";
}
}

echo "</table></div>";
?>
</div>
</div>
</div>
<?php RenderContentEnd(); ?>

0 comments on commit 924b072

Please sign in to comment.