diff --git a/src/core/Tile.js b/src/core/Tile.js index 0b41a11..3fd6de7 100644 --- a/src/core/Tile.js +++ b/src/core/Tile.js @@ -20,33 +20,145 @@ import { TILE_SIZE, TILE_ZOOM_LEVEL } from './constants'; /* * Deletes a subtile from a tile (paints it in color 0), * if we wouldn't do it, it would be black + * @param tileSize size of the tile within the chunk * @param palette Palette to use * @param subtilesInTile how many subtiles are in a tile (per dimension) * @param cell subtile to delete [dx, dy] * @param buffer Uint8Array for RGB values of tile */ function deleteSubtilefromTile( + tileSize, palette, subtilesInTile, cell, buffer, ) { const [dx, dy] = cell; - const offset = (dx + dy * TILE_SIZE * subtilesInTile) * TILE_SIZE; - for (let row = 0; row < TILE_SIZE; row += 1) { - let channelOffset = (offset + row * TILE_SIZE * subtilesInTile) * 3; - const max = channelOffset + TILE_SIZE * 3; + const offset = (dx + dy * tileSize * subtilesInTile) * tileSize; + const [blankR, blankG, blankB] = palette.rgb; + for (let row = 0; row < tileSize; row += 1) { + let channelOffset = (offset + row * tileSize * subtilesInTile) * 3; + const max = channelOffset + tileSize * 3; while (channelOffset < max) { - // eslint-disable-next-line prefer-destructuring - buffer[channelOffset++] = palette.rgb[0]; - // eslint-disable-next-line prefer-destructuring - buffer[channelOffset++] = palette.rgb[1]; - // eslint-disable-next-line prefer-destructuring - buffer[channelOffset++] = palette.rgb[2]; + buffer[channelOffset++] = blankR; + buffer[channelOffset++] = blankG; + buffer[channelOffset++] = blankB; } } } +function addShrunkenSubtileToTile( + subtilesInTile, + cell, + subtile, + buffer, +) { + const tileSize = TILE_SIZE; + const [dx, dy] = cell; + const offset = (dx + dy * subtilesInTile * tileSize / 4) * tileSize / 4; + const target = tileSize / 4; + let tr; + let tg; + let tb; + let pos; + let tmp; + const linePad = (tileSize * 3 - 1) * 3; + for (let row = 0; row < target; row += 1) { + let channelOffset = (offset + row * target * subtilesInTile) * 3; + const max = channelOffset + target * 3; + pos = row * tileSize * 12; + while (channelOffset < max) { + tr = subtile[pos++]; + tg = subtile[pos++]; + tb = subtile[pos++]; + pos += 9; + tmp = pos + linePad; + buffer[channelOffset++] = (subtile[tmp++] + tr) / 2; + buffer[channelOffset++] = (subtile[tmp++] + tg) / 2; + buffer[channelOffset++] = (subtile[tmp++] + tb) / 2; + } + } +} + +/* + * this was a failed try, it ended up being slow + * and low quality +function addShrunkenIndexedSubtilesToTile( + palette, + tileSize, + subtilesInTile, + cell, + inpTile, + buffer, +) { + const [dx, dy] = cell; + const subtileSize = tileSize / subtilesInTile; + const inpTileLength = inpTile.length; + const offset = (dx + dy * tileSize) * subtileSize; + const { rgb } = palette; + let tr; + let tg; + let tb; + let channelOffset; + let posA; + let posB; + let clr; + const linePad = (tileSize + 1) * (subtilesInTile - 1); + let amountFullRows = Math.floor(inpTileLength / subtilesInTile); + // use available data + for (let row = 0; row < amountFullRows; row += 1) { + channelOffset = (offset + row * tileSize) * 3; + const max = channelOffset + subtileSize * 3; + posA = row * tileSize * subtilesInTile; + posB = posA + linePad; + while (channelOffset < max) { + clr = (inpTile[posA] & 0x3F) * 3; + tr = rgb[clr++]; + tg = rgb[clr++]; + tb = rgb[clr]; + posA += subtilesInTile; + clr = (inpTile[posB] & 0x3F) * 3; + buffer[channelOffset++] = (rgb[clr++] + tr) / 2; + buffer[channelOffset++] = (rgb[clr++] + tg) / 2; + buffer[channelOffset++] = (rgb[clr] + tb) / 2; + posB += subtilesInTile; + } + } + // padding the rest + [tr, tg, tb] = rgb; + if (inpTileLength % subtilesInTile) { + channelOffset = (offset + amountFullRows * tileSize) * 3; + const max = channelOffset + subtileSize * 3; + posA = amountFullRows * tileSize * subtilesInTile; + while (channelOffset < max) { + if (posA < inpTileLength) { + clr = (inpTile[posA] & 0x3F) * 3; + buffer[channelOffset++] = (rgb[clr++] + tr) / 2; + buffer[channelOffset++] = (rgb[clr++] + tg) / 2; + buffer[channelOffset++] = (rgb[clr] + tb) / 2; + posA += subtilesInTile; + } else { + buffer[channelOffset++] = tr; + buffer[channelOffset++] = tg; + buffer[channelOffset++] = tb; + } + } + amountFullRows += 1; + } + if (amountFullRows < subtileSize) { + for (let row = amountFullRows; row < subtileSize; row += 1) { + channelOffset = (offset + row * tileSize) * 3; + const max = channelOffset + subtileSize * 3; + while (channelOffset < max) { + buffer[channelOffset++] = tr; + buffer[channelOffset++] = tg; + buffer[channelOffset++] = tb; + } + } + } +} +*/ + /* * @param subtilesInTile how many subtiles are in a tile (per dimension) * @param cell where to add the tile [dx, dy] @@ -59,12 +171,13 @@ function addRGBSubtiletoTile( subtile, buffer, ) { + const tileSize = TILE_SIZE; const [dx, dy] = cell; - const chunkOffset = (dx + dy * subtilesInTile * TILE_SIZE) * TILE_SIZE; + const chunkOffset = (dx + dy * subtilesInTile * tileSize) * tileSize; let pos = 0; - for (let row = 0; row < TILE_SIZE; row += 1) { - let channelOffset = (chunkOffset + row * TILE_SIZE * subtilesInTile) * 3; - const max = channelOffset + TILE_SIZE * 3; + for (let row = 0; row < tileSize; row += 1) { + let channelOffset = (chunkOffset + row * tileSize * subtilesInTile) * 3; + const max = channelOffset + tileSize * 3; while (channelOffset < max) { buffer[channelOffset++] = subtile[pos++]; buffer[channelOffset++] = subtile[pos++]; @@ -87,8 +200,9 @@ function addIndexedSubtiletoTile( subtile, buffer, ) { + const tileSize = TILE_SIZE; const [dx, dy] = cell; - const chunkOffset = (dx + dy * subtilesInTile * TILE_SIZE) * TILE_SIZE; + const chunkOffset = (dx + dy * subtilesInTile * tileSize) * tileSize; const { rgb } = palette; const emptyR = rgb[0]; @@ -97,9 +211,9 @@ function addIndexedSubtiletoTile( let pos = 0; let clr; - for (let row = 0; row < TILE_SIZE; row += 1) { - let channelOffset = (chunkOffset + row * TILE_SIZE * subtilesInTile) * 3; - const max = channelOffset + TILE_SIZE * 3; + for (let row = 0; row < tileSize; row += 1) { + let channelOffset = (chunkOffset + row * tileSize * subtilesInTile) * 3; + const max = channelOffset + tileSize * 3; while (channelOffset < max) { if (pos < subtile.length) { clr = (subtile[pos++] & 0x3F) * 3; @@ -183,7 +297,13 @@ export async function createZoomTileFromChunk( if (na.length !== TILE_ZOOM_LEVEL * TILE_ZOOM_LEVEL) { na.forEach((element) => { - deleteSubtilefromTile(palette, TILE_ZOOM_LEVEL, element, tileRGBBuffer); + deleteSubtilefromTile( + TILE_SIZE, + palette, + TILE_ZOOM_LEVEL, + element, + tileRGBBuffer, + ); }); const filename = tileFileName(canvasTileFolder, [maxTiledZoom - 1, x, y]); @@ -227,7 +347,7 @@ export async function createZoomedTile( ) { const palette = gPalette || new Palette(canvas.colors); const tileRGBBuffer = new Uint8Array( - TILE_SIZE * TILE_SIZE * TILE_ZOOM_LEVEL * TILE_ZOOM_LEVEL * 3, + TILE_SIZE * TILE_SIZE * 3, ); const [z, x, y] = cell; @@ -243,7 +363,12 @@ export async function createZoomedTile( } try { const chunk = await sharp(chunkfile).removeAlpha().raw().toBuffer(); - addRGBSubtiletoTile(TILE_ZOOM_LEVEL, [dx, dy], chunk, tileRGBBuffer); + addShrunkenSubtileToTile( + TILE_ZOOM_LEVEL, + [dx, dy], + chunk, + tileRGBBuffer, + ); } catch (error) { console.error( // eslint-disable-next-line max-len @@ -255,7 +380,13 @@ export async function createZoomedTile( if (na.length !== TILE_ZOOM_LEVEL * TILE_ZOOM_LEVEL) { na.forEach((element) => { - deleteSubtilefromTile(palette, TILE_ZOOM_LEVEL, element, tileRGBBuffer); + deleteSubtilefromTile( + TILE_SIZE / TILE_ZOOM_LEVEL, + palette, + TILE_ZOOM_LEVEL, + element, + tileRGBBuffer, + ); }); const filename = tileFileName(canvasTileFolder, [z, x, y]); @@ -265,12 +396,12 @@ export async function createZoomedTile( tileRGBBuffer.buffer, ), { raw: { - width: TILE_SIZE * TILE_ZOOM_LEVEL, - height: TILE_SIZE * TILE_ZOOM_LEVEL, + width: TILE_SIZE, + height: TILE_SIZE, channels: 3, }, }, - ).resize(TILE_SIZE).toFile(filename); + ).toFile(filename); } catch (error) { console.error( `Tiling: Error on createZoomedTile: ${error.message}`, @@ -402,7 +533,7 @@ export async function createTexture( } na.forEach((element) => { - deleteSubtilefromTile(palette, amount, element, textureBuffer); + deleteSubtilefromTile(TILE_SIZE, palette, amount, element, textureBuffer); }); const filename = `${canvasTileFolder}/texture.png`; diff --git a/src/socket/SocketClient.js b/src/socket/SocketClient.js index a404a9c..266d710 100644 --- a/src/socket/SocketClient.js +++ b/src/socket/SocketClient.js @@ -1,5 +1,3 @@ -/* @flow - */ // allow the websocket to be noisy on the console /* eslint-disable no-console */ @@ -15,36 +13,36 @@ 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'; const chunks = []; class SocketClient extends EventEmitter { - url: string; - ws: WebSocket; - canvasId: number; - channelId: number; - timeConnected: number; - isConnected: number; - isConnecting: boolean; - msgQueue: Array; - constructor() { super(); console.log('Creating WebSocketClient'); - this.isConnecting = false; - this.isConnected = false; this.ws = null; this.canvasId = '0'; this.channelId = 0; + /* + * properties set in connect and open: + * this.timeLastConnecting + * this.timeLastPing + * this.timeLastSent + */ + this.readyState = WebSocket.CLOSED; this.msgQueue = []; + + this.checkHealth = this.checkHealth.bind(this); + setInterval(this.checkHealth, 2000); } async connect() { - this.isConnecting = true; + this.readyState = WebSocket.CONNECTING; if (this.ws) { console.log('WebSocket already open, not starting'); } - this.timeConnected = Date.now(); + this.timeLastConnecting = Date.now(); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = `${protocol}//${window.location.hostname}${ window.location.port ? `:${window.location.port}` : '' @@ -54,18 +52,34 @@ class SocketClient extends EventEmitter { this.ws.onopen = this.onOpen.bind(this); this.ws.onmessage = this.onMessage.bind(this); this.ws.onclose = this.onClose.bind(this); - this.ws.onerror = this.onError.bind(this); + this.ws.onerror = (err) => { + console.error('Socket encountered error, closing socket', err); + }; + } + + checkHealth() { + if (this.readyState === WebSocket.OPEN) { + const now = Date.now(); + if (now - 14000 > this.timeLastPing) { + // server didn't send anything, probably dead + console.log('Server is silent, killing websocket'); + this.readyState = WebSocket.CLOSING; + this.ws.close(); + } + if (now - 10000 > this.timeLastSent) { + // make sure we send something at least all 12s + this.sendWhenReady(Ping.dehydrate()); + } + } } sendWhenReady(msg) { - if (this.isConnected) { + this.timeLastSent = Date.now(); + if (this.readyState === WebSocket.OPEN) { this.ws.send(msg); } else { console.log('Tried sending message when websocket was closed!'); this.msgQueue.push(msg); - if (!this.isConnecting) { - this.connect(); - } } } @@ -76,8 +90,11 @@ class SocketClient extends EventEmitter { } onOpen() { - this.isConnecting = false; - this.isConnected = true; + const now = Date.now(); + this.timeLastPing = now; + this.timeLastSent = now; + this.readyState = WebSocket.OPEN; + this.emit('open', {}); if (this.canvasId !== null) { this.ws.send(RegisterCanvas.dehydrate(this.canvasId)); @@ -87,11 +104,6 @@ class SocketClient extends EventEmitter { this.ws.send(RegisterMultipleChunks.dehydrate(chunks)); } - onError(err) { - console.error('Socket encountered error, closing socket', err); - this.ws.close(); - } - setCanvas(canvasId) { /* canvasId can be string or integer, thanks to * JSON not allowing integer keys @@ -111,14 +123,18 @@ class SocketClient extends EventEmitter { const chunkid = (i << 8) | j; chunks.push(chunkid); const buffer = RegisterChunk.dehydrate(chunkid); - if (this.isConnected) this.ws.send(buffer); + if (this.readyState === WebSocket.OPEN) { + this.ws.send(buffer); + } } deRegisterChunk(cell) { const [i, j] = cell; const chunkid = (i << 8) | j; const buffer = DeRegisterChunk.dehydrate(chunkid); - if (this.isConnected) this.ws.send(buffer); + if (this.readyState === WebSocket.OPEN) { + this.ws.send(buffer); + } const pos = chunks.indexOf(chunkid); if (~pos) chunks.splice(pos, 1); } @@ -129,17 +145,15 @@ class SocketClient extends EventEmitter { * @param pixel Array of [[offset, color],...] pixels within chunk */ requestPlacePixels( - i: number, j: number, - pixels: Array, + i, j, + pixels, ) { const buffer = PixelUpdate.dehydrate(i, j, pixels); this.sendWhenReady(buffer); } sendChatMessage(message, channelId) { - if (this.isConnected) { - this.ws.send(JSON.stringify([message, channelId])); - } + this.sendWhenReady(JSON.stringify([message, channelId])); } onMessage({ data: message }) { @@ -196,6 +210,10 @@ class SocketClient extends EventEmitter { this.emit('pixelReturn', PixelReturn.hydrate(data)); break; case OnlineCounter.OP_CODE: + /* + * using online counter as sign-of-life ping + */ + this.timeLastPing = Date.now(); this.emit('onlineCounter', OnlineCounter.hydrate(data)); break; case CoolDownPacket.OP_CODE: @@ -215,9 +233,9 @@ class SocketClient extends EventEmitter { onClose(e) { this.emit('close'); this.ws = null; - this.isConnected = false; + this.readyState = WebSocket.CONNECTING; // reconnect in 1s if last connect was longer than 7s ago, else 5s - const timeout = this.timeConnected < Date.now() - 7000 ? 1000 : 5000; + const timeout = this.timeLastConnecting < Date.now() - 7000 ? 1000 : 5000; console.warn( `Socket is closed. Reconnect will be attempted in ${timeout} ms.`, e.reason, @@ -226,18 +244,14 @@ class SocketClient extends EventEmitter { setTimeout(() => this.connect(), 5000); } - close() { - this.ws.close(); - } - reconnect() { - if (this.isConnected) { - this.isConnected = false; + if (this.readyState === WebSocket.OPEN) { + this.readyState = WebSocket.CLOSING; console.log('Restarting WebSocket'); this.ws.onclose = null; this.ws.onmessage = null; - this.ws.close(); this.ws = null; + this.ws.close(); this.connect(); } } diff --git a/src/socket/SocketServer.js b/src/socket/SocketServer.js index 18a52e1..6f81c75 100644 --- a/src/socket/SocketServer.js +++ b/src/socket/SocketServer.js @@ -60,8 +60,8 @@ class SocketServer { this.broadcast = this.broadcast.bind(this); this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this); this.reloadUser = this.reloadUser.bind(this); - this.ping = this.ping.bind(this); this.onlineCounterBroadcast = this.onlineCounterBroadcast.bind(this); + this.checkHealth = this.checkHealth.bind(this); } initialize() { @@ -83,9 +83,8 @@ class SocketServer { }); wss.on('connection', async (ws, req) => { - ws.isAlive = true; + ws.timeLastMsg = Date.now(); ws.canvasId = null; - ws.startDate = Date.now(); const user = await authenticateClient(req); if (!user) { ws.close(); @@ -103,16 +102,13 @@ class SocketServer { logger.error(`WebSocket Client Error for ${ws.name}: ${e.message}`); }); - ws.on('pong', () => { - ws.isAlive = true; - }); - ws.on('close', () => { ipCounter.delete(ip); this.deleteAllChunks(ws); }); ws.on('message', (data, isBinary) => { + ws.timeLastMsg = Date.now(); if (isBinary) { this.onBinaryMessage(data, ws); } else { @@ -179,7 +175,7 @@ class SocketServer { }); setInterval(this.onlineCounterBroadcast, 10 * 1000); - setInterval(this.ping, 15 * 1000); + setInterval(this.checkHealth, 15 * 1000); } verifyClient(info, done) { @@ -351,16 +347,15 @@ class SocketServer { }); } - ping() { + checkHealth() { + const ts = Date.now() - 15000; this.wss.clients.forEach((ws) => { - if (!ws.isAlive) { - if (ws.user) { - logger.info(`Killing dead websocket from ${ws.user.ip}`); - } + if ( + ws.readyState === WebSocket.OPEN + && ts > ws.timeLastMsg + ) { + logger.info(`Killing dead websocket from ${ws.user.ip}`); ws.terminate(); - } else { - ws.isAlive = false; - ws.ping(() => {}); } }); } diff --git a/src/socket/packets/ChangedMe.js b/src/socket/packets/ChangedMe.js index 839ae8d..967fe08 100644 --- a/src/socket/packets/ChangedMe.js +++ b/src/socket/packets/ChangedMe.js @@ -5,9 +5,6 @@ export default { dehydrate() { // Server (sender) - const buffer = new ArrayBuffer(1); - const view = new DataView(buffer); - view.setInt8(0, OP_CODE); - return buffer; + return new Uint8Array([OP_CODE]).buffer; }, }; diff --git a/src/socket/packets/Ping.js b/src/socket/packets/Ping.js new file mode 100644 index 0000000..c80f6be --- /dev/null +++ b/src/socket/packets/Ping.js @@ -0,0 +1,10 @@ +const OP_CODE = 0xB0; + +export default { + OP_CODE, + + dehydrate() { + // Client (sender) + return new Uint8Array([OP_CODE]).buffer; + }, +}; diff --git a/src/ui/ChunkLoader2D.js b/src/ui/ChunkLoader2D.js index c50e11a..a7204d3 100644 --- a/src/ui/ChunkLoader2D.js +++ b/src/ui/ChunkLoader2D.js @@ -181,7 +181,6 @@ class ChunkLoader { ) { const chunkKey = `${zoom}:${cx}:${cy}`; let chunkRGB = this.chunks.get(chunkKey); - const { canvasId } = this; if (chunkRGB) { if (chunkRGB.ready) { return chunkRGB.image; @@ -200,7 +199,7 @@ class ChunkLoader { const preLoad = this.preLoadChunk(zoom, cx, cy, chunkRGB); if (preLoad) return preLoad; } - return (showLoadingTile) ? loadingTiles.getTile(canvasId) : null; + return (showLoadingTile) ? loadingTiles.getTile(this.canvasId) : null; } getHistoricalChunk(cx, cy, fetch, historicalDate, historicalTime = null) {