fix: websocket
This commit is contained in:
parent
2e6e339d6d
commit
626df5b912
|
@ -63,6 +63,7 @@ module.exports = {
|
|||
files: [
|
||||
'.eslintrc.cjs',
|
||||
'server.js',
|
||||
'app/websocket/**',
|
||||
'**/.server/**',
|
||||
'*.server.{js,jsx}',
|
||||
'**/{build,node}.js',
|
||||
|
|
|
@ -233,20 +233,6 @@ function Ui({disconnected}) {
|
|||
mainEntityRef,
|
||||
]);
|
||||
usePacket('EcsChange', onEcsChangePacket);
|
||||
const onTickPacket = useCallback((payload, client) => {
|
||||
if (!ecsRef.current || 0 === Object.keys(payload.ecs).length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ecsRef.current.apply(payload.ecs);
|
||||
client.emitter.invoke(':Ecs', payload.ecs);
|
||||
}
|
||||
catch (error) {
|
||||
ecsRef.current = undefined;
|
||||
console.error('tick crash', error);
|
||||
}
|
||||
}, [ecsRef]);
|
||||
usePacket('Tick', onTickPacket);
|
||||
const onEcsTick = useCallback((payload, ecs) => {
|
||||
for (const id in payload) {
|
||||
const entity = ecs.get(id);
|
||||
|
|
|
@ -18,12 +18,13 @@ export default function PlaySpecific() {
|
|||
const debugTuple = useState(false);
|
||||
const [Components, setComponents] = useState();
|
||||
const [Systems, setSystems] = useState();
|
||||
const reconnectionBackoff = useRef(0);
|
||||
const ecsRef = useRef();
|
||||
const [disconnected, setDisconnected] = useState(false);
|
||||
const params = useParams();
|
||||
const [type, url] = params['*'].split('/');
|
||||
useEffect(() => {
|
||||
if (!Client) {
|
||||
if (!Client || !Components || !Systems) {
|
||||
return;
|
||||
}
|
||||
const client = new Client();
|
||||
|
@ -35,7 +36,7 @@ export default function PlaySpecific() {
|
|||
return () => {
|
||||
client.disconnect();
|
||||
};
|
||||
}, [Client, url]);
|
||||
}, [Client, Components, Systems, url]);
|
||||
// Sneakily use beforeunload to snag some time to save.
|
||||
useEffect(() => {
|
||||
if ('local' !== type) {
|
||||
|
@ -84,19 +85,52 @@ export default function PlaySpecific() {
|
|||
client.removePacketListener('EcsChange', onEcsChangePacket);
|
||||
};
|
||||
}, [client, refreshEcs]);
|
||||
const onTickPacket = useCallback((payload) => {
|
||||
if (!ecsRef.current || 0 === Object.keys(payload.ecs).length) {
|
||||
return;
|
||||
}
|
||||
reconnectionBackoff.current = 0;
|
||||
setDisconnected(false);
|
||||
try {
|
||||
ecsRef.current.apply(payload.ecs);
|
||||
client.emitter.invoke(':Ecs', payload.ecs);
|
||||
}
|
||||
catch (error) {
|
||||
ecsRef.current = undefined;
|
||||
console.error('tick crash', error);
|
||||
}
|
||||
}, [client, ecsRef]);
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
client.addPacketListener('Tick', onTickPacket);
|
||||
return () => {
|
||||
client.removePacketListener('Tick', onTickPacket);
|
||||
};
|
||||
}, [client, onTickPacket]);
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
let handle;
|
||||
function onConnectionStatus(status) {
|
||||
switch (status) {
|
||||
case 'aborted': {
|
||||
if (!handle) {
|
||||
client.disconnect();
|
||||
setDisconnected(true);
|
||||
break;
|
||||
reconnectionBackoff.current = (
|
||||
Math.max(100, Math.min(1000, reconnectionBackoff.current * 2))
|
||||
);
|
||||
handle = setTimeout(
|
||||
() => {
|
||||
client.connect(url);
|
||||
handle = null;
|
||||
},
|
||||
reconnectionBackoff.current,
|
||||
);
|
||||
}
|
||||
case 'connected': {
|
||||
setDisconnected(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -104,8 +138,9 @@ export default function PlaySpecific() {
|
|||
client.addPacketListener('ConnectionStatus', onConnectionStatus);
|
||||
return () => {
|
||||
client.removePacketListener('ConnectionStatus', onConnectionStatus);
|
||||
clearTimeout(handle);
|
||||
};
|
||||
}, [client]);
|
||||
}, [client, url]);
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
return;
|
||||
|
@ -125,15 +160,6 @@ export default function PlaySpecific() {
|
|||
client.removePacketListener('Download', onDownload);
|
||||
};
|
||||
}, [client]);
|
||||
useEffect(() => {
|
||||
if (!client || !disconnected) {
|
||||
return;
|
||||
}
|
||||
async function reconnect() {
|
||||
await client.connect(url);
|
||||
}
|
||||
reconnect();
|
||||
}, [client, disconnected, mainEntityRef, url]);
|
||||
// useEffect(() => {
|
||||
// let source = true;
|
||||
// async function play() {
|
||||
|
|
|
@ -31,6 +31,10 @@ export default class LocalClient extends Client {
|
|||
new URL('./predictor.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
// loaded
|
||||
await new Promise((resolve) => {
|
||||
this.predictor.addEventListener('message', resolve, {once: true});
|
||||
});
|
||||
this.predictor.addEventListener('message', (event) => {
|
||||
const [flow, packet] = event.data;
|
||||
switch (flow) {
|
||||
|
@ -71,6 +75,9 @@ export default class LocalClient extends Client {
|
|||
if (CLIENT_INTERPOLATION) {
|
||||
this.interpolator.terminate();
|
||||
}
|
||||
if (CLIENT_PREDICTION) {
|
||||
this.predictor.terminate();
|
||||
}
|
||||
}
|
||||
transmit(packet) {
|
||||
if (CLIENT_PREDICTION) {
|
||||
|
|
|
@ -161,3 +161,6 @@ onmessage = (event) => {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
// sync with parent
|
||||
postMessage(null);
|
||||
|
|
|
@ -7,17 +7,19 @@ export default class RemoteClient extends Client {
|
|||
interpolator = null;
|
||||
predictor = null;
|
||||
async connect(host) {
|
||||
this.interpolator = new Worker(
|
||||
new URL('./interpolator.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
if (CLIENT_INTERPOLATION) {
|
||||
this.interpolator = new Worker(
|
||||
new URL('./interpolator.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
this.interpolator.addEventListener('message', (event) => {
|
||||
this.accept(event.data);
|
||||
const packet = event.data;
|
||||
if (CLIENT_PREDICTION) {
|
||||
this.predictor.postMessage([1, packet]);
|
||||
}
|
||||
else {
|
||||
this.accept(packet);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (CLIENT_PREDICTION) {
|
||||
|
@ -25,6 +27,10 @@ export default class RemoteClient extends Client {
|
|||
new URL('./predictor.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
// loaded
|
||||
await new Promise((resolve) => {
|
||||
this.predictor.addEventListener('message', resolve, {once: true});
|
||||
});
|
||||
this.predictor.addEventListener('message', (event) => {
|
||||
const [flow, packet] = event.data;
|
||||
switch (flow) {
|
||||
|
@ -35,29 +41,23 @@ export default class RemoteClient extends Client {
|
|||
break;
|
||||
}
|
||||
case 1: {
|
||||
if (CLIENT_INTERPOLATION) {
|
||||
this.interpolator.postMessage(packet);
|
||||
}
|
||||
else {
|
||||
this.accept(packet);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const url = new URL(`wss://${host}/ws`)
|
||||
this.socket = new WebSocket(url.href);
|
||||
this.socket = new WebSocket(`//${host}/silphius`);
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
this.socket.addEventListener('message', (event) => {
|
||||
this.throughput.$$down += event.data.byteLength;
|
||||
const packet = decode(event.data);
|
||||
if (CLIENT_PREDICTION) {
|
||||
this.predictor.postMessage([1, packet]);
|
||||
}
|
||||
else if (CLIENT_INTERPOLATION) {
|
||||
if (CLIENT_INTERPOLATION) {
|
||||
this.interpolator.postMessage(packet);
|
||||
}
|
||||
else if (CLIENT_PREDICTION) {
|
||||
this.predictor.postMessage([1, packet]);
|
||||
}
|
||||
else {
|
||||
this.accept(packet);
|
||||
}
|
||||
|
@ -68,12 +68,17 @@ export default class RemoteClient extends Client {
|
|||
this.socket.addEventListener('error', () => {
|
||||
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
|
||||
});
|
||||
this.socket.addEventListener('open', () => {
|
||||
this.accept({type: 'ConnectionStatus', payload: 'connected'});
|
||||
});
|
||||
}
|
||||
disconnect() {
|
||||
if (CLIENT_INTERPOLATION) {
|
||||
this.interpolator.terminate();
|
||||
}
|
||||
if (CLIENT_PREDICTION) {
|
||||
this.predictor.terminate();
|
||||
}
|
||||
}
|
||||
transmit(packet) {
|
||||
if (CLIENT_PREDICTION) {
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import {mkdir, readFile, unlink, writeFile} from 'node:fs/promises';
|
||||
import {dirname, join} from 'node:path';
|
||||
|
||||
import {WebSocketServer} from 'ws';
|
||||
|
||||
import Server from '@/silphius/net/server.js';
|
||||
import {getSession} from '@/silphius/server/session.server.js';
|
||||
import {loadResources, readAsset} from '@/lib/resources.js';
|
||||
import {loadResources as loadServerResources} from '@/lib/resources.server.js';
|
||||
|
||||
import Engine from './engine.js';
|
||||
|
||||
global.__silphiusWebsocket = null;
|
||||
|
||||
class SocketServer extends Server {
|
||||
async ensurePath(path) {
|
||||
await mkdir(path, {recursive: true});
|
||||
}
|
||||
async load() {
|
||||
await loadResources(await loadServerResources());
|
||||
}
|
||||
static qualify(path) {
|
||||
return join(process.cwd(), 'data', 'remote', 'UNIVERSE', path);
|
||||
}
|
||||
readAsset(path) {
|
||||
return readAsset(path) ?? new ArrayBuffer(0);
|
||||
}
|
||||
async readData(path) {
|
||||
const qualified = this.constructor.qualify(path);
|
||||
await this.ensurePath(dirname(qualified));
|
||||
return readFile(qualified);
|
||||
}
|
||||
async removeData(path) {
|
||||
await unlink(path);
|
||||
}
|
||||
async writeData(path, view) {
|
||||
const qualified = this.constructor.qualify(path);
|
||||
await this.ensurePath(dirname(qualified));
|
||||
await writeFile(qualified, view);
|
||||
}
|
||||
transmit(ws, packed) { ws.send(packed); }
|
||||
}
|
||||
|
||||
export async function handleUpgrade(request, socket, head) {
|
||||
if (!global.__silphiusWebsocket) {
|
||||
const engine = new Engine(SocketServer);
|
||||
await engine.load();
|
||||
engine.start();
|
||||
const handleConnection = async (ws, request) => {
|
||||
ws.on('close', async () => {
|
||||
await engine.disconnectPlayer(ws);
|
||||
})
|
||||
ws.on('message', (packed) => {
|
||||
engine.server.accept(ws, new DataView(packed.buffer, packed.byteOffset, packed.length));
|
||||
});
|
||||
const session = await getSession(request.headers['cookie']);
|
||||
await engine.connectPlayer(ws, session.get('id'));
|
||||
};
|
||||
const wss = new WebSocketServer({
|
||||
noServer: true,
|
||||
});
|
||||
wss.on('connection', handleConnection);
|
||||
global.__silphiusWebsocket = {engine, handleConnection, wss};
|
||||
}
|
||||
const {pathname} = new URL(request.url, 'wss://base.url');
|
||||
if (pathname === '/ws') {
|
||||
const {wss} = global.__silphiusWebsocket;
|
||||
wss.handleUpgrade(request, socket, head, function done(ws) {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
}
|
||||
else {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeUpdate', async () => {
|
||||
if (global.__silphiusWebsocket) {
|
||||
const {engine, handleConnection, wss} = global.__silphiusWebsocket;
|
||||
wss.off('connection', handleConnection);
|
||||
const connections = [];
|
||||
for (const [connection] of engine.connectedPlayers) {
|
||||
engine.server.send(connection, {type: 'EcsChange'});
|
||||
connections.push(connection);
|
||||
}
|
||||
await engine.stop();
|
||||
for (const connection of connections) {
|
||||
connection.close();
|
||||
}
|
||||
global.__silphiusWebsocket = null;
|
||||
}
|
||||
});
|
||||
import.meta.hot.accept();
|
||||
}
|
54
app/websocket/silphius.js
Normal file
54
app/websocket/silphius.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import {mkdir, readFile, unlink, writeFile} from 'node:fs/promises';
|
||||
import {dirname, join} from 'node:path';
|
||||
|
||||
import Server from '@/silphius/net/server.js';
|
||||
import Engine from '@/silphius/server/engine.js';
|
||||
import {getSession} from '@/silphius/server/session.server.js';
|
||||
import {loadResources, readAsset} from '@/lib/resources.js';
|
||||
import {loadResources as loadServerResources} from '@/lib/resources.server.js';
|
||||
|
||||
class SocketServer extends Server {
|
||||
async ensurePath(path) {
|
||||
await mkdir(path, {recursive: true});
|
||||
}
|
||||
async load() {
|
||||
await loadResources(await loadServerResources());
|
||||
}
|
||||
static qualify(path) {
|
||||
return join(process.cwd(), 'data', 'remote', 'UNIVERSE', path);
|
||||
}
|
||||
readAsset(path) {
|
||||
return readAsset(path) ?? new ArrayBuffer(0);
|
||||
}
|
||||
async readData(path) {
|
||||
const qualified = this.constructor.qualify(path);
|
||||
await this.ensurePath(dirname(qualified));
|
||||
return readFile(qualified);
|
||||
}
|
||||
async removeData(path) {
|
||||
await unlink(path);
|
||||
}
|
||||
async writeData(path, view) {
|
||||
const qualified = this.constructor.qualify(path);
|
||||
await this.ensurePath(dirname(qualified));
|
||||
await writeFile(qualified, view);
|
||||
}
|
||||
transmit(ws, packed) {
|
||||
ws.send(packed);
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Engine(SocketServer);
|
||||
await engine.load();
|
||||
engine.start();
|
||||
|
||||
export async function handleConnection(websocket, request) {
|
||||
websocket.on('close', async () => {
|
||||
await engine.disconnectPlayer(websocket);
|
||||
})
|
||||
websocket.on('message', (packed) => {
|
||||
engine.server.accept(websocket, new DataView(packed.buffer, packed.byteOffset, packed.length));
|
||||
});
|
||||
const session = await getSession(request.headers['cookie']);
|
||||
await engine.connectPlayer(websocket, session.get('id'));
|
||||
}
|
Loading…
Reference in New Issue
Block a user