diff --git a/README.md b/README.md index b585eef..e542a4c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ # NodeMultiEtt -Example server for the new websocket protocol/client in etterna (currently working in https://github.com/Nickito12/etterna/tree/uws) using json for packets/messages. +Example server for the new websocket protocol/client in Etterna (currently working in https://github.com/Nickito12/etterna/tree/uws) using json for packets/messages. -## JSON messages +## Building and Using + +TypeScript files are used to output the JavaScript server files. Run `tsc start.ts --outDir built/` to generate the JS files. + +For easy control, run the server using `pm2` like so: `pm2 start built/start.js --name multi` + +To connect to your local server, disconnect from EO (from network options) and then connect to `127.0.0.1`. (Normal server is `multi.etternaonline.com`) + +After changing code, recompile the TypeScript and restart the server: `tsc --outDir built/ & pm2 restart multi` + +## JSON Messages This follows a modified version of flux standard actions (https://github.com/redux-utilities/flux-standard-action#actions) diff --git a/src/ettServer.ts b/src/ettServer.ts index bc25321..0f4b761 100644 --- a/src/ettServer.ts +++ b/src/ettServer.ts @@ -10,7 +10,7 @@ import { Player, OPTIONS, READY, PLAYING, EVAL } from './player'; import { Room, SerializedRoom } from './room'; -import { colorize, opColor, ownerColor, playerColor, systemPrepend } from './utils'; +import { colorize, opColor, ownerColor, playerColor, systemColor, systemPrepend } from './utils'; import { ETTPMsgGuards, @@ -262,13 +262,6 @@ export class ETTServer { msgtype: msg.msgtype }); }, - lenny: (player: Player, command: string, params: string[], msg: ChatMsg) => { - this.onChat(player, { - msg: '( ͡° ͜ʖ ͡°)', - tab: msg.tab, - msgtype: msg.msgtype - }); - }, shrug: (player: Player, command: string, params: string[], msg: ChatMsg) => { this.onChat(player, { msg: '¯\\_(ツ)_/¯', @@ -277,17 +270,34 @@ export class ETTServer { }); }, help: (player: Player) => { - const helpMessage = `Commands:\n - /free - Enable free mode allows anyone to choose a chart (Privileged)\n - /freerate - Enable free rate allowing people to play any rate the want (Privileged)\n - /op - Give a player operator privileges, gives them access to privileged commands (Privileged)\n - /countdown - Enable a countdown before starting the chart (Privileged)\n - /stop - Stop the current countdown (Privileged)\n - /shrug - Our favorite little emoji\n - /roll - Roll a random number, you can specify a limit i.e. roll 1442\n - /help - This command right here!`; - // We send each line separetely because the client chatbox gets fucky with newlines in msgs - helpMessage.split('\n').forEach(l => player.sendChat(PRIVATE_MESSAGE, l)); + const helpMessage = `${colorize('Commands:',systemColor)} ${colorize('(Privileged)',opColor)} + ${colorize('/roll',systemColor)} - Roll a random number, you can specify a limit i.e. roll 1442 + ${colorize('/shrug',systemColor)} - Our favorite little emoji + ${colorize('/commonpacks',systemColor)} - Show a list of common packs between players + ${colorize('/ready',systemColor)} - Ready up + ${colorize('/ophelp',systemColor)} - Show all ${colorize('privileged commands',opColor)} + ${colorize('/free',opColor)} - Free mode allows anyone to choose a chart + ${colorize('/freerate',opColor)} - Free rate allows players to play any rate + ${colorize('/countdown',opColor)} - Enable a countdown before starting the chart + ${colorize('/stop',opColor)} - Stop the current countdown + ${colorize('/op',opColor)} - Give a player ${colorize('Operator',opColor)} privileges`; + //helpMessage.split('\n').forEach(l => player.sendChat(PRIVATE_MESSAGE, l)); //old + player.sendChat(PRIVATE_MESSAGE, helpMessage); + }, + ophelp: (player: Player) => { + const ophelpMessage = `${colorize('Operator Commands:',opColor)} ${colorize('(Owner)',ownerColor)} + ${colorize('/free',opColor)} - Free mode allows anyone to choose a chart + ${colorize('/freerate',opColor)} - Free rate allows players to play any rate + ${colorize('/force',opColor)} - Ignore players not being readied up + ${colorize('/countdown',opColor)} - Enable a countdown before starting the chart + ${colorize('/stop',opColor)} - Stop the current countdown + ${colorize('/op',opColor)} - Give a player ${colorize('Operator',opColor)} privileges + ${colorize('/kick',opColor)} - Kick a player + ${colorize('/selectionmode',opColor)} - Change what determines a unique song + ${colorize('/title',ownerColor)} - Change the room's title + ${colorize('/desc',ownerColor)} - Change the room's description + ${colorize('/pass',ownerColor)} - Change the room's password`; + player.sendChat(PRIVATE_MESSAGE, ophelpMessage); }, request: (player: Player, command: string, params: string[]) => { this.requestChart(player, params); @@ -302,7 +312,7 @@ export class ETTServer { }, commonpacks: (player: Player, room: Room, command: string, params: string[]) => { const commonPacks = room.commonPacks(); - if (commonPacks.length > 0) room.sendChat(`Common packs: ${commonPacks.join(',')}`); + if (commonPacks.length > 0) room.sendChat(`Common packs: ${commonPacks.join(', ')}`); else room.sendChat(`No pack in common between the players in the room :(`); }, countdown: (player: Player, room: Room, command: string, params: string[]) => { @@ -312,48 +322,45 @@ export class ETTServer { if (room.isOwner(player)) { room.desc = desc; this.updateRoom(room); - room.sendChat(''); - room.sendChat(`${systemPrepend}${player.user} changed the room description to ${desc}`); + if (desc) { + room.sendChat(`${systemPrepend}${player.user} changed the room description to ${desc}.`); + } else { + room.sendChat(`${systemPrepend}${player.user} removed the room description.`); + } } else { - player.sendChat( - ROOM_MESSAGE, - `${systemPrepend}${ - player.user - }, you're not the room owner so you cannot change the description`, - room.name - ); + room.sendChat(`${systemPrepend}${player.user} is not owner, cannot change the room description.`); } }, title: (player: Player, room: Room, command: string, [title]: string[]) => { - if (room.isOwner(player)) { + if (room.isOwner(player) && title) { room.name = title; this.updateRoom(room); - room.sendChat(''); - room.sendChat(`${systemPrepend}${player.user} renamed the room to ${title}`); + room.sendChat(`${systemPrepend}${player.user} renamed the room to ${title}.`); + } else if (room.isOwner(player) && !title) { + room.sendChat(`${systemPrepend}${player.user}, you didn't provide a room name.`); } else { - player.sendChat( - ROOM_MESSAGE, - `${systemPrepend}${ - player.user - }, you're not the room owner so you cannot change the title`, - room.name - ); + room.sendChat(`${systemPrepend}${player.user} is not owner, cannot change the room name.`); } }, pass: (player: Player, room: Room, command: string, [pass]: string[]) => { - if (room.isOwner(player)) { + if (room.isOwner(player) && pass) { + if (room.pass != "") { + room.sendChat(`${systemPrepend}${player.user} changed the room password.`); + } else { + room.sendChat(`${systemPrepend}${player.user} enabled a room password.`); + } room.pass = pass; this.updateRoom(room); - room.sendChat(''); - room.sendChat(`${systemPrepend}${player.user} changed the room password`); + } else if (room.isOwner(player) && !pass) { + if (room.pass != "") { + room.sendChat(`${systemPrepend}${player.user} removed the room password.`); + } else { + room.sendChat(`${systemPrepend}${player.user}, room already has no password.`); + } + room.pass = ""; + this.updateRoom(room); } else { - player.sendChat( - ROOM_MESSAGE, - `${systemPrepend}${ - player.user - }, you're not the room owner so you cannot change the password`, - room.name - ); + room.sendChat(`${systemPrepend}${player.user} is not owner, cannot change the room password.`); } }, kick: (player: Player, room: Room, command: string, [user]: string[]) => { @@ -368,22 +375,10 @@ export class ETTServer { playerToKick.send(makeMessage('kicked')); this.leaveRoom(playerToKick); } else { - player.sendChat( - ROOM_MESSAGE, - `${systemPrepend}${player.user}, you have insufficient rights to kick ${ - playerToKick.user - }`, - room.name - ); + room.sendChat(`${systemPrepend}${player.user} is not privileged, cannot kick ${ playerToKick.user }.`); } } else { - player.sendChat( - ROOM_MESSAGE, - `${systemPrepend}${ - player.user - }, you're not the room owner so you cannot change the password`, - room.name - ); + room.sendChat(`${systemPrepend}Player ${user} is not in lobby, cannot be kicked`); } }, force: (player: Player, room: Room) => { @@ -495,11 +490,11 @@ export class ETTServer { this.sendAll(makeMessage('deleteroom', { room: room.serialize() })); } else { // send notice to players in room that someone left - room.sendChat(`${systemPrepend}${player.user} left`); + room.sendChat(`${systemPrepend}${player.user} left.`); this.updateRoom(room); } player.send(makeMessage('leaveroom')); // So the client can exit the room when kicked - player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Left room ${room.name}`); + player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Left room ${room.name}.`); } resendAllRooms() { @@ -592,8 +587,7 @@ export class ETTServer { if (ws.readyState === 1) { ws.tmpaux(str); } else { - logger.debug(`Connection closed so msg not -ent: ${str}`); + logger.debug(`Connection closed so msg not sent: ${str}`); } }; } @@ -729,7 +723,7 @@ ent: ${str}`); onStartChart(player: Player, message: ChartMsg) { if (!player.room) { - player.sendPM(`${systemPrepend}You're not in a room`); + player.sendPM(`${systemPrepend}You're not in a room.`); return; } if (!player.room.canSelect(player)) { @@ -844,6 +838,7 @@ ent: ${str}`); if (res === true) { player.user = message.user; player.sendChat(LOBBY_MESSAGE, `Welcome to ${colorize(this.serverName)}`); + player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Enter ${colorize('/help',systemColor)} to see all commands.`); player.send(makeMessage('login', { logged: true, msg: '' })); this.sendLobbyList(player); this.addPlayerInLobbyLists(player); @@ -862,6 +857,7 @@ ent: ${str}`); bcrypt.hash(message.pass, saltRounds, (err: Error, hash: string) => { this.createAccount(player, hash); player.sendChat(LOBBY_MESSAGE, `Welcome to ${colorize(this.serverName)}`); + player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Enter ${colorize('/help',systemColor)} to see all commands.`); player.send(makeMessage('login', { logged: true, msg: '' })); this.sendLobbyList(player); this.addPlayerInLobbyLists(player); @@ -883,6 +879,7 @@ ent: ${str}`); player.user = user; player.sendChat(LOBBY_MESSAGE, `Welcome to ${colorize(this.serverName)}`); + player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Enter ${colorize('/help',systemColor)} to see all commands.`); player.send(makeMessage('login', { logged: true, msg: '' })); this.sendLobbyList(player); this.addPlayerInLobbyLists(player); @@ -963,7 +960,7 @@ ent: ${str}`); static onMissingChart(player: Player) { if (!player.user || !player.room) return; if (player.room) { - player.room.sendChat(`${systemPrepend}${player.user} doesnt have the chart`); + player.room.sendChat(`${systemPrepend}${player.user} doesnt have the chart.`); } } @@ -973,7 +970,7 @@ ent: ${str}`); } if (!message.name) { - player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Cannot use empty room name`); + player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Cannot use empty room name.`); return; } @@ -992,7 +989,7 @@ ent: ${str}`); player.readystate = false; } else { player.send(makeMessage('createroom', { created: false })); - player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Room name already in use`); + player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Room name already in use.`); } } @@ -1015,7 +1012,7 @@ ent: ${str}`); this.enterRoom(player, room); } else { player.send(makeMessage('enterroom', { entered: false })); - player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Incorrect password`); + player.sendChat(LOBBY_MESSAGE, `${systemPrepend}Incorrect password.`); } else { player.readystate = false; @@ -1098,7 +1095,7 @@ ent: ${str}`); if (!player.room || player.room.name !== message.tab) { player.sendChat( ROOM_MESSAGE, - `${systemPrepend}You're not in the room ${message.tab}`, + `${systemPrepend}You're not in the room ${message.tab}.`, message.tab ); return; @@ -1143,7 +1140,7 @@ ent: ${str}`); if (!this.userHasPemission(player, 'chartRequesting')) { player.sendChat( PRIVATE_MESSAGE, - `${systemPrepend}You are not allowed to send chart requests. Contact a server admin` + `${systemPrepend}You are not allowed to send chart requests. Contact a server admin.` ); return; } diff --git a/src/room.ts b/src/room.ts index daedb07..f6407b6 100644 --- a/src/room.ts +++ b/src/room.ts @@ -8,6 +8,7 @@ import { selectionModeDescriptions, selectionModes, stringToColour, + systemColor, systemPrepend, unauthorizedChat } from './utils'; @@ -99,10 +100,10 @@ export class Room { if (this.isOperatorOrOwner(player)) { if (this.forcestart === true) { this.forcestart = false; - this.sendChat(`${systemPrepend} force start disabled.`); + this.sendChat(`${systemPrepend} Force start disabled.`); } else { this.forcestart = true; - this.sendChat(`${systemPrepend} force start enabled for this song.`); + this.sendChat(`${systemPrepend} Force start enabled for this song.`); } } else { unauthorizedChat(player, true); @@ -149,7 +150,7 @@ export class Room { const selectionMode: (ch: Chart) => any = selectionModes[this.selectionMode]; if (!selectionMode) { - this.sendChat(`${systemPrepend}Invalid selection mode`); + this.sendChat(`${systemPrepend}Invalid selection mode.`); return {}; } @@ -192,7 +193,7 @@ export class Room { this.state = INGAME; this.send(makeMessage('startchart', { chart: newChart })); - this.sendChat(`${systemPrepend}Starting ${colorize(this.chart.title)}`); + this.sendChat(`${systemPrepend}Starting ${colorize(this.chart.title, systemColor)}`); this.playing = true; }); @@ -225,7 +226,7 @@ export class Room { this.state = INGAME; this.send(makeMessage('startchart', { chart: newChart })); - this.sendChat(`${systemPrepend}Starting ${colorize(this.chart.title)}`); + this.sendChat(`${systemPrepend}Starting ${colorize(this.chart.title, systemColor)}`); this.playing = true; } @@ -277,10 +278,10 @@ export class Room { this.send(makeMessage('selectchart', { chart: this.serializeChart() })); this.sendChat( `${systemPrepend}${player.user} selected ${colorize( - `${message.title} (${message.difficulty}: ${message.meter}) ${ - message.pack ? `[${message.pack}]` : `` - } ${message.rate ? ` ${parseFloat((message.rate / 1000).toFixed(2))}` : ''}`, - stringToColour(message.title) + `${message.title} (${message.difficulty}) ${message.rate ? ` ${ + parseFloat((message.rate / 1000).toFixed(2))}x` : ''} ${ + message.pack ? `[${message.pack}]` : `` } `, + systemColor )}` ); } @@ -313,7 +314,7 @@ export class Room { this.players.push(player); player.send(makeMessage('enterroom', { entered: true })); - this.sendChat(`${systemPrepend}${player.user} joined`); + this.sendChat(`${systemPrepend}${player.user} joined.`); player.state = READY; @@ -421,7 +422,7 @@ export class Room { if (this.isOperatorOrOwner(player)) { this.free = !this.free; this.sendChat( - `${systemPrepend}The room is now ${this.free ? '' : 'not '}in free song picking mode` + `${systemPrepend}The room is ${this.free ? 'now' : 'no longer'} in free song picking mode.` ); } else { unauthorizedChat(player, true); @@ -432,7 +433,7 @@ export class Room { if (this.isOperatorOrOwner(player)) { this.freerate = !this.freerate; - this.sendChat(`${systemPrepend}The room is now ${this.freerate ? '' : 'not'} rate free mode`); + this.sendChat(`${systemPrepend}The room is ${this.freerate ? 'now' : 'no longer'} in rate free mode.`); } else { unauthorizedChat(player, true); } @@ -445,19 +446,17 @@ export class Room { if (!selectionMode) { player.sendChat( 1, - `${systemPrepend}Invalid selection mode. Valid ones are:\n - ${JSON.stringify(selectionModeDescriptions, null, 4).replace(/[{}]/g, '')}`, + `${systemPrepend}Invalid selection mode, ${player.user}. Valid ones are: ${JSON.stringify( + selectionModeDescriptions, null, 4).replace(/[{}]/g, '')}`, this.name ); + } else { + this.selectionMode = +params[0]; + this.sendChat( + `${systemPrepend}The room is now in "${ selectionModeDescriptions[+params[0]] + }" selection mode.` + ); } - - this.selectionMode = +params[0]; - - this.sendChat( - `${systemPrepend}The room is now in "${ - selectionModeDescriptions[+params[0]] - }" selection mode` - ); } else { unauthorizedChat(player); } @@ -466,10 +465,9 @@ export class Room { roll(player: Player, command: string, params: string[]) { if (!Number.isNaN(parseInt(params[0], 10))) { const rolledNumber = Math.floor(Math.random() * parseInt(params[0], 10)); - - this.sendChat(`${systemPrepend}${player.user} rolled ${rolledNumber}`); + this.sendChat(`${systemPrepend}${player.user} rolled ${rolledNumber + 1} (max ${parseInt(params[0],10)}).`); } else { - this.sendChat(`${systemPrepend}${player.user} rolled ${Math.floor(Math.random() * 10)}`); + this.sendChat(`${systemPrepend}${player.user} rolled ${Math.floor(Math.random() * 100) + 1} (max 100).`); } } @@ -482,10 +480,10 @@ export class Room { if (!this.ops.find(x => x === params[0])) { this.ops.push(params[0]); - this.sendChat(`${systemPrepend}${params[0]} is now a room operator`); + this.sendChat(`${systemPrepend}${params[0]} is now a room operator.`); } else { this.ops = this.ops.filter(x => x !== params[0]); - this.sendChat(`${systemPrepend}${params[0]} is no longer a room operator`); + this.sendChat(`${systemPrepend}${params[0]} is no longer a room operator.`); } } else { unauthorizedChat(player); @@ -503,11 +501,11 @@ export class Room { let currentTimer: number = limit; this.timerInterval = setInterval(() => { - this.sendChat(`${systemPrepend}Starting in ${currentTimer} seconds`); + this.sendChat(`${systemPrepend}Starting in ${currentTimer} seconds.`); currentTimer -= 1; if (currentTimer === 0) { - this.sendChat(`${systemPrepend}Starting song in ${currentTimer} seconds`); + this.sendChat(`${systemPrepend}Starting song.`); clearInterval(this.timerInterval); this.countdownStarted = false; resolve(true); @@ -518,22 +516,21 @@ export class Room { stopTimer() { this.countdownStarted = false; - this.sendChat(`${systemPrepend}Song start cancelled!`); + this.sendChat(`${systemPrepend}Countdown cancelled!`); clearInterval(this.timerInterval); } enableCountdown(player: Player, command: string, params: string[]) { - if (this.countdown === true) { + if (this.countdown === true && !params[0]) { this.countdown = false; - this.sendChat(`${systemPrepend}Countdown disabled, songs will start instantly`); + this.sendChat(`${systemPrepend}Countdown disabled, songs will start instantly.`); return; - } - - if (!params[0]) { - this.sendChat(`${systemPrepend}Please set a countdown timer between 2 and 15`); + } else if (!params[0] || isNaN(parseInt(params[0])) || parseInt(params[0], 10) < 2 || parseInt(params[0], 10) > 20 ) { + this.sendChat(`${systemPrepend}Please set a countdown timer between 2 and 20.`); } else { - this.countdown = !this.countdown; + this.countdown = true; this.timerLimit = parseInt(params[0], 10); + this.sendChat(`${systemPrepend}Countdown of ${params[0]} seconds enabled.`); } } } diff --git a/src/utils.ts b/src/utils.ts index 787d147..fcb3d4b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -33,23 +33,6 @@ export function color(c: string) { return `|c0${c}`; } -export const systemPrepend = `${color('BBBBFF')}System:${color('FFFFFF')} `; -export const ownerColor = 'BBFFBB'; -export const playerColor = 'AAFFFF'; -export const opColor = 'FFBBBB'; - -export function unauthorizedChat(player: Player, operator: boolean = false) { - if (!player.room) { - // TODO - // The player is not in a room, but managed to instance a room command - idk how you - // would want to deal with this. - } else if (operator) { - player.sendChat(1, `${systemPrepend}You are not room owner or operator.`, player.room.name); - } else { - player.sendChat(1, `${systemPrepend}You are not room owner`, player.room.name); - } -} - export const stringToColour = function(str: string) { let hash = 0; @@ -71,3 +54,21 @@ export const stringToColour = function(str: string) { export function colorize(string: string, colour = stringToColour(string)) { return color(colour) + string + color('FFFFFF'); } + +export const ownerColor = 'BBFFBB'; +export const playerColor = 'AAFFFF'; +export const opColor = 'FFBBBB'; +export const systemColor = 'BBBBFF'; +export const systemPrepend = colorize('System:',systemColor) + " "; + +export function unauthorizedChat(player: Player, operator: boolean = false) { + if (!player.room) { + // TODO + // The player is not in a room, but managed to instance a room command - idk how you + // would want to deal with this. + } else if (operator) { + player.sendChat(1, `${systemPrepend}You are not room owner or operator.`, player.room.name); + } else { + player.sendChat(1, `${systemPrepend}You are not room owner.`, player.room.name); + } +}