store games in db

This commit is contained in:
Zutatensuppe 2021-07-12 01:28:14 +02:00
parent 126384e5bd
commit 4e528cc83d
14 changed files with 371 additions and 133 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>🧩 jigsaw.hyottoko.club</title> <title>🧩 jigsaw.hyottoko.club</title>
<script type="module" crossorigin src="/assets/index.97691b3e.js"></script> <script type="module" crossorigin src="/assets/index.63ff8630.js"></script>
<link rel="modulepreload" href="/assets/vendor.684f7bc8.js"> <link rel="modulepreload" href="/assets/vendor.684f7bc8.js">
<link rel="stylesheet" href="/assets/index.22dc307c.css"> <link rel="stylesheet" href="/assets/index.22dc307c.css">
</head> </head>

View file

@ -155,6 +155,7 @@ function encodeGame(data) {
data.scoreMode, data.scoreMode,
data.shapeMode, data.shapeMode,
data.snapMode, data.snapMode,
data.creatorUserId,
]; ];
} }
function decodeGame(data) { function decodeGame(data) {
@ -170,6 +171,7 @@ function decodeGame(data) {
scoreMode: data[6], scoreMode: data[6],
shapeMode: data[7], shapeMode: data[7],
snapMode: data[8], snapMode: data[8],
creatorUserId: data[9],
}; };
} }
function coordByPieceIdx(info, pieceIdx) { function coordByPieceIdx(info, pieceIdx) {
@ -1358,6 +1360,7 @@ const get = (gameId, offset = 0) => {
log[0][5] = DefaultScoreMode(log[0][5]); log[0][5] = DefaultScoreMode(log[0][5]);
log[0][6] = DefaultShapeMode(log[0][6]); log[0][6] = DefaultShapeMode(log[0][6]);
log[0][7] = DefaultSnapMode(log[0][7]); log[0][7] = DefaultSnapMode(log[0][7]);
log[0][8] = log[0][8] || null;
} }
return log; return log;
}; };
@ -1753,7 +1756,63 @@ function setDirty(gameId) {
function setClean(gameId) { function setClean(gameId) {
delete dirtyGames[gameId]; delete dirtyGames[gameId];
} }
function loadGames() { function loadGamesFromDb(db) {
const gameRows = db.getMany('games');
for (const gameRow of gameRows) {
loadGameFromDb(db, gameRow.id);
}
}
function loadGameFromDb(db, gameId) {
const gameRow = db.get('games', { id: gameId });
let game;
try {
game = JSON.parse(gameRow.data);
}
catch {
log$3.log(`[ERR] unable to load game from db ${gameId}`);
}
if (typeof game.puzzle.data.started === 'undefined') {
game.puzzle.data.started = gameRow.created;
}
if (typeof game.puzzle.data.finished === 'undefined') {
game.puzzle.data.finished = gameRow.finished;
}
if (!Array.isArray(game.players)) {
game.players = Object.values(game.players);
}
const gameObject = storeDataToGame(game, game.creator_user_id);
GameCommon.setGame(gameObject.id, gameObject);
}
function persistGamesToDb(db) {
for (const gameId of Object.keys(dirtyGames)) {
persistGameToDb(db, gameId);
}
}
function persistGameToDb(db, gameId) {
const game = GameCommon.get(gameId);
if (!game) {
log$3.error(`[ERROR] unable to persist non existing game ${gameId}`);
return;
}
if (game.id in dirtyGames) {
setClean(game.id);
}
db.upsert('games', {
id: game.id,
creator_user_id: game.creatorUserId,
image_id: game.puzzle.info.image?.id,
created: game.puzzle.data.started,
finished: game.puzzle.data.finished,
data: gameToStoreData(game)
}, {
id: game.id,
});
log$3.info(`[INFO] persisted game ${game.id}`);
}
/**
* @deprecated
*/
function loadGamesFromDisk() {
const files = fs.readdirSync(DATA_DIR); const files = fs.readdirSync(DATA_DIR);
for (const f of files) { for (const f of files) {
const m = f.match(/^([a-z0-9]+)\.json$/); const m = f.match(/^([a-z0-9]+)\.json$/);
@ -1761,10 +1820,13 @@ function loadGames() {
continue; continue;
} }
const gameId = m[1]; const gameId = m[1];
loadGame(gameId); loadGameFromDisk(gameId);
} }
} }
function loadGame(gameId) { /**
* @deprecated
*/
function loadGameFromDisk(gameId) {
const file = `${DATA_DIR}/${gameId}.json`; const file = `${DATA_DIR}/${gameId}.json`;
const contents = fs.readFileSync(file, 'utf-8'); const contents = fs.readFileSync(file, 'utf-8');
let game; let game;
@ -1786,27 +1848,21 @@ function loadGame(gameId) {
if (!Array.isArray(game.players)) { if (!Array.isArray(game.players)) {
game.players = Object.values(game.players); game.players = Object.values(game.players);
} }
const gameObject = { const gameObject = storeDataToGame(game, null);
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: {},
scoreMode: DefaultScoreMode(game.scoreMode),
shapeMode: DefaultShapeMode(game.shapeMode),
snapMode: DefaultSnapMode(game.snapMode),
};
GameCommon.setGame(gameObject.id, gameObject); GameCommon.setGame(gameObject.id, gameObject);
} }
function persistGames() { /**
* @deprecated
*/
function persistGamesToDisk() {
for (const gameId of Object.keys(dirtyGames)) { for (const gameId of Object.keys(dirtyGames)) {
persistGame(gameId); persistGameToDisk(gameId);
} }
} }
function persistGame(gameId) { /**
* @deprecated
*/
function persistGameToDisk(gameId) {
const game = GameCommon.get(gameId); const game = GameCommon.get(gameId);
if (!game) { if (!game) {
log$3.error(`[ERROR] unable to persist non existing game ${gameId}`); log$3.error(`[ERROR] unable to persist non existing game ${gameId}`);
@ -1815,7 +1871,27 @@ function persistGame(gameId) {
if (game.id in dirtyGames) { if (game.id in dirtyGames) {
setClean(game.id); setClean(game.id);
} }
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({ fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, gameToStoreData(game));
log$3.info(`[INFO] persisted game ${game.id}`);
}
function storeDataToGame(storeData, creatorUserId) {
return {
id: storeData.id,
creatorUserId,
rng: {
type: storeData.rng ? storeData.rng.type : '_fake_',
obj: storeData.rng ? Rng.unserialize(storeData.rng.obj) : new Rng(0),
},
puzzle: storeData.puzzle,
players: storeData.players,
evtInfos: {},
scoreMode: DefaultScoreMode(storeData.scoreMode),
shapeMode: DefaultShapeMode(storeData.shapeMode),
snapMode: DefaultSnapMode(storeData.snapMode),
};
}
function gameToStoreData(game) {
return JSON.stringify({
id: game.id, id: game.id,
rng: { rng: {
type: game.rng.type, type: game.rng.type,
@ -1826,22 +1902,27 @@ function persistGame(gameId) {
scoreMode: game.scoreMode, scoreMode: game.scoreMode,
shapeMode: game.shapeMode, shapeMode: game.shapeMode,
snapMode: game.snapMode, snapMode: game.snapMode,
})); });
log$3.info(`[INFO] persisted game ${game.id}`);
} }
var GameStorage = { var GameStorage = {
loadGames, // disk functions are deprecated
loadGame, loadGamesFromDisk,
persistGames, loadGameFromDisk,
persistGame, persistGamesToDisk,
persistGameToDisk,
loadGamesFromDb,
loadGameFromDb,
persistGamesToDb,
persistGameToDb,
setDirty, setDirty,
}; };
async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode) { async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId) {
const seed = Util.hash(gameId + ' ' + ts); const seed = Util.hash(gameId + ' ' + ts);
const rng = new Rng(seed); const rng = new Rng(seed);
return { return {
id: gameId, id: gameId,
creatorUserId,
rng: { type: 'Rng', obj: rng }, rng: { type: 'Rng', obj: rng },
puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode), puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode),
players: [], players: [],
@ -1851,10 +1932,10 @@ async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shape
snapMode, snapMode,
}; };
} }
async function createGame(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode) { async function createGame(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId) {
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode); const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode, creatorUserId);
GameLog.create(gameId, ts); GameLog.create(gameId, ts);
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode, shapeMode, snapMode); GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode, shapeMode, snapMode, gameObject.creatorUserId);
GameCommon.setGame(gameObject.id, gameObject); GameCommon.setGame(gameObject.id, gameObject);
GameStorage.setDirty(gameId); GameStorage.setDirty(gameId);
} }
@ -2101,10 +2182,7 @@ const storage = multer.diskStorage({
}); });
const upload = multer({ storage }).single('file'); const upload = multer({ storage }).single('file');
app.get('/api/me', (req, res) => { app.get('/api/me', (req, res) => {
let user = db.get('users', { let user = getUser(db, req);
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
});
res.send({ res.send({
id: user ? user.id : null, id: user ? user.id : null,
created: user ? user.created : null, created: user ? user.created : null,
@ -2137,7 +2215,7 @@ app.get('/api/replay-data', async (req, res) => {
if (offset === 0) { if (offset === 0) {
// also need the game // also need the game
game = await Game.createGameObject(gameId, log[0][2], log[0][3], // must be ImageInfo game = await Game.createGameObject(gameId, log[0][2], log[0][3], // must be ImageInfo
log[0][4], log[0][5], log[0][6], log[0][7]); log[0][4], log[0][5], log[0][6], log[0][7], log[0][8]);
} }
res.send({ log, game: game ? Util.encodeGame(game) : null }); res.send({ log, game: game ? Util.encodeGame(game) : null });
}); });
@ -2168,6 +2246,28 @@ app.get('/api/index-data', (req, res) => {
gamesFinished: games.filter(g => !!g.finished), gamesFinished: games.filter(g => !!g.finished),
}); });
}); });
const getOrCreateUser = (db, req) => {
let user = getUser(db, req);
if (!user) {
db.insert('users', {
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
'created': Time.timestamp(),
});
user = getUser(db, req);
}
return user;
};
const getUser = (db, req) => {
let user = db.get('users', {
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
});
if (user) {
user.id = parseInt(user.id, 10);
}
return user;
};
const setImageTags = (db, imageId, tags) => { const setImageTags = (db, imageId, tags) => {
tags.forEach((tag) => { tags.forEach((tag) => {
const slug = Util.slug(tag); const slug = Util.slug(tag);
@ -2181,21 +2281,14 @@ const setImageTags = (db, imageId, tags) => {
}); });
}; };
app.post('/api/save-image', express.json(), (req, res) => { app.post('/api/save-image', express.json(), (req, res) => {
let user = db.get('users', { let user = getUser(db, req);
'client_id': req.headers['client-id'], if (!user || !user.id) {
'client_secret': req.headers['client-secret'],
});
let userId = null;
if (user) {
userId = parseInt(user.id, 10);
}
else {
res.status(403).send({ ok: false, error: 'forbidden' }); res.status(403).send({ ok: false, error: 'forbidden' });
return; return;
} }
const data = req.body; const data = req.body;
let image = db.get('images', { id: data.id }); let image = db.get('images', { id: data.id });
if (parseInt(image.uploader_user_id, 10) !== userId) { if (parseInt(image.uploader_user_id, 10) !== user.id) {
res.status(403).send({ ok: false, error: 'forbidden' }); res.status(403).send({ ok: false, error: 'forbidden' });
return; return;
} }
@ -2223,24 +2316,10 @@ app.post('/api/upload', (req, res) => {
log.log(err); log.log(err);
res.status(400).send("Something went wrong!"); res.status(400).send("Something went wrong!");
} }
let user = db.get('users', { const user = getOrCreateUser(db, req);
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
});
let userId = null;
if (user) {
userId = user.id;
}
else {
userId = db.insert('users', {
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
'created': Time.timestamp(),
});
}
const dim = await Images.getDimensions(`${UPLOAD_DIR}/${req.file.filename}`); const dim = await Images.getDimensions(`${UPLOAD_DIR}/${req.file.filename}`);
const imageId = db.insert('images', { const imageId = db.insert('images', {
uploader_user_id: userId, uploader_user_id: user.id,
filename: req.file.filename, filename: req.file.filename,
filename_original: req.file.originalname, filename_original: req.file.originalname,
title: req.body.title || '', title: req.body.title || '',
@ -2256,12 +2335,17 @@ app.post('/api/upload', (req, res) => {
}); });
}); });
app.post('/api/newgame', express.json(), async (req, res) => { app.post('/api/newgame', express.json(), async (req, res) => {
let user = getOrCreateUser(db, req);
if (!user || !user.id) {
res.status(403).send({ ok: false, error: 'forbidden' });
return;
}
const gameSettings = req.body; const gameSettings = req.body;
log.log(gameSettings); log.log(gameSettings);
const gameId = Util.uniqId(); const gameId = Util.uniqId();
if (!GameCommon.exists(gameId)) { if (!GameCommon.exists(gameId)) {
const ts = Time.timestamp(); const ts = Time.timestamp();
await Game.createGame(gameId, gameSettings.tiles, gameSettings.image, ts, gameSettings.scoreMode, gameSettings.shapeMode, gameSettings.snapMode); await Game.createGame(gameId, gameSettings.tiles, gameSettings.image, ts, gameSettings.scoreMode, gameSettings.shapeMode, gameSettings.snapMode, user.id);
} }
res.send({ id: gameId }); res.send({ id: gameId });
}); });
@ -2343,7 +2427,7 @@ wss.on('message', async ({ socket, data }) => {
log.error(e); log.error(e);
} }
}); });
GameStorage.loadGames(); GameStorage.loadGamesFromDb(db);
const server = app.listen(port, hostname, () => log.log(`server running on http://${hostname}:${port}`)); const server = app.listen(port, hostname, () => log.log(`server running on http://${hostname}:${port}`));
wss.listen(); wss.listen();
const memoryUsageHuman = () => { const memoryUsageHuman = () => {
@ -2357,7 +2441,7 @@ memoryUsageHuman();
// persist games in fixed interval // persist games in fixed interval
const persistInterval = setInterval(() => { const persistInterval = setInterval(() => {
log.log('Persisting games...'); log.log('Persisting games...');
GameStorage.persistGames(); GameStorage.persistGamesToDb(db);
memoryUsageHuman(); memoryUsageHuman();
}, config.persistence.interval); }, config.persistence.interval);
const gracefulShutdown = (signal) => { const gracefulShutdown = (signal) => {
@ -2365,7 +2449,7 @@ const gracefulShutdown = (signal) => {
log.log('clearing persist interval...'); log.log('clearing persist interval...');
clearInterval(persistInterval); clearInterval(persistInterval);
log.log('persisting games...'); log.log('persisting games...');
GameStorage.persistGames(); GameStorage.persistGamesToDb(db);
log.log('shutting down webserver...'); log.log('shutting down webserver...');
server.close(); server.close();
log.log('shutting down websocketserver...'); log.log('shutting down websocketserver...');

View file

@ -47,7 +47,7 @@ function fixOne(gameId: string) {
log.log(g.puzzle.info.image.title, imageRow.id) log.log(g.puzzle.info.image.title, imageRow.id)
GameStorage.persistGame(gameId) GameStorage.persistGameToDb(db, gameId)
} else if (g.puzzle.info.image?.id) { } else if (g.puzzle.info.image?.id) {
const imageId = g.puzzle.info.image.id const imageId = g.puzzle.info.image.id
@ -55,7 +55,7 @@ function fixOne(gameId: string) {
log.log(g.puzzle.info.image.title, imageId) log.log(g.puzzle.info.image.title, imageId)
GameStorage.persistGame(gameId) GameStorage.persistGameToDb(db, gameId)
} }
// fix log // fix log
@ -81,7 +81,7 @@ function fixOne(gameId: string) {
} }
function fix() { function fix() {
GameStorage.loadGames() GameStorage.loadGamesFromDisk()
GameCommon.getAllGames().forEach((game: Game) => { GameCommon.getAllGames().forEach((game: Game) => {
fixOne(game.id) fixOne(game.id)
}) })

View file

@ -1,11 +1,16 @@
import GameCommon from '../src/common/GameCommon' import GameCommon from '../src/common/GameCommon'
import { logger } from '../src/common/Util' import { logger } from '../src/common/Util'
import Db from '../src/server/Db'
import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs'
import GameStorage from '../src/server/GameStorage' import GameStorage from '../src/server/GameStorage'
const log = logger('fix_tiles.js') const log = logger('fix_tiles.js')
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch(true)
function fix_tiles(gameId) { function fix_tiles(gameId) {
GameStorage.loadGame(gameId) GameStorage.loadGameFromDb(db, gameId)
let changed = false let changed = false
const tiles = GameCommon.getPiecesSortedByZIndex(gameId) const tiles = GameCommon.getPiecesSortedByZIndex(gameId)
for (let tile of tiles) { for (let tile of tiles) {
@ -27,7 +32,7 @@ function fix_tiles(gameId) {
} }
} }
if (changed) { if (changed) {
GameStorage.persistGame(gameId) GameStorage.persistGameToDb(db, gameId)
} }
} }

27
scripts/import_games.ts Normal file
View file

@ -0,0 +1,27 @@
import GameCommon from '../src/common/GameCommon'
import { Game } from '../src/common/Types'
import { logger } from '../src/common/Util'
import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs'
import Db from '../src/server/Db'
import GameStorage from '../src/server/GameStorage'
const log = logger('import_games.ts')
console.log(DB_FILE)
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch(true)
function run() {
GameStorage.loadGamesFromDisk()
GameCommon.getAllGames().forEach((game: Game) => {
if (!game.puzzle.info.image?.id) {
log.error(game.id + " has no image")
log.error(game.puzzle.info.image)
return
}
GameStorage.persistGameToDb(db, game.id)
})
}
run()

View file

@ -51,6 +51,7 @@ export type EncodedGame = FixedLengthArray<[
ScoreMode, ScoreMode,
ShapeMode, ShapeMode,
SnapMode, SnapMode,
number|null,
]> ]>
export interface ReplayData { export interface ReplayData {
@ -72,6 +73,7 @@ interface GameRng {
export interface Game { export interface Game {
id: string id: string
creatorUserId: number|null
players: Array<EncodedPlayer> players: Array<EncodedPlayer>
puzzle: Puzzle puzzle: Puzzle
evtInfos: Record<string, EvtInfo> evtInfos: Record<string, EvtInfo>

View file

@ -133,6 +133,7 @@ function encodeGame(data: Game): EncodedGame {
data.scoreMode, data.scoreMode,
data.shapeMode, data.shapeMode,
data.snapMode, data.snapMode,
data.creatorUserId,
] ]
} }
@ -149,6 +150,7 @@ function decodeGame(data: EncodedGame): Game {
scoreMode: data[6], scoreMode: data[6],
shapeMode: data[7], shapeMode: data[7],
snapMode: data[8], snapMode: data[8],
creatorUserId: data[9],
} }
} }

View file

@ -0,0 +1,11 @@
CREATE TABLE games (
id TEXT PRIMARY KEY,
creator_user_id INTEGER,
image_id INTEGER NOT NULL,
created TIMESTAMP NOT NULL,
finished TIMESTAMP NOT NULL,
data TEXT NOT NULL
);

View file

@ -16,12 +16,14 @@ async function createGameObject(
ts: Timestamp, ts: Timestamp,
scoreMode: ScoreMode, scoreMode: ScoreMode,
shapeMode: ShapeMode, shapeMode: ShapeMode,
snapMode: SnapMode snapMode: SnapMode,
creatorUserId: number|null
): Promise<Game> { ): Promise<Game> {
const seed = Util.hash(gameId + ' ' + ts) const seed = Util.hash(gameId + ' ' + ts)
const rng = new Rng(seed) const rng = new Rng(seed)
return { return {
id: gameId, id: gameId,
creatorUserId,
rng: { type: 'Rng', obj: rng }, rng: { type: 'Rng', obj: rng },
puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode), puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode),
players: [], players: [],
@ -39,7 +41,8 @@ async function createGame(
ts: Timestamp, ts: Timestamp,
scoreMode: ScoreMode, scoreMode: ScoreMode,
shapeMode: ShapeMode, shapeMode: ShapeMode,
snapMode: SnapMode snapMode: SnapMode,
creatorUserId: number
): Promise<void> { ): Promise<void> {
const gameObject = await createGameObject( const gameObject = await createGameObject(
gameId, gameId,
@ -48,7 +51,8 @@ async function createGame(
ts, ts,
scoreMode, scoreMode,
shapeMode, shapeMode,
snapMode snapMode,
creatorUserId
) )
GameLog.create(gameId, ts) GameLog.create(gameId, ts)
@ -61,7 +65,8 @@ async function createGame(
ts, ts,
scoreMode, scoreMode,
shapeMode, shapeMode,
snapMode snapMode,
gameObject.creatorUserId
) )
GameCommon.setGame(gameObject.id, gameObject) GameCommon.setGame(gameObject.id, gameObject)

View file

@ -90,6 +90,7 @@ const get = (
log[0][5] = DefaultScoreMode(log[0][5]) log[0][5] = DefaultScoreMode(log[0][5])
log[0][6] = DefaultShapeMode(log[0][6]) log[0][6] = DefaultShapeMode(log[0][6])
log[0][7] = DefaultSnapMode(log[0][7]) log[0][7] = DefaultSnapMode(log[0][7])
log[0][8] = log[0][8] || null
} }
return log return log
} }

View file

@ -5,6 +5,7 @@ import Util, { logger } from './../common/Util'
import { Rng } from './../common/Rng' import { Rng } from './../common/Rng'
import { DATA_DIR } from './Dirs' import { DATA_DIR } from './Dirs'
import Time from './../common/Time' import Time from './../common/Time'
import Db from './Db'
const log = logger('GameStorage.js') const log = logger('GameStorage.js')
@ -15,8 +16,73 @@ function setDirty(gameId: string): void {
function setClean(gameId: string): void { function setClean(gameId: string): void {
delete dirtyGames[gameId] delete dirtyGames[gameId]
} }
function loadGamesFromDb(db: Db): void {
const gameRows = db.getMany('games')
for (const gameRow of gameRows) {
loadGameFromDb(db, gameRow.id)
}
}
function loadGames(): void { function loadGameFromDb(db: Db, gameId: string): void {
const gameRow = db.get('games', {id: gameId})
let game
try {
game = JSON.parse(gameRow.data)
} catch {
log.log(`[ERR] unable to load game from db ${gameId}`);
}
if (typeof game.puzzle.data.started === 'undefined') {
game.puzzle.data.started = gameRow.created
}
if (typeof game.puzzle.data.finished === 'undefined') {
game.puzzle.data.finished = gameRow.finished
}
if (!Array.isArray(game.players)) {
game.players = Object.values(game.players)
}
const gameObject: Game = storeDataToGame(game, game.creator_user_id)
GameCommon.setGame(gameObject.id, gameObject)
}
function persistGamesToDb(db: Db): void {
for (const gameId of Object.keys(dirtyGames)) {
persistGameToDb(db, gameId)
}
}
function persistGameToDb(db: Db, gameId: string): void {
const game: Game|null = GameCommon.get(gameId)
if (!game) {
log.error(`[ERROR] unable to persist non existing game ${gameId}`)
return
}
if (game.id in dirtyGames) {
setClean(game.id)
}
db.upsert('games', {
id: game.id,
creator_user_id: game.creatorUserId,
image_id: game.puzzle.info.image?.id,
created: game.puzzle.data.started,
finished: game.puzzle.data.finished,
data: gameToStoreData(game)
}, {
id: game.id,
})
log.info(`[INFO] persisted game ${game.id}`)
}
/**
* @deprecated
*/
function loadGamesFromDisk(): void {
const files = fs.readdirSync(DATA_DIR) const files = fs.readdirSync(DATA_DIR)
for (const f of files) { for (const f of files) {
const m = f.match(/^([a-z0-9]+)\.json$/) const m = f.match(/^([a-z0-9]+)\.json$/)
@ -24,11 +90,14 @@ function loadGames(): void {
continue continue
} }
const gameId = m[1] const gameId = m[1]
loadGame(gameId) loadGameFromDisk(gameId)
} }
} }
function loadGame(gameId: string): void { /**
* @deprecated
*/
function loadGameFromDisk(gameId: string): void {
const file = `${DATA_DIR}/${gameId}.json` const file = `${DATA_DIR}/${gameId}.json`
const contents = fs.readFileSync(file, 'utf-8') const contents = fs.readFileSync(file, 'utf-8')
let game let game
@ -49,29 +118,23 @@ function loadGame(gameId: string): void {
if (!Array.isArray(game.players)) { if (!Array.isArray(game.players)) {
game.players = Object.values(game.players) game.players = Object.values(game.players)
} }
const gameObject: Game = { const gameObject: Game = storeDataToGame(game, null)
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: {},
scoreMode: DefaultScoreMode(game.scoreMode),
shapeMode: DefaultShapeMode(game.shapeMode),
snapMode: DefaultSnapMode(game.snapMode),
}
GameCommon.setGame(gameObject.id, gameObject) GameCommon.setGame(gameObject.id, gameObject)
} }
function persistGames(): void { /**
* @deprecated
*/
function persistGamesToDisk(): void {
for (const gameId of Object.keys(dirtyGames)) { for (const gameId of Object.keys(dirtyGames)) {
persistGame(gameId) persistGameToDisk(gameId)
} }
} }
function persistGame(gameId: string): void { /**
* @deprecated
*/
function persistGameToDisk(gameId: string): void {
const game = GameCommon.get(gameId) const game = GameCommon.get(gameId)
if (!game) { if (!game) {
log.error(`[ERROR] unable to persist non existing game ${gameId}`) log.error(`[ERROR] unable to persist non existing game ${gameId}`)
@ -81,7 +144,29 @@ function persistGame(gameId: string): void {
if (game.id in dirtyGames) { if (game.id in dirtyGames) {
setClean(game.id) setClean(game.id)
} }
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({ fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, gameToStoreData(game))
log.info(`[INFO] persisted game ${game.id}`)
}
function storeDataToGame(storeData: any, creatorUserId: number|null): Game {
return {
id: storeData.id,
creatorUserId,
rng: {
type: storeData.rng ? storeData.rng.type : '_fake_',
obj: storeData.rng ? Rng.unserialize(storeData.rng.obj) : new Rng(0),
},
puzzle: storeData.puzzle,
players: storeData.players,
evtInfos: {},
scoreMode: DefaultScoreMode(storeData.scoreMode),
shapeMode: DefaultShapeMode(storeData.shapeMode),
snapMode: DefaultSnapMode(storeData.snapMode),
}
}
function gameToStoreData(game: Game): string {
return JSON.stringify({
id: game.id, id: game.id,
rng: { rng: {
type: game.rng.type, type: game.rng.type,
@ -92,14 +177,20 @@ function persistGame(gameId: string): void {
scoreMode: game.scoreMode, scoreMode: game.scoreMode,
shapeMode: game.shapeMode, shapeMode: game.shapeMode,
snapMode: game.snapMode, snapMode: game.snapMode,
})) });
log.info(`[INFO] persisted game ${game.id}`)
} }
export default { export default {
loadGames, // disk functions are deprecated
loadGame, loadGamesFromDisk,
persistGames, loadGameFromDisk,
persistGame, persistGamesToDisk,
persistGameToDisk,
loadGamesFromDb,
loadGameFromDb,
persistGamesToDb,
persistGameToDb,
setDirty, setDirty,
} }

View file

@ -58,10 +58,7 @@ const storage = multer.diskStorage({
const upload = multer({storage}).single('file'); const upload = multer({storage}).single('file');
app.get('/api/me', (req, res): void => { app.get('/api/me', (req, res): void => {
let user = db.get('users', { let user = getUser(db, req)
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
})
res.send({ res.send({
id: user ? user.id : null, id: user ? user.id : null,
created: user ? user.created : null, created: user ? user.created : null,
@ -103,6 +100,7 @@ app.get('/api/replay-data', async (req, res): Promise<void> => {
log[0][5], log[0][5],
log[0][6], log[0][6],
log[0][7], log[0][7],
log[0][8], // creatorUserId
) )
} }
res.send({ log, game: game ? Util.encodeGame(game) : null }) res.send({ log, game: game ? Util.encodeGame(game) : null })
@ -144,6 +142,30 @@ interface SaveImageRequestData {
tags: string[] tags: string[]
} }
const getOrCreateUser = (db: Db, req: any): any => {
let user = getUser(db, req)
if (!user) {
db.insert('users', {
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
'created': Time.timestamp(),
})
user = getUser(db, req)
}
return user
}
const getUser = (db: Db, req: any): any => {
let user = db.get('users', {
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
})
if (user) {
user.id = parseInt(user.id, 10)
}
return user
}
const setImageTags = (db: Db, imageId: number, tags: string[]): void => { const setImageTags = (db: Db, imageId: number, tags: string[]): void => {
tags.forEach((tag: string) => { tags.forEach((tag: string) => {
const slug = Util.slug(tag) const slug = Util.slug(tag)
@ -158,21 +180,15 @@ const setImageTags = (db: Db, imageId: number, tags: string[]): void => {
} }
app.post('/api/save-image', express.json(), (req, res): void => { app.post('/api/save-image', express.json(), (req, res): void => {
let user = db.get('users', { let user = getUser(db, req)
'client_id': req.headers['client-id'], if (!user || !user.id) {
'client_secret': req.headers['client-secret'],
})
let userId: number|null = null
if (user) {
userId = parseInt(user.id, 10)
} else {
res.status(403).send({ ok: false, error: 'forbidden' }) res.status(403).send({ ok: false, error: 'forbidden' })
return return
} }
const data = req.body as SaveImageRequestData const data = req.body as SaveImageRequestData
let image = db.get('images', {id: data.id}) let image = db.get('images', {id: data.id})
if (parseInt(image.uploader_user_id, 10) !== userId) { if (parseInt(image.uploader_user_id, 10) !== user.id) {
res.status(403).send({ ok: false, error: 'forbidden' }) res.status(403).send({ ok: false, error: 'forbidden' })
return return
} }
@ -205,26 +221,13 @@ app.post('/api/upload', (req, res): void => {
res.status(400).send("Something went wrong!"); res.status(400).send("Something went wrong!");
} }
let user = db.get('users', { const user = getOrCreateUser(db, req)
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
})
let userId: number|null = null
if (user) {
userId = user.id
} else {
userId = db.insert('users', {
'client_id': req.headers['client-id'],
'client_secret': req.headers['client-secret'],
'created': Time.timestamp(),
}) as number
}
const dim = await Images.getDimensions( const dim = await Images.getDimensions(
`${UPLOAD_DIR}/${req.file.filename}` `${UPLOAD_DIR}/${req.file.filename}`
) )
const imageId = db.insert('images', { const imageId = db.insert('images', {
uploader_user_id: userId, uploader_user_id: user.id,
filename: req.file.filename, filename: req.file.filename,
filename_original: req.file.originalname, filename_original: req.file.originalname,
title: req.body.title || '', title: req.body.title || '',
@ -243,6 +246,12 @@ app.post('/api/upload', (req, res): void => {
}) })
app.post('/api/newgame', express.json(), async (req, res): Promise<void> => { app.post('/api/newgame', express.json(), async (req, res): Promise<void> => {
let user = getOrCreateUser(db, req)
if (!user || !user.id) {
res.status(403).send({ ok: false, error: 'forbidden' })
return
}
const gameSettings = req.body as GameSettings const gameSettings = req.body as GameSettings
log.log(gameSettings) log.log(gameSettings)
const gameId = Util.uniqId() const gameId = Util.uniqId()
@ -256,6 +265,7 @@ app.post('/api/newgame', express.json(), async (req, res): Promise<void> => {
gameSettings.scoreMode, gameSettings.scoreMode,
gameSettings.shapeMode, gameSettings.shapeMode,
gameSettings.snapMode, gameSettings.snapMode,
user.id,
) )
} }
res.send({ id: gameId }) res.send({ id: gameId })
@ -355,7 +365,7 @@ wss.on('message', async (
} }
}) })
GameStorage.loadGames() GameStorage.loadGamesFromDb(db)
const server = app.listen( const server = app.listen(
port, port,
hostname, hostname,
@ -378,7 +388,7 @@ memoryUsageHuman()
// persist games in fixed interval // persist games in fixed interval
const persistInterval = setInterval(() => { const persistInterval = setInterval(() => {
log.log('Persisting games...') log.log('Persisting games...')
GameStorage.persistGames() GameStorage.persistGamesToDb(db)
memoryUsageHuman() memoryUsageHuman()
}, config.persistence.interval) }, config.persistence.interval)
@ -390,7 +400,7 @@ const gracefulShutdown = (signal: string): void => {
clearInterval(persistInterval) clearInterval(persistInterval)
log.log('persisting games...') log.log('persisting games...')
GameStorage.persistGames() GameStorage.persistGamesToDb(db)
log.log('shutting down webserver...') log.log('shutting down webserver...')
server.close() server.close()