0, 'path' => '/intern/statistik/', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict', ]); session_start(); $configPath = __DIR__ . '/stats-config.local.php'; if (!is_file($configPath)) { http_response_code(503); exit('Die interne Statistik ist noch nicht konfiguriert.'); } $config = require $configPath; if (isset($_GET['logout'])) { $_SESSION = []; session_destroy(); header('Location: ./'); exit; } $loginError = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['password'])) { $userOk = hash_equals((string) $config['username'], (string) $_POST['username']); $passwordOk = hash_equals((string) $config['password_sha256'], hash('sha256', (string) $_POST['password'])); if ($userOk && $passwordOk) { session_regenerate_id(true); $_SESSION['authenticated'] = true; $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); header('Location: ./'); exit; } usleep(350000); $loginError = 'Anmeldung fehlgeschlagen.'; } function fetchJson(string $url, string $method = 'GET', ?string $body = null): ?array { if (function_exists('curl_init')) { $handle = curl_init($url); $options = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => 4, CURLOPT_TIMEOUT => 8, CURLOPT_USERAGENT => 'GerGames-Internal-Stats/1.0', CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json'], CURLOPT_CUSTOMREQUEST => $method, ]; if ($body !== null) { $options[CURLOPT_POSTFIELDS] = $body; } curl_setopt_array($handle, $options); $response = curl_exec($handle); $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); curl_close($handle); if (!is_string($response) || $status < 200 || $status >= 300) return null; } else { $context = stream_context_create(['http' => [ 'method' => $method, 'timeout' => 8, 'ignore_errors' => true, 'header' => "Accept: application/json\r\nContent-Type: application/json\r\nUser-Agent: GerGames-Internal-Stats/1.0\r\n", 'content' => $body ?? '', ]]); $response = @file_get_contents($url, false, $context); if (!is_string($response)) return null; } $decoded = json_decode($response, true); return is_array($decoded) ? $decoded : null; } function e(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } if (empty($_SESSION['authenticated'])): ?> GerGames – Interne Statistik

Interne Statistik

Geschützter GerGames-Bereich

