455 lines
15 KiB
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;
|
|
}
|
|
}
|