From e6ba469ef9a8f84f3a2fbc5f373d6ad4fc3686ac Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 11 Jul 2026 17:26:59 +0200 Subject: [PATCH] Restore locked files if not all removed within 60 seconds Removing one of the 3 AI-virus-protected files now starts a 60-second window (tracked session-wide via LockedFilesRemovalDeadline). If the other locked files aren't also removed before it expires, the virus restores whichever ones were deleted and broadcasts a red warning to the whole session, forcing players to coordinate the removal instead of picking them off one at a time. Also extends the Mercure broadcast payload with an optional 3rd "messageType" element so pushed messages can render red (virus) or green (mainframe) instead of always defaulting to green. --- assets/game1.js | 8 +-- src/Game/Enum/SessionSettingType.php | 1 + src/Game/Service/GameResponseService.php | 74 ++++++++++++++++++++++++ src/Game/Service/PlayerService.php | 17 ++++++ 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/assets/game1.js b/assets/game1.js index 1c546d3..3f2e0f3 100644 --- a/assets/game1.js +++ b/assets/game1.js @@ -14,7 +14,7 @@ function subscribeToMercure(mercurePublicUrl, topic, myScreen) { const data = JSON.parse(event.data); console.log('[Mercure][game1] Update:', data); - // data is [sendTo, message] + // data is [sendTo, message, messageType?] - messageType defaults to 'mainframe' (green) if (Array.isArray(data) && data.length >= 2) { const sendTo = parseInt(data[0]); // Filter: 0 means everyone, otherwise must match myScreen @@ -25,11 +25,7 @@ function subscribeToMercure(mercurePublicUrl, topic, myScreen) { const messageContainer = document.getElementById('message-container'); if (messageContainer) { - const msgEl = document.createElement('div'); - msgEl.className = 'message'; - msgEl.textContent = data[1]; - msgEl.style.color = '#0F0'; // Green for incoming messages - messageContainer.appendChild(msgEl); + appendResultMessage(messageContainer, data[1], data[2] || 'mainframe'); window.scrollTo(0, document.body.scrollHeight); if(stillPlayingSound) playSound(); diff --git a/src/Game/Enum/SessionSettingType.php b/src/Game/Enum/SessionSettingType.php index acc3720..ece0938 100644 --- a/src/Game/Enum/SessionSettingType.php +++ b/src/Game/Enum/SessionSettingType.php @@ -84,4 +84,5 @@ enum SessionSettingType: string case LOCK_FOR_PLAYER8 = 'LockForPlayer8'; case LOCK_FOR_PLAYER9 = 'LockForPlayer9'; case LOCK_FOR_PLAYER10 = 'LockForPlayer10'; + case LOCKED_FILES_REMOVAL_DEADLINE = 'LockedFilesRemovalDeadline'; } diff --git a/src/Game/Service/GameResponseService.php b/src/Game/Service/GameResponseService.php index c243fdc..3ddddd9 100644 --- a/src/Game/Service/GameResponseService.php +++ b/src/Game/Service/GameResponseService.php @@ -69,6 +69,8 @@ class GameResponseService if(!$player) return ['error' => 'You are not in a game.']; + $this->enforceLockedFilesRemovalDeadline($player->getSession()); + $this->logSessionActivity($player, 'PLAYER: ' . $message); $data = $this->handleLockedPlayer($message, $player); @@ -228,6 +230,7 @@ class GameResponseService return ['result' => ['You are not allowed to remove this file.']]; $this->playerService->addDeletedFileToSession($player, $fullPath); + $this->startLockedFilesRemovalDeadline($player->getSession(), $fullPath); $lock = $this->triggerLock($player); return [ @@ -1026,6 +1029,77 @@ class GameResponseService ]; } + /** + * Starts (if not already running) the 60-second window within which all locked files + * must be removed, or the ones already removed get restored by the virus. + */ + private function startLockedFilesRemovalDeadline(Session $session, string $removedFile): void + { + if (!in_array($removedFile, $this->getLockedFiles())) { + return; + } + + $setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE); + if ($setting && $setting->getValue()) { + return; // Window already running + } + + if (!$setting) { + $setting = new SessionSetting(); + $setting->setSession($session); + $setting->setName(SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE); + } + + $setting->setValue((string)(time() + 60)); + $this->entityManager->persist($setting); + $this->entityManager->flush(); + } + + /** + * Lazily checked on every player interaction: if the 60-second window to remove all + * locked files has expired without all of them being removed, restore whichever ones + * were removed and clear the window. + */ + private function enforceLockedFilesRemovalDeadline(Session $session): void + { + $setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE); + if (!$setting || !$setting->getValue()) { + return; + } + + if (time() < (int)$setting->getValue()) { + return; + } + + $player = $session->getPlayers()->first() ?: null; + $lockedFiles = $this->getLockedFiles(); + + if ($player) { + $deletedFiles = $this->playerService->getDeletedFilesOfSession($player); + $stillLocked = array_intersect($lockedFiles, $deletedFiles); + + if (count($stillLocked) < count($lockedFiles)) { + foreach ($stillLocked as $file) { + $this->playerService->removeDeletedFileFromSession($player, $file); + } + + if (!empty($stillLocked)) { + $topic = '/game/hub/' . $session->getId(); + $message = 'AI VIRUS: Integrity check complete. Restored files that were not fully purged in time.'; + try { + $this->hub->publish(new Update($topic, json_encode([0, $message, 'virus']))); + } catch (\Exception $e) { + // Mercure might be down + } + } + } + } + + $setting->setValue(null); + $this->entityManager->persist($setting); + $this->entityManager->flush(); + } + private function fileExists(string $file, Player $player) : bool { $files = $this->getAllPossibleFiles($player); diff --git a/src/Game/Service/PlayerService.php b/src/Game/Service/PlayerService.php index 10fed4b..e7d6349 100644 --- a/src/Game/Service/PlayerService.php +++ b/src/Game/Service/PlayerService.php @@ -93,4 +93,21 @@ class PlayerService $this->entityManager->flush(); } } + + public function removeDeletedFileFromSession(Player $player, string $filename): void + { + $setting = $this->sessionSettingRepository->getSetting($player->getSession(), SessionSettingType::SET_OF_DELETED_FILES); + if (!$setting || !$setting->getValue()) { + return; + } + + $deletedFiles = json_decode($setting->getValue(), true) ?? []; + $newDeletedFiles = array_values(array_diff($deletedFiles, [$filename])); + + if (count($newDeletedFiles) !== count($deletedFiles)) { + $setting->setValue(json_encode($newDeletedFiles)); + $this->entityManager->persist($setting); + $this->entityManager->flush(); + } + } }