$codeKey], JSON_THROW_ON_ERROR)); if (is_array($response) && isset($response['payload']) && is_string($response['payload'])) { $decodedPayload = json_decode($response['payload'], true); if (is_array($decodedPayload)) $response = $decodedPayload; } if (is_array($response) && !empty($response['accepted'])) { $_SESSION['promo_delete_flash'] = 'Der nicht eingelöste Code wurde gelöscht.'; header('Location: ./'); exit; } $reason = is_array($response) ? (string) ($response['error'] ?? '') : ''; $promoError = $reason === 'already_used' ? 'Dieser Code wurde inzwischen eingelöst und kann nicht gelöscht werden.' : ($reason === 'not_found' ? 'Der Code ist bereits gelöscht oder nicht mehr vorhanden.' : 'Der Code konnte nicht gelöscht werden. Bitte später erneut versuchen.'); } } elseif ($action === 'create') { $reward = (string) ($_POST['reward'] ?? ''); $maxUses = filter_var($_POST['max_uses'] ?? '1', FILTER_VALIDATE_INT); $note = trim((string) ($_POST['note'] ?? '')); $noteLength = function_exists('mb_strlen') ? mb_strlen($note, 'UTF-8') : strlen($note); $httpKey = trim((string) ($config['nakama_http_key'] ?? '')); $rpcBase = preg_replace('~/private_stats$~', '/create_unlock_code', (string) ($config['nakama_rpc_url'] ?? '')); if (!in_array($reward, ['all_towers', 'all_maps', 'all'], true)) { $promoError = 'Bitte wähle einen gültigen Freischaltumfang.'; } elseif ($maxUses === false || $maxUses < 1 || $maxUses > 50000) { $promoError = 'Die Anzahl der Einlösungen muss zwischen 1 und 50.000 liegen.'; } elseif ($noteLength > 160) { $promoError = 'Die Bemerkung darf höchstens 160 Zeichen lang sein.'; } elseif ($httpKey === '' || !is_string($rpcBase) || $rpcBase === (string) ($config['nakama_rpc_url'] ?? '')) { $promoError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.'; } else { $url = $rpcBase . '?http_key=' . rawurlencode($httpKey) . '&unwrap'; $response = fetchJson($url, 'POST', json_encode(['reward' => $reward, 'max_uses' => $maxUses, 'note' => $note], JSON_THROW_ON_ERROR)); if (is_array($response) && isset($response['payload']) && is_string($response['payload'])) { $decodedPayload = json_decode($response['payload'], true); if (is_array($decodedPayload)) $response = $decodedPayload; } if (is_array($response) && !empty($response['accepted']) && !empty($response['code'])) { $_SESSION['promo_flash'] = [ 'code' => (string) $response['code'], 'reward' => (string) ($response['reward'] ?? $reward), 'max_uses' => (int) ($response['max_uses'] ?? $maxUses), 'note' => (string) ($response['note'] ?? $note), ]; header('Location: ./'); exit; } $promoError = 'Der Server konnte keinen Code erstellen. Prüfe die Verbindung und versuche es erneut.'; } } else { $promoError = 'Unbekannte Code-Aktion.'; } } } $balanceError = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['balance_action'])) { if (!hash_equals((string) $_SESSION['csrf_token'], (string) ($_POST['csrf_token'] ?? ''))) { $balanceError = 'Die Sitzung ist abgelaufen. Bitte lade die Seite neu und versuche es erneut.'; } elseif ((string) $_POST['balance_action'] !== 'save') { $balanceError = 'Unbekannte Balance-Aktion.'; } else { $httpKey = trim((string) ($config['nakama_http_key'] ?? '')); $rpcBase = preg_replace('~/private_stats$~', '/set_game_balance', (string) ($config['nakama_rpc_url'] ?? '')); $towerForm = $_POST['towers'] ?? []; $enemyForm = $_POST['enemies'] ?? []; $balance = ['schema' => 2, 'towers' => [], 'enemies' => []]; for ($i = 0; $i < 7; $i++) { $row = is_array($towerForm[$i] ?? null) ? $towerForm[$i] : []; $balance['towers'][] = ['cost' => $row['cost'] ?? '', 'damage' => $row['damage'] ?? '', 'range' => $row['range'] ?? '', 'cooldown' => $row['cooldown'] ?? '', 'targetClass' => $row['targetClass'] ?? '', 'bothScale' => $row['bothScale'] ?? '']; } for ($i = 0; $i < 9; $i++) { $row = is_array($enemyForm[$i] ?? null) ? $enemyForm[$i] : []; $balance['enemies'][] = ['hp' => $row['hp'] ?? '', 'speed' => $row['speed'] ?? '', 'reward' => $row['reward'] ?? '', 'breachDamage' => $row['breachDamage'] ?? '', 'shieldResistance' => $row['shieldResistance'] ?? '']; } if ($httpKey === '' || !is_string($rpcBase) || $rpcBase === (string) ($config['nakama_rpc_url'] ?? '')) { $balanceError = 'Die Balance-Verwaltung ist noch nicht mit Nakama verbunden.'; } else { $url = $rpcBase . '?http_key=' . rawurlencode($httpKey) . '&unwrap'; $response = fetchJson($url, 'POST', json_encode(['config' => $balance], JSON_THROW_ON_ERROR)); if (is_array($response) && isset($response['payload']) && is_string($response['payload'])) { $decodedPayload = json_decode($response['payload'], true); if (is_array($decodedPayload)) $response = $decodedPayload; } if (is_array($response) && !empty($response['accepted'])) { $_SESSION['balance_flash'] = 'Balance gespeichert. Es wird beim nächsten Spielstart und in neuen Online-Partien verwendet.'; header('Location: ./#balance'); exit; } $balanceError = 'Der Server hat die Werte abgelehnt. Prüfe die Eingaben und ob der Runtime-Server aktualisiert wurde.'; } } } $balanceFlash = (string) ($_SESSION['balance_flash'] ?? ''); unset($_SESSION['balance_flash']); $balanceConfig = null; $balanceLoadError = ''; $httpKey = trim((string) ($config['nakama_http_key'] ?? '')); if ($httpKey !== '') { $balanceRpc = preg_replace('~/private_stats$~', '/get_game_balance', (string) ($config['nakama_rpc_url'] ?? '')); if (is_string($balanceRpc) && $balanceRpc !== (string) ($config['nakama_rpc_url'] ?? '')) { $balanceResponse = fetchJson($balanceRpc . '?http_key=' . rawurlencode($httpKey) . '&unwrap', 'POST', '{}'); if (is_array($balanceResponse) && isset($balanceResponse['payload']) && is_string($balanceResponse['payload'])) { $decodedPayload = json_decode($balanceResponse['payload'], true); if (is_array($decodedPayload)) $balanceResponse = $decodedPayload; } if (is_array($balanceResponse) && is_array($balanceResponse['config'] ?? null)) $balanceConfig = $balanceResponse['config']; else $balanceLoadError = 'Die aktuelle Balance konnte nicht geladen werden. Servermodul möglicherweise noch nicht aktualisiert.'; } } $releases = fetchJson((string) $config['gitea_releases_api']) ?? []; $downloadTotal = 0; $latestDownloads = 0; $latestRelease = 'Noch kein Release'; $platformDownloads = ['windows' => 0, 'android' => 0]; foreach ($releases as $releaseIndex => $release) { if (!is_array($release)) continue; if ($releaseIndex === 0) $latestRelease = (string) ($release['name'] ?? $release['tag_name'] ?? 'Aktuelles Release'); foreach (($release['assets'] ?? []) as $asset) { if (!is_array($asset)) continue; $count = max(0, (int) ($asset['download_count'] ?? 0)); $downloadTotal += $count; if ($releaseIndex === 0) $latestDownloads += $count; $name = strtolower((string) ($asset['name'] ?? '')); if (substr($name, -4) === '.exe') $platformDownloads['windows'] += $count; if (substr($name, -4) === '.apk') $platformDownloads['android'] += $count; } } $playerStats = null; $httpKey = trim((string) ($config['nakama_http_key'] ?? '')); if ($httpKey !== '') { $statsUrl = (string) $config['nakama_rpc_url'] . '?http_key=' . rawurlencode($httpKey) . '&unwrap'; $rpcResponse = fetchJson($statsUrl, 'POST', '{}'); if (is_array($rpcResponse)) { $playerStats = $rpcResponse; if (isset($rpcResponse['payload']) && is_string($rpcResponse['payload'])) { $payload = json_decode($rpcResponse['payload'], true); if (is_array($payload)) $playerStats = $payload; } } } $players = (int) ($playerStats['total_players'] ?? 0); $today = gmdate('Y-m-d'); $activeToday = (int) (($playerStats['daily_active'] ?? [])[$today] ?? 0); $updatedAt = isset($playerStats['updated_at']) ? date('d.m.Y H:i', intdiv((int) $playerStats['updated_at'], 1000)) : 'Noch keine Daten'; $promoCodes = null; $promoListError = ''; if ($httpKey !== '') { $listRpcUrl = preg_replace('~/private_stats$~', '/list_unlock_codes', (string) ($config['nakama_rpc_url'] ?? '')); if (is_string($listRpcUrl) && $listRpcUrl !== (string) ($config['nakama_rpc_url'] ?? '')) { $listUrl = $listRpcUrl . '?http_key=' . rawurlencode($httpKey) . '&unwrap'; $listResponse = fetchJson($listUrl, 'POST', '{}'); if (is_array($listResponse) && isset($listResponse['payload']) && is_string($listResponse['payload'])) { $decodedPayload = json_decode($listResponse['payload'], true); if (is_array($decodedPayload)) $listResponse = $decodedPayload; } if (is_array($listResponse) && isset($listResponse['codes']) && is_array($listResponse['codes'])) { $promoCodes = $listResponse['codes']; } else { $promoListError = 'Die Code-Übersicht konnte gerade nicht geladen werden.'; } } else { $promoListError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.'; } } else { $promoListError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.'; } $diagnosticRows = null; $diagnosticReports = []; $diagnosticError = ''; $diagnosticRound = strtolower(trim((string) ($_GET['round_id'] ?? ''))); if ($diagnosticRound !== '' && !preg_match('/^[a-f0-9]{16}$/D', $diagnosticRound)) $diagnosticRound = ''; if ($httpKey !== '') { $diagnosticRpc = preg_replace('~/private_stats$~', '/list_online_diagnostics', (string) ($config['nakama_rpc_url'] ?? '')); if (is_string($diagnosticRpc) && $diagnosticRpc !== (string) ($config['nakama_rpc_url'] ?? '')) { $diagnosticUrl = $diagnosticRpc . '?http_key=' . rawurlencode($httpKey) . '&unwrap'; $diagnosticResponse = fetchJson($diagnosticUrl, 'POST', '{}'); if (is_array($diagnosticResponse) && isset($diagnosticResponse['payload']) && is_string($diagnosticResponse['payload'])) { $decoded = json_decode($diagnosticResponse['payload'], true); if (is_array($decoded)) $diagnosticResponse = $decoded; } if (is_array($diagnosticResponse) && isset($diagnosticResponse['reports']) && is_array($diagnosticResponse['reports'])) { $diagnosticRows = $diagnosticResponse['reports']; if ($diagnosticRound !== '') { $detailResponse = fetchJson($diagnosticUrl, 'POST', json_encode(['round_id' => $diagnosticRound], JSON_THROW_ON_ERROR)); if (is_array($detailResponse) && isset($detailResponse['payload']) && is_string($detailResponse['payload'])) { $decoded = json_decode($detailResponse['payload'], true); if (is_array($decoded)) $detailResponse = $decoded; } if (is_array($detailResponse) && isset($detailResponse['reports']) && is_array($detailResponse['reports'])) { $diagnosticReports = $detailResponse['reports']; } else { $diagnosticError = 'Die ausgewählte Runde konnte nicht geladen werden.'; } } } else { $diagnosticError = 'Die Online-Diagnose konnte nicht geladen werden. Servermodul eventuell noch nicht aktualisiert.'; } } } ?> Void Bastion – Interne Statistik

GERGAMES · NICHT ÖFFENTLICH

Void Bastion Statistik

Abmelden
Downloads insgesamt
Downloads aktuelles Release
Registrierte Piloten
Heute aktive Piloten
Windows-Downloads
Android-Downloads

Die Downloadwerte stammen direkt aus den Gitea-Release-Anhängen und werden alle fünf Minuten neu geladen.

Die private Nakama-Statistik ist noch nicht mit dem HTTP-Schlüssel verbunden.

Letzte Spieleraktualisierung:

Diese Seite ist nicht verlinkt, von Suchmaschinen ausgeschlossen und verlangt eine Anmeldung. Spielerzahlen sind zusammengefasst; technische Rundenberichte sind nur hier einsehbar.

Online-Diagnose

Die beiden PCs senden ihre technischen Messwerte am Ende einer Online-Partie automatisch mit derselben Rundenkennung. Berichte sind maximal 14 Tage relevant; Namen und Raumcodes werden hier nicht gespeichert.

Noch keine Verbindung zur privaten Online-Diagnose.

Noch keine Berichte aus der neuen Spielversion vorhanden.

RundeSeiteZeitpunktVersionMessungen

Runde

Für diese Kennung liegen noch keine Berichte vor.

Seite ·

UTCSpielstände/sAbstand p95 / maxTick-SprüngeWarteschlange verworfenBildaussetzerGegner / Türme
/ ms (max ms) /
Einzelereignisse anzeigen
UTCEreignisDauerServer-Tick
ms

Zur Übersicht

Einheiten-Balancing

Eine gemeinsame serverseitige Konfiguration wird für Offline-Spiele beim nächsten Start geladen und in neuen Online-Partien direkt vom Server verwendet. Bei fehlender Verbindung nutzt das Spiel den letzten lokal gespeicherten Stand. Laufende Partien bleiben unverändert.

Türme

$name): $row = $balanceConfig['towers'][$i] ?? []; ?> 'Kosten','damage' => 'Schaden','range' => 'Reichweite','cooldown' => 'Sek./Schuss'] as $field => $label): ?>
TurmKostenSchadenReichweiteSek./SchussZielklasseMehrzweck-Faktor

