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; } }