refactor websocket packets

This commit is contained in:
HF 2022-10-02 22:46:12 +02:00
parent 693a403460
commit 7a2053fda3
29 changed files with 601 additions and 523 deletions

View File

@ -114,7 +114,7 @@ persistStore(store, {}, () => {
store.dispatch(initTimer());
store.dispatch(fetchMe());
SocketClient.connect();
SocketClient.initialize(store);
});
(function load() {

View File

@ -23,7 +23,7 @@ const GlobalCaptcha = ({ close }) => {
<form
onSubmit={async (e) => {
e.preventDefault();
const text = e.target.captcha.value;
const text = e.target.captcha.value.slice(0, 6);
if (!text || text.length < 4) {
return;
}

View File

@ -6,7 +6,9 @@
*
*/
import socketEvents from '../socket/socketEvents';
import PixelUpdate from '../socket/packets/PixelUpdateServer';
import {
hydratePixelUpdate,
} from '../socket/packets/server';
import { setPixelByOffset } from './setPixel';
import { TILE_SIZE } from './constants';
import { CANVAS_ID } from '../data/redis/Event';
@ -177,7 +179,7 @@ class Void {
i: pi,
j: pj,
pixels,
} = PixelUpdate.hydrate(buffer);
} = hydratePixelUpdate(buffer);
const { i, j } = this;
// 3x3 chunk area (this is hardcoded on multiple places)
if (pi >= i - 1 && pi <= i + 1 && pj >= j - 1 && pj <= j + 1) {

View File

@ -69,7 +69,7 @@ persistStore(store, {}, () => {
if (!parentExists()) {
store.dispatch(fetchMe());
SocketClient.connect();
SocketClient.initialize(store);
}
});

View File

@ -8,10 +8,20 @@
import { SHARD_NAME } from '../core/config';
import SocketEvents from './SockEvents';
import OnlineCounter from './packets/OnlineCounter';
import PixelUpdate from './packets/PixelUpdateServer';
import PixelUpdateMB from './packets/PixelUpdateMB';
import ChunkUpdate from './packets/ChunkUpdate';
import {
ONLINE_COUNTER_OP,
PIXEL_UPDATE_MB_OP,
CHUNK_UPDATE_MB_OP,
} from './packets/op';
import {
hydrateOnlineCounter,
hydratePixelUpdateMB,
hydrateChunkUpdateMB,
dehydratePixelUpdate,
dehydrateOnlineCounter,
dehydratePixelUpdateMB,
dehydrateChunkUpdateMB,
} from './packets/server';
import { pubsub } from '../data/redis/client';
import { combineObjects } from '../core/utils';
@ -253,25 +263,25 @@ class MessageBroker extends SocketEvents {
try {
const opcode = buffer[0];
switch (opcode) {
case PixelUpdateMB.OP_CODE: {
const puData = PixelUpdateMB.hydrate(buffer);
case PIXEL_UPDATE_MB_OP: {
const puData = hydratePixelUpdateMB(buffer);
super.emit('pixelUpdate', ...puData);
const chunkId = puData[1];
const chunk = [chunkId >> 8, chunkId & 0xFF];
super.emit('chunkUpdate', puData[0], chunk);
break;
}
case ChunkUpdate.OP_CODE: {
super.emit('chunkUpdate', ...ChunkUpdate.hydrate(buffer));
case CHUNK_UPDATE_MB_OP: {
super.emit('chunkUpdate', ...hydrateChunkUpdateMB(buffer));
break;
}
case OnlineCounter.OP_CODE: {
case ONLINE_COUNTER_OP: {
const data = new DataView(
buffer.buffer,
buffer.byteOffset,
buffer.length,
);
const cnt = OnlineCounter.hydrate(data);
const cnt = hydrateOnlineCounter(data);
this.updateShardOnlineCounter(shard, cnt);
break;
}
@ -324,9 +334,9 @@ class MessageBroker extends SocketEvents {
const j = chunkId & 0xFF;
this.publisher.publish(
this.thisShard,
PixelUpdateMB.dehydrate(canvasId, i, j, pixels),
dehydratePixelUpdateMB(canvasId, i, j, pixels),
);
const buffer = PixelUpdate.dehydrate(i, j, pixels);
const buffer = dehydratePixelUpdate(i, j, pixels);
super.emit('pixelUpdate', canvasId, chunkId, buffer);
super.emit('chunkUpdate', canvasId, [i, j]);
}
@ -353,18 +363,18 @@ class MessageBroker extends SocketEvents {
) {
this.publisher.publish(
this.thisShard,
ChunkUpdate.dehydrate(canvasId, chunk),
dehydrateChunkUpdateMB(canvasId, chunk),
);
super.emit('chunkUpdate', canvasId, chunk);
}
broadcastOnlineCounter(online) {
this.updateShardOnlineCounter(this.thisShard, online);
let buffer = OnlineCounter.dehydrate(online);
// send our online counter to other shards
let buffer = dehydrateOnlineCounter(online);
this.publisher.publish(this.thisShard, buffer);
// send total counter to our players
buffer = OnlineCounter.dehydrate(this.onlineCounter);
buffer = dehydrateOnlineCounter(this.onlineCounter);
super.emit('onlineCounter', buffer);
}

View File

@ -3,9 +3,10 @@
*/
import EventEmitter from 'events';
import OnlineCounter from './packets/OnlineCounter';
import PixelUpdate from './packets/PixelUpdateServer';
import {
dehydrateOnlineCounter,
dehydratePixelUpdate,
} from './packets/server';
class SocketEvents extends EventEmitter {
constructor() {
@ -97,7 +98,7 @@ class SocketEvents extends EventEmitter {
) {
const i = chunkId >> 8;
const j = chunkId & 0xFF;
const buffer = PixelUpdate.dehydrate(i, j, pixels);
const buffer = dehydratePixelUpdate(i, j, pixels);
this.emit('pixelUpdate', canvasId, chunkId, buffer);
this.emit('chunkUpdate', canvasId, [i, j]);
}
@ -254,7 +255,7 @@ class SocketEvents extends EventEmitter {
*/
broadcastOnlineCounter(online) {
this.onlineCounter = online;
const buffer = OnlineCounter.dehydrate(online);
const buffer = dehydrateOnlineCounter(online);
this.emit('onlineCounter', buffer);
}
}

View File

@ -1,19 +1,28 @@
// allow the websocket to be noisy on the console
/* eslint-disable no-console */
import EventEmitter from 'events';
import CoolDownPacket from './packets/CoolDownPacket';
import PixelUpdate from './packets/PixelUpdateClient';
import PixelReturn from './packets/PixelReturn';
import OnlineCounter from './packets/OnlineCounter';
import RegisterCanvas from './packets/RegisterCanvas';
import RegisterChunk from './packets/RegisterChunk';
import RegisterMultipleChunks from './packets/RegisterMultipleChunks';
import DeRegisterChunk from './packets/DeRegisterChunk';
import ChangedMe from './packets/ChangedMe';
import Ping from './packets/Ping';
import {
hydratePixelUpdate,
hydratePixelReturn,
hydrateOnlineCounter,
hydrateCoolDown,
dehydrateRegCanvas,
dehydrateRegChunk,
dehydrateRegMChunks,
dehydrateDeRegChunk,
dehydrateCaptchaSolution,
dehydratePixelUpdate,
dehydratePing,
} from './packets/client';
import {
PIXEL_UPDATE_OP,
PIXEL_RETURN_OP,
ONLINE_COUNTER_OP,
COOLDOWN_OP,
CHANGE_ME_OP,
} from './packets/op';
import { shardHost } from '../store/actions/fetch';
const chunks = [];
@ -22,8 +31,8 @@ class SocketClient extends EventEmitter {
constructor() {
super();
console.log('Creating WebSocketClient');
this.store = null;
this.ws = null;
this.canvasId = 0;
this.channelId = 0;
/*
* properties set in connect and open:
@ -38,6 +47,11 @@ class SocketClient extends EventEmitter {
setInterval(this.checkHealth, 2000);
}
initialize(store) {
this.store = store;
return this.connect();
}
async connect() {
this.readyState = WebSocket.CONNECTING;
if (this.ws) {
@ -70,7 +84,7 @@ class SocketClient extends EventEmitter {
}
if (now - 23000 > this.timeLastSent) {
// make sure we send something at least all 25s
this.send(Ping.dehydrate());
this.send(dehydratePing());
this.timeLastSent = now;
}
}
@ -110,31 +124,28 @@ class SocketClient extends EventEmitter {
this.emit('open', {});
this.readyState = WebSocket.OPEN;
this.send(RegisterCanvas.dehydrate(this.canvasId));
this.send(dehydrateRegCanvas(
this.store.getState().canvas,
));
console.log(`Register ${chunks.length} chunks`);
this.send(RegisterMultipleChunks.dehydrate(chunks));
this.send(dehydrateRegMChunks(chunks));
this.processMsgQueue();
}
setCanvas(canvasId) {
/* canvasId can be string or integer, thanks to
* JSON not allowing integer keys
*/
// eslint-disable-next-line eqeqeq
if (this.canvasId == canvasId || canvasId === null) {
if (canvasId === null) {
return;
}
console.log('Notify websocket server that we changed canvas');
this.canvasId = canvasId;
chunks.length = 0;
this.send(RegisterCanvas.dehydrate(this.canvasId));
this.send(dehydrateRegCanvas(canvasId));
}
registerChunk(cell) {
const [i, j] = cell;
const chunkid = (i << 8) | j;
chunks.push(chunkid);
const buffer = RegisterChunk.dehydrate(chunkid);
const buffer = dehydrateRegChunk(chunkid);
if (this.readyState === WebSocket.OPEN) {
this.send(buffer);
}
@ -143,7 +154,7 @@ class SocketClient extends EventEmitter {
deRegisterChunk(cell) {
const [i, j] = cell;
const chunkid = (i << 8) | j;
const buffer = DeRegisterChunk.dehydrate(chunkid);
const buffer = dehydrateDeRegChunk(chunkid);
if (this.readyState === WebSocket.OPEN) {
this.send(buffer);
}
@ -151,6 +162,12 @@ class SocketClient extends EventEmitter {
if (~pos) chunks.splice(pos, 1);
}
/*
sendCaptchaSolution(solution) {
const buffer = dehydrateCaptchaSolution(solution);
}
*/
/*
* Send pixel request
* @param i, j chunk coordinates
@ -160,7 +177,7 @@ class SocketClient extends EventEmitter {
i, j,
pixels,
) {
const buffer = PixelUpdate.dehydrate(i, j, pixels);
const buffer = dehydratePixelUpdate(i, j, pixels);
this.sendWhenReady(buffer);
}
@ -196,7 +213,6 @@ class SocketClient extends EventEmitter {
name, text, country, Number(channelId), userId);
return;
}
case 2: {
// signal
const [signal, args] = data;
@ -217,19 +233,19 @@ class SocketClient extends EventEmitter {
this.timeLastPing = Date.now();
switch (opcode) {
case PixelUpdate.OP_CODE:
this.emit('pixelUpdate', PixelUpdate.hydrate(data));
case PIXEL_UPDATE_OP:
this.emit('pixelUpdate', hydratePixelUpdate(data));
break;
case PixelReturn.OP_CODE:
this.emit('pixelReturn', PixelReturn.hydrate(data));
case PIXEL_RETURN_OP:
this.emit('pixelReturn', hydratePixelReturn(data));
break;
case OnlineCounter.OP_CODE:
this.emit('onlineCounter', OnlineCounter.hydrate(data));
case ONLINE_COUNTER_OP:
this.emit('onlineCounter', hydrateOnlineCounter(data));
break;
case CoolDownPacket.OP_CODE:
this.emit('cooldownPacket', CoolDownPacket.hydrate(data));
case COOLDOWN_OP:
this.emit('cooldownPacket', hydrateCoolDown(data));
break;
case ChangedMe.OP_CODE:
case CHANGE_ME_OP:
console.log('Websocket requested api/me reload');
this.emit('changedMe');
this.reconnect();

View File

@ -7,18 +7,28 @@ import logger from '../core/logger';
import canvases from '../core/canvases';
import Counter from '../utils/Counter';
import { getIPFromRequest, getHostFromRequest } from '../utils/ip';
import CoolDownPacket from './packets/CoolDownPacket';
import PixelUpdate from './packets/PixelUpdateServer';
import PixelReturn from './packets/PixelReturn';
import RegisterCanvas from './packets/RegisterCanvas';
import RegisterChunk from './packets/RegisterChunk';
import RegisterMultipleChunks from './packets/RegisterMultipleChunks';
import DeRegisterChunk from './packets/DeRegisterChunk';
import DeRegisterMultipleChunks from './packets/DeRegisterMultipleChunks';
import ChangedMe from './packets/ChangedMe';
import OnlineCounter from './packets/OnlineCounter';
import {
REG_CANVAS_OP,
PIXEL_UPDATE_OP,
REG_CHUNK_OP,
REG_MCHUNKS_OP,
DEREG_CHUNK_OP,
DEREG_MCHUNKS_OP,
} from './packets/op';
import {
hydrateRegCanvas,
hydrateRegChunk,
hydrateDeRegChunk,
hydrateRegMChunks,
hydrateDeRegMChunks,
hydrateCaptchaSolution,
hydratePixelUpdate,
dehydrateChangeMe,
dehydrateOnlineCounter,
dehydratePixelUpdate,
dehydrateCoolDown,
dehydratePixelReturn,
} from './packets/server';
import socketEvents from './socketEvents';
import chatProvider, { ChatProvider } from '../core/ChatProvider';
import authenticateClient from './authenticateClient';
@ -88,7 +98,7 @@ class SocketServer {
const { ip } = user;
ws.send(OnlineCounter.dehydrate(socketEvents.onlineCounter));
ws.send(dehydrateOnlineCounter(socketEvents.onlineCounter));
ws.on('error', (e) => {
logger.error(`WebSocket Client Error for ${ws.name}: ${e.message}`);
@ -341,7 +351,7 @@ class SocketServer {
if (ws.name === name) {
await ws.user.reload();
ws.name = ws.user.getName();
const buffer = ChangedMe.dehydrate();
const buffer = dehydrateChangeMe();
ws.send(buffer);
}
});
@ -474,7 +484,7 @@ class SocketServer {
try {
const opcode = buffer[0];
switch (opcode) {
case PixelUpdate.OP_CODE: {
case PIXEL_UPDATE_OP: {
const { canvasId, user } = ws;
const { ip } = user;
@ -499,7 +509,7 @@ class SocketServer {
const {
i, j, pixels,
} = PixelUpdate.hydrate(buffer);
} = hydratePixelUpdate(buffer);
const {
wait,
coolDown,
@ -522,7 +532,7 @@ class SocketServer {
}
}
ws.send(PixelReturn.dehydrate(
ws.send(dehydratePixelReturn(
retCode,
wait,
coolDown,
@ -531,42 +541,38 @@ class SocketServer {
));
break;
}
case RegisterCanvas.OP_CODE: {
const canvasId = RegisterCanvas.hydrate(buffer);
case REG_CANVAS_OP: {
const canvasId = hydrateRegCanvas(buffer);
if (!canvases[canvasId]) return;
if (ws.canvasId !== null && ws.canvasId !== canvasId) {
this.deleteAllChunks(ws);
}
ws.canvasId = canvasId;
const wait = await ws.user.getWait(canvasId);
ws.send(CoolDownPacket.dehydrate(wait));
ws.send(dehydrateCoolDown(wait));
break;
}
case RegisterChunk.OP_CODE: {
const chunkid = RegisterChunk.hydrate(buffer);
case REG_CHUNK_OP: {
const chunkid = hydrateRegChunk(buffer);
this.pushChunk(chunkid, ws);
break;
}
case RegisterMultipleChunks.OP_CODE: {
case REG_MCHUNKS_OP: {
this.deleteAllChunks(ws);
let posu = 2;
while (posu < buffer.length) {
const chunkid = buffer[posu++] | buffer[posu++] << 8;
hydrateRegMChunks(buffer, (chunkid) => {
this.pushChunk(chunkid, ws);
}
});
break;
}
case DeRegisterChunk.OP_CODE: {
const chunkidn = DeRegisterChunk.hydrate(buffer);
this.deleteChunk(chunkidn, ws);
case DEREG_CHUNK_OP: {
const chunkid = hydrateDeRegChunk(buffer);
this.deleteChunk(chunkid, ws);
break;
}
case DeRegisterMultipleChunks.OP_CODE: {
let posl = 2;
while (posl < buffer.length) {
const chunkid = buffer[posl++] | buffer[posl++] << 8;
case DEREG_MCHUNKS_OP: {
hydrateDeRegMChunks(buffer, (chunkid) => {
this.deleteChunk(chunkid, ws);
}
});
break;
}
default:

View File

@ -1,10 +0,0 @@
const OP_CODE = 0xA6;
export default {
OP_CODE,
dehydrate() {
// Server (sender)
return new Uint8Array([OP_CODE]).buffer;
},
};

View File

@ -1,31 +0,0 @@
/*
* notify that chunk changed
* (not sent over websocket, server only)
*/
const OP_CODE = 0xC4;
export default {
OP_CODE,
/*
* @return canvasId, [i, j]
*/
hydrate(data) {
const canvasId = data[1];
const i = data.readUInt8(2);
const j = data.readUInt8(3);
return [canvasId, [i, j]];
},
/*
* @param canvasId,
* chunkid id consisting of chunk coordinates
*/
dehydrate(canvasId, [i, j]) {
return Buffer.from([
OP_CODE,
canvasId,
i,
j,
]);
},
};

View File

@ -1,16 +0,0 @@
const OP_CODE = 0xC2;
export default {
OP_CODE,
hydrate(data) {
// client (receiver)
return data.getUint32(1);
},
dehydrate(wait) {
// Server (sender)
const buffer = Buffer.allocUnsafe(1 + 4);
buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt32BE(wait, 1);
return buffer;
},
};

View File

@ -1,18 +0,0 @@
const OP_CODE = 0xA2;
export default {
OP_CODE,
hydrate(data) {
// SERVER (Receiver)
const i = data[1] << 8 | data[2];
return i;
},
dehydrate(chunkid) {
// CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 2);
const view = new DataView(buffer);
view.setInt8(0, OP_CODE);
view.setInt16(1, chunkid);
return buffer;
},
};

View File

@ -1,20 +0,0 @@
const OP_CODE = 0xA4;
export default {
OP_CODE,
/*
* @param chunks Array of chunks
*/
dehydrate(chunks) {
// CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 1 + chunks.length * 2);
const view = new Uint16Array(buffer);
// this will result into a double first byte, but still better than
// shifting 16bit integers around later
view[0] = OP_CODE;
for (let cnt = 0; cnt < chunks.length; cnt += 1) {
view[cnt + 1] = chunks[cnt];
}
return buffer;
},
};

View File

@ -1,51 +0,0 @@
/*
*
* Numbers of online players per canvas
*
*/
const OP_CODE = 0xA7;
export default {
OP_CODE,
// CLIENT (receiver)
/*
* {
* total: totalOnline,
* canvasId: online,
* ....
* }
*/
hydrate(data) {
const online = {};
online.total = data.getUint16(1);
let off = data.byteLength;
while (off > 3) {
const onlineUsers = data.getUint16(off -= 2);
const canvas = data.getUint8(off -= 1);
online[canvas] = onlineUsers;
}
return online;
},
dehydrate(online) {
// SERVER (sender)
if (!process.env.BROWSER) {
const canvasIds = Object.keys(online).filter((id) => id !== 'total');
const buffer = Buffer.allocUnsafe(3 + canvasIds.length * (1 + 2));
buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt16BE(online.total, 1);
let cnt = 1;
for (let p = 0; p < canvasIds.length; p += 1) {
const canvasId = canvasIds[p];
const onlineUsers = online[canvasId];
buffer.writeUInt8(Number(canvasId), cnt += 2);
buffer.writeUInt16BE(onlineUsers, cnt += 1);
}
return buffer;
}
return 0;
},
};

View File

@ -1,10 +0,0 @@
const OP_CODE = 0xB0;
export default {
OP_CODE,
dehydrate() {
// Client (sender)
return new Uint8Array([OP_CODE]).buffer;
},
};

View File

@ -1,38 +0,0 @@
const OP_CODE = 0xC3;
export default {
OP_CODE,
hydrate(data) {
// Client (receiver)
const retCode = data.getUint8(1);
const wait = data.getUint32(2);
const coolDownSeconds = data.getInt16(6);
const pxlCnt = data.getUint8(8);
const rankedPxlCnt = data.getUint8(9);
return {
retCode,
wait,
coolDownSeconds,
pxlCnt,
rankedPxlCnt,
};
},
dehydrate(
retCode,
wait,
coolDown,
pxlCnt,
rankedPxlCnt,
) {
// Server (sender)
const buffer = Buffer.allocUnsafe(1 + 1 + 4 + 2 + 1 + 1);
buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt8(retCode, 1);
buffer.writeUInt32BE(wait, 2);
const coolDownSeconds = Math.round(coolDown / 1000);
buffer.writeInt16BE(coolDownSeconds, 6);
buffer.writeUInt8(pxlCnt, 8);
buffer.writeUInt8(rankedPxlCnt, 9);
return buffer;
},
};

View File

@ -1,66 +0,0 @@
/*
* Packet for sending and receiving pixels per chunk
* Multiple pixels can be sent at once
* Client side
*
*/
const OP_CODE = 0xC1;
export default {
OP_CODE,
/*
* @param data DataVies
*/
hydrate(data) {
/*
* chunk coordinates
*/
const i = data.getUint8(1);
const j = data.getUint8(2);
/*
* offset and color of every pixel
* 3 bytes offset
* 1 byte color
*/
const pixels = [];
let off = data.byteLength;
while (off > 3) {
const color = data.getUint8(off -= 1);
const offsetL = data.getUint16(off -= 2);
const offsetH = data.getUint8(off -= 1) << 16;
pixels.push([offsetH | offsetL, color]);
}
return {
i, j, pixels,
};
},
dehydrate(i, j, pixels) {
const buffer = new ArrayBuffer(1 + 1 + 1 + pixels.length * 4);
const view = new DataView(buffer);
view.setUint8(0, OP_CODE);
/*
* chunk coordinates
*/
view.setUint8(1, i);
view.setUint8(2, j);
/*
* offset and color of every pixel
* 3 bytes offset
* 1 byte color
*/
let cnt = 2;
let p = pixels.length;
while (p) {
p -= 1;
const [offset, color] = pixels[p];
view.setUint8(cnt += 1, offset >>> 16);
view.setUint16(cnt += 1, offset & 0x00FFFF);
view.setUint8(cnt += 2, color);
}
return buffer;
},
};

View File

@ -1,44 +0,0 @@
/*
* Packet for sending and receiving pixels over Message Broker between shards
* Multiple pixels can be sent at once
*
*/
const OP_CODE = 0xC1;
export default {
OP_CODE,
/*
* returns info and PixelUpdate package to send to clients
*/
hydrate(data) {
const canvasId = data[1];
data.writeUInt8(OP_CODE, 1);
const chunkId = data.readUInt16BE(2);
const pixelUpdate = Buffer.from(
data.buffer,
data.byteOffset + 1,
data.length - 1,
);
return [
canvasId,
chunkId,
pixelUpdate,
];
},
/*
* @param canvasId
* @param chunkId id consisting of chunk coordinates
* @param pixels Buffer with offset and color of one or more pixels
*/
dehydrate(canvasId, i, j, pixels) {
const index = new Uint8Array([
OP_CODE,
canvasId,
i,
j,
]);
return Buffer.concat([index, pixels]);
},
};

View File

@ -1,50 +0,0 @@
/*
* Packet for sending and receiving pixels per chunk
* Multiple pixels can be sent at once
* Server side.
*
* */
const OP_CODE = 0xC1;
export default {
OP_CODE,
hydrate(data) {
/*
* chunk coordinates
*/
const i = data.readUInt8(1);
const j = data.readUInt8(2);
/*
* offset and color of every pixel
* 3 bytes offset
* 1 byte color
*/
const pixels = [];
let off = data.length;
/*
* limit the max amount of pixels that can be
* receive to 500
*/
let pxlcnt = 0;
while (off > 3 && pxlcnt < 500) {
const color = data.readUInt8(off -= 1);
const offsetL = data.readUInt16BE(off -= 2);
const offsetH = data.readUInt8(off -= 1) << 16;
pixels.push([offsetH | offsetL, color]);
pxlcnt += 1;
}
return {
i, j, pixels,
};
},
/*
* @param chunkId id consisting of chunk coordinates
* @param pixels Buffer with offset and color of one or more pixels
*/
dehydrate(i, j, pixels) {
const index = new Uint8Array([OP_CODE, i, j]);
return Buffer.concat([index, pixels]);
},
};

View File

@ -1,6 +1,4 @@
# Binary Websocket Packages
Note that the node Server receives in [Buffer](https://nodejs.org/api/buffer.html), while the client receives [DataViews](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) and sends ArrayBuffers.
Therefor the server can't share the same code with the client for hydrate / dehydrate.
Most packages are unidirectional so hydrate is for either client or server and dehydrate for the other one.
Bidrectional packages have two files, one for Client, another one for Server.
Note that the node Server receives and sends in [Buffer](https://nodejs.org/api/buffer.html), while the client receives [DataViews](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) and sends ArrayBuffers.
Therefor the server can't share the same code with the client for hydrate / dehydrate and it's split in two files.

View File

@ -1,18 +0,0 @@
const OP_CODE = 0xA0;
export default {
OP_CODE,
hydrate(data) {
// SERVER (Receiver)
const canvasId = data[1];
return canvasId;
},
dehydrate(canvasId) {
// CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 1);
const view = new DataView(buffer);
view.setInt8(0, OP_CODE);
view.setInt8(1, Number(canvasId));
return buffer;
},
};

View File

@ -1,18 +0,0 @@
const OP_CODE = 0xA1;
export default {
OP_CODE,
hydrate(data) {
// SERVER (Receiver)
const i = data[1] << 8 | data[2];
return i;
},
dehydrate(chunkid) {
// CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 2);
const view = new DataView(buffer);
view.setInt8(0, OP_CODE);
view.setInt16(1, chunkid);
return buffer;
},
};

View File

@ -1,20 +0,0 @@
const OP_CODE = 0xA3;
export default {
OP_CODE,
/*
* @param chunks Array of chunks
*/
dehydrate(chunks) {
// CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 1 + chunks.length * 2);
const view = new Uint16Array(buffer);
// this will result into a double first byte, but still better than
// shifting 16bit integers around later
view[0] = OP_CODE;
for (let cnt = 0; cnt < chunks.length; cnt += 1) {
view[cnt + 1] = chunks[cnt];
}
return buffer;
},
};

View File

@ -0,0 +1,196 @@
/*
* client package hydration
*/
import {
REG_CANVAS_OP,
REG_CHUNK_OP,
DEREG_CHUNK_OP,
REG_MCHUNKS_OP,
DEREG_MCHUNKS_OP,
CAPTCHA_SOLUTION_OP,
PING_OP,
PIXEL_UPDATE_OP,
} from './op';
/*
* data in hydrate functions is DataView
*/
/*
* @return {
* total: totalOnline,
* canvasId: online,
* ....
* }
*/
export function hydrateOnlineCounter(data) {
const online = {};
online.total = data.getUint16(1);
let off = data.byteLength;
while (off > 3) {
const onlineUsers = data.getUint16(off -= 2);
const canvas = data.getUint8(off -= 1);
online[canvas] = onlineUsers;
}
return online;
}
/*
* @return chunk coordinates and array of pixel offset and colors
*/
export function hydratePixelUpdate(data) {
const i = data.getUint8(1);
const j = data.getUint8(2);
/*
* offset and color of every pixel
* 3 bytes offset
* 1 byte color
*/
const pixels = [];
let off = data.byteLength;
while (off > 3) {
const color = data.getUint8(off -= 1);
const offsetL = data.getUint16(off -= 2);
const offsetH = data.getUint8(off -= 1) << 16;
pixels.push([offsetH | offsetL, color]);
}
return {
i, j, pixels,
};
}
/*
* @return cooldown in ms
*/
export function hydrateCoolDown(data) {
return data.getUint32(1);
}
/*
* @return see ui/placePixels
*/
export function hydratePixelReturn(data) {
// Client (receiver)
const retCode = data.getUint8(1);
const wait = data.getUint32(2);
const coolDownSeconds = data.getInt16(6);
const pxlCnt = data.getUint8(8);
const rankedPxlCnt = data.getUint8(9);
return {
retCode,
wait,
coolDownSeconds,
pxlCnt,
rankedPxlCnt,
};
}
/*
* dehydrate functions return ArrayBuffer object
*/
/*
* @param canvasId
*/
export function dehydrateRegCanvas(canvasId) {
const buffer = new ArrayBuffer(1 + 1);
const view = new DataView(buffer);
view.setInt8(0, REG_CANVAS_OP);
view.setInt8(1, Number(canvasId));
return buffer;
}
/*
* @param chunkid
*/
export function dehydrateRegChunk(chunkid) {
const buffer = new ArrayBuffer(1 + 2);
const view = new DataView(buffer);
view.setInt8(0, REG_CHUNK_OP);
view.setInt16(1, chunkid);
return buffer;
}
/*
* @param chunkid
*/
export function dehydrateDeRegChunk(chunkid) {
const buffer = new ArrayBuffer(1 + 2);
const view = new DataView(buffer);
view.setInt8(0, DEREG_CHUNK_OP);
view.setInt16(1, chunkid);
return buffer;
}
/*
* @param chunks Array of chunkIds
*/
export function dehydrateRegMChunks(chunks) {
const buffer = new ArrayBuffer(1 + 1 + chunks.length * 2);
const view = new Uint16Array(buffer);
// this will result into a double first byte, but still better than
// shifting 16bit integers around later
view[0] = REG_MCHUNKS_OP;
for (let cnt = 0; cnt < chunks.length; cnt += 1) {
view[cnt + 1] = chunks[cnt];
}
return buffer;
}
/*
* @param chunks Array of chunkIds
*/
export function dehydrateDeRegMChunks(chunks) {
const buffer = new ArrayBuffer(1 + 1 + chunks.length * 2);
const view = new Uint16Array(buffer);
// this will result into a double first byte, but still better than
// shifting 16bit integers around later
view[0] = DEREG_MCHUNKS_OP;
for (let cnt = 0; cnt < chunks.length; cnt += 1) {
view[cnt + 1] = chunks[cnt];
}
return buffer;
}
/*
* @param solution string of entered captcha
*/
export function dehydrateCaptchaSolution(solution) {
const encoder = new TextEncoder();
const view = encoder.encode(solution);
const buffer = new Uint8Array(view.byteLength + 1);
buffer[0] = CAPTCHA_SOLUTION_OP;
buffer.set(view, 1);
return buffer.buffer;
}
export function dehydratePing() {
return new Uint8Array([PING_OP]).buffer;
}
/*
* @param i, j chunk coordinates
* @param pixels array of offsets and colors of pixels
*/
export function dehydratePixelUpdate(i, j, pixels) {
const buffer = new ArrayBuffer(1 + 1 + 1 + pixels.length * 4);
const view = new DataView(buffer);
view.setUint8(0, PIXEL_UPDATE_OP);
view.setUint8(1, i);
view.setUint8(2, j);
/*
* offset and color of every pixel
* 3 bytes offset
* 1 byte color
*/
let cnt = 2;
let p = pixels.length;
while (p) {
p -= 1;
const [offset, color] = pixels[p];
view.setUint8(cnt += 1, offset >>> 16);
view.setUint16(cnt += 1, offset & 0x00FFFF);
view.setUint8(cnt += 2, color);
}
return buffer;
}

21
src/socket/packets/op.js Normal file
View File

@ -0,0 +1,21 @@
/*
* OP CODES
*/
/*
* we export code so that webpack can directly resolve them
*/
export const REG_CANVAS_OP = 0xA0;
export const REG_CHUNK_OP = 0xA1;
export const DEREG_CHUNK_OP = 0xA2;
export const REG_MCHUNKS_OP = 0xA3;
export const DEREG_MCHUNKS_OP = 0xA4;
export const CAPTCHA_SOLUTION_OP = 0xA5;
export const CHANGE_ME_OP = 0xA6;
export const ONLINE_COUNTER_OP = 0xA7;
export const PING_OP = 0xB0;
export const PIXEL_UPDATE_OP = 0xC1;
export const PIXEL_UPDATE_MB_OP = 0xC1;
export const COOLDOWN_OP = 0xC2;
export const PIXEL_RETURN_OP = 0xC3;
export const CHUNK_UPDATE_MB_OP = 0xC4;

View File

@ -0,0 +1,239 @@
/*
* server package hydration
*/
import {
CHANGE_ME_OP,
ONLINE_COUNTER_OP,
PIXEL_UPDATE_OP,
PIXEL_UPDATE_MB_OP,
COOLDOWN_OP,
PIXEL_RETURN_OP,
CHUNK_UPDATE_MB_OP,
} from './op';
/*
* data in hydrate function is a nodejs Buffer
*/
/*
* @return canvasId
*/
export function hydrateRegCanvas(data) {
const canvasId = data[1];
return canvasId;
}
/*
* @return {
* total: totalOnline,
* canvasId: online,
* ....
* }
*/
export function hydrateOnlineCounter(data) {
const online = {};
online.total = data.readUInt16BE(1);
let off = data.length;
while (off > 3) {
const onlineUsers = data.readUInt16BE(off -= 2);
const canvas = data.readUInt8(off -= 1);
online[canvas] = onlineUsers;
}
return online;
}
/*
* @return chunkId
*/
export function hydrateRegChunk(data) {
const i = data[1] << 8 | data[2];
return i;
}
/*
* @return chunkId
*/
export function hydrateDeRegChunk(data) {
const i = data[1] << 8 | data[2];
return i;
}
/*
* cb execute with individual chunkids
*/
export function hydrateRegMChunks(data, cb) {
let posu = 2;
while (posu < data.length) {
const chunkid = data[posu++] | data[posu++] << 8;
cb(chunkid);
}
}
/*
* cb execute with individual chunkids
*/
export function hydrateDeRegMChunks(data, cb) {
let posl = 2;
while (posl < data.length) {
const chunkid = data[posl++] | data[posl++] << 8;
cb(chunkid);
}
}
/*
* @return captcha solution
*/
export function hydrateCaptchaSolution(data) {
return data.toString('utf8', 1);
}
/*
* @return chunk id and array of pixel offset and color
*/
export function hydratePixelUpdate(data) {
const i = data.readUInt8(1);
const j = data.readUInt8(2);
const pixels = [];
let off = data.length;
let pxlcnt = 0;
while (off > 3 && pxlcnt < 500) {
const color = data.readUInt8(off -= 1);
const offsetL = data.readUInt16BE(off -= 2);
const offsetH = data.readUInt8(off -= 1) << 16;
pixels.push([offsetH | offsetL, color]);
pxlcnt += 1;
}
return {
i, j, pixels,
};
}
/*
* @returns info and PixelUpdate package to send to clients
*/
export function hydratePixelUpdateMB(data) {
const canvasId = data[1];
data.writeUInt8(PIXEL_UPDATE_OP, 1);
const chunkId = data.readUInt16BE(2);
const pixelUpdate = Buffer.from(
data.buffer,
data.byteOffset + 1,
data.length - 1,
);
return [
canvasId,
chunkId,
pixelUpdate,
];
}
/*
* @return canvasid and chunk coords
*/
export function hydrateChunkUpdateMB(data) {
const canvasId = data[1];
const i = data.readUInt8(2);
const j = data.readUInt8(3);
return [canvasId, [i, j]];
}
/*
* dehydrate functions return nodejs Buffer object
*/
/*
* returns buffer with only OP_CODE
*/
export function dehydrateChangeMe() {
return Buffer.from([CHANGE_ME_OP]);
}
/*
* @param {
* total: totalOnline,
* canvasId: online,
* ....
* }
*/
export function dehydrateOnlineCounter(online) {
const canvasIds = Object.keys(online).filter((id) => id !== 'total');
const buffer = Buffer.allocUnsafe(3 + canvasIds.length * (1 + 2));
buffer.writeUInt8(ONLINE_COUNTER_OP, 0);
buffer.writeUInt16BE(online.total, 1);
let cnt = 1;
for (let p = 0; p < canvasIds.length; p += 1) {
const canvasId = canvasIds[p];
const onlineUsers = online[canvasId];
buffer.writeUInt8(Number(canvasId), cnt += 2);
buffer.writeUInt16BE(onlineUsers, cnt += 1);
}
return buffer;
}
/*
* @param chunkId id consisting of chunk coordinates
* @param pixels Buffer with offset and color of one or more pixels
*/
export function dehydratePixelUpdate(i, j, pixels) {
const index = new Uint8Array([PIXEL_UPDATE_OP, i, j]);
return Buffer.concat([index, pixels]);
}
/*
* @param canvasId
* @param chunkId id consisting of chunk coordinates
* @param pixels Buffer with offset and color of one or more pixels
*/
export function dehydratePixelUpdateMB(canvasId, i, j, pixels) {
const index = new Uint8Array([
PIXEL_UPDATE_MB_OP,
canvasId,
i,
j,
]);
return Buffer.concat([index, pixels]);
}
/*
* @param wait cooldown in ms
*/
export function dehydrateCoolDown(wait) {
const buffer = Buffer.allocUnsafe(1 + 4);
buffer.writeUInt8(COOLDOWN_OP, 0);
buffer.writeUInt32BE(wait, 1);
return buffer;
}
/*
* for params see core/draw or ui/placePixel
*/
export function dehydratePixelReturn(
retCode,
wait,
coolDown,
pxlCnt,
rankedPxlCnt,
) {
const buffer = Buffer.allocUnsafe(1 + 1 + 4 + 2 + 1 + 1);
buffer.writeUInt8(PIXEL_RETURN_OP, 0);
buffer.writeUInt8(retCode, 1);
buffer.writeUInt32BE(wait, 2);
const coolDownSeconds = Math.round(coolDown / 1000);
buffer.writeInt16BE(coolDownSeconds, 6);
buffer.writeUInt8(pxlCnt, 8);
buffer.writeUInt8(rankedPxlCnt, 9);
return buffer;
}
/*
* @param canvasId
* @param Array with chunk coordinates
*/
export function dehydrateChunkUpdateMB(canvasId, [i, j]) {
return Buffer.from([
CHUNK_UPDATE_MB_OP,
canvasId,
i,
j,
]);
}

View File

@ -148,7 +148,7 @@ export function selectColor(color) {
export function selectCanvas(canvasId) {
return {
type: 's/SELECT_CANVAS',
canvasId,
canvasId: String(canvasId),
};
}

View File

@ -41,26 +41,25 @@ export default (store) => (next) => (action) => {
break;
}
default:
// nothing
}
const ret = next(action);
// executed after reducers
switch (action.type) {
case 'RELOAD_URL':
case 's/SELECT_CANVAS':
case 's/REC_ME': {
const prevState = store.getState();
const ret = next(action);
const state = store.getState();
const { canvasId } = state.canvas;
SocketClient.setCanvas(canvasId);
break;
if (prevState.canvas.canvasId === canvasId) {
// TODO see if this is the case anywhere
console.log('Not triggering change canvas');
} else {
SocketClient.setCanvas(canvasId);
}
return ret;
}
default:
// nothing
}
return ret;
return next(action);
};

View File

@ -5,10 +5,10 @@
import SocketClient from '../../socket/SocketClient';
export default () => (next) => (action) => {
export default (store) => (next) => (action) => {
if (SocketClient.readyState === WebSocket.CLOSED) {
if (action.type === 't/PARENT_CLOSED') {
SocketClient.connect();
SocketClient.initialize(store);
}
} else {
switch (action.type) {