Building A Modern MUSH With Typescript Part 4: Flags, Files, and clients - Oh my!
Tags: typescript, tutorial, node, mush
Published: 2020-03-16 18:51:52

Hello! If you've been following along? Thanks for coming back! Else Welcome to the series! in [Part 3](https://dev.to/lcanady/writing-a-modern-mush-server-in-typescript-part-3-13cd), 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! :)
```JavaScript
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.
```JavaScript
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.
```JavaScript
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!