fix: connection semantics, HMR

This commit is contained in:
cha0s 2024-06-13 12:24:32 -05:00
parent eb599c60c0
commit dbe25f86e5
11 changed files with 131 additions and 53 deletions

View File

@ -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));

View File

@ -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]);
}

View File

@ -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;
}

View File

@ -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() {

View File

@ -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();
});

View File

@ -1,3 +0,0 @@
import Packet from '@/net/packet.js';
export default class ConnectionAborted extends Packet {}

View File

@ -0,0 +1,3 @@
import Packet from '@/net/packet.js';
export default class ConnectionStatus extends Packet {}

View File

@ -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));
}
}
}

View File

@ -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}) {
<Pixi />
<Dom>
<HotBar active={0} slots={Array(10).fill(0).map(() => {})} />
{disconnected && (
{showDisconnected && (
<Disconnected />
)}
</Dom>

View File

@ -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 (
<div className={styles.play}>
{client && (
<ClientContext.Provider value={client}>
<Ui disconnected={disconnected} />
</ClientContext.Provider>
)}
<ClientContext.Provider value={client}>
<Ui disconnected={disconnected} />
</ClientContext.Provider>
</div>
);
}

View File

@ -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));
});
}
}