Files
Escapepage/src/Game/Service/GameDashboardService.php

455 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Game\Service;
use App\Game\Entity\Game;
use App\Game\Entity\Player;
use App\Game\Entity\Session;
use App\Game\Entity\SessionSetting;
use App\Game\Enum\GameStatus;
use App\Game\Enum\SessionSettingType;
use App\Game\Enum\SessionStatus;
use App\Game\Repository\GameRepository;
use App\Game\Repository\SessionRepository;
use App\Tech\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class GameDashboardService
{
public function __construct(
private readonly GameRepository $gameRepository,
private readonly SessionRepository $sessionRepository,
private readonly EntityManagerInterface $entityManager,
private readonly HubInterface $hub,
#[Autowire('%env(MERCURE_TOPIC_BASE)%')]
private readonly string $mercureTopicBase,
) {
}
/**
* @return Session[]
*/
public function getSessionsForUser(UserInterface $user): array
{
return $this->sessionRepository->createQueryBuilder('s')
->innerJoin('s.players', 'p')
->where('p.user = :user')
->setParameter('user', $user)
->getQuery()
->getResult();
}
/**
* @return Game[]
*/
public function getAvailableGames(bool $isAdmin): array
{
if ($isAdmin) {
return $this->gameRepository->findAll();
}
return $this->gameRepository->findBy(['status' => GameStatus::OPEN]);
}
public function createSession(Game $game, UserInterface $user, bool $isAdmin): ?Session
{
if ($game->getStatus() !== GameStatus::OPEN && !$isAdmin) {
return null;
}
if(!$user instanceof User)
return null;
$session = new Session();
$session->setGame($game);
$session->setStatus(SessionStatus::CREATED);
$session->setTimer(0);
$player = new Player();
$player->setUser($user);
$session->addPlayer($player);
$this->entityManager->persist($session);
$this->entityManager->persist($player);
$this->entityManager->flush();
if (count($session->getPlayers()) === $session->getGame()->getNumberOfPlayers()) {
$this->startSession($session);
}
return $session;
}
public function joinSession(string $inviteCode, UserInterface $user): bool
{
if (!$user instanceof User) {
return false;
}
$setting = $this->entityManager->getRepository(SessionSetting::class)->findOneBy([
'name' => SessionSettingType::INVITE_CODE,
'value' => $inviteCode,
]);
if (!$setting) {
return false;
}
$session = $setting->getSession();
// Check if user is already in this session
foreach ($session->getPlayers() as $player) {
if ($player->getUser() === $user) {
return true; // Already joined
}
}
$playerCount = count($session->getPlayers());
if ($playerCount >= $session->getGame()->getNumberOfPlayers()) {
return false; // Session full
}
$player = new Player();
$player->setUser($user);
$session->addPlayer($player);
$this->entityManager->persist($player);
$this->entityManager->flush();
if (count($session->getPlayers()) === $session->getGame()->getNumberOfPlayers()) {
$this->startSession($session);
}
return true;
}
public function leaveSession(Session $session, UserInterface $user): bool
{
if (!$user instanceof User) {
return false;
}
if (!in_array($session->getStatus(), [SessionStatus::CREATED, SessionStatus::READY]) || $session->getTimer() > 0) {
return false;
}
$playerToDelete = null;
foreach ($session->getPlayers() as $player) {
if ($player->getUser() === $user) {
$playerToDelete = $player;
break;
}
}
if (!$playerToDelete) {
return false;
}
if ($session->getStatus() === SessionStatus::READY) {
$session->setStatus(SessionStatus::CREATED);
// Clear assignments for all remaining players since the game needs to be "started" again
foreach ($session->getPlayers() as $player) {
$player->setScreen(null);
}
}
// Remove player specific settings (like rights)
foreach ($session->getSettings() as $setting) {
if ($setting->getPlayer() === $playerToDelete) {
$session->removeSetting($setting);
$this->entityManager->remove($setting);
}
}
$session->removePlayer($playerToDelete);
$this->entityManager->remove($playerToDelete);
// If no players left, we might want to delete the session and its remaining settings
if ($session->getPlayers()->isEmpty()) {
foreach ($session->getSettings() as $setting) {
$this->entityManager->remove($setting);
}
$this->entityManager->remove($session);
}
$this->entityManager->flush();
return true;
}
private function initializePlayerSettings(Player $player): void
{
$screen = $player->getScreen();
$rightsSettingName = SessionSettingType::tryFrom('RightsForPlayer' . $screen);
if ($rightsSettingName) {
$setting = new SessionSetting();
$setting->setSession($player->getSession());
$setting->setPlayer($player);
$setting->setName($rightsSettingName);
$setting->setValue(json_encode(['chat', 'help', 'ls', 'pwd']));
$this->entityManager->persist($setting);
}
$pwdSettingName = SessionSettingType::tryFrom('PwdForPlayer' . $screen);
if ($pwdSettingName) {
$setting = new SessionSetting();
$setting->setSession($player->getSession());
$setting->setPlayer($player);
$setting->setName($pwdSettingName);
$setting->setValue('var/home/' . $player->getUser()->getUsername());
$this->entityManager->persist($setting);
}
$chatTrackingSettingName = SessionSettingType::tryFrom('ChatTrackingForPlayer' . $screen);
if ($chatTrackingSettingName) {
$setting = new SessionSetting();
$setting->setSession($player->getSession());
$setting->setPlayer($player);
$setting->setName($chatTrackingSettingName);
$setting->setValue(json_encode([]));
$this->entityManager->persist($setting);
}
$verifyCodesSettingName = SessionSettingType::tryFrom('VerifyCodesForPlayer' . $screen);
if ($verifyCodesSettingName) {
$codes = [];
$numPlayers = $player->getSession()->getGame()->getNumberOfPlayers();
for ($i = 1; $i <= $numPlayers; $i++) {
if ($i !== $screen) {
$codes[$i] = bin2hex(random_bytes(3)); // 6 characters code
}
}
$setting = new SessionSetting();
$setting->setSession($player->getSession());
$setting->setPlayer($player);
$setting->setName($verifyCodesSettingName);
$setting->setValue(json_encode($codes));
$this->entityManager->persist($setting);
}
$verificationProgressSettingName = SessionSettingType::tryFrom('VerificationProgressForPlayer' . $screen);
if ($verificationProgressSettingName) {
$setting = new SessionSetting();
$setting->setSession($player->getSession());
$setting->setPlayer($player);
$setting->setName($verificationProgressSettingName);
$setting->setValue(json_encode([]));
$this->entityManager->persist($setting);
}
}
public function startSession(Session $session): bool
{
if ($session->getStatus() !== SessionStatus::CREATED) {
return false;
}
$players = $session->getPlayers()->toArray();
if (count($players) < $session->getGame()->getNumberOfPlayers()) {
return false;
}
// Clean up any existing assignments (e.g. if we reverted from READY to CREATED)
foreach ($session->getSettings() as $setting) {
if ($setting->getPlayer() !== null) {
$session->removeSetting($setting);
$this->entityManager->remove($setting);
}
}
// Shuffle players to assign random screens
shuffle($players);
foreach ($players as $index => $player) {
$screen = $index + 1;
$player->setScreen($screen);
$this->initializePlayerSettings($player);
$this->entityManager->persist($player);
}
$session->setStatus(SessionStatus::READY);
$this->entityManager->persist($session);
$this->entityManager->flush();
return true;
}
public function toggleReady(Session $session, User $user): bool
{
if ($session->getStatus() !== SessionStatus::READY) {
return false;
}
$player = null;
foreach ($session->getPlayers() as $p) {
if ($p->getUser() === $user) {
$player = $p;
break;
}
}
if (!$player) {
return false;
}
$settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
if (!$settingName) {
return false;
}
/** @var \App\Game\Repository\SessionSettingRepository $settingRepo */
$settingRepo = $this->entityManager->getRepository(SessionSetting::class);
$setting = $settingRepo->getSetting($session, $settingName, $player);
if ($setting) {
$session->removeSetting($setting);
$this->entityManager->remove($setting);
} else {
$setting = new SessionSetting();
$setting->setSession($session);
$setting->setPlayer($player);
$setting->setName($settingName);
$setting->setValue((string)(new \DateTime())->getTimestamp());
$this->entityManager->persist($setting);
}
$this->checkAllPlayersReady($session);
$this->entityManager->flush();
try {
$topic = $this->mercureTopicBase . '/game/hub-' . $session->getId();
$this->hub->publish(new Update($topic, json_encode(['type' => 'player_ready', 'player' => $player->getScreen(), 'ready' => !$setting])));
} catch (\Exception $e) {
// Mercure might be down, but we don't want to crash the game
}
return true;
}
public function checkAllPlayersReady(Session $session): void
{
if ($session->getStatus() !== SessionStatus::READY) {
return;
}
$players = $session->getPlayers();
$numPlayers = $session->getGame()->getNumberOfPlayers();
if (count($players) < $numPlayers) {
return;
}
$readyPlayersCount = 0;
$now = new \DateTime();
$anyReset = false;
/** @var \App\Game\Repository\SessionSettingRepository $settingRepo */
$settingRepo = $this->entityManager->getRepository(SessionSetting::class);
foreach ($players as $player) {
$settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
if (!$settingName) {
continue;
}
$setting = $settingRepo->getSetting($session, $settingName, $player);
if ($setting) {
$readyAtTimestamp = (int)$setting->getValue();
// Check timeout: 1 minute = 60 seconds
if (($now->getTimestamp() - $readyAtTimestamp) > 60) {
$session->removeSetting($setting);
$this->entityManager->remove($setting);
$anyReset = true;
} else {
$readyPlayersCount++;
}
}
}
if ($anyReset) {
$this->entityManager->flush();
try {
$topic = $this->mercureTopicBase . '/game/hub-' . $session->getId();
$this->hub->publish(new Update($topic, json_encode(['type' => 'player_ready'])));
} catch (\Exception $e) {
// Mercure might be down
}
}
if ($readyPlayersCount === $numPlayers) {
$session->setStatus(SessionStatus::PLAYING);
$this->entityManager->persist($session);
// Clean up ready settings
foreach ($players as $player) {
$settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
if ($settingName) {
$setting = $settingRepo->getSetting($session, $settingName, $player);
if ($setting) {
$session->removeSetting($setting);
$this->entityManager->remove($setting);
}
}
}
try {
$topic = $this->mercureTopicBase . '/game/hub-' . $session->getId();
$this->hub->publish(new Update($topic, json_encode(['type' => 'all_ready'])));
} catch (\Exception $e) {
// Mercure might be down, but we don't want to crash the game
}
}
}
public function generateInviteCode(Session $session, UserInterface $user, bool $isAdmin): ?string
{
// Security check: is user part of this session?
$isPlayer = false;
foreach ($session->getPlayers() as $player) {
if ($player->getUser() === $user) {
$isPlayer = true;
break;
}
}
if (!$isPlayer && !$isAdmin) {
return null;
}
$inviteCode = bin2hex(random_bytes(4));
$setting = null;
foreach ($session->getSettings() as $s) {
if ($s->getName() === SessionSettingType::INVITE_CODE) {
$setting = $s;
break;
}
}
if (!$setting) {
$setting = new SessionSetting();
$setting->setSession($session);
$setting->setName(SessionSettingType::INVITE_CODE);
}
$setting->setValue($inviteCode);
$this->entityManager->persist($setting);
$this->entityManager->flush();
return $inviteCode;
}
}