diff --git a/assets/game1.js b/assets/game1.js index 3f2e0f3..b4c7952 100644 --- a/assets/game1.js +++ b/assets/game1.js @@ -177,6 +177,42 @@ function applyLock(lockData, apiEchoUrl, messageContainer) { }, 1000); } +let filesRemovalTimer = null; +let scheduledFilesRemovalDeadline = null; + +async function pingFilesRemovalDeadline(apiEchoUrl) { + if (!apiEchoUrl) return; + try { + // A no-op message is enough to make the server evaluate the deadline server-side; + // the actual restore notice (if any) arrives for everyone via the Mercure broadcast. + await fetchJson(apiEchoUrl, { + method: 'POST', + body: { message: '', ts: new Date().toISOString() }, + }); + } catch (e) { + console.error('[Game1] Failed to ping files-removal deadline:', e); + } +} + +function scheduleFilesRemovalCheck(deadline, apiEchoUrl) { + if (!deadline || scheduledFilesRemovalDeadline === deadline) { + return; // nothing to (re)schedule + } + scheduledFilesRemovalDeadline = deadline; + + if (filesRemovalTimer) { + clearTimeout(filesRemovalTimer); + filesRemovalTimer = null; + } + + const delay = Math.max(0, deadline * 1000 - Date.now()); + filesRemovalTimer = setTimeout(() => { + filesRemovalTimer = null; + scheduledFilesRemovalDeadline = null; + pingFilesRemovalDeadline(apiEchoUrl); + }, delay); +} + async function fetchJson(url, options = {}) { const opts = { ...options }; const headers = new Headers(opts.headers || {}); @@ -236,6 +272,12 @@ document.addEventListener('DOMContentLoaded', async () => { const lockLockedAt = cfgEl.dataset.lockLockedAt; const lockRevealAt = cfgEl.dataset.lockRevealAt; const lockUnlockAt = cfgEl.dataset.lockUnlockAt; + const filesRemovalDeadline = cfgEl.dataset.filesRemovalDeadline; + + // Resume the auto-restore timer after a page refresh, if a window is already running + if (filesRemovalDeadline) { + scheduleFilesRemovalCheck(parseInt(filesRemovalDeadline, 10), apiEchoUrl); + } if (mercurePublicUrl && topic) { subscribeToMercure(mercurePublicUrl, topic, screen); @@ -391,6 +433,10 @@ document.addEventListener('DOMContentLoaded', async () => { } else if (response.result.locked === false) { clearLock(); } + + if (response.result.filesRemovalDeadline) { + scheduleFilesRemovalCheck(response.result.filesRemovalDeadline, apiEchoUrl); + } } } catch (err) { console.error('[API][game1] Failed to send message:', err); diff --git a/src/Game/Controller/GameController.php b/src/Game/Controller/GameController.php index cc07c06..c104677 100644 --- a/src/Game/Controller/GameController.php +++ b/src/Game/Controller/GameController.php @@ -160,12 +160,14 @@ final class GameController extends AbstractController $screen = $player ? $player->getScreen() : 0; $session_id = $session->getId(); $lock = $player ? $gameResponseService->getPublicLockState($player) : null; + $filesRemovalDeadline = $gameResponseService->getPublicLockedFilesDeadline($session); return $this->render('game/index.html.twig', [ 'session' => $session, 'screen' => $screen, 'session_id' => $session_id, 'lock' => $lock, + 'filesRemovalDeadline' => $filesRemovalDeadline, ]); } diff --git a/src/Game/Service/GameResponseService.php b/src/Game/Service/GameResponseService.php index 3ddddd9..2899782 100644 --- a/src/Game/Service/GameResponseService.php +++ b/src/Game/Service/GameResponseService.php @@ -230,7 +230,7 @@ class GameResponseService return ['result' => ['You are not allowed to remove this file.']]; $this->playerService->addDeletedFileToSession($player, $fullPath); - $this->startLockedFilesRemovalDeadline($player->getSession(), $fullPath); + $filesRemovalDeadline = $this->startLockedFilesRemovalDeadline($player->getSession(), $fullPath); $lock = $this->triggerLock($player); return [ @@ -243,6 +243,7 @@ class GameResponseService 'lockedAt' => $lock['lockedAt'], 'revealAt' => $lock['revealAt'], 'unlockAt' => $lock['unlockAt'], + 'filesRemovalDeadline' => $filesRemovalDeadline, ]; case 'sudo': if(!in_array('sudo', $rechten)) @@ -1031,17 +1032,19 @@ 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. + * must be removed, or the ones already removed get restored by the virus. Returns the + * (new or already-running) deadline as a unix timestamp, or null if the removed file + * wasn't a locked one. */ - private function startLockedFilesRemovalDeadline(Session $session, string $removedFile): void + private function startLockedFilesRemovalDeadline(Session $session, string $removedFile): ?int { if (!in_array($removedFile, $this->getLockedFiles())) { - return; + return null; } $setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE); if ($setting && $setting->getValue()) { - return; // Window already running + return (int)$setting->getValue(); // Window already running } if (!$setting) { @@ -1050,9 +1053,28 @@ class GameResponseService $setting->setName(SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE); } - $setting->setValue((string)(time() + 60)); + $deadline = time() + 60; + $setting->setValue((string)$deadline); $this->entityManager->persist($setting); $this->entityManager->flush(); + + return $deadline; + } + + /** + * Public, session-wide view of the active locked-files removal deadline (if any), safe + * to expose on page load so the UI can schedule its auto-check timer after a refresh. + */ + public function getPublicLockedFilesDeadline(Session $session): ?int + { + $setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE); + if (!$setting || !$setting->getValue()) { + return null; + } + + $deadline = (int)$setting->getValue(); + + return $deadline > time() ? $deadline : null; } /** diff --git a/templates/game/index.html.twig b/templates/game/index.html.twig index 0f31c04..b37268a 100644 --- a/templates/game/index.html.twig +++ b/templates/game/index.html.twig @@ -26,6 +26,9 @@ data-lock-reveal-at="{{ lock.revealAt }}" data-lock-unlock-at="{{ lock.unlockAt }}" {% endif %} + {% if filesRemovalDeadline %} + data-files-removal-deadline="{{ filesRemovalDeadline }}" + {% endif %} style="display:none">