add MMORPG style Event

This commit is contained in:
HF 2020-05-24 17:52:04 +02:00
parent 4baadec329
commit dc6b1f8cbe
16 changed files with 766 additions and 58 deletions

View File

@ -92,6 +92,7 @@ Configuration takes place in the environment variables that are defined in ecosy
| BACKUP_DIR | mounted directory of backup server | "/mnt/backup/" |
| GMAIL_USER | gmail username if used for mails | "ppfun@gmail.com" |
| GMAIL_PW | gmail password if used for mails | "lolrofls" |
| HOURLY_EVENT | run hourly void event on main canvas | 1 |
Notes:
@ -238,6 +239,10 @@ Alternatively you can run it with pm2, just like pixelplanet. An example ecosyst
Note:
- You do not have to run backups or historical view, it's optional.
### Hourly Event
Hourly event is an MMORPG style event that launches once in two hours where users have to fight against a growing void that starts at a random position at the main canvas. If they complete it successfully, the whole canvas will have half cooldown for a few minutes.
### Historical view
![historicalview](promotion/historicalview.gif)

View File

@ -36,7 +36,8 @@ function init() {
ProtocolClient.on('pixelUpdate', ({
i, j, offset, color,
}) => {
store.dispatch(receivePixelUpdate(i, j, offset, color));
// remove protection
store.dispatch(receivePixelUpdate(i, j, offset, color & 0x7F));
});
ProtocolClient.on('pixelReturn', ({
retCode, wait, coolDownSeconds,

View File

@ -20,9 +20,12 @@ function ChatMessage({
}
const isInfo = (name === 'info');
const isEvent = (name === 'event');
let className = 'msg';
if (isInfo) {
className += ' info';
} else if (isEvent) {
className += ' event';
} else if (msgArray[0][1].charAt(0) === '>') {
className += ' greentext';
}
@ -30,7 +33,7 @@ function ChatMessage({
return (
<p className="chatmsg">
{
(!isInfo)
(!isInfo && !isEvent)
&& (
<span>
<img

View File

@ -156,7 +156,7 @@ export async function imagemask2Canvas(
}
}
if (pxlCnt) {
const ret = await RedisCanvas.setChunk(cx, cy, chunk);
const ret = await RedisCanvas.setChunk(cx, cy, chunk, canvasId);
if (ret) {
logger.info(`Loaded ${pxlCnt} pixels into chunk ${cx}, ${cy}.`);
}
@ -192,7 +192,10 @@ export async function protectCanvasArea(
const canvasMinXY = -(canvas.size / 2);
const [ucx, ucy] = getChunkOfPixel(canvas.size, x, y);
const [lcx, lcy] = getChunkOfPixel(canvas.size, x + width, y + height);
const [lcx, lcy] = getChunkOfPixel(
canvas.size, x + width - 1,
y + height - 1,
);
let chunk;
for (let cx = ucx; cx <= lcx; cx += 1) {
@ -203,8 +206,8 @@ export async function protectCanvasArea(
}
chunk = new Uint8Array(chunk);
// offset of area in chunk
const cOffX = x - cx * TILE_SIZE + canvasMinXY;
const cOffY = y - cy * TILE_SIZE + canvasMinXY;
const cOffX = x - cx * TILE_SIZE - canvasMinXY;
const cOffY = y - cy * TILE_SIZE - canvasMinXY;
const cOffXE = cOffX + width;
const cOffYE = cOffY + height;
const startX = (cOffX > 0) ? cOffX : 0;
@ -212,19 +215,19 @@ export async function protectCanvasArea(
const endX = (cOffXE >= TILE_SIZE) ? TILE_SIZE : cOffXE;
const endY = (cOffYE >= TILE_SIZE) ? TILE_SIZE : cOffYE;
let pxlCnt = 0;
for (let py = startX; py < endX; py += 1) {
for (let px = startY; px < endY; px += 1) {
const offset = (px + py * TILE_SIZE) * 3;
for (let py = startY; py < endY; py += 1) {
for (let px = startX; px < endX; px += 1) {
const offset = px + py * TILE_SIZE;
if (protect) {
chunk[offset] |= 0x80;
} else {
chunk[offset] &= 0x07;
chunk[offset] &= 0x7F;
}
pxlCnt += 1;
}
}
if (pxlCnt) {
const ret = await RedisCanvas.setChunk(cx, cy, chunk);
const ret = await RedisCanvas.setChunk(cx, cy, chunk, canvasId);
if (ret) {
// eslint-disable-next-line max-len
logger.info(`Set protection for ${pxlCnt} pixels in chunk ${cx}, ${cy}.`);

183
src/core/Void.js Normal file
View File

@ -0,0 +1,183 @@
/*
* this is the actual event
* A ever growing circle of random pixels starts at event area
* users fight it with background pixels
* if it reaches the TARGET_RADIUS size, the event is lost
*
* @flow
*/
import webSockets from '../socket/websockets';
import WebSocketEvents from '../socket/WebSocketEvents';
import PixelUpdate from '../socket/packets/PixelUpdateServer';
import { setPixelByOffset } from './setPixel';
import { TILE_SIZE } from './constants';
import { CANVAS_ID } from '../data/models/Event';
// eslint-disable-next-line import/no-unresolved
import canvases from './canvases.json';
const TARGET_RADIUS = 62;
const EVENT_DURATION_MIN = 10;
// const EVENT_DURATION_MIN = 1;
class Void extends WebSocketEvents {
i: number;
j: number;
maxClr: number;
msTimeout: number;
pixelStack: Array;
area: Object;
curRadius: number;
curAngle: number;
curAngleDelta: number;
ended: boolean;
constructor(centerCell) {
super();
// chunk coordinates
const [i, j] = centerCell;
this.i = i;
this.j = j;
this.ended = false;
this.maxClr = canvases[CANVAS_ID].colors.length;
const area = TARGET_RADIUS ** 2 * Math.PI;
const online = webSockets.onlineCounter;
const requiredSpeed = online * 4;
const ppm = Math.ceil(area / EVENT_DURATION_MIN + requiredSpeed);
// timeout between pixels
this.msTimeout = 60 * 1000 / ppm;
// area where we log placed pixels
this.area = new Uint8Array(TILE_SIZE * 3 * TILE_SIZE * 3);
// array of pixels that we place before continue building (instant-defense)
this.pixelStack = [];
this.curRadius = 0;
this.curAngle = 0;
this.curAngleDelta = Math.PI;
this.voidLoop = this.voidLoop.bind(this);
this.cancel = this.cancel.bind(this);
this.checkStatus = this.checkStatus.bind(this);
this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this);
webSockets.addListener(this);
this.voidLoop();
}
/*
* send pixel relative to 3x3 tile area
*/
sendPixel(x, y, clr) {
const [u, v, off] = Void.coordsToOffset(x, y);
const i = this.i + u;
const j = this.j + v;
this.area[x + y * TILE_SIZE * 3] = clr;
setPixelByOffset(CANVAS_ID, clr, i, j, off);
}
/*
* check if pixel is set by us
* x, y relative to 3x3 tiles area
*/
isSet(x, y, resetIfSet = false) {
const off = x + y * TILE_SIZE * 3;
const clr = this.area[off];
if (clr) {
if (resetIfSet) this.area[off] = 0;
return true;
}
return false;
}
static coordsToOffset(x, y) {
const ox = x % TILE_SIZE;
const oy = y % TILE_SIZE;
const off = ox + oy * TILE_SIZE;
const u = (x - ox) / TILE_SIZE - 1;
const v = (y - oy) / TILE_SIZE - 1;
return [u, v, off];
}
voidLoop() {
if (this.ended) {
return;
}
let clr = 0;
while (clr <= 2 || clr === 25) {
// choose random color
clr = Math.floor(Math.random() * this.maxClr);
}
const pxl = this.pixelStack.pop();
if (pxl) {
// use stack pixel if available
const [x, y] = pxl;
this.sendPixel(x, y, clr);
} else {
// build in a circle
/* that really is the best here */
// eslint-disable-next-line no-constant-condition
while (true) {
this.curAngle += this.curAngleDelta;
if (this.curAngle > 2 * Math.PI) {
// it does skip some pixel, but thats ok
this.curRadius += 1;
if (this.curRadius > TARGET_RADIUS) {
this.ended = true;
return;
}
this.curAngleDelta = 2 * Math.PI / (2 * this.curRadius * Math.PI);
this.curAngle = 0;
}
const { curAngle, curRadius } = this;
let gk = Math.sin(curAngle) * curRadius;
let ak = Math.cos(curAngle) * curRadius;
if (gk > 0) gk = Math.floor(gk);
else gk = Math.ceil(gk);
if (ak > 0) ak = Math.floor(ak);
else ak = Math.ceil(ak);
const x = ak + TILE_SIZE * 1.5;
const y = gk + TILE_SIZE * 1.5;
if (this.isSet(x, y)) {
continue;
}
this.sendPixel(x, y, clr);
break;
}
}
setTimeout(this.voidLoop, this.msTimeout);
}
cancel() {
webSockets.remListener(this);
this.ended = true;
}
checkStatus() {
if (this.ended) {
webSockets.remListener(this);
return 100;
}
return Math.floor(this.curRadius * 100 / TARGET_RADIUS);
}
broadcastPixelBuffer(canvasId, chunkid, buffer) {
const {
i: pi,
j: pj,
offset: off,
color,
} = PixelUpdate.hydrate(buffer);
if (color <= 2 || color === 25) {
const { i, j } = this;
// 3x3 chunk area (this is hardcoded on multiple places)
if (pi >= i - 1 && pi <= i + 1 && pj >= j - 1 && pj <= j + 1) {
const uOff = (pi - i + 1) * TILE_SIZE;
const vOff = (pj - j + 1) * TILE_SIZE;
const x = uOff + off % TILE_SIZE;
const y = vOff + Math.floor(off / TILE_SIZE);
if (this.isSet(x, y, true)) {
this.pixelStack.push([x, y]);
}
}
}
}
}
export default Void;

View File

@ -41,6 +41,9 @@ export const DISCORD_INVITE = process.env.DISCORD_INVITE
// Logging
export const LOG_MYSQL = parseInt(process.env.LOG_MYSQL, 10) || false;
// do hourly event
export const HOURLY_EVENT = parseInt(process.env.HOURLY_EVENT, 10) || false;
// Accounts
export const APISOCKET_KEY = process.env.APISOCKET_KEY || 'changethis';
// Comma seperated list of user ids of Admins

View File

@ -5,61 +5,21 @@ import { using } from 'bluebird';
import type { User } from '../data/models';
import { redlock } from '../data/redis';
import {
getChunkOfPixel,
getOffsetOfPixel,
getPixelFromChunkOffset,
} from './utils';
import webSockets from '../socket/websockets';
import logger, { pixelLogger } from './logger';
import RedisCanvas from '../data/models/RedisCanvas';
import {
setPixelByOffset,
setPixelByCoords,
} from './setPixel';
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';
/**
*
* @param canvasId
* @param canvasId
* @param color
* @param x
* @param y
* @param z optional, if given its 3d canvas
*/
export function setPixelByCoords(
canvasId: number,
color: ColorIndex,
x: number,
y: number,
z: number = null,
) {
const canvasSize = canvases[canvasId].size;
const [i, j] = getChunkOfPixel(canvasSize, x, y, z);
const offset = getOffsetOfPixel(canvasSize, x, y, z);
RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId);
webSockets.broadcastPixel(canvasId, i, j, offset, color);
}
/**
*
* 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
@ -143,6 +103,14 @@ export async function drawByOffset(
coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd;
if (user.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();
@ -299,6 +267,14 @@ export async function drawByCoords(
let coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd;
if (user.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();

354
src/core/event.js Normal file
View File

@ -0,0 +1,354 @@
/*
* This is an even that happens all 2h,
* if the users complete, they will get rewarded by half the cooldown sitewide
*
* @flow
*/
import logger from './logger';
import {
nextEvent,
setNextEvent,
getEventArea,
clearOldEvent,
CANVAS_ID,
} from '../data/models/Event';
import Void from './Void';
import { protectCanvasArea } from './Image';
import { setPixelByOffset } from './setPixel';
import { TILE_SIZE } from './constants';
import chatProvider from './ChatProvider';
import { HOURLY_EVENT } from './config';
// eslint-disable-next-line import/no-unresolved
import canvases from './canvases.json';
// steps in minutes for event stages
const STEPS = [30, 10, 2, 1, 0, -10, -15, -40, -60];
// const STEPS = [4, 3, 2, 1, 0, -1, -2, -3, -4];
// gap between events in min, starting 1h after last event
// so 60 is a 2h gap, has to be higher than first and highest STEP numbers
const EVENT_GAP_MIN = 60;
// const EVENT_GAP_MIN = 5;
/*
* draw cross in center of chunk
* @param centerCell chunk coordinates
* @param clr color
* @param style 0 solid, 1 dashed, 2 dashed invert
* @param radius Radius (total width/height will be radius * 2 + 1)
*/
function drawCross(centerCell, clr, style, radius) {
const [i, j] = centerCell;
const center = (TILE_SIZE + 1) * TILE_SIZE / 2;
if (style !== 2) {
setPixelByOffset(CANVAS_ID, clr, i, j, center);
}
for (let r = 1; r < radius; r += 1) {
if (style) {
if (r % 2) {
if (style === 1) continue;
} else if (style === 2) {
continue;
}
}
let offset = center - TILE_SIZE * r;
setPixelByOffset(CANVAS_ID, clr, i, j, offset);
offset = center + TILE_SIZE * r;
setPixelByOffset(CANVAS_ID, clr, i, j, offset);
offset = center - r;
setPixelByOffset(CANVAS_ID, clr, i, j, offset);
offset = center + r;
setPixelByOffset(CANVAS_ID, clr, i, j, offset);
}
}
class Event {
eventState: number;
eventTimestamp: number;
eventCenter: Array;
eventCenterC: Array;
eventArea: Array;
success: boolean;
void: Object;
chatTimeout: number;
constructor() {
this.enabled = HOURLY_EVENT;
this.eventState = -1;
this.eventCenterC = null;
this.void = null;
// 0 if waiting
// 1 if won
// 2 if lost
this.success = 0;
this.chatTimeout = 0;
this.runEventLoop = this.runEventLoop.bind(this);
if (HOURLY_EVENT) {
this.initEventLoop();
}
}
async initEventLoop() {
let eventTimestamp = await nextEvent();
if (!eventTimestamp) {
eventTimestamp = await Event.setNextEvent();
await this.calcEventCenter();
const [x, y, w, h] = this.eventArea;
await protectCanvasArea(CANVAS_ID, x, y, w, h, true);
}
this.eventTimestamp = eventTimestamp;
await this.calcEventCenter();
this.runEventLoop();
}
eventTimer() {
const now = Date.now();
return Math.floor((this.eventTimestamp - now) / 1000);
}
async calcEventCenter() {
const cCoor = await getEventArea();
if (cCoor) {
this.eventCenterC = cCoor;
const {
size: canvasSize,
} = canvases[CANVAS_ID];
const [ux, uy] = cCoor.map((z) => (z - 1) * TILE_SIZE - canvasSize / 2);
this.eventArea = [ux, uy, TILE_SIZE * 3, TILE_SIZE * 3];
}
}
static getDirection(x, y) {
const { size: canvasSize } = canvases[CANVAS_ID];
let direction = null;
const distSquared = x ** 2 + y ** 2;
if (distSquared < 1000 ** 2) direction = 'center';
else if (x < 0 && y < 0) direction = 'North-West';
else if (x >= 0 && y < 0) direction = 'North-East';
else if (x < 0 && y >= 0) direction = 'South-West';
else if (x >= 0 && y >= 0) direction = 'South-East';
if (distSquared > (canvasSize / 2) ** 2) direction = `far ${direction}`;
return direction;
}
static async setNextEvent() {
// define next Event area
const { size: canvasSize } = canvases[CANVAS_ID];
// make sure that its the center of a 3x3 area
const i = Math.floor(Math.random() * (canvasSize / TILE_SIZE - 2)) + 1;
const j = Math.floor(Math.random() * (canvasSize / TILE_SIZE - 2)) + 1;
// backup it and schedul next event in 1h
await setNextEvent(EVENT_GAP_MIN, i, j);
const timestamp = await nextEvent();
const x = i * TILE_SIZE - canvasSize / 2;
const y = j * TILE_SIZE - canvasSize / 2;
chatProvider.broadcastChatMessage(
'event',
`Suspicious activity spotted in ${Event.getDirection(x, y)}`,
);
drawCross([i, j], 19, 0, 13);
logger.info(`Set next Event in 60min at ${x},${y}`);
return timestamp;
}
async runEventLoop() {
const {
eventState,
} = this;
const eventSeconds = this.eventTimer();
const eventMinutes = eventSeconds / 60;
if (eventMinutes > STEPS[0]) {
// 1h to 30min before Event: blinking dotted cross
if (eventState !== 1) {
this.eventState = 1;
// color 15 protected
drawCross(this.eventCenterC, 15, 1, 9);
drawCross(this.eventCenterC, 0, 2, 9);
} else {
this.eventState = 2;
drawCross(this.eventCenterC, 16, 2, 9);
drawCross(this.eventCenterC, 0, 1, 9);
}
setTimeout(this.runEventLoop, 2000);
} else if (eventMinutes > STEPS[1]) {
// 10min to 30min before Event: blinking solid cross
if (eventState !== 3 && eventState !== 4) {
this.eventState = 3;
const [x, y] = this.eventArea;
chatProvider.broadcastChatMessage(
'event',
`Unstable area at ${Event.getDirection(x, y)} at concerning level`,
);
}
if (eventState !== 3) {
this.eventState = 3;
drawCross(this.eventCenterC, 30, 1, 9);
drawCross(this.eventCenterC, 0, 2, 9);
} else {
this.eventState = 4;
drawCross(this.eventCenterC, 31, 2, 9);
drawCross(this.eventCenterC, 0, 1, 9);
}
setTimeout(this.runEventLoop, 1500);
} else if (eventMinutes > STEPS[2]) {
// 2min to 10min before Event: blinking solid cross
if (eventState !== 5) {
this.eventState = 5;
drawCross(this.eventCenterC, 12, 0, 7);
} else {
this.eventState = 6;
drawCross(this.eventCenterC, 13, 0, 7);
}
setTimeout(this.runEventLoop, 1000);
} else if (eventMinutes > STEPS[3]) {
// 1min to 2min before Event: blinking solid cross red small
if (eventState !== 7 && eventState !== 8) {
this.eventState = 7;
const [x, y] = this.eventArea;
const [xNear, yNear] = [x, y].map((z) => {
const rand = Math.random() * 3000 - 500;
return Math.floor(z + TILE_SIZE * 1.5 + rand);
});
chatProvider.broadcastChatMessage(
'event',
`Alert! Void is rising in 2min near #d,${xNear},${yNear},30`,
);
}
if (eventState !== 7) {
drawCross(this.eventCenterC, 11, 0, 5);
this.eventState = 7;
} else {
drawCross(this.eventCenterC, 12, 0, 5);
this.eventState = 8;
}
setTimeout(this.runEventLoop, 1000);
} else if (eventMinutes > STEPS[4]) {
// 1min till Event: blinking solid cross red small fase
if (eventState !== 9 && eventState !== 10) {
this.eventState = 9;
chatProvider.broadcastChatMessage(
'event',
'Alert! Threat rising!',
);
}
if (eventState !== 9) {
this.eventState = 9;
drawCross(this.eventCenterC, 11, 0, 3);
} else {
this.eventState = 10;
drawCross(this.eventCenterC, 19, 0, 3);
}
setTimeout(this.runEventLoop, 500);
} else if (eventMinutes > STEPS[5]) {
if (eventState !== 11) {
// start event
const [x, y, w, h] = this.eventArea;
await protectCanvasArea(CANVAS_ID, x, y, w, h, false);
logger.info(`Starting Event at ${x},${y} now`);
chatProvider.broadcastChatMessage(
'event',
'Fight starting!',
);
this.void = new Void(this.eventCenterC);
this.eventState = 11;
} else if (this.void) {
const percent = this.void.checkStatus();
if (percent === 100) {
// event lost
logger.info(`Event got lost after ${Math.abs(eventMinutes)} min`);
chatProvider.broadcastChatMessage(
'event',
'Threat couldn\'t be contained, abandon area',
);
this.success = 2;
this.void = null;
const [x, y, w, h] = this.eventArea;
await protectCanvasArea(CANVAS_ID, x, y, w, h, true);
} else {
const now = Date.now();
if (now > this.chatTimeout) {
chatProvider.broadcastChatMessage(
'event',
`Void reached ${percent}% of its max size`,
);
this.chatTimeout = now + 40000;
}
}
}
// run event for 10min
setTimeout(this.runEventLoop, 1000);
} else if (eventMinutes > STEPS[6]) {
if (eventState !== 12) {
// after 10min of event
// check if won
if (this.void) {
if (this.void.checkStatus() !== 100) {
// event won
logger.info('Event got won! Cooldown sitewide now half.');
chatProvider.broadcastChatMessage(
'event',
'Threat successfully defeated. Good work!',
);
this.success = 1;
}
this.void.cancel();
this.void = null;
}
this.eventState = 12;
}
// for 30min after event
// do nothing
setTimeout(this.runEventLoop, 60000);
} else if (eventMinutes > STEPS[7]) {
if (eventState !== 13) {
// 5min after last Event
// end debuff if lost
if (this.success === 2) {
chatProvider.broadcastChatMessage(
'event',
'Void seems to leave again.',
);
this.success = 0;
}
this.eventState = 13;
}
setTimeout(this.runEventLoop, 60000);
} else if (eventMinutes > STEPS[8]) {
if (eventState !== 14) {
// 30min after last Event
// clear old event area
// reset success state
logger.info('Restoring old event area');
await clearOldEvent();
if (this.success === 1) {
chatProvider.broadcastChatMessage(
'event',
'Celebration time over, get back to work.',
);
this.success = 0;
}
this.eventState = 14;
}
// 30min to 50min after last Event
// do nothing
setTimeout(this.runEventLoop, 60000);
} else {
// 50min after last Event / 1h before next Event
// define and protect it
this.eventTimestamp = await Event.setNextEvent();
await this.calcEventCenter();
const [x, y, w, h] = this.eventArea;
await protectCanvasArea(CANVAS_ID, x, y, w, h, true);
setTimeout(this.runEventLoop, 60000);
}
}
}
const rpgEvent = new Event();
export default rpgEvent;

52
src/core/setPixel.js Normal file
View File

@ -0,0 +1,52 @@
/* @flow */
import RedisCanvas from '../data/models/RedisCanvas';
import webSockets from '../socket/websockets';
import {
getChunkOfPixel,
getOffsetOfPixel,
} from './utils';
// eslint-disable-next-line import/no-unresolved
import canvases from './canvases.json';
/**
*
* @param canvasId
* @param canvasId
* @param color
* @param x
* @param y
* @param z optional, if given its 3d canvas
*/
export function setPixelByCoords(
canvasId: number,
color: ColorIndex,
x: number,
y: number,
z: number = null,
) {
const canvasSize = canvases[canvasId].size;
const [i, j] = getChunkOfPixel(canvasSize, x, y, z);
const offset = getOffsetOfPixel(canvasSize, x, y, z);
RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId);
webSockets.broadcastPixel(canvasId, i, j, offset, color);
}
/**
*
* 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);
}

106
src/data/models/Event.js Normal file
View File

@ -0,0 +1,106 @@
/*
*
* data saving for hourly events
*
* @flow
*/
// its ok if its slow
/* eslint-disable no-await-in-loop */
import redis from '../redis';
import logger from '../../core/logger';
import RedisCanvas from './RedisCanvas';
const EVENT_TIMESTAMP_KEY = 'evt:time';
const EVENT_POSITION_KEY = 'evt:pos';
const EVENT_BACKUP_PREFIX = 'evt:bck';
// Note: Events always happen on canvas 0
export const CANVAS_ID = '0';
/*
* @return time till next event in seconds
*/
export async function nextEvent() {
const timestamp = await redis.getAsync(EVENT_TIMESTAMP_KEY);
if (timestamp) {
return Number(timestamp.toString());
}
return null;
}
/*
* @return cell of chunk coordinates of event
*/
export async function getEventArea() {
const pos = await redis.getAsync(EVENT_POSITION_KEY);
if (pos) {
return pos.toString().split(':').map((z) => Number(z));
}
return null;
}
/*
* restore area effected by last event
*/
export async function clearOldEvent() {
const pos = await getEventArea();
if (pos) {
const [i, j] = pos;
logger.info(`Restore last event area at ${i}/${j}`);
// 3x3 chunk area centered at i,j
for (let jc = j - 1; jc <= j + 1; jc += 1) {
for (let ic = i - 1; ic <= i + 1; ic += 1) {
const chunkKey = `${EVENT_BACKUP_PREFIX}:${ic}:${jc}`;
const chunk = await redis.getAsync(chunkKey);
if (!chunk) {
logger.warn(
// eslint-disable-next-line max-len
`Couldn't get chunk event backup for ${ic}/${jc}, which is weird`,
);
continue;
}
if (chunk.length <= 256) {
logger.info(
// eslint-disable-next-line max-len
`Tiny chunk in event backup, not-generated chunk at ${ic}/${jc}`,
);
await redis.delAsync(`ch:${CANVAS_ID}:${ic}:${jc}`);
} else {
logger.info(
`Restoring chunk ${ic}/${jc} from event`,
);
await redis.setAsync(`ch:${CANVAS_ID}:${ic}:${jc}`, chunk);
}
await redis.delAsync(chunkKey);
}
}
await redis.delAsync(EVENT_POSITION_KEY);
}
}
/*
* Set time of next event
* @param minutes minutes till next event
* @param i, j chunk coordinates of center of event
*/
export async function setNextEvent(minutes: number, i: number, j: number) {
await clearOldEvent();
for (let jc = j - 1; jc <= j + 1; jc += 1) {
for (let ic = i - 1; ic <= i + 1; ic += 1) {
let chunk = await redis.getAsync(`ch:${CANVAS_ID}:${ic}:${jc}`);
if (!chunk) {
// place a dummy Array inside to mark chunk as none-existent
const buff = new Uint8Array(3);
chunk = Buffer.from(buff);
// place dummy pixel to make RedisCanvas create chunk
await RedisCanvas.setPixelInChunk(ic, jc, 0, 0, CANVAS_ID);
}
const chunkKey = `${EVENT_BACKUP_PREFIX}:${ic}:${jc}`;
await redis.setAsync(chunkKey, chunk);
}
}
await redis.setAsync(EVENT_POSITION_KEY, `${i}:${j}`);
const timestamp = Date.now() + minutes * 60 * 1000;
await redis.setAsync(EVENT_TIMESTAMP_KEY, timestamp);
}

View File

@ -38,7 +38,9 @@ class RedisCanvas {
i: number,
j: number,
): Promise<Buffer> {
// this key is also hardcoded into core/tilesBackup.js
// this key is also hardcoded into
// core/tilesBackup.js
// and ./EventData.js
const key = `ch:${canvasId}:${i}:${j}`;
return redis.getAsync(key);
}

View File

@ -15,7 +15,8 @@ import WebSocketEvents from './WebSocketEvents';
import webSockets from './websockets';
import { getIPFromRequest } from '../utils/ip';
import Minecraft from '../core/minecraft';
import { drawByCoords, setPixelByCoords } from '../core/draw';
import { setPixelByCoords } from '../core/setPixel';
import { drawByCoords } from '../core/draw';
import logger from '../core/logger';
import { APISOCKET_KEY } from '../core/config';
import chatProvider from '../core/ChatProvider';

View File

@ -11,15 +11,24 @@ import PixelUpdate from './packets/PixelUpdateServer';
class WebSockets {
listeners: Array<Object>;
onlineCounter: number;
constructor() {
this.listeners = [];
this.onlineCounter = 0;
}
addListener(listener) {
this.listeners.push(listener);
}
remListener(listener) {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
}
/*
* broadcast message via websocket
* @param message Message to send
@ -125,6 +134,7 @@ class WebSockets {
* @param online Number of users online
*/
broadcastOnlineCounter(online: number) {
this.onlineCounter = online;
const buffer = OnlineCounter.dehydrate({ online });
this.listeners.forEach(
(listener) => listener.broadcastOnlineCounter(buffer),

View File

@ -95,6 +95,9 @@ tr:nth-child(even) {
.msg.info{
color: #ff91a6;
}
.msg.event{
color: #9dc8ff;
}
.msg.greentext{
color: #94ff94;
}

View File

@ -92,6 +92,9 @@ tr:nth-child(even) {
.msg.info{
color: #ff91a6;
}
.msg.event{
color: #9dc8ff;
}
.msg.greentext{
color: #94ff94;
}

View File

@ -409,6 +409,9 @@ tr:nth-child(even) {
.msg.info {
color: #cc0000;
}
.msg.event {
color: #3955c6;
}
.msg.greentext{
color: green;
}