add per-canvas online counter

This commit is contained in:
HF 2022-01-04 11:31:10 +01:00
parent 6170d35631
commit c2cbca1387
13 changed files with 157 additions and 34 deletions

View File

@ -58,6 +58,12 @@ export function toggleAutoZoomIn() {
};
}
export function toggleOnlineCanvas() {
return {
type: 'TOGGLE_ONLINE_CANVAS',
};
}
export function toggleMute() {
return {
type: 'TOGGLE_MUTE',

View File

@ -17,6 +17,7 @@ export type Action =
| { type: 'TOGGLE_GRID' }
| { type: 'TOGGLE_PIXEL_NOTIFY' }
| { type: 'TOGGLE_AUTO_ZOOM_IN' }
| { type: 'TOGGLE_ONLINE_CANVAS' }
| { type: 'TOGGLE_MUTE' }
| { type: 'TOGGLE_OPEN_PALETTE' }
| { type: 'TOGGLE_COMPACT_PALETTE' }

View File

@ -51,7 +51,7 @@ function init() {
ProtocolClient.on('cooldownPacket', (coolDown) => {
store.dispatch(receiveCoolDown(coolDown));
});
ProtocolClient.on('onlineCounter', ({ online }) => {
ProtocolClient.on('onlineCounter', (online) => {
store.dispatch(receiveOnline(online));
});
ProtocolClient.on('chatMessage', (

View File

@ -9,7 +9,9 @@ import { t } from 'ttag';
import { THREE_CANVAS_HEIGHT } from '../core/constants';
const CanvasItem = ({ canvasId, canvas, selCanvas }) => (
const CanvasItem = ({
canvasId, canvas, selCanvas, online,
}) => (
<div
className="cvbtn"
onClick={() => selCanvas(canvasId)}
@ -23,6 +25,12 @@ const CanvasItem = ({ canvasId, canvas, selCanvas }) => (
/>
<p className="modalcvtext">
<span className="modaltitle">{canvas.title}</span><br />
{(online) && (
<>
{t`Online Users`}:&nbsp;
<span className="modalinfo">{online}</span><br />
</>
)}
<span className="modalinfo">{canvas.desc}</span><br />
{t`Cooldown`}:&nbsp;
<span className="modalinfo">
@ -33,7 +41,7 @@ const CanvasItem = ({ canvasId, canvas, selCanvas }) => (
{t`Stacking till`}:&nbsp;
<span className="modalinfo"> {canvas.cds / 1000}s</span><br />
{t`Ranked`}:&nbsp;
<span className="modalinfo">{(canvas.ranked) ? 'Yes' : 'No'}</span><br />
<span className="modalinfo">{(canvas.ranked) ? t`Yes` : t`No`}</span><br />
{(canvas.req !== -1) ? <span>{t`Requirements`}:<br /></span> : null}
<span className="modalinfo">
{(canvas.req !== -1) ? <span>{t`User Account`} </span> : null}

View File

@ -4,9 +4,11 @@
*/
import React from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { FaUser, FaPaintBrush } from 'react-icons/fa';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { FaUser, FaPaintBrush, FaFlipboard } from 'react-icons/fa';
import { t } from 'ttag';
import { toggleOnlineCanvas } from '../actions';
import { numberToString } from '../core/utils';
@ -15,19 +17,42 @@ const OnlineBox = () => {
online,
totalPixels,
name,
onlineCanvas,
canvasId,
] = useSelector((state) => [
state.ranks.online,
state.ranks.totalPixels,
state.user.name,
state.gui.onlineCanvas,
state.canvas.canvasId,
], shallowEqual);
const dispatch = useDispatch();
const onlineUsers = (onlineCanvas) ? online[canvasId] : online.total;
return (
<div>
{(online || name)
{(onlineUsers || name)
? (
<div className="onlinebox">
{(online)
&& <span title={t`User online`}>{online} <FaUser />&nbsp;</span>}
<div
className="onlinebox"
role="button"
tabIndex="0"
onClick={() => dispatch(toggleOnlineCanvas())}
>
{(onlineUsers)
&& (
<span
title={(onlineCanvas)
? t`Online Users on Canvas`
: t`Total Online Users`}
>
{onlineUsers}
<FaUser />
{(onlineCanvas) && <FaFlipboard />}
&nbsp;
</span>
)}
{(name != null)
&& (
<span title={t`Pixels placed`}>

View File

@ -12,9 +12,10 @@ import { changeWindowType, selectCanvas } from '../../actions';
const CanvasSelect = ({ windowId }) => {
const [canvases, showHiddenCanvases] = useSelector((state) => [
const [canvases, showHiddenCanvases, online] = useSelector((state) => [
state.canvas.canvases,
state.canvas.showHiddenCanvases,
state.ranks.online,
], shallowEqual);
const dispatch = useDispatch();
const selCanvas = useCallback((canvasId) => dispatch(selectCanvas(canvasId)),
@ -44,6 +45,8 @@ const CanvasSelect = ({ windowId }) => {
(!canvases[canvasId].hid || showHiddenCanvases)
&& (
<CanvasItem
key={canvasId}
online={online[canvasId]}
canvasId={canvasId}
canvas={canvases[canvasId]}
selCanvas={selCanvas}

View File

@ -7,7 +7,11 @@ const initialState = {
isLightGrid: false,
compactPalette: false,
paletteOpen: true,
// top-left button menu
menuOpen: false,
// show online users per canvas instead of total
onlineCanvas: false,
// selected theme
style: 'default',
};
@ -38,6 +42,13 @@ export default function gui(
};
}
case 'TOGGLE_ONLINE_CANVAS': {
return {
...state,
onlineCanvas: !state.onlineCanvas,
};
}
case 'TOGGLE_POTATO_MODE': {
return {
...state,

View File

@ -5,7 +5,15 @@ const initialState = {
ranking: 1488,
dailyRanking: 1488,
// global stats
online: 1,
/*
* {
* total: totalUsersOnline,
* canvasId: onlineAtCanvas,
* }
*/
online: {
total: 0,
},
totalRanking: {},
totalDailyRanking: {},
};

View File

@ -11,7 +11,13 @@ import PixelUpdate from './packets/PixelUpdateServer';
class SocketEvents extends EventEmitter {
constructor() {
super();
this.onlineCounter = 0;
/*
* {
* canvasId: onlineUsers,
* ...
* }
*/
this.onlineCounter = {};
}
/*
@ -144,7 +150,7 @@ class SocketEvents extends EventEmitter {
*/
broadcastOnlineCounter(online: number) {
this.onlineCounter = online;
const buffer = OnlineCounter.dehydrate({ online });
const buffer = OnlineCounter.dehydrate(online);
this.emit('broadcast', buffer);
}
}

View File

@ -4,6 +4,7 @@
import WebSocket from 'ws';
import logger from '../core/logger';
import canvases from '../canvases.json';
import Counter from '../utils/Counter';
import { getIPFromRequest } from '../utils/ip';
@ -82,11 +83,7 @@ class SocketServer {
ws.name = user.getName();
cheapDetector(user.ip);
ws.send(OnlineCounter.dehydrate({
online: ipCounter.amount() || 0,
}));
const ip = getIPFromRequest(req);
ws.send(OnlineCounter.dehydrate(socketEvents.onlineCounter));
ws.on('error', (e) => {
logger.error(`WebSocket Client Error for ${ws.name}: ${e.message}`);
@ -95,9 +92,7 @@ class SocketServer {
ws.on('pong', heartbeat);
ws.on('close', () => {
// is close called on terminate?
// possible memory leak?
ipCounter.delete(ip);
ipCounter.delete(getIPFromRequest(req));
this.deleteAllChunks(ws);
});
@ -114,6 +109,7 @@ class SocketServer {
this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this);
this.reloadUser = this.reloadUser.bind(this);
this.ping = this.ping.bind(this);
this.onlineCounterBroadcast = this.onlineCounterBroadcast.bind(this);
socketEvents.on('broadcast', this.broadcast);
socketEvents.on('pixelUpdate', this.broadcastPixelBuffer);
@ -170,7 +166,7 @@ class SocketServer {
});
});
setInterval(SocketServer.onlineCounterBroadcast, 10 * 1000);
setInterval(this.onlineCounterBroadcast, 10 * 1000);
setInterval(this.ping, 45 * 1000);
}
@ -294,8 +290,36 @@ class SocketServer {
});
}
static onlineCounterBroadcast() {
const online = ipCounter.amount() || 0;
onlineCounterBroadcast() {
const online = {};
online.total = ipCounter.amount() || 0;
const ipsPerCanvas = {};
const it = this.wss.clients.keys();
let client = it.next();
while (!client.done) {
const ws = client.value;
if (ws.readyState === WebSocket.OPEN
&& ws.user
) {
const canvasId = ws.canvasId || 0;
const { ip } = ws.user;
// only count unique IPs per canvas
if (!ipsPerCanvas[canvasId]) {
ipsPerCanvas[canvasId] = [ip];
} else if (ipsPerCanvas[canvasId].includes(ip)) {
client = it.next();
continue;
} else {
ipsPerCanvas[canvasId].push(ip);
}
//--
if (!online[canvasId]) {
online[canvasId] = 0;
}
online[canvasId] += 1;
}
client = it.next();
}
socketEvents.broadcastOnlineCounter(online);
}
@ -406,6 +430,7 @@ class SocketServer {
}
case RegisterCanvas.OP_CODE: {
const canvasId = RegisterCanvas.hydrate(buffer);
if (!canvases[canvasId]) return;
if (ws.canvasId !== null && ws.canvasId !== canvasId) {
this.deleteAllChunks(ws);
}

View File

@ -1,19 +1,48 @@
/*
*
* Numbers of online players per canvas
*
*/
const OP_CODE = 0xA7;
export default {
OP_CODE,
// CLIENT (receiver)
/*
* {
* total: totalOnline,
* canvasId: online,
* ....
* }
*/
hydrate(data) {
// CLIENT (receiver)
const online = data.getInt16(1);
return { online };
const online = {};
online.total = data.getUint16(1);
let off = data.byteLength;
while (off > 3) {
const onlineUsers = data.getUint16(off -= 2);
const canvas = data.getUint8(off -= 1);
online[canvas] = onlineUsers;
}
return online;
},
dehydrate({ online }) {
dehydrate(online) {
// SERVER (sender)
if (!process.env.BROWSER) {
const buffer = Buffer.allocUnsafe(1 + 2);
buffer.writeUInt8(OP_CODE, 0);
const canvasIds = Object.keys(online);
buffer.writeInt16BE(online, 1);
const buffer = Buffer.allocUnsafe(3 + canvasIds.length * (1 + 2));
buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt16BE(online.total, 1);
let cnt = 1;
for (let p = 0; p < canvasIds.length; p += 1) {
const canvasId = canvasIds[p];
const onlineUsers = online[canvasId];
buffer.writeUInt8(Number(canvasId), cnt += 2);
buffer.writeUInt16BE(onlineUsers, cnt += 1);
}
return buffer;
}

View File

@ -330,6 +330,7 @@ tr:nth-child(even) {
.onlinebox {
left: 16px;
bottom: 57px;
cursor: pointer;
white-space: nowrap;
}
@ -800,7 +801,7 @@ tr:nth-child(even) {
padding: 0;
}
.actionbuttons:hover, .coorbox:hover, .menu > div:hover {
.actionbuttons:hover, .coorbox:hover, .menu > div:hover, .onlinebox:hover {
background-color: #d2d2d2cc;
}

View File

@ -107,8 +107,8 @@ export default ({
externals: [
/\/proxies\.json$/,
/\/canvases\.json$/,
/^\.\/styleassets\.json$/,
/^\.\/assets\.json$/,
/\/styleassets\.json$/,
/\/assets\.json$/,
nodeExternals(),
],