<?php
namespace App\Controller;
use App\Entity\Area;
use App\Entity\PveAreaTicker;
use App\Entity\PvpAreaTicker;
use App\Entity\User;
use App\Form\Type\LoginType;
use App\Model\AreaService;
use App\Model\DiscordTriggerService;
use App\Model\Game\GameUserService;
use App\Model\GameService;
use App\Model\Helper\GameHelper;
use App\Model\UserService;
use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\RouterInterface;
/**
* @Route("/game")
*/
class GameController extends AbstractController
{
/**
* @var AreaService
*/
private $gameService;
/**
* @var GameHelper
*/
private $gameHelper;
/**
* @var GameUserService
*/
private $userService;
/**
* @var DiscordTriggerService
*/
private $discordTriggerService;
public function __construct(GameService $gameService, GameHelper $gameHelper, GameUserService $userService, DiscordTriggerService $discordTriggerService)
{
$this->gameService = $gameService;
$this->gameHelper = $gameHelper;
$this->userService = $userService;
$this->discordTriggerService = $discordTriggerService;
}
/**
* @Route("", name="game_index")
*
* @return Response
*/
public function index(): Response
{
/** @var User $user */
$user = $this->getUser();
if (!$user) {
$form = $this->createForm(LoginType::class);
return $this->render('game/login.html.twig', [
'form' => $form->createView(),
'page' => 'game',
]);
}
if (!$user->hasArea()) {
return $this->redirectToRoute('area_index');
}
return $this->redirectToRoute('game_townhall');
}
/**
* @Route("/hold", name="game_tick_hold")
*
* @param Request $request
* @param RouterInterface $router
*
* @return Response
*/
public function tickHold(Request $request, RouterInterface $router): Response
{
/** @var User $user */
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('game_index');
}
$route = null;
try {
$route = $router->match($request->get('ref'));
} catch (Exception $e) {
// route requires params, show townhall route instead
}
return $this->render('game/tick_hold.html.twig', [
'ref' => is_array($route) && isset($route['_route']) ? $route['_route'] : 'game_index'
]);
}
/**
* @Route("/choose-area/{areaId}", name="game_choose_area")
*
* @param int $areaId
*
* @return Response
*/
public function chooseArea(int $areaId = 0): Response
{
/** @var User $user */
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute("game_index");
}
if ($areaId > 0) {
$area = $this->gameService->areaService->getArea($areaId);
if ($user->isInArea($area)) {
$this->userService->setActiveArea($user, $area->getName());
return $this->redirectToRoute('game_index');
}
}
return $this->render('game/choose_area.html.twig', [
'user' => $user,
'areas' => $this->gameService->areaService->getActiveAreas(),
'page' => 'game',
]);
}
/**
* @Route("/tick/{key}", name="game_tick")
*
* @param string $key
* @return Response
*/
public function tick(string $key, KernelInterface $kernel): Response
{
if ($key != $this->getParameter('tickkey')) {
// TODO report unauthorized access
return new Response("", Response::HTTP_NOT_FOUND);
}
$errors = [];
$areas = $this->gameService->areaService->getActiveAreas();
foreach ($areas as $area) {
$lock = $kernel->getProjectDir() . "/tickarealock_{$area->getId()}.lock";
if (!file_exists($lock) || filemtime($lock) > time() + 300) {
fopen($lock, 'w');
$e = null;
$this->handleTick($area, $e);
unlink($lock);
if ($e != null) {
$errors[] = $e;
}
} else {
$errors[] = "Tick lock file found: $lock";
}
}
// Restart bot (poor mans solution)
$this->discordTriggerService->addTrigger(null, DiscordTriggerService::TRIGGER_CLAN_INTEGRATE, 0);
if (count($errors) > 0) {
throw new \RuntimeException(count($errors) . " errors while ticking", 500, $errors[0] instanceof Exception ? $errors[0] : null);
}
return new Response("", Response::HTTP_OK);
}
/**
* @Route("/tick-manual/{areaId}", name="game_tick_manual")
*
* @param int $areaId
* @param KernelInterface $kernel
* @param Request $request
*
* @return Response
* @throws Exception
*/
public function manualTick(int $areaId, KernelInterface $kernel, Request $request): Response
{
if ($kernel->getEnvironment() == 'prod') {
$this->addFlash('notice', "Naughty naughty");
return $this->redirectToRoute('game_townhall');
}
// Lock process
$lock = $kernel->getProjectDir() . "/tickarealock_{$areaId}.lock";
if (!file_exists($lock) || filemtime($lock) > time() + 300) {
fopen($lock, 'w');
$e = null;
$area = $this->gameService->areaService->getArea($areaId);
$this->addFlash('notice', "Ticking " . ($area->getSettings()->turn + 1));
$this->handleTick($area, $e);
unlink($lock);
if ($e != null) {
throw $e;
}
} else {
$this->addFlash('notice', "Tick lock file found");
}
if (!empty($request->server->all()['HTTP_REFERER'])) {
return $this->redirect($request->server->all()['HTTP_REFERER']);
}
return $this->redirectToRoute('game_townhall');
}
private function handleTick(Area $area, &$e)
{
if (!$area->isRunning()) {
return;
}
try {
// Set the area manually on the service layer, don't rely on pulling it from session
$this->gameService->setArea($area);
$this->gameHelper->setServices($this->gameService);
$this->gameHelper->log = []; // reset log
if ($area->getType() == Area::TYPE_PVP) {
$ticker = new PvpAreaTicker($area);
} else {
$ticker = new PveAreaTicker($area);
}
$start = microtime(true);
$this->gameService->areaDb()->begin_transaction();
$this->gameHelper->tick($ticker);
$time = microtime(true) - $start;
$this->gameService->areaService->insertTurnLog($area, "Total Execution Time: {$time} seconds. " . date("d/m/Y H:i") . "\nLog:\n" . implode("\n", $this->gameHelper->log));
$this->gameService->commit();
} catch (Exception $e) {
$this->gameService->areaDb()->rollback();
$this->gameHelper->log[] = $e->getTraceAsString();
$this->gameService->areaService->insertTurnLog($area, "Tick failed. " . $e->getMessage() . "\nLog:\n" . implode("\n", $this->gameHelper->log));
$this->gameService->commit();
// TODO Log error
}
}
}