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.
This commit is contained in:
Frank
2026-07-11 17:26:59 +02:00
parent f6a0d62017
commit e6ba469ef9
4 changed files with 94 additions and 6 deletions
+2 -6
View File
@@ -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();
+1
View File
@@ -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';
}
+74
View File
@@ -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);
+17
View File
@@ -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();
}
}
}