In Part 2 We set up socket.io on the server-side and created our basic input parsing middleware engine. In today's installment, we're going to create the command handling middleware and a couple of commands!
Before we begin we'll have to update our project structure just a bit. From your project root:
mkdir src/middleware
mkdir src/commands
Defining src/api/commands.ts
, our In-Game Command Structure
The first thing we'll want to do is define the methods for handling the internals of a command. Note: We haven't defined flags yet, or utils we'll get to them soon!
import { types } from "util";
import { loadDir } from "./utils";
import mu from "./mu";
import { Marked } from "@ts-stack/markdown";
type Exec = (id: string, args: string[]) => Promise<string>;
export class MuCommand {
private _pattern: RegExp | string;
flags: string;
name: string;
exec: Exec;
constructor({
name,
flags,
pattern,
exec
}: {
name: string;
flags?: string;
pattern: RegExp | string;
exec: Exec;
}) {
this.name = name;
this._pattern = pattern;
this.flags = flags || "";
this.exec = exec;
}
Because I want users to be able to define in-game commands with both wildcard matching and regular expressions, I made a getter and setter for MuCommands
. internally, the engine runs on regular expressions - so they need to be converted before being called by the command handling middleware.
/**
* Getter for the pattern. Always return a regex string.
*/
get pattern() {
return types.isRegExp(this._pattern)
? this._pattern
: this._globStringToRegex(this._pattern);
}
/**
* Set the pattern.
*/
set pattern(str: string | RegExp) {
this._pattern = str;
}
Here's where the actual conversion process lives. It basically escapes out all special characters, before converting wildcard characters *
and ?
into regular expressions.
/**
* Convert a wildcard(glob) string to a regular expression.
* @param str The string to convert to regex.
*/
private _globStringToRegex(str: string) {
return new RegExp(
this._preg_quote(str)
.replace(/\\\*/g, "(.*)")
.replace(/\\\?/g, "(.)"),
"gi"
);
}
/**
* Escape a string of characters to be Regex escaped.
* @param str The string to convert to a regex statement.
* @param delimiter The character to separate out words in
* the string.
*/
private _preg_quote(str: string, delimiter?: string) {
// http://kevin.vanzonneveld.net
// + original by: booeyOH
// + improved by: Ates Goral (http://magnetiq.com)
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + bugfixed by: Onno Marsman
// + improved by: Brett Zamir (http://brett-zamir.me)
// * example 1: preg_quote("$40");
// * returns 1: '\$40'
// * example 2: preg_quote("*RRRING* Hello?");
// * returns 2: '\*RRRING\* Hello\?'
// * example 3: preg_quote("\\.+*?[^]$(){}=!<>|:");
// * returns 3: '\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:'
return (str + "").replace(
new RegExp(
"[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\" + (delimiter || "") + "-]",
"g"
),
"\\$&"
);
}
}
Now we can define the command management system! This is going to be another singleton, as we only want it to be instantiated once.
export class Commands {
cmds: MuCommand[];
private static instance: Commands;
private constructor() {
this.cmds = [];
this.init();
}
When the class initializes it attempts to load all of the files
from the given directory.
/**
* initialize the object.
*/
init() {
loadDir("../commands/", (name: string) =>
console.log(`Module loaded: ${name}`)
);
}
/**
* Add a new command to the system.
* @param command The command object to be added to the system
*/
add({
name,
flags,
pattern,
exec
}: {
name: string;
flags?: string;
pattern: RegExp | string;
exec: Exec;
}) {
const command = new MuCommand({ name, flags, pattern, exec });
this.cmds.push(command);
}
match
is a little intimidating at first, but I'm basically chaining array functions together. First I map through the commands and test against the command's pattern. If a match is found it returns an object, else it returns false. Then I filter through that map to take out the false entries. Finally, I return the first match, just in case there's more than one.
/**
* Match a string to a command pattern.
* @param str The string to match the command against.
*/
match(str: string) {
return this.cmds
.map(cmd => {
const matched = str.match(cmd.pattern);
if (matched) {
return {
args: matched,
exec: cmd.exec,
flags: cmd.flags
};
} else {
return;
}
})
.filter(Boolean)[0];
}
The force
method allows us to skip the command matching and fire a command directly from the command handler.
async force(id: string, name: string, args: string[] = []) {
const response = {
id,
payload: {
command: name,
message: await this.cmds
.filter(cmd =>
cmd.name.toLowerCase() === name.toLowerCase()
)[0]
.exec(id, args)
}
};
if (response.payload.message)
response.payload.message = Marked.parse(response.payload.message);
mu.io?.to(id).send(response.payload);
}
static getInstance() {
if (!Commands.instance) Commands.instance = new Commands();
return Commands.instance;
}
}
export default Commands.getInstance();
An example command src/commands/test.ts
:
import cmds from '../api/commands'
export default () => {
cmds.add({
name: "Test",
pattern: /^[+@]?test$/g,
exec: async (id: string, args: any[]) => "Made it!!"; });
}
Creating src/middleware/commands.middleware.ts
Now we can edit our command middleware. Again, flags
aren't defined yet, but we'll get to that soon!
import { MiddlewareNext, MuRequest } from "../api/parser";
import cmds from "../api/commands";
import flags from "../api/flags";
import mu from "../api/mu";
Since the middleware is just a function, we'll export default
the module with the required arguments.
export default async (req: MuRequest, next: MiddlewareNext) => {
const id = req.socket.id;
const message = req.payload.message || "";
let matched = cmds.match(message);
let flgs: Boolean;
Here I made a helper function expression to encapsulate the logic of matching flags (coming soon!) without having to make my main logic too spammy. This basically just checks a few conditions to make sure the enactor has the right permissions to use the command.
const _hasFlags = () => {
if (matched && mu.connMap.has(id)) {
const char = mu.connMap.get(id);
return flags.hasFlags(char!, matched.flags);
} else {
return false;
}
};
if (matched && (!matched.flags || _hasFlags())) {
// Matching command found!
// run the command and await results
const results = await matched
.exec(id, matched.args)
.catch((err: Error) => next(err, req));
req.payload.matched = matched ? true : false;
req.payload.message = results;
return next(null, req);
} else if (!mu.connMap.has(id)) {
req.payload.matched = matched ? true : false;
req.payload.message = "";
return next(null, req);
}
return next(null, req);
};
And with that, our command handling code is done! In our next installment, we'll try to cover flags, text files, and building a simple client so we can check out our work so far!
Thanks for stopping in for the read! Feel free to follow, leave a comment, or discuss!
Top comments (0)