feat: cleanup chat commands
authorxangelo <me@xangelo.ca>
Thu, 7 Sep 2023 17:56:29 +0000 (13:56 -0400)
committerxangelo <me@xangelo.ca>
Thu, 7 Sep 2023 17:56:29 +0000 (13:56 -0400)
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
src/server/chat-commands.ts
src/server/chat-commands/base.ts [new file with mode: 0644]
src/server/chat-commands/index.ts [new file with mode: 0644]
src/server/chat-commands/refresh-cities.ts [new file with mode: 0644]
src/server/chat-commands/refresh-monsters.ts [new file with mode: 0644]
src/server/chat-commands/refresh-shops.ts [new file with mode: 0644]
src/server/chat-commands/say.ts [new file with mode: 0644]
src/server/chat-commands/set-level.ts [new file with mode: 0644]
src/server/player.ts
src/shared/player.ts

index 0aa2689c95a6e594999ccffc22309771648ee358..4a6a9dd5dc990d87863633bb185f77c7fd246ff5 100644 (file)
@@ -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') {
index 5e43627095d6ec5a19b5ec5395fd74ea2fa466ca..527e77e30a1a4636a60d06835d8bb1efa7e84cc1 100644 (file)
@@ -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<Message> {
-  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<void> {
+  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 (file)
index 0000000..8241067
--- /dev/null
@@ -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<void>;
+
+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 (file)
index 0000000..59e2d79
--- /dev/null
@@ -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<ChatCommand>();
+
+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 (file)
index 0000000..25c94cb
--- /dev/null
@@ -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 (file)
index 0000000..e0c0624
--- /dev/null
@@ -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 (file)
index 0000000..2f90c3b
--- /dev/null
@@ -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 (file)
index 0000000..1c1104e
--- /dev/null
@@ -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 (file)
index 0000000..640a194
--- /dev/null
@@ -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);
index a9d077c23c68b474b0355cb0ff23ec76f9a55fbf..b9579698ab9fc9fa6a0eade540d0fd22886c4f4e 100644 (file)
@@ -31,6 +31,32 @@ export async function loadPlayer(authToken: string): Promise<Player> {
   return res;
 }
 
+export async function findPlayerByUsername(username: string): Promise<Player> {
+  const res = await db.first()
+            .select(
+              'players.*',
+              'profession_levels.level',
+              'profession_levels.exp',
+              db.raw(`coalesce(pp.permissions, '[]'::json) as permissions`)
+            )
+            .from<Player>('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<Player> {
   const raw: Partial<Player> = {
     username: `Player${Date.now().toString().substr(-7)}`,
index 93d75f3bdb8ed3816ae3803f1e095ac07879f50c..564ed1d0943125315088f6fc6312051550f00422 100644 (file)
@@ -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,