- Game1 terminal: flesh out the virtual filesystem with a realistic
spread of Linux directories/files (~70 dirs, ~140 files) so it no
longer reads as an obviously small puzzle set, without touching any
win-condition or rapport files.
- Add app:hints:check command + a php-cron container (BusyBox crond)
that nudges players who haven't contacted every teammate 5 minutes
into a session, via a new 'hint' Mercure message type.
- Log the cron command's output to var/log/cron/cron.log and rotate
it (25MB / 90 days) via logrotate, run daily from the same crontab.
- Redirect PHP's error_log and Symfony's prod app/deprecation logs
from stderr-only into var/log/php/*.log (kept alongside stderr),
with the same rotation policy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removing all 3 locked files while the timer is still running now marks
the session WON immediately (checked right after every successful rm)
and broadcasts a "game_finished" signal over Mercure so every
connected player gets redirected together, not just the one who
removed the last file. A new /won/{session} route + won.html.twig
mirrors the existing lost flow (victory narrative + the same feedback
form).
The existing timer-expiry path already set LOST but always redirected
to lostUrl regardless of actual status; it now picks won/lost based on
the status the server reports.
Also fixes a pre-existing bug on the lost page (and would-be bug on
the new won page): PlayerService::GetCurrentlyActiveAsPlayer() only
matches players in READY/PLAYING sessions, so by the time a session
has ended it always returned null there, silently breaking the
feedback form. Both pages now look the player up directly via
PlayerRepository instead.
Added a navigatingAway flag so the page's "confirm before leaving"
prompt doesn't block our own win/lose redirects.
Previously the restore check only ran lazily on a player's next
message, so files could stay wrongly-removed for an arbitrary amount
of time after the window expired. The frontend now schedules a
setTimeout (using the server-provided deadline, mirroring the terminal
lock's reveal timer) that pings the backend right at the deadline so
the check runs promptly regardless of player activity. Restored via
data-files-removal-deadline on page load too, so a refresh mid-window
doesn't lose the timer.
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.
Connects the previously-disconnected /decode command to per-player
messages: player 1 unlocks sudo for everyone, player 2 unlocks a scan
command that reveals which files the AI virus has locked, player 3
learns those files should be removed.
Also adds a retaliation mechanic: successfully removing a file locks
the player's terminal. The AI virus locks it out in red immediately;
the mainframe reveals a 12-character recovery code in green after 30
seconds, which can be submitted via /unlock to restore access early,
otherwise the terminal auto-recovers after 45 seconds.
checkIfAllPlayersVerified() read and wrote the EveryoneVerified
setting scoped to whichever player happened to trigger the check
(getSetting(..., $player) / setPlayer($player)), but it's meant to be
a single session-wide flag. getFileContent() correctly reads it as
session-global (no player, filters player IS NULL), so the two never
matched: the "everyone verified" Mercure message still fired (that
code path only checks its own player-scoped copy), but the special
code injection into the Doyle/Vega/Lennox report files never ran
since getFileContent() never found the setting. Store and read it
consistently as session-global.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getFileContent() built the game1 filesystem path as a relative
string with no leading slash. That only resolves correctly when
PHP's cwd happens to be the project root (e.g. CLI), but under
PHP-FPM/nginx the cwd is nginx's document root (public/), so
file_exists() always failed for real requests even though the file
existed and the in-memory virtual file list (used by ls) said it
should. Use the already-injected $projectDir (%kernel.project_dir%),
matching how the class already builds the session log path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>