Merge branch 'devel'

This commit is contained in:
HF 2022-06-25 21:36:22 +02:00
commit a069bb9d75
6 changed files with 237 additions and 91 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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