From 8bcbdbfe3525b010c190a4b605c9b1884f061b59 Mon Sep 17 00:00:00 2001 From: Kim Ravn Hansen Date: Fri, 5 Sep 2025 17:47:28 +0200 Subject: [PATCH] stuff and things --- server/models/game.js | 16 +- server/models/item.js | 4 +- server/public/index.html | 2 +- server/server.js | 366 ++++++++++++++++++--------------------- server/utils/helpers.js | 12 -- server/utils/messages.js | 127 ++++++++++++++ 6 files changed, 306 insertions(+), 221 deletions(-) delete mode 100755 server/utils/helpers.js create mode 100755 server/utils/messages.js diff --git a/server/models/game.js b/server/models/game.js index 274523c..82371f2 100755 --- a/server/models/game.js +++ b/server/models/game.js @@ -8,10 +8,10 @@ */ import WebSocket from "ws"; -import { Character } from "./character"; -import { ItemTemplate } from "./item"; +import { Character } from "./character.js"; +import { ItemTemplate } from "./item.js"; -class Game{ +export class Game{ /** @type {Map} List of all item templates in the game */ _itemTemplates = new Map(); @@ -28,14 +28,10 @@ class Game{ _characters = new Map(); /** + * All players ever registered, mapped by name => player. + * * @protected * @type {Map} Map of users in the game username->Player */ - _playersByName = new Map(); - - /** - * @protected - * @type {Map} Map of users in the game username->Player - */ - _playersBySocket = new Map(); + _players = new Map(); get players() { return this._players; } } diff --git a/server/models/item.js b/server/models/item.js index 090868f..ef4f625 100755 --- a/server/models/item.js +++ b/server/models/item.js @@ -1,4 +1,4 @@ -import { cleanIdentifier } from "../utils/helpers"; +import { cleanName } from "../utils/id.js"; /** * Item templates are the built-in basic items of the game. @@ -56,7 +56,7 @@ export class ItemTemplate { throw new Error("itemSlots must be a finite number!"); } if (typeof id === "undefined") { - id = cleanIdentifier(name); + id = cleanName(name); } if (typeof id !== "string") { throw new Error("id must be a string!"); diff --git a/server/public/index.html b/server/public/index.html index e8d4e27..5b207f5 100755 --- a/server/public/index.html +++ b/server/public/index.html @@ -132,7 +132,7 @@ this.updateStatus('Connecting...', 'connecting'); try { - this.ws = new WebSocket(wsUrl); + this. s = new WebSocket(wsUrl); this.ws.onopen = () => { this.updateStatus('Connected', 'connected'); diff --git a/server/server.js b/server/server.js index 99ed095..3824ffe 100755 --- a/server/server.js +++ b/server/server.js @@ -4,90 +4,147 @@ import path from "path"; import fs from "fs"; import { Player } from "./models/player.js"; import { Game } from "./models/game.js"; +import { ClientMessage, MSG_ERROR, MSG_MESSAGE, MSG_PROMPT, MSG_CALAMITY } from "./utils/messages.js"; +class Session { + /** @type {boolean} */ + usernameProcessed = false; -/** - * Parse a string with json-encoded data without throwing exceptions. - * - * @param {string} data - * @return {any} - */ -function parseJson(data) { - if (typeof data !== "string") { - console.error("Attempting to parse json, but data was not even a string", data); - return; - } + /** @type {boolean} */ + passwordProcessed = false; - try { - return JSON.parse(data) - } catch (error) { - console.error('Error parsing data as json:', error, data); - } + /** @type {boolean} */ + ready = false; + + /** @type Date */ + latestPing; + + /** @type {Player} */ + player; } class MudServer { - constructor() { - this.game = new Game(); + /** @type {Map} */ + sessions = new Map(); + + /** @type {Game} */ + game = new Game(); + + /** + * Send a message via a websocket. + * + * @param {WebSocket} websocket + * @param {string|number} messageType + * @param {...any} args + */ + send(websocket, messageType, ...args) { + // create array consisting of [messageType, args[0], args[1], ... ]; + websocket.send(JSON.stringify([messageType, ...args])); } /** - * @param {WebSocket} ws + * @param {WebSocket} websocket */ - onConnectionEstabished(ws) { - console.log('New connection established'); + onConnectionEstabished(websocket) { + console.log("New connection established"); + this.sessions[websocket] = new Session(); - ws.send(JSON.stringify( - ["m", "Welcome to the WebSocket MUD!\nWhat is your username name?"] - )); - ws.on('message', (data) => { - this.onIncomingMessage(parseJson(data)); - }); + websocket.on("message", (data) => { this.onIncomingMessage(websocket, data) }); + websocket.on("close", () => { this.onConnectionClosed(websocket); }); - ws.on('close', () => { - this.onConnectionClosed(ws); - }); + this.send(websocket, MSG_MESSAGE, "Welcome to MUUUHD", "big"); + this.send(websocket, MSG_PROMPT, "Please enter your username"); } /** - * @param {WebSocket} ws - * @param {strings} message - * @returns + * @param {WebSocket} websocket + * @param {strings} data */ - onIncomingMessage(ws, message) { - const player = this.players.get(ws); + onIncomingMessage(websocket, data) { + const session = this.sessions.get(websocket); - if (!player) { - // Player hasn't been created yet, expecting name - const name = message.content.trim(); - if (name && !this.players.has(name)) { - this.createPlayer(ws, name); - } else { - /** - * @todo: send an array instead of object. - * element 1 is the type - * element 2 is the content - * element 3+ are expansions - */ - ws.send(JSON.stringify({ - type: 'message', - content: 'Invalid name or name already taken. Please choose another:' - })); + if (!session) { + console.error("Incoming message from a client without a session!", data); + this.send(websocket, MSG_ERROR, "terminal", "You do not have an active session. Go away!") + websocket.close(); + return + } + + let message; + + try { + message = new ClientMessage(data); + } catch (error) { + console.error("Bad websocket message", data, error); + this.send(websocket, MSG_ERROR, "terminal", "You sent me a bad message! Goodbye...") + websocket.close(); + return + } + + if (!session.usernameProcessed) { + // + //---------------------------------------------------- + // We haven"t gotten a username yet, so we expect one. + //---------------------------------------------------- + if (!message.hasUsername()) { + console.error("User should have sent a “username” message, but sent something else instead") + this.send(websocket, MSG_CALAMITY, "I expected you to send me a username, but you sent me something else instead. You bad! Goodbye...") + + // for now, just close the socket. + websocket.close(); } + + const player = this.game.players.get(message.username); + + if (!player) { + // player not found - for now, just close the connection - make a better + console.log("Invalid username sent during login: %s", username); + this.send(websocket, MSG_ERROR, "Invalid username"); + this.send(websocket, MSG_PROMPT, "Please enter a valid username"); + } + + // correct username, tentatively assign player to session + // even though we have not yet validated the password. + session.player = player; + session.usernameProcessed = true; + this.send(websocket, MSG_MESSAGE, "Username received"); + this.send(websocket, MSG_PROMPT, "Enter your password"); + return; } - // Process command - this.processCommand(player, message.content.trim()); + // + //---------------------------------------------------- + // The player has entered a valid username, now expect + // a password. + //---------------------------------------------------- + if (!session.passwordProcessed) { + if (!message.hasPassword) { + console.error("Youser should have sent a “password” message, but sent this instead: %s", message.type); + } + } + + + // + //---------------------------------------------------- + // Process the player's commands + //---------------------------------------------------- + if (message.isCommand()) { + // switch case for commands. + return; + } + + console.error("We have received a message we couldn't handle!!!", message); } /** - * - * @param {WebSocket} ws - * @param {string} name + * + * @param {WebSocket} websocket + * @param {string} name */ - createPlayer(ws, name) { - const player = new Player(name, ws); - this.players.set(ws, player); + createPlayer(websocket, name) { + const player = new Player(name, websocket); + this.players.set(websocket, player); this.players.set(name, player); const startRoom = this.rooms.get("town_square"); @@ -98,204 +155,121 @@ class MudServer { } /** - * - * @param {Player} player - * @param {string} input + * + * @param {Player} player + * @param {string} input */ processCommand(player, input) { - const args = input.toLowerCase().split(' '); + const args = input.toLowerCase().split(" "); const command = args[0]; switch (command) { - case 'look': - case 'l': + case "look": + case "l": this.showRoom(player); break; - case 'go': - case 'move': + case "go": + case "move": if (args[1]) { this.movePlayer(player, args[1]); } else { - player.sendMessage('Go where?'); + player.sendMessage("Go where?"); } break; - case 'north': - case 'n': - this.movePlayer(player, 'north'); + case "north": + case "n": + this.movePlayer(player, "north"); break; - case 'south': - case 's': - this.movePlayer(player, 'south'); + case "south": + case "s": + this.movePlayer(player, "south"); break; - case 'east': - case 'e': - this.movePlayer(player, 'east'); + case "east": + case "e": + this.movePlayer(player, "east"); break; - case 'west': - case 'w': - this.movePlayer(player, 'west'); + case "west": + case "w": + this.movePlayer(player, "west"); break; - case 'say': + case "say": if (args.length > 1) { - const message = args.slice(1).join(' '); + const message = args.slice(1).join(" "); this.sayToRoom(player, message); } else { - player.sendMessage('Say what?'); + player.sendMessage("Say what?"); } break; - case 'who': + case "who": this.showOnlinePlayers(player); break; - case 'inventory': - case 'inv': + case "inventory": + case "inv": this.showInventory(player); break; - case 'help': + case "help": this.showHelp(player); break; - case 'quit': - player.sendMessage('Goodbye!'); + case "quit": + player.sendMessage("Goodbye!"); player.websocket.close(); break; default: - player.sendMessage(`Unknown command: ${command}. Type 'help' for available commands.`); + player.sendMessage(`Unknown command: ${command}. Type "help" for available commands.`); } player.sendPrompt(); } - /** - * - * @param {Player} player - * @param {*} direction - * @returns - */ - movePlayer(player, direction) { - const currentRoom = this.rooms.get(player.currentRoom); - const newRoomId = currentRoom.exits[direction]; - - if (!newRoomId) { - player.sendMessage('You cannot go that way.'); - return; - } - - const newRoom = this.rooms.get(newRoomId); - if (!newRoom) { - player.sendMessage('That area is not accessible right now.'); - return; - } - - // Remove from current room and add to new room - currentRoom.removePlayer(player); - player.currentRoom = newRoomId; - newRoom.addPlayer(player); - - this.showRoom(player); - } - - showRoom(player) { - const room = this.rooms.get(player.currentRoom); - let description = `\n=== ${room.name} ===\n`; - description += `${room.description}\n`; - - // Show exits - const exits = Object.keys(room.exits); - if (exits.length > 0) { - description += `\nExits: ${exits.join(', ')}`; - } - - // Show other players - const otherPlayers = room.getPlayersExcept(player); - if (otherPlayers.length > 0) { - description += `\nPlayers here: ${otherPlayers.map(p => p.name).join(', ')}`; - } - - player.sendMessage(description); - } - - sayToRoom(player, message) { - const room = this.rooms.get(player.currentRoom); - const fullMessage = `${player.name} says: "${message}"`; - - room.broadcastToRoom(fullMessage, player); - player.sendMessage(`You say: "${message}"`); - } - - showOnlinePlayers(player) { - const playerList = Array.from(this.players.keys()); - player.sendMessage(`Online players (${playerList.length}): ${playerList.join(', ')}`); - } - - showInventory(player) { - if (player.inventory.length === 0) { - player.sendMessage('Your inventory is empty.'); - } else { - player.sendMessage(`Inventory: ${player.inventory.join(', ')}`); - } - } - - showHelp(player) { - const helpText = ` -Available Commands: -- look, l: Look around the current room -- go , : Move in a direction (north, south, east, west, n, s, e, w) -- say : Say something to other players in the room -- who: See who else is online -- inventory, inv: Check your inventory -- help: Show this help message -- quit: Leave the game - `; - player.sendMessage(helpText); - } - /** * Called when a websocket connection is closing. + * + * @param {WebSocket} websocket */ - onConnectionClosed(ws) { - const player = this.players.get(ws); - if (player) { - console.log(`Player ${player.name} disconnected`); + onConnectionClosed(websocket) { + const session = this.sessions.get(websocket); - // Remove from room - const room = this.rooms.get(player.currentRoom); - if (room) { - room.removePlayer(player); - } + if (session && session.player) { + console.log(`Player ${player.username} disconnected`); - // Clean up references - this.players.delete(ws); - this.players.delete(player.name); + // + // Handle player logout (move the or hide their characters) + // this.game.playerLoggedOut(); + } else { + console.log("A player without a session disconnected"); } + + this.sessions.delete(websocket); } } // Create HTTP server for serving the client const server = http.createServer((req, res) => { - // let filePath = path.join(__dirname, 'public', req.url === '/' ? 'index.html' : req.url); - let filePath = path.join('public', req.url === '/' ? 'index.html' : req.url); + // let filePath = path.join(__dirname, "public", req.url === "/" ? "index.html" : req.url); + let filePath = path.join("public", req.url === "/" ? "index.html" : req.url); const ext = path.extname(filePath); const contentTypes = { - '.js': 'application/javascript', - '.css': 'text/css', - '.html': 'text/html', + ".js": "application/javascript", + ".css": "text/css", + ".html": "text/html", }; if (!contentTypes[ext]) { // Invalid file, pretend it did not exist! res.writeHead(404); - res.end('File not found'); + res.end(`File ${filePath} not found (invalid $ext)`); return; } @@ -304,19 +278,19 @@ const server = http.createServer((req, res) => { fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404); - res.end('File not found'); + res.end(`File ${filePath} . ${ext} not found (${err})`); return; } - res.writeHead(200, { 'Content-Type': contentType }); + res.writeHead(200, { "Content-Type": contentType }); res.end(data); }); }); // Create WebSocket server -const wss = new WebSocketServer({ server }); +const websocketServer = new WebSocketServer({ server }); const mudServer = new MudServer(); -wss.on('connection', (ws) => { +websocketServer.on("connection", (ws) => { mudServer.onConnectionEstabished(ws); }); diff --git a/server/utils/helpers.js b/server/utils/helpers.js deleted file mode 100755 index 0e23a37..0000000 --- a/server/utils/helpers.js +++ /dev/null @@ -1,12 +0,0 @@ -export function rollDice(sides) { - const r = Math.random() - return Math.floor(r * sides) + 1; -} - -export function d6() { - return rollDice(6); -} - -export function d8() { - return rollDice(8); -} diff --git a/server/utils/messages.js b/server/utils/messages.js new file mode 100755 index 0000000..f3553ed --- /dev/null +++ b/server/utils/messages.js @@ -0,0 +1,127 @@ +/** + * Very bad logic error. Player must quit game, refresh page, and log in again. + * + * Client-->Server + * or + * Server-->Client-->Plater + */ +export const MSG_CALAMITY = "calamity"; + +/** Tell recipient that an error has occurred */ +export const MSG_ERROR = "e"; + +/** + * Message to be displayed. + * + * Server-->Client-->Player + */ +export const MSG_MESSAGE = "m"; + +/** + * Message contains the player's password (or hash or whatever). + * + * Player-->Client-->Server + */ +export const MSG_PASSWORD = "pass"; + +/** + * Server-->Client-->Player + * + * Server tells the client to prompt the player for some info + */ +export const MSG_PROMPT = "ask"; + +/** + * Client sends the player's username to the server + * + * Player-->Client-->Server + */ +export const MSG_USERNAME = "user"; + +/** + * Player has entered a command, and wants to do something. + * + * Player-->Client-->Server + */ +export const MSG_COMMAND = "c"; + +/** + * Represents a message sent from client to server. + */ +export class ClientMessage { + /** + * @protected + * @type {any[]} _arr The array that contains the message data + */ + _arr; + + /** The message type. + * + * One of the MSG_* constants from this document. + * + * @returns {string} + */ + get type() { + return this._arr[0]; + } + + + /** + * @param {string} msgData the raw text data in the websocket message. + */ + constructor(msgData) { + if (typeof msgData !== "string") { + throw new Error("Could not create client message. Attempting to parse json, but data was not even a string, it was a " + typeof msgData); + return + } + + try { + this._arr = JSON.parse(msgData); + } catch (_) { + throw new Error(`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`); + } + + if (typeof this._arr !== "array") { + throw new Error(`Could not create client message. Excpected an array, but got a ${typeof this._arr}`); + } + + if (this._arr.length < 1) { + throw new Error("Could not create client message. Excpected an array with at least 1 element, but got an empty one"); + } + + this._arr = arr; + } + + /** Does this message contain a message that should be displayed to the user the "normal" way? */ + isMessage() { + return this._arr[0] === "m"; + } + + /** Does this message contain a username-response from the client? */ + hasUsername() { + return this._arr[0] === MSG_USERNAME; + } + + /** Does this message contain a password-response from the client? */ + hasPassword() { + return this._arr[0] === MSG_PASSWORD; + } + + /** @returns {string|false} Get the username stored in this message */ + get username() { + return this.hasUsername() ? this._arr[1] : false; + } + + /** @returns {string|false} Get the password stored in this message */ + get password() { + return this.hasPassword() ? this._arr[1] : false; + } + + get command() { + return this.isCommand() ? this._attr[1] : false; + } + + isCommand() { + return this._raw[0] === MSG_COMMAND; + } +}