Things and støff

This commit is contained in:
Kim Ravn Hansen
2025-09-14 20:43:06 +02:00
parent ed91a7f2f7
commit aeb9d776fc
25 changed files with 688 additions and 591 deletions

View File

@@ -1,9 +1,9 @@
import WebSocket from "ws";
import { Player } from "./player.js";
import * as msg from "../utils/messages.js";
import { mustBeString, mustBe } from "../utils/mustbe.js";
import { Scene } from "../scenes/scene.js";
import { gGame } from "./globals.js";
import { formatMessage, MessageType } from "../utils/messages.js";
export class Session {
/** @type {WebSocket} */
@@ -71,7 +71,7 @@ export class Session {
/**
* Send a message via our websocket.
*
* @param {string|number} messageType
* @param {MessageType} messageType The message "header" (the first arg in the array sent to the client) holds the message type.
* @param {...any} args
*/
send(messageType, ...args) {
@@ -79,7 +79,7 @@ export class Session {
console.error("Trying to send a message without a valid websocket", messageType, args);
return;
}
this._websocket.send(JSON.stringify([messageType, ...args]));
this._websocket.send(formatMessage(messageType, ...args));
}
/**
@@ -100,7 +100,7 @@ export class Session {
}
this.send(
msg.PROMPT, // message type
MessageType.PROMPT, // message type
text, // TODO: prompt text must be string or an array of strings
mustBe(options, "object"),
);
@@ -113,12 +113,12 @@ export class Session {
* @param {object?} options message options for the client.
*/
sendText(text, options = {}) {
this.send(msg.TEXT, text, options);
this.send(MessageType.TEXT, text, options);
}
/** @param {string|string[]} errorMessage */
sendError(errorMessage) {
this.send(msg.ERROR, mustBeString(errorMessage));
sendError(errorMessage, options = { verbatim: true }) {
this.send(MessageType.ERROR, mustBeString(errorMessage), options);
}
/**
@@ -128,15 +128,15 @@ export class Session {
calamity(errorMessage) {
//
// The client should know not to format calamaties anyway, but we add “preformatted” anyway
this.send(msg.CALAMITY, errorMessage, { preformatted: true });
this.send(MessageType.CALAMITY, errorMessage, { preformatted: true });
this.close();
}
/**
* @param {string} systemMessageType
* @param {MessageType} systemMessageType
* @param {any?} value
*/
sendSystemMessage(systemMessageType, value = undefined) {
this.send(msg.SYSTEM, mustBeString(systemMessageType), value);
this.send(MessageType.SYSTEM, mustBeString(systemMessageType), value);
}
}

View File

@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"figlet": "^1.8.2",
"sprintf-js": "^1.1.3",
"ws": "^8.14.2"
},
"devDependencies": {
@@ -378,6 +379,12 @@
"node": ">=10"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

View File

@@ -18,6 +18,7 @@
"license": "MIT",
"dependencies": {
"figlet": "^1.8.2",
"sprintf-js": "^1.1.3",
"ws": "^8.14.2"
},
"devDependencies": {
@@ -33,7 +34,6 @@
"quoteProps": "consistent",
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 4,
"bracketSpacing": true,
"objectWrap": "preserve",
"arrowParens": "always"

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,11 +1,16 @@
import { crackdown } from "./crackdown.js";
import { parseArgs } from "./parseArgs.js";
import { MessageType } from "./messages.js";
const MsgContext.REPLY = "R";
const QUIT = "QUIT";
const HELP = "HELP";
const COLON = ":";
/** Regex to validate if a :help [topic] command i entered correctly */
const helpRegex = /^:help(?:\s+(.*))?$/;
const colonRegex = /^:([a-z0-9_]+)(?:\s+(.*))?$/;
/** Regex to validate if a :<command> [args] was entered correctly */
const colonRegex = /^:([a-z0-9_]+)(?:\s+(.*?)\s*)?$/;
/**
* The client that talks to the MUD Sever
*/
class MUDClient {
//
// Constructor
@@ -14,7 +19,7 @@ class MUDClient {
this.websocket = null;
/** @type {boolean} Are we in development mode (decided by the server); */
this.dev = false;
this.isDev = false;
this.promptOptions = {};
this.shouldReply = false;
@@ -176,8 +181,8 @@ class MUDClient {
//
// The quit command has its own message type
if (inputText === ":quit") {
this.send(QUIT);
this.writeToOutput("> " + inputText, { class: "input" });
this.send(MessageType.QUIT);
this.writeToOutput("> " + inputText, { verbatim: true, class: "input" });
return;
}
@@ -193,8 +198,8 @@ class MUDClient {
let help = helpRegex.exec(inputText);
if (help) {
console.log("here");
help[1] ? this.send(HELP, help[1].trim()) : this.send(HELP);
this.writeToOutput("> " + inputText, { class: "input" });
help[1] ? this.send(MshType.HELP, help[1].trim()) : this.send(MshType.HELP);
this.writeToOutput("> " + inputText, { verbatim: true, class: "input" });
return;
}
@@ -204,19 +209,14 @@ class MUDClient {
// _ | (_| (_) | | | | | | | | | | | (_| | | | | (_| |
// (_) \___\___/|_| |_| |_|_| |_| |_|\__,_|_| |_|\__,_|
//------------------------------------------------------
let colonCommand = colonRegex.exec(inputText);
if (colonCommand) {
this.send(COLON, colonCommand[1], colonCommand[2]);
this.writeToOutput("> " + inputText, { class: "input" });
const colon = colonRegex.exec(inputText);
if (colon) {
const args = typeof colon[2] === "string" ? parseArgs(colon[2]) : [];
this.send(MessageType.COLON, colon[1], args);
this.writeToOutput("> " + inputText, { verbatim: true, class: "input colon" });
return;
}
//
// The server doesn't want any input from us, so we just ignore this input
if (!this.shouldReply) {
// the server is not ready for data!
return;
}
// _
// _ __ ___ _ __ | |_ _
// | '__/ _ \ '_ \| | | | |
@@ -227,6 +227,12 @@ class MUDClient {
// We handle replies below
//-------------------------
//
if (!this.shouldReply) {
// the server is not ready for data!
return;
}
// The server wants a password, let's hash it before sending it.
if (this.promptOptions.password) {
inputText = await this.hashPassword(inputText);
@@ -238,14 +244,14 @@ class MUDClient {
this.username = inputText;
}
this.send(REPLY, inputText);
this.send(MessageType.REPLY, inputText);
this.shouldReply = false;
this.promptOptions = {};
//
// We add our own command to the output stream so the
// player can see what they typed.
this.writeToOutput("> " + inputText, { class: "input" });
this.writeToOutput("> " + inputText, { verbatim: true, class: "input" });
return;
}
@@ -257,7 +263,7 @@ class MUDClient {
//
/** @param {any[]} data*/
onMessageReceived(data) {
if (this.dev) {
if (this.isDev) {
console.debug(data);
}
const messageType = data.shift();
@@ -291,7 +297,7 @@ class MUDClient {
return this.handleDebugMessages(data);
}
if (this.dev) {
if (this.isDev) {
this.writeToOutput(`unknown message type: ${messageType}: ${JSON.stringify(data)}`, {
class: "debug",
verbatim: true,
@@ -314,7 +320,7 @@ class MUDClient {
// Debug messages let the server send data to be displayed on the player's screen
// and also logged to the players browser's log.
handleDebugMessages(data) {
if (!this.dev) {
if (!this.isDev) {
return; // debug messages are thrown away if we're not in dev mode.
}
this.writeToOutput(data, { class: "debug", verbatim: true });
@@ -332,16 +338,16 @@ class MUDClient {
console.debug("Incoming system message", data);
/** @type {string} */
const messageType = data.shift();
const systemMessageType = data.shift();
switch (messageType) {
switch (systemMessageType) {
case "username":
this.username = data[0];
break;
case "dev":
// This is a message that tells us that the server is in
// "dev" mode, and that we should do the same.
this.dev = data[0];
this.isDev = !!data[0];
this.status.textContent = "[DEV] " + this.status.textContent;
break;
case "salt":
@@ -349,12 +355,12 @@ class MUDClient {
console.debug("updating crypto salt", data[0]);
break;
default:
console.debug("unknown system message", messageType, data);
console.debug("unknown system message", systemMessageType, data);
}
// If we're in dev mode, we should output all system messages (in a shaded/faint fashion).
if (this.dev) {
this.writeToOutput(`system message: ${messageType} = ${JSON.stringify(data)}`, { class: "debug" });
if (this.isDev) {
this.writeToOutput(`system message: ${systemMessageType} = ${JSON.stringify(data)}`, { class: "debug" });
}
return;
}
@@ -436,7 +442,7 @@ class MUDClient {
* @param {string} className
*/
updateStatus(message, className) {
this.status.textContent = this.dev ? `[DEV] Status: ${message}` : `Status: ${message}`;
this.status.textContent = this.isDev ? `[DEV] Status: ${message}` : `Status: ${message}`;
this.status.className = className;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
server/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
server/public/messages.js Symbolic link
View File

@@ -0,0 +1 @@
../utils/messages.js

1
server/public/mustbe.js Symbolic link
View File

@@ -0,0 +1 @@
../utils/mustbe.js

View File

@@ -1,5 +1,3 @@
import { mustBeString } from "./mustbe.js";
/**
* Parse a command string into arguments. For use with colon-commands.
*
@@ -7,7 +5,9 @@ import { mustBeString } from "./mustbe.js";
* @returns {(string|number)[]} Command arguments
*/
export function parseArgs(cmdString) {
mustBeString(cmdString);
if (typeof cmdString === "string") {
throw new Error("Expected string. GoT a finger in the eye instead");
}
const args = [];
const quoteChars = ["'", '"', "`"];
const backslash = "\\";
@@ -16,7 +16,8 @@ export function parseArgs(cmdString) {
let inQuotes = false; // are we inside quotes of some kind?
let currentQuoteChar = ""; // if were in quotes, which are they?
const push = (value) => {
// helper function
const pushVal = (value) => {
const n = Number(value);
if (Number.isSafeInteger(n)) {
args.push(n);
@@ -39,7 +40,7 @@ export function parseArgs(cmdString) {
} else if (char === " " || char === "\t") {
// Whitespace - end current arg if it exists
if (currentArg) {
push(currentArg);
pushVal(currentArg);
currentArg = "";
}
// Skip multiple whitespace
@@ -68,7 +69,7 @@ export function parseArgs(cmdString) {
// Add final argument if exists
if (currentArg) {
push(currentArg);
pushVal(currentArg);
}
if (currentQuoteChar) {
@@ -79,5 +80,3 @@ export function parseArgs(cmdString) {
return args;
}
console.log(parseArgs("\"k1m er '-9 ' `anus pikke`"));

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -2,10 +2,10 @@ import { PasswordPrompt } from "./passwordPrompt.js";
import { Player } from "../../models/player.js";
import { Scene } from "../scene.js";
import { UsernamePrompt } from "./usernamePrompt.js";
import { CreateUsernamePrompt } from "../playerCreation/createUsernamePrompt.js";
/** @property {Session} session */
export class AuthenticationScene extends Scene {
introText = [
"= Welcome", //
];
@@ -15,13 +15,13 @@ export class AuthenticationScene extends Scene {
onReady() {
// current prompt
this.doPrompt(new UsernamePrompt(this));
this.show(UsernamePrompt);
}
/** @param {Player} player */
usernameAccepted(player) {
this.player = player;
this.doPrompt(new PasswordPrompt(this));
this.show(PasswordPrompt);
}
passwordAccepted() {
@@ -34,4 +34,8 @@ export class AuthenticationScene extends Scene {
this.session.setScene("new JustLoggedInScene");
}
}
createPlayer() {
scene.session.setScene(new PlayerCreationScene(this.scene));
}
}

View File

@@ -1,7 +1,6 @@
import { Prompt } from "../prompt.js";
import * as security from "../../utils/security.js";
import { Config } from "../../config.js";
import { context } from "../../utils/messages.js";
export class PasswordPrompt extends Prompt {
//

View File

@@ -1,11 +1,16 @@
import { Player } from "../../models/player.js";
import { Prompt } from "../prompt.js";
import * as security from "../../utils/security.js";
import { context } from "../../utils/messages.js";
import { gGame } from "../../models/globals.js";
import { PlayerCreationScene } from "../playerCreation/playerCreationSene.js";
import { Config } from "../../config.js";
import { AuthenticationScene } from "./authenticationScene.js";
/**
* @class
*
* @property {AuthenticationScene} scene
*/
export class UsernamePrompt extends Prompt {
//
promptText = [
@@ -24,17 +29,14 @@ export class UsernamePrompt extends Prompt {
// Let the client know that we're asking for a username
promptOptions = { username: true };
//
// User entered ":create"
onColon_create() {
// User creation scene.
this.scene.session.setScene(new PlayerCreationScene(this.scene));
/** @returns {AuthenticationScene} */
get scene() {
return this._scene;
}
//
// User replied to our prompt
onReply(text) {
//
// do basic syntax checks on usernames
if (!security.isUsernameSane(text)) {

View File

@@ -1,7 +1,6 @@
import { Prompt } from "../prompt.js";
import * as security from "../../utils/security.js";
import { Config } from "../../config.js";
import { context } from "../../utils/messages.js";
export class CreatePasswordPrompt extends Prompt {
//

View File

@@ -19,7 +19,7 @@ export class PlayerCreationScene extends Scene {
this.session.calamity("Server is full, no more players can be created");
}
this.doPrompt(new CreateUsernamePrompt(this));
this.showPrompt(new CreateUsernamePrompt(this));
}
/**
@@ -27,13 +27,13 @@ export class PlayerCreationScene extends Scene {
*
* @param {string} username
*/
onUsernameAccepted(username) {
usernameAccepted(username) {
const player = gGame.createPlayer(username);
this.player = player;
this.session.sendSystemMessage("salt", player.salt);
this.session.sendText(`Username _*${username}*_ is available, and I've reserved it for you :)`);
this.doPrompt("new passwordprompt");
this.showPrompt("new passwordprompt");
}
/**
@@ -42,9 +42,15 @@ export class PlayerCreationScene extends Scene {
*
* @param {string} password
*/
onPasswordAccepted(password) {
passwordAccepted(password) {
this.password = password;
this.session.sendText("*_Success_* ✅ You will now be asked to log in again, sorry for that ;)");
this.player.setPasswordHash(security.generateHash(this.password));
}
//
// User entered ":create"
onColon__create() {
this.scene.createPlayer();
}
}

View File

@@ -1,10 +1,10 @@
import figlet from "figlet";
import { gGame } from "../models/globals.js";
import * as msg from "../utils/messages.js";
import { Session } from "../models/session.js";
import { Scene } from "./scene.js";
import { WebsocketMessage } from "../utils/messages.js";
import { MessageType, WebsocketMessage } from "../utils/messages.js";
import { mustBe, mustBeString } from "../utils/mustbe.js";
import { sprintf } from "sprintf-js";
/**
* @typedef {object} PromptMethods
@@ -19,8 +19,13 @@ import { mustBe, mustBeString } from "../utils/mustbe.js";
* - onColon(...)
*/
export class Prompt {
/** @protected @readonly @constant @type {Scene} */
scene;
/** @private @readonly @type {Scene} */
_scene;
/** @type {Scene} */
get scene() {
return this._scene;
}
//
// Extra info about the prompt we send to the client.
@@ -58,7 +63,7 @@ export class Prompt {
if (!(scene instanceof Scene)) {
throw new Error("Expected an instance of >>Scene<< but got " + typeof scene);
}
this.scene = scene;
this._scene = scene;
}
/**
@@ -92,24 +97,42 @@ export class Prompt {
* Triggered when a user types a :command that begins with a colon
*
* @param {string} command
* @param {string} argLine
* @param {any[]} args
*/
onColon(command, argLine) {
const methodName = "onColon_" + command;
const method = this[methodName];
if (typeof method === "function") {
method.call(this, argLine);
onColon(command, args) {
const methodName = "onColon__" + command;
const property = this[methodName];
//
// Default: we have no handler for the Foo command,
// So let's see if daddy can handle it.
if (property === undefined) {
return this.scene.onColon(command, args);
}
//
// If the prompt has a method called onColon_foo() =>
if (typeof property === "function") {
property.call(this, args);
return;
}
//
// For static "denial of commands" such as :inv ==> "you cannot access your inventory right now"
if (typeof method === "string") {
this.sendText(method);
// If the prompt has a _string_ called onColon_foo =>
if (typeof property === "string") {
this.sendText(property);
return;
}
// :inv ==> you cannot INV right now
this.sendError(`You cannot ${command.toUpperCase()} right now`);
//
// We found a property that has the right name but the wrong type.
throw new Error(
[
`Logic error. Prompt has a handler for a command called ${command}`,
`but it is neither a function or a string, but a ${typeof property}`,
].join(" "),
);
}
/**
@@ -167,7 +190,7 @@ export class Prompt {
}
/**
* @param {string} systemMessageType
* @param {string} systemMessageType The subtype of the system message (dev, salt, username, etc.)
* @param {any?} value
*/
sendSystemMessage(...args) {
@@ -184,13 +207,6 @@ export class Prompt {
//
// Easter ægg
onColon_pull_out_wand = "You cannot pull out your wand right now! But thanks for trying 😘🍌🍆";
//
// Easter ægg2
onColon_imperial(argLine) {
const n = Number(argLine);
this.sendText(`${n} centimeters is only ${n / 2.54} inches. This is why americans have such small wands`);
}
// Example of having a string as a colon-handler
onColon__pull_out_wand = "You cannot pull out your wand right now! But thanks for trying 😘🍌🍆";
}

View File

@@ -1,11 +1,16 @@
import { Session } from "../models/session.js";
import { Prompt } from "./prompt.js";
const MsgContext.ERROR_INSANE_PASSWORD = "Invalid password.";
const MsgContext.ERROR_INCORRECT_PASSWOD = "Incorrect password.";
/**
* Scene - a class for showing one or more prompts in a row.
*
* Scenes are mostly there to keep track of which prompt to show,
* and to store data for subsequent prompts to access.
*
* The prompts themselves are responsible for data validation and
* interpretation.
*
* @abstract
* @method onReady
*/
export class Scene {
/**
@@ -31,8 +36,7 @@ export class Scene {
return this._prompt;
}
constructor() {
}
constructor() {}
/** @param {Session} session */
execute(session) {
@@ -51,8 +55,73 @@ export class Scene {
/**
* @param {Prompt} prompt
*/
doPrompt(prompt) {
showPrompt(prompt) {
this._prompt = prompt;
prompt.execute();
}
/** @param {new (scene: Scene) => Prompt} promptClassReference */
show(promptClassReference) {
this.showPrompt(new promptClassReference(this));
}
/**
* Triggered when a user types a :command that begins with a colon
* and the current Prompt cannot handle that command.
*
* @param {string} command
* @param {any[]} args
*/
onColon(command, args) {
const propertyName = "onColon__" + command;
const property = this[propertyName];
//
// Default: we have no handler for the Foo command
if (property === undefined) {
this.session.sendError(`You cannot ${command.toUpperCase()} right now`, { verbatim: true }); // :foo ==> you cannot FOO right now
return;
}
//
// If the prompt has a method called onColon_foo() =>
if (typeof property === "function") {
property.call(this, args);
return;
}
//
// If the prompt has a _string_ called onColon_foo =>
if (typeof property === "string") {
this.session.sendText(property);
return;
}
//
// We found a property that has the right name but the wrong type.
throw new Error(
[
`Logic error. Scene has a handler for a command called ${command}`,
`but it is neither a function or a string, but a ${typeof property}`,
].join(" "),
);
}
//
// Easter ægg
// Example dynamic colon handler
/** @param {any[]} args */
onColon__imperial(args) {
if (args.length === 0) {
this.session.sendText("The imperial system is the freeest system ever. Also the least good");
}
const n = Number(args[0]);
this.session.sendText(
sprintf("%.2f centimeters is only %.2f inches. This is american wands are so short!", n, n / 2.54),
);
}
onColon__hi = "Hoe";
}

View File

@@ -2,12 +2,12 @@ import WebSocket, { WebSocketServer } from "ws";
import http from "http";
import path from "path";
import fs from "fs";
import * as msg from "./utils/messages.js";
import { Session } from "./models/session.js";
import { GameSeeder } from "./seeders/gameSeeder.js";
import { Config } from "./config.js";
import { gGame } from "./models/globals.js";
import { AuthenticationScene } from "./scenes/authentication/authenticationScene.js";
import { MessageType, WebsocketMessage, formatMessage } from "./utils/messages.js";
// __ __ _ _ ____ ____
// | \/ | | | | _ \ / ___| ___ _ ____ _____ _ __
@@ -32,7 +32,7 @@ class MudServer {
console.info("New connection established");
const session = new Session(websocket, gGame);
if (Config.dev) {
websocket.send(msg.prepareToSend(msg.SYSTEM, "dev", true));
websocket.send(formatMessage(MessageType.SYSTEM, "dev", true));
}
// ____ _ ___ ____ _____
@@ -56,7 +56,7 @@ class MudServer {
this.onMessage(session, data);
} catch (error) {
console.error(error, data.toString(), data);
websocket.send(msg.prepareToSend(msg.CALAMITY, error));
websocket.send(formatMessage(MessageType.CALAMITY, error));
session.close();
}
});
@@ -159,7 +159,7 @@ class MudServer {
return;
}
const msgObj = new msg.WebsocketMessage(data.toString());
const msgObj = new WebsocketMessage(data.toString());
//
// Handle replies to prompts. The main workhorse of the game.
@@ -174,7 +174,7 @@ class MudServer {
}
//
// Handle QUIT messages. When the player types :quit
// Handle MessageType.QUIT messages. When the player types :quit
if (msgObj.isQuit()) {
session.scene.onQuit();
session.close(0, "Closing the socket, graceful goodbye!");
@@ -184,7 +184,7 @@ class MudServer {
//
// Handle any text that starts with ":" that isn't :help or :quit
if (msgObj.isColon()) {
return session.scene.prompt.onColon(msgObj.command, msgObj.argLine);
return session.scene.prompt.onColon(msgObj.command, msgObj.args);
}
//

View File

@@ -1,19 +1,6 @@
import { mustBe, mustBeInteger, mustBeString, mustMatch } from "./mustbe.js";
import { mustBe, mustBeString, mustMatch } from "./mustbe.js";
const colonCommandRegex = /^:([a-z0-9_]+)(:?\s*(.*))?$/;
/**
* Enum-like object holding placeholder tokens.
*
* @readonly
* @enum {string}
*/
export const MsgContext = Object.freeze({
PASSWORD: ":password",
USERNAME: ":username",
});
export const MsgTtype = Object.freeze({
export const MessageType = Object.freeze({
/**
* Very bad logic error. Player must quit game, refresh page, and log in again.
*
@@ -28,7 +15,7 @@ export const MsgTtype = Object.freeze({
*
* Server-->Client-->Player
*/
MsgContext.ERROR: "E",
ERROR: "E",
/**
* Message to be displayed.
@@ -42,7 +29,7 @@ export const MsgTtype = Object.freeze({
*
* Player-->Client-->Server
*/
MsgContext.REPLY: "R",
REPLY: "R",
/**
* Player wants to quit.
@@ -93,7 +80,7 @@ export const MsgTtype = Object.freeze({
* Represents a message sent to/from client
*
* @property {string?} command
* @property {string?} argLine
* @property {any[]} args
*/
export class WebsocketMessage {
/** @protected @type {any[]} _arr The array that contains the message data */
@@ -136,22 +123,22 @@ export class WebsocketMessage {
this.type = mustBeString(data[0]);
switch (this.type) {
case MsgContext.REPLY: // player ==> client ==> server
case MessageType.REPLY: // player ==> client ==> server
this.text = mustBeString(data[1]);
break;
case HELP: // player ==> client ==> server
case MessageType.HELP: // player ==> client ==> server
this.text = data[1] === undefined ? "" : mustBeString(data[1]).trim();
break;
case COLON: // player ==> client ==> server
case MessageType.COLON: // player ==> client ==> server
this.command = mustMatch(data[1], /^[a-z0-9_]+$/);
this.argLine = data[2]; // parse??
this.args = mustBe(data[2], "any[]");
break;
case DEBUG: // server ==> client
case MsgContext.ERROR: // server ==> client ==> player
case QUIT: // player ==> client ==> server
case SYSTEM: // client <==> server
case PROMPT: // server ==> client ==> player
case TEXT: // server ==> client ==> player
case MessageType.DEBUG: // server ==> client
case MessageType.ERROR: // server ==> client ==> player
case MessageType.QUIT: // player ==> client ==> server
case MessageType.SYSTEM: // client <==> server
case MessageType.PROMPT: // server ==> client ==> player
case MessageType.TEXT: // server ==> client ==> player
break;
default:
throw new Error(`Unknown message type: >>${typeof this.type}<<`);
@@ -159,27 +146,27 @@ export class WebsocketMessage {
}
isQuit() {
return this.type === QUIT;
return this.type === MessageType.QUIT;
}
isHelp() {
return this.type === HELP;
return this.type === MessageType.HELP;
}
isColon() {
return this.type === COLON;
return this.type === MessageType.COLON;
}
isReply() {
return this.type === MsgContext.REPLY;
return this.type === MessageType.REPLY;
}
isSysMessage() {
return this.type === SYSTEM;
return this.type === MessageType.SYSTEM;
}
isDebug() {
return this.type === DEBUG;
return this.type === MessageType.DEBUG;
}
}
@@ -189,6 +176,6 @@ export class WebsocketMessage {
* @param {string} messageType
* @param {...any} args
*/
export function prepareToSend(messageType, ...args) {
export function formatMessage(messageType, ...args) {
return JSON.stringify([messageType, ...args]);
}

View File

@@ -12,8 +12,14 @@ export function mustBe(value, ...types) {
return value;
}
const isArray = Array.isArray(value);
if (isArray && (types.includes("any[]") || types.includes("array"))) {
return value;
}
// NOTE: only checks first element of array if it's a string.
if (types.includes("strings[]") && Array.isArray(value) && (value.length === 0 || typeof value[0] === "string")) {
if (isArray && types.includes("strings[]") && (value.length === 0 || typeof value[0] === "string")) {
return value;
}
@@ -24,12 +30,6 @@ export function mustBeString(value) {
return mustBe(value, "string");
}
export function mustBeInteger(value) {
if (typeof value === "number" && Number.isSafeInteger(value)) {
return value;
}
}
/**
*
* @param {string} str

View File

@@ -222,7 +222,7 @@ export function frameText(text, options) {
let output = "";
//
// GENERATE THE MARGIN SPACE ABOVE THE FRAMED TEXT
// GENERATE THE MARGIN SPACE ABOVE THE FRAMED MsgType.TEXT
//
// ( we insert space characters even though )
// ( they wouldn't normally be visible. But )
@@ -263,7 +263,7 @@ export function frameText(text, options) {
).repeat(options.vPadding);
//
// GENERATE FRAMED TEXT SEGMENT
// GENERATE FRAMED MsgType.TEXT SEGMENT
//
// ║ My pretty ║
// ║ text here ║
@@ -318,7 +318,7 @@ export function frameText(text, options) {
"\n";
//
// GENERATE THE MARGIN SPACE BELOW THE FRAMED TEXT
// GENERATE THE MARGIN SPACE BELOW THE FRAMED MsgType.TEXT
//
// ( we insert space characters even though )
// ( they wouldn't normally be visible. But )