start to move set pixel request to websocket

remove killing of old websocket after 30min
catch binary ws package errors
change CoolDownPacket to ms
stop messing with void bot
fix reddit login image alt-text
force subrender on pixel set to avoid seemingly freeze
excape redex characters in username
This commit is contained in:
HF 2020-05-14 01:27:27 +02:00
parent 897b56a9b9
commit 3a7ecf21cc
33 changed files with 828 additions and 378 deletions

View File

@ -163,19 +163,15 @@ export function selectCanvas(canvasId: number): Action {
};
}
export function placePixel(coordinates: Cell, color: ColorIndex): Action {
export function placedPixel(): Action {
return {
type: 'PLACE_PIXEL',
coordinates,
color,
};
}
export function pixelWait(coordinates: Cell, color: ColorIndex): Action {
export function pixelWait(): Action {
return {
type: 'PIXEL_WAIT',
coordinates,
color,
};
}
@ -230,92 +226,120 @@ export function notify(notification: string) {
};
}
export function requestPlacePixel(
canvasId: number,
coordinates: Cell,
let pixelTimeout = null;
export function tryPlacePixel(
i: number,
j: number,
offset: number,
color: ColorIndex,
token: ?string = null,
): ThunkAction {
const [x, y, z] = coordinates;
return async (dispatch) => {
const body = JSON.stringify({
cn: canvasId,
x,
y,
z,
clr: color,
token,
pixelTimeout = Date.now() + 5000;
await dispatch(setPlaceAllowed(false));
// TODO:
// this is for resending after captcha returned
// window is ugly, put it into redux or something
window.pixel = {
i,
j,
offset,
color,
};
dispatch({
type: 'REQUEST_PLACE_PIXEL',
i,
j,
offset,
color,
});
dispatch(setPlaceAllowed(false));
try {
const response = await fetch('/api/pixel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
// https://github.com/github/fetch/issues/349
credentials: 'include',
});
const {
success,
waitSeconds,
coolDownSeconds,
errors,
errorTitle,
} = await response.json();
if (waitSeconds) {
dispatch(setWait(waitSeconds * 1000));
}
const coolDownNotify = Math.round(coolDownSeconds);
if (coolDownSeconds) {
dispatch(notify(coolDownNotify));
}
if (response.ok) {
if (success) {
dispatch(placePixel(coordinates, color));
} else {
dispatch(pixelWait(coordinates, color));
}
return;
}
if (response.status === 422) {
window.pixel = { canvasId, coordinates, color };
window.grecaptcha.execute();
return;
}
dispatch(pixelFailure());
dispatch(sweetAlert(
(errorTitle || `Error ${response.status}`),
errors[0].msg,
'error',
'OK',
));
} finally {
dispatch(setPlaceAllowed(true));
}
};
}
export function tryPlacePixel(
coordinates: Cell,
color: ?ColorIndex = null,
export function receivePixelReturn(
retCode: number,
wait: number,
coolDownSeconds: number,
): ThunkAction {
return (dispatch, getState) => {
const state = getState();
const {
canvasId,
} = state.canvas;
const selectedColor = (color === undefined || color === null)
? state.canvas.selectedColor
: color;
return (dispatch) => {
try {
if (wait) {
dispatch(setWait(wait));
}
if (coolDownSeconds) {
dispatch(notify(coolDownSeconds));
}
dispatch(requestPlacePixel(canvasId, coordinates, selectedColor));
let errorTitle = null;
let msg = null;
switch (retCode) {
case 0:
dispatch(placedPixel());
break;
case 1:
errorTitle = 'Invalid Canvas';
msg = 'This canvas doesn\'t exist';
break;
case 2:
errorTitle = 'Invalid Coordinates';
msg = 'x out of bounds';
break;
case 3:
errorTitle = 'Invalid Coordinates';
msg = 'y out of bounds';
break;
case 4:
errorTitle = 'Invalid Coordinates';
msg = 'z out of bounds';
break;
case 5:
errorTitle = 'Wrong Color';
msg = 'Invalid color selected';
break;
case 6:
errorTitle = 'Just for registered Users';
msg = 'You have to be logged in to place on this canvas';
break;
case 7:
errorTitle = 'Place more :)';
// eslint-disable-next-line max-len
msg = 'You can not access this canvas yet. You need to place more pixels';
break;
case 8:
errorTitle = 'Oww noo';
msg = 'This pixel is protected.';
break;
case 9:
// pixestack used up
dispatch(pixelWait());
break;
case 10:
// captcha
window.grecaptcha.execute();
break;
case 11:
errorTitle = 'No Proxies Allowed :(';
msg = 'You are using a Proxy.';
break;
default:
errorTitle = 'Weird';
msg = 'Couldn\'t set Pixel';
}
if (msg) {
dispatch(pixelFailure());
dispatch(sweetAlert(
(errorTitle || `Error ${retCode}`),
msg,
'error',
'OK',
));
}
} finally {
pixelTimeout = null;
dispatch(setPlaceAllowed(true));
}
};
}
@ -428,11 +452,11 @@ export function receiveBigChunkFailure(center: Cell, error: Error): Action {
}
export function receiveCoolDown(
waitSeconds: number,
wait: number,
): Action {
return {
type: 'RECEIVE_COOLDOWN',
waitSeconds,
wait,
};
}
@ -565,14 +589,28 @@ function endCoolDown(): Action {
function getPendingActions(state): Array<Action> {
const actions = [];
const now = Date.now();
const { wait } = state.user;
if (wait === null || wait === undefined) return actions;
const coolDown = wait - Date.now();
const coolDown = wait - now;
if (coolDown > 0) actions.push(setCoolDown(coolDown));
else actions.push(endCoolDown());
if (wait !== null && wait !== undefined) {
if (coolDown > 0) actions.push(setCoolDown(coolDown));
else actions.push(endCoolDown());
}
if (pixelTimeout && now > pixelTimeout) {
actions.push(pixelFailure());
pixelTimeout = null;
actions.push(setPlaceAllowed(true));
actions.push(sweetAlert(
'Error :(',
'Didn\'t get an answer from pixelplanet. Maybe try to refresh?',
'error',
'OK',
));
}
return actions;
}

View File

@ -32,13 +32,20 @@ export type Action =
| { type: 'SET_HOVER', hover: Cell }
| { type: 'UNSET_HOVER' }
| { type: 'SET_WAIT', wait: ?number }
| { type: 'RECEIVE_COOLDOWN', wait: number }
| { type: 'SET_MOBILE', mobile: boolean }
| { type: 'COOLDOWN_END' }
| { type: 'COOLDOWN_SET', coolDown: number }
| { type: 'SELECT_COLOR', color: ColorIndex }
| { type: 'SELECT_CANVAS', canvasId: number }
| { type: 'PLACE_PIXEL', coordinates: Cell, color: ColorIndex, wait: string }
| { type: 'PIXEL_WAIT', coordinates: Cell, color: ColorIndex, wait: string }
| { type: 'REQUEST_PLACE_PIXEL',
i: number,
j: number,
offset: number,
color: ColorIndex,
}
| { type: 'PLACE_PIXEL' }
| { type: 'PIXEL_WAIT' }
| { type: 'PIXEL_FAILURE' }
| { type: 'SET_VIEW_COORDINATES', view: Cell }
| { type: 'SET_SCALE', scale: number, zoompoint: Cell }
@ -46,7 +53,6 @@ export type Action =
| { type: 'PRE_LOADED_BIG_CHUNK', center: Cell }
| { type: 'RECEIVE_BIG_CHUNK', center: Cell }
| { type: 'RECEIVE_BIG_CHUNK_FAILURE', center: Cell, error: Error }
| { type: 'RECEIVE_COOLDOWN', waitSeconds: number }
| { type: 'RECEIVE_PIXEL_UPDATE',
i: number,
j: number,

View File

@ -1,5 +1,6 @@
/* @flow */
// eslint-disable-next-line no-unused-vars
import fetch from 'isomorphic-fetch'; // TODO put in the beggining with webpack!
import './styles/font.css';
@ -8,14 +9,15 @@ import './styles/font.css';
import onKeyPress from './controls/keypress';
import {
receivePixelUpdate,
receiveCoolDown,
fetchMe,
fetchStats,
initTimer,
urlChange,
receiveOnline,
receiveCoolDown,
receiveChatMessage,
receiveChatHistory,
receivePixelReturn,
setMobile,
} from './actions';
import store from './ui/store';
@ -35,8 +37,13 @@ function init() {
}) => {
store.dispatch(receivePixelUpdate(i, j, offset, color));
});
ProtocolClient.on('cooldownPacket', (waitSeconds) => {
store.dispatch(receiveCoolDown(waitSeconds));
ProtocolClient.on('pixelReturn', ({
retCode, wait, coolDownSeconds,
}) => {
store.dispatch(receivePixelReturn(retCode, wait, coolDownSeconds));
});
ProtocolClient.on('cooldownPacket', (coolDown) => {
store.dispatch(receiveCoolDown(coolDown));
});
ProtocolClient.on('onlineCounter', ({ online }) => {
store.dispatch(receiveOnline(online));
@ -74,27 +81,6 @@ function init() {
store.dispatch(fetchStats());
setInterval(() => { store.dispatch(fetchStats()); }, 300000);
// mess with void bot :)
function ayylmao() {
let cnt = 0;
for (let i = 0; i < document.body.children.length; i += 1) {
const node = document.body.children[i];
if (node.nodeName === 'SCRIPT' && node.src === '') {
cnt += 1;
}
}
if (cnt > 1) {
document.body.style.setProperty(
'-webkit-transform', 'rotate(-180deg)',
null,
);
fetch('https://assets.pixelplanet.fun/iamabot');
window.fetch = () => true;
}
}
ayylmao();
setInterval(ayylmao, 120000);
}
init();

View File

@ -18,6 +18,9 @@ import ProtocolClient from '../socket/ProtocolClient';
import { saveSelection, restoreSelection } from '../utils/storeSelection';
import splitChatMessage from '../core/chatMessageFilter';
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
const Chat = ({
chatMessages,
@ -50,7 +53,7 @@ const Chat = ({
useEffect(() => {
const regExp = (ownName)
? new RegExp(`(^|\\s+)(@${ownName})(\\s+|$)`, 'g')
? new RegExp(`(^|\\s+)(@${escapeRegExp(ownName)})(\\s+|$)`, 'g')
: null;
setNameRegExp(regExp);
}, [ownName]);

View File

@ -8,13 +8,28 @@
import React from 'react';
import store from '../ui/store';
import { requestPlacePixel } from '../actions';
import { tryPlacePixel } from '../actions';
function onCaptcha(token: string) {
const { canvasId, coordinates, color } = window.pixel;
async function onCaptcha(token: string) {
const body = JSON.stringify({
token,
});
await fetch('/api/captcha', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
// https://github.com/github/fetch/issues/349
credentials: 'include',
});
const {
i, j, offset, color,
} = window.pixel;
store.dispatch(tryPlacePixel(i, j, offset, color));
store.dispatch(requestPlacePixel(canvasId, coordinates, color, token));
window.grecaptcha.reset();
}
// https://stackoverflow.com/questions/41717304/recaptcha-google-data-callback-with-angularjs

View File

@ -65,7 +65,7 @@ const LogInArea = ({ register, forgotPassword, me }) => (
style={logoStyle}
width={32}
src={`${window.assetserver}/vklogo.svg`}
alt="vk"
alt="VK"
/>
</a>
<a href="./api/auth/reddit">
@ -73,7 +73,7 @@ const LogInArea = ({ register, forgotPassword, me }) => (
style={logoStyle}
width={32}
src={`${window.assetserver}/redditlogo.svg`}
alt="vk"
alt="Reddit"
/>
</a>
<h2>or register here:</h2>

View File

@ -24,6 +24,8 @@ import {
} from '../actions';
import {
screenToWorld,
getChunkOfPixel,
getOffsetOfPixel,
} from '../core/utils';
let store = null;
@ -145,11 +147,14 @@ export function initControls(renderer, viewport: HTMLCanvasElement, curStore) {
if (!placeAllowed) return;
// dirty trick: to fetch only before multiple 3 AND on user action
// if (pixelsPlaced % 3 === 0) requestAds();
if (selectedColor !== renderer.getColorIndexOfPixel(...cell)) {
store.dispatch(tryPlacePixel(cell));
const { canvasSize } = state.canvas;
const [i, j] = getChunkOfPixel(canvasSize, ...cell);
const offset = getOffsetOfPixel(canvasSize, ...cell);
store.dispatch(tryPlacePixel(
i, j, offset,
selectedColor,
));
}
});

View File

@ -7,11 +7,6 @@ import User from '../data/models/User';
import webSockets from '../socket/websockets';
import { CHAT_CHANNELS } from './constants';
import { cheapDetector } from './isProxy';
import {
USE_PROXYCHECK,
} from './config';
export class ChatProvider {
/*
@ -93,14 +88,6 @@ export class ChatProvider {
}
}
if (USE_PROXYCHECK && user.ip && await cheapDetector(user.ip)) {
logger.info(
`${name} / ${user.ip} tried to send chat message with proxy`,
);
return 'You can not send chat messages with a proxy';
}
for (let i = 0; i < this.substitutes.length; i += 1) {
const subsitute = this.substitutes[i];
message = message.replace(subsitute.regexp, subsitute.replace);

View File

@ -4,24 +4,30 @@ import { using } from 'bluebird';
import type { User } from '../data/models';
import { redlock } from '../data/redis';
import { getChunkOfPixel, getOffsetOfPixel } from './utils';
import {
getChunkOfPixel,
getOffsetOfPixel,
getPixelFromChunkOffset,
} from './utils';
import webSockets from '../socket/websockets';
import logger from './logger';
import logger, { pixelLogger } from './logger';
import RedisCanvas from '../data/models/RedisCanvas';
// eslint-disable-next-line import/no-unresolved
import canvases from './canvases.json';
import { THREE_CANVAS_HEIGHT } from './constants';
import { THREE_CANVAS_HEIGHT, THREE_TILE_SIZE, TILE_SIZE } from './constants';
/**
*
* @param canvasId
* @param canvasId
* @param color
* @param x
* @param y
* @param color
* @param z optional, if given its 3d canvas
*/
export function setPixel(
export function setPixelByCoords(
canvasId: number,
color: ColorIndex,
x: number,
@ -37,6 +43,150 @@ export function setPixel(
/**
*
* By Offset is prefered on server side
* @param canvasId
* @param i Chunk coordinates
* @param j
* @param offset Offset of pixel withing chunk
*/
export function setPixelByOffset(
canvasId: number,
color: ColorIndex,
i: number,
j: number,
offset: number,
) {
RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId);
webSockets.broadcastPixel(canvasId, i, j, offset, color);
}
/**
*
* 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 offset Offset of pixel withing chunk
*/
export async function drawByOffset(
user: User,
canvasId: number,
color: ColorIndex,
i: number,
j: number,
offset: number,
): Promise<Object> {
let wait = 0;
let coolDown = 0;
let retCode = 0;
logger.info(`Got request for ${canvasId} ${i} ${j} ${offset} ${color}`);
const canvas = canvases[canvasId];
if (!canvas) {
// canvas doesn't exist
return {
wait,
coolDown,
retCode: 1,
};
}
const { size: canvasSize, v: is3d } = canvas;
try {
const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE;
if (i >= canvasSize / tileSize) {
// x out of bounds
throw new Error(2);
}
if (j >= canvasSize / tileSize) {
// y out of bounds
throw new Error(3);
}
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 out of bounds
throw new Error(5);
}
if (canvas.req !== -1) {
if (user.id === null) {
// not logged in
throw new Error(6);
}
const totalPixels = await user.getTotalPixels();
if (totalPixels < canvas.req) {
// not enough pixels placed yet
throw new Error(7);
}
}
const setColor = await RedisCanvas.getPixelByOffset(canvasId, i, j, offset);
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 && !user.isAdmin())
) {
// protected pixel
throw new Error(8);
}
coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd;
if (user.isAdmin()) {
coolDown = 0.0;
}
const now = Date.now();
wait = await user.getWait(canvasId);
if (!wait) wait = now;
wait += coolDown;
const waitLeft = wait - now;
if (waitLeft > canvas.cds) {
// cooldown stack used
wait = waitLeft - coolDown;
coolDown = canvas.cds - waitLeft;
throw new Error(9);
}
setPixelByOffset(canvasId, color, i, j, offset);
user.setWait(waitLeft, canvasId);
if (canvas.ranked) {
user.incrementPixelcount();
}
wait = waitLeft;
} catch (e) {
retCode = parseInt(e.message, 10);
if (Number.isNaN(retCode)) {
throw e;
}
}
const [x, y, z] = getPixelFromChunkOffset(i, j, offset, canvasSize, is3d);
// eslint-disable-next-line max-len
pixelLogger.info(`${user.ip} ${user.id} ${canvasId} ${x} ${y} ${z} ${color} ${retCode}`);
return {
wait,
coolDown,
retCode,
};
}
/**
*
* Old version of draw that returns explicit error messages
* used for http json api/pixel, used with coordinates
* @param user
* @param canvasId
* @param x
@ -44,7 +194,7 @@ export function setPixel(
* @param color
* @returns {Promise.<Object>}
*/
async function draw(
export async function drawByCoords(
user: User,
canvasId: number,
color: ColorIndex,
@ -178,7 +328,7 @@ async function draw(
};
}
setPixel(canvasId, color, x, y, z);
setPixelByCoords(canvasId, color, x, y, z);
user.setWait(waitLeft, canvasId);
if (canvas.ranked) {
@ -191,18 +341,20 @@ async function draw(
};
}
/**
* 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 color
* @param z (optional for 3d canvas)
* @returns {Promise.<boolean>}
*/
function drawSafe(
export function drawSafeByCoords(
user: User,
canvasId: number,
color: ColorIndex,
@ -211,7 +363,7 @@ function drawSafe(
z: number = null,
): Promise<Cell> {
if (user.isAdmin()) {
return draw(user, canvasId, color, x, y, z);
return drawByCoords(user, canvasId, color, x, y, z);
}
// can just check for one unique occurence,
@ -223,7 +375,7 @@ function drawSafe(
using(
redlock.disposer(`locks:${userId}`, 5000, logger.error),
async () => {
const ret = await draw(user, canvasId, color, x, y, z);
const ret = await drawByCoords(user, canvasId, color, x, y, z);
resolve(ret);
},
); // <-- unlock is automatically handled by bluebird
@ -231,6 +383,42 @@ function drawSafe(
}
export const drawUnsafe = draw;
/**
* 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 i Chunk coordinates
* @param j
* @param offset Offset of pixel withing chunk
* @returns {Promise.<boolean>}
*/
export function drawSafeByOffset(
user: User,
canvasId: number,
color: ColorIndex,
i: number,
j: number,
offset: number,
): Promise<Cell> {
if (user.isAdmin()) {
return drawByOffset(user, canvasId, color, i, j, offset);
}
export default drawSafe;
// 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 drawByOffset(user, canvasId, color, i, j, offset);
resolve(ret);
},
); // <-- unlock is automatically handled by bluebird
});
}

View File

@ -162,7 +162,7 @@ async function withoutCache(f, ip) {
let lock = 4;
const checking = [];
async function withCache(f, ip) {
if (!ip) return true;
if (!ip || ip === '0.0.0.1') return true;
// get from cache, if there
const ipKey = getIPv6Subnet(ip);
const key = `isprox:${ipKey}`;

View File

@ -38,6 +38,8 @@ export function clamp(n: number, min: number, max: number): number {
return Math.max(min, Math.min(n, max));
}
// z is assumed to be height here
// in ui and rendeer, y is height
export function getChunkOfPixel(
canvasSize: number,
x: number,
@ -109,18 +111,26 @@ export function getIdFromObject(obj: Object, ident: string): number {
return null;
}
// z is returned as height here
// in ui and rendeer, y is height
export function getPixelFromChunkOffset(
i: number,
j: number,
offset: number,
canvasSize: number,
is3d: boolean = false,
): Cell {
const cx = mod(offset, TILE_SIZE);
const cy = Math.floor(offset / TILE_SIZE);
const devOffset = canvasSize / 2 / TILE_SIZE;
const x = ((i - devOffset) * TILE_SIZE) + cx;
const y = ((j - devOffset) * TILE_SIZE) + cy;
return [x, y];
const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE;
const cx = offset % tileSize;
const off = offset - cx;
let cy = off % (tileSize * tileSize);
const z = (is3d) ? (off - cy) / tileSize / tileSize : null;
cy /= tileSize;
const devOffset = canvasSize / 2 / tileSize;
const x = ((i - devOffset) * tileSize) + cx;
const y = ((j - devOffset) * tileSize) + cy;
return [x, y, z];
}
export function getCellInsideChunk(

View File

@ -94,16 +94,13 @@ class RedisCanvas {
static async getPixelIfExists(
canvasId: number,
x: number,
y: number,
z: number = null,
i: number,
j: number,
offset: number,
): Promise<number> {
// 1st bit -> protected or not
// 2nd bit -> unused
// rest (6 bits) -> index of color
const canvasSize = canvases[canvasId].size;
const [i, j] = getChunkOfPixel(canvasSize, x, y, z);
const offset = getOffsetOfPixel(canvasSize, x, y, z);
const args = [
`ch:${canvasId}:${i}:${j}`,
'GET',
@ -116,13 +113,27 @@ class RedisCanvas {
return color;
}
static async getPixelByOffset(
canvasId: number,
i: number,
j: number,
offset: number,
): Promise<number> {
const clr = RedisCanvas.getPixelIfExists(canvasId, i, j, offset);
return (clr == null) ? 0 : clr;
}
static async getPixel(
canvasId: number,
x: number,
y: number,
z: number = null,
): Promise<number> {
const clr = RedisCanvas.getPixelIfExists(canvasId, x, y, z);
const canvasSize = canvases[canvasId].size;
const [i, j] = getChunkOfPixel(canvasSize, x, y, z);
const offset = getOffsetOfPixel(canvasSize, x, y, z);
const clr = RedisCanvas.getPixelIfExists(canvasId, i, j, offset);
return (clr == null) ? 0 : clr;
}
}

View File

@ -64,7 +64,7 @@ export default function user(
const { coolDown } = action;
return {
...state,
coolDown,
coolDown: coolDown || null,
};
}
@ -96,6 +96,18 @@ export default function user(
};
}
case 'RECEIVE_COOLDOWN': {
const { wait: duration } = action;
const wait = duration
? new Date(Date.now() + duration)
: null;
return {
...state,
wait,
coolDown: null,
};
}
case 'SET_MOBILE': {
const { mobile: isOnMobile } = action;
return {
@ -150,18 +162,6 @@ export default function user(
};
}
case 'RECEIVE_COOLDOWN': {
const { waitSeconds } = action;
const wait = waitSeconds
? new Date(Date.now() + waitSeconds * 1000)
: null;
return {
...state,
wait,
coolDown: null,
};
}
case 'RECEIVE_ME': {
const {
name,

85
src/routes/api/captcha.js Normal file
View File

@ -0,0 +1,85 @@
/*
* This is just for verifying captcha tokens,
* the actual notification that a captcha is needed is sent
* with the pixel return answer when sending apixel on websocket
*
* @flow
*/
import type { Request, Response } from 'express';
import logger from '../../core/logger';
import redis from '../../data/redis';
import verifyCaptcha from '../../utils/recaptcha';
import {
RECAPTCHA_SECRET,
RECAPTCHA_TIME,
} from '../../core/config';
const TTL_CACHE = RECAPTCHA_TIME * 60; // seconds
export default async (req: Request, res: Response) => {
if (!RECAPTCHA_SECRET) {
res.status(200)
.json({
errors: [{
msg:
'No need for a captcha here',
}],
});
return;
}
const user = req.user || req.noauthUser;
const { ip } = user;
try {
const { token } = req.body;
if (!token) {
res.status(400)
.json({ errors: [{ msg: 'No token given' }] });
return;
}
const key = `human:${ip}`;
const ttl: number = await redis.ttlAsync(key);
if (ttl > 0) {
res.status(400)
.json({
errors: [{
msg:
'Why would you even want to solve a captcha?',
}],
});
return;
}
if (!await verifyCaptcha(token, ip)) {
logger.info(`CAPTCHA ${ip} failed his captcha`);
res.status(422)
.json({
errors: [{
msg:
'You failed your captcha',
}],
});
return;
}
// save to cache
await redis.setAsync(key, 'y', 'EX', TTL_CACHE);
res.status(200)
.json({ success: true });
} catch (error) {
logger.error('checkHuman', error);
res.status(500)
.json({
errors: [{
msg:
'Server error occured',
}],
});
}
};

View File

@ -13,7 +13,8 @@ import { getIPFromRequest, getIPv6Subnet } from '../../utils/ip';
import me from './me';
import mctp from './mctp';
import pixel from './pixel';
// import pixel from './pixel';
import captcha from './captcha';
import auth from './auth';
import ranking from './ranking';
import history from './history';
@ -63,7 +64,11 @@ router.use((err, req, res, next) => {
* rate limiting should occure outside,
* with nginx or whatever
*/
router.post('/pixel', pixel);
/* api pixel got deactivated in favor of websocket */
/* keeping it still here to enable it again if needed */
// router.post('/pixel', pixel);
router.post('/captcha', captcha);
/*
* passport authenticate

View File

@ -5,7 +5,7 @@
import type { Request, Response } from 'express';
import draw from '../../core/draw';
import { drawSafeByCoords } from '../../core/draw';
import {
blacklistDetector,
cheapDetector,
@ -197,7 +197,7 @@ async function place(req: Request, res: Response) {
const {
errorTitle, error, success, waitSeconds, coolDownSeconds,
} = await draw(user, cn, clr, x, y, z);
} = await drawSafeByCoords(user, cn, clr, x, y, z);
logger.log('debug', success);
if (success) {

View File

@ -15,7 +15,7 @@ import WebSocketEvents from './WebSocketEvents';
import webSockets from './websockets';
import { getIPFromRequest } from '../utils/ip';
import Minecraft from '../core/minecraft';
import { drawUnsafe, setPixel } from '../core/draw';
import { drawByCoords, setPixelByCoords } from '../core/draw';
import logger from '../core/logger';
import { APISOCKET_KEY } from '../core/config';
import chatProvider from '../core/ChatProvider';
@ -192,7 +192,7 @@ class APISocketServer extends WebSocketEvents {
if (clr < 0 || clr > 32) return;
// be aware that user null has no cd
if (!minecraftid && !ip) {
setPixel('0', clr, x, y);
setPixelByCoords('0', clr, x, y);
ws.send(JSON.stringify(['retpxl', null, null, true, 0, 0]));
return;
}
@ -200,7 +200,7 @@ class APISocketServer extends WebSocketEvents {
user.ip = ip;
const {
error, success, waitSeconds, coolDownSeconds,
} = await drawUnsafe(user, '0', clr, x, y, null);
} = await drawByCoords(user, '0', clr, x, y, null);
ws.send(JSON.stringify([
'retpxl',
(minecraftid) || ip,

View File

@ -9,7 +9,8 @@
import EventEmitter from 'events';
import CoolDownPacket from './packets/CoolDownPacket';
import PixelUpdate from './packets/PixelUpdate';
import PixelUpdate from './packets/PixelUpdateClient';
import PixelReturn from './packets/PixelReturn';
import OnlineCounter from './packets/OnlineCounter';
import RegisterCanvas from './packets/RegisterCanvas';
import RegisterChunk from './packets/RegisterChunk';
@ -130,6 +131,14 @@ class ProtocolClient extends EventEmitter {
if (~pos) chunks.splice(pos, 1);
}
requestPlacePixel(
i, j, offset,
color,
) {
const buffer = PixelUpdate.dehydrate(i, j, offset, color);
this.sendWhenReady(buffer);
}
requestChatHistory() {
const buffer = RequestChatHistory.dehydrate();
if (this.isConnected) this.ws.send(buffer);
@ -186,6 +195,9 @@ class ProtocolClient extends EventEmitter {
case PixelUpdate.OP_CODE:
this.emit('pixelUpdate', PixelUpdate.hydrate(data));
break;
case PixelReturn.OP_CODE:
this.emit('pixelReturn', PixelReturn.hydrate(data));
break;
case OnlineCounter.OP_CODE:
this.emit('onlineCounter', OnlineCounter.hydrate(data));
break;

View File

@ -8,13 +8,15 @@ import Counter from '../utils/Counter';
import RateLimiter from '../utils/RateLimiter';
import { getIPFromRequest } from '../utils/ip';
import CoolDownPacket from './packets/CoolDownPacket';
import PixelUpdate from './packets/PixelUpdateServer';
import PixelReturn from './packets/PixelReturn';
import RegisterCanvas from './packets/RegisterCanvas';
import RegisterChunk from './packets/RegisterChunk';
import RegisterMultipleChunks from './packets/RegisterMultipleChunks';
import DeRegisterChunk from './packets/DeRegisterChunk';
import DeRegisterMultipleChunks from './packets/DeRegisterMultipleChunks';
import RequestChatHistory from './packets/RequestChatHistory';
import CoolDownPacket from './packets/CoolDownPacket';
import ChangedMe from './packets/ChangedMe';
import OnlineCounter from './packets/OnlineCounter';
@ -22,6 +24,14 @@ import chatProvider, { ChatProvider } from '../core/ChatProvider';
import authenticateClient from './verifyClient';
import WebSocketEvents from './WebSocketEvents';
import webSockets from './websockets';
import { drawSafeByOffset } from '../core/draw';
import redis from '../data/redis';
import { cheapDetector, blacklistDetector } from '../core/isProxy';
import {
USE_PROXYCHECK,
RECAPTCHA_SECRET,
} from '../core/config';
const ipCounter: Counter<string> = new Counter();
@ -32,10 +42,11 @@ function heartbeat() {
async function verifyClient(info, done) {
const { req } = info;
const { headers } = req;
// Limiting socket connections per ip
const ip = await getIPFromRequest(req);
logger.info(`Got ws request from ${ip}`);
logger.info(`Got ws request from ${ip} via ${headers.origin}`);
if (ipCounter.get(ip) > 50) {
logger.info(`Client ${ip} has more than 50 connections open.`);
return done(false);
@ -81,7 +92,7 @@ class SocketServer extends WebSocketEvents {
ws.user = user;
ws.name = (user.regUser) ? user.regUser.name : null;
ws.rateLimiter = new RateLimiter(20, 15, true);
const ip = await getIPFromRequest(req);
SocketServer.checkIfProxy(ws);
if (ws.name) {
ws.send(`"${ws.name}"`);
@ -91,6 +102,7 @@ class SocketServer extends WebSocketEvents {
online: this.wss.clients.size || 0,
}));
const ip = await getIPFromRequest(req);
ws.on('error', (e) => {
logger.error(`WebSocket Client Error for ${ws.name}: ${e.message}`);
});
@ -111,9 +123,15 @@ class SocketServer extends WebSocketEvents {
this.onlineCounterBroadcast = this.onlineCounterBroadcast.bind(this);
this.ping = this.ping.bind(this);
this.killOld = this.killOld.bind(this);
setInterval(this.killOld, 10 * 60 * 1000);
/*
* i don't tink that we really need that, it just stresses the server
* with lots of reconnects at once, the overhead of having a few idle
* connections isn't too bad in comparison
*/
// this.killOld = this.killOld.bind(this);
// setInterval(this.killOld, 10 * 60 * 1000);
setInterval(this.onlineCounterBroadcast, 10 * 1000);
// https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
setInterval(this.ping, 45 * 1000);
@ -207,7 +225,7 @@ class SocketServer extends WebSocketEvents {
const now = Date.now();
this.wss.clients.forEach((ws) => {
const lifetime = now - ws.startDate;
if (lifetime > 30 * 60 * 1000) ws.terminate();
if (lifetime > 30 * 60 * 1000 && Math.random() < 0.3) ws.terminate();
});
}
@ -227,6 +245,16 @@ class SocketServer extends WebSocketEvents {
webSockets.broadcastOnlineCounter(online);
}
static async checkIfProxy(ws) {
const { ip } = ws.user;
if (USE_PROXYCHECK && ip && await cheapDetector(ip)) {
return true;
} if (await blacklistDetector(ip)) {
return true;
}
return false;
}
static async onTextMessage(text, ws) {
try {
let message;
@ -247,6 +275,7 @@ class SocketServer extends WebSocketEvents {
message = message.trim();
if (ws.name && message) {
const { user } = ws;
const waitLeft = ws.rateLimiter.tick();
if (waitLeft) {
ws.send(JSON.stringify([
@ -258,8 +287,22 @@ class SocketServer extends WebSocketEvents {
]));
return;
}
// check proxy
if (await SocketServer.checkIfProxy(ws)) {
logger.info(
`${ws.name} / ${user.ip} tried to send chat message with proxy`,
);
ws.send(JSON.stringify([
'info',
'You can not send chat messages with a proxy',
'il',
channelId,
]));
return;
}
//
const errorMsg = await chatProvider.sendMessage(
ws.user,
user,
message,
channelId,
);
@ -286,7 +329,7 @@ class SocketServer extends WebSocketEvents {
logger.info('Got empty message or message from unidentified ws');
}
} catch {
logger.info('Got invalid ws message');
logger.info('Got invalid ws text message');
}
}
@ -294,52 +337,98 @@ class SocketServer extends WebSocketEvents {
if (buffer.byteLength === 0) return;
const opcode = buffer[0];
switch (opcode) {
case RegisterCanvas.OP_CODE: {
const canvasId = RegisterCanvas.hydrate(buffer);
if (ws.canvasId !== null && ws.canvasId !== canvasId) {
this.deleteAllChunks(ws);
try {
switch (opcode) {
case PixelUpdate.OP_CODE: {
const { canvasId, user } = ws;
if (canvasId === null) {
return;
}
const { ip } = user;
// check if captcha needed
if (RECAPTCHA_SECRET) {
const key = `human:${ip}`;
const ttl: number = await redis.ttlAsync(key);
if (ttl <= 0) {
// need captcha
logger.info(`CAPTCHA ${ip} / ${ws.name} got captcha`);
ws.send(PixelReturn.dehydrate(10, 0, 0));
break;
}
}
// (re)check for Proxy
if (await SocketServer.checkIfProxy(ws)) {
ws.send(PixelReturn.dehydrate(11, 0, 0));
break;
}
// receive pixels here
const {
i, j, offset,
color,
} = PixelUpdate.hydrate(buffer);
const {
wait,
coolDown,
retCode,
} = await drawSafeByOffset(
ws.user,
ws.canvasId,
color,
i, j, offset,
);
logger.info(`send: ${wait}, ${coolDown}, ${retCode}`);
ws.send(PixelReturn.dehydrate(retCode, wait, coolDown));
break;
}
ws.canvasId = canvasId;
const wait = await ws.user.getWait(canvasId);
const waitSeconds = (wait) ? Math.ceil((wait - Date.now()) / 1000) : 0;
ws.send(CoolDownPacket.dehydrate(waitSeconds));
break;
}
case RegisterChunk.OP_CODE: {
const chunkid = RegisterChunk.hydrate(buffer);
this.pushChunk(chunkid, ws);
break;
}
case RegisterMultipleChunks.OP_CODE: {
this.deleteAllChunks(ws);
let posu = 2;
while (posu < buffer.length) {
const chunkid = buffer[posu++] | buffer[posu++] << 8;
case RegisterCanvas.OP_CODE: {
const canvasId = RegisterCanvas.hydrate(buffer);
if (ws.canvasId !== null && ws.canvasId !== canvasId) {
this.deleteAllChunks(ws);
}
ws.canvasId = canvasId;
const wait = await ws.user.getWait(canvasId);
const waitMs = (wait) ? wait - Date.now() : 0;
ws.send(CoolDownPacket.dehydrate(waitMs));
break;
}
case RegisterChunk.OP_CODE: {
const chunkid = RegisterChunk.hydrate(buffer);
this.pushChunk(chunkid, ws);
break;
}
break;
}
case DeRegisterChunk.OP_CODE: {
const chunkidn = DeRegisterChunk.hydrate(buffer);
this.deleteChunk(chunkidn, ws);
break;
}
case DeRegisterMultipleChunks.OP_CODE: {
let posl = 2;
while (posl < buffer.length) {
const chunkid = buffer[posl++] | buffer[posl++] << 8;
this.deleteChunk(chunkid, ws);
case RegisterMultipleChunks.OP_CODE: {
this.deleteAllChunks(ws);
let posu = 2;
while (posu < buffer.length) {
const chunkid = buffer[posu++] | buffer[posu++] << 8;
this.pushChunk(chunkid, ws);
}
break;
}
break;
case DeRegisterChunk.OP_CODE: {
const chunkidn = DeRegisterChunk.hydrate(buffer);
this.deleteChunk(chunkidn, ws);
break;
}
case DeRegisterMultipleChunks.OP_CODE: {
let posl = 2;
while (posl < buffer.length) {
const chunkid = buffer[posl++] | buffer[posl++] << 8;
this.deleteChunk(chunkid, ws);
}
break;
}
case RequestChatHistory.OP_CODE: {
const history = JSON.stringify(chatProvider.history);
ws.send(history);
break;
}
default:
break;
}
case RequestChatHistory.OP_CODE: {
const history = JSON.stringify(chatProvider.history);
ws.send(history);
break;
}
default:
break;
} catch (e) {
logger.info('Got invalid ws binary message');
throw e;
}
}

View File

@ -6,16 +6,12 @@ const OP_CODE = 0xC2;
export default {
OP_CODE,
hydrate(data: DataView) {
// SERVER (Client)
const waitSeconds = data.getUint16(1);
return waitSeconds;
return data.getUint32(1);
},
dehydrate(waitSeconds): Buffer {
// CLIENT (Sender)
const buffer = Buffer.allocUnsafe(1 + 2);
dehydrate(wait): Buffer {
const buffer = Buffer.allocUnsafe(1 + 4);
buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt16BE(waitSeconds, 1);
buffer.writeUInt32BE(wait, 1);
return buffer;
},
};

View File

@ -0,0 +1,27 @@
/* @flow */
const OP_CODE = 0xC3;
export default {
OP_CODE,
hydrate(data: DataView) {
const retCode = data.getUint8(1);
const wait = data.getUint32(2);
const coolDownSeconds = data.getInt16(6);
return {
retCode,
wait,
coolDownSeconds,
};
},
dehydrate(retCode, wait, coolDown): Buffer {
const buffer = Buffer.allocUnsafe(1 + 1 + 4 + 1 + 2);
buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt8(retCode, 1);
buffer.writeUInt32BE(wait, 2);
const coolDownSeconds = Math.round(coolDown / 1000);
buffer.writeInt16BE(coolDownSeconds, 6);
return buffer;
},
};

View File

@ -1,46 +0,0 @@
/* @flow */
import type { ColorIndex } from '../../core/Palette';
type PixelUpdatePacket = {
x: number,
y: number,
color: ColorIndex,
};
const OP_CODE = 0xC1; // Chunk Update
export default {
OP_CODE,
hydrate(data: DataView): PixelUpdatePacket {
// CLIENT
const i = data.getUint8(1);
const j = data.getUint8(2);
const offset = (data.getUint8(3) << 16) | data.getUint16(4);
const color = data.getUint8(6);
// const offset = data.getUint16(5);
// const color = data.getUint8(7);
return {
i, j, offset, color,
};
},
dehydrate(i, j, offset, color): Buffer {
// SERVER
if (!process.env.BROWSER) {
const buffer = Buffer.allocUnsafe(1 + 1 + 1 + 1 + 2 + 1);
buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt8(i, 1);
buffer.writeUInt8(j, 2);
buffer.writeUInt8(offset >>> 16, 3);
buffer.writeUInt16BE(offset & 0x00FFFF, 4);
buffer.writeUInt8(color, 6);
// buffer.writeUInt16BE(offset, 5);
// buffer.writeUInt8(color, 7);
return buffer;
}
return null;
},
};

View File

@ -0,0 +1,40 @@
/* @flow */
import type { ColorIndex } from '../../core/Palette';
type PixelUpdatePacket = {
x: number,
y: number,
color: ColorIndex,
};
const OP_CODE = 0xC1;
export default {
OP_CODE,
hydrate(data: DataView): PixelUpdatePacket {
const i = data.getUint8(1);
const j = data.getUint8(2);
const offset = (data.getUint8(3) << 16) | data.getUint16(4);
const color = data.getUint8(6);
return {
i, j, offset, color,
};
},
dehydrate(i, j, offset, color): Buffer {
const buffer = new ArrayBuffer(1 + 1 + 1 + 1 + 2 + 1);
const view = new DataView(buffer);
view.setUint8(0, OP_CODE);
view.setUint8(1, i);
view.setUint8(2, j);
view.setUint8(3, offset >>> 16);
view.setUint16(4, offset & 0x00FFFF);
view.setUint8(6, color);
return buffer;
},
};

View File

@ -0,0 +1,37 @@
/* @flow */
import type { ColorIndex } from '../../core/Palette';
type PixelUpdatePacket = {
x: number,
y: number,
color: ColorIndex,
};
const OP_CODE = 0xC1;
export default {
OP_CODE,
hydrate(data: Buffer): PixelUpdatePacket {
const i = data.readUInt8(1);
const j = data.readUInt8(2);
const offset = (data.readUInt8(3) << 16) | data.readUInt16BE(4);
const color = data.readUInt8(6);
return {
i, j, offset, color,
};
},
dehydrate(i, j, offset, color): Buffer {
const buffer = Buffer.allocUnsafe(1 + 1 + 1 + 1 + 2 + 1);
buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt8(i, 1);
buffer.writeUInt8(j, 2);
buffer.writeUInt8(offset >>> 16, 3);
buffer.writeUInt16BE(offset & 0x00FFFF, 4);
buffer.writeUInt8(color, 6);
return buffer;
},
};

View File

@ -6,7 +6,7 @@
*/
import OnlineCounter from './packets/OnlineCounter';
import PixelUpdate from './packets/PixelUpdate';
import PixelUpdate from './packets/PixelUpdateServer';
class WebSockets {

View File

@ -1,28 +0,0 @@
/**
* Copyright 2016 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE
*/
import track from './track';
export default () => (next) => (action) => {
track(action);
return next(action);
};

View File

@ -105,8 +105,7 @@ export default (store) => (next) => (action) => {
case 'PLACE_PIXEL': {
if (mute) break;
const { color } = action;
const { palette } = state.canvas;
const { palette, selectedColor: color } = state.canvas;
const colorsAmount = palette.colors.length;
const clrFreq = 100 + Math.log(color / colorsAmount + 1) * 300;

View File

@ -10,7 +10,6 @@ import swal from './sweetAlert';
import protocolClientHook from './protocolClientHook';
import rendererHook from './rendererHook';
// import ads from './ads';
// import analytics from './analytics';
import array from './array';
import promise from './promise';
import notifications from './notifications';
@ -41,7 +40,6 @@ const store = createStore(
protocolClientHook,
rendererHook,
// ads,
// analytics,
logger,
),
),

View File

@ -30,6 +30,17 @@ export default (store) => (next) => (action) => {
break;
}
case 'REQUEST_PLACE_PIXEL': {
const {
i, j, offset, color,
} = action;
ProtocolClient.requestPlacePixel(
i, j, offset,
color,
);
break;
}
default:
// nothing
}

View File

@ -47,6 +47,12 @@ export default (store) => (next) => (action) => {
break;
}
case 'SET_PLACE_ALLOWED': {
const renderer = getRenderer();
renderer.forceNextSubRender = true;
break;
}
case 'TOGGLE_HISTORICAL_VIEW':
case 'SET_SCALE': {
const {

View File

@ -1,46 +0,0 @@
/**
* Copyright 2016 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE
*
* @flow
*/
import type { Action } from '../actions/types';
export default function track(action: Action): void {
if (typeof window.ga === 'undefined') return;
switch (action.type) {
case 'PLACE_PIXEL': {
const [x, y] = action.coordinates;
window.ga('send', {
hitType: 'event',
eventCategory: 'Place',
eventAction: action.color,
eventLabel: `${x},${y}`,
});
break;
}
default:
// nothing
}
}

View File

@ -87,7 +87,7 @@ class Chunk {
timestamp: number;
constructor(palette, key, xc, zc) {
this.array = [0, xc, zc];
this.cell = [0, xc, zc];
this.key = key;
this.palette = palette;
this.timestamp = Date.now();

View File

@ -12,6 +12,7 @@ import VoxelPainterControls from '../controls/VoxelPainterControls';
import ChunkLoader from './ChunkLoader3D';
import {
getChunkOfPixel,
getOffsetOfPixel,
} from '../core/utils';
import {
THREE_TILE_SIZE,
@ -458,6 +459,21 @@ class Renderer {
}
}
placeVoxel(x: number, y: number, z: number, color: number = null) {
const {
store,
} = this;
const state = store.getState();
const {
canvasSize,
selectedColor,
} = state.canvas;
const chClr = (color === null) ? selectedColor : color;
const [i, j] = getChunkOfPixel(canvasSize, x, y, z);
const offset = getOffsetOfPixel(canvasSize, x, y, z);
store.dispatch(tryPlacePixel(i, j, offset, chClr));
}
multiTapEnd() {
const {
store,
@ -486,7 +502,7 @@ class Renderer {
if (this.rollOverMesh.position.y < 0) {
return;
}
store.dispatch(tryPlacePixel([px, py, pz]));
this.placeVoxel(px, py, pz);
break;
}
case 2: {
@ -512,8 +528,8 @@ class Renderer {
return;
}
if (target.clone().sub(camera.position).length() <= 50) {
const cell = target.toArray();
store.dispatch(tryPlacePixel(cell, 0));
const [x, y, z] = target.toArray();
this.placeVoxel(x, y, z, 0);
}
}
break;
@ -602,8 +618,8 @@ class Renderer {
.addScalar(0.5)
.floor();
if (target.clone().sub(camera.position).length() < 120) {
const cell = target.toArray();
store.dispatch(tryPlacePixel(cell));
const [x, y, z] = target.toArray();
this.placeVoxel(x, y, z);
}
} else if (button === 1) {
// middle mouse button
@ -635,8 +651,8 @@ class Renderer {
return;
}
if (target.clone().sub(camera.position).length() < 120) {
const cell = target.toArray();
store.dispatch(tryPlacePixel(cell, 0));
const [x, y, z] = target.toArray();
this.placeVoxel(x, y, z, 0);
}
}
}