chg: dev: increase the minimum PHP version to the latest major - and massive refactor on back-end, like Controllers and Repositories #4
This commit is contained in:
@@ -255,13 +255,45 @@
|
|||||||
transition: all 200ms ease;
|
transition: all 200ms ease;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
background: linear-gradient(to bottom, rgba(45, 138, 168, 0.9) 0%, rgba(35, 111, 135, 0.95) 100%);
|
background: linear-gradient(to bottom, rgba(45, 138, 168, 0.9) 0%, rgba(35, 111, 135, 0.95) 100%);
|
||||||
border-color: rgba(149, 207, 245, 0.5);
|
border-color: rgba(149, 207, 245, 0.5);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 0 14px rgba(35, 111, 135, 0.5);
|
box-shadow: 0 0 14px rgba(35, 111, 135, 0.5);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.opd-join--waiting {
|
||||||
|
background: linear-gradient(to bottom, rgba(26, 80, 104, 0.6) 0%, rgba(15, 50, 70, 0.7) 100%);
|
||||||
|
border-color: rgba(35, 111, 135, 0.3);
|
||||||
|
color: rgba(149, 207, 245, 0.6);
|
||||||
|
opacity: 1;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-declined {
|
||||||
|
font: 600 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 120, 120, 0.85);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(180, 60, 60, 0.3);
|
||||||
|
background: rgba(180, 60, 60, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.opd-note {
|
.opd-note {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
|
||||||
const DIALOG_SX = {
|
const DIALOG_SX = {
|
||||||
@@ -45,6 +45,9 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const [snapshotLoaded, setSnapshotLoaded] = useState(false);
|
const [snapshotLoaded, setSnapshotLoaded] = useState(false);
|
||||||
|
const [challengingGameAssoc, setChallengingGameAssoc] = useState(null);
|
||||||
|
const [declinedMsg, setDeclinedMsg] = useState('');
|
||||||
|
const declinedTimerRef = useRef(null);
|
||||||
|
|
||||||
const addPlayer = useCallback(entry => {
|
const addPlayer = useCallback(entry => {
|
||||||
setPlayers(prev =>
|
setPlayers(prev =>
|
||||||
@@ -103,6 +106,31 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
return () => es.close();
|
return () => es.close();
|
||||||
}, [open, snapshotLoaded, addPlayer, removePlayer, currentGameAssoc]);
|
}, [open, snapshotLoaded, addPlayer, removePlayer, currentGameAssoc]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
setChallengingGameAssoc(null);
|
||||||
|
clearTimeout(declinedTimerRef.current);
|
||||||
|
setDeclinedMsg('Challenge was not accepted.');
|
||||||
|
declinedTimerRef.current = setTimeout(() => setDeclinedMsg(''), 3500);
|
||||||
|
};
|
||||||
|
window.addEventListener('challenge-declined', handler);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('challenge-declined', handler);
|
||||||
|
clearTimeout(declinedTimerRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChallenge = player => {
|
||||||
|
if (challengingGameAssoc) return;
|
||||||
|
setChallengingGameAssoc(player.gameAssoc);
|
||||||
|
setDeclinedMsg('');
|
||||||
|
fetch('/api/game/challenge/' + player.gameAssoc, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ challengerGameAssoc: currentGameAssoc }),
|
||||||
|
}).catch(() => setChallengingGameAssoc(null));
|
||||||
|
};
|
||||||
|
|
||||||
const visible = players
|
const visible = players
|
||||||
.filter(p => p.gameAssoc !== currentGameAssoc)
|
.filter(p => p.gameAssoc !== currentGameAssoc)
|
||||||
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()));
|
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()));
|
||||||
@@ -177,24 +205,38 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && shown.map(player => (
|
{declinedMsg && (
|
||||||
<div key={player.gameAssoc} className="opd-row">
|
<div className="opd-declined">
|
||||||
<div className="opd-avatar">
|
<i className="fa fa-times-circle" />
|
||||||
{player.name.slice(0, 2).toUpperCase()}
|
{' '}{declinedMsg}
|
||||||
</div>
|
|
||||||
<div className="opd-info">
|
|
||||||
<span className="opd-name">{player.name}</span>
|
|
||||||
<span className="opd-since">
|
|
||||||
<i className="fa fa-clock-o" />
|
|
||||||
{' '}Waiting {formatSince(player.since)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a className="opd-join" href={`/play/${player.gameAssoc}`}>
|
|
||||||
<i className="fa fa-play" />
|
|
||||||
Join
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
{!loading && shown.map(player => {
|
||||||
|
const isWaiting = challengingGameAssoc === player.gameAssoc;
|
||||||
|
return (
|
||||||
|
<div key={player.gameAssoc} className="opd-row">
|
||||||
|
<div className="opd-avatar">
|
||||||
|
{player.name.slice(0, 2).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="opd-info">
|
||||||
|
<span className="opd-name">{player.name}</span>
|
||||||
|
<span className="opd-since">
|
||||||
|
<i className="fa fa-clock-o" />
|
||||||
|
{' '}Waiting {formatSince(player.since)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={`opd-join${isWaiting ? ' opd-join--waiting' : ''}`}
|
||||||
|
onClick={() => handleChallenge(player)}
|
||||||
|
disabled={!!challengingGameAssoc}
|
||||||
|
>
|
||||||
|
<i className={`fa ${isWaiting ? 'fa-spinner fa-spin' : 'fa-play'}`} />
|
||||||
|
{isWaiting ? 'Waiting...' : 'Join'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!loading && hasMore && (
|
{!loading && hasMore && (
|
||||||
|
|||||||
@@ -130,6 +130,51 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
showOverlay('The connection has been lost w/ your friend...', 'Please, restart the game!');
|
showOverlay('The connection has been lost w/ your friend...', 'Please, restart the game!');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const wChallenge = payload => {
|
||||||
|
const { challengerName, challengerGameAssoc } = payload;
|
||||||
|
|
||||||
|
const handleAccept = () => {
|
||||||
|
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ accepted: true, targetGameAssoc: gameAssoc }),
|
||||||
|
}).then(() => {
|
||||||
|
showOverlay('Challenge accepted!', 'Waiting for the challenger to join...');
|
||||||
|
}).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecline = () => {
|
||||||
|
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ accepted: false, targetGameAssoc: gameAssoc }),
|
||||||
|
}).then(() => {
|
||||||
|
showOverlay('We are waiting for your opponent...', gameAssoc ? (
|
||||||
|
<WaitingOverlayContent
|
||||||
|
shareUrl={window.location.origin + '/play/' + gameAssoc}
|
||||||
|
currentGameAssoc={gameAssoc}
|
||||||
|
/>
|
||||||
|
) : '');
|
||||||
|
}).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
showOverlay(
|
||||||
|
challengerName + ' wants to challenge you!',
|
||||||
|
<div className="resign">
|
||||||
|
<a onClick={handleAccept}>Accept</a>
|
||||||
|
<a onClick={handleDecline}>Decline</a>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wChallengeResponse = payload => {
|
||||||
|
if (payload.accepted) {
|
||||||
|
window.location.href = '/play/' + payload.targetGameAssoc;
|
||||||
|
} else {
|
||||||
|
window.dispatchEvent(new CustomEvent('challenge-declined'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const wTopic = payload => {
|
const wTopic = payload => {
|
||||||
if (webPlayerRef.current !== payload.data.player) {
|
if (webPlayerRef.current !== payload.data.player) {
|
||||||
if (null === payload.data.resign) {
|
if (null === payload.data.resign) {
|
||||||
@@ -152,6 +197,12 @@ const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMercureMessage = payload => {
|
const handleMercureMessage = payload => {
|
||||||
|
if (undefined !== payload.type) {
|
||||||
|
if ('challenge' === payload.type) wChallenge(payload);
|
||||||
|
else if ('challenge-response' === payload.type) wChallengeResponse(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (undefined !== payload.data) {
|
if (undefined !== payload.data) {
|
||||||
wTopic(payload);
|
wTopic(payload);
|
||||||
} else if (undefined === payload.msg) {
|
} else if (undefined === payload.msg) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"type": "project",
|
"type": "project",
|
||||||
"license": "proprietary",
|
"license": "proprietary",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.2",
|
"php": ">=8.5",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"doctrine/dbal": "^3.7",
|
"doctrine/dbal": "^3.7",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace App\Controller;
|
|||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +26,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
* @link www.splendidbear.org
|
* @link www.splendidbear.org
|
||||||
* @since 2026. 04. 09.
|
* @since 2026. 04. 09.
|
||||||
*/
|
*/
|
||||||
|
#[AsController]
|
||||||
class GameController extends AbstractController
|
class GameController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +35,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
* @link www.splendidbear.org
|
* @link www.splendidbear.org
|
||||||
* @since 2026. 04. 09.
|
* @since 2026. 04. 09.
|
||||||
*/
|
*/
|
||||||
|
#[AsController]
|
||||||
class MercureController extends AbstractController
|
class MercureController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -83,6 +85,27 @@ class MercureController extends AbstractController
|
|||||||
return $this->json(['success' => true]);
|
return $this->json(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/api/game/challenge/{targetGameAssoc}', name: 'MineSeekerBundle_api_game_challenge', methods: ['POST'])]
|
||||||
|
public function challenge(string $targetGameAssoc, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->toArray();
|
||||||
|
$challengerGameAssoc = $data['challengerGameAssoc'] ?? '';
|
||||||
|
$this->topicManager->publishChallenge($targetGameAssoc, $challengerGameAssoc);
|
||||||
|
|
||||||
|
return $this->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/game/challenge/respond/{challengerGameAssoc}', name: 'MineSeekerBundle_api_game_challenge_respond', methods: ['POST'])]
|
||||||
|
public function challengeRespond(string $challengerGameAssoc, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->toArray();
|
||||||
|
$accepted = (bool)($data['accepted'] ?? false);
|
||||||
|
$targetGameAssoc = $data['targetGameAssoc'] ?? '';
|
||||||
|
$this->topicManager->publishChallengeResponse($challengerGameAssoc, $accepted, $targetGameAssoc);
|
||||||
|
|
||||||
|
return $this->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/api/game/waiting', name: 'MineSeekerBundle_api_game_waiting', methods: ['GET'])]
|
#[Route('/api/game/waiting', name: 'MineSeekerBundle_api_game_waiting', methods: ['GET'])]
|
||||||
public function waiting(PlayedGameRepository $repo): JsonResponse
|
public function waiting(PlayedGameRepository $repo): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,9 +11,10 @@
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use App\Repository\PlayedGameRepository;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,58 +27,26 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
* @link www.splendidbear.org
|
* @link www.splendidbear.org
|
||||||
* @since 2026. 04. 11.
|
* @since 2026. 04. 11.
|
||||||
*/
|
*/
|
||||||
|
#[AsController]
|
||||||
class ProfileController extends AbstractController
|
class ProfileController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/profile', name: 'MineSeekerBundle_profile')]
|
public function __construct(private readonly PlayedGameRepository $repo) { }
|
||||||
public function index(EntityManagerInterface $em): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
|
||||||
|
|
||||||
|
#[Route('/profile', name: 'MineSeekerBundle_profile')]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||||
$finished = '(g.redPoints IS NOT NULL OR g.resign IS NOT NULL)';
|
|
||||||
|
|
||||||
$total = (int) $em->createQuery(
|
|
||||||
"SELECT COUNT(g.id) FROM App\Entity\PlayedGame g
|
|
||||||
WHERE (g.red = :u OR g.blue = :u) AND {$finished}"
|
|
||||||
)->setParameter('u', $user)->getSingleScalarResult();
|
|
||||||
|
|
||||||
$wins = (int) $em->createQuery(
|
|
||||||
"SELECT COUNT(g.id) FROM App\Entity\PlayedGame g WHERE (
|
|
||||||
(g.red = :u AND g.redPoints > g.bluePoints AND g.resign IS NULL) OR
|
|
||||||
(g.blue = :u AND g.bluePoints > g.redPoints AND g.resign IS NULL) OR
|
|
||||||
(g.red = :u AND g.resign = 'blue') OR
|
|
||||||
(g.blue = :u AND g.resign = 'red')
|
|
||||||
)"
|
|
||||||
)->setParameter('u', $user)->getSingleScalarResult();
|
|
||||||
|
|
||||||
$losses = (int) $em->createQuery(
|
|
||||||
"SELECT COUNT(g.id) FROM App\Entity\PlayedGame g WHERE (
|
|
||||||
(g.red = :u AND g.bluePoints > g.redPoints AND g.resign IS NULL) OR
|
|
||||||
(g.blue = :u AND g.redPoints > g.bluePoints AND g.resign IS NULL) OR
|
|
||||||
(g.red = :u AND g.resign = 'red') OR
|
|
||||||
(g.blue = :u AND g.resign = 'blue')
|
|
||||||
)"
|
|
||||||
)->setParameter('u', $user)->getSingleScalarResult();
|
|
||||||
|
|
||||||
$bombs = (int) $em->createQuery(
|
|
||||||
"SELECT COUNT(g.id) FROM App\Entity\PlayedGame g WHERE
|
|
||||||
(g.red = :u AND g.redExplodedBomb = true) OR
|
|
||||||
(g.blue = :u AND g.blueExplodedBomb = true)"
|
|
||||||
)->setParameter('u', $user)->getSingleScalarResult();
|
|
||||||
|
|
||||||
$recent = $em->createQuery(
|
|
||||||
"SELECT g FROM App\Entity\PlayedGame g
|
|
||||||
LEFT JOIN g.red rr LEFT JOIN g.blue bb
|
|
||||||
LEFT JOIN g.redAnon ra LEFT JOIN g.blueAnon ba
|
|
||||||
WHERE (g.red = :u OR g.blue = :u) AND {$finished}
|
|
||||||
ORDER BY g.updated DESC"
|
|
||||||
)->setParameter('u', $user)->setMaxResults(10)->getResult();
|
|
||||||
|
|
||||||
return $this->render('Security/profile.html.twig', [
|
return $this->render('Security/profile.html.twig', [
|
||||||
'stats' => compact('total', 'wins', 'losses', 'bombs'),
|
'stats' => [
|
||||||
'recent' => $recent,
|
'total' => $this->repo->countFinishedForUser($user),
|
||||||
|
'wins' => $this->repo->countWinsForUser($user),
|
||||||
|
'losses' => $this->repo->countLossesForUser($user),
|
||||||
|
'bombs' => $this->repo->countBombsForUser($user),
|
||||||
|
],
|
||||||
|
'recent' => $this->repo->findRecentFinishedForUser($user),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
|||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
@@ -32,6 +33,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
|||||||
* @link www.splendidbear.org
|
* @link www.splendidbear.org
|
||||||
* @since 2026. 04. 11.
|
* @since 2026. 04. 11.
|
||||||
*/
|
*/
|
||||||
|
#[AsController]
|
||||||
class SecurityController extends AbstractController
|
class SecurityController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/login', name: 'MineSeekerBundle_login')]
|
#[Route('/login', name: 'MineSeekerBundle_login')]
|
||||||
@@ -93,7 +95,7 @@ class SecurityController extends AbstractController
|
|||||||
if (empty($errors)) {
|
if (empty($errors)) {
|
||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
|
|
||||||
$user = (new User())
|
$user = new User()
|
||||||
->setUsername($username)
|
->setUsername($username)
|
||||||
->setEmail($email)
|
->setEmail($email)
|
||||||
->setIsVerified(false)
|
->setIsVerified(false)
|
||||||
@@ -111,7 +113,7 @@ class SecurityController extends AbstractController
|
|||||||
);
|
);
|
||||||
|
|
||||||
$mailer->send(
|
$mailer->send(
|
||||||
(new TemplatedEmail())
|
new TemplatedEmail()
|
||||||
->from('noreply@mineseeker.ninja')
|
->from('noreply@mineseeker.ninja')
|
||||||
->to($email)
|
->to($email)
|
||||||
->subject('Activate your MineSeeker account')
|
->subject('Activate your MineSeeker account')
|
||||||
|
|||||||
@@ -11,9 +11,14 @@
|
|||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
|
|
||||||
use App\Entity\PlayedGame;
|
use App\Entity\PlayedGame;
|
||||||
|
use App\Entity\User;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class PlayedGameRepository
|
* Class PlayedGameRepository
|
||||||
@@ -29,35 +34,265 @@ use Doctrine\Persistence\ManagerRegistry;
|
|||||||
* @method PlayedGame|null findOneBy(array $criteria, array $orderBy = null)
|
* @method PlayedGame|null findOneBy(array $criteria, array $orderBy = null)
|
||||||
* @method PlayedGame[] findAll()
|
* @method PlayedGame[] findAll()
|
||||||
* @method PlayedGame[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
* @method PlayedGame[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||||
* @method PlayedGame|null findOneByGameAssoc($gameAssoc)
|
|
||||||
*/
|
*/
|
||||||
class PlayedGameRepository extends ServiceEntityRepository
|
class PlayedGameRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(ManagerRegistry $registry, private readonly LoggerInterface $logger)
|
||||||
* PlayedGameRepository constructor.
|
|
||||||
*
|
|
||||||
* @param ManagerRegistry $registry
|
|
||||||
*/
|
|
||||||
public function __construct(ManagerRegistry $registry)
|
|
||||||
{
|
{
|
||||||
parent::__construct($registry, PlayedGame::class);
|
parent::__construct($registry, PlayedGame::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findOneByGameAssoc(string $gameAssoc): ?PlayedGame
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('p');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $qb
|
||||||
|
->where($qb->expr()->eq('p.gameAssoc', ':gameAssoc'))
|
||||||
|
->setParameter('gameAssoc', $gameAssoc)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
} catch (NonUniqueResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly multiple results found when looking up gameAssoc: $gameAssoc",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countFinishedForUser(User $user): int
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('g');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (int) $qb
|
||||||
|
->select('COUNT(g.id)')
|
||||||
|
->where($qb->expr()->andX(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->eq('g.red', ':u'),
|
||||||
|
$qb->expr()->eq('g.blue', ':u'),
|
||||||
|
),
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNotNull('g.redPoints'),
|
||||||
|
$qb->expr()->isNotNull('g.resign'),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
->setParameter('u', $user)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
} catch (NoResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly no result found when counting finished games for user: {$user->getUsername()}",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
} catch (NonUniqueResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly multiple results found when counting finished games for user: {$user->getUsername()}",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countWinsForUser(User $user): int
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('g');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (int) $qb
|
||||||
|
->select('COUNT(g.id)')
|
||||||
|
->where($qb->expr()->orX(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.red', ':u'),
|
||||||
|
$qb->expr()->gt('g.redPoints', 'g.bluePoints'),
|
||||||
|
$qb->expr()->isNull('g.resign'),
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.blue', ':u'),
|
||||||
|
$qb->expr()->gt('g.bluePoints', 'g.redPoints'),
|
||||||
|
$qb->expr()->isNull('g.resign'),
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.red', ':u'),
|
||||||
|
$qb->expr()->eq('g.resign', ':blue'),
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.blue', ':u'),
|
||||||
|
$qb->expr()->eq('g.resign', ':red'),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
->setParameter('u', $user)
|
||||||
|
->setParameter('blue', 'blue')
|
||||||
|
->setParameter('red', 'red')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
} catch (NoResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly no result found when counting wins for user: {$user->getUsername()}",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
} catch (NonUniqueResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly multiple results found when counting wins for user: {$user->getUsername()}",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countLossesForUser(User $user): int
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('g');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (int) $qb
|
||||||
|
->select('COUNT(g.id)')
|
||||||
|
->where($qb->expr()->orX(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.red', ':u'),
|
||||||
|
$qb->expr()->gt('g.bluePoints', 'g.redPoints'),
|
||||||
|
$qb->expr()->isNull('g.resign'),
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.blue', ':u'),
|
||||||
|
$qb->expr()->gt('g.redPoints', 'g.bluePoints'),
|
||||||
|
$qb->expr()->isNull('g.resign'),
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.red', ':u'),
|
||||||
|
$qb->expr()->eq('g.resign', ':red'),
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.blue', ':u'),
|
||||||
|
$qb->expr()->eq('g.resign', ':blue'),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
->setParameter('u', $user)
|
||||||
|
->setParameter('red', 'red')
|
||||||
|
->setParameter('blue', 'blue')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
} catch (NoResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly no result found when counting losses for user: {$user->getUsername()}",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
} catch (NonUniqueResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly multiple results found when counting losses for user: {$user->getUsername()}",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countBombsForUser(User $user): int
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('g');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (int) $qb
|
||||||
|
->select('COUNT(g.id)')
|
||||||
|
->where($qb->expr()->orX(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.red', ':u'),
|
||||||
|
$qb->expr()->eq('g.redExplodedBomb', 'true'),
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('g.blue', ':u'),
|
||||||
|
$qb->expr()->eq('g.blueExplodedBomb', 'true'),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
->setParameter('u', $user)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
} catch (NoResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly no result found when counting bombs for user: {$user->getUsername()}",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
} catch (NonUniqueResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unexpectedly multiple results found when counting bombs for user: {$user->getUsername()}",
|
||||||
|
0,
|
||||||
|
$e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PlayedGame[]
|
||||||
|
*/
|
||||||
|
public function findRecentFinishedForUser(User $user, int $limit = 10): array
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('g');
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->addSelect('rr', 'bb', 'ra', 'ba')
|
||||||
|
->leftJoin('g.red', 'rr')
|
||||||
|
->leftJoin('g.blue', 'bb')
|
||||||
|
->leftJoin('g.redAnon', 'ra')
|
||||||
|
->leftJoin('g.blueAnon', 'ba')
|
||||||
|
->where($qb->expr()->andX(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->eq('g.red', ':u'),
|
||||||
|
$qb->expr()->eq('g.blue', ':u'),
|
||||||
|
),
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNotNull('g.redPoints'),
|
||||||
|
$qb->expr()->isNotNull('g.resign'),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
->setParameter('u', $user)
|
||||||
|
->orderBy('g.updated', 'DESC')
|
||||||
|
->setMaxResults($limit)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
public function findWaitingGames(int $limit = 20): array
|
public function findWaitingGames(int $limit = 20): array
|
||||||
{
|
{
|
||||||
// Any legitimately waiting game was updated within the last 10 minutes.
|
// Any legitimately waiting game was updated within the last 10 minutes.
|
||||||
// Abandoned games are stamped with updated = 2000-01-01, so they fail this filter.
|
// Abandoned games are stamped with updated = 2000-01-01, so they fail this filter.
|
||||||
$cutoff = new DateTime('-10 minutes');
|
$qb = $this->createQueryBuilder('p');
|
||||||
|
|
||||||
return $this->createQueryBuilder('p')
|
return $qb
|
||||||
->where('p.resign IS NULL')
|
->where($qb->expr()->isNull('p.resign'))
|
||||||
->andWhere('p.updated > :cutoff')
|
->andWhere($qb->expr()->gt('p.updated', ':cutoff'))
|
||||||
->andWhere(
|
->andWhere($qb->expr()->orX(
|
||||||
'(p.red IS NOT NULL OR p.redAnon IS NOT NULL) AND (p.blue IS NULL AND p.blueAnon IS NULL)
|
$qb->expr()->andX(
|
||||||
OR (p.blue IS NOT NULL OR p.blueAnon IS NOT NULL) AND (p.red IS NULL AND p.redAnon IS NULL)'
|
$qb->expr()->orX(
|
||||||
)
|
$qb->expr()->isNotNull('p.red'),
|
||||||
|
$qb->expr()->isNotNull('p.redAnon'),
|
||||||
|
),
|
||||||
|
$qb->expr()->isNull('p.blue'),
|
||||||
|
$qb->expr()->isNull('p.blueAnon'),
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNotNull('p.blue'),
|
||||||
|
$qb->expr()->isNotNull('p.blueAnon'),
|
||||||
|
),
|
||||||
|
$qb->expr()->isNull('p.red'),
|
||||||
|
$qb->expr()->isNull('p.redAnon'),
|
||||||
|
),
|
||||||
|
))
|
||||||
->orderBy('p.updated', 'DESC')
|
->orderBy('p.updated', 'DESC')
|
||||||
->setParameter('cutoff', $cutoff)
|
->setParameter('cutoff', new DateTime('-10 minutes'))
|
||||||
->setMaxResults($limit)
|
->setMaxResults($limit)
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getResult();
|
->getResult();
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ namespace App\Repository;
|
|||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use RuntimeException;
|
||||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
@@ -31,14 +34,25 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
|||||||
*/
|
*/
|
||||||
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
|
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
|
||||||
{
|
{
|
||||||
public function __construct(ManagerRegistry $registry)
|
public function __construct(ManagerRegistry $registry, private readonly LoggerInterface $logger)
|
||||||
{
|
{
|
||||||
parent::__construct($registry, User::class);
|
parent::__construct($registry, User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findOneByUsername(string $username): ?User
|
public function findOneByUsername(string $username): ?User
|
||||||
{
|
{
|
||||||
return $this->findOneBy(['username' => $username]);
|
$qb = $this->createQueryBuilder('u');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $qb
|
||||||
|
->where($qb->expr()->eq('u.username', ':username'))
|
||||||
|
->setParameter('username', $username)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
} catch (NonUniqueResultException $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
throw new RuntimeException("Multiple users found with the same username: $username", 0, $e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Entity\Grid;
|
|||||||
use App\Entity\GridRow;
|
use App\Entity\GridRow;
|
||||||
use App\Entity\PlayedGame;
|
use App\Entity\PlayedGame;
|
||||||
use App\Interfaces\RpcManagerInterface;
|
use App\Interfaces\RpcManagerInterface;
|
||||||
|
use App\Repository\PlayedGameRepository;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Exception;
|
use Exception;
|
||||||
@@ -40,6 +41,7 @@ class RpcManager implements RpcManagerInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly PlayedGameRepository $playedGameRepository,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +49,7 @@ class RpcManager implements RpcManagerInterface
|
|||||||
{
|
{
|
||||||
$gameAssoc = is_array($params) ? $params[0] : $params;
|
$gameAssoc = is_array($params) ? $params[0] : $params;
|
||||||
|
|
||||||
$playedGame = $this->entityManager
|
$playedGame = $this->playedGameRepository->findOneByGameAssoc($gameAssoc);
|
||||||
->getRepository(PlayedGame::class)
|
|
||||||
->findOneByGameAssoc($gameAssoc);
|
|
||||||
|
|
||||||
if (null === $playedGame) {
|
if (null === $playedGame) {
|
||||||
try {
|
try {
|
||||||
@@ -77,9 +77,7 @@ class RpcManager implements RpcManagerInterface
|
|||||||
|
|
||||||
public function saveGrid(string $gameAssoc): bool
|
public function saveGrid(string $gameAssoc): bool
|
||||||
{
|
{
|
||||||
$existingGame = $this->entityManager
|
$existingGame = $this->playedGameRepository->findOneByGameAssoc($gameAssoc);
|
||||||
->getRepository(PlayedGame::class)
|
|
||||||
->findOneByGameAssoc($gameAssoc);
|
|
||||||
|
|
||||||
if (null !== $existingGame) {
|
if (null !== $existingGame) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ use App\Entity\PlayedGame;
|
|||||||
use App\Entity\Step;
|
use App\Entity\Step;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Interfaces\TopicManagerInterface;
|
use App\Interfaces\TopicManagerInterface;
|
||||||
|
use App\Repository\PlayedGameRepository;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@@ -37,12 +39,14 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
* @link www.splendidbear.org
|
* @link www.splendidbear.org
|
||||||
* @since 2026. 04. 09.
|
* @since 2026. 04. 09.
|
||||||
*/
|
*/
|
||||||
class TopicManager implements TopicManagerInterface
|
readonly class TopicManager implements TopicManagerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly HubInterface $hub,
|
private HubInterface $hub,
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private readonly LoggerInterface $logger
|
private LoggerInterface $logger,
|
||||||
|
private PlayedGameRepository $playedGameRepository,
|
||||||
|
private UserRepository $userRepository,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,9 +407,7 @@ class TopicManager implements TopicManagerInterface
|
|||||||
|
|
||||||
private function getPlayedGame(string $gameAssoc): ?PlayedGame
|
private function getPlayedGame(string $gameAssoc): ?PlayedGame
|
||||||
{
|
{
|
||||||
return $this->entityManager
|
return $this->playedGameRepository->findOneByGameAssoc($gameAssoc);
|
||||||
->getRepository(PlayedGame::class)
|
|
||||||
->findOneByGameAssoc($gameAssoc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getPlayerCount(array $users): int
|
private function getPlayerCount(array $users): int
|
||||||
@@ -475,9 +477,7 @@ class TopicManager implements TopicManagerInterface
|
|||||||
private function saveRegisteredUser(string $userName, int $count, PlayedGame $playedGame): void
|
private function saveRegisteredUser(string $userName, int $count, PlayedGame $playedGame): void
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = $this->entityManager
|
$user = $this->userRepository->findOneByUsername($userName);
|
||||||
->getRepository(User::class)
|
|
||||||
->findOneByUsername($userName);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($count === 1) {
|
if ($count === 1) {
|
||||||
@@ -524,6 +524,45 @@ class TopicManager implements TopicManagerInterface
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function publishChallenge(string $targetGameAssoc, string $challengerGameAssoc): void
|
||||||
|
{
|
||||||
|
$challengerGame = $this->getPlayedGame($challengerGameAssoc);
|
||||||
|
$challengerName = 'Unknown';
|
||||||
|
if (null !== $challengerGame) {
|
||||||
|
$users = $this->getUserCollection($challengerGame);
|
||||||
|
$challengerName = $users['red'] ?: $users['redAnon'] ?: $users['blue'] ?: $users['blueAnon'] ?: 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->hub->publish(new Update(
|
||||||
|
'mineseeker/channel/' . $targetGameAssoc,
|
||||||
|
json_encode([
|
||||||
|
'type' => 'challenge',
|
||||||
|
'challengerName' => $challengerName,
|
||||||
|
'challengerGameAssoc' => $challengerGameAssoc,
|
||||||
|
], JSON_THROW_ON_ERROR)
|
||||||
|
));
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
$this->logger->error('Challenge publish error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function publishChallengeResponse(string $challengerGameAssoc, bool $accepted, string $targetGameAssoc): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->hub->publish(new Update(
|
||||||
|
'mineseeker/channel/' . $challengerGameAssoc,
|
||||||
|
json_encode([
|
||||||
|
'type' => 'challenge-response',
|
||||||
|
'accepted' => $accepted,
|
||||||
|
'targetGameAssoc' => $targetGameAssoc,
|
||||||
|
], JSON_THROW_ON_ERROR)
|
||||||
|
));
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
$this->logger->error('Challenge response publish error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function publishToLobby(array $data): void
|
private function publishToLobby(array $data): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user