DEV Community

Cover image for Building A Modern MUSH With Typescript Part 4: Flags, Files, and clients - Oh my!
Lem Canady
Lem Canady

Posted on • Edited on

Building A Modern MUSH With Typescript Part 4: Flags, Files, and clients - Oh my!

Hello! If you've been following along? Thanks for coming back! Else Welcome to the series! in Part 3, We set up our command handling code - but it was missing the implementation of flags.ts Well today we'll cover that! As well as handling text files! We'll be ready to actually fire this thing up soon!

Creating src/api/flags.ts

The flags system enables permissions, behavior, or settings depending on if a flag is set or not. Note that I've referenced the database here which hasn't been set up yet - but I'll give you a hint at what we're covering soon! :)

import { game } from "../config/config.json";
import db, { DBObj } from "./database";

export interface Flag {
  name: string;
  code: string;
  lvl: number;
}

The actual Flags class is another singleton, because we only want one instance of the class running at a time.

export class Flags {
  private flags: Flag[];
  private static instance: Flags;
  private constructor() {
    this.flags = game.flags;
  }

  /**
   * Add a new flag to the system.
   * @param flg the Flag object to add
   */
  addFlag(flg: Flag) {
    this.flags.push(flg);
  }

  /**
   * Check to see if a flag exists.
   * @param flg The name of the flag to check
   */
  isFlag(flg: string) {
    return this.flags.map(flag => flag.name).indexOf(flg) ? true : false;
  }

  /**
   * Check to see if the given DBObj has the listed flags.
   * @param tar The target DBObj
   * @param flgs The list of flags to check against.
   */
  hasFlags(tar: DBObj, flgs: string) {
    return flgs
      .split(" ")
      .map(flag => (tar.flags.indexOf(flag) ? true : false))
      .indexOf(false)
      ? false
      : true;
  }

  /**
   * Add a flag to a DBObj.
   * @param tar The target DBObj
   * @param flg The flag to be added.
   */
  async setFlag(tar: DBObj, flg: string) {
    const flagSet = new Set(tar.flags);
    if (this.isFlag(flg)) {
      flagSet.add(flg);
      tar.flags = Array.from(flagSet);
      return await db.update({ _id: tar._id }, tar);
    }
    return false;
  }

  /**
   * Get the full name of a flag from a fragment.
   * Returns the first result.
   * @param flg The flag to get the name of
   */
  flagName(flg: string) {
    return this.flags
      .filter(flag => flag.name.match(new RegExp(flg, "i")))
      .map(flag => flag.name)[0];
  }

  /**
   * Remove a flag from a DBObj
   * @param tar The DBObj to remove the flag from
   * @param flag The flag to remove.
   */
  async remFlag(tar: DBObj, flag: string) {
    tar.flags = tar.flags.filter(flag => flag !== this.flagName(flag));
    return await db.update({ _id: tar._id }, tar);
  }

  /**
   * Find a character bit level (permission level).  
   * The higher the level, the more engine permissions.
   * @param tar The Target DBObj to compare.
   */
  private _bitLvl(tar: DBObj) {
    return this.flags
      .filter(flag => tar.flags.indexOf(flag.name))
      .map(flag => flag.lvl)
      .reduce((prev: number, curr: number) => (prev > curr ? prev : curr), 0);
  }

  /**
   * Check to see if the enactor has the permission level to modify
   * the target
   * @param en The enacting DBObj
   * @param tar The targeted DBObj
   */
  canEdit(en: DBObj, tar: DBObj) {
    return this._bitLvl(en) >= this._bitLvl(tar) ? true : false;
  }

  static getInstance() {
    if (!Flags.instance) Flags.instance = new Flags();
    return Flags.instance;
  }
}

export default flags.getInstance();

I didn't do a whole lot of intermittent commenting on this one - the file comments pretty much sum this one up. We're checking for, setting and removing flags with a couple of private helper functions. Please let me know if I need to break this code down further in the comments!

Creating src/api/text.ts

This is the code responsible for loading text files into memory for quick reference later during the lifecycle of the program. text.ts another singleton (There are quite a few in this project, whew!), instantiated only once.

import { readdirSync, readFileSync } from "fs";
import { resolve } from "path";

export interface FileInfo {
  name: string;
  text: string;
  category: string;
}

class TextFiles {
  private static instance: TextFiles
  private _index: FileInfo[];
  private constructor() {
    this._index = [];
  }

  /**
   * Load text files from a directory.
   * @param path The path to where the files are found.
   * @param category The base category for the files to load
   */
  load(path: string, category: string = "general") {
    const dir = readdirSync(resolve(__dirname, path), {
      encoding: "utf8",
      withFileTypes: true
    });

    // load files.
    dir.forEach(dirent => {
      if (dirent.isFile() && dirent.name.toLowerCase().endsWith(".md")) {
        const name = dirent.name?.split(".")[0].toLowerCase();
        const text = readFileSync(resolve(__dirname, path, dirent.name), {
          encoding: "utf8"
        });
        return this._index.push({ name, text, category });
      } else if (dirent.isDirectory()) {
        this.load(resolve(__dirname, path, dirent.name), dirent.name);
      }
    });
  }

  /**
   * Grab the contents of a stored text file.
   * @param name The name of the file to grab (without the extension)
   * @param category The file's category
   */
  get(name: string, category = "general") {
    const results = this._index.find(
      file =>
        file.name.toLowerCase() === name.toLowerCase() &&
        file.category.toLowerCase() === category.toLowerCase()
    );

    if (results) {
      return results.text;
    } else {
      return "";
    }
  }

  static getInstance(){
    if(!TextFiles.instance) TextFiles.instance = new TextFiles();
    return TextFiles.instance;
  }

}

export default TextFiles.getInstance();

Creating a Connect screen! (text/connect.md)

We'll have to modify our project structure just a little. From the root of your project:

mkdir text

Then we can define our simple connect screen. I plan on taking full advantage of markdown's ability to define HTML structure, like images! When the client is made, it should load our graphic!

![Alt Text!](images/ursamu_github_banner.png)

**Welcome To UrsaMU!**

This is an example server for the [UrsaMU](repo/not/ready) server.

To Create a new character, use `create <name> <password>`<br/>
To Connect to an existing character, use `connect <name> <password>`

Okay! I think that's enough for this post. In our next installment, we will work on making our database adapter, and also finally get the client made and boot this thing up!

Thanks for stopping in to read! Feel free to follow to keep up to date with my releases - I'm planning on an article every few days until the barebones are ready!

Top comments (0)