From dbe25f86e540137a2ea373275044432e1738047f Mon Sep 17 00:00:00 2001 From: cha0s Date: Thu, 13 Jun 2024 12:24:32 -0500 Subject: [PATCH] fix: connection semantics, HMR --- app/engine/engine.js | 16 ++++++--- app/hooks/use-packet.js | 5 ++- app/net/client/prediction.js | 15 ++++---- app/net/client/remote.js | 18 +++++++--- app/net/server/worker.js | 9 ++++- app/packets/connection-aborted.js | 3 -- app/packets/connection-status.js | 3 ++ app/react-components/ecs.jsx | 6 ++-- app/react-components/ui.jsx | 19 ++++++++-- app/routes/_main-menu.play.$/route.jsx | 40 +++++++++++++++------ app/websocket.js | 50 +++++++++++++++++--------- 11 files changed, 131 insertions(+), 53 deletions(-) delete mode 100644 app/packets/connection-aborted.js create mode 100644 app/packets/connection-status.js diff --git a/app/engine/engine.js b/app/engine/engine.js index a0bea16..9da1c8b 100644 --- a/app/engine/engine.js +++ b/app/engine/engine.js @@ -39,6 +39,18 @@ export default class Engine { incomingActions = []; + connections = []; + + connectedPlayers = new Map(); + + ecses = {}; + + frame = 0; + + last = Date.now(); + + server; + constructor(Server) { const ecs = new this.constructor.Ecs(); const layerSize = {x: Math.ceil(RESOLUTION.x / 4), y: Math.ceil(RESOLUTION.y / 4)}; @@ -68,10 +80,6 @@ export default class Engine { this.ecses = { 1: ecs, }; - this.connections = []; - this.connectedPlayers = new Map(); - this.frame = 0; - this.last = Date.now(); class SilphiusServer extends Server { accept(connection, packed) { super.accept(connection, decode(packed)); diff --git a/app/hooks/use-packet.js b/app/hooks/use-packet.js index e717fb4..a5ebecc 100644 --- a/app/hooks/use-packet.js +++ b/app/hooks/use-packet.js @@ -5,9 +5,12 @@ import ClientContext from '@/context/client.js'; export default function usePacket(type, fn, dependencies) { const client = useContext(ClientContext); useEffect(() => { + if (!client) { + return; + } client.addPacketListener(type, fn); return () => { client.removePacketListener(type, fn); }; - }, dependencies); + }, [client, ...dependencies]); } \ No newline at end of file diff --git a/app/net/client/prediction.js b/app/net/client/prediction.js index 0403e05..1621b98 100644 --- a/app/net/client/prediction.js +++ b/app/net/client/prediction.js @@ -15,21 +15,20 @@ onmessage = async (event) => { } socket = new WebSocket(url.href); socket.binaryType = 'arraybuffer'; - const {promise, resolve, reject} = Promise.withResolvers(); + const {promise, resolve} = Promise.withResolvers(); socket.addEventListener('open', resolve); - socket.addEventListener('error', reject); + socket.addEventListener('error', () => { + postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'})); + close(); + }); await promise; socket.removeEventListener('open', resolve); - socket.removeEventListener('error', reject); socket.addEventListener('message', onMessage); socket.addEventListener('close', () => { - postMessage(encode({type: 'ConnectionAborted'})); - close(); - }); - socket.addEventListener('error', () => { - postMessage(encode({type: 'ConnectionAborted'})); + postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'})); close(); }); + postMessage(encode({type: 'ConnectionStatus', payload: 'connected'})); connected = true; return; } diff --git a/app/net/client/remote.js b/app/net/client/remote.js index 662acc4..2010988 100644 --- a/app/net/client/remote.js +++ b/app/net/client/remote.js @@ -1,4 +1,5 @@ import {CLIENT_PREDICTION} from '@/constants.js'; +import {encode} from '@/packets/index.js'; import Client from './client.js'; @@ -30,12 +31,21 @@ export default class RemoteClient extends Client { } this.socket = new WebSocket(url.href); this.socket.binaryType = 'arraybuffer'; - this.socket.onmessage = (event) => { + const onMessage = (event) => { this.accept(event.data); - }; - await new Promise((resolve) => { - this.socket.onopen = resolve; + } + const {promise, resolve} = Promise.withResolvers(); + this.socket.addEventListener('open', resolve); + this.socket.addEventListener('error', () => { + this.accept(encode({type: 'ConnectionStatus', payload: 'aborted'})); }); + await promise; + this.socket.removeEventListener('open', resolve); + this.socket.addEventListener('message', onMessage); + this.socket.addEventListener('close', () => { + this.accept(encode({type: 'ConnectionStatus', payload: 'aborted'})); + }); + this.accept(encode({type: 'ConnectionStatus', payload: 'connected'})); } } disconnect() { diff --git a/app/net/server/worker.js b/app/net/server/worker.js index dbf0757..d953825 100644 --- a/app/net/server/worker.js +++ b/app/net/server/worker.js @@ -1,4 +1,5 @@ -import Engine from '@/engine/engine.js'; +import Engine from '../../engine/engine.js'; +import {encode} from '@/packets/index.js'; import Server from './server.js'; @@ -16,4 +17,10 @@ onmessage = (event) => { await engine.load(); engine.start(); await engine.connectPlayer(undefined); + postMessage(encode({type: 'ConnectionStatus', payload: 'connected'})); })(); + +import.meta.hot.accept('../../engine/engine.js', () => { + postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'})); + close(); +}); diff --git a/app/packets/connection-aborted.js b/app/packets/connection-aborted.js deleted file mode 100644 index d92794a..0000000 --- a/app/packets/connection-aborted.js +++ /dev/null @@ -1,3 +0,0 @@ -import Packet from '@/net/packet.js'; - -export default class ConnectionAborted extends Packet {} diff --git a/app/packets/connection-status.js b/app/packets/connection-status.js new file mode 100644 index 0000000..2d4596e --- /dev/null +++ b/app/packets/connection-status.js @@ -0,0 +1,3 @@ +import Packet from '@/net/packet.js'; + +export default class ConnectionStatus extends Packet {} diff --git a/app/react-components/ecs.jsx b/app/react-components/ecs.jsx index 764c69d..712bb5d 100644 --- a/app/react-components/ecs.jsx +++ b/app/react-components/ecs.jsx @@ -1,7 +1,7 @@ import {Container} from '@pixi/react'; import {useState} from 'react'; -import {RESOLUTION} from '@/constants.js' +import {RESOLUTION} from '@/constants.js'; import Ecs from '@/engine/ecs.js'; import usePacket from '@/hooks/use-packet.js'; @@ -25,9 +25,7 @@ export default function EcsComponent() { else { updatedEntities[id] = ecs.get(id); if (updatedEntities[id].MainEntity) { - if (!mainEntity) { - setMainEntity(ecs.get(id)); - } + setMainEntity(ecs.get(id)); } } } diff --git a/app/react-components/ui.jsx b/app/react-components/ui.jsx index d781fac..fa4083a 100644 --- a/app/react-components/ui.jsx +++ b/app/react-components/ui.jsx @@ -1,4 +1,4 @@ -import {useContext, useEffect} from 'react'; +import {useContext, useEffect, useState} from 'react'; import addKeyListener from '@/add-key-listener.js'; import {ACTION_MAP, RESOLUTION} from '@/constants.js'; @@ -20,6 +20,21 @@ const KEY_MAP = { export default function Ui({disconnected}) { // Key input. const client = useContext(ClientContext); + const [showDisconnected, setShowDisconnected] = useState(false); + useEffect(() => { + let handle; + if (disconnected) { + handle = setTimeout(() => { + setShowDisconnected(true); + }, 200); + } + else { + setShowDisconnected(false) + } + return () => { + clearTimeout(handle); + }; + }, [disconnected]); useEffect(() => { return addKeyListener(document.body, ({type, payload}) => { if (type in KEY_MAP && payload in ACTION_MAP) { @@ -44,7 +59,7 @@ export default function Ui({disconnected}) { {})} /> - {disconnected && ( + {showDisconnected && ( )} diff --git a/app/routes/_main-menu.play.$/route.jsx b/app/routes/_main-menu.play.$/route.jsx index f3e5953..5dacb4d 100644 --- a/app/routes/_main-menu.play.$/route.jsx +++ b/app/routes/_main-menu.play.$/route.jsx @@ -11,7 +11,7 @@ import styles from './play.module.css'; export default function Index() { const [client, setClient] = useState(); - const [disconnected, setDisconnected] = useState(); + const [disconnected, setDisconnected] = useState(false); const params = useParams(); const [type, url] = params['*'].split('/'); useEffect(() => { @@ -46,21 +46,41 @@ export default function Index() { if (!client) { return; } - function onConnectionAborted() { - setDisconnected(true); + function onConnectionStatus(status) { + switch (status) { + case 'aborted': { + setDisconnected(true); + break; + } + case 'connected': { + setDisconnected(false); + break; + } + } } - client.addPacketListener('ConnectionAborted', onConnectionAborted); + client.addPacketListener('ConnectionStatus', onConnectionStatus); return () => { - client.removePacketListener('ConnectionAborted', onConnectionAborted); + client.removePacketListener('ConnectionStatus', onConnectionStatus); }; }, [client]); + useEffect(() => { + if (!disconnected) { + return; + } + async function reconnect() { + await client.connect(url); + } + reconnect(); + const handle = setInterval(reconnect, 1000); + return () => { + clearInterval(handle); + }; + }, [client, disconnected, url]); return (
- {client && ( - - - - )} + + +
); } diff --git a/app/websocket.js b/app/websocket.js index e69b412..60a4ca0 100644 --- a/app/websocket.js +++ b/app/websocket.js @@ -1,7 +1,7 @@ import {WebSocketServer} from 'ws'; -import Engine from '@/engine/engine.js'; -import Server from '@/net/server/server.js'; +import Engine from './engine/engine.js'; +import Server from './net/server/server.js'; const wss = new WebSocketServer({ noServer: true, @@ -26,20 +26,38 @@ export default async function listen(server) { transmit(ws, packed) { ws.send(packed); } } - const engine = new Engine(SocketServer); - - await engine.load(); - engine.start(); - - async function onConnect(ws) { - ws.on('close', () => { - engine.disconnectPlayer(ws); - }) - ws.on('message', (packed) => { - engine.server.accept(ws, new DataView(packed.buffer, packed.byteOffset, packed.length)); - }); - await engine.connectPlayer(ws); + let onConnect; + function makeOnConnect(engine) { + return async (ws) => { + ws.on('close', () => { + engine.disconnectPlayer(ws); + }) + ws.on('message', (packed) => { + engine.server.accept(ws, new DataView(packed.buffer, packed.byteOffset, packed.length)); + }); + await engine.connectPlayer(ws); + }; } - wss.on('connection', onConnect); + let engine; + async function makeEngine(Engine) { + const engine = new Engine(SocketServer); + await engine.load(); + engine.start(); + return engine; + } + + engine = await makeEngine(Engine); + wss.on('connection', onConnect = makeOnConnect(engine)); + + if (import.meta.hot) { + import.meta.hot.accept('./engine/engine.js', async ({default: Engine}) => { + wss.off('connection', onConnect); + for (const [connection] of engine.connectedPlayers) { + connection.close(); + } + engine = await makeEngine(Engine); + wss.on('connection', onConnect = makeOnConnect(engine)); + }); + } }