449 lines
10 KiB
JavaScript
449 lines
10 KiB
JavaScript
/*
|
|
* draw pixel on canvas
|
|
*/
|
|
import { using } from 'bluebird';
|
|
|
|
import { redlock } from '../data/redis';
|
|
import {
|
|
getPixelFromChunkOffset,
|
|
} from './utils';
|
|
import logger, { pixelLogger } from './logger';
|
|
import RedisCanvas from '../data/models/RedisCanvas';
|
|
import {
|
|
setPixelByOffset,
|
|
setPixelByCoords,
|
|
} from './setPixel';
|
|
import rankings from './ranking';
|
|
import rpgEvent from './event';
|
|
// eslint-disable-next-line import/no-unresolved
|
|
import canvases from './canvases.json';
|
|
|
|
import { THREE_CANVAS_HEIGHT, THREE_TILE_SIZE, TILE_SIZE } from './constants';
|
|
|
|
|
|
/**
|
|
*
|
|
* By Offset is prefered on server side
|
|
* This gets used by websocket pixel placing requests
|
|
* @param user user that can be registered, but doesn't have to
|
|
* @param canvasId
|
|
* @param i Chunk coordinates
|
|
* @param j
|
|
* @param pixels Array of indiviual pixels within the chunk, with:
|
|
* [[offset, color], [offset2, color2],...]
|
|
* Offset is the offset of the pixel within the chunk
|
|
* @return Promise<Object>
|
|
*/
|
|
export async function drawByOffsets(
|
|
user,
|
|
canvasId,
|
|
i,
|
|
j,
|
|
pixels,
|
|
) {
|
|
let wait = 0;
|
|
let coolDown = 0;
|
|
let retCode = 0;
|
|
let pxlCnt = 0;
|
|
|
|
const canvas = canvases[canvasId];
|
|
|
|
try {
|
|
if (!canvas) {
|
|
// canvas doesn't exist
|
|
throw new Error(1);
|
|
}
|
|
|
|
const canvasSize = canvas.size;
|
|
const is3d = !!canvas.v;
|
|
|
|
wait = await user.getWait(canvasId);
|
|
|
|
const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE;
|
|
/*
|
|
* canvas/chunk validation
|
|
*/
|
|
if (i >= canvasSize / tileSize) {
|
|
// x out of bounds
|
|
// (we don't have to check for <0 becaue it is received as uint)
|
|
throw new Error(2);
|
|
}
|
|
if (j >= canvasSize / tileSize) {
|
|
// y out of bounds
|
|
// (we don't have to check for <0 becaue it is received as uint)
|
|
throw new Error(3);
|
|
}
|
|
|
|
const isAdmin = (user.userlvl === 1);
|
|
|
|
if (canvas.req !== undefined && !isAdmin) {
|
|
if (user.id === null) {
|
|
// not logged in
|
|
throw new Error(6);
|
|
}
|
|
if (canvas.req > 0) {
|
|
const totalPixels = await user.getTotalPixels();
|
|
if (totalPixels < canvas.req) {
|
|
// not enough pixels placed yet
|
|
throw new Error(7);
|
|
}
|
|
}
|
|
if (canvas.req === 'top' && !rankings.prevTop.includes(user.id)) {
|
|
throw new Error(12);
|
|
}
|
|
}
|
|
|
|
let coolDownFactor = 1;
|
|
if (rpgEvent.success) {
|
|
if (rpgEvent.success === 1) {
|
|
// if hourly event got won
|
|
coolDownFactor = 0.5;
|
|
} else {
|
|
// if hourly event got lost
|
|
coolDownFactor = 2;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* TODO benchmark if requesting by pixel or chunk is better
|
|
*/
|
|
|
|
while (pixels.length) {
|
|
const [offset, color] = pixels.pop();
|
|
|
|
|
|
const [x, y, z] = getPixelFromChunkOffset(i, j, offset, canvasSize, is3d);
|
|
pixelLogger.info(
|
|
`${user.ip} ${user.id} ${canvasId} ${x} ${y} ${z} ${color} ${retCode}`,
|
|
);
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const setColor = await RedisCanvas.getPixelByOffset(
|
|
canvasId,
|
|
i, j,
|
|
offset,
|
|
);
|
|
|
|
const clrIgnore = canvas.cli || 0;
|
|
|
|
/*
|
|
* pixel validation
|
|
*/
|
|
const maxSize = (is3d) ? tileSize * tileSize * THREE_CANVAS_HEIGHT
|
|
: tileSize * tileSize;
|
|
if (offset >= maxSize) {
|
|
// z out of bounds or weird stuff
|
|
throw new Error(4);
|
|
}
|
|
if (color >= canvas.colors.length
|
|
|| (color < clrIgnore && !(canvas.v && color === 0))
|
|
) {
|
|
// color out of bounds
|
|
throw new Error(5);
|
|
}
|
|
|
|
if (setColor & 0x80
|
|
/* 3D Canvas Minecraft Avatars */
|
|
// && x >= 96 && x <= 128 && z >= 35 && z <= 100
|
|
// 96 - 128 on x
|
|
// 32 - 128 on z
|
|
|| (canvas.v && i === 19 && j >= 17 && j < 20 && !isAdmin)
|
|
) {
|
|
// protected pixel
|
|
throw new Error(8);
|
|
}
|
|
|
|
coolDown = ((setColor & 0x3F) >= clrIgnore && canvas.pcd)
|
|
? canvas.pcd : canvas.bcd;
|
|
if (isAdmin) {
|
|
coolDown = 0.0;
|
|
} else {
|
|
coolDown *= coolDownFactor;
|
|
// temporary lowered cooldown
|
|
}
|
|
|
|
wait += coolDown;
|
|
if (wait > canvas.cds) {
|
|
// cooldown stack used
|
|
wait -= coolDown;
|
|
coolDown = canvas.cds - wait - coolDown;
|
|
throw new Error(9);
|
|
}
|
|
|
|
setPixelByOffset(canvasId, color, i, j, offset);
|
|
pxlCnt += 1;
|
|
}
|
|
} catch (e) {
|
|
retCode = parseInt(e.message, 10);
|
|
if (Number.isNaN(retCode)) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
if (pxlCnt) {
|
|
user.setWait(wait, canvasId);
|
|
if (canvas.ranked) {
|
|
user.incrementPixelcount(pxlCnt);
|
|
}
|
|
}
|
|
|
|
return {
|
|
wait,
|
|
coolDown,
|
|
pxlCnt,
|
|
retCode,
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* Old version of draw that returns explicit error messages
|
|
* used for http json api/pixel, used with coordinates
|
|
* Is not used anywhere currently, but we keep it around.
|
|
* @param user
|
|
* @param canvasId
|
|
* @param x
|
|
* @param y
|
|
* @param color
|
|
* @returns {Promise.<Object>}
|
|
*/
|
|
export async function drawByCoords(
|
|
user,
|
|
canvasId,
|
|
color,
|
|
x,
|
|
y,
|
|
z = null,
|
|
) {
|
|
const canvas = canvases[canvasId];
|
|
|
|
if (!canvas) {
|
|
return {
|
|
error: 'This canvas does not exist',
|
|
success: false,
|
|
};
|
|
}
|
|
|
|
const canvasMaxXY = canvas.size / 2;
|
|
const canvasMinXY = -canvasMaxXY;
|
|
if (x < canvasMinXY || x >= canvasMaxXY) {
|
|
return {
|
|
error: 'x Coordinate not within canvas',
|
|
success: false,
|
|
};
|
|
}
|
|
|
|
const clrIgnore = canvas.cli || 0;
|
|
|
|
if (canvas.v) {
|
|
if (z < canvasMinXY || z >= canvasMaxXY) {
|
|
return {
|
|
error: 'z Coordinate not within canvas',
|
|
success: false,
|
|
};
|
|
}
|
|
if (y >= THREE_CANVAS_HEIGHT) {
|
|
return {
|
|
error: 'You reached build limit. Can\'t place higher than 128 blocks.',
|
|
success: false,
|
|
};
|
|
}
|
|
if (y < 0) {
|
|
return {
|
|
error: 'Can\'t place on y < 0',
|
|
success: false,
|
|
};
|
|
}
|
|
if (z === null) {
|
|
return {
|
|
error: 'This is a 3D canvas. z is required.',
|
|
success: false,
|
|
};
|
|
}
|
|
} else {
|
|
if (y < canvasMinXY || y >= canvasMaxXY) {
|
|
return {
|
|
error: 'y Coordinate not within canvas',
|
|
success: false,
|
|
};
|
|
}
|
|
if (color < clrIgnore) {
|
|
return {
|
|
error: 'Invalid color selected',
|
|
success: false,
|
|
};
|
|
}
|
|
if (z !== null) {
|
|
if (!canvas.v) {
|
|
return {
|
|
error: 'This is not a 3D canvas',
|
|
success: false,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (color < 0 || color >= canvas.colors.length) {
|
|
return {
|
|
error: 'Invalid color selected',
|
|
success: false,
|
|
};
|
|
}
|
|
|
|
if (canvas.req !== -1) {
|
|
if (user.id === null) {
|
|
return {
|
|
errorTitle: 'Not Logged In',
|
|
error: 'You need to be logged in to use this canvas.',
|
|
success: false,
|
|
};
|
|
}
|
|
// if the canvas has a requirement of totalPixels that the user
|
|
// has to have set
|
|
const totalPixels = await user.getTotalPixels();
|
|
if (totalPixels < canvas.req) {
|
|
return {
|
|
errorTitle: 'Not Yet :(',
|
|
// eslint-disable-next-line max-len
|
|
error: `You need to set ${canvas.req} pixels on another canvas first, before you can use this one.`,
|
|
success: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
const isAdmin = (user.userlvl === 1);
|
|
const setColor = await RedisCanvas.getPixel(canvasId, x, y, z);
|
|
|
|
/*
|
|
* bitwise operation to get rid of protection
|
|
*/
|
|
let coolDown = ((setColor & 0x3F) >= clrIgnore && canvas.pcd)
|
|
? canvas.pcd : canvas.bcd;
|
|
if (isAdmin) {
|
|
coolDown = 0.0;
|
|
} else if (rpgEvent.success) {
|
|
if (rpgEvent.success === 1) {
|
|
// if HOURLY_EVENT got won
|
|
coolDown /= 2;
|
|
} else {
|
|
// if HOURLY_EVENT got lost
|
|
coolDown *= 2;
|
|
}
|
|
}
|
|
|
|
const now = Date.now();
|
|
let wait = await user.getWait(canvasId);
|
|
if (!wait) wait = now;
|
|
wait += coolDown;
|
|
const waitLeft = wait - now;
|
|
if (waitLeft > canvas.cds) {
|
|
return {
|
|
success: false,
|
|
waitSeconds: (waitLeft - coolDown) / 1000,
|
|
coolDownSeconds: (canvas.cds - waitLeft) / 1000,
|
|
};
|
|
}
|
|
|
|
if (setColor & 0x80
|
|
|| (canvas.v
|
|
&& x >= 96 && x <= 128 && z >= 35 && z <= 100
|
|
&& !isAdmin)
|
|
) {
|
|
logger.info(`${user.ip} tried to set on protected pixel (${x}, ${y})`);
|
|
return {
|
|
errorTitle: 'Pixel Protection',
|
|
error: 'This pixel is protected',
|
|
success: false,
|
|
waitSeconds: (waitLeft - coolDown) / 1000,
|
|
};
|
|
}
|
|
|
|
setPixelByCoords(canvasId, color, x, y, z);
|
|
|
|
user.setWait(waitLeft, canvasId);
|
|
/* hardcode to not count pixels in antarctica */
|
|
// eslint-disable-next-line eqeqeq
|
|
if (canvas.ranked && (canvasId != 0 || y < 14450)) {
|
|
user.incrementPixelcount();
|
|
}
|
|
return {
|
|
success: true,
|
|
waitSeconds: waitLeft / 1000,
|
|
coolDownSeconds: coolDown / 1000,
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* This function is a wrapper for draw. It fixes race condition exploits
|
|
* It permits just placing one pixel at a time per user.
|
|
*
|
|
* @param user
|
|
* @param canvasId
|
|
* @param color
|
|
* @param x
|
|
* @param y
|
|
* @param z (optional for 3d canvas)
|
|
*/
|
|
export function drawSafeByCoords(
|
|
user,
|
|
canvasId,
|
|
color,
|
|
x,
|
|
y,
|
|
z = null,
|
|
) {
|
|
// can just check for one unique occurence,
|
|
// we use ip, because id for logged out users is
|
|
// always null
|
|
const userId = user.ip;
|
|
|
|
return new Promise((resolve) => {
|
|
using(
|
|
redlock.disposer(`locks:${userId}`, 5000, logger.error),
|
|
async () => {
|
|
const ret = await drawByCoords(user, canvasId, color, x, y, z);
|
|
resolve(ret);
|
|
},
|
|
); // <-- unlock is automatically handled by bluebird
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* This function is a wrapper for draw. It fixes race condition exploits
|
|
* It permits just placing one pixel at a time per user.
|
|
*
|
|
* @param user
|
|
* @param canvasId
|
|
* @param i Chunk coordinates
|
|
* @param j
|
|
* @param pixels Array of indiviual pixels within the chunk, with:
|
|
* [[offset, color], [offset2, color2],...]
|
|
* Offset is the offset of the pixel within the chunk
|
|
* @return Promise<Object>
|
|
*/
|
|
export function drawSafeByOffsets(
|
|
user,
|
|
canvasId,
|
|
i,
|
|
j,
|
|
pixels,
|
|
) {
|
|
// can just check for one unique occurence,
|
|
// we use ip, because id for logged out users is
|
|
// always null
|
|
const userId = user.ip;
|
|
|
|
return new Promise((resolve) => {
|
|
using(
|
|
redlock.disposer(`locks:${userId}`, 5000, logger.error),
|
|
async () => {
|
|
const ret = await drawByOffsets(user, canvasId, i, j, pixels);
|
|
resolve(ret);
|
|
},
|
|
); // <-- unlock is automatically handled by bluebird
|
|
});
|
|
}
|