1, 'stats' => [ 'totalEvents' => 0, 'totalVisits' => 0, 'uniquePlayers' => 0, 'visitsToday' => 0, 'todayKey' => '', ], 'players' => [], 'events' => [], ]; } function player_log_read(): array { if (!is_file(PLAYER_LOG_FILE)) { return player_log_default(); } $raw = @file_get_contents(PLAYER_LOG_FILE); $j = json_decode($raw ?: '{}', true); if (!is_array($j)) { return player_log_default(); } return array_replace_recursive(player_log_default(), $j); } function player_log_write(array $data): bool { if (!is_dir(ADMIN_PRIVATE_DIR)) { if (!@mkdir(ADMIN_PRIVATE_DIR, 0750, true)) { return false; } } $tmp = PLAYER_LOG_FILE . '.tmp.' . bin2hex(random_bytes(4)); $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); if ($json === false) { return false; } if (file_put_contents($tmp, $json, LOCK_EX) === false) { return false; } if (!rename($tmp, PLAYER_LOG_FILE)) { @unlink($tmp); return false; } return true; } function player_log_today_key(): string { $tz = new DateTimeZone('Asia/Bangkok'); return (new DateTimeImmutable('now', $tz))->format('Y-m-d'); } function player_log_reset_today_if_needed(array &$log): void { $today = player_log_today_key(); if (($log['stats']['todayKey'] ?? '') !== $today) { $log['stats']['todayKey'] = $today; $log['stats']['visitsToday'] = 0; } } function player_log_allowed_events(): array { return [ 'login_guest' => true, 'login_google' => true, 'login_facebook' => true, 'lobby_enter' => true, 'room_enter' => true, 'game_enter' => true, 'quiz_battle_enter' => true, 'logout' => true, ]; } function player_log_sanitize_name(string $raw): string { $s = trim($raw); $s = preg_replace('/[\x00-\x1f\x7f]/u', '', $s) ?? ''; if (function_exists('mb_substr')) { return mb_substr($s, 0, 32); } return substr($s, 0, 32); } function player_log_sanitize_page(string $raw): string { $s = trim($raw); if (function_exists('mb_substr')) { return mb_substr($s, 0, 180); } return substr($s, 0, 180); } /** event ที่นับเป็น "การเข้าใช้งาน" */ function player_log_counts_as_visit(string $event): bool { return isset([ 'login_guest' => true, 'login_google' => true, 'login_facebook' => true, 'lobby_enter' => true, 'room_enter' => true, 'game_enter' => true, ][$event]); } function player_log_track( string $playerKey, string $event, string $displayName = '', string $loginType = 'guest', string $page = '', array $meta = [] ): array { $allowed = player_log_allowed_events(); if (!isset($allowed[$event])) { return ['ok' => false, 'error' => 'unknown event']; } if (!valid_player_key($playerKey)) { return ['ok' => false, 'error' => 'invalid playerKey']; } $lt = in_array($loginType, ['guest', 'google', 'facebook', 'email'], true) ? $loginType : 'guest'; $name = player_log_sanitize_name($displayName); $page = player_log_sanitize_page($page); $now = gmdate('c'); $nowTs = time(); $log = player_log_read(); player_log_reset_today_if_needed($log); if (!isset($log['players']) || !is_array($log['players'])) { $log['players'] = []; } if (!isset($log['events']) || !is_array($log['events'])) { $log['events'] = []; } $isNewPlayer = !isset($log['players'][$playerKey]); if ($isNewPlayer) { $log['players'][$playerKey] = [ 'playerKey' => $playerKey, 'displayName' => $name !== '' ? $name : 'ผู้เล่น', 'loginType' => $lt, 'visitCount' => 0, 'eventCount' => 0, 'firstSeenAt' => $now, 'lastSeenAt' => $now, 'lastVisitAt' => null, 'lastEvent' => $event, 'lastPage' => $page, ]; $log['stats']['uniquePlayers'] = count($log['players']); } $p = &$log['players'][$playerKey]; if ($name !== '') { $p['displayName'] = $name; } $p['loginType'] = $lt; $p['lastSeenAt'] = $now; $p['lastEvent'] = $event; if ($page !== '') { $p['lastPage'] = $page; } $p['eventCount'] = max(0, (int) ($p['eventCount'] ?? 0)) + 1; $countVisit = false; if (player_log_counts_as_visit($event)) { $lastVisitAt = isset($p['lastVisitAt']) ? strtotime((string) $p['lastVisitAt']) : false; if ($lastVisitAt === false || ($nowTs - $lastVisitAt) >= PLAYER_LOG_SESSION_GAP_SEC) { $countVisit = true; $p['visitCount'] = max(0, (int) ($p['visitCount'] ?? 0)) + 1; $p['lastVisitAt'] = $now; $log['stats']['totalVisits'] = max(0, (int) ($log['stats']['totalVisits'] ?? 0)) + 1; $log['stats']['visitsToday'] = max(0, (int) ($log['stats']['visitsToday'] ?? 0)) + 1; } } $log['stats']['totalEvents'] = max(0, (int) ($log['stats']['totalEvents'] ?? 0)) + 1; $ev = [ 'id' => bin2hex(random_bytes(8)), 'at' => $now, 'playerKey' => $playerKey, 'displayName' => (string) ($p['displayName'] ?? ''), 'loginType' => $lt, 'event' => $event, 'page' => $page, 'meta' => is_array($meta) ? $meta : [], 'countedVisit' => $countVisit, ]; array_unshift($log['events'], $ev); if (count($log['events']) > PLAYER_LOG_MAX_EVENTS) { $log['events'] = array_slice($log['events'], 0, PLAYER_LOG_MAX_EVENTS); } if (!player_log_write($log)) { return ['ok' => false, 'error' => 'บันทึก log ไม่สำเร็จ']; } return [ 'ok' => true, 'countedVisit' => $countVisit, 'visitCount' => (int) ($p['visitCount'] ?? 0), ]; } function player_log_public_summary(array $log): array { player_log_reset_today_if_needed($log); return [ 'totalEvents' => max(0, (int) ($log['stats']['totalEvents'] ?? 0)), 'totalVisits' => max(0, (int) ($log['stats']['totalVisits'] ?? 0)), 'uniquePlayers' => max(0, (int) ($log['stats']['uniquePlayers'] ?? count($log['players'] ?? []))), 'visitsToday' => max(0, (int) ($log['stats']['visitsToday'] ?? 0)), 'todayKey' => (string) ($log['stats']['todayKey'] ?? player_log_today_key()), 'eventCap' => PLAYER_LOG_MAX_EVENTS, 'storedEvents' => count($log['events'] ?? []), ]; } function player_log_players_list(array $log, string $search = '', int $limit = 200): array { $rows = []; foreach ($log['players'] ?? [] as $p) { if (!is_array($p)) { continue; } $key = (string) ($p['playerKey'] ?? ''); if ($key === '') { continue; } if ($search !== '') { $hay = strtolower($key . ' ' . ($p['displayName'] ?? '') . ' ' . ($p['loginType'] ?? '')); if (strpos($hay, strtolower($search)) === false) { continue; } } $rows[] = [ 'playerKey' => $key, 'displayName' => (string) ($p['displayName'] ?? 'ผู้เล่น'), 'loginType' => (string) ($p['loginType'] ?? 'guest'), 'visitCount' => max(0, (int) ($p['visitCount'] ?? 0)), 'eventCount' => max(0, (int) ($p['eventCount'] ?? 0)), 'firstSeenAt' => (string) ($p['firstSeenAt'] ?? ''), 'lastSeenAt' => (string) ($p['lastSeenAt'] ?? ''), 'lastVisitAt' => (string) ($p['lastVisitAt'] ?? ''), 'lastEvent' => (string) ($p['lastEvent'] ?? ''), 'lastPage' => (string) ($p['lastPage'] ?? ''), ]; } usort($rows, static function ($a, $b) { return strcmp((string) ($b['lastSeenAt'] ?? ''), (string) ($a['lastSeenAt'] ?? '')); }); if ($limit > 0 && count($rows) > $limit) { $rows = array_slice($rows, 0, $limit); } return $rows; } function player_log_events_list(array $log, array $opts = []): array { $search = trim((string) ($opts['search'] ?? '')); $event = trim((string) ($opts['event'] ?? '')); $playerKey = trim((string) ($opts['playerKey'] ?? '')); $limit = max(1, min(500, (int) ($opts['limit'] ?? 100))); $offset = max(0, (int) ($opts['offset'] ?? 0)); $rows = []; foreach ($log['events'] ?? [] as $ev) { if (!is_array($ev)) { continue; } if ($event !== '' && ($ev['event'] ?? '') !== $event) { continue; } if ($playerKey !== '' && ($ev['playerKey'] ?? '') !== $playerKey) { continue; } if ($search !== '') { $hay = strtolower( ($ev['playerKey'] ?? '') . ' ' . ($ev['displayName'] ?? '') . ' ' . ($ev['event'] ?? '') . ' ' . ($ev['page'] ?? '') ); if (strpos($hay, strtolower($search)) === false) { continue; } } $rows[] = $ev; } $total = count($rows); if ($offset > 0) { $rows = array_slice($rows, $offset); } if (count($rows) > $limit) { $rows = array_slice($rows, 0, $limit); } return ['rows' => $rows, 'total' => $total]; }