puzzle/server/index.js
2021-05-01 00:16:08 +02:00

284 lines
7.1 KiB
JavaScript

import WebSocketServer from './WebSocketServer.js'
import express from 'express'
import multer from 'multer'
import config from './../config.js'
import Protocol from './../common/Protocol.js'
import Util, { logger } from './../common/Util.js'
import Game from './Game.js'
import twing from 'twing'
import bodyParser from 'body-parser'
import v8 from 'v8'
import GameLog from './GameLog.js'
import GameSockets from './GameSockets.js'
import Time from '../common/Time.js'
import Images from './Images.js'
import {
UPLOAD_DIR,
UPLOAD_URL,
COMMON_DIR,
PUBLIC_DIR,
TEMPLATE_DIR,
} from './Dirs.js'
import GameCommon from '../common/GameCommon.js'
import GameStorage from './GameStorage.js'
const log = logger('index.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 , file.originalname);
}
})
const upload = multer({storage}).single('file');
const statics = express.static(PUBLIC_DIR)
const render = async (template, data) => {
const loader = new twing.TwingLoaderFilesystem(TEMPLATE_DIR)
const env = new twing.TwingEnvironment(loader)
return env.render(template, data)
}
app.use('/g/:gid', async (req, res, next) => {
res.send(await render('game.html.twig', {
GAME_ID: req.params.gid,
WS_ADDRESS: config.ws.connectstring,
}))
})
app.use('/replay/:gid', async (req, res, next) => {
res.send(await render('replay.html.twig', {
GAME_ID: req.params.gid,
WS_ADDRESS: config.ws.connectstring,
}))
})
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({ url: `/g/${gameId}` })
})
app.use('/common/', express.static(COMMON_DIR))
app.use('/uploads/', express.static(UPLOAD_DIR))
app.use('/', async (req, res, next) => {
if (req.path === '/') {
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(await render('index.html.twig', {
gamesRunning: games.filter(g => !g.finished),
gamesFinished: games.filter(g => !!g.finished),
images: Images.allImages(),
}))
} else {
statics(req, res, next)
}
})
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)
const game = await Game.createGameObject(
gameId,
log[0][2],
log[0][3],
log[0][4],
log[0][5] || GameCommon.SCORE_MODE_FINAL
)
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')
})