Compare commits
44 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68a267bd70 | ||
|
|
bf4897bf83 | ||
|
|
b4980e367c | ||
|
|
e7f86b5ef8 | ||
|
|
4e528cc83d | ||
|
|
126384e5bd | ||
|
|
e5fb49ecb1 | ||
|
|
c11229a5e5 | ||
|
|
1008106355 | ||
|
|
65daeb0247 | ||
|
|
d2d5968d02 | ||
|
|
8f31a669d5 | ||
|
|
e7628895c9 | ||
|
|
bbcfd42008 | ||
|
|
518092d269 | ||
|
|
0cb1cec210 | ||
|
|
2fb0e959ae | ||
|
|
7759cdc806 | ||
|
|
2b0dc392da | ||
|
|
d009f84156 | ||
|
|
a406d8abe8 | ||
|
|
b44ccbf819 | ||
|
|
b43d45ecc6 | ||
|
|
ac0116fc52 | ||
|
|
9c0ceb685e | ||
|
|
b8673e6a40 | ||
|
|
22933ad6b9 | ||
|
|
b8c193b5dc | ||
|
|
47381da36f | ||
|
|
b410f400fa | ||
|
|
4b10fbc01b | ||
|
|
accd38eb02 | ||
|
|
59d0d0dc2a | ||
|
|
10d56e2898 | ||
|
|
ff69a5e195 | ||
|
|
0882d3befd | ||
|
|
d9ab766e18 | ||
|
|
19301cfc81 | ||
|
|
cdb02da14d | ||
|
|
86ceca4bea | ||
|
|
60ae6e8a08 | ||
|
|
849d39dac2 | ||
|
|
3447681f10 | ||
|
|
514b3c6b22 |
51 changed files with 1823 additions and 945 deletions
File diff suppressed because one or more lines are too long
1
build/public/assets/index.63ff8630.js
Normal file
1
build/public/assets/index.63ff8630.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,9 +4,9 @@
|
|||
<meta charset="UTF-8">
|
||||
|
||||
<title>🧩 jigsaw.hyottoko.club</title>
|
||||
<script type="module" crossorigin src="/assets/index.7efa4c6c.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index.63ff8630.js"></script>
|
||||
<link rel="modulepreload" href="/assets/vendor.684f7bc8.js">
|
||||
<link rel="stylesheet" href="/assets/index.8f0efd0f.css">
|
||||
<link rel="stylesheet" href="/assets/index.22dc307c.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import express from 'express';
|
|||
import compression from 'compression';
|
||||
import multer from 'multer';
|
||||
import fs from 'fs';
|
||||
import readline from 'readline';
|
||||
import stream from 'stream';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import sizeOf from 'image-size';
|
||||
|
|
@ -13,29 +11,6 @@ import sharp from 'sharp';
|
|||
import v8 from 'v8';
|
||||
import bsqlite from 'better-sqlite3';
|
||||
|
||||
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 = {}));
|
||||
var ShapeMode;
|
||||
(function (ShapeMode) {
|
||||
ShapeMode[ShapeMode["NORMAL"] = 0] = "NORMAL";
|
||||
ShapeMode[ShapeMode["ANY"] = 1] = "ANY";
|
||||
ShapeMode[ShapeMode["FLAT"] = 2] = "FLAT";
|
||||
})(ShapeMode || (ShapeMode = {}));
|
||||
var SnapMode;
|
||||
(function (SnapMode) {
|
||||
SnapMode[SnapMode["NORMAL"] = 0] = "NORMAL";
|
||||
SnapMode[SnapMode["REAL"] = 1] = "REAL";
|
||||
})(SnapMode || (SnapMode = {}));
|
||||
|
||||
class Rng {
|
||||
constructor(seed) {
|
||||
this.rand_high = seed || 0xDEADC0DE;
|
||||
|
|
@ -177,9 +152,10 @@ function encodeGame(data) {
|
|||
data.puzzle,
|
||||
data.players,
|
||||
data.evtInfos,
|
||||
data.scoreMode || ScoreMode.FINAL,
|
||||
data.shapeMode || ShapeMode.ANY,
|
||||
data.snapMode || SnapMode.NORMAL,
|
||||
data.scoreMode,
|
||||
data.shapeMode,
|
||||
data.snapMode,
|
||||
data.creatorUserId,
|
||||
];
|
||||
}
|
||||
function decodeGame(data) {
|
||||
|
|
@ -195,6 +171,7 @@ function decodeGame(data) {
|
|||
scoreMode: data[6],
|
||||
shapeMode: data[7],
|
||||
snapMode: data[8],
|
||||
creatorUserId: data[9],
|
||||
};
|
||||
}
|
||||
function coordByPieceIdx(info, pieceIdx) {
|
||||
|
|
@ -360,6 +337,13 @@ const INPUT_EV_PLAYER_NAME = 8;
|
|||
const INPUT_EV_MOVE = 9;
|
||||
const INPUT_EV_TOGGLE_PREVIEW = 10;
|
||||
const INPUT_EV_TOGGLE_SOUNDS = 11;
|
||||
const INPUT_EV_REPLAY_TOGGLE_PAUSE = 12;
|
||||
const INPUT_EV_REPLAY_SPEED_UP = 13;
|
||||
const INPUT_EV_REPLAY_SPEED_DOWN = 14;
|
||||
const INPUT_EV_TOGGLE_PLAYER_NAMES = 15;
|
||||
const INPUT_EV_CENTER_FIT_PUZZLE = 16;
|
||||
const INPUT_EV_TOGGLE_FIXED_PIECES = 17;
|
||||
const INPUT_EV_TOGGLE_LOOSE_PIECES = 18;
|
||||
const CHANGE_DATA = 1;
|
||||
const CHANGE_TILE = 2;
|
||||
const CHANGE_PLAYER = 3;
|
||||
|
|
@ -383,6 +367,13 @@ var Protocol = {
|
|||
INPUT_EV_PLAYER_NAME,
|
||||
INPUT_EV_TOGGLE_PREVIEW,
|
||||
INPUT_EV_TOGGLE_SOUNDS,
|
||||
INPUT_EV_REPLAY_TOGGLE_PAUSE,
|
||||
INPUT_EV_REPLAY_SPEED_UP,
|
||||
INPUT_EV_REPLAY_SPEED_DOWN,
|
||||
INPUT_EV_TOGGLE_PLAYER_NAMES,
|
||||
INPUT_EV_CENTER_FIT_PUZZLE,
|
||||
INPUT_EV_TOGGLE_FIXED_PIECES,
|
||||
INPUT_EV_TOGGLE_LOOSE_PIECES,
|
||||
CHANGE_DATA,
|
||||
CHANGE_TILE,
|
||||
CHANGE_PLAYER,
|
||||
|
|
@ -486,6 +477,47 @@ var Time = {
|
|||
durationStr,
|
||||
};
|
||||
|
||||
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 = {}));
|
||||
var ShapeMode;
|
||||
(function (ShapeMode) {
|
||||
ShapeMode[ShapeMode["NORMAL"] = 0] = "NORMAL";
|
||||
ShapeMode[ShapeMode["ANY"] = 1] = "ANY";
|
||||
ShapeMode[ShapeMode["FLAT"] = 2] = "FLAT";
|
||||
})(ShapeMode || (ShapeMode = {}));
|
||||
var SnapMode;
|
||||
(function (SnapMode) {
|
||||
SnapMode[SnapMode["NORMAL"] = 0] = "NORMAL";
|
||||
SnapMode[SnapMode["REAL"] = 1] = "REAL";
|
||||
})(SnapMode || (SnapMode = {}));
|
||||
const DefaultScoreMode = (v) => {
|
||||
if (v === ScoreMode.FINAL || v === ScoreMode.ANY) {
|
||||
return v;
|
||||
}
|
||||
return ScoreMode.FINAL;
|
||||
};
|
||||
const DefaultShapeMode = (v) => {
|
||||
if (v === ShapeMode.NORMAL || v === ShapeMode.ANY || v === ShapeMode.FLAT) {
|
||||
return v;
|
||||
}
|
||||
return ShapeMode.NORMAL;
|
||||
};
|
||||
const DefaultSnapMode = (v) => {
|
||||
if (v === SnapMode.NORMAL || v === SnapMode.REAL) {
|
||||
return v;
|
||||
}
|
||||
return SnapMode.NORMAL;
|
||||
};
|
||||
|
||||
const IDLE_TIMEOUT_SEC = 30;
|
||||
// Map<gameId, Game>
|
||||
const GAMES = {};
|
||||
|
|
@ -580,12 +612,16 @@ function setEvtInfo(gameId, playerId, evtInfo) {
|
|||
}
|
||||
function getAllGames() {
|
||||
return Object.values(GAMES).sort((a, b) => {
|
||||
const finished = isFinished(a.id);
|
||||
// when both have same finished state, sort by started
|
||||
if (isFinished(a.id) === isFinished(b.id)) {
|
||||
if (finished === isFinished(b.id)) {
|
||||
if (finished) {
|
||||
return b.puzzle.data.finished - a.puzzle.data.finished;
|
||||
}
|
||||
return b.puzzle.data.started - a.puzzle.data.started;
|
||||
}
|
||||
// otherwise, sort: unfinished, finished
|
||||
return isFinished(a.id) ? 1 : -1;
|
||||
return finished ? 1 : -1;
|
||||
});
|
||||
}
|
||||
function getAllPlayers(gameId) {
|
||||
|
|
@ -600,16 +636,18 @@ function getPieceCount(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;
|
||||
const imageUrl = GAMES[gameId].puzzle.info.image?.url
|
||||
|| GAMES[gameId].puzzle.info.imageUrl;
|
||||
if (!imageUrl) {
|
||||
throw new Error('[2021-07-11] no image url set');
|
||||
}
|
||||
return imageUrl;
|
||||
}
|
||||
function getScoreMode(gameId) {
|
||||
return GAMES[gameId].scoreMode || ScoreMode.FINAL;
|
||||
return GAMES[gameId].scoreMode;
|
||||
}
|
||||
function getSnapMode(gameId) {
|
||||
return GAMES[gameId].snapMode || SnapMode.NORMAL;
|
||||
return GAMES[gameId].snapMode;
|
||||
}
|
||||
function isFinished(gameId) {
|
||||
return getFinishedPiecesCount(gameId) === getPieceCount(gameId);
|
||||
|
|
@ -1169,6 +1207,12 @@ function handleInput$1(gameId, playerId, input, ts, onSnap) {
|
|||
changePlayer(gameId, playerId, { d, ts });
|
||||
_playerChange();
|
||||
}
|
||||
if (snapped && getSnapMode(gameId) === SnapMode.REAL) {
|
||||
if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) {
|
||||
changeData(gameId, { finished: ts });
|
||||
_dataChange();
|
||||
}
|
||||
}
|
||||
if (snapped && onSnap) {
|
||||
onSnap(playerId);
|
||||
}
|
||||
|
|
@ -1211,7 +1255,6 @@ var GameCommon = {
|
|||
getFinishedPiecesCount,
|
||||
getPieceCount,
|
||||
getImageUrl,
|
||||
setImageUrl,
|
||||
get: get$1,
|
||||
getAllGames,
|
||||
getPlayerBgColor,
|
||||
|
|
@ -1249,6 +1292,7 @@ const PUBLIC_DIR = `${BASE_DIR}/build/public/`;
|
|||
const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`;
|
||||
const DB_FILE = `${BASE_DIR}/data/db.sqlite`;
|
||||
|
||||
const LINES_PER_LOG_FILE = 10000;
|
||||
const POST_GAME_LOG_DURATION = 5 * Time.MIN;
|
||||
const shouldLog = (finishTs, currentTs) => {
|
||||
// when not finished yet, always log
|
||||
|
|
@ -1260,55 +1304,65 @@ const shouldLog = (finishTs, currentTs) => {
|
|||
const timeSinceGameEnd = currentTs - finishTs;
|
||||
return timeSinceGameEnd <= POST_GAME_LOG_DURATION;
|
||||
};
|
||||
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`;
|
||||
const create = (gameId) => {
|
||||
const file = filename(gameId);
|
||||
if (!fs.existsSync(file)) {
|
||||
fs.appendFileSync(file, '');
|
||||
const filename = (gameId, offset) => `${DATA_DIR}/log_${gameId}-${offset}.log`;
|
||||
const idxname = (gameId) => `${DATA_DIR}/log_${gameId}.idx.log`;
|
||||
const create = (gameId, ts) => {
|
||||
const idxfile = idxname(gameId);
|
||||
if (!fs.existsSync(idxfile)) {
|
||||
fs.appendFileSync(idxfile, JSON.stringify({
|
||||
gameId: gameId,
|
||||
total: 0,
|
||||
lastTs: ts,
|
||||
currentFile: '',
|
||||
perFile: LINES_PER_LOG_FILE,
|
||||
}));
|
||||
}
|
||||
};
|
||||
const exists = (gameId) => {
|
||||
const file = filename(gameId);
|
||||
return fs.existsSync(file);
|
||||
const idxfile = idxname(gameId);
|
||||
return fs.existsSync(idxfile);
|
||||
};
|
||||
const _log = (gameId, ...args) => {
|
||||
const file = filename(gameId);
|
||||
if (!fs.existsSync(file)) {
|
||||
const _log = (gameId, type, ...args) => {
|
||||
const idxfile = idxname(gameId);
|
||||
if (!fs.existsSync(idxfile)) {
|
||||
return;
|
||||
}
|
||||
const str = JSON.stringify(args);
|
||||
fs.appendFileSync(file, str + "\n");
|
||||
const idxObj = JSON.parse(fs.readFileSync(idxfile, 'utf-8'));
|
||||
if (idxObj.total % idxObj.perFile === 0) {
|
||||
idxObj.currentFile = filename(gameId, idxObj.total);
|
||||
}
|
||||
const tsIdx = type === Protocol.LOG_HEADER ? 3 : (args.length - 1);
|
||||
const ts = args[tsIdx];
|
||||
if (type !== Protocol.LOG_HEADER) {
|
||||
// for everything but header save the diff to last log entry
|
||||
args[tsIdx] = ts - idxObj.lastTs;
|
||||
}
|
||||
const line = JSON.stringify([type, ...args]).slice(1, -1);
|
||||
fs.appendFileSync(idxObj.currentFile, line + "\n");
|
||||
idxObj.total++;
|
||||
idxObj.lastTs = ts;
|
||||
fs.writeFileSync(idxfile, JSON.stringify(idxObj));
|
||||
};
|
||||
const get = async (gameId, offset = 0, size = 10000) => {
|
||||
const file = filename(gameId);
|
||||
const get = (gameId, offset = 0) => {
|
||||
const idxfile = idxname(gameId);
|
||||
if (!fs.existsSync(idxfile)) {
|
||||
return [];
|
||||
}
|
||||
const file = filename(gameId, offset);
|
||||
if (!fs.existsSync(file)) {
|
||||
return [];
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const instream = fs.createReadStream(file);
|
||||
const outstream = new stream.Writable();
|
||||
const rl = readline.createInterface(instream, outstream);
|
||||
const lines = [];
|
||||
let i = -1;
|
||||
rl.on('line', (line) => {
|
||||
if (!line) {
|
||||
// skip empty
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
if (offset > i) {
|
||||
return;
|
||||
}
|
||||
if (offset + size <= i) {
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
lines.push(JSON.parse(line));
|
||||
});
|
||||
rl.on('close', () => {
|
||||
resolve(lines);
|
||||
});
|
||||
const lines = fs.readFileSync(file, 'utf-8').split("\n");
|
||||
const log = lines.filter(line => !!line).map(line => {
|
||||
return JSON.parse(`[${line}]`);
|
||||
});
|
||||
if (offset === 0 && log.length > 0) {
|
||||
log[0][5] = DefaultScoreMode(log[0][5]);
|
||||
log[0][6] = DefaultShapeMode(log[0][6]);
|
||||
log[0][7] = DefaultSnapMode(log[0][7]);
|
||||
log[0][8] = log[0][8] || null;
|
||||
}
|
||||
return log;
|
||||
};
|
||||
var GameLog = {
|
||||
shouldLog,
|
||||
|
|
@ -1316,6 +1370,8 @@ var GameLog = {
|
|||
exists,
|
||||
log: _log,
|
||||
get,
|
||||
filename,
|
||||
idxname,
|
||||
};
|
||||
|
||||
const log$4 = logger('Images.ts');
|
||||
|
|
@ -1390,12 +1446,14 @@ const imageFromDb = (db, imageId) => {
|
|||
const i = db.get('images', { id: imageId });
|
||||
return {
|
||||
id: i.id,
|
||||
uploaderUserId: i.uploader_user_id,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
tags: getTags(db, i.id),
|
||||
created: i.created * 1000,
|
||||
width: i.width,
|
||||
height: i.height,
|
||||
};
|
||||
};
|
||||
const allImagesFromDb = (db, tagSlugs, orderBy) => {
|
||||
|
|
@ -1427,35 +1485,42 @@ inner join images i on i.id = ixc.image_id ${where.sql};
|
|||
const images = db.getMany('images', wheresRaw, orderByMap[orderBy]);
|
||||
return images.map(i => ({
|
||||
id: i.id,
|
||||
uploaderUserId: i.uploader_user_id,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
tags: getTags(db, i.id),
|
||||
created: i.created * 1000,
|
||||
width: i.width,
|
||||
height: i.height,
|
||||
}));
|
||||
};
|
||||
/**
|
||||
* @deprecated old function, now database is used
|
||||
*/
|
||||
const allImagesFromDisk = (tags, sort) => {
|
||||
let images = fs.readdirSync(UPLOAD_DIR)
|
||||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||
.map(f => ({
|
||||
id: 0,
|
||||
uploaderUserId: null,
|
||||
filename: f,
|
||||
file: `${UPLOAD_DIR}/${f}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||
title: f.replace(/\.[a-z]+$/, ''),
|
||||
tags: [],
|
||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
width: 0,
|
||||
height: 0, // may have to fill when the function is used again
|
||||
}));
|
||||
switch (sort) {
|
||||
case 'alpha_asc':
|
||||
images = images.sort((a, b) => {
|
||||
return a.file > b.file ? 1 : -1;
|
||||
return a.filename > b.filename ? 1 : -1;
|
||||
});
|
||||
break;
|
||||
case 'alpha_desc':
|
||||
images = images.sort((a, b) => {
|
||||
return a.file < b.file ? 1 : -1;
|
||||
return a.filename < b.filename ? 1 : -1;
|
||||
});
|
||||
break;
|
||||
case 'date_asc':
|
||||
|
|
@ -1501,7 +1566,7 @@ var Images = {
|
|||
// final resized version of the puzzle image
|
||||
const TILE_SIZE = 64;
|
||||
async function createPuzzle(rng, targetTiles, image, ts, shapeMode) {
|
||||
const imagePath = image.file;
|
||||
const imagePath = `${UPLOAD_DIR}/${image.filename}`;
|
||||
const imageUrl = image.url;
|
||||
// determine puzzle information from the image dimensions
|
||||
const dim = await Images.getDimensions(imagePath);
|
||||
|
|
@ -1600,6 +1665,7 @@ async function createPuzzle(rng, targetTiles, image, ts, shapeMode) {
|
|||
// information that was used to create the puzzle
|
||||
targetTiles: targetTiles,
|
||||
imageUrl,
|
||||
image: image,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
tileSize: info.tileSize,
|
||||
|
|
@ -1690,7 +1756,63 @@ function setDirty(gameId) {
|
|||
function setClean(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);
|
||||
for (const f of files) {
|
||||
const m = f.match(/^([a-z0-9]+)\.json$/);
|
||||
|
|
@ -1698,10 +1820,13 @@ function loadGames() {
|
|||
continue;
|
||||
}
|
||||
const gameId = m[1];
|
||||
loadGame(gameId);
|
||||
loadGameFromDisk(gameId);
|
||||
}
|
||||
}
|
||||
function loadGame(gameId) {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
function loadGameFromDisk(gameId) {
|
||||
const file = `${DATA_DIR}/${gameId}.json`;
|
||||
const contents = fs.readFileSync(file, 'utf-8');
|
||||
let game;
|
||||
|
|
@ -1723,27 +1848,21 @@ function loadGame(gameId) {
|
|||
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: {},
|
||||
scoreMode: game.scoreMode || ScoreMode.FINAL,
|
||||
shapeMode: game.shapeMode || ShapeMode.ANY,
|
||||
snapMode: game.snapMode || SnapMode.NORMAL,
|
||||
};
|
||||
const gameObject = storeDataToGame(game, null);
|
||||
GameCommon.setGame(gameObject.id, gameObject);
|
||||
}
|
||||
function persistGames() {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
function persistGamesToDisk() {
|
||||
for (const gameId of Object.keys(dirtyGames)) {
|
||||
persistGame(gameId);
|
||||
persistGameToDisk(gameId);
|
||||
}
|
||||
}
|
||||
function persistGame(gameId) {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
function persistGameToDisk(gameId) {
|
||||
const game = GameCommon.get(gameId);
|
||||
if (!game) {
|
||||
log$3.error(`[ERROR] unable to persist non existing game ${gameId}`);
|
||||
|
|
@ -1752,7 +1871,27 @@ function persistGame(gameId) {
|
|||
if (game.id in dirtyGames) {
|
||||
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,
|
||||
rng: {
|
||||
type: game.rng.type,
|
||||
|
|
@ -1763,22 +1902,27 @@ function persistGame(gameId) {
|
|||
scoreMode: game.scoreMode,
|
||||
shapeMode: game.shapeMode,
|
||||
snapMode: game.snapMode,
|
||||
}));
|
||||
log$3.info(`[INFO] persisted game ${game.id}`);
|
||||
});
|
||||
}
|
||||
var GameStorage = {
|
||||
loadGames,
|
||||
loadGame,
|
||||
persistGames,
|
||||
persistGame,
|
||||
// disk functions are deprecated
|
||||
loadGamesFromDisk,
|
||||
loadGameFromDisk,
|
||||
persistGamesToDisk,
|
||||
persistGameToDisk,
|
||||
loadGamesFromDb,
|
||||
loadGameFromDb,
|
||||
persistGamesToDb,
|
||||
persistGameToDb,
|
||||
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 rng = new Rng(seed);
|
||||
return {
|
||||
id: gameId,
|
||||
creatorUserId,
|
||||
rng: { type: 'Rng', obj: rng },
|
||||
puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode),
|
||||
players: [],
|
||||
|
|
@ -1788,22 +1932,21 @@ async function createGameObject(gameId, targetTiles, image, ts, scoreMode, shape
|
|||
snapMode,
|
||||
};
|
||||
}
|
||||
async function createGame(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode) {
|
||||
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode, shapeMode, snapMode);
|
||||
GameLog.create(gameId);
|
||||
GameLog.log(gameId, Protocol.LOG_HEADER, 1, 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, creatorUserId);
|
||||
GameLog.create(gameId, ts);
|
||||
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode, shapeMode, snapMode, gameObject.creatorUserId);
|
||||
GameCommon.setGame(gameObject.id, gameObject);
|
||||
GameStorage.setDirty(gameId);
|
||||
}
|
||||
function addPlayer(gameId, playerId, ts) {
|
||||
if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), 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);
|
||||
GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, ts);
|
||||
}
|
||||
else {
|
||||
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, diff);
|
||||
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, ts);
|
||||
}
|
||||
}
|
||||
GameCommon.addPlayer(gameId, playerId, ts);
|
||||
|
|
@ -1812,8 +1955,7 @@ function addPlayer(gameId, playerId, ts) {
|
|||
function handleInput(gameId, playerId, input, ts) {
|
||||
if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), ts)) {
|
||||
const idx = GameCommon.getPlayerIndexById(gameId, playerId);
|
||||
const diff = ts - GameCommon.getStartTs(gameId);
|
||||
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff);
|
||||
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, ts);
|
||||
}
|
||||
const ret = GameCommon.handleInput(gameId, playerId, input, ts);
|
||||
GameStorage.setDirty(gameId);
|
||||
|
|
@ -2039,6 +2181,13 @@ const storage = multer.diskStorage({
|
|||
}
|
||||
});
|
||||
const upload = multer({ storage }).single('file');
|
||||
app.get('/api/me', (req, res) => {
|
||||
let user = getUser(db, req);
|
||||
res.send({
|
||||
id: user ? user.id : null,
|
||||
created: user ? user.created : null,
|
||||
});
|
||||
});
|
||||
app.get('/api/conf', (req, res) => {
|
||||
res.send({
|
||||
WS_ADDRESS: config.ws.connectstring,
|
||||
|
|
@ -2061,11 +2210,12 @@ app.get('/api/replay-data', async (req, res) => {
|
|||
res.status(404).send({ reason: 'no log found' });
|
||||
return;
|
||||
}
|
||||
const log = await GameLog.get(gameId, offset, size);
|
||||
const log = GameLog.get(gameId, offset);
|
||||
let game = null;
|
||||
if (offset === 0) {
|
||||
// also need the game
|
||||
game = await Game.createGameObject(gameId, log[0][2], log[0][3], log[0][4], log[0][5] || ScoreMode.FINAL, log[0][6] || ShapeMode.NORMAL, log[0][7] || SnapMode.NORMAL);
|
||||
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][8]);
|
||||
}
|
||||
res.send({ log, game: game ? Util.encodeGame(game) : null });
|
||||
});
|
||||
|
|
@ -2096,6 +2246,28 @@ app.get('/api/index-data', (req, res) => {
|
|||
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) => {
|
||||
tags.forEach((tag) => {
|
||||
const slug = Util.slug(tag);
|
||||
|
|
@ -2109,7 +2281,17 @@ const setImageTags = (db, imageId, tags) => {
|
|||
});
|
||||
};
|
||||
app.post('/api/save-image', express.json(), (req, res) => {
|
||||
let user = getUser(db, req);
|
||||
if (!user || !user.id) {
|
||||
res.status(403).send({ ok: false, error: 'forbidden' });
|
||||
return;
|
||||
}
|
||||
const data = req.body;
|
||||
let image = db.get('images', { id: data.id });
|
||||
if (parseInt(image.uploader_user_id, 10) !== user.id) {
|
||||
res.status(403).send({ ok: false, error: 'forbidden' });
|
||||
return;
|
||||
}
|
||||
db.update('images', {
|
||||
title: data.title,
|
||||
}, {
|
||||
|
|
@ -2134,11 +2316,16 @@ app.post('/api/upload', (req, res) => {
|
|||
log.log(err);
|
||||
res.status(400).send("Something went wrong!");
|
||||
}
|
||||
const user = getOrCreateUser(db, req);
|
||||
const dim = await Images.getDimensions(`${UPLOAD_DIR}/${req.file.filename}`);
|
||||
const imageId = db.insert('images', {
|
||||
uploader_user_id: user.id,
|
||||
filename: req.file.filename,
|
||||
filename_original: req.file.originalname,
|
||||
title: req.body.title || '',
|
||||
created: Time.timestamp(),
|
||||
width: dim.w,
|
||||
height: dim.h,
|
||||
});
|
||||
if (req.body.tags) {
|
||||
const tags = req.body.tags.split(',').filter((tag) => !!tag);
|
||||
|
|
@ -2148,12 +2335,17 @@ app.post('/api/upload', (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;
|
||||
log.log(gameSettings);
|
||||
const gameId = Util.uniqId();
|
||||
if (!GameCommon.exists(gameId)) {
|
||||
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 });
|
||||
});
|
||||
|
|
@ -2235,7 +2427,7 @@ wss.on('message', async ({ socket, data }) => {
|
|||
log.error(e);
|
||||
}
|
||||
});
|
||||
GameStorage.loadGames();
|
||||
GameStorage.loadGamesFromDb(db);
|
||||
const server = app.listen(port, hostname, () => log.log(`server running on http://${hostname}:${port}`));
|
||||
wss.listen();
|
||||
const memoryUsageHuman = () => {
|
||||
|
|
@ -2249,7 +2441,7 @@ memoryUsageHuman();
|
|||
// persist games in fixed interval
|
||||
const persistInterval = setInterval(() => {
|
||||
log.log('Persisting games...');
|
||||
GameStorage.persistGames();
|
||||
GameStorage.persistGamesToDb(db);
|
||||
memoryUsageHuman();
|
||||
}, config.persistence.interval);
|
||||
const gracefulShutdown = (signal) => {
|
||||
|
|
@ -2257,7 +2449,7 @@ const gracefulShutdown = (signal) => {
|
|||
log.log('clearing persist interval...');
|
||||
clearInterval(persistInterval);
|
||||
log.log('persisting games...');
|
||||
GameStorage.persistGames();
|
||||
GameStorage.persistGamesToDb(db);
|
||||
log.log('shutting down webserver...');
|
||||
server.close();
|
||||
log.log('shutting down websocketserver...');
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ export default {
|
|||
"image-size",
|
||||
"multer",
|
||||
"path",
|
||||
"readline",
|
||||
"sharp",
|
||||
"stream",
|
||||
"url",
|
||||
"v8",
|
||||
"ws",
|
||||
|
|
|
|||
90
scripts/fix_games_image_info.ts
Normal file
90
scripts/fix_games_image_info.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import GameCommon from '../src/common/GameCommon'
|
||||
import GameLog from '../src/server/GameLog'
|
||||
import { Game } from '../src/common/Types'
|
||||
import { logger } from '../src/common/Util'
|
||||
import { DB_FILE, DB_PATCHES_DIR, UPLOAD_DIR } from '../src/server/Dirs'
|
||||
import Db from '../src/server/Db'
|
||||
import GameStorage from '../src/server/GameStorage'
|
||||
import fs from 'fs'
|
||||
|
||||
const log = logger('fix_games_image_info.ts')
|
||||
|
||||
import Images from '../src/server/Images'
|
||||
|
||||
console.log(DB_FILE)
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||
db.patch(true)
|
||||
|
||||
// ;(async () => {
|
||||
// let images = db.getMany('images')
|
||||
// for (let image of images) {
|
||||
// console.log(image.filename)
|
||||
// let dim = await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`)
|
||||
// console.log(await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`))
|
||||
// image.width = dim.w
|
||||
// image.height = dim.h
|
||||
// db.upsert('images', image, { id: image.id })
|
||||
// }
|
||||
// })()
|
||||
|
||||
function fixOne(gameId: string) {
|
||||
let g = GameCommon.get(gameId)
|
||||
if (!g) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!g.puzzle.info.image && g.puzzle.info.imageUrl) {
|
||||
log.log('game id: ', gameId)
|
||||
const parts = g.puzzle.info.imageUrl.split('/')
|
||||
const fileName = parts[parts.length - 1]
|
||||
const imageRow = db.get('images', {filename: fileName})
|
||||
if (!imageRow) {
|
||||
return
|
||||
}
|
||||
|
||||
g.puzzle.info.image = Images.imageFromDb(db, imageRow.id)
|
||||
|
||||
log.log(g.puzzle.info.image.title, imageRow.id)
|
||||
|
||||
GameStorage.persistGameToDb(db, gameId)
|
||||
} else if (g.puzzle.info.image?.id) {
|
||||
const imageId = g.puzzle.info.image.id
|
||||
|
||||
g.puzzle.info.image = Images.imageFromDb(db, imageId)
|
||||
|
||||
log.log(g.puzzle.info.image.title, imageId)
|
||||
|
||||
GameStorage.persistGameToDb(db, gameId)
|
||||
}
|
||||
|
||||
// fix log
|
||||
const file = GameLog.filename(gameId, 0)
|
||||
if (!fs.existsSync(file)) {
|
||||
return
|
||||
}
|
||||
|
||||
const lines = fs.readFileSync(file, 'utf-8').split("\n")
|
||||
const l = lines.filter(line => !!line).map(line => {
|
||||
return JSON.parse(`[${line}]`)
|
||||
})
|
||||
if (l && l[0] && !l[0][3].id) {
|
||||
log.log(l[0][3])
|
||||
l[0][3] = g.puzzle.info.image
|
||||
const newlines = l.map(ll => {
|
||||
return JSON.stringify(ll).slice(1, -1)
|
||||
}).join("\n") + "\n"
|
||||
console.log(g.puzzle.info.image)
|
||||
// process.exit(0)
|
||||
fs.writeFileSync(file, newlines)
|
||||
}
|
||||
}
|
||||
|
||||
function fix() {
|
||||
GameStorage.loadGamesFromDisk()
|
||||
GameCommon.getAllGames().forEach((game: Game) => {
|
||||
fixOne(game.id)
|
||||
})
|
||||
}
|
||||
|
||||
fix()
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import GameCommon from '../src/common/GameCommon'
|
||||
import { logger } from '../src/common/Util'
|
||||
import GameStorage from '../src/server/GameStorage'
|
||||
|
||||
const log = logger('fix_image.js')
|
||||
|
||||
function fix(gameId) {
|
||||
GameStorage.loadGame(gameId)
|
||||
let changed = false
|
||||
|
||||
let imgUrl = GameCommon.getImageUrl(gameId)
|
||||
if (imgUrl.match(/^\/example-images\//)) {
|
||||
log.log(`found bad imgUrl: ${imgUrl}`)
|
||||
imgUrl = imgUrl.replace(/^\/example-images\//, '/uploads/')
|
||||
GameCommon.setImageUrl(gameId, imgUrl)
|
||||
changed = true
|
||||
}
|
||||
if (changed) {
|
||||
GameStorage.persistGame(gameId)
|
||||
}
|
||||
}
|
||||
|
||||
fix(process.argv[2])
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import GameCommon from '../src/common/GameCommon'
|
||||
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'
|
||||
|
||||
const log = logger('fix_tiles.js')
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||
db.patch(true)
|
||||
|
||||
function fix_tiles(gameId) {
|
||||
GameStorage.loadGame(gameId)
|
||||
GameStorage.loadGameFromDb(db, gameId)
|
||||
let changed = false
|
||||
const tiles = GameCommon.getPiecesSortedByZIndex(gameId)
|
||||
for (let tile of tiles) {
|
||||
|
|
@ -27,7 +32,7 @@ function fix_tiles(gameId) {
|
|||
}
|
||||
}
|
||||
if (changed) {
|
||||
GameStorage.persistGame(gameId)
|
||||
GameStorage.persistGameToDb(db, gameId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
27
scripts/import_games.ts
Normal file
27
scripts/import_games.ts
Normal 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()
|
||||
18
scripts/import_image_sizes.ts
Normal file
18
scripts/import_image_sizes.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { DB_FILE, DB_PATCHES_DIR, UPLOAD_DIR } from '../src/server/Dirs'
|
||||
import Db from '../src/server/Db'
|
||||
import Images from '../src/server/Images'
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||
db.patch(true)
|
||||
|
||||
;(async () => {
|
||||
let images = db.getMany('images')
|
||||
for (let image of images) {
|
||||
console.log(image.filename)
|
||||
let dim = await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`)
|
||||
console.log(await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`))
|
||||
image.width = dim.w
|
||||
image.height = dim.h
|
||||
db.upsert('images', image, { id: image.id })
|
||||
}
|
||||
})()
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
import fs from 'fs'
|
||||
import Protocol from '../src/common/Protocol'
|
||||
import { logger } from '../src/common/Util'
|
||||
import { DATA_DIR } from '../src/server/Dirs'
|
||||
|
||||
const log = logger('rewrite_logs')
|
||||
|
||||
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`
|
||||
|
||||
const rewrite = (gameId) => {
|
||||
const file = filename(gameId)
|
||||
log.log(file)
|
||||
if (!fs.existsSync(file)) {
|
||||
return []
|
||||
}
|
||||
let playerIds = [];
|
||||
let startTs = null
|
||||
const lines = fs.readFileSync(file, 'utf-8').split("\n")
|
||||
const linesNew = lines.filter(line => !!line).map((line) => {
|
||||
const json = JSON.parse(line)
|
||||
const m = {
|
||||
createGame: Protocol.LOG_HEADER,
|
||||
addPlayer: Protocol.LOG_ADD_PLAYER,
|
||||
handleInput: Protocol.LOG_HANDLE_INPUT,
|
||||
}
|
||||
const action = json[0]
|
||||
if (action in m) {
|
||||
json[0] = m[action]
|
||||
if (json[0] === Protocol.LOG_HANDLE_INPUT) {
|
||||
const inputm = {
|
||||
down: Protocol.INPUT_EV_MOUSE_DOWN,
|
||||
up: Protocol.INPUT_EV_MOUSE_UP,
|
||||
move: Protocol.INPUT_EV_MOUSE_MOVE,
|
||||
zoomin: Protocol.INPUT_EV_ZOOM_IN,
|
||||
zoomout: Protocol.INPUT_EV_ZOOM_OUT,
|
||||
bg_color: Protocol.INPUT_EV_BG_COLOR,
|
||||
player_color: Protocol.INPUT_EV_PLAYER_COLOR,
|
||||
player_name: Protocol.INPUT_EV_PLAYER_NAME,
|
||||
}
|
||||
const inputa = json[2][0]
|
||||
if (inputa in inputm) {
|
||||
json[2][0] = inputm[inputa]
|
||||
} else {
|
||||
throw '[ invalid input log line: "' + line + '" ]'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw '[ invalid general log line: "' + line + '" ]'
|
||||
}
|
||||
|
||||
if (json[0] === Protocol.LOG_ADD_PLAYER) {
|
||||
if (playerIds.indexOf(json[1]) === -1) {
|
||||
playerIds.push(json[1])
|
||||
} else {
|
||||
json[0] = Protocol.LOG_UPDATE_PLAYER
|
||||
json[1] = playerIds.indexOf(json[1])
|
||||
}
|
||||
}
|
||||
|
||||
if (json[0] === Protocol.LOG_HANDLE_INPUT) {
|
||||
json[1] = playerIds.indexOf(json[1])
|
||||
if (json[1] === -1) {
|
||||
throw '[ invalid player ... "' + line + '" ]'
|
||||
}
|
||||
}
|
||||
|
||||
if (json[0] === Protocol.LOG_HEADER) {
|
||||
startTs = json[json.length - 1]
|
||||
json[4] = json[3]
|
||||
json[3] = json[2]
|
||||
json[2] = json[1]
|
||||
json[1] = 1
|
||||
} else {
|
||||
json[json.length - 1] = json[json.length - 1] - startTs
|
||||
}
|
||||
return JSON.stringify(json)
|
||||
})
|
||||
|
||||
fs.writeFileSync(file, linesNew.join("\n") + "\n")
|
||||
}
|
||||
|
||||
rewrite(process.argv[2])
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
|
||||
# server for built files
|
||||
nodemon --max-old-space-size=64 -e js build/server/main.js -c config.json
|
||||
nodemon --watch build --max-old-space-size=64 -e js build/server/main.js -c config.json
|
||||
|
|
|
|||
73
scripts/split_logs.ts
Normal file
73
scripts/split_logs.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import fs from 'fs'
|
||||
import { logger } from '../src/common/Util'
|
||||
import { DATA_DIR } from '../src/server/Dirs'
|
||||
import { filename } from '../src/server/GameLog'
|
||||
|
||||
const log = logger('rewrite_logs')
|
||||
|
||||
interface IdxOld {
|
||||
total: number
|
||||
currentFile: string
|
||||
perFile: number
|
||||
}
|
||||
|
||||
interface Idx {
|
||||
gameId: string
|
||||
total: number
|
||||
lastTs: number
|
||||
currentFile: string
|
||||
perFile: number
|
||||
}
|
||||
const doit = (idxfile: string): void => {
|
||||
const gameId: string = (idxfile.match(/^log_([a-z0-9]+)\.idx\.log$/) as any[])[1]
|
||||
const idxOld: IdxOld = JSON.parse(fs.readFileSync(DATA_DIR + '/' + idxfile, 'utf-8'))
|
||||
|
||||
let currentFile = filename(gameId, 0)
|
||||
const idxNew: Idx = {
|
||||
gameId: gameId,
|
||||
total: 0,
|
||||
lastTs: 0,
|
||||
currentFile: currentFile,
|
||||
perFile: idxOld.perFile
|
||||
}
|
||||
|
||||
let firstTs = 0
|
||||
while (fs.existsSync(currentFile)) {
|
||||
idxNew.currentFile = currentFile
|
||||
const log = fs.readFileSync(currentFile, 'utf-8').split("\n")
|
||||
const newLines = []
|
||||
const lines = log.filter(line => !!line).map(line => {
|
||||
return JSON.parse(line)
|
||||
})
|
||||
for (const l of lines) {
|
||||
if (idxNew.total === 0) {
|
||||
firstTs = l[4]
|
||||
idxNew.lastTs = l[4]
|
||||
newLines.push(JSON.stringify(l).slice(1, -1))
|
||||
} else {
|
||||
const ts = firstTs + l[l.length - 1]
|
||||
const diff = ts - idxNew.lastTs
|
||||
idxNew.lastTs = ts
|
||||
const newL = l.slice(0, -1)
|
||||
newL.push(diff)
|
||||
newLines.push(JSON.stringify(newL).slice(1, -1))
|
||||
}
|
||||
idxNew.total++
|
||||
}
|
||||
fs.writeFileSync(idxNew.currentFile, newLines.join("\n") + "\n")
|
||||
currentFile = filename(gameId, idxNew.total)
|
||||
}
|
||||
|
||||
fs.writeFileSync(DATA_DIR + '/' + idxfile, JSON.stringify(idxNew))
|
||||
console.log('done: ' + gameId)
|
||||
}
|
||||
|
||||
let indexfiles = fs.readdirSync(DATA_DIR)
|
||||
.filter(f => f.toLowerCase().match(/^log_[a-z0-9]+\.idx\.log$/))
|
||||
|
||||
|
||||
;(async () => {
|
||||
for (const file of indexfiles) {
|
||||
await doit(file)
|
||||
}
|
||||
})()
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh -e
|
||||
|
||||
node --experimental-specifier-resolution=node --loader ts-node/esm $@
|
||||
node --max-old-space-size=256 --experimental-specifier-resolution=node --loader ts-node/esm $@
|
||||
|
|
|
|||
|
|
@ -138,12 +138,16 @@ function setEvtInfo(
|
|||
|
||||
function getAllGames(): Array<Game> {
|
||||
return Object.values(GAMES).sort((a: Game, b: Game) => {
|
||||
const finished = isFinished(a.id)
|
||||
// when both have same finished state, sort by started
|
||||
if (isFinished(a.id) === isFinished(b.id)) {
|
||||
if (finished === isFinished(b.id)) {
|
||||
if (finished) {
|
||||
return b.puzzle.data.finished - a.puzzle.data.finished
|
||||
}
|
||||
return b.puzzle.data.started - a.puzzle.data.started
|
||||
}
|
||||
// otherwise, sort: unfinished, finished
|
||||
return isFinished(a.id) ? 1 : -1
|
||||
return finished ? 1 : -1
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -162,19 +166,20 @@ function getPieceCount(gameId: string): number {
|
|||
}
|
||||
|
||||
function getImageUrl(gameId: string): string {
|
||||
return GAMES[gameId].puzzle.info.imageUrl
|
||||
}
|
||||
|
||||
function setImageUrl(gameId: string, imageUrl: string): void {
|
||||
GAMES[gameId].puzzle.info.imageUrl = imageUrl
|
||||
const imageUrl = GAMES[gameId].puzzle.info.image?.url
|
||||
|| GAMES[gameId].puzzle.info.imageUrl
|
||||
if (!imageUrl) {
|
||||
throw new Error('[2021-07-11] no image url set')
|
||||
}
|
||||
return imageUrl
|
||||
}
|
||||
|
||||
function getScoreMode(gameId: string): ScoreMode {
|
||||
return GAMES[gameId].scoreMode || ScoreMode.FINAL
|
||||
return GAMES[gameId].scoreMode
|
||||
}
|
||||
|
||||
function getSnapMode(gameId: string): SnapMode {
|
||||
return GAMES[gameId].snapMode || SnapMode.NORMAL
|
||||
return GAMES[gameId].snapMode
|
||||
}
|
||||
|
||||
function isFinished(gameId: string): boolean {
|
||||
|
|
@ -848,6 +853,13 @@ function handleInput(
|
|||
changePlayer(gameId, playerId, { d, ts })
|
||||
_playerChange()
|
||||
}
|
||||
|
||||
if (snapped && getSnapMode(gameId) === SnapMode.REAL) {
|
||||
if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) {
|
||||
changeData(gameId, { finished: ts })
|
||||
_dataChange()
|
||||
}
|
||||
}
|
||||
if (snapped && onSnap) {
|
||||
onSnap(playerId)
|
||||
}
|
||||
|
|
@ -888,7 +900,6 @@ export default {
|
|||
getFinishedPiecesCount,
|
||||
getPieceCount,
|
||||
getImageUrl,
|
||||
setImageUrl,
|
||||
get,
|
||||
getAllGames,
|
||||
getPlayerBgColor,
|
||||
|
|
|
|||
|
|
@ -43,11 +43,6 @@ const EV_SERVER_INIT = 4
|
|||
const EV_CLIENT_EVENT = 2
|
||||
const EV_CLIENT_INIT = 3
|
||||
|
||||
const EV_CLIENT_INIT_REPLAY = 5
|
||||
const EV_SERVER_INIT_REPLAY = 6
|
||||
const EV_CLIENT_REPLAY_EVENT = 7
|
||||
const EV_SERVER_REPLAY_EVENT = 8
|
||||
|
||||
const LOG_HEADER = 1
|
||||
const LOG_ADD_PLAYER = 2
|
||||
const LOG_UPDATE_PLAYER = 4
|
||||
|
|
@ -65,6 +60,16 @@ const INPUT_EV_MOVE = 9
|
|||
const INPUT_EV_TOGGLE_PREVIEW = 10
|
||||
const INPUT_EV_TOGGLE_SOUNDS = 11
|
||||
|
||||
const INPUT_EV_REPLAY_TOGGLE_PAUSE = 12
|
||||
const INPUT_EV_REPLAY_SPEED_UP = 13
|
||||
const INPUT_EV_REPLAY_SPEED_DOWN = 14
|
||||
|
||||
const INPUT_EV_TOGGLE_PLAYER_NAMES = 15
|
||||
const INPUT_EV_CENTER_FIT_PUZZLE = 16
|
||||
|
||||
const INPUT_EV_TOGGLE_FIXED_PIECES = 17
|
||||
const INPUT_EV_TOGGLE_LOOSE_PIECES = 18
|
||||
|
||||
const CHANGE_DATA = 1
|
||||
const CHANGE_TILE = 2
|
||||
const CHANGE_PLAYER = 3
|
||||
|
|
@ -75,11 +80,6 @@ export default {
|
|||
EV_CLIENT_EVENT,
|
||||
EV_CLIENT_INIT,
|
||||
|
||||
EV_CLIENT_INIT_REPLAY,
|
||||
EV_SERVER_INIT_REPLAY,
|
||||
EV_CLIENT_REPLAY_EVENT,
|
||||
EV_SERVER_REPLAY_EVENT,
|
||||
|
||||
LOG_HEADER,
|
||||
LOG_ADD_PLAYER,
|
||||
LOG_UPDATE_PLAYER,
|
||||
|
|
@ -100,6 +100,16 @@ export default {
|
|||
INPUT_EV_TOGGLE_PREVIEW,
|
||||
INPUT_EV_TOGGLE_SOUNDS,
|
||||
|
||||
INPUT_EV_REPLAY_TOGGLE_PAUSE,
|
||||
INPUT_EV_REPLAY_SPEED_UP,
|
||||
INPUT_EV_REPLAY_SPEED_DOWN,
|
||||
|
||||
INPUT_EV_TOGGLE_PLAYER_NAMES,
|
||||
INPUT_EV_CENTER_FIT_PUZZLE,
|
||||
|
||||
INPUT_EV_TOGGLE_FIXED_PIECES,
|
||||
INPUT_EV_TOGGLE_LOOSE_PIECES,
|
||||
|
||||
CHANGE_DATA,
|
||||
CHANGE_TILE,
|
||||
CHANGE_PLAYER,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export type EncodedGame = FixedLengthArray<[
|
|||
ScoreMode,
|
||||
ShapeMode,
|
||||
SnapMode,
|
||||
number|null,
|
||||
]>
|
||||
|
||||
export interface ReplayData {
|
||||
|
|
@ -72,12 +73,13 @@ interface GameRng {
|
|||
|
||||
export interface Game {
|
||||
id: string
|
||||
creatorUserId: number|null
|
||||
players: Array<EncodedPlayer>
|
||||
puzzle: Puzzle
|
||||
evtInfos: Record<string, EvtInfo>
|
||||
scoreMode?: ScoreMode
|
||||
shapeMode?: ShapeMode
|
||||
snapMode?: SnapMode
|
||||
scoreMode: ScoreMode
|
||||
shapeMode: ShapeMode
|
||||
snapMode: SnapMode
|
||||
rng: GameRng
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +95,7 @@ export interface Image {
|
|||
|
||||
export interface GameSettings {
|
||||
tiles: number
|
||||
image: Image
|
||||
image: ImageInfo
|
||||
scoreMode: ScoreMode
|
||||
shapeMode: ShapeMode
|
||||
snapMode: SnapMode
|
||||
|
|
@ -152,10 +154,24 @@ export interface PieceChange {
|
|||
group?: number
|
||||
}
|
||||
|
||||
export interface ImageInfo
|
||||
{
|
||||
id: number
|
||||
uploaderUserId: number|null
|
||||
filename: string
|
||||
url: string
|
||||
title: string
|
||||
tags: Tag[]
|
||||
created: Timestamp
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface PuzzleInfo {
|
||||
table: PuzzleTable
|
||||
targetTiles: number,
|
||||
imageUrl: string
|
||||
targetTiles: number
|
||||
imageUrl?: string // deprecated, use image.url instead
|
||||
image?: ImageInfo
|
||||
|
||||
width: number
|
||||
height: number
|
||||
|
|
@ -216,3 +232,24 @@ export enum SnapMode {
|
|||
NORMAL = 0,
|
||||
REAL = 1,
|
||||
}
|
||||
|
||||
export const DefaultScoreMode = (v: any): ScoreMode => {
|
||||
if (v === ScoreMode.FINAL || v === ScoreMode.ANY) {
|
||||
return v
|
||||
}
|
||||
return ScoreMode.FINAL
|
||||
}
|
||||
|
||||
export const DefaultShapeMode = (v: any): ShapeMode => {
|
||||
if (v === ShapeMode.NORMAL || v === ShapeMode.ANY || v === ShapeMode.FLAT) {
|
||||
return v
|
||||
}
|
||||
return ShapeMode.NORMAL
|
||||
}
|
||||
|
||||
export const DefaultSnapMode = (v: any): SnapMode => {
|
||||
if (v === SnapMode.NORMAL || v === SnapMode.REAL) {
|
||||
return v
|
||||
}
|
||||
return SnapMode.NORMAL
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,9 +130,10 @@ function encodeGame(data: Game): EncodedGame {
|
|||
data.puzzle,
|
||||
data.players,
|
||||
data.evtInfos,
|
||||
data.scoreMode || ScoreMode.FINAL,
|
||||
data.shapeMode || ShapeMode.ANY,
|
||||
data.snapMode || SnapMode.NORMAL,
|
||||
data.scoreMode,
|
||||
data.shapeMode,
|
||||
data.snapMode,
|
||||
data.creatorUserId,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +150,7 @@ function decodeGame(data: EncodedGame): Game {
|
|||
scoreMode: data[6],
|
||||
shapeMode: data[7],
|
||||
snapMode: data[8],
|
||||
creatorUserId: data[9],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
22
src/dbpatches/02_image_sizes.sqlite
Normal file
22
src/dbpatches/02_image_sizes.sqlite
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- Add width/height to images table
|
||||
|
||||
CREATE TABLE images_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
created TIMESTAMP NOT NULL,
|
||||
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
filename_original TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
|
||||
width INTEGER NOT NULL,
|
||||
height INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO images_new
|
||||
SELECT id, created, filename, filename_original, title, 0, 0
|
||||
FROM images;
|
||||
|
||||
DROP TABLE images;
|
||||
|
||||
ALTER TABLE images_new RENAME TO images;
|
||||
45
src/dbpatches/03_users.sqlite
Normal file
45
src/dbpatches/03_users.sqlite
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
created TIMESTAMP NOT NULL,
|
||||
|
||||
client_id TEXT NOT NULL,
|
||||
client_secret TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE images_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uploader_user_id INTEGER,
|
||||
|
||||
created TIMESTAMP NOT NULL,
|
||||
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
filename_original TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
|
||||
width INTEGER NOT NULL,
|
||||
height INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE image_x_category_new (
|
||||
image_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO images_new
|
||||
SELECT id, NULL, created, filename, filename_original, title, width, height
|
||||
FROM images;
|
||||
|
||||
INSERT INTO image_x_category_new
|
||||
SELECT image_id, category_id
|
||||
FROM image_x_category;
|
||||
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
DROP TABLE images;
|
||||
DROP TABLE image_x_category;
|
||||
|
||||
ALTER TABLE images_new RENAME TO images;
|
||||
ALTER TABLE image_x_category_new RENAME TO image_x_category;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
11
src/dbpatches/04_games.sqlite
Normal file
11
src/dbpatches/04_games.sqlite
Normal 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
|
||||
);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<ul class="nav" v-if="showNav">
|
||||
<li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li>
|
||||
<li><router-link class="btn" :to="{name: 'index'}">Games overview</router-link></li>
|
||||
<li><router-link class="btn" :to="{name: 'new-game'}">New game</router-link></li>
|
||||
</ul>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ export default function Camera () {
|
|||
let y = 0
|
||||
let curZoom = 1
|
||||
|
||||
const reset = () => {
|
||||
x = 0
|
||||
y = 0
|
||||
curZoom = 1
|
||||
}
|
||||
|
||||
const move = (byX: number, byY: number) => {
|
||||
x += byX / curZoom
|
||||
y += byY / curZoom
|
||||
|
|
@ -130,9 +136,11 @@ export default function Camera () {
|
|||
|
||||
return {
|
||||
getCurrentZoom: () => curZoom,
|
||||
reset,
|
||||
move,
|
||||
canZoom,
|
||||
zoom,
|
||||
setZoom,
|
||||
worldToViewport,
|
||||
worldToViewportRaw,
|
||||
worldDimToViewport, // not used outside
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { ClientEvent, EncodedGame, GameEvent, ReplayData, ServerEvent } from '../common/Types'
|
||||
import Util, { logger } from '../common/Util'
|
||||
import Protocol from './../common/Protocol'
|
||||
import xhr from './xhr'
|
||||
|
||||
const log = logger('Communication.js')
|
||||
|
||||
|
|
@ -112,61 +113,12 @@ function connect(
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
let onReplayDataCallback = (logEntry: any[]) => {}
|
||||
const onReplayData = (fn: any) => {
|
||||
onReplayDataCallback = fn
|
||||
}
|
||||
function connectReplay(
|
||||
address: string,
|
||||
gameId: string,
|
||||
clientId: string
|
||||
): Promise<EncodedGame> {
|
||||
clientSeq = 0
|
||||
events = {}
|
||||
setConnectionState(CONN_STATE_CONNECTING)
|
||||
return new Promise(resolve => {
|
||||
ws = new WebSocket(address, clientId + '|' + gameId)
|
||||
ws.onopen = () => {
|
||||
setConnectionState(CONN_STATE_CONNECTED)
|
||||
send([Protocol.EV_CLIENT_INIT_REPLAY])
|
||||
}
|
||||
|
||||
ws.onmessage = (e: MessageEvent) => {
|
||||
const msg: ServerEvent = JSON.parse(e.data)
|
||||
const msgType = msg[0]
|
||||
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
|
||||
const game = msg[1]
|
||||
resolve(game)
|
||||
} else if (msgType === Protocol.EV_SERVER_REPLAY_EVENT) {
|
||||
onReplayDataCallback(msg[1])
|
||||
} else {
|
||||
throw `[ 2021-06-05 invalid connectReplay msgType ${msgType} ]`
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
setConnectionState(CONN_STATE_DISCONNECTED)
|
||||
throw `[ 2021-05-15 onerror ]`
|
||||
}
|
||||
|
||||
ws.onclose = (e: CloseEvent) => {
|
||||
if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) {
|
||||
setConnectionState(CONN_STATE_CLOSED)
|
||||
} else {
|
||||
setConnectionState(CONN_STATE_DISCONNECTED)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function requestReplayData(
|
||||
gameId: string,
|
||||
offset: number,
|
||||
size: number
|
||||
offset: number
|
||||
): Promise<ReplayData> {
|
||||
const args = { gameId, offset, size }
|
||||
const res = await fetch(`/api/replay-data${Util.asQueryArgs(args)}`)
|
||||
const args = { gameId, offset }
|
||||
const res = await xhr.get(`/api/replay-data${Util.asQueryArgs(args)}`, {})
|
||||
const json: ReplayData = await res.json()
|
||||
return json
|
||||
}
|
||||
|
|
@ -187,17 +139,7 @@ function sendClientEvent(evt: GameEvent): void {
|
|||
send([Protocol.EV_CLIENT_EVENT, clientSeq, events[clientSeq]])
|
||||
}
|
||||
|
||||
function requestMoreReplayData(): void {
|
||||
send([Protocol.EV_CLIENT_REPLAY_EVENT])
|
||||
}
|
||||
|
||||
export default {
|
||||
connectReplay,
|
||||
requestMoreReplayData,
|
||||
onReplayData,
|
||||
|
||||
|
||||
|
||||
connect,
|
||||
requestReplayData,
|
||||
disconnect,
|
||||
|
|
|
|||
177
src/frontend/EventAdapter.ts
Normal file
177
src/frontend/EventAdapter.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import Protocol from "../common/Protocol"
|
||||
import { GameEvent } from "../common/Types"
|
||||
import { MODE_REPLAY } from "./game"
|
||||
|
||||
function EventAdapter (
|
||||
canvas: HTMLCanvasElement,
|
||||
window: any,
|
||||
viewport: any,
|
||||
MODE: string
|
||||
) {
|
||||
let events: Array<GameEvent> = []
|
||||
|
||||
let KEYS_ON = true
|
||||
|
||||
let LEFT = false
|
||||
let RIGHT = false
|
||||
let UP = false
|
||||
let DOWN = false
|
||||
let ZOOM_IN = false
|
||||
let ZOOM_OUT = false
|
||||
let SHIFT = false
|
||||
|
||||
const toWorldPoint = (x: number, y: number): [number, number] => {
|
||||
const pos = viewport.viewportToWorld({x, y})
|
||||
return [pos.x, pos.y]
|
||||
}
|
||||
|
||||
const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
|
||||
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
|
||||
|
||||
const key = (state: boolean, ev: KeyboardEvent) => {
|
||||
if (!KEYS_ON) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ev.code === 'ShiftLeft' || ev.code === 'ShiftRight') {
|
||||
SHIFT = state
|
||||
} else if (ev.code === 'ArrowUp' || ev.code === 'KeyW') {
|
||||
UP = state
|
||||
} else if (ev.code === 'ArrowDown' || ev.code === 'KeyS') {
|
||||
DOWN = state
|
||||
} else if (ev.code === 'ArrowLeft' || ev.code === 'KeyA') {
|
||||
LEFT = state
|
||||
} else if (ev.code === 'ArrowRight' || ev.code === 'KeyD') {
|
||||
RIGHT = state
|
||||
} else if (ev.code === 'KeyQ') {
|
||||
ZOOM_OUT = state
|
||||
} else if (ev.code === 'KeyE') {
|
||||
ZOOM_IN = state
|
||||
}
|
||||
}
|
||||
|
||||
let lastMouse: [number, number]|null = null
|
||||
canvas.addEventListener('mousedown', (ev) => {
|
||||
lastMouse = mousePos(ev)
|
||||
if (ev.button === 0) {
|
||||
addEvent([Protocol.INPUT_EV_MOUSE_DOWN, ...lastMouse])
|
||||
}
|
||||
})
|
||||
|
||||
canvas.addEventListener('mouseup', (ev) => {
|
||||
lastMouse = mousePos(ev)
|
||||
if (ev.button === 0) {
|
||||
addEvent([Protocol.INPUT_EV_MOUSE_UP, ...lastMouse])
|
||||
}
|
||||
})
|
||||
|
||||
canvas.addEventListener('mousemove', (ev) => {
|
||||
lastMouse = mousePos(ev)
|
||||
addEvent([Protocol.INPUT_EV_MOUSE_MOVE, ...lastMouse])
|
||||
})
|
||||
|
||||
canvas.addEventListener('wheel', (ev) => {
|
||||
lastMouse = mousePos(ev)
|
||||
if (viewport.canZoom(ev.deltaY < 0 ? 'in' : 'out')) {
|
||||
const evt = ev.deltaY < 0
|
||||
? Protocol.INPUT_EV_ZOOM_IN
|
||||
: Protocol.INPUT_EV_ZOOM_OUT
|
||||
addEvent([evt, ...lastMouse])
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
|
||||
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
|
||||
|
||||
window.addEventListener('keypress', (ev: KeyboardEvent) => {
|
||||
if (!KEYS_ON) {
|
||||
return
|
||||
}
|
||||
if (ev.code === 'Space') {
|
||||
addEvent([Protocol.INPUT_EV_TOGGLE_PREVIEW])
|
||||
}
|
||||
|
||||
if (MODE === MODE_REPLAY) {
|
||||
if (ev.code === 'KeyI') {
|
||||
addEvent([Protocol.INPUT_EV_REPLAY_SPEED_UP])
|
||||
}
|
||||
|
||||
if (ev.code === 'KeyO') {
|
||||
addEvent([Protocol.INPUT_EV_REPLAY_SPEED_DOWN])
|
||||
}
|
||||
|
||||
if (ev.code === 'KeyP') {
|
||||
addEvent([Protocol.INPUT_EV_REPLAY_TOGGLE_PAUSE])
|
||||
}
|
||||
}
|
||||
if (ev.code === 'KeyF') {
|
||||
addEvent([Protocol.INPUT_EV_TOGGLE_FIXED_PIECES])
|
||||
}
|
||||
if (ev.code === 'KeyG') {
|
||||
addEvent([Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES])
|
||||
}
|
||||
if (ev.code === 'KeyM') {
|
||||
addEvent([Protocol.INPUT_EV_TOGGLE_SOUNDS])
|
||||
}
|
||||
if (ev.code === 'KeyN') {
|
||||
addEvent([Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES])
|
||||
}
|
||||
if (ev.code === 'KeyC') {
|
||||
addEvent([Protocol.INPUT_EV_CENTER_FIT_PUZZLE])
|
||||
}
|
||||
})
|
||||
|
||||
const addEvent = (event: GameEvent) => {
|
||||
events.push(event)
|
||||
}
|
||||
|
||||
const consumeAll = (): GameEvent[] => {
|
||||
if (events.length === 0) {
|
||||
return []
|
||||
}
|
||||
const all = events.slice()
|
||||
events = []
|
||||
return all
|
||||
}
|
||||
|
||||
const createKeyEvents = (): void => {
|
||||
const w = (LEFT ? 1 : 0) - (RIGHT ? 1 : 0)
|
||||
const h = (UP ? 1 : 0) - (DOWN ? 1 : 0)
|
||||
if (w !== 0 || h !== 0) {
|
||||
const amount = (SHIFT ? 24 : 12) * Math.sqrt(viewport.getCurrentZoom())
|
||||
const pos = viewport.viewportDimToWorld({w: w * amount, h: h * amount})
|
||||
addEvent([Protocol.INPUT_EV_MOVE, pos.w, pos.h])
|
||||
if (lastMouse) {
|
||||
lastMouse[0] -= pos.w
|
||||
lastMouse[1] -= pos.h
|
||||
}
|
||||
}
|
||||
|
||||
if (ZOOM_IN && ZOOM_OUT) {
|
||||
// cancel each other out
|
||||
} else if (ZOOM_IN) {
|
||||
if (viewport.canZoom('in')) {
|
||||
const target = lastMouse || canvasCenter()
|
||||
addEvent([Protocol.INPUT_EV_ZOOM_IN, ...target])
|
||||
}
|
||||
} else if (ZOOM_OUT) {
|
||||
if (viewport.canZoom('out')) {
|
||||
const target = lastMouse || canvasCenter()
|
||||
addEvent([Protocol.INPUT_EV_ZOOM_OUT, ...target])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setHotkeys = (state: boolean) => {
|
||||
KEYS_ON = state
|
||||
}
|
||||
|
||||
return {
|
||||
addEvent,
|
||||
consumeAll,
|
||||
createKeyEvents,
|
||||
setHotkeys,
|
||||
}
|
||||
}
|
||||
|
||||
export default EventAdapter
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import Geometry, { Rect } from '../common/Geometry'
|
||||
import Graphics from './Graphics'
|
||||
import Util, { logger } from './../common/Util'
|
||||
import { Puzzle, PuzzleInfo, PieceShape, EncodedPiece } from './../common/GameCommon'
|
||||
import { Puzzle, PuzzleInfo, PieceShape, EncodedPiece } from './../common/Types'
|
||||
|
||||
const log = logger('PuzzleGraphics.js')
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,17 @@ export default defineComponent({
|
|||
height: 90%;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) and (min-height: 720px),
|
||||
(max-width: 1000px) {
|
||||
.edit-image-dialog .overlay-content {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: 1fr min-content min-content;
|
||||
grid-template-areas:
|
||||
"image"
|
||||
"settings"
|
||||
"buttons";
|
||||
}
|
||||
}
|
||||
.edit-image-dialog .area-image {
|
||||
grid-area: image;
|
||||
margin: 20px;
|
||||
|
|
|
|||
|
|
@ -10,9 +10,17 @@
|
|||
<tr><td>🔍+ Zoom in:</td><td><div><kbd>E</kbd>/🖱️-Wheel</div></td></tr>
|
||||
<tr><td>🔍- Zoom out:</td><td><div><kbd>Q</kbd>/🖱️-Wheel</div></td></tr>
|
||||
<tr><td>🖼️ Toggle preview:</td><td><div><kbd>Space</kbd></div></td></tr>
|
||||
<tr><td>🎯 Center puzzle in screen:</td><td><div><kbd>C</kbd></div></td></tr>
|
||||
|
||||
<tr><td>🧩✔️ Toggle fixed pieces:</td><td><div><kbd>F</kbd></div></td></tr>
|
||||
<tr><td>🧩❓ Toggle loose pieces:</td><td><div><kbd>G</kbd></div></td></tr>
|
||||
|
||||
<tr><td>👤 Toggle player names:</td><td><div><kbd>N</kbd></div></td></tr>
|
||||
<tr><td>🔉 Toggle sounds:</td><td><div><kbd>M</kbd></div></td></tr>
|
||||
|
||||
<tr><td>⏫ Speed up (replay):</td><td><div><kbd>I</kbd></div></td></tr>
|
||||
<tr><td>⏬ Speed down (replay):</td><td><div><kbd>O</kbd></div></td></tr>
|
||||
<tr><td>⏸️ Pause (replay):</td><td><div><kbd>P</kbd></div></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class="imageteaser"
|
||||
:style="style"
|
||||
@click="onClick">
|
||||
<div class="btn edit" @click.stop="onEditClick">✏️</div>
|
||||
<div class="btn edit" v-if="canEdit" @click.stop="onEditClick">✏️</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
|
@ -18,12 +18,18 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
style (): object {
|
||||
style(): object {
|
||||
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
|
||||
return {
|
||||
'backgroundImage': `url("${url}")`,
|
||||
}
|
||||
},
|
||||
canEdit(): boolean {
|
||||
if (!this.$me.id) {
|
||||
return false
|
||||
}
|
||||
return this.$me.id === this.image.uploaderUserId
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
click: null,
|
||||
|
|
|
|||
68
src/frontend/components/InfoOverlay.vue
Normal file
68
src/frontend/components/InfoOverlay.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div class="overlay transparent" @click="$emit('bgclick')">
|
||||
<table class="overlay-content help" @click.stop="">
|
||||
<tr>
|
||||
<td colspan="2">Info about this puzzle</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Image Title: </td>
|
||||
<td>{{game.puzzle.info.image.title}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scoring: </td>
|
||||
<td><span :title="snapMode[1]">{{scoreMode[0]}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Shapes: </td>
|
||||
<td><span :title="snapMode[1]">{{shapeMode[0]}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Snapping: </td>
|
||||
<td><span :title="snapMode[1]">{{snapMode[0]}}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { Game, ScoreMode, ShapeMode, SnapMode } from '../../common/Types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'help-overlay',
|
||||
emits: {
|
||||
bgclick: null,
|
||||
},
|
||||
props: {
|
||||
game: {
|
||||
type: Object as PropType<Game>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
scoreMode () {
|
||||
switch (this.game.scoreMode) {
|
||||
case ScoreMode.ANY: return ['Any', 'Score when pieces are connected to each other or on final location']
|
||||
case ScoreMode.FINAL:
|
||||
default: return ['Final', 'Score when pieces are put to their final location']
|
||||
}
|
||||
},
|
||||
shapeMode () {
|
||||
switch (this.game.shapeMode) {
|
||||
case ShapeMode.FLAT: return ['Flat', 'All pieces flat on all sides']
|
||||
case ShapeMode.ANY: return ['Any', 'Flat pieces can occur anywhere']
|
||||
case ShapeMode.NORMAL:
|
||||
default:
|
||||
return ['Normal', '']
|
||||
}
|
||||
},
|
||||
snapMode () {
|
||||
switch (this.game.snapMode) {
|
||||
case SnapMode.REAL: return ['Real', 'Pieces snap only to corners, already snapped pieces and to each other']
|
||||
case SnapMode.NORMAL:
|
||||
default:
|
||||
return ['Normal', 'Pieces snap to final destination and to each other']
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -6,7 +6,10 @@
|
|||
<div class="has-image">
|
||||
<responsive-image :src="image.url" :title="image.title" />
|
||||
</div>
|
||||
<div class="image-title" v-if="image.title">"{{image.title}}"</div>
|
||||
<div class="image-title" v-if="image.title || image.width || image.height">
|
||||
<span class="image-title-title" v-if="image.title">"{{image.title}}"</span>
|
||||
<span class="image-title-dim" v-if="image.width || image.height">({{image.width}} ✕ {{image.height}})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="area-settings">
|
||||
|
|
@ -18,27 +21,34 @@
|
|||
<tr>
|
||||
<td><label>Scoring: </label></td>
|
||||
<td>
|
||||
<label><input type="radio" v-model="scoreMode" value="1" /> Any (Score when pieces are connected to each other or on final location)</label>
|
||||
<label><input type="radio" v-model="scoreMode" value="1" />
|
||||
Any (Score when pieces are connected to each other or on final location)</label>
|
||||
<br />
|
||||
<label><input type="radio" v-model="scoreMode" value="0" /> Final (Score when pieces are put to their final location)</label>
|
||||
<label><input type="radio" v-model="scoreMode" value="0" />
|
||||
Final (Score when pieces are put to their final location)</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Shapes: </label></td>
|
||||
<td>
|
||||
<label><input type="radio" v-model="shapeMode" value="0" /> Normal</label>
|
||||
<label><input type="radio" v-model="shapeMode" value="0" />
|
||||
Normal</label>
|
||||
<br />
|
||||
<label><input type="radio" v-model="shapeMode" value="1" /> Any (flat pieces can occur anywhere)</label>
|
||||
<label><input type="radio" v-model="shapeMode" value="1" />
|
||||
Any (Flat pieces can occur anywhere)</label>
|
||||
<br />
|
||||
<label><input type="radio" v-model="shapeMode" value="2" /> Flat (all pieces flat on all sides)</label>
|
||||
<label><input type="radio" v-model="shapeMode" value="2" />
|
||||
Flat (All pieces flat on all sides)</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Snapping: </label></td>
|
||||
<td>
|
||||
<label><input type="radio" v-model="snapMode" value="0" /> Normal (pieces snap to final destination and to each other)</label>
|
||||
<label><input type="radio" v-model="snapMode" value="0" />
|
||||
Normal (Pieces snap to final destination and to each other)</label>
|
||||
<br />
|
||||
<label><input type="radio" v-model="snapMode" value="1" /> Real (pieces snap only to corners, already snapped pieces and to each other)</label>
|
||||
<label><input type="radio" v-model="snapMode" value="1" />
|
||||
Real (Pieces snap only to corners, already snapped pieces and to each other)</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -142,7 +152,8 @@ export default defineComponent({
|
|||
"image-title";
|
||||
margin-right: 1em;
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
@media (max-width: 1400px) and (min-height: 720px),
|
||||
(max-width: 1000px) {
|
||||
.new-game-dialog .overlay-content {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: 1fr min-content min-content;
|
||||
|
|
@ -192,4 +203,8 @@ export default defineComponent({
|
|||
top: .5em;
|
||||
left: .5em;
|
||||
}
|
||||
|
||||
.new-game-dialog .image-title > span { margin-right: .5em; }
|
||||
.new-game-dialog .image-title > span:last-child { margin-right: 0; }
|
||||
.image-title-dim { display: inline-block; white-space: no-wrap; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ gallery", if possible!
|
|||
@dragover="onDragover"
|
||||
@dragleave="onDragleave">
|
||||
<!-- TODO: ... -->
|
||||
<div class="drop-target"></div>
|
||||
<div v-if="previewUrl" class="has-image">
|
||||
<span class="remove btn" @click="previewUrl=''">X</span>
|
||||
<responsive-image :src="previewUrl" />
|
||||
|
|
@ -48,10 +49,21 @@ gallery", if possible!
|
|||
</div>
|
||||
|
||||
<div class="area-buttons">
|
||||
<button class="btn" :disabled="!canPostToGallery" @click="postToGallery">🖼️ Post to gallery</button>
|
||||
<button class="btn" :disabled="!canSetupGameClick" @click="setupGameClick">🧩 Post to gallery <br /> + set up game</button>
|
||||
<button class="btn"
|
||||
:disabled="!canPostToGallery"
|
||||
@click="postToGallery"
|
||||
>
|
||||
<template v-if="uploading === 'postToGallery'">Uploading ({{uploadProgressPercent}}%)</template>
|
||||
<template v-else>🖼️ Post to gallery</template>
|
||||
</button>
|
||||
<button class="btn"
|
||||
:disabled="!canSetupGameClick"
|
||||
@click="setupGameClick"
|
||||
>
|
||||
<template v-if="uploading === 'setupGame'">Uploading ({{uploadProgressPercent}}%)</template>
|
||||
<template v-else>🧩 Post to gallery <br /> + set up game</template>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -74,6 +86,12 @@ export default defineComponent({
|
|||
autocompleteTags: {
|
||||
type: Function,
|
||||
},
|
||||
uploadProgress: {
|
||||
type: Number,
|
||||
},
|
||||
uploading: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
bgclick: null,
|
||||
|
|
@ -90,10 +108,19 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
uploadProgressPercent (): number {
|
||||
return this.uploadProgress ? Math.round(this.uploadProgress * 100) : 0
|
||||
},
|
||||
canPostToGallery (): boolean {
|
||||
if (this.uploading) {
|
||||
return false
|
||||
}
|
||||
return !!(this.previewUrl && this.file)
|
||||
},
|
||||
canSetupGameClick (): boolean {
|
||||
if (this.uploading) {
|
||||
return false
|
||||
}
|
||||
return !!(this.previewUrl && this.file)
|
||||
},
|
||||
},
|
||||
|
|
@ -183,7 +210,8 @@ export default defineComponent({
|
|||
height: 90%;
|
||||
width: 80%;
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
@media (max-width: 1400px) and (min-height: 720px),
|
||||
(max-width: 1000px) {
|
||||
.new-image-dialog .overlay-content {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: 1fr min-content min-content;
|
||||
|
|
@ -212,9 +240,6 @@ export default defineComponent({
|
|||
.new-image-dialog .area-image.droppable {
|
||||
border: dashed 6px;
|
||||
}
|
||||
.area-image * {
|
||||
pointer-events: none;
|
||||
}
|
||||
.new-image-dialog .area-image .has-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
@ -256,4 +281,16 @@ export default defineComponent({
|
|||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
.area-image .drop-target {
|
||||
display: none;
|
||||
}
|
||||
.area-image.droppable .drop-target {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,24 @@
|
|||
<td><label>Sounds: </label></td>
|
||||
<td><input type="checkbox" v-model="modelValue.soundsEnabled" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Sounds Volume: </label></td>
|
||||
<td class="sound-volume">
|
||||
<span @click="decreaseVolume">🔉</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
:value="modelValue.soundsVolume"
|
||||
@change="updateVolume"
|
||||
/>
|
||||
<span @click="increaseVolume">🔊</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Show player names: </label></td>
|
||||
<td><input type="checkbox" v-model="modelValue.showPlayerNames" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -30,7 +48,23 @@ export default defineComponent({
|
|||
'update:modelValue': null,
|
||||
},
|
||||
props: {
|
||||
modelValue: Object,
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateVolume (ev: Event): void {
|
||||
(this.modelValue as any).soundsVolume = (ev.target as HTMLInputElement).value
|
||||
},
|
||||
decreaseVolume (): void {
|
||||
const vol = parseInt(this.modelValue.soundsVolume, 10) - 5
|
||||
this.modelValue.soundsVolume = Math.max(0, vol)
|
||||
},
|
||||
increaseVolume (): void {
|
||||
const vol = parseInt(this.modelValue.soundsVolume, 10) + 5
|
||||
this.modelValue.soundsVolume = Math.min(100, vol)
|
||||
},
|
||||
},
|
||||
created () {
|
||||
// TODO: ts type PlayerSettings
|
||||
|
|
@ -40,3 +74,7 @@ export default defineComponent({
|
|||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.sound-volume span { cursor: pointer; user-select: none; }
|
||||
.sound-volume input { vertical-align: middle; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -56,14 +56,14 @@ export default defineComponent({
|
|||
},
|
||||
methods: {
|
||||
onKeyUp (ev: KeyboardEvent) {
|
||||
if (ev.key === 'ArrowDown' && this.autocomplete.values.length > 0) {
|
||||
if (ev.code === 'ArrowDown' && this.autocomplete.values.length > 0) {
|
||||
if (this.autocomplete.idx < this.autocomplete.values.length - 1) {
|
||||
this.autocomplete.idx++
|
||||
}
|
||||
ev.stopPropagation()
|
||||
return false
|
||||
}
|
||||
if (ev.key === 'ArrowUp' && this.autocomplete.values.length > 0) {
|
||||
if (ev.code === 'ArrowUp' && this.autocomplete.values.length > 0) {
|
||||
if (this.autocomplete.idx > 0) {
|
||||
this.autocomplete.idx--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import xhr from '../xhr'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'upload',
|
||||
|
|
@ -21,8 +22,7 @@ export default defineComponent({
|
|||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
const res = await fetch('/upload', {
|
||||
method: 'post',
|
||||
const res = await xhr.post('/upload', {
|
||||
body: formData,
|
||||
})
|
||||
const j = await res.json()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import Game from './../common/GameCommon'
|
|||
import fireworksController from './Fireworks'
|
||||
import Protocol from '../common/Protocol'
|
||||
import Time from '../common/Time'
|
||||
import settings from './settings'
|
||||
import { SETTINGS } from './settings'
|
||||
import { Dim, Point } from '../common/Geometry'
|
||||
import {
|
||||
FixedLengthArray,
|
||||
|
|
@ -20,9 +22,9 @@ import {
|
|||
EncodedGame,
|
||||
ReplayData,
|
||||
Timestamp,
|
||||
GameEvent,
|
||||
ServerEvent,
|
||||
} from '../common/Types'
|
||||
import EventAdapter from './EventAdapter'
|
||||
declare global {
|
||||
interface Window {
|
||||
DEBUG?: boolean
|
||||
|
|
@ -44,6 +46,7 @@ let PIECE_VIEW_FIXED = true
|
|||
let PIECE_VIEW_LOOSE = true
|
||||
|
||||
interface Hud {
|
||||
setPuzzleCut: () => void
|
||||
setActivePlayers: (v: Array<any>) => void
|
||||
setIdlePlayers: (v: Array<any>) => void
|
||||
setFinished: (v: boolean) => void
|
||||
|
|
@ -53,6 +56,7 @@ interface Hud {
|
|||
setConnectionState: (v: number) => void
|
||||
togglePreview: () => void
|
||||
toggleSoundsEnabled: () => void
|
||||
togglePlayerNames: () => void
|
||||
setReplaySpeed?: (v: number) => void
|
||||
setReplayPaused?: (v: boolean) => void
|
||||
}
|
||||
|
|
@ -69,7 +73,6 @@ interface Replay {
|
|||
skipNonActionPhases: boolean
|
||||
//
|
||||
dataOffset: number
|
||||
dataSize: number
|
||||
}
|
||||
|
||||
const shouldDrawPiece = (piece: Piece) => {
|
||||
|
|
@ -93,154 +96,6 @@ function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
|
|||
return canvas
|
||||
}
|
||||
|
||||
function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
|
||||
let events: Array<GameEvent> = []
|
||||
|
||||
let KEYS_ON = true
|
||||
|
||||
let LEFT = false
|
||||
let RIGHT = false
|
||||
let UP = false
|
||||
let DOWN = false
|
||||
let ZOOM_IN = false
|
||||
let ZOOM_OUT = false
|
||||
let SHIFT = false
|
||||
|
||||
const toWorldPoint = (x: number, y: number): [number, number] => {
|
||||
const pos = viewport.viewportToWorld({x, y})
|
||||
return [pos.x, pos.y]
|
||||
}
|
||||
|
||||
const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
|
||||
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
|
||||
|
||||
const key = (state: boolean, ev: KeyboardEvent) => {
|
||||
if (!KEYS_ON) {
|
||||
return
|
||||
}
|
||||
if (ev.key === 'Shift') {
|
||||
SHIFT = state
|
||||
} else if (ev.key === 'ArrowUp' || ev.key === 'w' || ev.key === 'W') {
|
||||
UP = state
|
||||
} else if (ev.key === 'ArrowDown' || ev.key === 's' || ev.key === 'S') {
|
||||
DOWN = state
|
||||
} else if (ev.key === 'ArrowLeft' || ev.key === 'a' || ev.key === 'A') {
|
||||
LEFT = state
|
||||
} else if (ev.key === 'ArrowRight' || ev.key === 'd' || ev.key === 'D') {
|
||||
RIGHT = state
|
||||
} else if (ev.key === 'q') {
|
||||
ZOOM_OUT = state
|
||||
} else if (ev.key === 'e') {
|
||||
ZOOM_IN = state
|
||||
}
|
||||
}
|
||||
|
||||
let lastMouse: [number, number]|null = null
|
||||
canvas.addEventListener('mousedown', (ev) => {
|
||||
lastMouse = mousePos(ev)
|
||||
if (ev.button === 0) {
|
||||
addEvent([Protocol.INPUT_EV_MOUSE_DOWN, ...lastMouse])
|
||||
}
|
||||
})
|
||||
|
||||
canvas.addEventListener('mouseup', (ev) => {
|
||||
lastMouse = mousePos(ev)
|
||||
if (ev.button === 0) {
|
||||
addEvent([Protocol.INPUT_EV_MOUSE_UP, ...lastMouse])
|
||||
}
|
||||
})
|
||||
|
||||
canvas.addEventListener('mousemove', (ev) => {
|
||||
lastMouse = mousePos(ev)
|
||||
addEvent([Protocol.INPUT_EV_MOUSE_MOVE, ...lastMouse])
|
||||
})
|
||||
|
||||
canvas.addEventListener('wheel', (ev) => {
|
||||
lastMouse = mousePos(ev)
|
||||
if (viewport.canZoom(ev.deltaY < 0 ? 'in' : 'out')) {
|
||||
const evt = ev.deltaY < 0
|
||||
? Protocol.INPUT_EV_ZOOM_IN
|
||||
: Protocol.INPUT_EV_ZOOM_OUT
|
||||
addEvent([evt, ...lastMouse])
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
|
||||
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
|
||||
|
||||
window.addEventListener('keypress', (ev: KeyboardEvent) => {
|
||||
if (!KEYS_ON) {
|
||||
return
|
||||
}
|
||||
if (ev.key === ' ') {
|
||||
addEvent([Protocol.INPUT_EV_TOGGLE_PREVIEW])
|
||||
}
|
||||
if (ev.key === 'F' || ev.key === 'f') {
|
||||
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
|
||||
RERENDER = true
|
||||
}
|
||||
if (ev.key === 'G' || ev.key === 'g') {
|
||||
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
|
||||
RERENDER = true
|
||||
}
|
||||
if (ev.key === 'M' || ev.key === 'm') {
|
||||
addEvent([Protocol.INPUT_EV_TOGGLE_SOUNDS])
|
||||
}
|
||||
})
|
||||
|
||||
const addEvent = (event: GameEvent) => {
|
||||
events.push(event)
|
||||
}
|
||||
|
||||
const consumeAll = (): GameEvent[] => {
|
||||
if (events.length === 0) {
|
||||
return []
|
||||
}
|
||||
const all = events.slice()
|
||||
events = []
|
||||
return all
|
||||
}
|
||||
|
||||
const createKeyEvents = (): void => {
|
||||
const w = (LEFT ? 1 : 0) - (RIGHT ? 1 : 0)
|
||||
const h = (UP ? 1 : 0) - (DOWN ? 1 : 0)
|
||||
if (w !== 0 || h !== 0) {
|
||||
const amount = (SHIFT ? 24 : 12) * Math.sqrt(viewport.getCurrentZoom())
|
||||
const pos = viewport.viewportDimToWorld({w: w * amount, h: h * amount})
|
||||
addEvent([Protocol.INPUT_EV_MOVE, pos.w, pos.h])
|
||||
if (lastMouse) {
|
||||
lastMouse[0] -= pos.w
|
||||
lastMouse[1] -= pos.h
|
||||
}
|
||||
}
|
||||
|
||||
if (ZOOM_IN && ZOOM_OUT) {
|
||||
// cancel each other out
|
||||
} else if (ZOOM_IN) {
|
||||
if (viewport.canZoom('in')) {
|
||||
const target = lastMouse || canvasCenter()
|
||||
addEvent([Protocol.INPUT_EV_ZOOM_IN, ...target])
|
||||
}
|
||||
} else if (ZOOM_OUT) {
|
||||
if (viewport.canZoom('out')) {
|
||||
const target = lastMouse || canvasCenter()
|
||||
addEvent([Protocol.INPUT_EV_ZOOM_OUT, ...target])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setHotkeys = (state: boolean) => {
|
||||
KEYS_ON = state
|
||||
}
|
||||
|
||||
return {
|
||||
addEvent,
|
||||
consumeAll,
|
||||
createKeyEvents,
|
||||
setHotkeys,
|
||||
}
|
||||
}
|
||||
|
||||
export async function main(
|
||||
gameId: string,
|
||||
clientId: string,
|
||||
|
|
@ -301,15 +156,35 @@ export async function main(
|
|||
lastRealTs: 0,
|
||||
lastGameTs: 0,
|
||||
gameStartTs: 0,
|
||||
skipNonActionPhases: false,
|
||||
skipNonActionPhases: true,
|
||||
dataOffset: 0,
|
||||
dataSize: 10000,
|
||||
}
|
||||
|
||||
Communication.onConnectionStateChange((state) => {
|
||||
HUD.setConnectionState(state)
|
||||
})
|
||||
|
||||
const queryNextReplayBatch = async (
|
||||
gameId: string
|
||||
): Promise<ReplayData> => {
|
||||
const offset = REPLAY.dataOffset
|
||||
REPLAY.dataOffset += 10000 // meh
|
||||
const replay: ReplayData = await Communication.requestReplayData(
|
||||
gameId,
|
||||
offset
|
||||
)
|
||||
|
||||
// cut log that was already handled
|
||||
REPLAY.log = REPLAY.log.slice(REPLAY.logPointer)
|
||||
REPLAY.logPointer = 0
|
||||
REPLAY.log.push(...replay.log)
|
||||
|
||||
if (replay.log.length === 0) {
|
||||
REPLAY.final = true
|
||||
}
|
||||
return replay
|
||||
}
|
||||
|
||||
let TIME: () => number = () => 0
|
||||
const connect = async () => {
|
||||
if (MODE === MODE_PLAY) {
|
||||
|
|
@ -318,38 +193,16 @@ export async function main(
|
|||
Game.setGame(gameObject.id, gameObject)
|
||||
TIME = () => Time.timestamp()
|
||||
} else if (MODE === MODE_REPLAY) {
|
||||
REPLAY.logPointer = 0
|
||||
REPLAY.dataSize = 10000
|
||||
REPLAY.speeds = [0.5, 1, 2, 5, 10, 20, 50, 100, 250, 500]
|
||||
REPLAY.speedIdx = 1
|
||||
|
||||
Communication.onReplayData((logLines: any[][]) => {
|
||||
if (logLines.length === 0) {
|
||||
console.log('MUHHHHH FINAL!!!!')
|
||||
REPLAY.final = true
|
||||
} else {
|
||||
if (REPLAY.logPointer === 0) {
|
||||
REPLAY.log.push(...logLines)
|
||||
} else {
|
||||
REPLAY.log = REPLAY.log.slice(REPLAY.logPointer - 1)
|
||||
REPLAY.logPointer = 1
|
||||
REPLAY.log.push(...logLines)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const game: any = await Communication.connectReplay(wsAddress, gameId, clientId)
|
||||
if (!game) {
|
||||
const replay: ReplayData = await queryNextReplayBatch(gameId)
|
||||
if (!replay.game) {
|
||||
throw '[ 2021-05-29 no game received ]'
|
||||
}
|
||||
const gameObject: GameType = Util.decodeGame(game)
|
||||
const gameObject: GameType = Util.decodeGame(replay.game)
|
||||
Game.setGame(gameObject.id, gameObject)
|
||||
|
||||
REPLAY.lastRealTs = Time.timestamp()
|
||||
REPLAY.gameStartTs = Game.getStartTs(gameId)
|
||||
REPLAY.gameStartTs = parseInt(replay.log[0][4], 10)
|
||||
REPLAY.lastGameTs = REPLAY.gameStartTs
|
||||
REPLAY.paused = false
|
||||
REPLAY.skipNonActionPhases = false
|
||||
|
||||
TIME = () => REPLAY.lastGameTs
|
||||
} else {
|
||||
|
|
@ -389,17 +242,40 @@ export async function main(
|
|||
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||
canvas.classList.add('loaded')
|
||||
HUD.setPuzzleCut()
|
||||
|
||||
// initialize some view data
|
||||
// this global data will change according to input events
|
||||
const viewport = Camera()
|
||||
// center viewport
|
||||
viewport.move(
|
||||
-(TABLE_WIDTH - canvas.width) /2,
|
||||
-(TABLE_HEIGHT - canvas.height) /2
|
||||
)
|
||||
|
||||
const evts = EventAdapter(canvas, window, viewport)
|
||||
const centerPuzzle = () => {
|
||||
// center on the puzzle
|
||||
viewport.reset()
|
||||
viewport.move(
|
||||
-(TABLE_WIDTH - canvas.width) /2,
|
||||
-(TABLE_HEIGHT - canvas.height) /2
|
||||
)
|
||||
|
||||
// zoom viewport to fit whole puzzle in
|
||||
const x = viewport.worldDimToViewport(BOARD_DIM)
|
||||
const border = 20
|
||||
const targetW = canvas.width - (border * 2)
|
||||
const targetH = canvas.height - (border * 2)
|
||||
if (
|
||||
(x.w > targetW || x.h > targetH)
|
||||
|| (x.w < targetW && x.h < targetH)
|
||||
) {
|
||||
const zoom = Math.min(targetW / x.w, targetH / x.h)
|
||||
viewport.setZoom(zoom, {
|
||||
x: canvas.width / 2,
|
||||
y: canvas.height / 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
centerPuzzle()
|
||||
|
||||
const evts = EventAdapter(canvas, window, viewport, MODE)
|
||||
|
||||
const previewImageUrl = Game.getImageUrl(gameId)
|
||||
|
||||
|
|
@ -423,27 +299,42 @@ export async function main(
|
|||
let finished = longFinished
|
||||
const justFinished = () => finished && !longFinished
|
||||
|
||||
const playerSoundEnabled = (): boolean => {
|
||||
const enabled = localStorage.getItem('sound_enabled')
|
||||
if (enabled === null) {
|
||||
return false
|
||||
}
|
||||
return enabled === '1'
|
||||
const playerSoundVolume = (): number => {
|
||||
return settings.getInt(SETTINGS.SOUND_VOLUME, 100)
|
||||
}
|
||||
const playerSoundEnabled = (): boolean => {
|
||||
return settings.getBool(SETTINGS.SOUND_ENABLED, false)
|
||||
}
|
||||
const showPlayerNames = (): boolean => {
|
||||
return settings.getBool(SETTINGS.SHOW_PLAYER_NAMES, true)
|
||||
}
|
||||
|
||||
const playClick = () => {
|
||||
const vol = playerSoundVolume()
|
||||
clickAudio.volume = vol / 100
|
||||
clickAudio.play()
|
||||
}
|
||||
|
||||
const playerBgColor = () => {
|
||||
return (Game.getPlayerBgColor(gameId, clientId)
|
||||
|| localStorage.getItem('bg_color')
|
||||
|| '#222222')
|
||||
if (MODE === MODE_REPLAY) {
|
||||
return settings.getStr(SETTINGS.COLOR_BACKGROUND, '#222222')
|
||||
}
|
||||
return Game.getPlayerBgColor(gameId, clientId)
|
||||
|| settings.getStr(SETTINGS.COLOR_BACKGROUND, '#222222')
|
||||
}
|
||||
const playerColor = () => {
|
||||
return (Game.getPlayerColor(gameId, clientId)
|
||||
|| localStorage.getItem('player_color')
|
||||
|| '#ffffff')
|
||||
if (MODE === MODE_REPLAY) {
|
||||
return settings.getStr(SETTINGS.PLAYER_COLOR, '#ffffff')
|
||||
}
|
||||
return Game.getPlayerColor(gameId, clientId)
|
||||
|| settings.getStr(SETTINGS.PLAYER_COLOR, '#ffffff')
|
||||
}
|
||||
const playerName = () => {
|
||||
return (Game.getPlayerName(gameId, clientId)
|
||||
|| localStorage.getItem('player_name')
|
||||
|| 'anon')
|
||||
if (MODE === MODE_REPLAY) {
|
||||
return settings.getStr(SETTINGS.PLAYER_NAME, 'anon')
|
||||
}
|
||||
return Game.getPlayerName(gameId, clientId)
|
||||
|| settings.getStr(SETTINGS.PLAYER_NAME, 'anon')
|
||||
}
|
||||
|
||||
let cursorDown: string = ''
|
||||
|
|
@ -514,9 +405,6 @@ export async function main(
|
|||
doSetSpeedStatus()
|
||||
}
|
||||
|
||||
// // TODO: remove (make changable via interface)
|
||||
REPLAY.skipNonActionPhases = true
|
||||
|
||||
if (MODE === MODE_PLAY) {
|
||||
Communication.onServerChange((msg: ServerEvent) => {
|
||||
const msgType = msg[0]
|
||||
|
|
@ -573,11 +461,10 @@ export async function main(
|
|||
return false
|
||||
}
|
||||
|
||||
let GAME_TS = REPLAY.lastGameTs
|
||||
const next = async () => {
|
||||
if (REPLAY.logPointer >= REPLAY.log.length) {
|
||||
Communication.requestMoreReplayData()
|
||||
to = setTimeout(next, 50)
|
||||
return
|
||||
if (REPLAY.logPointer + 1 >= REPLAY.log.length) {
|
||||
await queryNextReplayBatch(gameId)
|
||||
}
|
||||
|
||||
const realTs = Time.timestamp()
|
||||
|
|
@ -593,37 +480,36 @@ export async function main(
|
|||
if (REPLAY.paused) {
|
||||
break
|
||||
}
|
||||
if (REPLAY.logPointer >= REPLAY.log.length) {
|
||||
const nextIdx = REPLAY.logPointer + 1
|
||||
if (nextIdx >= REPLAY.log.length) {
|
||||
break
|
||||
}
|
||||
|
||||
const lastLogEntry = REPLAY.logPointer > 0 ? REPLAY.log[REPLAY.logPointer - 1] : null
|
||||
const lastTs: Timestamp = REPLAY.gameStartTs + (lastLogEntry ? lastLogEntry[lastLogEntry.length - 1] : 0)
|
||||
const nextLogEntry = REPLAY.log[REPLAY.logPointer]
|
||||
const nextTs: Timestamp = REPLAY.gameStartTs + nextLogEntry[nextLogEntry.length - 1]
|
||||
const currLogEntry = REPLAY.log[REPLAY.logPointer]
|
||||
const currTs: Timestamp = GAME_TS + currLogEntry[currLogEntry.length - 1]
|
||||
|
||||
const nextLogEntry = REPLAY.log[nextIdx]
|
||||
const diffToNext = nextLogEntry[nextLogEntry.length - 1]
|
||||
const nextTs: Timestamp = currTs + diffToNext
|
||||
if (nextTs > maxGameTs) {
|
||||
// next log entry is too far into the future
|
||||
if (REPLAY.skipNonActionPhases && (maxGameTs + 50 < nextTs)) {
|
||||
const skipInterval = nextTs - lastTs
|
||||
// lets skip to the next log entry
|
||||
// log.info('skipping non-action, from', maxGameTs, skipInterval)
|
||||
maxGameTs += skipInterval
|
||||
if (REPLAY.skipNonActionPhases && (maxGameTs + 500 * Time.MS < nextTs)) {
|
||||
maxGameTs += diffToNext
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
GAME_TS = currTs
|
||||
if (handleLogEntry(nextLogEntry, nextTs)) {
|
||||
RERENDER = true
|
||||
}
|
||||
REPLAY.logPointer++
|
||||
REPLAY.logPointer = nextIdx
|
||||
} while (true)
|
||||
REPLAY.lastRealTs = realTs
|
||||
REPLAY.lastGameTs = maxGameTs
|
||||
updateTimerElements()
|
||||
|
||||
if (REPLAY.final && REPLAY.logPointer + 1 >= REPLAY.log.length) {
|
||||
// done
|
||||
} else {
|
||||
if (!REPLAY.final) {
|
||||
to = setTimeout(next, 50)
|
||||
}
|
||||
}
|
||||
|
|
@ -682,6 +568,16 @@ export async function main(
|
|||
HUD.togglePreview()
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_SOUNDS) {
|
||||
HUD.toggleSoundsEnabled()
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES) {
|
||||
HUD.togglePlayerNames()
|
||||
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
|
||||
centerPuzzle()
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_FIXED_PIECES) {
|
||||
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
|
||||
RERENDER = true
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES) {
|
||||
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
|
||||
RERENDER = true
|
||||
}
|
||||
|
||||
// LOCAL + SERVER CHANGES
|
||||
|
|
@ -694,7 +590,7 @@ export async function main(
|
|||
ts,
|
||||
(playerId: string) => {
|
||||
if (playerSoundEnabled()) {
|
||||
clickAudio.play()
|
||||
playClick()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -706,7 +602,13 @@ export async function main(
|
|||
// LOCAL ONLY CHANGES
|
||||
// -------------------------------------------------------------
|
||||
const type = evt[0]
|
||||
if (type === Protocol.INPUT_EV_MOVE) {
|
||||
if (type === Protocol.INPUT_EV_REPLAY_TOGGLE_PAUSE) {
|
||||
replayOnPauseToggle()
|
||||
} else if (type === Protocol.INPUT_EV_REPLAY_SPEED_DOWN) {
|
||||
replayOnSpeedDown()
|
||||
} else if (type === Protocol.INPUT_EV_REPLAY_SPEED_UP) {
|
||||
replayOnSpeedUp()
|
||||
} else if (type === Protocol.INPUT_EV_MOVE) {
|
||||
const diffX = evt[1]
|
||||
const diffY = evt[2]
|
||||
RERENDER = true
|
||||
|
|
@ -723,11 +625,15 @@ export async function main(
|
|||
|
||||
_last_mouse_down = mouse
|
||||
}
|
||||
} else if (type === Protocol.INPUT_EV_PLAYER_COLOR) {
|
||||
updatePlayerCursorColor(evt[1])
|
||||
} else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
|
||||
const pos = { x: evt[1], y: evt[2] }
|
||||
_last_mouse_down = viewport.worldToViewport(pos)
|
||||
updatePlayerCursorState(true)
|
||||
} else if (type === Protocol.INPUT_EV_MOUSE_UP) {
|
||||
_last_mouse_down = null
|
||||
updatePlayerCursorState(false)
|
||||
} else if (type === Protocol.INPUT_EV_ZOOM_IN) {
|
||||
const pos = { x: evt[1], y: evt[2] }
|
||||
RERENDER = true
|
||||
|
|
@ -738,6 +644,18 @@ export async function main(
|
|||
viewport.zoom('out', viewport.worldToViewport(pos))
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_PREVIEW) {
|
||||
HUD.togglePreview()
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_SOUNDS) {
|
||||
HUD.toggleSoundsEnabled()
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES) {
|
||||
HUD.togglePlayerNames()
|
||||
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
|
||||
centerPuzzle()
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_FIXED_PIECES) {
|
||||
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
|
||||
RERENDER = true
|
||||
} else if (type === Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES) {
|
||||
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
|
||||
RERENDER = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -813,10 +731,12 @@ export async function main(
|
|||
bmp = await getPlayerCursor(p)
|
||||
pos = viewport.worldToViewport(p)
|
||||
ctx.drawImage(bmp, pos.x - CURSOR_W_2, pos.y - CURSOR_H_2)
|
||||
// performance:
|
||||
// not drawing text directly here, to have less ctx
|
||||
// switches between drawImage and fillTxt
|
||||
texts.push([`${p.name} (${p.points})`, pos.x, pos.y + CURSOR_H])
|
||||
if (showPlayerNames()) {
|
||||
// performance:
|
||||
// not drawing text directly here, to have less ctx
|
||||
// switches between drawImage and fillTxt
|
||||
texts.push([`${p.name} (${p.points})`, pos.x, pos.y + CURSOR_H])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -854,19 +774,26 @@ export async function main(
|
|||
evts.setHotkeys(state)
|
||||
},
|
||||
onBgChange: (value: string) => {
|
||||
localStorage.setItem('bg_color', value)
|
||||
settings.setStr(SETTINGS.COLOR_BACKGROUND, value)
|
||||
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
|
||||
},
|
||||
onColorChange: (value: string) => {
|
||||
localStorage.setItem('player_color', value)
|
||||
settings.setStr(SETTINGS.PLAYER_COLOR, value)
|
||||
evts.addEvent([Protocol.INPUT_EV_PLAYER_COLOR, value])
|
||||
},
|
||||
onNameChange: (value: string) => {
|
||||
localStorage.setItem('player_name', value)
|
||||
settings.setStr(SETTINGS.PLAYER_NAME, value)
|
||||
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
|
||||
},
|
||||
onSoundsEnabledChange: (value: boolean) => {
|
||||
localStorage.setItem('sound_enabled', value ? '1' : '0')
|
||||
settings.setBool(SETTINGS.SOUND_ENABLED, value)
|
||||
},
|
||||
onSoundsVolumeChange: (value: number) => {
|
||||
settings.setInt(SETTINGS.SOUND_VOLUME, value)
|
||||
playClick()
|
||||
},
|
||||
onShowPlayerNamesChange: (value: boolean) => {
|
||||
settings.setBool(SETTINGS.SHOW_PLAYER_NAMES, value)
|
||||
},
|
||||
replayOnSpeedUp,
|
||||
replayOnSpeedDown,
|
||||
|
|
@ -877,7 +804,10 @@ export async function main(
|
|||
color: playerColor(),
|
||||
name: playerName(),
|
||||
soundsEnabled: playerSoundEnabled(),
|
||||
soundsVolume: playerSoundVolume(),
|
||||
showPlayerNames: showPlayerNames(),
|
||||
},
|
||||
game: Game.get(gameId),
|
||||
disconnect: Communication.disconnect,
|
||||
connect: connect,
|
||||
unload: unload,
|
||||
|
|
|
|||
|
|
@ -7,19 +7,36 @@ import NewGame from './views/NewGame.vue'
|
|||
import Game from './views/Game.vue'
|
||||
import Replay from './views/Replay.vue'
|
||||
import Util from './../common/Util'
|
||||
import settings from './settings'
|
||||
import xhr from './xhr'
|
||||
|
||||
(async () => {
|
||||
const res = await fetch(`/api/conf`)
|
||||
const conf = await res.json()
|
||||
|
||||
function initme() {
|
||||
let ID = localStorage.getItem('ID')
|
||||
function initClientSecret() {
|
||||
let SECRET = settings.getStr('SECRET', '')
|
||||
if (!SECRET) {
|
||||
SECRET = Util.uniqId()
|
||||
settings.setStr('SECRET', SECRET)
|
||||
}
|
||||
return SECRET
|
||||
}
|
||||
function initClientId() {
|
||||
let ID = settings.getStr('ID', '')
|
||||
if (!ID) {
|
||||
ID = Util.uniqId()
|
||||
localStorage.setItem('ID', ID)
|
||||
settings.setStr('ID', ID)
|
||||
}
|
||||
return ID
|
||||
}
|
||||
const clientId = initClientId()
|
||||
const clientSecret = initClientSecret()
|
||||
xhr.setClientId(clientId)
|
||||
xhr.setClientSecret(clientSecret)
|
||||
|
||||
const meRes = await xhr.get(`/api/me`, {})
|
||||
const me = await meRes.json()
|
||||
|
||||
const confRes = await xhr.get(`/api/conf`, {})
|
||||
const conf = await confRes.json()
|
||||
|
||||
const router = VueRouter.createRouter({
|
||||
history: VueRouter.createWebHashHistory(),
|
||||
|
|
@ -39,8 +56,9 @@ import Util from './../common/Util'
|
|||
})
|
||||
|
||||
const app = Vue.createApp(App)
|
||||
app.config.globalProperties.$me = me
|
||||
app.config.globalProperties.$config = conf
|
||||
app.config.globalProperties.$clientId = initme()
|
||||
app.config.globalProperties.$clientId = clientId
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
})()
|
||||
|
|
|
|||
66
src/frontend/settings.ts
Normal file
66
src/frontend/settings.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Player settings
|
||||
*/
|
||||
|
||||
export const SETTINGS = {
|
||||
SOUND_VOLUME: 'sound_volume',
|
||||
SOUND_ENABLED: 'sound_enabled',
|
||||
COLOR_BACKGROUND: 'bg_color',
|
||||
PLAYER_COLOR: 'player_color',
|
||||
PLAYER_NAME: 'player_name',
|
||||
SHOW_PLAYER_NAMES: 'show_player_names',
|
||||
}
|
||||
|
||||
const set = (setting: string, value: string): void => {
|
||||
localStorage.setItem(setting, value)
|
||||
}
|
||||
|
||||
const get = (setting: string): any => {
|
||||
return localStorage.getItem(setting)
|
||||
}
|
||||
|
||||
const setInt = (setting: string, val: number): void => {
|
||||
set(setting, `${val}`)
|
||||
}
|
||||
|
||||
const getInt = (setting: string, def: number): number => {
|
||||
const value = get(setting)
|
||||
if (value === null) {
|
||||
return def
|
||||
}
|
||||
const vol = parseInt(value, 10)
|
||||
return isNaN(vol) ? def : vol
|
||||
}
|
||||
|
||||
const setBool = (setting: string, val: boolean): void => {
|
||||
set(setting, val ? '1' : '0')
|
||||
}
|
||||
|
||||
const getBool = (setting: string, def: boolean): boolean => {
|
||||
const value = get(setting)
|
||||
if (value === null) {
|
||||
return def
|
||||
}
|
||||
return value === '1'
|
||||
}
|
||||
|
||||
const setStr = (setting: string, val: string): void => {
|
||||
set(setting, val)
|
||||
}
|
||||
|
||||
const getStr = (setting: string, def: string): string => {
|
||||
const value = get(setting)
|
||||
if (value === null) {
|
||||
return def
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export default {
|
||||
setInt,
|
||||
getInt,
|
||||
setBool,
|
||||
getBool,
|
||||
setStr,
|
||||
getStr,
|
||||
}
|
||||
|
|
@ -2,8 +2,15 @@
|
|||
<div id="game">
|
||||
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
|
||||
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
|
||||
<info-overlay v-if="g.game" v-show="overlay === 'info'" @bgclick="toggle('info', true)" :game="g.game" />
|
||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||
|
||||
<div class="overlay" v-if="cuttingPuzzle">
|
||||
<div class="overlay-content">
|
||||
<div>⏳ Cutting puzzle, please wait... ⏳</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<connection-overlay
|
||||
:connectionState="connectionState"
|
||||
@reconnect="reconnect"
|
||||
|
|
@ -21,7 +28,8 @@
|
|||
<router-link class="opener" :to="{name: 'index'}" target="_blank">🧩 Puzzles</router-link>
|
||||
<div class="opener" @click="toggle('preview', false)">🖼️ Preview</div>
|
||||
<div class="opener" @click="toggle('settings', true)">🛠️ Settings</div>
|
||||
<div class="opener" @click="toggle('help', true)">ℹ️ Help</div>
|
||||
<div class="opener" @click="toggle('info', true)">ℹ️ Info</div>
|
||||
<div class="opener" @click="toggle('help', true)">⌨️ Hotkeys</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -35,11 +43,12 @@ import Scores from './../components/Scores.vue'
|
|||
import PuzzleStatus from './../components/PuzzleStatus.vue'
|
||||
import SettingsOverlay from './../components/SettingsOverlay.vue'
|
||||
import PreviewOverlay from './../components/PreviewOverlay.vue'
|
||||
import InfoOverlay from './../components/InfoOverlay.vue'
|
||||
import ConnectionOverlay from './../components/ConnectionOverlay.vue'
|
||||
import HelpOverlay from './../components/HelpOverlay.vue'
|
||||
|
||||
import { main, MODE_PLAY } from './../game'
|
||||
import { Player } from '../../common/Types'
|
||||
import { Game, Player } from '../../common/Types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'game',
|
||||
|
|
@ -48,6 +57,7 @@ export default defineComponent({
|
|||
Scores,
|
||||
SettingsOverlay,
|
||||
PreviewOverlay,
|
||||
InfoOverlay,
|
||||
ConnectionOverlay,
|
||||
HelpOverlay,
|
||||
},
|
||||
|
|
@ -64,6 +74,7 @@ export default defineComponent({
|
|||
overlay: '',
|
||||
|
||||
connectionState: 0,
|
||||
cuttingPuzzle: true,
|
||||
|
||||
g: {
|
||||
player: {
|
||||
|
|
@ -71,13 +82,18 @@ export default defineComponent({
|
|||
color: '',
|
||||
name: '',
|
||||
soundsEnabled: false,
|
||||
soundsVolume: 100,
|
||||
showPlayerNames: true,
|
||||
},
|
||||
game: null as Game|null,
|
||||
previewImageUrl: '',
|
||||
setHotkeys: (v: boolean) => {},
|
||||
onBgChange: (v: string) => {},
|
||||
onColorChange: (v: string) => {},
|
||||
onNameChange: (v: string) => {},
|
||||
onSoundsEnabledChange: (v: boolean) => {},
|
||||
onSoundsVolumeChange: (v: number) => {},
|
||||
onShowPlayerNamesChange: (v: boolean) => {},
|
||||
connect: () => {},
|
||||
disconnect: () => {},
|
||||
unload: () => {},
|
||||
|
|
@ -100,6 +116,12 @@ export default defineComponent({
|
|||
this.$watch(() => this.g.player.soundsEnabled, (value: boolean) => {
|
||||
this.g.onSoundsEnabledChange(value)
|
||||
})
|
||||
this.$watch(() => this.g.player.soundsVolume, (value: number) => {
|
||||
this.g.onSoundsVolumeChange(value)
|
||||
})
|
||||
this.$watch(() => this.g.player.showPlayerNames, (value: boolean) => {
|
||||
this.g.onShowPlayerNamesChange(value)
|
||||
})
|
||||
this.g = await main(
|
||||
`${this.$route.params.id}`,
|
||||
// @ts-ignore
|
||||
|
|
@ -109,15 +131,17 @@ export default defineComponent({
|
|||
MODE_PLAY,
|
||||
this.$el,
|
||||
{
|
||||
setPuzzleCut: () => { this.cuttingPuzzle = false },
|
||||
setActivePlayers: (v: Array<Player>) => { this.activePlayers = v },
|
||||
setIdlePlayers: (v: Array<Player>) => { this.idlePlayers = v },
|
||||
setFinished: (v: boolean) => { this.finished = v },
|
||||
setDuration: (v: number) => { this.duration = v },
|
||||
setPiecesDone: (v: number) => { this.piecesDone = v },
|
||||
setPiecesTotal: (v: number) => { this.piecesTotal = v },
|
||||
setConnectionState: (v: number) => { this.connectionState = v },
|
||||
togglePreview: () => { this.toggle('preview', false) },
|
||||
setConnectionState: (v: number) => { this.connectionState = v },
|
||||
toggleSoundsEnabled: () => { this.g.player.soundsEnabled = !this.g.player.soundsEnabled },
|
||||
togglePlayerNames: () => { this.g.player.showPlayerNames = !this.g.player.showPlayerNames },
|
||||
}
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import xhr from '../xhr'
|
||||
|
||||
import GameTeaser from './../components/GameTeaser.vue'
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
async created() {
|
||||
const res = await fetch('/api/index-data')
|
||||
const res = await xhr.get('/api/index-data', {})
|
||||
const json = await res.json()
|
||||
this.gamesRunning = json.gamesRunning
|
||||
this.gamesFinished = json.gamesFinished
|
||||
|
|
|
|||
|
|
@ -44,8 +44,11 @@ in jigsawpuzzles.io
|
|||
v-if="dialog==='new-image'"
|
||||
:autocompleteTags="autocompleteTags"
|
||||
@bgclick="dialog=''"
|
||||
:uploadProgress="uploadProgress"
|
||||
:uploading="uploading"
|
||||
@postToGalleryClick="postToGalleryClick"
|
||||
@setupGameClick="setupGameClick" />
|
||||
@setupGameClick="setupGameClick"
|
||||
/>
|
||||
<edit-image-dialog
|
||||
v-if="dialog==='edit-image'"
|
||||
:autocompleteTags="autocompleteTags"
|
||||
|
|
@ -61,7 +64,7 @@ in jigsawpuzzles.io
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import ImageLibrary from './../components/ImageLibrary.vue'
|
||||
import NewImageDialog from './../components/NewImageDialog.vue'
|
||||
|
|
@ -69,6 +72,7 @@ import EditImageDialog from './../components/EditImageDialog.vue'
|
|||
import NewGameDialog from './../components/NewGameDialog.vue'
|
||||
import { GameSettings, Image, Tag } from '../../common/Types'
|
||||
import Util from '../../common/Util'
|
||||
import xhr from '../xhr'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
|
@ -97,6 +101,9 @@ export default defineComponent({
|
|||
} as Image,
|
||||
|
||||
dialog: '',
|
||||
|
||||
uploading: '',
|
||||
uploadProgress: 0,
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
|
|
@ -126,7 +133,7 @@ export default defineComponent({
|
|||
this.filtersChanged()
|
||||
},
|
||||
async loadImages () {
|
||||
const res = await fetch(`/api/newgame-data${Util.asQueryArgs(this.filters)}`)
|
||||
const res = await xhr.get(`/api/newgame-data${Util.asQueryArgs(this.filters)}`, {})
|
||||
const json = await res.json()
|
||||
this.images = json.images
|
||||
this.tags = json.tags
|
||||
|
|
@ -143,20 +150,22 @@ export default defineComponent({
|
|||
this.dialog = 'edit-image'
|
||||
},
|
||||
async uploadImage (data: any) {
|
||||
this.uploadProgress = 0
|
||||
const formData = new FormData();
|
||||
formData.append('file', data.file, data.file.name);
|
||||
formData.append('title', data.title)
|
||||
formData.append('tags', data.tags)
|
||||
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'post',
|
||||
const res = await xhr.post('/api/upload', {
|
||||
body: formData,
|
||||
onUploadProgress: (evt: ProgressEvent<XMLHttpRequestEventTarget>): void => {
|
||||
this.uploadProgress = evt.loaded / evt.total
|
||||
},
|
||||
})
|
||||
this.uploadProgress = 1
|
||||
return await res.json()
|
||||
},
|
||||
async saveImage (data: any) {
|
||||
const res = await fetch('/api/save-image', {
|
||||
method: 'post',
|
||||
const res = await xhr.post('/api/save-image', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
|
|
@ -170,24 +179,31 @@ export default defineComponent({
|
|||
return await res.json()
|
||||
},
|
||||
async onSaveImageClick(data: any) {
|
||||
await this.saveImage(data)
|
||||
this.dialog = ''
|
||||
await this.loadImages()
|
||||
const res = await this.saveImage(data)
|
||||
if (res.ok) {
|
||||
this.dialog = ''
|
||||
await this.loadImages()
|
||||
} else {
|
||||
alert(res.error)
|
||||
}
|
||||
},
|
||||
async postToGalleryClick(data: any) {
|
||||
this.uploading = 'postToGallery'
|
||||
await this.uploadImage(data)
|
||||
this.uploading = ''
|
||||
this.dialog = ''
|
||||
await this.loadImages()
|
||||
},
|
||||
async setupGameClick (data: any) {
|
||||
this.uploading = 'setupGame'
|
||||
const image = await this.uploadImage(data)
|
||||
this.uploading = ''
|
||||
this.loadImages() // load images in background
|
||||
this.image = image
|
||||
this.dialog = 'new-game'
|
||||
},
|
||||
async onNewGame(gameSettings: GameSettings) {
|
||||
const res = await fetch('/api/newgame', {
|
||||
method: 'post',
|
||||
const res = await xhr.post('/api/newgame', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
|
|
|
|||
|
|
@ -2,8 +2,15 @@
|
|||
<div id="replay">
|
||||
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
|
||||
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
|
||||
<info-overlay v-if="g.game" v-show="overlay === 'info'" @bgclick="toggle('info', true)" :game="g.game" />
|
||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||
|
||||
<div class="overlay" v-if="cuttingPuzzle">
|
||||
<div class="overlay-content">
|
||||
<div>⏳ Cutting puzzle, please wait... ⏳</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<puzzle-status
|
||||
:finished="finished"
|
||||
:duration="duration"
|
||||
|
|
@ -23,7 +30,8 @@
|
|||
<router-link class="opener" :to="{name: 'index'}" target="_blank">🧩 Puzzles</router-link>
|
||||
<div class="opener" @click="toggle('preview', false)">🖼️ Preview</div>
|
||||
<div class="opener" @click="toggle('settings', true)">🛠️ Settings</div>
|
||||
<div class="opener" @click="toggle('help', true)">ℹ️ Help</div>
|
||||
<div class="opener" @click="toggle('info', true)">ℹ️ Info</div>
|
||||
<div class="opener" @click="toggle('help', true)">⌨️ Hotkeys</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -37,9 +45,11 @@ import Scores from './../components/Scores.vue'
|
|||
import PuzzleStatus from './../components/PuzzleStatus.vue'
|
||||
import SettingsOverlay from './../components/SettingsOverlay.vue'
|
||||
import PreviewOverlay from './../components/PreviewOverlay.vue'
|
||||
import InfoOverlay from './../components/InfoOverlay.vue'
|
||||
import HelpOverlay from './../components/HelpOverlay.vue'
|
||||
|
||||
import { main, MODE_REPLAY } from './../game'
|
||||
import { Game, Player } from '../../common/Types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'replay',
|
||||
|
|
@ -48,12 +58,13 @@ export default defineComponent({
|
|||
Scores,
|
||||
SettingsOverlay,
|
||||
PreviewOverlay,
|
||||
InfoOverlay,
|
||||
HelpOverlay,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activePlayers: [] as Array<any>,
|
||||
idlePlayers: [] as Array<any>,
|
||||
activePlayers: [] as Array<Player>,
|
||||
idlePlayers: [] as Array<Player>,
|
||||
|
||||
finished: false,
|
||||
duration: 0,
|
||||
|
|
@ -63,6 +74,7 @@ export default defineComponent({
|
|||
overlay: '',
|
||||
|
||||
connectionState: 0,
|
||||
cuttingPuzzle: true,
|
||||
|
||||
g: {
|
||||
player: {
|
||||
|
|
@ -70,13 +82,18 @@ export default defineComponent({
|
|||
color: '',
|
||||
name: '',
|
||||
soundsEnabled: false,
|
||||
soundsVolume: 100,
|
||||
showPlayerNames: true,
|
||||
},
|
||||
game: null as Game|null,
|
||||
previewImageUrl: '',
|
||||
setHotkeys: (v: boolean) => {},
|
||||
onBgChange: (v: string) => {},
|
||||
onColorChange: (v: string) => {},
|
||||
onNameChange: (v: string) => {},
|
||||
onSoundsEnabledChange: (v: boolean) => {},
|
||||
onSoundsVolumeChange: (v: number) => {},
|
||||
onShowPlayerNamesChange: (v: boolean) => {},
|
||||
replayOnSpeedUp: () => {},
|
||||
replayOnSpeedDown: () => {},
|
||||
replayOnPauseToggle: () => {},
|
||||
|
|
@ -107,6 +124,12 @@ export default defineComponent({
|
|||
this.$watch(() => this.g.player.soundsEnabled, (value: boolean) => {
|
||||
this.g.onSoundsEnabledChange(value)
|
||||
})
|
||||
this.$watch(() => this.g.player.soundsVolume, (value: number) => {
|
||||
this.g.onSoundsVolumeChange(value)
|
||||
})
|
||||
this.$watch(() => this.g.player.showPlayerNames, (value: boolean) => {
|
||||
this.g.onShowPlayerNamesChange(value)
|
||||
})
|
||||
this.g = await main(
|
||||
`${this.$route.params.id}`,
|
||||
// @ts-ignore
|
||||
|
|
@ -116,8 +139,9 @@ export default defineComponent({
|
|||
MODE_REPLAY,
|
||||
this.$el,
|
||||
{
|
||||
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
|
||||
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
|
||||
setPuzzleCut: () => { this.cuttingPuzzle = false },
|
||||
setActivePlayers: (v: Array<Player>) => { this.activePlayers = v },
|
||||
setIdlePlayers: (v: Array<Player>) => { this.idlePlayers = v },
|
||||
setFinished: (v: boolean) => { this.finished = v },
|
||||
setDuration: (v: number) => { this.duration = v },
|
||||
setPiecesDone: (v: number) => { this.piecesDone = v },
|
||||
|
|
@ -127,6 +151,7 @@ export default defineComponent({
|
|||
setReplaySpeed: (v: number) => { this.replay.speed = v },
|
||||
setReplayPaused: (v: boolean) => { this.replay.paused = v },
|
||||
toggleSoundsEnabled: () => { this.g.player.soundsEnabled = !this.g.player.soundsEnabled },
|
||||
togglePlayerNames: () => { this.g.player.showPlayerNames = !this.g.player.showPlayerNames },
|
||||
}
|
||||
)
|
||||
},
|
||||
|
|
|
|||
68
src/frontend/xhr.ts
Normal file
68
src/frontend/xhr.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
export interface Response {
|
||||
status: number,
|
||||
text: string,
|
||||
json: () => Promise<any>,
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
body: FormData|string,
|
||||
headers?: any,
|
||||
onUploadProgress?: (ev: ProgressEvent<XMLHttpRequestEventTarget>) => any,
|
||||
}
|
||||
|
||||
let xhrClientId: string = ''
|
||||
let xhrClientSecret: string = ''
|
||||
const request = async (
|
||||
method: string,
|
||||
url: string,
|
||||
options: Options
|
||||
): Promise<Response> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new window.XMLHttpRequest()
|
||||
xhr.open(method, url, true)
|
||||
xhr.withCredentials = true
|
||||
for (const k in options.headers || {}) {
|
||||
xhr.setRequestHeader(k, options.headers[k])
|
||||
}
|
||||
|
||||
xhr.setRequestHeader('Client-Id', xhrClientId)
|
||||
xhr.setRequestHeader('Client-Secret', xhrClientSecret)
|
||||
|
||||
xhr.addEventListener('load', function (ev: ProgressEvent<XMLHttpRequestEventTarget>
|
||||
) {
|
||||
resolve({
|
||||
status: this.status,
|
||||
text: this.responseText,
|
||||
json: async () => JSON.parse(this.responseText),
|
||||
})
|
||||
})
|
||||
xhr.addEventListener('error', function (ev: ProgressEvent<XMLHttpRequestEventTarget>) {
|
||||
reject(new Error('xhr error'))
|
||||
})
|
||||
if (xhr.upload && options.onUploadProgress) {
|
||||
xhr.upload.addEventListener('progress', function (ev: ProgressEvent<XMLHttpRequestEventTarget>) {
|
||||
// typescript complains without this extra check
|
||||
if (options.onUploadProgress) {
|
||||
options.onUploadProgress(ev)
|
||||
}
|
||||
})
|
||||
}
|
||||
xhr.send(options.body || null)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
request,
|
||||
get: (url: string, options: any): Promise<Response> => {
|
||||
return request('get', url, options)
|
||||
},
|
||||
post: (url: string, options: any): Promise<Response> => {
|
||||
return request('post', url, options)
|
||||
},
|
||||
setClientId: (clientId: string): void => {
|
||||
xhrClientId = clientId
|
||||
},
|
||||
setClientSecret: (clientSecret: string): void => {
|
||||
xhrClientSecret = clientSecret
|
||||
},
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import GameCommon from './../common/GameCommon'
|
||||
import { Change, Game, Input, ScoreMode, ShapeMode, SnapMode, Timestamp } from './../common/Types'
|
||||
import { Change, Game, Input, ScoreMode, ShapeMode, SnapMode,ImageInfo, Timestamp, GameSettings } from './../common/Types'
|
||||
import Util, { logger } from './../common/Util'
|
||||
import { Rng } from './../common/Rng'
|
||||
import GameLog from './GameLog'
|
||||
import { createPuzzle, PuzzleCreationImageInfo } from './Puzzle'
|
||||
import { createPuzzle } from './Puzzle'
|
||||
import Protocol from './../common/Protocol'
|
||||
import GameStorage from './GameStorage'
|
||||
|
||||
|
|
@ -12,16 +12,18 @@ const log = logger('Game.ts')
|
|||
async function createGameObject(
|
||||
gameId: string,
|
||||
targetTiles: number,
|
||||
image: PuzzleCreationImageInfo,
|
||||
image: ImageInfo,
|
||||
ts: Timestamp,
|
||||
scoreMode: ScoreMode,
|
||||
shapeMode: ShapeMode,
|
||||
snapMode: SnapMode
|
||||
snapMode: SnapMode,
|
||||
creatorUserId: number|null
|
||||
): Promise<Game> {
|
||||
const seed = Util.hash(gameId + ' ' + ts)
|
||||
const rng = new Rng(seed)
|
||||
return {
|
||||
id: gameId,
|
||||
creatorUserId,
|
||||
rng: { type: 'Rng', obj: rng },
|
||||
puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode),
|
||||
players: [],
|
||||
|
|
@ -32,50 +34,54 @@ async function createGameObject(
|
|||
}
|
||||
}
|
||||
|
||||
async function createGame(
|
||||
gameId: string,
|
||||
targetTiles: number,
|
||||
image: PuzzleCreationImageInfo,
|
||||
async function createNewGame(
|
||||
gameSettings: GameSettings,
|
||||
ts: Timestamp,
|
||||
scoreMode: ScoreMode,
|
||||
shapeMode: ShapeMode,
|
||||
snapMode: SnapMode
|
||||
): Promise<void> {
|
||||
creatorUserId: number
|
||||
): Promise<string> {
|
||||
let gameId;
|
||||
do {
|
||||
gameId = Util.uniqId()
|
||||
} while (GameCommon.exists(gameId))
|
||||
|
||||
const gameObject = await createGameObject(
|
||||
gameId,
|
||||
targetTiles,
|
||||
image,
|
||||
gameSettings.tiles,
|
||||
gameSettings.image,
|
||||
ts,
|
||||
scoreMode,
|
||||
shapeMode,
|
||||
snapMode
|
||||
gameSettings.scoreMode,
|
||||
gameSettings.shapeMode,
|
||||
gameSettings.snapMode,
|
||||
creatorUserId
|
||||
)
|
||||
|
||||
GameLog.create(gameId)
|
||||
GameLog.create(gameId, ts)
|
||||
GameLog.log(
|
||||
gameId,
|
||||
Protocol.LOG_HEADER,
|
||||
1,
|
||||
targetTiles,
|
||||
image,
|
||||
gameSettings.tiles,
|
||||
gameSettings.image,
|
||||
ts,
|
||||
scoreMode,
|
||||
shapeMode,
|
||||
snapMode
|
||||
gameSettings.scoreMode,
|
||||
gameSettings.shapeMode,
|
||||
gameSettings.snapMode,
|
||||
gameObject.creatorUserId
|
||||
)
|
||||
|
||||
GameCommon.setGame(gameObject.id, gameObject)
|
||||
GameStorage.setDirty(gameId)
|
||||
|
||||
return gameId
|
||||
}
|
||||
|
||||
function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
|
||||
if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), 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)
|
||||
GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, ts)
|
||||
} else {
|
||||
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, diff)
|
||||
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, ts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,8 +97,7 @@ function handleInput(
|
|||
): Array<Change> {
|
||||
if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), ts)) {
|
||||
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
|
||||
const diff = ts - GameCommon.getStartTs(gameId)
|
||||
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)
|
||||
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, ts)
|
||||
}
|
||||
|
||||
const ret = GameCommon.handleInput(gameId, playerId, input, ts)
|
||||
|
|
@ -102,7 +107,7 @@ function handleInput(
|
|||
|
||||
export default {
|
||||
createGameObject,
|
||||
createGame,
|
||||
createNewGame,
|
||||
addPlayer,
|
||||
handleInput,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import WebSocket from 'ws'
|
||||
import fs from 'fs'
|
||||
import readline from 'readline'
|
||||
import stream from 'stream'
|
||||
import Protocol from '../common/Protocol'
|
||||
import Time from '../common/Time'
|
||||
import { Game as GameType, ScoreMode, ShapeMode, SnapMode, Timestamp } from '../common/Types'
|
||||
import { DefaultScoreMode, DefaultShapeMode, DefaultSnapMode, Timestamp } from '../common/Types'
|
||||
import { logger } from './../common/Util'
|
||||
import { DATA_DIR } from './../server/Dirs'
|
||||
import Game from './Game'
|
||||
|
||||
const log = logger('GameLog.js')
|
||||
|
||||
const LINES_PER_LOG_FILE = 10000
|
||||
const POST_GAME_LOG_DURATION = 5 * Time.MIN
|
||||
|
||||
const shouldLog = (finishTs: Timestamp, currentTs: Timestamp): boolean => {
|
||||
|
|
@ -24,180 +22,85 @@ const shouldLog = (finishTs: Timestamp, currentTs: Timestamp): boolean => {
|
|||
return timeSinceGameEnd <= POST_GAME_LOG_DURATION
|
||||
}
|
||||
|
||||
const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log`
|
||||
export const filename = (gameId: string, offset: number) => `${DATA_DIR}/log_${gameId}-${offset}.log`
|
||||
export const idxname = (gameId: string) => `${DATA_DIR}/log_${gameId}.idx.log`
|
||||
|
||||
const create = (gameId: string): void => {
|
||||
const file = filename(gameId)
|
||||
if (!fs.existsSync(file)) {
|
||||
fs.appendFileSync(file, '')
|
||||
const create = (gameId: string, ts: Timestamp): void => {
|
||||
const idxfile = idxname(gameId)
|
||||
if (!fs.existsSync(idxfile)) {
|
||||
fs.appendFileSync(idxfile, JSON.stringify({
|
||||
gameId: gameId,
|
||||
total: 0,
|
||||
lastTs: ts,
|
||||
currentFile: '',
|
||||
perFile: LINES_PER_LOG_FILE,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const exists = (gameId: string): boolean => {
|
||||
const file = filename(gameId)
|
||||
return fs.existsSync(file)
|
||||
const idxfile = idxname(gameId)
|
||||
return fs.existsSync(idxfile)
|
||||
}
|
||||
|
||||
const _log = (gameId: string, ...args: Array<any>): void => {
|
||||
const file = filename(gameId)
|
||||
if (!fs.existsSync(file)) {
|
||||
const _log = (gameId: string, type: number, ...args: Array<any>): void => {
|
||||
const idxfile = idxname(gameId)
|
||||
if (!fs.existsSync(idxfile)) {
|
||||
return
|
||||
}
|
||||
const str = JSON.stringify(args)
|
||||
fs.appendFileSync(file, str + "\n")
|
||||
|
||||
const idxObj = JSON.parse(fs.readFileSync(idxfile, 'utf-8'))
|
||||
if (idxObj.total % idxObj.perFile === 0) {
|
||||
idxObj.currentFile = filename(gameId, idxObj.total)
|
||||
}
|
||||
|
||||
const tsIdx = type === Protocol.LOG_HEADER ? 3 : (args.length - 1)
|
||||
const ts: Timestamp = args[tsIdx]
|
||||
if (type !== Protocol.LOG_HEADER) {
|
||||
// for everything but header save the diff to last log entry
|
||||
args[tsIdx] = ts - idxObj.lastTs
|
||||
}
|
||||
const line = JSON.stringify([type, ...args]).slice(1, -1)
|
||||
fs.appendFileSync(idxObj.currentFile, line + "\n")
|
||||
|
||||
idxObj.total++
|
||||
idxObj.lastTs = ts
|
||||
fs.writeFileSync(idxfile, JSON.stringify(idxObj))
|
||||
}
|
||||
|
||||
const get = async (
|
||||
const get = (
|
||||
gameId: string,
|
||||
offset: number = 0,
|
||||
size: number = 10000
|
||||
): Promise<any[]> => {
|
||||
const file = filename(gameId)
|
||||
): any[] => {
|
||||
const idxfile = idxname(gameId)
|
||||
if (!fs.existsSync(idxfile)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const file = filename(gameId, offset)
|
||||
if (!fs.existsSync(file)) {
|
||||
return []
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const instream = fs.createReadStream(file)
|
||||
const outstream = new stream.Writable()
|
||||
const rl = readline.createInterface(instream, outstream)
|
||||
const lines: any[] = []
|
||||
let i = -1
|
||||
rl.on('line', (line) => {
|
||||
if (!line) {
|
||||
// skip empty
|
||||
return
|
||||
}
|
||||
i++
|
||||
if (offset > i) {
|
||||
return
|
||||
}
|
||||
if (offset + size <= i) {
|
||||
rl.close()
|
||||
return
|
||||
}
|
||||
lines.push(JSON.parse(line))
|
||||
})
|
||||
rl.on('close', () => {
|
||||
resolve(lines)
|
||||
})
|
||||
|
||||
const lines = fs.readFileSync(file, 'utf-8').split("\n")
|
||||
const log = lines.filter(line => !!line).map(line => {
|
||||
return JSON.parse(`[${line}]`)
|
||||
})
|
||||
}
|
||||
|
||||
interface LineReader {
|
||||
readLine: () => Promise<string>,
|
||||
}
|
||||
|
||||
const createLineReader = async (
|
||||
gameId: string
|
||||
): Promise<LineReader|null> => {
|
||||
const stream: fs.ReadStream = await new Promise(resolve => {
|
||||
const file = filename(gameId)
|
||||
if (!fs.existsSync(file)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const instream = fs.createReadStream(file)
|
||||
instream.on('readable', () => {
|
||||
resolve(instream)
|
||||
})
|
||||
})
|
||||
|
||||
let line = ''
|
||||
const readLine = async (): Promise<string> => {
|
||||
return new Promise(resolve => {
|
||||
let chunk
|
||||
let resolved = false
|
||||
while (null !== (chunk = stream.read(1))) {
|
||||
if (chunk.toString() === "\n") {
|
||||
resolve(line)
|
||||
line = ''
|
||||
resolved = true
|
||||
break
|
||||
}
|
||||
line += chunk
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
resolve('')
|
||||
}
|
||||
})
|
||||
if (offset === 0 && log.length > 0) {
|
||||
log[0][5] = DefaultScoreMode(log[0][5])
|
||||
log[0][6] = DefaultShapeMode(log[0][6])
|
||||
log[0][7] = DefaultSnapMode(log[0][7])
|
||||
log[0][8] = log[0][8] || null // creatorUserId
|
||||
}
|
||||
|
||||
return {
|
||||
readLine,
|
||||
}
|
||||
}
|
||||
|
||||
interface SocketGameLog {
|
||||
socket: WebSocket
|
||||
rl: LineReader
|
||||
}
|
||||
|
||||
const connected: SocketGameLog[] = []
|
||||
const getSocketGameLog = (
|
||||
socket: WebSocket,
|
||||
): SocketGameLog|null => {
|
||||
for (const entry of connected) {
|
||||
if (entry.socket === socket) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const open = async (socket: WebSocket, gameId: string): Promise<GameType|null> => {
|
||||
const rl = await createLineReader(gameId)
|
||||
if (!rl) {
|
||||
return null
|
||||
}
|
||||
const socketGameLog = {
|
||||
socket: socket,
|
||||
rl: rl,
|
||||
}
|
||||
const line = await rl.readLine()
|
||||
const log = JSON.parse(line)
|
||||
connected.push(socketGameLog)
|
||||
const g: GameType = await Game.createGameObject(
|
||||
gameId,
|
||||
log[2],
|
||||
log[3],
|
||||
log[4],
|
||||
log[5] || ScoreMode.FINAL,
|
||||
log[6] || ShapeMode.NORMAL,
|
||||
log[7] || SnapMode.NORMAL,
|
||||
)
|
||||
return g
|
||||
}
|
||||
|
||||
const getNextBySocket = async (socket: WebSocket): Promise<any[]> => {
|
||||
const socketGameLog = getSocketGameLog(socket)
|
||||
if (!socketGameLog) {
|
||||
return []
|
||||
}
|
||||
|
||||
const lines = []
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const line = await socketGameLog.rl.readLine()
|
||||
if (line) {
|
||||
try {
|
||||
lines.push(JSON.parse(line))
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
log.error(line)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return lines
|
||||
return log
|
||||
}
|
||||
|
||||
export default {
|
||||
open,
|
||||
getNextBySocket,
|
||||
shouldLog,
|
||||
create,
|
||||
exists,
|
||||
log: _log,
|
||||
get,
|
||||
filename,
|
||||
idxname,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import fs from 'fs'
|
||||
import GameCommon from './../common/GameCommon'
|
||||
import { Game, Piece, ScoreMode, ShapeMode, SnapMode } from './../common/Types'
|
||||
import { DefaultScoreMode, DefaultShapeMode, DefaultSnapMode, Game, Piece } from './../common/Types'
|
||||
import Util, { logger } from './../common/Util'
|
||||
import { Rng } from './../common/Rng'
|
||||
import { DATA_DIR } from './Dirs'
|
||||
import Time from './../common/Time'
|
||||
import Db from './Db'
|
||||
|
||||
const log = logger('GameStorage.js')
|
||||
|
||||
|
|
@ -15,8 +16,73 @@ function setDirty(gameId: string): void {
|
|||
function setClean(gameId: string): void {
|
||||
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)
|
||||
for (const f of files) {
|
||||
const m = f.match(/^([a-z0-9]+)\.json$/)
|
||||
|
|
@ -24,11 +90,14 @@ function loadGames(): void {
|
|||
continue
|
||||
}
|
||||
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 contents = fs.readFileSync(file, 'utf-8')
|
||||
let game
|
||||
|
|
@ -49,39 +118,29 @@ function loadGame(gameId: string): void {
|
|||
if (!Array.isArray(game.players)) {
|
||||
game.players = Object.values(game.players)
|
||||
}
|
||||
const gameObject: Game = {
|
||||
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: game.scoreMode || ScoreMode.FINAL,
|
||||
shapeMode: game.shapeMode || ShapeMode.ANY,
|
||||
snapMode: game.snapMode || SnapMode.NORMAL,
|
||||
}
|
||||
const gameObject: Game = storeDataToGame(game, null)
|
||||
GameCommon.setGame(gameObject.id, gameObject)
|
||||
}
|
||||
|
||||
function persistGames(): void {
|
||||
for (const gameId of Object.keys(dirtyGames)) {
|
||||
persistGame(gameId)
|
||||
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 persistGame(gameId: string): void {
|
||||
const game = GameCommon.get(gameId)
|
||||
if (!game) {
|
||||
log.error(`[ERROR] unable to persist non existing game ${gameId}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (game.id in dirtyGames) {
|
||||
setClean(game.id)
|
||||
}
|
||||
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({
|
||||
function gameToStoreData(game: Game): string {
|
||||
return JSON.stringify({
|
||||
id: game.id,
|
||||
rng: {
|
||||
type: game.rng.type,
|
||||
|
|
@ -92,14 +151,18 @@ function persistGame(gameId: string): void {
|
|||
scoreMode: game.scoreMode,
|
||||
shapeMode: game.shapeMode,
|
||||
snapMode: game.snapMode,
|
||||
}))
|
||||
log.info(`[INFO] persisted game ${game.id}`)
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
loadGames,
|
||||
loadGame,
|
||||
persistGames,
|
||||
persistGame,
|
||||
// disk functions are deprecated
|
||||
loadGamesFromDisk,
|
||||
loadGameFromDisk,
|
||||
|
||||
loadGamesFromDb,
|
||||
loadGameFromDb,
|
||||
persistGamesToDb,
|
||||
persistGameToDb,
|
||||
|
||||
setDirty,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,29 +6,11 @@ import sharp from 'sharp'
|
|||
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs'
|
||||
import Db, { OrderBy, WhereRaw } from './Db'
|
||||
import { Dim } from '../common/Geometry'
|
||||
import { logger } from '../common/Util'
|
||||
import { Timestamp } from '../common/Types'
|
||||
import Util, { logger } from '../common/Util'
|
||||
import { Tag, ImageInfo } from '../common/Types'
|
||||
|
||||
const log = logger('Images.ts')
|
||||
|
||||
interface Tag
|
||||
{
|
||||
id: number
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface ImageInfo
|
||||
{
|
||||
id: number
|
||||
filename: string
|
||||
file: string
|
||||
url: string
|
||||
title: string
|
||||
tags: Tag[]
|
||||
created: Timestamp,
|
||||
}
|
||||
|
||||
const resizeImage = async (filename: string): Promise<void> => {
|
||||
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
|
||||
return
|
||||
|
|
@ -103,12 +85,14 @@ const imageFromDb = (db: Db, imageId: number): ImageInfo => {
|
|||
const i = db.get('images', { id: imageId })
|
||||
return {
|
||||
id: i.id,
|
||||
uploaderUserId: i.uploader_user_id,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
tags: getTags(db, i.id),
|
||||
created: i.created * 1000,
|
||||
width: i.width,
|
||||
height: i.height,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,15 +131,20 @@ inner join images i on i.id = ixc.image_id ${where.sql};
|
|||
|
||||
return images.map(i => ({
|
||||
id: i.id as number,
|
||||
uploaderUserId: i.uploader_user_id,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
tags: getTags(db, i.id),
|
||||
created: i.created * 1000,
|
||||
width: i.width,
|
||||
height: i.height,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated old function, now database is used
|
||||
*/
|
||||
const allImagesFromDisk = (
|
||||
tags: string[],
|
||||
sort: string
|
||||
|
|
@ -164,24 +153,26 @@ const allImagesFromDisk = (
|
|||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||
.map(f => ({
|
||||
id: 0,
|
||||
uploaderUserId: null,
|
||||
filename: f,
|
||||
file: `${UPLOAD_DIR}/${f}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||
title: f.replace(/\.[a-z]+$/, ''),
|
||||
tags: [] as Tag[],
|
||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
width: 0, // may have to fill when the function is used again
|
||||
height: 0, // may have to fill when the function is used again
|
||||
}))
|
||||
|
||||
switch (sort) {
|
||||
case 'alpha_asc':
|
||||
images = images.sort((a, b) => {
|
||||
return a.file > b.file ? 1 : -1
|
||||
return a.filename > b.filename ? 1 : -1
|
||||
})
|
||||
break;
|
||||
|
||||
case 'alpha_desc':
|
||||
images = images.sort((a, b) => {
|
||||
return a.file < b.file ? 1 : -1
|
||||
return a.filename < b.filename ? 1 : -1
|
||||
})
|
||||
break;
|
||||
|
||||
|
|
@ -218,6 +209,20 @@ async function getDimensions(imagePath: string): Promise<Dim> {
|
|||
}
|
||||
}
|
||||
|
||||
const setTags = (db: Db, imageId: number, tags: string[]): void => {
|
||||
db.delete('image_x_category', { image_id: imageId })
|
||||
tags.forEach((tag: string) => {
|
||||
const slug = Util.slug(tag)
|
||||
const id = db.upsert('categories', { slug, title: tag }, { slug }, 'id')
|
||||
if (id) {
|
||||
db.insert('image_x_category', {
|
||||
image_id: imageId,
|
||||
category_id: id,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
allImagesFromDisk,
|
||||
imageFromDb,
|
||||
|
|
@ -225,4 +230,5 @@ export default {
|
|||
getAllTags,
|
||||
resizeImage,
|
||||
getDimensions,
|
||||
setTags,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import Util from './../common/Util'
|
||||
import { Rng } from './../common/Rng'
|
||||
import Images from './Images'
|
||||
import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle, ShapeMode } from '../common/Types'
|
||||
import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle, ShapeMode, ImageInfo } from '../common/Types'
|
||||
import { Dim, Point } from '../common/Geometry'
|
||||
|
||||
export interface PuzzleCreationImageInfo {
|
||||
file: string
|
||||
url: string
|
||||
}
|
||||
import { UPLOAD_DIR } from './Dirs'
|
||||
|
||||
export interface PuzzleCreationInfo {
|
||||
width: number
|
||||
|
|
@ -27,11 +23,11 @@ const TILE_SIZE = 64
|
|||
async function createPuzzle(
|
||||
rng: Rng,
|
||||
targetTiles: number,
|
||||
image: PuzzleCreationImageInfo,
|
||||
image: ImageInfo,
|
||||
ts: number,
|
||||
shapeMode: ShapeMode
|
||||
): Promise<Puzzle> {
|
||||
const imagePath = image.file
|
||||
const imagePath = `${UPLOAD_DIR}/${image.filename}`
|
||||
const imageUrl = image.url
|
||||
|
||||
// determine puzzle information from the image dimensions
|
||||
|
|
@ -139,7 +135,8 @@ async function createPuzzle(
|
|||
},
|
||||
// information that was used to create the puzzle
|
||||
targetTiles: targetTiles,
|
||||
imageUrl,
|
||||
imageUrl, // todo: remove
|
||||
image: image,
|
||||
|
||||
width: info.width, // actual puzzle width (same as bitmap.width)
|
||||
height: info.height, // actual puzzle height (same as bitmap.height)
|
||||
|
|
|
|||
36
src/server/Users.ts
Normal file
36
src/server/Users.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Time from '../common/Time'
|
||||
import Db from './Db'
|
||||
|
||||
const TABLE = 'users'
|
||||
|
||||
const HEADER_CLIENT_ID = 'client-id'
|
||||
const HEADER_CLIENT_SECRET = 'client-secret'
|
||||
|
||||
const getOrCreateUser = (db: Db, req: any): any => {
|
||||
let user = getUser(db, req)
|
||||
if (!user) {
|
||||
db.insert(TABLE, {
|
||||
'client_id': req.headers[HEADER_CLIENT_ID],
|
||||
'client_secret': req.headers[HEADER_CLIENT_SECRET],
|
||||
'created': Time.timestamp(),
|
||||
})
|
||||
user = getUser(db, req)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
const getUser = (db: Db, req: any): any => {
|
||||
const user = db.get(TABLE, {
|
||||
'client_id': req.headers[HEADER_CLIENT_ID],
|
||||
'client_secret': req.headers[HEADER_CLIENT_SECRET],
|
||||
})
|
||||
if (user) {
|
||||
user.id = parseInt(user.id, 10)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
export default {
|
||||
getOrCreateUser,
|
||||
getUser,
|
||||
}
|
||||
|
|
@ -19,9 +19,10 @@ import {
|
|||
UPLOAD_DIR,
|
||||
} from './Dirs'
|
||||
import GameCommon from '../common/GameCommon'
|
||||
import { ServerEvent, Game as GameType, GameSettings, ScoreMode, ShapeMode, SnapMode } from '../common/Types'
|
||||
import { ServerEvent, Game as GameType, GameSettings } from '../common/Types'
|
||||
import GameStorage from './GameStorage'
|
||||
import Db from './Db'
|
||||
import Users from './Users'
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||
db.patch()
|
||||
|
|
@ -57,6 +58,14 @@ const storage = multer.diskStorage({
|
|||
})
|
||||
const upload = multer({storage}).single('file');
|
||||
|
||||
app.get('/api/me', (req, res): void => {
|
||||
let user = Users.getUser(db, req)
|
||||
res.send({
|
||||
id: user ? user.id : null,
|
||||
created: user ? user.created : null,
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/api/conf', (req, res): void => {
|
||||
res.send({
|
||||
WS_ADDRESS: config.ws.connectstring,
|
||||
|
|
@ -80,18 +89,19 @@ app.get('/api/replay-data', async (req, res): Promise<void> => {
|
|||
res.status(404).send({ reason: 'no log found' })
|
||||
return
|
||||
}
|
||||
const log = await GameLog.get(gameId, offset, size)
|
||||
const log = GameLog.get(gameId, offset)
|
||||
let game: GameType|null = null
|
||||
if (offset === 0) {
|
||||
// also need the game
|
||||
game = await Game.createGameObject(
|
||||
gameId,
|
||||
log[0][2],
|
||||
log[0][3],
|
||||
log[0][3], // must be ImageInfo
|
||||
log[0][4],
|
||||
log[0][5] || ScoreMode.FINAL,
|
||||
log[0][6] || ShapeMode.NORMAL,
|
||||
log[0][7] || SnapMode.NORMAL,
|
||||
log[0][5],
|
||||
log[0][6],
|
||||
log[0][7],
|
||||
log[0][8], // creatorUserId
|
||||
)
|
||||
}
|
||||
res.send({ log, game: game ? Util.encodeGame(game) : null })
|
||||
|
|
@ -133,32 +143,27 @@ interface SaveImageRequestData {
|
|||
tags: string[]
|
||||
}
|
||||
|
||||
const setImageTags = (db: Db, imageId: number, tags: string[]): void => {
|
||||
tags.forEach((tag: string) => {
|
||||
const slug = Util.slug(tag)
|
||||
const id = db.upsert('categories', { slug, title: tag }, { slug }, 'id')
|
||||
if (id) {
|
||||
db.insert('image_x_category', {
|
||||
image_id: imageId,
|
||||
category_id: id,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
app.post('/api/save-image', express.json(), (req, res): void => {
|
||||
const user = Users.getUser(db, req)
|
||||
if (!user || !user.id) {
|
||||
res.status(403).send({ ok: false, error: 'forbidden' })
|
||||
return
|
||||
}
|
||||
|
||||
const data = req.body as SaveImageRequestData
|
||||
const image = db.get('images', {id: data.id})
|
||||
if (parseInt(image.uploader_user_id, 10) !== user.id) {
|
||||
res.status(403).send({ ok: false, error: 'forbidden' })
|
||||
return
|
||||
}
|
||||
|
||||
db.update('images', {
|
||||
title: data.title,
|
||||
}, {
|
||||
id: data.id,
|
||||
})
|
||||
|
||||
db.delete('image_x_category', { image_id: data.id })
|
||||
|
||||
if (data.tags) {
|
||||
setImageTags(db, data.id, data.tags)
|
||||
}
|
||||
Images.setTags(db, data.id, data.tags || [])
|
||||
|
||||
res.send({ ok: true })
|
||||
})
|
||||
|
|
@ -166,26 +171,36 @@ app.post('/api/upload', (req, res): void => {
|
|||
upload(req, res, async (err: any): Promise<void> => {
|
||||
if (err) {
|
||||
log.log(err)
|
||||
res.status(400).send("Something went wrong!");
|
||||
res.status(400).send("Something went wrong!")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await Images.resizeImage(req.file.filename)
|
||||
} catch (err) {
|
||||
log.log(err)
|
||||
res.status(400).send("Something went wrong!");
|
||||
res.status(400).send("Something went wrong!")
|
||||
return
|
||||
}
|
||||
|
||||
const user = Users.getOrCreateUser(db, req)
|
||||
|
||||
const dim = await Images.getDimensions(
|
||||
`${UPLOAD_DIR}/${req.file.filename}`
|
||||
)
|
||||
const imageId = db.insert('images', {
|
||||
uploader_user_id: user.id,
|
||||
filename: req.file.filename,
|
||||
filename_original: req.file.originalname,
|
||||
title: req.body.title || '',
|
||||
created: Time.timestamp(),
|
||||
width: dim.w,
|
||||
height: dim.h,
|
||||
})
|
||||
|
||||
if (req.body.tags) {
|
||||
const tags = req.body.tags.split(',').filter((tag: string) => !!tag)
|
||||
setImageTags(db, imageId as number, tags)
|
||||
Images.setTags(db, imageId as number, tags)
|
||||
}
|
||||
|
||||
res.send(Images.imageFromDb(db, imageId as number))
|
||||
|
|
@ -193,21 +208,12 @@ app.post('/api/upload', (req, res): void => {
|
|||
})
|
||||
|
||||
app.post('/api/newgame', express.json(), async (req, res): Promise<void> => {
|
||||
const gameSettings = req.body as GameSettings
|
||||
log.log(gameSettings)
|
||||
const gameId = Util.uniqId()
|
||||
if (!GameCommon.exists(gameId)) {
|
||||
const ts = Time.timestamp()
|
||||
await Game.createGame(
|
||||
gameId,
|
||||
gameSettings.tiles,
|
||||
gameSettings.image,
|
||||
ts,
|
||||
gameSettings.scoreMode,
|
||||
gameSettings.shapeMode,
|
||||
gameSettings.snapMode,
|
||||
)
|
||||
}
|
||||
const user = Users.getOrCreateUser(db, req)
|
||||
const gameId = await Game.createNewGame(
|
||||
req.body as GameSettings,
|
||||
Time.timestamp(),
|
||||
user.id
|
||||
)
|
||||
res.send({ id: gameId })
|
||||
})
|
||||
|
||||
|
|
@ -247,37 +253,6 @@ wss.on('message', async (
|
|||
const msg = JSON.parse(data as string)
|
||||
const msgType = msg[0]
|
||||
switch (msgType) {
|
||||
case Protocol.EV_CLIENT_INIT_REPLAY: {
|
||||
if (!GameLog.exists(gameId)) {
|
||||
throw `[gamelog ${gameId} does not exist... ]`
|
||||
}
|
||||
|
||||
// should connect the socket with game log
|
||||
// pseudo code
|
||||
const game = await GameLog.open(socket, gameId)
|
||||
if (!game) {
|
||||
throw `[game not created :/ ]`
|
||||
}
|
||||
notify(
|
||||
[Protocol.EV_SERVER_INIT_REPLAY, Util.encodeGame(game)],
|
||||
[socket]
|
||||
)
|
||||
} break
|
||||
|
||||
case Protocol.EV_CLIENT_REPLAY_EVENT: {
|
||||
if (!GameLog.exists(gameId)) {
|
||||
throw `[gamelog ${gameId} does not exist... ]`
|
||||
}
|
||||
// should read next some lines from game log, using the game
|
||||
// log connected with this socket
|
||||
// pseudo code
|
||||
const logEntries = await GameLog.getNextBySocket(socket)
|
||||
notify(
|
||||
[Protocol.EV_SERVER_REPLAY_EVENT, logEntries],
|
||||
[socket]
|
||||
)
|
||||
} break
|
||||
|
||||
case Protocol.EV_CLIENT_INIT: {
|
||||
if (!GameCommon.exists(gameId)) {
|
||||
throw `[game ${gameId} does not exist... ]`
|
||||
|
|
@ -336,7 +311,7 @@ wss.on('message', async (
|
|||
}
|
||||
})
|
||||
|
||||
GameStorage.loadGames()
|
||||
GameStorage.loadGamesFromDb(db)
|
||||
const server = app.listen(
|
||||
port,
|
||||
hostname,
|
||||
|
|
@ -359,7 +334,7 @@ memoryUsageHuman()
|
|||
// persist games in fixed interval
|
||||
const persistInterval = setInterval(() => {
|
||||
log.log('Persisting games...')
|
||||
GameStorage.persistGames()
|
||||
GameStorage.persistGamesToDb(db)
|
||||
|
||||
memoryUsageHuman()
|
||||
}, config.persistence.interval)
|
||||
|
|
@ -371,7 +346,7 @@ const gracefulShutdown = (signal: string): void => {
|
|||
clearInterval(persistInterval)
|
||||
|
||||
log.log('persisting games...')
|
||||
GameStorage.persistGames()
|
||||
GameStorage.persistGamesToDb(db)
|
||||
|
||||
log.log('shutting down webserver...')
|
||||
server.close()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue