stuffy
This commit is contained in:
0
server/utils/dice.js
Executable file → Normal file
0
server/utils/dice.js
Executable file → Normal file
0
server/utils/id.js
Executable file → Normal file
0
server/utils/id.js
Executable file → Normal file
@@ -1,201 +1,186 @@
|
||||
import { mustBe, mustBeInteger, mustBeString, mustMatch } from "./mustbe.js";
|
||||
|
||||
const colonCommandRegex = /^:([a-z0-9_]+)(:?\s*(.*))?$/;
|
||||
|
||||
/**
|
||||
* Very bad logic error. Player must quit game, refresh page, and log in again.
|
||||
* Enum-like object holding placeholder tokens.
|
||||
*
|
||||
* Client-->Server
|
||||
* or
|
||||
* Server-->Client-->Plater
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export const CALAMITY = "calamity";
|
||||
export const MsgContext = Object.freeze({
|
||||
PASSWORD: ":password",
|
||||
USERNAME: ":username",
|
||||
});
|
||||
|
||||
export const MsgTtype = Object.freeze({
|
||||
/**
|
||||
* Very bad logic error. Player must quit game, refresh page, and log in again.
|
||||
*
|
||||
* Client-->Server
|
||||
* or
|
||||
* Server-->Client-->Plater
|
||||
*/
|
||||
CALAMITY: "CALAMITY",
|
||||
|
||||
/**
|
||||
* Tell recipient that an error has occurred
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
*/
|
||||
MsgContext.ERROR: "E",
|
||||
|
||||
/**
|
||||
* Message to be displayed.
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
*/
|
||||
TEXT: "T",
|
||||
|
||||
/**
|
||||
* Player has entered data, and sends it to server.
|
||||
*
|
||||
* Player-->Client-->Server
|
||||
*/
|
||||
MsgContext.REPLY: "R",
|
||||
|
||||
/**
|
||||
* Player wants to quit.
|
||||
*
|
||||
* Player-->Client-->Server
|
||||
*/
|
||||
QUIT: "QUIT",
|
||||
|
||||
/**
|
||||
* Player wants help
|
||||
*
|
||||
* Player-->Client-->Server
|
||||
*/
|
||||
HELP: "HELP",
|
||||
|
||||
/**
|
||||
* Server tells the client to prompt the player for some data
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
*/
|
||||
PROMPT: "P",
|
||||
|
||||
/**
|
||||
* Server tells the client to prompt the player for some data
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
*/
|
||||
SYSTEM: "_",
|
||||
|
||||
/**
|
||||
* Debug message, to be completely ignored in production
|
||||
*
|
||||
* Client-->Server
|
||||
* or
|
||||
* Server-->Client-->Plater
|
||||
*/
|
||||
DEBUG: "dbg",
|
||||
|
||||
/**
|
||||
* Player sent colon-prefixed, an out-of-order, command
|
||||
*
|
||||
* Player-->Client-->Server
|
||||
*/
|
||||
COLON: ":",
|
||||
});
|
||||
|
||||
/**
|
||||
* Tell recipient that an error has occurred
|
||||
* Represents a message sent to/from client
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
* @property {string?} command
|
||||
* @property {string?} argLine
|
||||
*/
|
||||
export const ERROR = "e";
|
||||
export class WebsocketMessage {
|
||||
/** @protected @type {any[]} _arr The array that contains the message data */
|
||||
_data;
|
||||
|
||||
/**
|
||||
* Message to be displayed.
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
*/
|
||||
export const MESSAGE = "m";
|
||||
/** @constant @readonly @type {string} _arr The array that contains the message data */
|
||||
type;
|
||||
|
||||
/**
|
||||
* Player has entered data, and sends it to server.
|
||||
*
|
||||
* Player-->Client-->Server
|
||||
*/
|
||||
export const REPLY = "reply";
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Player wants to quit.
|
||||
*
|
||||
* Player-->Client-->Server
|
||||
*/
|
||||
export const QUIT = "quit";
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(msgData);
|
||||
} catch (_) {
|
||||
throw new Error(
|
||||
`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Player wants help
|
||||
*
|
||||
* Player-->Client-->Server
|
||||
*/
|
||||
export const HELP = "help";
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error(`Could not create client message. Excpected an array, but got a ${typeof this._data}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server tells the client to prompt the player for some data
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
*/
|
||||
export const PROMPT = "prompt";
|
||||
if (data.length < 1) {
|
||||
throw new Error(
|
||||
"Could not create client message. Excpected an array with at least 1 element, but got an empty one",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Player has entered a command, and wants to do something.
|
||||
*
|
||||
* Player-->Client-->Server
|
||||
*/
|
||||
export const COMMAND = "c";
|
||||
this.type = mustBeString(data[0]);
|
||||
|
||||
/**
|
||||
* Server tells the client to prompt the player for some data
|
||||
*
|
||||
* Server-->Client-->Player
|
||||
*/
|
||||
export const SYSTEM = "_";
|
||||
|
||||
/**
|
||||
* Debug message, to be completely ignored in production
|
||||
*
|
||||
* Client-->Server
|
||||
* or
|
||||
* Server-->Client-->Plater
|
||||
*/
|
||||
export const DEBUG = "dbg";
|
||||
|
||||
/**
|
||||
* Represents a message sent from client to server.
|
||||
*/
|
||||
export class ClientMessage {
|
||||
/**
|
||||
* @protected
|
||||
* @type {any[]} _arr The array that contains the message data
|
||||
*/
|
||||
_attr;
|
||||
|
||||
/** The message type.
|
||||
*
|
||||
* One of the * constants from this document.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get type() {
|
||||
return this._attr[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;
|
||||
switch (this.type) {
|
||||
case MsgContext.REPLY: // player ==> client ==> server
|
||||
this.text = mustBeString(data[1]);
|
||||
break;
|
||||
case HELP: // player ==> client ==> server
|
||||
this.text = data[1] === undefined ? "" : mustBeString(data[1]).trim();
|
||||
break;
|
||||
case COLON: // player ==> client ==> server
|
||||
this.command = mustMatch(data[1], /^[a-z0-9_]+$/);
|
||||
this.argLine = data[2]; // parse??
|
||||
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
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown message type: >>${typeof this.type}<<`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this._attr = JSON.parse(msgData);
|
||||
} catch (_) {
|
||||
throw new Error(
|
||||
`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`,
|
||||
);
|
||||
isQuit() {
|
||||
return this.type === QUIT;
|
||||
}
|
||||
|
||||
if (!Array.isArray(this._attr)) {
|
||||
throw new Error(
|
||||
`Could not create client message. Excpected an array, but got a ${typeof this._attr}`,
|
||||
);
|
||||
isHelp() {
|
||||
return this.type === HELP;
|
||||
}
|
||||
|
||||
if (this._attr.length < 1) {
|
||||
throw new Error(
|
||||
"Could not create client message. Excpected an array with at least 1 element, but got an empty one",
|
||||
);
|
||||
}
|
||||
}
|
||||
hasCommand() {
|
||||
return this._attr.length > 1 && this._attr[0] === COMMAND;
|
||||
}
|
||||
|
||||
/** Does this message contain a username-response from the client? */
|
||||
isUsernameResponse() {
|
||||
return (
|
||||
this._attr.length === 4 &&
|
||||
this._attr[0] === REPLY &&
|
||||
this._attr[1] === "username" &&
|
||||
typeof this._attr[2] === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/** Does this message contain a password-response from the client? */
|
||||
isPasswordResponse() {
|
||||
return (
|
||||
this._attr.length === 4 &&
|
||||
this._attr[0] === REPLY &&
|
||||
this._attr[1] === "password" &&
|
||||
typeof this._attr[2] === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/** @returns {boolean} does this message indicate the player wants to quit */
|
||||
isQuitCommand() {
|
||||
return this._attr[0] === QUIT;
|
||||
}
|
||||
|
||||
isHelpCommand() {
|
||||
return this._attr[0] === HELP;
|
||||
}
|
||||
|
||||
/** @returns {boolean} is this a debug message? */
|
||||
isDebug() {
|
||||
return this._attr.length === 2 && this._attr[0] === DEBUG;
|
||||
}
|
||||
|
||||
isIntegerResponse() {
|
||||
return (
|
||||
this._attr.length === 4 &&
|
||||
this._attr[0] === REPLY &&
|
||||
this._attr[1] === "integer" &&
|
||||
(typeof this._attr[2] === "string" ||
|
||||
typeof this._attr[2] === "number") &&
|
||||
Number.isInteger(Number(this._attr[2]))
|
||||
);
|
||||
}
|
||||
|
||||
/** @returns {number} integer */
|
||||
get integer() {
|
||||
if (!this.isIntegerResponse()) {
|
||||
return undefined;
|
||||
isColon() {
|
||||
return this.type === COLON;
|
||||
}
|
||||
|
||||
return Number.parseInt(this._attr[2]);
|
||||
}
|
||||
isReply() {
|
||||
return this.type === MsgContext.REPLY;
|
||||
}
|
||||
|
||||
/** @returns {string|false} Get the username stored in this message */
|
||||
get username() {
|
||||
return this.isUsernameResponse() ? this._attr[2] : false;
|
||||
}
|
||||
isSysMessage() {
|
||||
return this.type === SYSTEM;
|
||||
}
|
||||
|
||||
/** @returns {string|false} Get the password stored in this message */
|
||||
get password() {
|
||||
return this.isPasswordResponse() ? this._attr[2] : false;
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
get command() {
|
||||
return this.hasCommand() ? this._attr[1] : false;
|
||||
}
|
||||
isDebug() {
|
||||
return this.type === DEBUG;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,6 +189,6 @@ export class ClientMessage {
|
||||
* @param {string} messageType
|
||||
* @param {...any} args
|
||||
*/
|
||||
export function prepare(messageType, ...args) {
|
||||
return JSON.stringify([messageType, ...args]);
|
||||
export function prepareToSend(messageType, ...args) {
|
||||
return JSON.stringify([messageType, ...args]);
|
||||
}
|
||||
|
||||
44
server/utils/mustbe.js
Executable file
44
server/utils/mustbe.js
Executable file
@@ -0,0 +1,44 @@
|
||||
export function mustBe(value, ...types) {
|
||||
//
|
||||
// empty type enforcement.
|
||||
// Means we just want value to be define
|
||||
if (types.length === 0 && typeof value !== "undefined") {
|
||||
return value;
|
||||
}
|
||||
|
||||
//
|
||||
// value has a valid type
|
||||
if (types.includes(typeof value)) {
|
||||
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")) {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error("Invalid data type. Expected >>" + types.join(" or ") + "<< but got " + typeof value);
|
||||
}
|
||||
|
||||
export function mustBeString(value) {
|
||||
return mustBe(value, "string");
|
||||
}
|
||||
|
||||
export function mustBeInteger(value) {
|
||||
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {RegExp} regex
|
||||
*/
|
||||
export function mustMatch(str, regex) {
|
||||
if (!regex.test(str)) {
|
||||
throw new Error(`String did not satisfy ${regex}`);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
83
server/utils/parseArgs.js
Normal file
83
server/utils/parseArgs.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { mustBeString } from "./mustbe.js";
|
||||
|
||||
/**
|
||||
* Parse a command string into arguments. For use with colon-commands.
|
||||
*
|
||||
* @param {string} cmdString;
|
||||
* @returns {(string|number)[]} Command arguments
|
||||
*/
|
||||
export function parseArgs(cmdString) {
|
||||
mustBeString(cmdString);
|
||||
const args = [];
|
||||
const quoteChars = ["'", '"', "`"];
|
||||
const backslash = "\\";
|
||||
|
||||
let currentArg = ""; // The arg we are currently constructing
|
||||
let inQuotes = false; // are we inside quotes of some kind?
|
||||
let currentQuoteChar = ""; // if were in quotes, which are they?
|
||||
|
||||
const push = (value) => {
|
||||
const n = Number(value);
|
||||
if (Number.isSafeInteger(n)) {
|
||||
args.push(n);
|
||||
} else if (Number.isFinite(n)) {
|
||||
args.push(n);
|
||||
} else {
|
||||
args.push(value);
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < cmdString.length; i++) {
|
||||
const char = cmdString[i];
|
||||
const nextChar = cmdString[i + 1];
|
||||
|
||||
if (!inQuotes) {
|
||||
// Not in quotes - look for quote start or whitespace
|
||||
if (quoteChars.includes(char)) {
|
||||
inQuotes = true;
|
||||
currentQuoteChar = char;
|
||||
} else if (char === " " || char === "\t") {
|
||||
// Whitespace - end current arg if it exists
|
||||
if (currentArg) {
|
||||
push(currentArg);
|
||||
currentArg = "";
|
||||
}
|
||||
// Skip multiple whitespace
|
||||
while (cmdString[i + 1] === " " || cmdString[i + 1] === "\t") i++;
|
||||
} else {
|
||||
currentArg += char;
|
||||
}
|
||||
} else {
|
||||
// Inside quotes
|
||||
if (char === currentQuoteChar) {
|
||||
// Found matching quote - end quoted section
|
||||
inQuotes = false;
|
||||
currentQuoteChar = "";
|
||||
} else if (char === backslash && (nextChar === currentQuoteChar || nextChar === backslash)) {
|
||||
// Escape sequence - add the escaped character
|
||||
currentArg += nextChar;
|
||||
//
|
||||
// Todo, maybe add support for \n newlines? Why would I ?
|
||||
//
|
||||
i++; // Skip next character
|
||||
} else {
|
||||
currentArg += char;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add final argument if exists
|
||||
if (currentArg) {
|
||||
push(currentArg);
|
||||
}
|
||||
|
||||
if (currentQuoteChar) {
|
||||
// We allow quotes to not be terminated
|
||||
// It allows players to do stuff like `:say "wolla my lovely friend` and not have the text modified or misinterpreted in any way
|
||||
// May be good for chat where you dont want every word split into individual arguments
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
console.log(parseArgs("\"k1m er '-9 ' `anus pikke`"));
|
||||
39
server/utils/random.js
Executable file → Normal file
39
server/utils/random.js
Executable file → Normal file
@@ -4,24 +4,44 @@
|
||||
*/
|
||||
export class Xorshift32 {
|
||||
/* @type {number} */
|
||||
initialSeed;
|
||||
|
||||
/**
|
||||
* State holds a single uint32.
|
||||
* It's useful for staying within modulo 2**32.
|
||||
*
|
||||
* @type {Uint32Array}
|
||||
*/
|
||||
state;
|
||||
|
||||
/** @param {number} seed */
|
||||
constructor(seed) {
|
||||
this.state = seed | 0;
|
||||
if (seed === undefined) {
|
||||
const maxInt32 = 2 ** 32;
|
||||
seed = Math.floor(Math.random() * (maxInt32 - 1)) + 1;
|
||||
}
|
||||
seed = seed | 0;
|
||||
console.info("RNG Initial Seed %d", seed);
|
||||
this.state = Uint32Array.of(seed);
|
||||
}
|
||||
|
||||
/** @protected Shuffle the internal state. */
|
||||
shuffle() {
|
||||
//
|
||||
// Run the actual xorshift32 algorithm
|
||||
let x = this.state;
|
||||
x ^= x << 13;
|
||||
x ^= x >>> 17;
|
||||
x ^= x << 5;
|
||||
x = (x >>> 0) / 4294967296;
|
||||
console.log("RNG Shuffle: Initial State: %d", this.state);
|
||||
this.state[0] ^= this.state[0] << 13;
|
||||
this.state[0] ^= this.state[0] >>> 17;
|
||||
this.state[0] ^= this.state[0] << 5;
|
||||
|
||||
this.state = x;
|
||||
// We could also do something like this:
|
||||
// x ^= x << 13;
|
||||
// x ^= x >> 17;
|
||||
// x ^= x << 5;
|
||||
// return x;
|
||||
// But we'd have to xor the x with 2^32 after every op,
|
||||
// we get that "for free" by using the uint32array
|
||||
|
||||
console.log("RNG Shuffle: Exit State: %d", this.state);
|
||||
return this.state[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,4 +97,3 @@ export class Xorshift32 {
|
||||
return num + greaterThanOrEqual;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
0
server/utils/security.js
Executable file → Normal file
0
server/utils/security.js
Executable file → Normal file
0
server/utils/tui.js
Executable file → Normal file
0
server/utils/tui.js
Executable file → Normal file
Reference in New Issue
Block a user