From 074d6ad45d7afae4b03f07a92f6e173e4083a1c8 Mon Sep 17 00:00:00 2001 From: xangelo Date: Thu, 7 Sep 2023 13:56:29 -0400 Subject: [PATCH] feat: cleanup chat commands It's now easy to add chat commands independently without touching the main server code. You get - raw string without the `/server` prefix - Socket.io Server - Socket.io Socket - Calling Player Adn you are free to do whatever you need. All existing commands have been moved to this format. --- src/server/api.ts | 8 +-- src/server/chat-commands.ts | 64 ++++++-------------- src/server/chat-commands/base.ts | 14 +++++ src/server/chat-commands/index.ts | 14 +++++ src/server/chat-commands/refresh-cities.ts | 16 +++++ src/server/chat-commands/refresh-monsters.ts | 16 +++++ src/server/chat-commands/refresh-shops.ts | 18 ++++++ src/server/chat-commands/say.ts | 14 +++++ src/server/chat-commands/set-level.ts | 42 +++++++++++++ src/server/player.ts | 26 ++++++++ src/shared/player.ts | 2 +- 11 files changed, 182 insertions(+), 52 deletions(-) create mode 100644 src/server/chat-commands/base.ts create mode 100644 src/server/chat-commands/index.ts create mode 100644 src/server/chat-commands/refresh-cities.ts create mode 100644 src/server/chat-commands/refresh-monsters.ts create mode 100644 src/server/chat-commands/refresh-shops.ts create mode 100644 src/server/chat-commands/say.ts create mode 100644 src/server/chat-commands/set-level.ts diff --git a/src/server/api.ts b/src/server/api.ts index 0aa2689..4a6a9dd 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -163,14 +163,12 @@ app.post('/chat', authEndpoint, async (req: Request, res: Response) => { } if(msg.startsWith('/server') && req.player.permissions.includes('admin')) { + const sender = io.sockets.sockets.get(cache.get(`socket:${req.player.id}`)); try { - const output = await handleChatCommands(msg, req.player, io); - if(output) { - io.to(cache.get(`socket:${req.player.id}`)).emit('chat', renderChatMessage(output)); - } + await handleChatCommands(msg, req.player, io, sender); } catch(e) { - io.to(cache.get(`socket:${req.player.id}`)).emit('chat', renderChatMessage(broadcastMessage('server', e.message))); + sender.emit('chat', renderChatMessage(broadcastMessage('server', e.message))); } } else if(msg === '/online') { diff --git a/src/server/chat-commands.ts b/src/server/chat-commands.ts index 5e43627..527e77e 100644 --- a/src/server/chat-commands.ts +++ b/src/server/chat-commands.ts @@ -1,52 +1,24 @@ -import { Server } from 'socket.io'; -import { maxHp, maxVigor, Player } from '../shared/player'; -import { createMonsters } from '../../seeds/monsters'; -import { createAllCitiesAndLocations } from '../../seeds/cities'; -import { createShopItems, createShopEquipment } from '../../seeds/shop_items'; -import { broadcastMessage, Message } from '../shared/message'; -import { updatePlayer } from './player'; +import { Server, Socket } from 'socket.io'; +import { Player } from '../shared/player'; +import { broadcastMessage } from '../shared/message'; +import { renderChatMessage } from './views/chat'; +import { Commands } from './chat-commands/'; -export async function handleChatCommands(msg: string, player: Player, io: Server): Promise { - let message: Message; - if(msg === '/server refresh-monsters') { - await createMonsters(); - message = broadcastMessage('server', 'Monster refresh!'); - } - else if(msg === '/server refresh-cities') { - await createAllCitiesAndLocations(); - message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!'); - } - else if(msg === '/server refresh-shops') { - await createShopItems(); - await createShopEquipment(); - message = broadcastMessage('server', 'Refresh shop items'); - } - else if(msg.startsWith('/server set-level')) { - const level = parseInt(msg.split(' ').pop()); - if(level < 1) { - message = broadcastMessage('server', 'Needs to be at least level 1'); - } - else { - message = broadcastMessage('server', `Set player level: ${level}`); - player.level = level; - player.strength = 4; - player.constitution = 4; - player.dexterity = 4; - player.intelligence = 4; - player.hp = maxHp(player.constitution, player.level); - player.vigor = maxVigor(player.constitution, player.level); - player.stat_points = level-1; +export async function handleChatCommands(msg: string, player: Player, io: Server, sender: Socket): Promise { + const rawCommand = msg.split('/server ')[1]; - await updatePlayer(player); + let matched = false; + Commands.forEach(async command => { + if(command.regex.test(rawCommand)) { + matched = true; + console.log(`${player.username} running command: [${rawCommand}]`); + await command.handler(rawCommand, sender, player, io); } - } - else { - const str = msg.split('/server ')[1]; - if(str) { - message = broadcastMessage('server', str); - } - } + }); - return message; + if(!matched) { + const message = broadcastMessage('server', `Invalid command: [${rawCommand}]`); + sender.emit('chat', renderChatMessage(message)); + } } diff --git a/src/server/chat-commands/base.ts b/src/server/chat-commands/base.ts new file mode 100644 index 0000000..8241067 --- /dev/null +++ b/src/server/chat-commands/base.ts @@ -0,0 +1,14 @@ +import type { Player } from '../../shared/player'; +import type { Server, Socket } from 'socket.io'; + +type ChatCommandHandler = (command: string, sender: Socket, player: Player, io: Server) => Promise; + +export class ChatCommand { + constructor( + public name: string, + public regex: RegExp, + public handler: ChatCommandHandler + ) { + + } +} diff --git a/src/server/chat-commands/index.ts b/src/server/chat-commands/index.ts new file mode 100644 index 0000000..59e2d79 --- /dev/null +++ b/src/server/chat-commands/index.ts @@ -0,0 +1,14 @@ +import { ChatCommand } from "./base"; +import { refreshMonsters } from "./refresh-monsters"; +import { refreshCities } from './refresh-cities'; +import { refreshShops } from './refresh-shops'; +import { setLevel } from './set-level'; +import { say } from './say'; + +export const Commands = new Set(); + +Commands.add(refreshMonsters); +Commands.add(refreshCities); +Commands.add(refreshShops); +Commands.add(setLevel); +Commands.add(say); diff --git a/src/server/chat-commands/refresh-cities.ts b/src/server/chat-commands/refresh-cities.ts new file mode 100644 index 0000000..25c94cb --- /dev/null +++ b/src/server/chat-commands/refresh-cities.ts @@ -0,0 +1,16 @@ +import { ChatCommand } from './base'; +import { Socket } from 'socket.io'; +import { createAllCitiesAndLocations } from '../../../seeds/cities'; +import { broadcastMessage } from '../../shared/message'; +import { renderChatMessage } from './../views/chat'; +import { Player } from '../../shared/player'; + +async function handler(rawCommand: string, sender: Socket, player: Player) { + if(player.permissions.includes('admin')) { + await createAllCitiesAndLocations(); + const message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!'); + sender.emit('chat', renderChatMessage(message)); + } +} + +export const refreshCities = new ChatCommand('refresh-cities', new RegExp(/^refresh-cities/), handler); diff --git a/src/server/chat-commands/refresh-monsters.ts b/src/server/chat-commands/refresh-monsters.ts new file mode 100644 index 0000000..e0c0624 --- /dev/null +++ b/src/server/chat-commands/refresh-monsters.ts @@ -0,0 +1,16 @@ +import { ChatCommand } from './base'; +import { Socket } from 'socket.io'; +import { createMonsters } from '../../../seeds/monsters'; +import { broadcastMessage } from '../../shared/message'; +import { renderChatMessage } from './../views/chat'; +import { Player } from '../../shared/player'; + +async function handler(rawCommand: string, sender: Socket, player: Player) { + if(player.permissions.includes('admin')) { + await createMonsters(); + const message = broadcastMessage('server', 'Monsters refreshed!'); + sender.emit('chat', renderChatMessage(message)); + } +} + +export const refreshMonsters = new ChatCommand('refresh-monsters', new RegExp(/^refresh-monsters$/), handler); diff --git a/src/server/chat-commands/refresh-shops.ts b/src/server/chat-commands/refresh-shops.ts new file mode 100644 index 0000000..2f90c3b --- /dev/null +++ b/src/server/chat-commands/refresh-shops.ts @@ -0,0 +1,18 @@ +import { ChatCommand } from './base'; +import { Socket } from 'socket.io'; +import { createShopItems, createShopEquipment } from '../../../seeds/shop_items'; +import { broadcastMessage } from '../../shared/message'; +import { renderChatMessage } from './../views/chat'; +import { Player } from '../../shared/player'; + +async function handler(rawCommand: string, sender: Socket, player: Player) { + if(player.permissions.includes('admin')) { + await createShopItems(); + await createShopEquipment(); + const message = broadcastMessage('server', 'Shop items refreshed!'); + sender.emit('chat', renderChatMessage(message)); + + } +} + +export const refreshShops = new ChatCommand('refresh-shops', new RegExp(/^refresh-shops/), handler); diff --git a/src/server/chat-commands/say.ts b/src/server/chat-commands/say.ts new file mode 100644 index 0000000..1c1104e --- /dev/null +++ b/src/server/chat-commands/say.ts @@ -0,0 +1,14 @@ +import { ChatCommand } from './base'; +import { Socket } from 'socket.io'; +import { broadcastMessage, Message } from '../../shared/message'; +import { renderChatMessage } from './../views/chat'; +import { Player } from '../../shared/player'; + +async function handler(rawCommand: string, sender: Socket, player: Player) { + if(player.permissions.includes('moderator') || player.permissions.includes('admin')) { + let message: Message = broadcastMessage('server', rawCommand.split('say ')[1].trim()); + sender.emit('chat', renderChatMessage(message)); + } +} + +export const say = new ChatCommand('say', new RegExp(/^say (.*)+/), handler); diff --git a/src/server/chat-commands/set-level.ts b/src/server/chat-commands/set-level.ts new file mode 100644 index 0000000..640a194 --- /dev/null +++ b/src/server/chat-commands/set-level.ts @@ -0,0 +1,42 @@ +import { ChatCommand } from './base'; +import { Socket } from 'socket.io'; +import { broadcastMessage, Message } from '../../shared/message'; +import { renderChatMessage } from './../views/chat'; +import { updatePlayer } from '../player'; +import { maxHp, maxVigor, Player } from '../../shared/player'; + +async function handler(rawCommand: string, sender: Socket, player: Player) { + if(player.permissions.includes('admin') || player.permissions.includes('tester')) { + let message: Message; + // command in set-level username level + const pieces = rawCommand.split(' '); + if(pieces.length !== 2) { + message = broadcastMessage('server', 'format: /set-level level'); + } + else { + const level = parseInt(pieces.pop() || '0'); + if(level < 1) { + message = broadcastMessage('server', 'format: /set-level [level >= 1]'); + } + else { + message = broadcastMessage('server', `Set to level ${level}. Please reload.`); + + player.level = level; + player.strength = 4; + player.constitution = 4; + player.dexterity = 4; + player.intelligence = 4; + player.hp = maxHp(player.constitution, player.level); + player.vigor = maxVigor(player.constitution, player.level); + player.stat_points = level-1; + + await updatePlayer(player); + } + } + + sender.emit('chat', renderChatMessage(message)); + + } +} + +export const setLevel = new ChatCommand('set-level', new RegExp(/^set-level \d+$/), handler); diff --git a/src/server/player.ts b/src/server/player.ts index a9d077c..b957969 100644 --- a/src/server/player.ts +++ b/src/server/player.ts @@ -31,6 +31,32 @@ export async function loadPlayer(authToken: string): Promise { return res; } +export async function findPlayerByUsername(username: string): Promise { + const res = await db.first() + .select( + 'players.*', + 'profession_levels.level', + 'profession_levels.exp', + db.raw(`coalesce(pp.permissions, '[]'::json) as permissions`) + ) + .from('players') + .join('profession_levels', function() { + this.on(function() { + this.on('profession_levels.player_id', '=', 'players.id') + this.andOn('profession_levels.profession', '=', 'players.profession') + }) + }) + .leftJoin( + db.raw(`(select json_agg(pp.permission) as permissions, pp.player_id from player_permissions pp group by pp.player_id) pp`), + 'pp.player_id','=', 'players.id' + ) + .where({ + 'players.username': username + }); + + return res; +} + export async function createPlayer(): Promise { const raw: Partial = { username: `Player${Date.now().toString().substr(-7)}`, diff --git a/src/shared/player.ts b/src/shared/player.ts index 93d75f3..564ed1d 100644 --- a/src/shared/player.ts +++ b/src/shared/player.ts @@ -3,7 +3,7 @@ import { Stat } from './stats'; import { SkillDefinition, Skill } from './skills'; import { EquippedItemDetails } from './equipped'; -export type Permission = 'admin' | 'moderator'; +export type Permission = 'admin' | 'moderator' | 'tester'; export type Player = { id: string, -- 2.25.1