puzzle/build/server/main.js

1827 lines
57 KiB
JavaScript
Raw Normal View History

2021-05-17 00:28:26 +02:00
import WebSocket from 'ws';
import express from 'express';
import multer from 'multer';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import sizeOf from 'image-size';
import exif from 'exif';
import sharp from 'sharp';
import bodyParser from 'body-parser';
import v8 from 'v8';
class Rng {
constructor(seed) {
this.rand_high = seed || 0xDEADC0DE;
this.rand_low = seed ^ 0x49616E42;
}
random(min, max) {
this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff;
this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff;
var n = (this.rand_high >>> 0) / 0xffffffff;
return (min + n * (max - min + 1)) | 0;
}
// get one random item from the given array
choice(array) {
return array[this.random(0, array.length - 1)];
}
// return a shuffled (shallow) copy of the given array
shuffle(array) {
const arr = array.slice();
for (let i = 0; i <= arr.length - 2; i++) {
const j = this.random(i, arr.length - 1);
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
return arr;
}
2021-05-17 00:28:26 +02:00
static serialize(rng) {
return {
rand_high: rng.rand_high,
rand_low: rng.rand_low
};
}
static unserialize(rngSerialized) {
const rng = new Rng(0);
rng.rand_high = rngSerialized.rand_high;
rng.rand_low = rngSerialized.rand_low;
return rng;
}
}
const pad = (x, pad) => {
const str = `${x}`;
if (str.length >= pad.length) {
return str;
}
return pad.substr(0, pad.length - str.length) + str;
};
const logger = (...pre) => {
const log = (m) => (...args) => {
const d = new Date();
const hh = pad(d.getHours(), '00');
const mm = pad(d.getMinutes(), '00');
const ss = pad(d.getSeconds(), '00');
console[m](`${hh}:${mm}:${ss}`, ...pre, ...args);
};
return {
log: log('log'),
error: log('error'),
info: log('info'),
};
};
// get a unique id
const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2);
function encodeShape(data) {
/* encoded in 1 byte:
00000000
^^ top
^^ right
^^ bottom
^^ left
*/
return ((data.top + 1) << 0)
| ((data.right + 1) << 2)
| ((data.bottom + 1) << 4)
| ((data.left + 1) << 6);
}
function decodeShape(data) {
return {
top: (data >> 0 & 0b11) - 1,
right: (data >> 2 & 0b11) - 1,
bottom: (data >> 4 & 0b11) - 1,
left: (data >> 6 & 0b11) - 1,
};
}
function encodeTile(data) {
return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group];
}
function decodeTile(data) {
return {
idx: data[0],
pos: {
x: data[1],
y: data[2],
},
z: data[3],
owner: data[4],
group: data[5],
};
}
function encodePlayer(data) {
return [
data.id,
data.x,
data.y,
data.d,
data.name,
data.color,
data.bgcolor,
data.points,
data.ts,
];
}
function decodePlayer(data) {
return {
id: data[0],
x: data[1],
y: data[2],
d: data[3],
name: data[4],
color: data[5],
bgcolor: data[6],
points: data[7],
ts: data[8],
};
}
function encodeGame(data) {
if (Array.isArray(data)) {
return data;
}
return [
data.id,
data.rng.type,
Rng.serialize(data.rng.obj),
data.puzzle,
data.players,
data.evtInfos,
data.scoreMode,
];
}
function decodeGame(data) {
if (!Array.isArray(data)) {
return data;
}
return {
id: data[0],
rng: {
type: data[1],
obj: Rng.unserialize(data[2]),
},
puzzle: data[3],
players: data[4],
evtInfos: data[5],
scoreMode: data[6],
};
}
function coordByTileIdx(info, tileIdx) {
const wTiles = info.width / info.tileSize;
return {
x: tileIdx % wTiles,
y: Math.floor(tileIdx / wTiles),
};
}
const hash = (str) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
let chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
var Util = {
hash,
uniqId,
encodeShape,
decodeShape,
encodeTile,
decodeTile,
encodePlayer,
decodePlayer,
encodeGame,
decodeGame,
coordByTileIdx,
};
const log$4 = logger('WebSocketServer.js');
/*
Example config
config = {
hostname: 'localhost',
port: 1338,
connectstring: `ws://localhost:1338/ws`,
}
*/
class EvtBus {
constructor() {
this._on = {};
}
on(type, callback) {
this._on[type] = this._on[type] || [];
this._on[type].push(callback);
}
dispatch(type, ...args) {
(this._on[type] || []).forEach((cb) => {
cb(...args);
});
}
}
class WebSocketServer {
constructor(config) {
this.config = config;
this._websocketserver = null;
this.evt = new EvtBus();
}
on(type, callback) {
this.evt.on(type, callback);
}
listen() {
this._websocketserver = new WebSocket.Server(this.config);
this._websocketserver.on('connection', (socket, request) => {
const pathname = new URL(this.config.connectstring).pathname;
if (request.url.indexOf(pathname) !== 0) {
log$4.log('bad request url: ', request.url);
socket.close();
return;
}
socket.on('message', (data) => {
log$4.log(`ws`, socket.protocol, data);
this.evt.dispatch('message', { socket, data });
});
socket.on('close', () => {
this.evt.dispatch('close', { socket });
});
});
}
close() {
if (this._websocketserver) {
this._websocketserver.close();
}
}
notifyOne(data, socket) {
socket.send(JSON.stringify(data));
}
}
/*
SERVER_CLIENT_MESSAGE_PROTOCOL
NOTE: clients always send game id and their id
when creating sockets (via socket.protocol), so
this doesn't need to be set in each message data
NOTE: first value in the array is always the type of event/message
when describing them below, the value each has is used
instead of writing EVENT_TYPE or something ismilar
EV_CLIENT_EVENT: event triggered by clients and sent to server
[
EV_CLIENT_EVENT, // constant value, type of event
CLIENT_SEQ, // sequence number sent by client.
EV_DATA, // (eg. mouse input info)
]
EV_SERVER_EVENT: event sent to clients after recieving a client
event and processing it
[
EV_SERVER_EVENT, // constant value, type of event
CLIENT_ID, // user who sent the client event
CLIENT_SEQ, // sequence number of the client event msg
CHANGES_TRIGGERED_BY_CLIENT_EVENT,
]
EV_CLIENT_INIT: event sent by client to enter a game
[
EV_CLIENT_INIT, // constant value, type of event
]
EV_SERVER_INIT: event sent to one client after that client
connects to a game
[
EV_SERVER_INIT, // constant value, type of event
GAME, // complete game instance required by
// client to build client side of the game
]
*/
const EV_SERVER_EVENT = 1;
const EV_SERVER_INIT = 4;
const EV_SERVER_INIT_REPLAY = 5;
const EV_CLIENT_EVENT = 2;
const EV_CLIENT_INIT = 3;
const EV_CLIENT_INIT_REPLAY = 6;
const LOG_HEADER = 1;
const LOG_ADD_PLAYER = 2;
const LOG_UPDATE_PLAYER = 4;
const LOG_HANDLE_INPUT = 3;
const INPUT_EV_MOUSE_DOWN = 1;
const INPUT_EV_MOUSE_UP = 2;
const INPUT_EV_MOUSE_MOVE = 3;
const INPUT_EV_ZOOM_IN = 4;
const INPUT_EV_ZOOM_OUT = 5;
const INPUT_EV_BG_COLOR = 6;
const INPUT_EV_PLAYER_COLOR = 7;
const INPUT_EV_PLAYER_NAME = 8;
const INPUT_EV_MOVE = 9;
const INPUT_EV_TOGGLE_PREVIEW = 10;
const CHANGE_DATA = 1;
const CHANGE_TILE = 2;
const CHANGE_PLAYER = 3;
var Protocol = {
EV_SERVER_EVENT,
EV_SERVER_INIT,
EV_SERVER_INIT_REPLAY,
EV_CLIENT_EVENT,
EV_CLIENT_INIT,
EV_CLIENT_INIT_REPLAY,
LOG_HEADER,
LOG_ADD_PLAYER,
LOG_UPDATE_PLAYER,
LOG_HANDLE_INPUT,
INPUT_EV_MOVE,
INPUT_EV_MOUSE_DOWN,
INPUT_EV_MOUSE_UP,
INPUT_EV_MOUSE_MOVE,
INPUT_EV_ZOOM_IN,
INPUT_EV_ZOOM_OUT,
INPUT_EV_BG_COLOR,
INPUT_EV_PLAYER_COLOR,
INPUT_EV_PLAYER_NAME,
INPUT_EV_TOGGLE_PREVIEW,
CHANGE_DATA,
CHANGE_TILE,
CHANGE_PLAYER,
};
function pointSub(a, b) {
return { x: a.x - b.x, y: a.y - b.y };
}
function pointAdd(a, b) {
return { x: a.x + b.x, y: a.y + b.y };
}
function pointDistance(a, b) {
const diffX = a.x - b.x;
const diffY = a.y - b.y;
return Math.sqrt(diffX * diffX + diffY * diffY);
}
function pointInBounds(pt, rect) {
return pt.x >= rect.x
&& pt.x <= rect.x + rect.w
&& pt.y >= rect.y
&& pt.y <= rect.y + rect.h;
}
function rectCenter(rect) {
return {
x: rect.x + (rect.w / 2),
y: rect.y + (rect.h / 2),
};
}
/**
* Returns a rectangle with same dimensions as the given one, but
* location (x/y) moved by x and y.
*
* @param {x, y, w,, h} rect
* @param number x
* @param number y
* @returns {x, y, w, h}
*/
function rectMoved(rect, x, y) {
return {
x: rect.x + x,
y: rect.y + y,
w: rect.w,
h: rect.h,
};
}
/**
* Returns true if the rectangles overlap, including their borders.
*
* @param {x, y, w, h} rectA
* @param {x, y, w, h} rectB
* @returns bool
*/
function rectsOverlap(rectA, rectB) {
return !(rectB.x > (rectA.x + rectA.w)
|| rectA.x > (rectB.x + rectB.w)
|| rectB.y > (rectA.y + rectA.h)
|| rectA.y > (rectB.y + rectB.h));
}
function rectCenterDistance(rectA, rectB) {
return pointDistance(rectCenter(rectA), rectCenter(rectB));
}
var Geometry = {
pointSub,
pointAdd,
pointDistance,
pointInBounds,
rectCenter,
rectMoved,
rectCenterDistance,
rectsOverlap,
};
const MS = 1;
const SEC = MS * 1000;
const MIN = SEC * 60;
const HOUR = MIN * 60;
const DAY = HOUR * 24;
const timestamp = () => {
const d = new Date();
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
};
const durationStr = (duration) => {
const d = Math.floor(duration / DAY);
duration = duration % DAY;
const h = Math.floor(duration / HOUR);
duration = duration % HOUR;
const m = Math.floor(duration / MIN);
duration = duration % MIN;
const s = Math.floor(duration / SEC);
return `${d}d ${h}h ${m}m ${s}s`;
};
const timeDiffStr = (from, to) => durationStr(to - from);
var Time = {
MS,
SEC,
MIN,
HOUR,
DAY,
timestamp,
timeDiffStr,
durationStr,
};
2021-05-17 02:32:33 +02:00
var PieceEdge;
(function (PieceEdge) {
PieceEdge[PieceEdge["Flat"] = 0] = "Flat";
PieceEdge[PieceEdge["Out"] = 1] = "Out";
PieceEdge[PieceEdge["In"] = -1] = "In";
})(PieceEdge || (PieceEdge = {}));
var ScoreMode;
(function (ScoreMode) {
ScoreMode[ScoreMode["FINAL"] = 0] = "FINAL";
ScoreMode[ScoreMode["ANY"] = 1] = "ANY";
})(ScoreMode || (ScoreMode = {}));
2021-05-17 00:28:26 +02:00
const IDLE_TIMEOUT_SEC = 30;
// Map<gameId, Game>
const GAMES = {};
function exists$1(gameId) {
return (!!GAMES[gameId]) || false;
}
function __createPlayerObject(id, ts) {
return {
id: id,
x: 0,
y: 0,
d: 0,
name: null,
color: null,
bgcolor: null,
points: 0,
ts: ts,
};
}
function setGame(gameId, game) {
GAMES[gameId] = game;
}
function getPlayerIndexById(gameId, playerId) {
let i = 0;
for (let player of GAMES[gameId].players) {
if (Util.decodePlayer(player).id === playerId) {
return i;
}
i++;
}
return -1;
}
function getPlayerIdByIndex(gameId, playerIndex) {
if (GAMES[gameId].players.length > playerIndex) {
return Util.decodePlayer(GAMES[gameId].players[playerIndex]).id;
}
return null;
}
function getPlayer(gameId, playerId) {
2021-05-17 02:32:33 +02:00
const idx = getPlayerIndexById(gameId, playerId);
2021-05-17 00:28:26 +02:00
return Util.decodePlayer(GAMES[gameId].players[idx]);
}
function setPlayer(gameId, playerId, player) {
2021-05-17 02:32:33 +02:00
const idx = getPlayerIndexById(gameId, playerId);
2021-05-17 00:28:26 +02:00
if (idx === -1) {
GAMES[gameId].players.push(Util.encodePlayer(player));
}
else {
GAMES[gameId].players[idx] = Util.encodePlayer(player);
}
}
function setTile(gameId, tileIdx, tile) {
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile);
}
function setPuzzleData(gameId, data) {
GAMES[gameId].puzzle.data = data;
}
function playerExists(gameId, playerId) {
const idx = getPlayerIndexById(gameId, playerId);
return idx !== -1;
}
function getActivePlayers(gameId, ts) {
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC;
return getAllPlayers(gameId).filter((p) => p.ts >= minTs);
}
function getIdlePlayers(gameId, ts) {
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC;
return getAllPlayers(gameId).filter((p) => p.ts < minTs && p.points > 0);
}
function addPlayer$1(gameId, playerId, ts) {
if (!playerExists(gameId, playerId)) {
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts));
}
else {
changePlayer(gameId, playerId, { ts });
}
}
function getEvtInfo(gameId, playerId) {
if (playerId in GAMES[gameId].evtInfos) {
return GAMES[gameId].evtInfos[playerId];
}
return {
_last_mouse: null,
_last_mouse_down: null,
};
}
function setEvtInfo(gameId, playerId, evtInfo) {
GAMES[gameId].evtInfos[playerId] = evtInfo;
}
function getAllGames() {
return Object.values(GAMES).sort((a, b) => {
// when both have same finished state, sort by started
if (isFinished(a.id) === isFinished(b.id)) {
return b.puzzle.data.started - a.puzzle.data.started;
}
// otherwise, sort: unfinished, finished
return isFinished(a.id) ? 1 : -1;
});
}
function getAllPlayers(gameId) {
return GAMES[gameId]
? GAMES[gameId].players.map(Util.decodePlayer)
: [];
}
function get$1(gameId) {
return GAMES[gameId];
}
function getTileCount(gameId) {
return GAMES[gameId].puzzle.tiles.length;
}
function getImageUrl(gameId) {
return GAMES[gameId].puzzle.info.imageUrl;
}
function setImageUrl(gameId, imageUrl) {
GAMES[gameId].puzzle.info.imageUrl = imageUrl;
}
function getScoreMode(gameId) {
2021-05-17 02:32:33 +02:00
return GAMES[gameId].scoreMode || ScoreMode.FINAL;
2021-05-17 00:28:26 +02:00
}
function isFinished(gameId) {
return getFinishedTileCount(gameId) === getTileCount(gameId);
}
function getFinishedTileCount(gameId) {
let count = 0;
for (let t of GAMES[gameId].puzzle.tiles) {
if (Util.decodeTile(t).owner === -1) {
count++;
}
}
return count;
}
function getTilesSortedByZIndex(gameId) {
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile);
return tiles.sort((t1, t2) => t1.z - t2.z);
}
function changePlayer(gameId, playerId, change) {
const player = getPlayer(gameId, playerId);
for (let k of Object.keys(change)) {
2021-05-17 02:32:33 +02:00
// @ts-ignore
2021-05-17 00:28:26 +02:00
player[k] = change[k];
}
setPlayer(gameId, playerId, player);
}
function changeData(gameId, change) {
for (let k of Object.keys(change)) {
// @ts-ignore
GAMES[gameId].puzzle.data[k] = change[k];
}
}
function changeTile(gameId, tileIdx, change) {
for (let k of Object.keys(change)) {
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]);
2021-05-17 02:32:33 +02:00
// @ts-ignore
2021-05-17 00:28:26 +02:00
tile[k] = change[k];
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile);
}
}
const getTile = (gameId, tileIdx) => {
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]);
};
const getTileGroup = (gameId, tileIdx) => {
const tile = getTile(gameId, tileIdx);
return tile.group;
};
const getFinalTilePos = (gameId, tileIdx) => {
const info = GAMES[gameId].puzzle.info;
const boardPos = {
x: (info.table.width - info.width) / 2,
y: (info.table.height - info.height) / 2
};
const srcPos = srcPosByTileIdx(gameId, tileIdx);
return Geometry.pointAdd(boardPos, srcPos);
};
const getTilePos = (gameId, tileIdx) => {
const tile = getTile(gameId, tileIdx);
return tile.pos;
};
// todo: instead, just make the table bigger and use that :)
const getBounds = (gameId) => {
const tw = getTableWidth(gameId);
const th = getTableHeight(gameId);
const overX = Math.round(tw / 4);
const overY = Math.round(th / 4);
return {
x: 0 - overX,
y: 0 - overY,
w: tw + 2 * overX,
h: th + 2 * overY,
};
};
const getTileBounds = (gameId, tileIdx) => {
const s = getTileSize(gameId);
const tile = getTile(gameId, tileIdx);
return {
x: tile.pos.x,
y: tile.pos.y,
w: s,
h: s,
};
};
const getTileZIndex = (gameId, tileIdx) => {
const tile = getTile(gameId, tileIdx);
return tile.z;
};
const getFirstOwnedTileIdx = (gameId, playerId) => {
for (let t of GAMES[gameId].puzzle.tiles) {
const tile = Util.decodeTile(t);
if (tile.owner === playerId) {
return tile.idx;
}
}
return -1;
};
const getFirstOwnedTile = (gameId, playerId) => {
const idx = getFirstOwnedTileIdx(gameId, playerId);
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx];
};
const getTileDrawOffset = (gameId) => {
return GAMES[gameId].puzzle.info.tileDrawOffset;
};
const getTileDrawSize = (gameId) => {
return GAMES[gameId].puzzle.info.tileDrawSize;
};
const getTileSize = (gameId) => {
return GAMES[gameId].puzzle.info.tileSize;
};
const getStartTs = (gameId) => {
return GAMES[gameId].puzzle.data.started;
};
const getFinishTs = (gameId) => {
return GAMES[gameId].puzzle.data.finished;
};
const getMaxGroup = (gameId) => {
return GAMES[gameId].puzzle.data.maxGroup;
};
const getMaxZIndex = (gameId) => {
return GAMES[gameId].puzzle.data.maxZ;
};
const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
let maxZ = 0;
for (let tileIdx of tileIdxs) {
let tileZIndex = getTileZIndex(gameId, tileIdx);
if (tileZIndex > maxZ) {
maxZ = tileZIndex;
}
}
return maxZ;
};
function srcPosByTileIdx(gameId, tileIdx) {
const info = GAMES[gameId].puzzle.info;
const c = Util.coordByTileIdx(info, tileIdx);
const cx = c.x * info.tileSize;
const cy = c.y * info.tileSize;
return { x: cx, y: cy };
}
function getSurroundingTilesByIdx(gameId, tileIdx) {
const info = GAMES[gameId].puzzle.info;
const c = Util.coordByTileIdx(info, tileIdx);
return [
// top
(c.y > 0) ? (tileIdx - info.tilesX) : -1,
// right
(c.x < info.tilesX - 1) ? (tileIdx + 1) : -1,
// bottom
(c.y < info.tilesY - 1) ? (tileIdx + info.tilesX) : -1,
// left
(c.x > 0) ? (tileIdx - 1) : -1,
];
}
const setTilesZIndex = (gameId, tileIdxs, zIndex) => {
for (let tilesIdx of tileIdxs) {
changeTile(gameId, tilesIdx, { z: zIndex });
}
};
const moveTileDiff = (gameId, tileIdx, diff) => {
const oldPos = getTilePos(gameId, tileIdx);
const pos = Geometry.pointAdd(oldPos, diff);
changeTile(gameId, tileIdx, { pos });
};
const moveTilesDiff = (gameId, tileIdxs, diff) => {
const tileDrawSize = getTileDrawSize(gameId);
const bounds = getBounds(gameId);
const cappedDiff = diff;
for (let tileIdx of tileIdxs) {
const t = getTile(gameId, tileIdx);
if (t.pos.x + diff.x < bounds.x) {
cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x);
}
else if (t.pos.x + tileDrawSize + diff.x > bounds.x + bounds.w) {
cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + tileDrawSize, cappedDiff.x);
}
if (t.pos.y + diff.y < bounds.y) {
cappedDiff.y = Math.max(bounds.y - t.pos.y, cappedDiff.y);
}
else if (t.pos.y + tileDrawSize + diff.y > bounds.y + bounds.h) {
cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + tileDrawSize, cappedDiff.y);
}
}
for (let tileIdx of tileIdxs) {
moveTileDiff(gameId, tileIdx, cappedDiff);
}
};
const finishTiles = (gameId, tileIdxs) => {
for (let tileIdx of tileIdxs) {
changeTile(gameId, tileIdx, { owner: -1, z: 1 });
}
};
const setTilesOwner = (gameId, tileIdxs, owner) => {
for (let tileIdx of tileIdxs) {
changeTile(gameId, tileIdx, { owner });
}
};
// get all grouped tiles for a tile
function getGroupedTileIdxs(gameId, tileIdx) {
const tiles = GAMES[gameId].puzzle.tiles;
const tile = Util.decodeTile(tiles[tileIdx]);
const grouped = [];
if (tile.group) {
for (let other of tiles) {
const otherTile = Util.decodeTile(other);
if (otherTile.group === tile.group) {
grouped.push(otherTile.idx);
}
}
}
else {
grouped.push(tile.idx);
}
return grouped;
}
// Returns the index of the puzzle tile with the highest z index
// that is not finished yet and that matches the position
const freeTileIdxByPos = (gameId, pos) => {
let info = GAMES[gameId].puzzle.info;
let tiles = GAMES[gameId].puzzle.tiles;
let maxZ = -1;
let tileIdx = -1;
for (let idx = 0; idx < tiles.length; idx++) {
const tile = Util.decodeTile(tiles[idx]);
if (tile.owner !== 0) {
continue;
}
const collisionRect = {
x: tile.pos.x,
y: tile.pos.y,
w: info.tileSize,
h: info.tileSize,
};
if (Geometry.pointInBounds(pos, collisionRect)) {
if (maxZ === -1 || tile.z > maxZ) {
maxZ = tile.z;
tileIdx = idx;
}
}
}
return tileIdx;
};
const getPlayerBgColor = (gameId, playerId) => {
const p = getPlayer(gameId, playerId);
return p ? p.bgcolor : null;
};
const getPlayerColor = (gameId, playerId) => {
const p = getPlayer(gameId, playerId);
return p ? p.color : null;
};
const getPlayerName = (gameId, playerId) => {
const p = getPlayer(gameId, playerId);
return p ? p.name : null;
};
const getPlayerPoints = (gameId, playerId) => {
const p = getPlayer(gameId, playerId);
2021-05-17 02:32:33 +02:00
return p ? p.points : 0;
2021-05-17 00:28:26 +02:00
};
// determine if two tiles are grouped together
const areGrouped = (gameId, tileIdx1, tileIdx2) => {
const g1 = getTileGroup(gameId, tileIdx1);
const g2 = getTileGroup(gameId, tileIdx2);
return g1 && g1 === g2;
};
const getTableWidth = (gameId) => {
return GAMES[gameId].puzzle.info.table.width;
};
const getTableHeight = (gameId) => {
return GAMES[gameId].puzzle.info.table.height;
};
const getPuzzle = (gameId) => {
return GAMES[gameId].puzzle;
};
const getRng = (gameId) => {
return GAMES[gameId].rng.obj;
};
const getPuzzleWidth = (gameId) => {
return GAMES[gameId].puzzle.info.width;
};
const getPuzzleHeight = (gameId) => {
return GAMES[gameId].puzzle.info.height;
};
function handleInput$1(gameId, playerId, input, ts) {
const puzzle = GAMES[gameId].puzzle;
const evtInfo = getEvtInfo(gameId, playerId);
const changes = [];
const _dataChange = () => {
changes.push([Protocol.CHANGE_DATA, puzzle.data]);
};
const _tileChange = (tileIdx) => {
changes.push([
Protocol.CHANGE_TILE,
Util.encodeTile(getTile(gameId, tileIdx)),
]);
};
const _tileChanges = (tileIdxs) => {
for (const tileIdx of tileIdxs) {
_tileChange(tileIdx);
}
};
const _playerChange = () => {
changes.push([
Protocol.CHANGE_PLAYER,
Util.encodePlayer(getPlayer(gameId, playerId)),
]);
};
// put both tiles (and their grouped tiles) in the same group
const groupTiles = (gameId, tileIdx1, tileIdx2) => {
const tiles = GAMES[gameId].puzzle.tiles;
const group1 = getTileGroup(gameId, tileIdx1);
const group2 = getTileGroup(gameId, tileIdx2);
let group;
const searchGroups = [];
if (group1) {
searchGroups.push(group1);
}
if (group2) {
searchGroups.push(group2);
}
if (group1) {
group = group1;
}
else if (group2) {
group = group2;
}
else {
const maxGroup = getMaxGroup(gameId) + 1;
changeData(gameId, { maxGroup });
_dataChange();
group = getMaxGroup(gameId);
}
changeTile(gameId, tileIdx1, { group });
_tileChange(tileIdx1);
changeTile(gameId, tileIdx2, { group });
_tileChange(tileIdx2);
// TODO: strange
if (searchGroups.length > 0) {
for (const t of tiles) {
const tile = Util.decodeTile(t);
if (searchGroups.includes(tile.group)) {
changeTile(gameId, tile.idx, { group });
_tileChange(tile.idx);
}
}
}
};
const type = input[0];
if (type === Protocol.INPUT_EV_BG_COLOR) {
const bgcolor = input[1];
changePlayer(gameId, playerId, { bgcolor, ts });
_playerChange();
}
else if (type === Protocol.INPUT_EV_PLAYER_COLOR) {
const color = input[1];
changePlayer(gameId, playerId, { color, ts });
_playerChange();
}
else if (type === Protocol.INPUT_EV_PLAYER_NAME) {
const name = `${input[1]}`.substr(0, 16);
changePlayer(gameId, playerId, { name, ts });
_playerChange();
}
else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
const x = input[1];
const y = input[2];
const pos = { x, y };
changePlayer(gameId, playerId, { d: 1, ts });
_playerChange();
evtInfo._last_mouse_down = pos;
const tileIdxAtPos = freeTileIdxByPos(gameId, pos);
if (tileIdxAtPos >= 0) {
let maxZ = getMaxZIndex(gameId) + 1;
changeData(gameId, { maxZ });
_dataChange();
const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos);
setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId));
setTilesOwner(gameId, tileIdxs, playerId);
_tileChanges(tileIdxs);
}
evtInfo._last_mouse = pos;
}
else if (type === Protocol.INPUT_EV_MOUSE_MOVE) {
const x = input[1];
const y = input[2];
const pos = { x, y };
if (evtInfo._last_mouse_down === null) {
// player is just moving the hand
changePlayer(gameId, playerId, { x, y, ts });
_playerChange();
}
else {
let tileIdx = getFirstOwnedTileIdx(gameId, playerId);
if (tileIdx >= 0) {
// player is moving a tile (and hand)
changePlayer(gameId, playerId, { x, y, ts });
_playerChange();
// check if pos is on the tile, otherwise dont move
// (mouse could be out of table, but tile stays on it)
const tileIdxs = getGroupedTileIdxs(gameId, tileIdx);
let anyOk = Geometry.pointInBounds(pos, getBounds(gameId))
&& Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId));
for (let idx of tileIdxs) {
const bounds = getTileBounds(gameId, idx);
if (Geometry.pointInBounds(pos, bounds)) {
anyOk = true;
break;
}
}
if (anyOk) {
const diffX = x - evtInfo._last_mouse_down.x;
const diffY = y - evtInfo._last_mouse_down.y;
const diff = { x: diffX, y: diffY };
moveTilesDiff(gameId, tileIdxs, diff);
_tileChanges(tileIdxs);
}
}
else {
// player is just moving map, so no change in position!
changePlayer(gameId, playerId, { ts });
_playerChange();
}
evtInfo._last_mouse_down = pos;
}
evtInfo._last_mouse = pos;
}
else if (type === Protocol.INPUT_EV_MOUSE_UP) {
const x = input[1];
const y = input[2];
const pos = { x, y };
const d = 0;
evtInfo._last_mouse_down = null;
let tileIdx = getFirstOwnedTileIdx(gameId, playerId);
if (tileIdx >= 0) {
// drop the tile(s)
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx);
setTilesOwner(gameId, tileIdxs, 0);
_tileChanges(tileIdxs);
// Check if the tile was dropped near the final location
let tilePos = getTilePos(gameId, tileIdx);
let finalPos = getFinalTilePos(gameId, tileIdx);
if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) {
let diff = Geometry.pointSub(finalPos, tilePos);
// Snap the tile to the final destination
moveTilesDiff(gameId, tileIdxs, diff);
finishTiles(gameId, tileIdxs);
_tileChanges(tileIdxs);
let points = getPlayerPoints(gameId, playerId);
2021-05-17 02:32:33 +02:00
if (getScoreMode(gameId) === ScoreMode.FINAL) {
2021-05-17 00:28:26 +02:00
points += tileIdxs.length;
}
2021-05-17 02:32:33 +02:00
else if (getScoreMode(gameId) === ScoreMode.ANY) {
2021-05-17 00:28:26 +02:00
points += 1;
}
else ;
changePlayer(gameId, playerId, { d, ts, points });
_playerChange();
// check if the puzzle is finished
if (getFinishedTileCount(gameId) === getTileCount(gameId)) {
changeData(gameId, { finished: ts });
_dataChange();
}
}
else {
// Snap to other tiles
const check = (gameId, tileIdx, otherTileIdx, off) => {
let info = GAMES[gameId].puzzle.info;
if (otherTileIdx < 0) {
return false;
}
if (areGrouped(gameId, tileIdx, otherTileIdx)) {
return false;
}
const tilePos = getTilePos(gameId, tileIdx);
const dstPos = Geometry.pointAdd(getTilePos(gameId, otherTileIdx), { x: off[0] * info.tileSize, y: off[1] * info.tileSize });
if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) {
let diff = Geometry.pointSub(dstPos, tilePos);
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx);
moveTilesDiff(gameId, tileIdxs, diff);
groupTiles(gameId, tileIdx, otherTileIdx);
tileIdxs = getGroupedTileIdxs(gameId, tileIdx);
const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs);
setTilesZIndex(gameId, tileIdxs, zIndex);
_tileChanges(tileIdxs);
return true;
}
return false;
};
let snapped = false;
for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) {
let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp);
if (check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top
|| check(gameId, tileIdxTmp, othersIdxs[1], [-1, 0]) // right
|| check(gameId, tileIdxTmp, othersIdxs[2], [0, -1]) // bottom
|| check(gameId, tileIdxTmp, othersIdxs[3], [1, 0]) // left
) {
snapped = true;
break;
}
}
2021-05-17 02:32:33 +02:00
if (snapped && getScoreMode(gameId) === ScoreMode.ANY) {
2021-05-17 00:28:26 +02:00
const points = getPlayerPoints(gameId, playerId) + 1;
changePlayer(gameId, playerId, { d, ts, points });
_playerChange();
}
else {
changePlayer(gameId, playerId, { d, ts });
_playerChange();
}
}
}
else {
changePlayer(gameId, playerId, { d, ts });
_playerChange();
}
evtInfo._last_mouse = pos;
}
else if (type === Protocol.INPUT_EV_ZOOM_IN) {
const x = input[1];
const y = input[2];
changePlayer(gameId, playerId, { x, y, ts });
_playerChange();
evtInfo._last_mouse = { x, y };
}
else if (type === Protocol.INPUT_EV_ZOOM_OUT) {
const x = input[1];
const y = input[2];
changePlayer(gameId, playerId, { x, y, ts });
_playerChange();
evtInfo._last_mouse = { x, y };
}
else {
changePlayer(gameId, playerId, { ts });
_playerChange();
}
setEvtInfo(gameId, playerId, evtInfo);
return changes;
}
var GameCommon = {
__createPlayerObject,
setGame,
exists: exists$1,
playerExists,
getActivePlayers,
getIdlePlayers,
addPlayer: addPlayer$1,
getFinishedTileCount,
getTileCount,
getImageUrl,
setImageUrl,
get: get$1,
getAllGames,
getPlayerBgColor,
getPlayerColor,
getPlayerName,
getPlayerIndexById,
getPlayerIdByIndex,
changePlayer,
setPlayer,
setTile,
setPuzzleData,
getTableWidth,
getTableHeight,
getPuzzle,
getRng,
getPuzzleWidth,
getPuzzleHeight,
getTilesSortedByZIndex,
getFirstOwnedTile,
getTileDrawOffset,
getTileDrawSize,
getFinalTilePos,
getStartTs,
getFinishTs,
handleInput: handleInput$1,
};
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const BASE_DIR = `${__dirname}/../..`;
const DATA_DIR = `${BASE_DIR}/data`;
const UPLOAD_DIR = `${BASE_DIR}/data/uploads`;
const UPLOAD_URL = `/uploads`;
const PUBLIC_DIR = `${BASE_DIR}/build/public/`;
const log$3 = logger('GameLog.js');
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`;
const create = (gameId) => {
const file = filename(gameId);
if (!fs.existsSync(file)) {
fs.appendFileSync(file, '');
}
};
const exists = (gameId) => {
const file = filename(gameId);
return fs.existsSync(file);
};
const _log = (gameId, ...args) => {
const file = filename(gameId);
if (!fs.existsSync(file)) {
return;
}
const str = JSON.stringify(args);
fs.appendFileSync(file, str + "\n");
};
const get = (gameId) => {
const file = filename(gameId);
if (!fs.existsSync(file)) {
return [];
}
const lines = fs.readFileSync(file, 'utf-8').split("\n");
return lines.filter((line) => !!line).map((line) => {
try {
return JSON.parse(line);
}
catch (e) {
log$3.log(line);
log$3.log(e);
}
});
};
var GameLog = {
create,
exists,
log: _log,
get,
};
const resizeImage = async (filename) => {
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
return;
}
const imagePath = `${UPLOAD_DIR}/${filename}`;
const imageOutPath = `${UPLOAD_DIR}/r/${filename}`;
const orientation = await getExifOrientation(imagePath);
let sharpImg = sharp(imagePath, { failOnError: false });
// when image is rotated to the left or right, switch width/height
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
if (orientation === 6) {
sharpImg = sharpImg.rotate();
}
else if (orientation === 3) {
sharpImg = sharpImg.rotate().rotate();
}
else if (orientation === 8) {
sharpImg = sharpImg.rotate().rotate().rotate();
}
const sizes = [
[150, 100],
[375, 210],
];
for (let [w, h] of sizes) {
console.log(w, h, imagePath);
await sharpImg.resize(w, h, { fit: 'contain' }).toFile(`${imageOutPath}-${w}x${h}.webp`);
}
};
async function getExifOrientation(imagePath) {
return new Promise((resolve, reject) => {
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
if (error) {
resolve(0);
}
else {
resolve(exifData.image.Orientation);
}
});
});
}
const allImages = () => {
const images = fs.readdirSync(UPLOAD_DIR)
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
.map(f => ({
filename: f,
file: `${UPLOAD_DIR}/${f}`,
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
}))
.sort((a, b) => {
return fs.statSync(b.file).mtime.getTime() -
fs.statSync(a.file).mtime.getTime();
});
return images;
};
async function getDimensions(imagePath) {
let dimensions = sizeOf(imagePath);
const orientation = await getExifOrientation(imagePath);
// when image is rotated to the left or right, switch width/height
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
if (orientation === 6 || orientation === 8) {
return {
width: dimensions.height,
height: dimensions.width,
};
}
return dimensions;
}
var Images = {
allImages,
resizeImage,
getDimensions,
};
// cut size of each puzzle tile in the
// final resized version of the puzzle image
const TILE_SIZE = 64;
async function createPuzzle(rng, targetTiles, image, ts) {
const imagePath = image.file;
const imageUrl = image.url;
// determine puzzle information from the image dimensions
const dim = await Images.getDimensions(imagePath);
if (!dim || !dim.width || !dim.height) {
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`;
}
const info = determinePuzzleInfo(dim.width, dim.height, targetTiles);
let tiles = new Array(info.tiles);
for (let i = 0; i < tiles.length; i++) {
tiles[i] = { idx: i };
}
const shapes = determinePuzzleTileShapes(rng, info);
let positions = new Array(info.tiles);
for (let tile of tiles) {
const coord = Util.coordByTileIdx(info, tile.idx);
positions[tile.idx] = {
// instead of info.tileSize, we use info.tileDrawSize
// to spread the tiles a bit
x: coord.x * info.tileSize * 1.5,
y: coord.y * info.tileSize * 1.5,
};
}
const tableWidth = info.width * 3;
const tableHeight = info.height * 3;
const off = info.tileSize * 1.5;
let last = {
x: info.width - (1 * off),
y: info.height - (2 * off),
};
let countX = Math.ceil(info.width / off) + 2;
let countY = Math.ceil(info.height / off) + 2;
let diffX = off;
let diffY = 0;
let index = 0;
for (let pos of positions) {
pos.x = last.x;
pos.y = last.y;
last.x += diffX;
last.y += diffY;
index++;
// did we move horizontally?
if (diffX !== 0) {
if (index === countX) {
diffY = diffX;
countY++;
diffX = 0;
index = 0;
}
}
else {
if (index === countY) {
diffX = -diffY;
countX++;
diffY = 0;
index = 0;
}
}
}
// then shuffle the positions
positions = rng.shuffle(positions);
2021-05-17 02:32:33 +02:00
const pieces = tiles.map(tile => {
2021-05-17 00:28:26 +02:00
return Util.encodeTile({
idx: tile.idx,
group: 0,
z: 0,
// who owns the tile
// 0 = free for taking
// -1 = finished
// other values: id of player who has the tile
owner: 0,
// physical current position of the tile (x/y in pixels)
// this position is the initial position only and is the
// value that changes when moving a tile
pos: positions[tile.idx],
});
});
// Complete puzzle object
return {
// tiles array
2021-05-17 02:32:33 +02:00
tiles: pieces,
2021-05-17 00:28:26 +02:00
// game data for puzzle, data changes during the game
data: {
// TODO: maybe calculate this each time?
maxZ: 0,
maxGroup: 0,
started: ts,
finished: 0, // finish timestamp
},
// static puzzle information. stays same for complete duration of
// the game
info: {
table: {
width: tableWidth,
height: tableHeight,
},
// information that was used to create the puzzle
targetTiles: targetTiles,
imageUrl,
width: info.width,
height: info.height,
tileSize: info.tileSize,
tileDrawSize: info.tileDrawSize,
tileMarginWidth: info.tileMarginWidth,
// offset in x and y when drawing tiles, so that they appear to be at pos
tileDrawOffset: (info.tileDrawSize - info.tileSize) / -2,
// max distance between tile and destination that
// makes the tile snap to destination
snapDistance: info.tileSize / 2,
tiles: info.tiles,
tilesX: info.tilesX,
tilesY: info.tilesY,
// ( index => {x, y} )
// this is not the physical coordinate, but
// the tile_coordinate
// this can be used to determine where the
// final destination of a tile is
shapes: shapes, // tile shapes
},
};
}
function determinePuzzleTileShapes(rng, info) {
const tabs = [-1, 1];
const shapes = new Array(info.tiles);
for (let i = 0; i < info.tiles; i++) {
let coord = Util.coordByTileIdx(info, i);
shapes[i] = {
top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
right: coord.x === info.tilesX - 1 ? 0 : rng.choice(tabs),
2021-05-17 00:28:26 +02:00
left: coord.x === 0 ? 0 : shapes[i - 1].right * -1,
bottom: coord.y === info.tilesY - 1 ? 0 : rng.choice(tabs),
2021-05-17 00:28:26 +02:00
};
}
return shapes.map(Util.encodeShape);
}
const determineTilesXY = (w, h, targetTiles) => {
const w_ = w < h ? (w * h) : (w * w);
const h_ = w < h ? (h * h) : (w * h);
let size = 0;
let tiles = 0;
do {
size++;
tiles = Math.floor(w_ / size) * Math.floor(h_ / size);
} while (tiles >= targetTiles);
size--;
return {
tilesX: Math.round(w_ / size),
tilesY: Math.round(h_ / size),
};
};
const determinePuzzleInfo = (w, h, targetTiles) => {
const { tilesX, tilesY } = determineTilesXY(w, h, targetTiles);
const tiles = tilesX * tilesY;
const tileSize = TILE_SIZE;
const width = tilesX * tileSize;
const height = tilesY * tileSize;
const tileMarginWidth = tileSize * .5;
const tileDrawSize = Math.round(tileSize + tileMarginWidth * 2);
return {
width,
height,
tileSize,
tileMarginWidth,
tileDrawSize,
tiles,
tilesX,
tilesY,
};
};
const log$2 = logger('GameStorage.js');
const DIRTY_GAMES = {};
function setDirty(gameId) {
DIRTY_GAMES[gameId] = true;
}
function setClean(gameId) {
delete DIRTY_GAMES[gameId];
}
function loadGames() {
const files = fs.readdirSync(DATA_DIR);
for (const f of files) {
const m = f.match(/^([a-z0-9]+)\.json$/);
if (!m) {
continue;
}
const gameId = m[1];
loadGame(gameId);
}
}
function loadGame(gameId) {
const file = `${DATA_DIR}/${gameId}.json`;
const contents = fs.readFileSync(file, 'utf-8');
let game;
try {
game = JSON.parse(contents);
}
catch {
log$2.log(`[ERR] unable to load game from file ${file}`);
}
if (typeof game.puzzle.data.started === 'undefined') {
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs);
}
if (typeof game.puzzle.data.finished === 'undefined') {
let unfinished = game.puzzle.tiles.map(Util.decodeTile).find((t) => t.owner !== -1);
game.puzzle.data.finished = unfinished ? 0 : Time.timestamp();
}
if (!Array.isArray(game.players)) {
game.players = Object.values(game.players);
}
const gameObject = {
id: game.id,
rng: {
type: game.rng ? game.rng.type : '_fake_',
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(0),
},
puzzle: game.puzzle,
players: game.players,
evtInfos: {},
2021-05-17 02:32:33 +02:00
scoreMode: game.scoreMode || ScoreMode.FINAL,
2021-05-17 00:28:26 +02:00
};
GameCommon.setGame(gameObject.id, gameObject);
}
function persistGames() {
for (const gameId of Object.keys(DIRTY_GAMES)) {
persistGame(gameId);
}
}
function persistGame(gameId) {
const game = GameCommon.get(gameId);
if (game.id in DIRTY_GAMES) {
setClean(game.id);
}
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({
id: game.id,
rng: {
type: game.rng.type,
obj: Rng.serialize(game.rng.obj),
},
puzzle: game.puzzle,
players: game.players,
scoreMode: game.scoreMode,
}));
log$2.info(`[INFO] persisted game ${game.id}`);
}
var GameStorage = {
loadGames,
loadGame,
persistGames,
persistGame,
setDirty,
};
async function createGameObject(gameId, targetTiles, image, ts, scoreMode) {
const seed = Util.hash(gameId + ' ' + ts);
const rng = new Rng(seed);
return {
id: gameId,
rng: { type: 'Rng', obj: rng },
puzzle: await createPuzzle(rng, targetTiles, image, ts),
players: [],
evtInfos: {},
scoreMode,
};
}
async function createGame(gameId, targetTiles, image, ts, scoreMode) {
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode);
GameLog.create(gameId);
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode);
GameCommon.setGame(gameObject.id, gameObject);
GameStorage.setDirty(gameId);
}
function addPlayer(gameId, playerId, ts) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId);
const diff = ts - GameCommon.getStartTs(gameId);
if (idx === -1) {
GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, diff);
}
else {
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, diff);
}
GameCommon.addPlayer(gameId, playerId, ts);
GameStorage.setDirty(gameId);
}
function handleInput(gameId, playerId, input, ts) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId);
const diff = ts - GameCommon.getStartTs(gameId);
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff);
const ret = GameCommon.handleInput(gameId, playerId, input, ts);
GameStorage.setDirty(gameId);
return ret;
}
var Game = {
createGameObject,
createGame,
addPlayer,
handleInput,
getAllGames: GameCommon.getAllGames,
getActivePlayers: GameCommon.getActivePlayers,
getFinishedTileCount: GameCommon.getFinishedTileCount,
getImageUrl: GameCommon.getImageUrl,
getTileCount: GameCommon.getTileCount,
exists: GameCommon.exists,
playerExists: GameCommon.playerExists,
get: GameCommon.get,
getStartTs: GameCommon.getStartTs,
getFinishTs: GameCommon.getFinishTs,
};
const log$1 = logger('GameSocket.js');
// Map<gameId, Socket[]>
const SOCKETS = {};
function socketExists(gameId, socket) {
if (!(gameId in SOCKETS)) {
return false;
}
return SOCKETS[gameId].includes(socket);
}
function removeSocket(gameId, socket) {
if (!(gameId in SOCKETS)) {
return;
}
SOCKETS[gameId] = SOCKETS[gameId].filter((s) => s !== socket);
log$1.log('removed socket: ', gameId, socket.protocol);
log$1.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
}
function addSocket(gameId, socket) {
if (!(gameId in SOCKETS)) {
SOCKETS[gameId] = [];
}
if (!SOCKETS[gameId].includes(socket)) {
SOCKETS[gameId].push(socket);
log$1.log('added socket: ', gameId, socket.protocol);
log$1.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
}
}
function getSockets(gameId) {
if (!(gameId in SOCKETS)) {
return [];
}
return SOCKETS[gameId];
}
var GameSockets = {
addSocket,
removeSocket,
socketExists,
getSockets,
};
let configFile = '';
let last = '';
for (const val of process.argv) {
if (last === '-c') {
configFile = val;
}
last = val;
}
if (configFile === '') {
process.exit(2);
}
const config = JSON.parse(String(fs.readFileSync(configFile)));
const log = logger('main.js');
const port = config.http.port;
const hostname = config.http.hostname;
const app = express();
const storage = multer.diskStorage({
destination: UPLOAD_DIR,
filename: function (req, file, cb) {
cb(null, `${Util.uniqId()}-${file.originalname}`);
2021-05-17 00:28:26 +02:00
}
});
const upload = multer({ storage }).single('file');
app.get('/api/conf', (req, res) => {
res.send({
WS_ADDRESS: config.ws.connectstring,
});
});
app.get('/api/newgame-data', (req, res) => {
res.send({
images: Images.allImages(),
});
});
app.get('/api/index-data', (req, res) => {
const ts = Time.timestamp();
const games = [
...Game.getAllGames().map((game) => ({
id: game.id,
hasReplay: GameLog.exists(game.id),
started: Game.getStartTs(game.id),
finished: Game.getFinishTs(game.id),
tilesFinished: Game.getFinishedTileCount(game.id),
tilesTotal: Game.getTileCount(game.id),
players: Game.getActivePlayers(game.id, ts).length,
imageUrl: Game.getImageUrl(game.id),
})),
];
res.send({
gamesRunning: games.filter(g => !g.finished),
gamesFinished: games.filter(g => !!g.finished),
});
});
app.post('/upload', (req, res) => {
upload(req, res, async (err) => {
if (err) {
log.log(err);
res.status(400).send("Something went wrong!");
}
try {
await Images.resizeImage(req.file.filename);
}
catch (err) {
log.log(err);
res.status(400).send("Something went wrong!");
}
res.send({
image: {
file: `${UPLOAD_DIR}/${req.file.filename}`,
url: `${UPLOAD_URL}/${req.file.filename}`,
},
});
});
});
app.post('/newgame', bodyParser.json(), async (req, res) => {
log.log(req.body.tiles, req.body.image);
const gameId = Util.uniqId();
if (!Game.exists(gameId)) {
const ts = Time.timestamp();
await Game.createGame(gameId, req.body.tiles, req.body.image, ts, req.body.scoreMode);
}
res.send({ id: gameId });
});
app.use('/uploads/', express.static(UPLOAD_DIR));
app.use('/', express.static(PUBLIC_DIR));
const wss = new WebSocketServer(config.ws);
const notify = (data, sockets) => {
// TODO: throttle?
for (let socket of sockets) {
wss.notifyOne(data, socket);
}
};
wss.on('close', async ({ socket }) => {
try {
const proto = socket.protocol.split('|');
const clientId = proto[0];
const gameId = proto[1];
GameSockets.removeSocket(gameId, socket);
}
catch (e) {
log.error(e);
}
});
wss.on('message', async ({ socket, data }) => {
try {
const proto = socket.protocol.split('|');
const clientId = proto[0];
const gameId = proto[1];
const msg = JSON.parse(data);
const msgType = msg[0];
switch (msgType) {
case Protocol.EV_CLIENT_INIT_REPLAY:
{
if (!GameLog.exists(gameId)) {
throw `[gamelog ${gameId} does not exist... ]`;
}
const log = GameLog.get(gameId);
2021-05-17 02:32:33 +02:00
const game = await Game.createGameObject(gameId, log[0][2], log[0][3], log[0][4], log[0][5] || ScoreMode.FINAL);
2021-05-17 00:28:26 +02:00
notify([Protocol.EV_SERVER_INIT_REPLAY, Util.encodeGame(game), log], [socket]);
}
break;
case Protocol.EV_CLIENT_INIT:
{
if (!Game.exists(gameId)) {
throw `[game ${gameId} does not exist... ]`;
}
const ts = Time.timestamp();
Game.addPlayer(gameId, clientId, ts);
GameSockets.addSocket(gameId, socket);
const game = Game.get(gameId);
notify([Protocol.EV_SERVER_INIT, Util.encodeGame(game)], [socket]);
}
break;
case Protocol.EV_CLIENT_EVENT:
{
if (!Game.exists(gameId)) {
throw `[game ${gameId} does not exist... ]`;
}
const clientSeq = msg[1];
const clientEvtData = msg[2];
const ts = Time.timestamp();
let sendGame = false;
if (!Game.playerExists(gameId, clientId)) {
Game.addPlayer(gameId, clientId, ts);
sendGame = true;
}
if (!GameSockets.socketExists(gameId, socket)) {
GameSockets.addSocket(gameId, socket);
sendGame = true;
}
if (sendGame) {
const game = Game.get(gameId);
notify([Protocol.EV_SERVER_INIT, Util.encodeGame(game)], [socket]);
}
const changes = Game.handleInput(gameId, clientId, clientEvtData, ts);
notify([Protocol.EV_SERVER_EVENT, clientId, clientSeq, changes], GameSockets.getSockets(gameId));
}
break;
}
}
catch (e) {
log.error(e);
}
});
GameStorage.loadGames();
const server = app.listen(port, hostname, () => log.log(`server running on http://${hostname}:${port}`));
wss.listen();
const memoryUsageHuman = () => {
const totalHeapSize = v8.getHeapStatistics().total_available_size;
let totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2);
log.log(`Total heap size (bytes) ${totalHeapSize}, (GB ~${totalHeapSizeInGB})`);
const used = process.memoryUsage().heapUsed / 1024 / 1024;
log.log(`Mem: ${Math.round(used * 100) / 100}M`);
};
memoryUsageHuman();
// persist games in fixed interval
const persistInterval = setInterval(() => {
log.log('Persisting games...');
GameStorage.persistGames();
memoryUsageHuman();
}, config.persistence.interval);
const gracefulShutdown = (signal) => {
log.log(`${signal} received...`);
log.log('clearing persist interval...');
clearInterval(persistInterval);
log.log('persisting games...');
GameStorage.persistGames();
log.log('shutting down webserver...');
server.close();
log.log('shutting down websocketserver...');
wss.close();
log.log('shutting down...');
process.exit();
};
// used by nodemon
process.once('SIGUSR2', function () {
gracefulShutdown('SIGUSR2');
});
process.once('SIGINT', function (code) {
gracefulShutdown('SIGINT');
});
process.once('SIGTERM', function (code) {
gracefulShutdown('SIGTERM');
});