Gegner

$name): $row = $balanceConfig['enemies'][$i] ?? []; ?> 'HP','speed' => 'Geschwindigkeit','reward' => 'Belohnung','breachDamage' => 'Reaktorschaden','shieldResistance' => 'Schild-Widerstand'] as $field => $label): ?>
EinheitHülle / HPGeschwindigkeitBelohnungReaktorschadenSchild-Widerstand

Das Balancing-Formular wird angezeigt, sobald der Runtime-Server die Balance-Funktionen bereitstellt.

Freischaltcodes erstellen

Codes und Bemerkungen werden ausschließlich in der geschützten Serverablage verwaltet. Der Klartext ist nur in diesem angemeldeten Bereich sichtbar.

Code erstellt: · offen · maximal Einlösungen ·

Code-Übersicht

Noch keine Freischaltcodes vorhanden.

= $maxUses ? 'redeemed' : ($uses > 0 ? 'partial' : 'open'); $statusLabel = $status === 'redeemed' ? 'Eingelöst' : ($status === 'partial' ? 'Teilweise eingelöst' : 'Offen'); $createdAt = (int) ($promoCode['created_at'] ?? 0); $createdLabel = $createdAt > 0 ? date('d.m.Y H:i', intdiv($createdAt, 1000)) : '—'; $codeLabel = (string) ($promoCode['code'] ?? ''); if ($codeLabel === '') $codeLabel = 'Alter Code – Klartext nicht gespeichert'; $codeKey = strtolower((string) ($promoCode['code_key'] ?? '')); ?>
CodeFreischaltungStatusEinlösungenErstelltBemerkungAktion
/
—