chg: dev: replace the legacy gos/web-socket-bundle & replace it with Mercure protocol #4
This commit is contained in:
10
.env.dist
10
.env.dist
@@ -18,3 +18,13 @@ DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
|
|||||||
###> symfony/mailer ###
|
###> symfony/mailer ###
|
||||||
# MAILER_DSN=smtp://localhost
|
# MAILER_DSN=smtp://localhost
|
||||||
###< symfony/mailer ###
|
###< symfony/mailer ###
|
||||||
|
|
||||||
|
###> symfony/mercure-bundle ###
|
||||||
|
# See https://symfony.com/doc/current/mercure.html#configuration
|
||||||
|
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
|
||||||
|
MERCURE_URL=https://example.com/.well-known/mercure
|
||||||
|
# The public URL of the Mercure hub, used by the browser to connect
|
||||||
|
MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
|
||||||
|
# The secret used to sign the JWTs
|
||||||
|
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
||||||
|
###< symfony/mercure-bundle ###
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ ReactDOM.render(
|
|||||||
<MineSeeker
|
<MineSeeker
|
||||||
env={document.getElementById("mine-wrapper").dataset.env}
|
env={document.getElementById("mine-wrapper").dataset.env}
|
||||||
gameId={document.getElementById("mine-wrapper").dataset.gameId}
|
gameId={document.getElementById("mine-wrapper").dataset.gameId}
|
||||||
ssl={document.getElementById("mine-wrapper").dataset.ssl}
|
|
||||||
/>,
|
/>,
|
||||||
document.getElementById("mine-wrapper"),
|
document.getElementById("mine-wrapper"),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,16 +11,22 @@ class MineSeeker extends React.Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
env: props.env,
|
env: props.env,
|
||||||
ssl: props.ssl,
|
|
||||||
gameInherited: '' !== props.gameId,
|
gameInherited: '' !== props.gameId,
|
||||||
gameAssoc: gameAssoc,
|
gameAssoc: gameAssoc,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
session: null,
|
|
||||||
createGrid: false,
|
|
||||||
stepCache: [],
|
stepCache: [],
|
||||||
connectionLost: false,
|
connectionLost: false,
|
||||||
end: false,
|
end: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** SSE connection (not React state — no re-render needed) */
|
||||||
|
this.eventSource = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Users from GET /api/game/connect (inherited game).
|
||||||
|
* Passed to wSubscribe so it can determine the local player's colour.
|
||||||
|
*/
|
||||||
|
this.rpcUsers = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
currectGridSize() {
|
currectGridSize() {
|
||||||
@@ -37,9 +43,6 @@ class MineSeeker extends React.Component {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* STEP
|
* STEP
|
||||||
*
|
|
||||||
* @param coords
|
|
||||||
* @returns {{red: *, blue: *}}
|
|
||||||
*/
|
*/
|
||||||
makePointsCalcAndStep(coords) {
|
makePointsCalcAndStep(coords) {
|
||||||
let users = this.refs.gridControl.refs.userControl,
|
let users = this.refs.gridControl.refs.userControl,
|
||||||
@@ -63,14 +66,10 @@ class MineSeeker extends React.Component {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* START
|
* START
|
||||||
*
|
|
||||||
* @param payload
|
|
||||||
*/
|
*/
|
||||||
makeGameStart(payload) {
|
makeGameStart(payload) {
|
||||||
/** every time the blue starts */
|
|
||||||
this.refs.gridControl.refs.userControl.setState({ activePlayer: 1 });
|
this.refs.gridControl.refs.userControl.setState({ activePlayer: 1 });
|
||||||
|
|
||||||
/** Set up player names w/ server data */
|
|
||||||
this.refs.gridControl.refs.userControl.refs.red.setState({
|
this.refs.gridControl.refs.userControl.refs.red.setState({
|
||||||
name: '' !== payload.users.red ? payload.users.red : payload.users.redAnon,
|
name: '' !== payload.users.red ? payload.users.red : payload.users.redAnon,
|
||||||
});
|
});
|
||||||
@@ -88,10 +87,6 @@ class MineSeeker extends React.Component {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* THE END
|
* THE END
|
||||||
*
|
|
||||||
* @param bluePoints
|
|
||||||
* @param redPoints
|
|
||||||
* @param resign
|
|
||||||
*/
|
*/
|
||||||
makeGameEndIfItEnds(bluePoints, redPoints, resign = false) {
|
makeGameEndIfItEnds(bluePoints, redPoints, resign = false) {
|
||||||
let redWins = 25 < redPoints,
|
let redWins = 25 < redPoints,
|
||||||
@@ -109,7 +104,6 @@ class MineSeeker extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.refs.gridControl.showLeftMines();
|
this.refs.gridControl.showLeftMines();
|
||||||
|
|
||||||
this.refs.gridControl.refs.userControl.setState({ activePlayer: false });
|
this.refs.gridControl.refs.userControl.setState({ activePlayer: false });
|
||||||
this.refs.gridControl.refs.userControl.refs.red.setState({ desc: '' });
|
this.refs.gridControl.refs.userControl.refs.red.setState({ desc: '' });
|
||||||
this.refs.gridControl.refs.userControl.refs.blue.setState({ desc: '' });
|
this.refs.gridControl.refs.userControl.refs.blue.setState({ desc: '' });
|
||||||
@@ -128,25 +122,19 @@ class MineSeeker extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.setState({ end: true });
|
this.setState({ end: true });
|
||||||
|
|
||||||
this.makeGameEndIfItEnds(0, 0, true);
|
this.makeGameEndIfItEnds(0, 0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
clickResign() {
|
clickResign() {
|
||||||
/** PUBLISH */
|
let resignColor = this.refs.gridControl.refs.userControl.state.activePlayer ? 'blue' : 'red';
|
||||||
this.state.session.publish(this.state.channel, {
|
this.publishStep({ resign: resignColor });
|
||||||
'resign': this.refs.gridControl.refs.userControl.state.activePlayer ? 'blue' : 'red',
|
|
||||||
});
|
|
||||||
this.resignProcess(this.refs.gridControl.state.webPlayer);
|
this.resignProcess(this.refs.gridControl.state.webPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
clickResignCancel() {
|
clickResignCancel() {
|
||||||
this.refs.gridControl.setState({
|
this.refs.gridControl.setState({ overlay: false });
|
||||||
overlay: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** RESIGN */
|
|
||||||
resign() {
|
resign() {
|
||||||
let users = this.refs.gridControl.refs.userControl,
|
let users = this.refs.gridControl.refs.userControl,
|
||||||
activePlayer = users.state.activePlayer ? 'blue' : 'red';
|
activePlayer = users.state.activePlayer ? 'blue' : 'red';
|
||||||
@@ -165,11 +153,133 @@ class MineSeeker extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wInit(session, gridServer, gridClient) {
|
// ------------------------------------------------------------------ //
|
||||||
this.setState({ session: session });
|
// Mercure message handlers (same logic as former WAMP callbacks)
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
/** save session to GridControl */
|
wSubscribe(payload, rpcUsers = null) {
|
||||||
/** render grid fields - @see #12 */
|
'dev' === this.state.env && console.info(
|
||||||
|
('undefined' !== typeof payload.user ? payload.user : 'user') + ' has been subscribed to the channel!',
|
||||||
|
);
|
||||||
|
|
||||||
|
let firstUser = !rpcUsers;
|
||||||
|
|
||||||
|
null === this.refs.gridControl.state.webPlayer && this.refs.gridControl.setState({
|
||||||
|
webPlayer: payload.user === payload.users.blue
|
||||||
|
|| (
|
||||||
|
firstUser && '' !== payload.users.blueAnon
|
||||||
|
|| !firstUser && ('' === rpcUsers.blueAnon && '' === rpcUsers.blue)
|
||||||
|
)
|
||||||
|
? 'blue' : 'red',
|
||||||
|
});
|
||||||
|
|
||||||
|
(900 > $(document).width()) && this.currectGridSize();
|
||||||
|
|
||||||
|
if (
|
||||||
|
2 === payload.userCnt
|
||||||
|
&& (
|
||||||
|
!this.state.connectionLost
|
||||||
|
|| this.state.connectionLost && false === this.refs.gridControl.refs.userControl.state.activePlayer && !this.state.end
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.makeGameStart(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wUnsubscribe(payload) {
|
||||||
|
'dev' === this.state.env && console.info(payload.msg);
|
||||||
|
|
||||||
|
this.refs.gridControl.setState({
|
||||||
|
overlay: true,
|
||||||
|
overlayTitle: 'The connection has been lost w/ your friend...',
|
||||||
|
overlaySubTitle: 'Please, restart the game!',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wTopic(payload) {
|
||||||
|
if (this.refs.gridControl.state.webPlayer !== payload.data.player) {
|
||||||
|
if (null === payload.data.resign) {
|
||||||
|
'dev' === this.state.env && console.warn(payload.user + ' has been stepped to coords: ' + payload.data.coords[0] + ', ' + payload.data.coords[1]);
|
||||||
|
'dev' === this.state.env && console.warn('Opponent stepped: Auto-Step process');
|
||||||
|
|
||||||
|
this.refs.gridControl.refs.userControl.setState({ bombSelected: payload.data.bomb });
|
||||||
|
|
||||||
|
let points = this.makePointsCalcAndStep(payload.data.coords);
|
||||||
|
this.makeGameEndIfItEnds(points.blue, points.red);
|
||||||
|
} else {
|
||||||
|
this.resignProcess(payload.data.resign);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Mercure / SSE connection
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches every incoming SSE message.
|
||||||
|
* Distinguishes subscription events, game-step events, and disconnect events
|
||||||
|
* using the same payload shape as the former WAMP broadcast.
|
||||||
|
*/
|
||||||
|
handleMercureMessage(payload) {
|
||||||
|
let isTopicEvent = 'undefined' !== typeof payload.data;
|
||||||
|
let isNotUnsubscribe = 'undefined' === typeof payload.msg;
|
||||||
|
|
||||||
|
if (isTopicEvent) {
|
||||||
|
this.wTopic(payload);
|
||||||
|
} else if (isNotUnsubscribe) {
|
||||||
|
this.wSubscribe(payload, this.rpcUsers);
|
||||||
|
} else {
|
||||||
|
this.wUnsubscribe(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reconnection: replay cached steps once both players are back */
|
||||||
|
if (2 === payload.userCnt && this.state.connectionLost) {
|
||||||
|
'dev' === this.state.env && console.info('Reconnection process');
|
||||||
|
|
||||||
|
let cache = this.state.stepCache;
|
||||||
|
cache.forEach(item => this.publishStep(item));
|
||||||
|
this.setState({ connectionLost: false, stepCache: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openEventSource() {
|
||||||
|
const wrapper = document.getElementById('mine-wrapper');
|
||||||
|
const hubUrl = wrapper.dataset.mercureHubUrl;
|
||||||
|
const subscriberJwt = wrapper.dataset.mercureSubscriberJwt;
|
||||||
|
|
||||||
|
const url = new URL(hubUrl, window.location.origin);
|
||||||
|
url.searchParams.append('topic', this.state.channel);
|
||||||
|
if (subscriberJwt) {
|
||||||
|
url.searchParams.append('authorization', subscriberJwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventSource = new EventSource(url.toString());
|
||||||
|
|
||||||
|
this.eventSource.onmessage = event => {
|
||||||
|
this.handleMercureMessage(JSON.parse(event.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onopen = () => {
|
||||||
|
'dev' === this.state.env && console.info('SSE connection opened');
|
||||||
|
|
||||||
|
if (this.state.connectionLost) {
|
||||||
|
'dev' === this.state.env && console.info('SSE reconnected — rejoining channel');
|
||||||
|
this.joinGame();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onerror = () => {
|
||||||
|
'dev' === this.state.env && console.error('SSE connection error / lost');
|
||||||
|
this.setState({ connectionLost: true });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
wInit(gridServer, gridClient) {
|
||||||
this.refs.gridControl.setState({
|
this.refs.gridControl.setState({
|
||||||
grid: this.state.gameInherited ? gridServer : gridClient,
|
grid: this.state.gameInherited ? gridServer : gridClient,
|
||||||
channel: this.state.channel,
|
channel: this.state.channel,
|
||||||
@@ -207,177 +317,86 @@ class MineSeeker extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
wSubscribe(payload, rpcUsers = null) {
|
/** POST /api/game/join — register this player, broadcast subscription event via Mercure */
|
||||||
'dev' === this.state.env && console.info(
|
joinGame() {
|
||||||
('undefined' !== typeof payload.user ? payload.user : 'user') + ' has been subscribed to the channel!',
|
return fetch('/api/game/join/' + this.state.gameAssoc, {
|
||||||
);
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
let firstUser = !rpcUsers;
|
}).catch(e => 'dev' === this.state.env && console.error('Join error', e));
|
||||||
|
|
||||||
null === this.refs.gridControl.state.webPlayer && this.refs.gridControl.setState({
|
|
||||||
webPlayer: payload.user === payload.users.blue
|
|
||||||
|| (
|
|
||||||
firstUser && '' !== payload.users.blueAnon
|
|
||||||
|| !firstUser && ('' === rpcUsers.blueAnon && '' === rpcUsers.blue)
|
|
||||||
)
|
|
||||||
? 'blue' : 'red',
|
|
||||||
});
|
|
||||||
|
|
||||||
/** rwd */
|
|
||||||
(900 > $(document).width()) && this.currectGridSize();
|
|
||||||
|
|
||||||
/** every user has been came */
|
|
||||||
if (
|
|
||||||
2 === payload.userCnt
|
|
||||||
&& (
|
|
||||||
!this.state.connectionLost
|
|
||||||
|| this.state.connectionLost && false === this.refs.gridControl.refs.userControl.state.activePlayer && !this.state.end
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.makeGameStart(payload);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wUnsubscribe(payload) {
|
/** POST /api/game/step — persist a move and fan it out via Mercure */
|
||||||
'dev' === this.state.env && console.info(payload.msg);
|
publishStep(dataPack) {
|
||||||
|
return fetch('/api/game/step/' + this.state.gameAssoc, {
|
||||||
this.refs.gridControl.setState({
|
method: 'POST',
|
||||||
overlay: true,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
overlayTitle: 'The connection has been lost w/ your friend...',
|
body: JSON.stringify(dataPack),
|
||||||
overlaySubTitle: 'Please, restart the game!',
|
}).catch(e => 'dev' === this.state.env && console.error('Step error', e));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wTopic(payload) {
|
// ------------------------------------------------------------------ //
|
||||||
/** Auto-Step if this player is not the current user */
|
// Lifecycle
|
||||||
if (this.refs.gridControl.state.webPlayer !== payload.data.player) {
|
// ------------------------------------------------------------------ //
|
||||||
if (null === payload.data.resign) {
|
|
||||||
'dev' === this.state.env && console.warn(payload.user + ' has been stepped to coords: ' + payload.data.coords[0] + ', ' + payload.data.coords[1]);
|
|
||||||
'dev' === this.state.env && console.warn('Opponent stepped: Auto-Step process');
|
|
||||||
|
|
||||||
this.refs.gridControl.refs.userControl.setState({ bombSelected: payload.data.bomb });
|
|
||||||
|
|
||||||
/** STEP */
|
|
||||||
let points = this.makePointsCalcAndStep(payload.data.coords);
|
|
||||||
|
|
||||||
/** THE END */
|
|
||||||
this.makeGameEndIfItEnds(points.blue, points.red);
|
|
||||||
} else {
|
|
||||||
/** RESIGN */
|
|
||||||
/** THE END */
|
|
||||||
this.resignProcess(payload.data.resign);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Connect - Subscribe */
|
|
||||||
subscribe(rpcUsers = null) {
|
|
||||||
this.state.session.subscribe(
|
|
||||||
this.state.channel,
|
|
||||||
(uri, payload) => {
|
|
||||||
let isTopicEvent = 'undefined' !== typeof payload.data,
|
|
||||||
isNotUnsubscribe = 'undefined' === typeof payload.msg;
|
|
||||||
|
|
||||||
/** CONNECTION */
|
|
||||||
if (isTopicEvent) {
|
|
||||||
this.wTopic(payload);
|
|
||||||
} else {
|
|
||||||
if (isNotUnsubscribe) {
|
|
||||||
this.wSubscribe(payload, rpcUsers);
|
|
||||||
} else {
|
|
||||||
this.wUnsubscribe(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** RECONNECTION */
|
|
||||||
if (2 === payload.userCnt && this.state.connectionLost) {
|
|
||||||
'dev' === this.state.env && console.info('Reconnection process');
|
|
||||||
|
|
||||||
/** PUBLISH */
|
|
||||||
let cache = this.state.stepCache;
|
|
||||||
cache.forEach(item => this.state.session.publish(this.state.channel, item));
|
|
||||||
this.setState({ connectionLost: false, stepCache: [] });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
connectWithWebsocket() {
|
|
||||||
/** Create Websocket w/ Bahnhof.js */
|
|
||||||
let websocket = WS.connect(
|
|
||||||
('true' === this.state.ssl ? 'wss' : 'ws') + '://' + window.location.hostname + '/ws/',
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect
|
|
||||||
* Session is an Autobahn JS WAMP session.
|
|
||||||
*/
|
|
||||||
websocket.on('socket/connect', session => {
|
|
||||||
'dev' === this.state.env && console.info('Successfully connected to the Server!');
|
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
if (!this.state.connectionLost) {
|
if (!this.state.connectionLost) {
|
||||||
let gridClient = this.state.gameInherited || new Grid().state.grid;
|
let gridClient = this.state.gameInherited ? null : new Grid().state.grid;
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* Connect - RPC
|
if (this.state.gameInherited) {
|
||||||
* Send grid information to the server
|
/** Fetch existing grid and player info */
|
||||||
*/
|
const resp = await fetch('/api/game/connect/' + this.state.gameAssoc);
|
||||||
session
|
const b64 = await resp.text();
|
||||||
.call(
|
const serverData = JSON.parse(window.atob(b64));
|
||||||
this.state.gameInherited ? 'mineseeker-rpc/connectGame' : 'mineseeker-rpc/startGame',
|
|
||||||
this.state.gameInherited ? this.state.gameAssoc : [window.btoa(JSON.stringify(gridClient)), this.state.gameAssoc],
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
data => {
|
|
||||||
'dev' === this.state.env && console.info('RPC has been called');
|
|
||||||
|
|
||||||
let serverData = true !== data[0]
|
if ('undefined' === typeof serverData.grid || null === serverData.grid) {
|
||||||
? JSON.parse(window.atob(data))
|
|
||||||
: data;
|
|
||||||
|
|
||||||
/** Check the grid if the user is inherited @see #30 */
|
|
||||||
if ((this.state.gameInherited && 'undefined' !== typeof serverData.grid) || !this.state.gameInherited) {
|
|
||||||
this.wInit(session, serverData.grid, gridClient);
|
|
||||||
this.subscribe(this.state.gameInherited && serverData.users);
|
|
||||||
} else {
|
|
||||||
this.refs.gridControl.setState({
|
this.refs.gridControl.setState({
|
||||||
overlay: true,
|
overlay: true,
|
||||||
overlayTitle: 'This channel does not exists!',
|
overlayTitle: 'This channel does not exists!',
|
||||||
overlaySubTitle: <a href="/play" target="_self">Restart game!</a>,
|
overlaySubTitle: <a href="/play" target="_self">Restart game!</a>,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.error('This channel does not exists!');
|
console.error('This channel does not exists!');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
(error, desc) => 'dev' === this.state.env && console.error(['RPC Error', error, desc]),
|
this.rpcUsers = serverData.users;
|
||||||
);
|
this.openEventSource();
|
||||||
|
this.wInit(serverData.grid, null);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({ session: session });
|
/** Create the game record with this client's grid */
|
||||||
this.subscribe();
|
await fetch('/api/game/start', {
|
||||||
}
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
grid: window.btoa(JSON.stringify(gridClient)),
|
||||||
|
gameAssoc: this.state.gameAssoc,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
this.openEventSource();
|
||||||
* DisConnect
|
this.wInit(null, gridClient);
|
||||||
* Error provides us with some insight into the disconnection: error.reason and error.code
|
}
|
||||||
*/
|
|
||||||
websocket.on('socket/disconnect', error => {
|
|
||||||
'dev' === this.state.env && console.error('Disconnected for ' + error.reason + ' with code ' + error.code);
|
|
||||||
|
|
||||||
6 === error.code && this.setState({ connectionLost: true });
|
'dev' === this.state.env && console.info('Connection initialised — joining channel');
|
||||||
3 === error.code && setTimeout(this.componentDidMount.bind(this), 500);
|
await this.joinGame();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
'dev' === this.state.env && console.error('Connection error', e);
|
||||||
|
setTimeout(() => this.componentDidMount(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
/** Hard-reconnect path */
|
||||||
|
this.openEventSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notify the server when the player closes / navigates away */
|
||||||
|
window.addEventListener('pagehide', () => {
|
||||||
|
navigator.sendBeacon('/api/game/leave/' + this.state.gameAssoc);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** After rendering */
|
|
||||||
componentDidMount() {
|
|
||||||
this.connectWithWebsocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache the steps unless reconnection
|
|
||||||
*
|
|
||||||
* @param dataPack
|
|
||||||
*/
|
|
||||||
cachePublish(dataPack) {
|
cachePublish(dataPack) {
|
||||||
let cache = this.state.stepCache;
|
let cache = this.state.stepCache;
|
||||||
cache.push(dataPack);
|
cache.push(dataPack);
|
||||||
@@ -387,30 +406,24 @@ class MineSeeker extends React.Component {
|
|||||||
onClick(coords) {
|
onClick(coords) {
|
||||||
let activePlayer = this.refs.gridControl.refs.userControl.state.activePlayer ? 'blue' : 'red';
|
let activePlayer = this.refs.gridControl.refs.userControl.state.activePlayer ? 'blue' : 'red';
|
||||||
|
|
||||||
/** if the clicked field is NEVER CLICKED */
|
|
||||||
if (this.refs.gridControl.checkFieldHasBeenNeverClicked(coords[0], coords[1])) {
|
if (this.refs.gridControl.checkFieldHasBeenNeverClicked(coords[0], coords[1])) {
|
||||||
/** Player step and it is the current player */
|
|
||||||
if (activePlayer === this.refs.gridControl.state.webPlayer) {
|
if (activePlayer === this.refs.gridControl.state.webPlayer) {
|
||||||
/** STEP */
|
|
||||||
let points = this.makePointsCalcAndStep(coords);
|
let points = this.makePointsCalcAndStep(coords);
|
||||||
|
|
||||||
/** THE END */
|
|
||||||
this.makeGameEndIfItEnds(points.blue, points.red);
|
this.makeGameEndIfItEnds(points.blue, points.red);
|
||||||
|
|
||||||
let dataPack = {
|
let dataPack = {
|
||||||
'coords': coords,
|
coords: coords,
|
||||||
'player': activePlayer,
|
player: activePlayer,
|
||||||
'bomb': this.refs.gridControl.refs.userControl.state.bombSelected,
|
bomb: this.refs.gridControl.refs.userControl.state.bombSelected,
|
||||||
'redPoints': points.red,
|
redPoints: points.red,
|
||||||
'bluePoints': points.blue,
|
bluePoints: points.blue,
|
||||||
'resign': null,
|
resign: null,
|
||||||
'redExplodedBomb': 'red' === activePlayer && this.refs.gridControl.refs.userControl.state.bombSelected,
|
redExplodedBomb: 'red' === activePlayer && this.refs.gridControl.refs.userControl.state.bombSelected,
|
||||||
'blueExplodedBomb': 'blue' === activePlayer && this.refs.gridControl.refs.userControl.state.bombSelected,
|
blueExplodedBomb: 'blue' === activePlayer && this.refs.gridControl.refs.userControl.state.bombSelected,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** PUBLISH */
|
|
||||||
!this.state.connectionLost
|
!this.state.connectionLost
|
||||||
? this.state.session.publish(this.state.channel, dataPack)
|
? this.publishStep(dataPack)
|
||||||
: this.cachePublish(dataPack);
|
: this.cachePublish(dataPack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,8 +173,11 @@ class GridControl extends React.Component {
|
|||||||
inactivePlayer = userControl.state.activePlayer ? 'red' : 'blue';
|
inactivePlayer = userControl.state.activePlayer ? 'red' : 'blue';
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userControl.state.bombSelected && idx === (max - 1)
|
userControl.state.bombSelected
|
||||||
|| !idx && !userControl.state.bombSelected && 'm' !== currentObject
|
&& idx === (max - 1)
|
||||||
|
|| !idx
|
||||||
|
&& !userControl.state.bombSelected
|
||||||
|
&& 'm' !== currentObject
|
||||||
) {
|
) {
|
||||||
userControl.setState({
|
userControl.setState({
|
||||||
activePlayer: userControl.state.activePlayer ? 0 : 1,
|
activePlayer: userControl.state.activePlayer ? 0 : 1,
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
"doctrine/doctrine-bundle": ">=2.11 <2.14",
|
"doctrine/doctrine-bundle": ">=2.11 <2.14",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||||
"doctrine/orm": "^2.6",
|
"doctrine/orm": "^2.6",
|
||||||
"gos/web-socket-bundle": "^3.0",
|
|
||||||
"symfony/console": "6.4.*",
|
"symfony/console": "6.4.*",
|
||||||
"symfony/flex": "^2.10.0",
|
"symfony/flex": "^2.10.0",
|
||||||
"symfony/framework-bundle": "6.4.*",
|
"symfony/framework-bundle": "6.4.*",
|
||||||
|
"symfony/http-client": "6.4.*",
|
||||||
"symfony/mailer": "6.4.*",
|
"symfony/mailer": "6.4.*",
|
||||||
|
"symfony/mercure": "^0.6",
|
||||||
|
"symfony/mercure-bundle": "*",
|
||||||
"symfony/monolog-bundle": "^3.8",
|
"symfony/monolog-bundle": "^3.8",
|
||||||
"symfony/security-bundle": "6.4.*",
|
"symfony/security-bundle": "6.4.*",
|
||||||
"symfony/translation": "6.4.*",
|
"symfony/translation": "6.4.*",
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"symfony/yaml": "6.4.*"
|
"symfony/yaml": "6.4.*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"firebase/php-jwt": "^7.0",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"symfony/dotenv": "6.4.*",
|
"symfony/dotenv": "6.4.*",
|
||||||
"symfony/maker-bundle": "^1.5",
|
"symfony/maker-bundle": "^1.5",
|
||||||
|
|||||||
1790
composer.lock
generated
1790
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,7 @@ return [
|
|||||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
Gos\Bundle\PubSubRouterBundle\GosPubSubRouterBundle::class => ['all' => true],
|
|
||||||
Gos\Bundle\WebSocketBundle\GosWebSocketBundle::class => ['all' => true],
|
|
||||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
#doctrine_cache:
|
|
||||||
# providers:
|
|
||||||
# redis_cache:
|
|
||||||
# redis:
|
|
||||||
# host: localhost
|
|
||||||
# port: 6379
|
|
||||||
# database: 3
|
|
||||||
# websocket_cache_client:
|
|
||||||
# type: redis
|
|
||||||
# alias: gos_web_socket.client_storage.driver.redis
|
|
||||||
|
|
||||||
gos_web_socket:
|
|
||||||
server:
|
|
||||||
host: 0.0.0.0
|
|
||||||
port: "%mineseeker.websocket%"
|
|
||||||
router:
|
|
||||||
resources:
|
|
||||||
- '%kernel.project_dir%/config/pubsub/routing.yaml'
|
|
||||||
client:
|
|
||||||
firewall: secured_area
|
|
||||||
# session_handler: "@session.handler.pdo"
|
|
||||||
# storage:
|
|
||||||
# driver: "@gos_web_socket.client_storage.driver.predis"
|
|
||||||
# ttl: 28800 #(optionally) time to live if you use redis driver
|
|
||||||
# prefix: client #(optionally) prefix if you use redis driver, create key "client:1" instead key "1"
|
|
||||||
ping:
|
|
||||||
services:
|
|
||||||
- { name: doctrine.dbal.default_connection, type: doctrine, interval: 300 }
|
|
||||||
8
config/packages/mercure.yaml
Normal file
8
config/packages/mercure.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
mercure:
|
||||||
|
hubs:
|
||||||
|
default:
|
||||||
|
url: '%env(MERCURE_URL)%'
|
||||||
|
public_url: '%env(MERCURE_PUBLIC_URL)%'
|
||||||
|
jwt:
|
||||||
|
secret: '%env(MERCURE_JWT_SECRET)%'
|
||||||
|
publish: '*'
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
## Topic Configuration
|
|
||||||
mineseeker_topic:
|
|
||||||
channel: mineseeker/channel/{game}
|
|
||||||
callback: 'mineseeker.topic'
|
|
||||||
# requirements:
|
|
||||||
# method:
|
|
||||||
# path: '[a-z1-9A-Z]+'
|
|
||||||
|
|
||||||
# Remote Procedure Call Configuration
|
|
||||||
mineseeker_rpc:
|
|
||||||
channel: mineseeker-rpc/{method}
|
|
||||||
callback: 'mineseeker.rpc'
|
|
||||||
requirements:
|
|
||||||
method: "[a-zA-Z_]+"
|
|
||||||
@@ -25,3 +25,29 @@ MineSeekerBundle_contact:
|
|||||||
MineSeekerBundle_landing:
|
MineSeekerBundle_landing:
|
||||||
path: /landing-page
|
path: /landing-page
|
||||||
controller: App\Controller\GameController::landing
|
controller: App\Controller\GameController::landing
|
||||||
|
|
||||||
|
MineSeekerBundle_api_game_start:
|
||||||
|
path: /api/game/start
|
||||||
|
controller: App\Controller\MercureController::start
|
||||||
|
methods: [POST]
|
||||||
|
|
||||||
|
MineSeekerBundle_api_game_connect:
|
||||||
|
path: /api/game/connect/{gameAssoc}
|
||||||
|
controller: App\Controller\MercureController::connect
|
||||||
|
methods: [GET]
|
||||||
|
|
||||||
|
MineSeekerBundle_api_game_join:
|
||||||
|
path: /api/game/join/{gameAssoc}
|
||||||
|
controller: App\Controller\MercureController::join
|
||||||
|
methods: [POST]
|
||||||
|
|
||||||
|
MineSeekerBundle_api_game_step:
|
||||||
|
path: /api/game/step/{gameAssoc}
|
||||||
|
controller: App\Controller\MercureController::step
|
||||||
|
methods: [POST]
|
||||||
|
|
||||||
|
MineSeekerBundle_api_game_leave:
|
||||||
|
path: /api/game/leave/{gameAssoc}
|
||||||
|
controller: App\Controller\MercureController::leave
|
||||||
|
methods: [POST]
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,6 @@
|
|||||||
parameters:
|
parameters:
|
||||||
locale: 'en'
|
locale: 'en'
|
||||||
jotunheimr.version: 1.1.0-20191026
|
jotunheimr.version: 1.1.0-20191026
|
||||||
facebook.api: 320599508311862
|
|
||||||
facebook.api-secret: 18d4f48cdd274bccee2678e5eff3f557
|
|
||||||
facebook.version: 'v2.8'
|
|
||||||
facebook.scope: 'public_profile,email,user_friends'
|
|
||||||
mineseeker.websocket: 6450
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
@@ -22,44 +17,10 @@ services:
|
|||||||
# this creates a service per class whose id is the fully-qualified class name
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
App\:
|
App\:
|
||||||
resource: '../src/*'
|
resource: '../src/*'
|
||||||
exclude: '../src/{Command/WebsocketServerCommand.php,Entity,Migrations,Tests,Kernel.php,Periodic}'
|
exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
|
||||||
|
|
||||||
# controllers are imported separately to make sure services can be injected
|
# controllers are imported separately to make sure services can be injected
|
||||||
# as action arguments even if you don't extend any base controller class
|
# as action arguments even if you don't extend any base controller class
|
||||||
App\Controller\:
|
App\Controller\:
|
||||||
resource: '../src/Controller'
|
resource: '../src/Controller'
|
||||||
tags: [ 'controller.service_arguments' ]
|
tags: [ 'controller.service_arguments' ]
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
|
||||||
# please note that last definitions always *replace* previous ones
|
|
||||||
|
|
||||||
mineseeker.topic_sample_service:
|
|
||||||
class: App\Topic\MineseekerTopic
|
|
||||||
tags:
|
|
||||||
- { name: gos_web_socket.topic }
|
|
||||||
|
|
||||||
mineseeker.rpc_sample_service:
|
|
||||||
class: App\Rpc\MineseekerRpc
|
|
||||||
public: true
|
|
||||||
tags:
|
|
||||||
- { name: gos_web_socket.rpc }
|
|
||||||
|
|
||||||
# Override gos WebsocketServerCommand to avoid --profile conflict with Symfony 6.4 global option
|
|
||||||
gos_web_socket.command.websocket_server:
|
|
||||||
class: App\Command\WebsocketServerCommand
|
|
||||||
arguments:
|
|
||||||
- '@gos_web_socket.server.launcher'
|
|
||||||
- '%gos_web_socket.server.host%'
|
|
||||||
- '%gos_web_socket.server.port%'
|
|
||||||
- '@gos_web_socket.registry.server'
|
|
||||||
tags:
|
|
||||||
- { name: console.command, command: 'gos:websocket:server' }
|
|
||||||
|
|
||||||
gos_web_socket_server.client_event.listener:
|
|
||||||
class: App\EventListener\MineseekerClientEventListener
|
|
||||||
tags:
|
|
||||||
- { name: kernel.event_listener, event: 'gos_web_socket.client_connected', method: onClientConnect }
|
|
||||||
- { name: kernel.event_listener, event: 'gos_web_socket.client_disconnected', method: onClientDisconnect }
|
|
||||||
- { name: kernel.event_listener, event: 'gos_web_socket.client_error', method: onClientError }
|
|
||||||
- { name: kernel.event_listener, event: 'gos_web_socket.server_launched', method: onServerStart }
|
|
||||||
- { name: kernel.event_listener, event: 'gos_web_socket.client_rejected', method: onClientRejected }
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.2.0",
|
"@fortawesome/fontawesome-free": "^5.2.0",
|
||||||
"autobahn": "^19.10.1",
|
|
||||||
"bootstrap": "3",
|
"bootstrap": "3",
|
||||||
"buffer": "^5.4.3",
|
"buffer": "^5.4.3",
|
||||||
"howler": "^2.1.2",
|
"howler": "^2.1.2",
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
<?php declare(strict_types=1);
|
|
||||||
/*
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2026 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Command;
|
|
||||||
|
|
||||||
use Gos\Bundle\WebSocketBundle\Server\App\Registry\ServerRegistry;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Server\ServerLauncherInterface;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Completion\CompletionInput;
|
|
||||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
|
||||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces gos WebsocketServerCommand to avoid the --profile option conflict
|
|
||||||
* introduced when Symfony 6.4 added --profile as a global console option.
|
|
||||||
*/
|
|
||||||
#[AsCommand(name: 'gos:websocket:server', description: 'Starts the websocket server')]
|
|
||||||
final class WebsocketServerCommand extends Command
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly ServerLauncherInterface $serverLauncher,
|
|
||||||
private readonly string $host,
|
|
||||||
private readonly int $port,
|
|
||||||
private readonly ?ServerRegistry $serverRegistry = null,
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this
|
|
||||||
->addArgument('name', InputArgument::OPTIONAL, 'Name of the server to start, launches the first registered server if not specified')
|
|
||||||
->addOption('ws-profile', 'm', InputOption::VALUE_NONE, 'Enable profiling of the websocket server')
|
|
||||||
->addOption('host', 'a', InputOption::VALUE_OPTIONAL, 'The hostname of the websocket server')
|
|
||||||
->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'The port of the websocket server');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
/** @var string $name */
|
|
||||||
$name = $input->getArgument('name');
|
|
||||||
|
|
||||||
/** @var string $host */
|
|
||||||
$host = null === $input->getOption('host') ? $this->host : $input->getOption('host');
|
|
||||||
|
|
||||||
/** @var int|string $port */
|
|
||||||
$port = null === $input->getOption('port') ? $this->port : $input->getOption('port');
|
|
||||||
|
|
||||||
if (!is_numeric($port)) {
|
|
||||||
throw new InvalidArgumentException('The port option must be a numeric value.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var bool $profile */
|
|
||||||
$profile = (bool) $input->getOption('ws-profile');
|
|
||||||
|
|
||||||
$this->serverLauncher->launch($name, $host, (int) $port, $profile);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
|
||||||
{
|
|
||||||
if ($input->mustSuggestArgumentValuesFor('name') && null !== $this->serverRegistry) {
|
|
||||||
$suggestions->suggestValues(array_keys($this->serverRegistry->getServers()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ 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\RequestStack;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +29,10 @@ class GameController extends AbstractController
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
#[Autowire(env: 'APP_ENV')]
|
#[Autowire(env: 'APP_ENV')]
|
||||||
private readonly string $env,
|
private readonly string $env,
|
||||||
private readonly RequestStack $request,
|
#[Autowire(env: 'MERCURE_PUBLIC_URL')]
|
||||||
|
private readonly string $mercurePublicUrl,
|
||||||
|
#[Autowire(env: 'MERCURE_SUBSCRIBER_JWT')]
|
||||||
|
private readonly string $mercureSubscriberJwt,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +45,8 @@ class GameController extends AbstractController
|
|||||||
{
|
{
|
||||||
return $this->render('Game/play.html.twig', [
|
return $this->render('Game/play.html.twig', [
|
||||||
'env' => $this->env,
|
'env' => $this->env,
|
||||||
'ssl' => $this->request->getCurrentRequest()->isSecure() ? 'true' : 'false',
|
'mercure_hub_url' => $this->mercurePublicUrl,
|
||||||
|
'mercure_subscriber_jwt' => $this->mercureSubscriberJwt,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
98
src/Controller/MercureController.php
Normal file
98
src/Controller/MercureController.php
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Util\RpcManager;
|
||||||
|
use App\Util\TopicManager;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class MercureController
|
||||||
|
*
|
||||||
|
* Handles HTTP API endpoints that replace the former WebSocket RPC and Topic handlers.
|
||||||
|
* Client → Server communication is via HTTP POST/GET.
|
||||||
|
* Server → Client broadcasting is via Mercure (SSE).
|
||||||
|
*
|
||||||
|
* @package App\Controller
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 09.
|
||||||
|
*/
|
||||||
|
class MercureController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TopicManager $topicManager,
|
||||||
|
private readonly RpcManager $rpcManager,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/game/start — save the grid and create the PlayedGame record */
|
||||||
|
public function start(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->toArray();
|
||||||
|
$result = $this->rpcManager->saveGrid([$data['grid'], $data['gameAssoc']]);
|
||||||
|
|
||||||
|
return $this->json(['success' => $result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/game/connect/{gameAssoc} — return grid + current user info (base64 JSON) */
|
||||||
|
public function connect(string $gameAssoc): Response
|
||||||
|
{
|
||||||
|
$payload = $this->rpcManager->getConnectInformation($gameAssoc);
|
||||||
|
|
||||||
|
return new Response($payload, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/game/join/{gameAssoc} — register the player, broadcast subscription event via Mercure */
|
||||||
|
public function join(string $gameAssoc, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->topicManager->subscribe($gameAssoc, $this->resolveUserName($request), $this->getUser());
|
||||||
|
|
||||||
|
return $this->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/game/step/{gameAssoc} — persist the step and broadcast game event via Mercure */
|
||||||
|
public function step(string $gameAssoc, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->topicManager->publish($gameAssoc, $this->resolveUserName($request), $request->toArray());
|
||||||
|
|
||||||
|
return $this->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/game/leave/{gameAssoc} — broadcast disconnect event via Mercure */
|
||||||
|
public function leave(string $gameAssoc, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->topicManager->unSubscribe($gameAssoc, $this->resolveUserName($request));
|
||||||
|
|
||||||
|
return $this->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveUserName(Request $request): string
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if (null !== $user) {
|
||||||
|
return $user->getUserIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = $request->getSession()->getId();
|
||||||
|
if (empty($sessionId)) {
|
||||||
|
$sessionId = bin2hex(random_bytes(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'anon_' . $sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use App\Repository\PlayedGameRepository;
|
use App\Repository\PlayedGameRepository;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping\Column;
|
use Doctrine\ORM\Mapping\Column;
|
||||||
use Doctrine\ORM\Mapping\Entity;
|
use Doctrine\ORM\Mapping\Entity;
|
||||||
@@ -19,6 +21,7 @@ use Doctrine\ORM\Mapping\GeneratedValue;
|
|||||||
use Doctrine\ORM\Mapping\Id;
|
use Doctrine\ORM\Mapping\Id;
|
||||||
use Doctrine\ORM\Mapping\JoinColumn;
|
use Doctrine\ORM\Mapping\JoinColumn;
|
||||||
use Doctrine\ORM\Mapping\ManyToOne;
|
use Doctrine\ORM\Mapping\ManyToOne;
|
||||||
|
use Doctrine\ORM\Mapping\OneToMany;
|
||||||
use Doctrine\ORM\Mapping\OneToOne;
|
use Doctrine\ORM\Mapping\OneToOne;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,107 +83,28 @@ class PlayedGame
|
|||||||
#[JoinColumn(name: 'blue_anon', referencedColumnName: 'id', nullable: true)]
|
#[JoinColumn(name: 'blue_anon', referencedColumnName: 'id', nullable: true)]
|
||||||
private ?Gamer $blueAnon = null;
|
private ?Gamer $blueAnon = null;
|
||||||
|
|
||||||
#[OneToOne(mappedBy: 'playedGame')]
|
#[OneToMany(mappedBy: 'playedGame', targetEntity: Step::class)]
|
||||||
private ?Step $step = null;
|
private Collection $steps;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->steps = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setId(?int $id): self
|
|
||||||
{
|
|
||||||
$this->id = $id;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getGameAssoc(): ?string
|
public function getGameAssoc(): ?string
|
||||||
{
|
{
|
||||||
return $this->gameAssoc;
|
return $this->gameAssoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setGameAssoc(?string $gameAssoc): self
|
public function setGameAssoc(?string $gameAssoc): void
|
||||||
{
|
{
|
||||||
$this->gameAssoc = $gameAssoc;
|
$this->gameAssoc = $gameAssoc;
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRedPoints(): ?int
|
|
||||||
{
|
|
||||||
return $this->redPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setRedPoints(?int $redPoints): self
|
|
||||||
{
|
|
||||||
$this->redPoints = $redPoints;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getBluePoints(): ?int
|
|
||||||
{
|
|
||||||
return $this->bluePoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setBluePoints(?int $bluePoints): self
|
|
||||||
{
|
|
||||||
$this->bluePoints = $bluePoints;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRedExplodedBomb(): ?bool
|
|
||||||
{
|
|
||||||
return $this->redExplodedBomb;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setRedExplodedBomb(?bool $redExplodedBomb): self
|
|
||||||
{
|
|
||||||
$this->redExplodedBomb = $redExplodedBomb;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getBlueExplodedBomb(): ?bool
|
|
||||||
{
|
|
||||||
return $this->blueExplodedBomb;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setBlueExplodedBomb(?bool $blueExplodedBomb): self
|
|
||||||
{
|
|
||||||
$this->blueExplodedBomb = $blueExplodedBomb;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getResign(): ?string
|
|
||||||
{
|
|
||||||
return $this->resign;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setResign(?string $resign): self
|
|
||||||
{
|
|
||||||
$this->resign = $resign;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCreated(): ?DateTime
|
|
||||||
{
|
|
||||||
return $this->created;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCreated(?DateTime $created): self
|
|
||||||
{
|
|
||||||
$this->created = $created;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUpdated(): ?DateTime
|
|
||||||
{
|
|
||||||
return $this->updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setUpdated(?DateTime $updated): self
|
|
||||||
{
|
|
||||||
$this->updated = $updated;
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getGrid(): ?Grid
|
public function getGrid(): ?Grid
|
||||||
@@ -188,10 +112,9 @@ class PlayedGame
|
|||||||
return $this->grid;
|
return $this->grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setGrid(?Grid $grid): self
|
public function setGrid(?Grid $grid): void
|
||||||
{
|
{
|
||||||
$this->grid = $grid;
|
$this->grid = $grid;
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRed(): ?User
|
public function getRed(): ?User
|
||||||
@@ -199,10 +122,9 @@ class PlayedGame
|
|||||||
return $this->red;
|
return $this->red;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setRed(?User $red): self
|
public function setRed(?User $red): void
|
||||||
{
|
{
|
||||||
$this->red = $red;
|
$this->red = $red;
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRedAnon(): ?Gamer
|
public function getRedAnon(): ?Gamer
|
||||||
@@ -210,10 +132,9 @@ class PlayedGame
|
|||||||
return $this->redAnon;
|
return $this->redAnon;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setRedAnon(?Gamer $redAnon): self
|
public function setRedAnon(?Gamer $redAnon): void
|
||||||
{
|
{
|
||||||
$this->redAnon = $redAnon;
|
$this->redAnon = $redAnon;
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBlue(): ?User
|
public function getBlue(): ?User
|
||||||
@@ -221,10 +142,9 @@ class PlayedGame
|
|||||||
return $this->blue;
|
return $this->blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setBlue(?User $blue): self
|
public function setBlue(?User $blue): void
|
||||||
{
|
{
|
||||||
$this->blue = $blue;
|
$this->blue = $blue;
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBlueAnon(): ?Gamer
|
public function getBlueAnon(): ?Gamer
|
||||||
@@ -232,20 +152,83 @@ class PlayedGame
|
|||||||
return $this->blueAnon;
|
return $this->blueAnon;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setBlueAnon(?Gamer $blueAnon): self
|
public function setBlueAnon(?Gamer $blueAnon): void
|
||||||
{
|
{
|
||||||
$this->blueAnon = $blueAnon;
|
$this->blueAnon = $blueAnon;
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStep(): ?Step
|
public function getRedPoints(): ?int
|
||||||
{
|
{
|
||||||
return $this->step;
|
return $this->redPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStep(?Step $step): self
|
public function setRedPoints(?int $redPoints): void
|
||||||
{
|
{
|
||||||
$this->step = $step;
|
$this->redPoints = $redPoints;
|
||||||
return $this;
|
}
|
||||||
|
|
||||||
|
public function getBluePoints(): ?int
|
||||||
|
{
|
||||||
|
return $this->bluePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBluePoints(?int $bluePoints): void
|
||||||
|
{
|
||||||
|
$this->bluePoints = $bluePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRedExplodedBomb(): ?bool
|
||||||
|
{
|
||||||
|
return $this->redExplodedBomb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRedExplodedBomb(?bool $redExplodedBomb): void
|
||||||
|
{
|
||||||
|
$this->redExplodedBomb = $redExplodedBomb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBlueExplodedBomb(): ?bool
|
||||||
|
{
|
||||||
|
return $this->blueExplodedBomb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBlueExplodedBomb(?bool $blueExplodedBomb): void
|
||||||
|
{
|
||||||
|
$this->blueExplodedBomb = $blueExplodedBomb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResign(): ?string
|
||||||
|
{
|
||||||
|
return $this->resign;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResign(?string $resign): void
|
||||||
|
{
|
||||||
|
$this->resign = $resign;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreated(): ?DateTime
|
||||||
|
{
|
||||||
|
return $this->created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreated(?DateTime $created): void
|
||||||
|
{
|
||||||
|
$this->created = $created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdated(): ?DateTime
|
||||||
|
{
|
||||||
|
return $this->updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdated(?DateTime $updated): void
|
||||||
|
{
|
||||||
|
$this->updated = $updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSteps(): Collection
|
||||||
|
{
|
||||||
|
return $this->steps;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Step
|
|||||||
#[Column(nullable: true)]
|
#[Column(nullable: true)]
|
||||||
private ?bool $wBomb = null;
|
private ?bool $wBomb = null;
|
||||||
|
|
||||||
#[ManyToOne(inversedBy: 'step')]
|
#[ManyToOne(inversedBy: 'steps')]
|
||||||
private ?PlayedGame $playedGame = null;
|
private ?PlayedGame $playedGame = null;
|
||||||
|
|
||||||
#[Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
#[Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2019 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\EventListener;
|
|
||||||
|
|
||||||
use Gos\Bundle\WebSocketBundle\Event\ClientConnectedEvent;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Event\ClientDisconnectedEvent;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Event\ClientErrorEvent;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Event\ClientRejectedEvent;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Event\ServerLaunchedEvent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class MineseekerClientEventListener
|
|
||||||
*
|
|
||||||
* @package App\EventListener
|
|
||||||
* @author Lang <https://www.splendidbear.org>
|
|
||||||
* @category Class
|
|
||||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
|
||||||
* @link www.splendidbear.org
|
|
||||||
* @since 2026. 04. 09.
|
|
||||||
*/
|
|
||||||
class MineseekerClientEventListener
|
|
||||||
{
|
|
||||||
public function onClientConnect(ClientConnectedEvent $event): void
|
|
||||||
{
|
|
||||||
$conn = $event->getConnection();
|
|
||||||
echo $conn->resourceId . ' connected' . PHP_EOL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onClientDisconnect(ClientDisconnectedEvent $event): void
|
|
||||||
{
|
|
||||||
$conn = $event->getConnection();
|
|
||||||
echo $conn->resourceId . ' disconnected' . PHP_EOL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onClientError(ClientErrorEvent $event): void
|
|
||||||
{
|
|
||||||
$conn = $event->getConnection();
|
|
||||||
$e = $event->getException();
|
|
||||||
echo 'connection error occurred: ' . $e->getMessage() . PHP_EOL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onServerStart(ServerLaunchedEvent $event): void
|
|
||||||
{
|
|
||||||
echo 'Server was successfully started !' . PHP_EOL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onClientRejected(ClientRejectedEvent $event): void
|
|
||||||
{
|
|
||||||
$origin = $event->getOrigin();
|
|
||||||
echo 'connection rejected from ' . $origin . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
|
|
||||||
namespace App\Interfaces;
|
namespace App\Interfaces;
|
||||||
|
|
||||||
use Ratchet\ConnectionInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Ratchet\Wamp\Topic;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface TopicManagerInterface
|
* Interface TopicManagerInterface
|
||||||
@@ -25,9 +24,9 @@ use Ratchet\Wamp\Topic;
|
|||||||
*/
|
*/
|
||||||
interface TopicManagerInterface
|
interface TopicManagerInterface
|
||||||
{
|
{
|
||||||
public function subscribe(Topic $topic, ConnectionInterface $connection): void;
|
public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user): void;
|
||||||
|
|
||||||
public function unSubscribe(Topic $topic, ConnectionInterface $connection): void;
|
public function unSubscribe(string $gameAssoc, string $userName): void;
|
||||||
|
|
||||||
public function publish(Topic $topic, ConnectionInterface $connection, $event): void;
|
public function publish(string $gameAssoc, string $userName, array $event): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2019 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Interfaces;
|
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface WebsocketManagerInterface
|
|
||||||
*
|
|
||||||
* @package App\Interfaces
|
|
||||||
* @author Lang <https://www.splendidbear.org>
|
|
||||||
* @category Interface
|
|
||||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
|
||||||
* @link www.splendidbear.org
|
|
||||||
* @since 2026. 04. 09.
|
|
||||||
*/
|
|
||||||
interface WebsocketManagerInterface
|
|
||||||
{
|
|
||||||
public function reConnect(EntityManagerInterface $entityManager): ?EntityManagerInterface;
|
|
||||||
}
|
|
||||||
0
src/Migrations/.gitignore
vendored
0
src/Migrations/.gitignore
vendored
42
src/Migrations/2026/04/Version20260409194708.php
Normal file
42
src/Migrations/2026/04/Version20260409194708.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Migrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Version20260409194708
|
||||||
|
*
|
||||||
|
* @package App\Migrations
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 09.
|
||||||
|
*/
|
||||||
|
final class Version20260409194708 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Refactor entities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE step RENAME COLUMN wbomb TO w_bomb');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE step RENAME COLUMN w_bomb TO wbomb');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2019 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Periodic;
|
|
||||||
|
|
||||||
use Gos\Bundle\WebSocketBundle\Periodic\PdoPeriodicPing;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Periodic\PeriodicInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class MinePeriodic
|
|
||||||
*
|
|
||||||
* @package App\Periodic
|
|
||||||
* @author Lang <https://www.splendidbear.org>
|
|
||||||
* @category Class
|
|
||||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
|
||||||
* @link www.splendidbear.org
|
|
||||||
* @since 2026. 04. 09.
|
|
||||||
*
|
|
||||||
* @method MinePeriodic|null find($id, $lockMode = null, $lockVersion = null)
|
|
||||||
* @method MinePeriodic|null findOneBy(array $criteria, array $orderBy = null)
|
|
||||||
* @method MinePeriodic[] findAll()
|
|
||||||
* @method MinePeriodic[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
|
||||||
*/
|
|
||||||
class MinePeriodic implements PeriodicInterface
|
|
||||||
{
|
|
||||||
public function __construct(private PdoPeriodicPing $ping) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is executed every 5 seconds.
|
|
||||||
*
|
|
||||||
* For more advanced functionality, try injecting
|
|
||||||
* a Topic Service to perform actions on your
|
|
||||||
* connections every x seconds.
|
|
||||||
*/
|
|
||||||
public function tick(): void
|
|
||||||
{
|
|
||||||
$this->ping->tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTimeout(): int
|
|
||||||
{
|
|
||||||
return 300;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<?php declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2019 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Rpc;
|
|
||||||
|
|
||||||
use App\Util\RpcManager;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Router\WampRequest;
|
|
||||||
use Gos\Bundle\WebSocketBundle\RPC\RpcInterface;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class MineseekerRpc
|
|
||||||
*
|
|
||||||
* @package App\Rpc
|
|
||||||
* @author Lang <https://www.splendidbear.org>
|
|
||||||
* @category Class
|
|
||||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
|
||||||
* @link www.splendidbear.org
|
|
||||||
* @since 2026. 04. 09.
|
|
||||||
*/
|
|
||||||
class MineseekerRpc implements RpcInterface
|
|
||||||
{
|
|
||||||
public function __construct(private RpcManager $manager) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of RPC, use for pubsub router (see step3)
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return 'mineseeker.rpc';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It handles the game starting processes
|
|
||||||
*
|
|
||||||
* @param ConnectionInterface $connection
|
|
||||||
* @param WampRequest $request
|
|
||||||
* @param array $params
|
|
||||||
*
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
public function startGame(ConnectionInterface $connection, WampRequest $request, array $params): bool
|
|
||||||
{
|
|
||||||
return $this->manager->saveGrid($params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It handles when somebody trying to connect to the party
|
|
||||||
*
|
|
||||||
* @param ConnectionInterface $connection
|
|
||||||
* @param WampRequest $request
|
|
||||||
* @param array $params
|
|
||||||
*
|
|
||||||
* @return string Json string for frontend w/ numbering consideration. (=> a number is not string)
|
|
||||||
*/
|
|
||||||
public function connectGame(ConnectionInterface $connection, WampRequest $request, array $params): string
|
|
||||||
{
|
|
||||||
return $this->manager->getConnectInformation($params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<?php declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2019 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Topic;
|
|
||||||
|
|
||||||
use App\Util\TopicManager;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Router\WampRequest;
|
|
||||||
use Gos\Bundle\WebSocketBundle\Topic\TopicInterface;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use Ratchet\Wamp\Topic;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class MineseekerTopic
|
|
||||||
*
|
|
||||||
* @package App\Topic
|
|
||||||
* @author Lang <https://www.splendidbear.org>
|
|
||||||
* @category Class
|
|
||||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
|
||||||
* @link www.splendidbear.org
|
|
||||||
* @since 2026. 04. 09.
|
|
||||||
*/
|
|
||||||
class MineseekerTopic implements TopicInterface
|
|
||||||
{
|
|
||||||
public function __construct(private TopicManager $manager) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like RPC is will use to prefix the channel
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return 'mineseeker.topic';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will receive any Subscription requests for this topic.
|
|
||||||
*
|
|
||||||
* @param ConnectionInterface $connection
|
|
||||||
* @param Topic $topic
|
|
||||||
* @param WampRequest $request
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function onSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request): void
|
|
||||||
{
|
|
||||||
$this->manager->subscribe($topic, $connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will receive any UnSubscription requests for this topic.
|
|
||||||
*
|
|
||||||
* @param ConnectionInterface $connection
|
|
||||||
* @param Topic $topic
|
|
||||||
* @param WampRequest $request
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function onUnSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request): void
|
|
||||||
{
|
|
||||||
$this->manager->unSubscribe($topic, $connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will receive any Publish requests for this topic.
|
|
||||||
*
|
|
||||||
* @param ConnectionInterface $connection
|
|
||||||
* @param Topic $topic
|
|
||||||
* @param WampRequest $request
|
|
||||||
* @param $event
|
|
||||||
* @param array $exclude
|
|
||||||
* @param array $eligible
|
|
||||||
*
|
|
||||||
* @return mixed|void
|
|
||||||
* @internal param Topic $Topic
|
|
||||||
* @internal param array $eligibles
|
|
||||||
*/
|
|
||||||
public function onPublish(
|
|
||||||
ConnectionInterface $connection,
|
|
||||||
Topic $topic,
|
|
||||||
WampRequest $request,
|
|
||||||
$event,
|
|
||||||
array $exclude,
|
|
||||||
array $eligible
|
|
||||||
) {
|
|
||||||
$this->manager->publish($topic, $connection, $event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,9 +16,10 @@ use App\Entity\PlayedGame;
|
|||||||
use App\Interfaces\RpcManagerInterface;
|
use App\Interfaces\RpcManagerInterface;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\ORMException;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use JsonException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class RpcManager
|
* Class RpcManager
|
||||||
@@ -30,13 +31,12 @@ use Psr\Log\LoggerInterface;
|
|||||||
* @link www.splendidbear.org
|
* @link www.splendidbear.org
|
||||||
* @since 2026. 04. 09.
|
* @since 2026. 04. 09.
|
||||||
*/
|
*/
|
||||||
class RpcManager extends WebsocketManager implements RpcManagerInterface
|
class RpcManager implements RpcManagerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
private LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
parent::__construct($logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getConnectInformation($params): string
|
public function getConnectInformation($params): string
|
||||||
@@ -45,17 +45,33 @@ class RpcManager extends WebsocketManager implements RpcManagerInterface
|
|||||||
$grid = $this->getGrid($gameAssoc);
|
$grid = $this->getGrid($gameAssoc);
|
||||||
$users = null !== $grid ? $this->getUsers($gameAssoc) : null;
|
$users = null !== $grid ? $this->getUsers($gameAssoc) : null;
|
||||||
|
|
||||||
|
try {
|
||||||
return base64_encode(json_encode([
|
return base64_encode(json_encode([
|
||||||
'grid' => $grid,
|
'grid' => $grid,
|
||||||
'users' => $users,
|
'users' => $users,
|
||||||
], JSON_THROW_ON_ERROR, 512));
|
], JSON_THROW_ON_ERROR, 512));
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
throw new RuntimeException($e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveGrid($data): bool
|
public function saveGrid($data): bool
|
||||||
{
|
{
|
||||||
|
$existingGame = $this->entityManager
|
||||||
|
->getRepository(PlayedGame::class)
|
||||||
|
->findOneByGameAssoc($data[1]);
|
||||||
|
|
||||||
|
if (null !== $existingGame) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$playedGame = new PlayedGame();
|
$playedGame = new PlayedGame();
|
||||||
$grid = new Grid();
|
$grid = new Grid();
|
||||||
|
try {
|
||||||
$rows = json_decode(base64_decode($data[0]), true, 512, JSON_THROW_ON_ERROR);
|
$rows = json_decode(base64_decode($data[0]), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
throw new RuntimeException($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
@@ -66,7 +82,6 @@ class RpcManager extends WebsocketManager implements RpcManagerInterface
|
|||||||
/** Save Row */
|
/** Save Row */
|
||||||
$gridRow->setGrid($grid);
|
$gridRow->setGrid($grid);
|
||||||
$this->entityManager->persist($gridRow);
|
$this->entityManager->persist($gridRow);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Save Grid */
|
/** Save Grid */
|
||||||
@@ -81,8 +96,6 @@ class RpcManager extends WebsocketManager implements RpcManagerInterface
|
|||||||
$this->entityManager->persist($playedGame);
|
$this->entityManager->persist($playedGame);
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
} catch (ORMException $e) {
|
|
||||||
$this->logger->error($e->getMessage());
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logger->error($e->getMessage());
|
$this->logger->error($e->getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,20 @@
|
|||||||
|
|
||||||
namespace App\Util;
|
namespace App\Util;
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
use App\Entity\Gamer;
|
use App\Entity\Gamer;
|
||||||
use App\Entity\PlayedGame;
|
use App\Entity\PlayedGame;
|
||||||
use App\Entity\Step;
|
use App\Entity\Step;
|
||||||
|
use App\Entity\User;
|
||||||
use App\Interfaces\TopicManagerInterface;
|
use App\Interfaces\TopicManagerInterface;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Gos\Bundle\WebSocketBundle\Client\ClientManipulatorInterface;
|
use RuntimeException;
|
||||||
|
use JsonException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Ratchet\ConnectionInterface;
|
use Symfony\Component\Mercure\HubInterface;
|
||||||
use Ratchet\Wamp\Topic;
|
use Symfony\Component\Mercure\Update;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class TopicManager
|
* Class TopicManager
|
||||||
@@ -35,79 +35,110 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|||||||
* @link www.splendidbear.org
|
* @link www.splendidbear.org
|
||||||
* @since 2026. 04. 09.
|
* @since 2026. 04. 09.
|
||||||
*/
|
*/
|
||||||
class TopicManager extends WebsocketManager implements TopicManagerInterface
|
class TopicManager implements TopicManagerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected ClientManipulatorInterface $clientManipulator,
|
private readonly HubInterface $hub,
|
||||||
protected EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
protected RequestStack $requestStack,
|
private readonly LoggerInterface $logger
|
||||||
protected LoggerInterface $logger
|
) {
|
||||||
)
|
|
||||||
{
|
|
||||||
parent::__construct($logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subscribe(Topic $topic, ConnectionInterface $connection): void
|
public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user): void
|
||||||
{
|
{
|
||||||
/** this will broadcast the message to ALL subscribers of this topic. */
|
$playedGame = $this->getPlayedGame($gameAssoc);
|
||||||
$user = $this->clientManipulator->getClient($connection);
|
if (null === $playedGame) {
|
||||||
$userName = $user->getUserIdentifier() ?: 'anon_' . $connection->resourceId;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/** if more user wants to connect than 2 to one channel */
|
$users = $this->getUserCollection($playedGame);
|
||||||
if ($topic->count() > 2) {
|
$count = $this->getPlayerCount($users);
|
||||||
$topic->remove($connection);
|
$isKnown = in_array($userName, array_filter(array_values($users)), true);
|
||||||
} else {
|
|
||||||
$users = $this->controlUsers($topic, $userName, $user);
|
|
||||||
|
|
||||||
$topic->broadcast([
|
/** Reject a third player who is not a reconnecting player */
|
||||||
'userTopicId' => $connection->resourceId,
|
if ($count >= 2 && !$isKnown) {
|
||||||
'channel' => $topic->getId(),
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save the player to the database on a fresh join */
|
||||||
|
if (!$isKnown && $count < 2) {
|
||||||
|
$users = $this->saveUserToDb($gameAssoc, $userName, $user, $count + 1);
|
||||||
|
$count = $this->getPlayerCount($users);
|
||||||
|
}
|
||||||
|
|
||||||
|
$topic = 'mineseeker/channel/' . $gameAssoc;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->hub->publish(new Update(
|
||||||
|
$topic,
|
||||||
|
json_encode([
|
||||||
|
'userTopicId' => $userName,
|
||||||
|
'channel' => $topic,
|
||||||
'user' => $userName,
|
'user' => $userName,
|
||||||
'userCnt' => $topic->count(),
|
'userCnt' => $count,
|
||||||
'users' => $users
|
'users' => $users,
|
||||||
]);
|
], JSON_THROW_ON_ERROR)
|
||||||
|
));
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
throw new RuntimeException($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function unSubscribe(Topic $topic, ConnectionInterface $connection): void
|
public function unSubscribe(string $gameAssoc, string $userName): void
|
||||||
{
|
{
|
||||||
/** This will broadcasts the message to ALL subscribers of this topic. */
|
$topic = 'mineseeker/channel/' . $gameAssoc;
|
||||||
$topic->broadcast(['msg' => $connection->resourceId . ' has left ' . $topic->getId()]);
|
|
||||||
|
$this->hub->publish(new Update(
|
||||||
|
$topic,
|
||||||
|
json_encode(['msg' => $userName . ' has left ' . $topic])
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function publish(Topic $topic, ConnectionInterface $connection, $event): void
|
public function publish(string $gameAssoc, string $userName, array $event): void
|
||||||
{
|
{
|
||||||
$user = $this->clientManipulator->getClient($connection);
|
|
||||||
$userName = $user->getUserIdentifier();
|
|
||||||
|
|
||||||
/** Save every step by user to db */
|
|
||||||
null === $event['resign']
|
null === $event['resign']
|
||||||
? $this->saveStepToDb($topic, $event)
|
? $this->saveStepToDb($gameAssoc, $event)
|
||||||
: $this->saveResignToDb($topic, $event['resign']);
|
: $this->saveResignToDb($gameAssoc, $event['resign']);
|
||||||
|
|
||||||
$topic->broadcast([
|
$playedGame = $this->getPlayedGame($gameAssoc);
|
||||||
'userTopicId' => $connection->resourceId,
|
$users = $this->getUserCollection($playedGame);
|
||||||
'channel' => $topic->getId(),
|
$count = $this->getPlayerCount($users);
|
||||||
|
$topic = 'mineseeker/channel/' . $gameAssoc;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->hub->publish(new Update(
|
||||||
|
$topic,
|
||||||
|
json_encode([
|
||||||
|
'userTopicId' => $userName,
|
||||||
|
'channel' => $topic,
|
||||||
'user' => $userName,
|
'user' => $userName,
|
||||||
'userCnt' => $topic->count(),
|
'userCnt' => $count,
|
||||||
'data' => $event
|
'data' => $event,
|
||||||
]);
|
], JSON_THROW_ON_ERROR)
|
||||||
|
));
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
throw new RuntimeException($e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getPlayedGame(string $gameAssoc): ?PlayedGame
|
||||||
* Save Resign event to database
|
|
||||||
*
|
|
||||||
* @param $topic
|
|
||||||
* @param $color
|
|
||||||
*/
|
|
||||||
private function saveResignToDb(Topic $topic, $color): void
|
|
||||||
{
|
{
|
||||||
$gameAssoc = explode('/', $topic->getId())[2];
|
return $this->entityManager
|
||||||
|
|
||||||
/** @var PlayedGame $playedGame */
|
|
||||||
$playedGame = $this->entityManager
|
|
||||||
->getRepository(PlayedGame::class)
|
->getRepository(PlayedGame::class)
|
||||||
->findOneByGameAssoc($gameAssoc);
|
->findOneByGameAssoc($gameAssoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPlayerCount(array $users): int
|
||||||
|
{
|
||||||
|
$red = '' !== $users['red'] || '' !== $users['redAnon'] ? 1 : 0;
|
||||||
|
$blue = '' !== $users['blue'] || '' !== $users['blueAnon'] ? 1 : 0;
|
||||||
|
|
||||||
|
return $red + $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveResignToDb(string $gameAssoc, string $color): void
|
||||||
|
{
|
||||||
|
$playedGame = $this->getPlayedGame($gameAssoc);
|
||||||
|
|
||||||
$playedGame->setResign($color);
|
$playedGame->setResign($color);
|
||||||
|
|
||||||
@@ -115,30 +146,17 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
|
|||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function saveStepToDb(string $gameAssoc, array $event): void
|
||||||
* Save steps and point information to database
|
|
||||||
*
|
|
||||||
* @param $topic
|
|
||||||
* @param $event
|
|
||||||
*/
|
|
||||||
private function saveStepToDb(Topic $topic, $event): void
|
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$gameAssoc = explode('/', $topic->getId())[2];
|
$playedGame = $this->getPlayedGame($gameAssoc);
|
||||||
|
|
||||||
/** @var PlayedGame $playedGame */
|
|
||||||
$playedGame = $this->entityManager
|
|
||||||
->getRepository(PlayedGame::class)
|
|
||||||
->findOneByGameAssoc($gameAssoc);
|
|
||||||
|
|
||||||
$step = new Step();
|
$step = new Step();
|
||||||
|
|
||||||
$step->setRow($event['coords'][0]);
|
$step->setRow($event['coords'][0]);
|
||||||
$step->setCol($event['coords'][1]);
|
$step->setCol($event['coords'][1]);
|
||||||
$step->setWBomb($event['bomb']);
|
$step->setWBomb($event['bomb']);
|
||||||
$step->setPlayedGame($playedGame);
|
$step->setPlayedGame($playedGame);
|
||||||
$step->setCreated(new DateTime());
|
$step->setCreated(new DateTime());
|
||||||
|
|
||||||
$this->entityManager->persist($step);
|
$this->entityManager->persist($step);
|
||||||
|
|
||||||
$playedGame->setBluePoints($event['bluePoints']);
|
$playedGame->setBluePoints($event['bluePoints']);
|
||||||
@@ -146,7 +164,6 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
|
|||||||
$playedGame->setBlueExplodedBomb($event['blueExplodedBomb'] ? true : null);
|
$playedGame->setBlueExplodedBomb($event['blueExplodedBomb'] ? true : null);
|
||||||
$playedGame->setRedExplodedBomb($event['redExplodedBomb'] ? true : null);
|
$playedGame->setRedExplodedBomb($event['redExplodedBomb'] ? true : null);
|
||||||
$playedGame->setUpdated(new DateTime());
|
$playedGame->setUpdated(new DateTime());
|
||||||
|
|
||||||
$this->entityManager->persist($playedGame);
|
$this->entityManager->persist($playedGame);
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
@@ -155,62 +172,11 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function saveUserToDb(string $gameAssoc, string $userName, ?UserInterface $user, int $count): array
|
||||||
* Control all users in a channel
|
|
||||||
*
|
|
||||||
* @param Topic $topic
|
|
||||||
* @param string $userName
|
|
||||||
* @param $user
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
private function controlUsers(Topic $topic, string $userName, TokenInterface $user): array
|
|
||||||
{
|
{
|
||||||
$gameAssoc = explode('/', $topic->getId())[2];
|
$playedGame = $this->getPlayedGame($gameAssoc);
|
||||||
|
|
||||||
/** @var PlayedGame $playedGame */
|
null !== $user
|
||||||
$playedGame = $this->entityManager
|
|
||||||
->getRepository(PlayedGame::class)
|
|
||||||
->findOneByGameAssoc($gameAssoc);
|
|
||||||
|
|
||||||
/** @var $users {array} */
|
|
||||||
$users = $this->getUserCollection($playedGame);
|
|
||||||
|
|
||||||
$red = '' !== $users['red'] || '' !== $users['redAnon'] ? 1 : 0;
|
|
||||||
$blue = '' !== $users['blue'] || '' !== $users['blueAnon'] ? 1 : 0;
|
|
||||||
$one = $topic->count() === 1;
|
|
||||||
$two = $topic->count() === 2;
|
|
||||||
|
|
||||||
/** This checks it is a reconnection */
|
|
||||||
if (($one && ($red + $blue === 0)) || ($two && ($red + $blue === 1))) {
|
|
||||||
/** @var $users {array} w/ save users to database */
|
|
||||||
$users = $this->saveUserToDb($topic, $userName, $user, $topic->count());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $users;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save user data to database
|
|
||||||
*
|
|
||||||
* @param $topic
|
|
||||||
* @param $userName
|
|
||||||
* @param $user
|
|
||||||
* @param $count
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
private function saveUserToDb(Topic $topic, string $userName, TokenInterface $user, $count)
|
|
||||||
{
|
|
||||||
$gameAssoc = explode('/', $topic->getId())[2];
|
|
||||||
|
|
||||||
/** @var PlayedGame $playedGame */
|
|
||||||
$playedGame = $this->entityManager
|
|
||||||
->getRepository(PlayedGame::class)
|
|
||||||
->findOneByGameAssoc($gameAssoc);
|
|
||||||
|
|
||||||
/** when the user is not anonym */
|
|
||||||
null !== $user->getUser()
|
|
||||||
? $this->saveRegisteredUser($userName, $count, $playedGame)
|
? $this->saveRegisteredUser($userName, $count, $playedGame)
|
||||||
: $this->saveAnonUser($userName, $count, $playedGame);
|
: $this->saveAnonUser($userName, $count, $playedGame);
|
||||||
|
|
||||||
@@ -220,23 +186,15 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
|
|||||||
return $this->getUserCollection($playedGame);
|
return $this->getUserCollection($playedGame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function saveRegisteredUser(string $userName, int $count, PlayedGame $playedGame): void
|
||||||
* Saves the registered user to the database
|
|
||||||
*
|
|
||||||
* @param string $userName
|
|
||||||
* @param int $count
|
|
||||||
* @param PlayedGame $playedGame
|
|
||||||
*/
|
|
||||||
private function saveRegisteredUser(string $userName, int $count, PlayedGame $playedGame)
|
|
||||||
{
|
{
|
||||||
/** @var User $FOSUser */
|
/** @var User $user */
|
||||||
$user = $this->entityManager
|
$user = $this->entityManager
|
||||||
->getRepository(User::class)
|
->getRepository(User::class)
|
||||||
->findOneByUsername($userName);
|
->findOneByUsername($userName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($count === 1) {
|
if ($count === 1) {
|
||||||
/** @var $random {integer} Active player: red: 0, blue: 1 */
|
|
||||||
$random = random_int(0, 1);
|
$random = random_int(0, 1);
|
||||||
!$random ? $playedGame->setRed($user) : $playedGame->setBlue($user);
|
!$random ? $playedGame->setRed($user) : $playedGame->setBlue($user);
|
||||||
} else {
|
} else {
|
||||||
@@ -249,17 +207,8 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function saveAnonUser(string $userName, int $count, PlayedGame $playedGame): void
|
||||||
* Save anonymous Gamer to database
|
|
||||||
*
|
|
||||||
* @param string $userName
|
|
||||||
* @param int $count
|
|
||||||
* @param PlayedGame $playedGame
|
|
||||||
*/
|
|
||||||
private function saveAnonUser(string $userName, int $count, PlayedGame $playedGame)
|
|
||||||
{
|
{
|
||||||
// $request = $this->requestStack->getCurrentRequest(); // TODO nem megy...
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$anon = new Gamer();
|
$anon = new Gamer();
|
||||||
$anon->setUsername($userName);
|
$anon->setUsername($userName);
|
||||||
@@ -267,7 +216,6 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
|
|||||||
$this->entityManager->persist($anon);
|
$this->entityManager->persist($anon);
|
||||||
|
|
||||||
if ($count === 1) {
|
if ($count === 1) {
|
||||||
/** @var $random {integer} Active player: red: 0, blue: 1 */
|
|
||||||
$random = random_int(0, 1);
|
$random = random_int(0, 1);
|
||||||
!$random ? $playedGame->setRedAnon($anon) : $playedGame->setBlueAnon($anon);
|
!$random ? $playedGame->setRedAnon($anon) : $playedGame->setBlueAnon($anon);
|
||||||
} else {
|
} else {
|
||||||
@@ -280,20 +228,13 @@ class TopicManager extends WebsocketManager implements TopicManagerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user collection from PlayedGame entity
|
|
||||||
*
|
|
||||||
* @param $playedGame
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
private function getUserCollection(PlayedGame $playedGame): array
|
private function getUserCollection(PlayedGame $playedGame): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'red' => null !== $playedGame->getRed() ? $playedGame->getRed()->getUsername() : '',
|
'red' => null !== $playedGame->getRed() ? $playedGame->getRed()->getUsername() : '',
|
||||||
'blue' => null !== $playedGame->getBlue() ? $playedGame->getBlue()->getUsername() : '',
|
'blue' => null !== $playedGame->getBlue() ? $playedGame->getBlue()->getUsername() : '',
|
||||||
'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '',
|
'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '',
|
||||||
'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : ''
|
'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
<?php declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2019 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Util;
|
|
||||||
|
|
||||||
use App\Interfaces\WebsocketManagerInterface;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class WebsocketManager
|
|
||||||
*
|
|
||||||
* @package App\Util
|
|
||||||
* @author Lang <https://www.splendidbear.org>
|
|
||||||
* @category Class
|
|
||||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
|
||||||
* @link www.splendidbear.org
|
|
||||||
* @since 2026. 04. 09.
|
|
||||||
*/
|
|
||||||
class WebsocketManager implements WebsocketManagerInterface
|
|
||||||
{
|
|
||||||
public function __construct(private LoggerInterface $logger) { }
|
|
||||||
|
|
||||||
public function reConnect(EntityManagerInterface $entityManager): ?EntityManagerInterface
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$connection = $entityManager->getConnection();
|
|
||||||
|
|
||||||
if (false === $connection->ping()) {
|
|
||||||
$connection->close();
|
|
||||||
$connection->connect();
|
|
||||||
}
|
|
||||||
} catch (RuntimeException $e) {
|
|
||||||
$this->logger->error($e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $entityManager;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
60
symfony.lock
60
symfony.lock
@@ -1,7 +1,4 @@
|
|||||||
{
|
{
|
||||||
"cboden/ratchet": {
|
|
||||||
"version": "v0.4.1"
|
|
||||||
},
|
|
||||||
"cocur/slugify": {
|
"cocur/slugify": {
|
||||||
"version": "v3.1"
|
"version": "v3.1"
|
||||||
},
|
},
|
||||||
@@ -74,27 +71,12 @@
|
|||||||
"egulias/email-validator": {
|
"egulias/email-validator": {
|
||||||
"version": "2.1.4"
|
"version": "2.1.4"
|
||||||
},
|
},
|
||||||
"evenement/evenement": {
|
|
||||||
"version": "v3.0.1"
|
|
||||||
},
|
|
||||||
"friendsofsymfony/user-bundle": {
|
"friendsofsymfony/user-bundle": {
|
||||||
"version": "v2.1.2"
|
"version": "v2.1.2"
|
||||||
},
|
},
|
||||||
"gos/pnctl-event-loop-emitter": {
|
"gos/pnctl-event-loop-emitter": {
|
||||||
"version": "v0.1.7"
|
"version": "v0.1.7"
|
||||||
},
|
},
|
||||||
"gos/pubsub-router-bundle": {
|
|
||||||
"version": "v0.3.3"
|
|
||||||
},
|
|
||||||
"gos/web-socket-bundle": {
|
|
||||||
"version": "v1.8.12"
|
|
||||||
},
|
|
||||||
"gos/websocket-client": {
|
|
||||||
"version": "v0.1.2"
|
|
||||||
},
|
|
||||||
"guzzlehttp/psr7": {
|
|
||||||
"version": "1.4.2"
|
|
||||||
},
|
|
||||||
"jdorn/sql-formatter": {
|
"jdorn/sql-formatter": {
|
||||||
"version": "v1.2.17"
|
"version": "v1.2.17"
|
||||||
},
|
},
|
||||||
@@ -119,42 +101,15 @@
|
|||||||
"psr/container": {
|
"psr/container": {
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"psr/http-message": {
|
|
||||||
"version": "1.0.1"
|
|
||||||
},
|
|
||||||
"psr/log": {
|
"psr/log": {
|
||||||
"version": "1.0.2"
|
"version": "1.0.2"
|
||||||
},
|
},
|
||||||
"psr/simple-cache": {
|
"psr/simple-cache": {
|
||||||
"version": "1.0.1"
|
"version": "1.0.1"
|
||||||
},
|
},
|
||||||
"ralouphie/getallheaders": {
|
|
||||||
"version": "3.0.3"
|
|
||||||
},
|
|
||||||
"ratchet/rfc6455": {
|
|
||||||
"version": "0.2.4"
|
|
||||||
},
|
|
||||||
"react/cache": {
|
|
||||||
"version": "v0.4.2"
|
|
||||||
},
|
|
||||||
"react/dns": {
|
|
||||||
"version": "v0.4.13"
|
|
||||||
},
|
|
||||||
"react/event-loop": {
|
|
||||||
"version": "v0.5.2"
|
|
||||||
},
|
|
||||||
"react/promise": {
|
|
||||||
"version": "v2.5.1"
|
|
||||||
},
|
|
||||||
"react/promise-timer": {
|
"react/promise-timer": {
|
||||||
"version": "v1.3.0"
|
"version": "v1.3.0"
|
||||||
},
|
},
|
||||||
"react/socket": {
|
|
||||||
"version": "v0.8.11"
|
|
||||||
},
|
|
||||||
"react/stream": {
|
|
||||||
"version": "v0.7.7"
|
|
||||||
},
|
|
||||||
"roave/security-advisories": {
|
"roave/security-advisories": {
|
||||||
"version": "dev-master"
|
"version": "dev-master"
|
||||||
},
|
},
|
||||||
@@ -275,6 +230,18 @@
|
|||||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"symfony/mercure-bundle": {
|
||||||
|
"version": "0.4",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "0.4",
|
||||||
|
"ref": "b141b8c8f13bc8c31d718a5488039b712c0d3592"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/mercure.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/mime": {
|
"symfony/mime": {
|
||||||
"version": "v4.3.5"
|
"version": "v4.3.5"
|
||||||
},
|
},
|
||||||
@@ -295,9 +262,6 @@
|
|||||||
"config/packages/test/monolog.yaml"
|
"config/packages/test/monolog.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"symfony/options-resolver": {
|
|
||||||
"version": "v4.0.9"
|
|
||||||
},
|
|
||||||
"symfony/orm-pack": {
|
"symfony/orm-pack": {
|
||||||
"version": "v1.0.5"
|
"version": "v1.0.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
<div class="mine-container">
|
<div class="mine-container">
|
||||||
<div id="mine-wrapper"
|
<div id="mine-wrapper"
|
||||||
data-env="{{ env }}"
|
data-env="{{ env }}"
|
||||||
data-ssl="{{ ssl }}"
|
data-game-id="{{ app.request.get('gameAssoc') }}"
|
||||||
data-game-id="{{ app.request.get('gameAssoc') }}">
|
data-mercure-hub-url="{{ mercure_hub_url }}"
|
||||||
|
data-mercure-subscriber-jwt="{{ mercure_subscriber_jwt }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{{ encore_entry_link_tags('mineseekerStyle') }}
|
{{ encore_entry_link_tags('mineseekerStyle') }}
|
||||||
|
|
||||||
<style type="text/css">
|
<style>
|
||||||
.mine-container {
|
.mine-container {
|
||||||
background: url('/images/bg-mineseeker-{{ random(1) }}-outbg.jpg') no-repeat;
|
background: url('/images/bg-mineseeker-{{ random(1) }}-outbg.jpg') no-repeat;
|
||||||
}
|
}
|
||||||
@@ -36,9 +37,5 @@
|
|||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ asset('bundles/goswebsocket/js/vendor/autobahn.min.js') }}"></script>
|
|
||||||
<script type="text/javascript" src="{{ asset('bundles/goswebsocket/js/websocket.js') }}"></script>
|
|
||||||
|
|
||||||
{{ encore_entry_script_tags('mineseeker') }}
|
{{ encore_entry_script_tags('mineseeker') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<script type="text/javascript">
|
|
||||||
window.fbAsyncInit = function () {
|
|
||||||
FB.init({
|
|
||||||
appId: '{{ facebook_api }}',
|
|
||||||
xfbml: true,
|
|
||||||
cookie: true,
|
|
||||||
status: true,
|
|
||||||
oauth: true,
|
|
||||||
version: '{{ facebook_api_version }}'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
(function (d, s, id) {
|
|
||||||
var js, fjs = d.getElementsByTagName(s)[0];
|
|
||||||
if (d.getElementById(id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
js = d.createElement(s);
|
|
||||||
js.id = id;
|
|
||||||
js.src = "//connect.facebook.net/en_US/sdk.js";
|
|
||||||
fjs.parentNode.insertBefore(js, fjs);
|
|
||||||
}(document, 'script', 'facebook-jssdk'));
|
|
||||||
</script>
|
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
<meta property="fb:app_id" content="{{ facebook_api }}">
|
<meta property="fb:app_id" content="{{ facebook_api }}">
|
||||||
{% block metas %}{% endblock %}
|
{% block metas %}{% endblock %}
|
||||||
<title>MineSeeker{% block title %}{% endblock %}</title>
|
<title>MineSeeker{% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
{% block stylesheets %}{% endblock %}
|
{% block stylesheets %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user