{"id":15391965,"url":"https://github.com/dhleong/rainboots","last_synced_at":"2025-08-21T17:32:58.286Z","repository":{"id":62434358,"uuid":"59686016","full_name":"dhleong/rainboots","owner":"dhleong","description":"A more elegant way to make MUD","archived":false,"fork":false,"pushed_at":"2019-07-19T12:37:32.000Z","size":159,"stargazers_count":3,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-12-18T18:42:58.169Z","etag":null,"topics":["mud","mud-server","telnet-server"],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dhleong.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-05-25T18:02:33.000Z","updated_at":"2024-11-28T01:05:59.000Z","dependencies_parsed_at":"2022-11-01T21:00:55.731Z","dependency_job_id":null,"html_url":"https://github.com/dhleong/rainboots","commit_stats":null,"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhleong%2Frainboots","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhleong%2Frainboots/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhleong%2Frainboots/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhleong%2Frainboots/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dhleong","download_url":"https://codeload.github.com/dhleong/rainboots/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230524156,"owners_count":18239516,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mud","mud-server","telnet-server"],"created_at":"2024-10-01T15:13:37.886Z","updated_at":"2024-12-20T02:22:12.328Z","avatar_url":"https://github.com/dhleong.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rainboots [![Clojars Project](https://img.shields.io/clojars/v/rainboots.svg?style=flat)](https://clojars.org/rainboots) [![Build Status](http://img.shields.io/travis/dhleong/rainboots.svg?style=flat)](https://travis-ci.org/dhleong/rainboots)\n\n*A more elegant way to make [MUD][1]*\n\n## What\n\nRainboots is super barebones, providing some important core\nfunctionality without getting in the way of making any sort of MUD\nexperience you can imagine. It aims to be flexible and extensible, so\nthat any number of features can be plugged in and shared, but is\nunopinionated about the ultimate experience. Almost every aspect---from\nlow-level input handling to user authentication---can be replaced and\nredesigned.\n\nRainboots doesn't even initially provide a room or navigation system,\nor even any basic commands---though it does provide some excellent\ntools for developing them!\n\nRainboots is implemented in Clojure for flexibility and rapid\ndevelopment.  New commands (or updated versions of existing commands!)\ncan easily be swapped in with a REPL on the fly, without needing to\nrestart the server.\n\nAt its core, Rainboots is a fancy telnet server, capable of sending and\nreceiving raw telnet signals, so if you want to make a telnet server\nfor whatever reason, Rainboots can be used for that, as well. Some (but\nnot all) telnet commands will be parsed into a nice, friendly Keyword,\nbut it will fall back to the integer value for any it doesn't\nknow---feel free to send a PR with any missing signals!\n\n## How\n\nA [sample][2] is included with Rainboots that shows\nsome basic usage, which we will describe in more detail here:\n\n### Starting the server\n\n```clojure\n;; you can refer specific functions if you like, but this is\n;;  the easiest way to get started:\n(ns your.awesome.game\n  (:require [rainboots\n             [command :refer :all]\n             [core :refer :all]]))\n\n(defn start-sample\n  []\n  (def svr\n    ;; Handlers and configuration are provided via keyword args.\n    ;; Rainboots provides some sane defaults, but there are a few\n    ;;  handlers you must provide yourself\n    (start-server\n      ;; Since rainboots doesn't provide its own auth mechanism,\n      ;;  you must install your own. The handler is just a function,\n      ;;  called with a client object and the user's input line (see below)\n      :on-auth on-auth\n      ;; The :on-connect handler is called as soon as a client connects,\n      ;;  with a client object as its arg. Sure, we could provide a default\n      ;;  for this, as well, but you'll want to customize the experience anyway\n      :on-connect on-connect)))\n```\n\nThat's it! Your basic MUD server is running. Before any commands can be\naccepted, however, you'll need to implement that `on-auth` handler.\n\n### Client Objects\n\nBefore we talk about auth, let's learn about client objects. A client object\nis simply a clojure map wrapped in an [atom][3].  There are a few \"reserved\"\nkeywords (enumerated below) which you should not overwrite (rainboots won't\nstop you; do so at your own peril!), and any keywords in the `rainboots.core`\nnamespace should probably also be left alone, but otherwise feel free to store\nany transient data in the client object/atom.\n\n\"Reserved\" keywords:\n\n- `:stream` holds the connection object, used by `(send!)`\n- `:ch` holds your character data, once auth'd\n- `:input-stack` is used by the default command handler for command sets\n- `:term-types` is a set of strings reported by the client\n\nOther keywords:\n\n- `:rainboots.core/closed?` is `true` if an only if the client is disconnected\n- `:rainboots.core/remote` is a map describing the remote connection (includes\n    keys like `:remote-user`)\n\nCharacter data management and formatting is totally up to you. As long as\nthere is *some* non-`nil` value stored in `:ch`, it will assume you are logged\nin.\n\n### Basic Auth\n\nNow that we know what a client object is (typically abbreviated `cli`\nin handlers), let's look at how to implement auth. Here is a minimal\nexample:\n\n```clojure\n(defn on-auth\n  [cli username]\n  (swap! cli assoc :ch username)\n  (send! cli \"Welcome back, \" username \"!\")\n```\n\nThis will be a very simple MUD, with no stats or attributes storable in\nthe user, but rainboots doesn't mind. As stated above, so long as `:ch`\nis non-`nil`, the user is \"logged in,\" and the `on-cmd` handler will be\ncalled (see below) instead of `on-auth`.\n\nA more interesting `on-auth` will probably want to use the client atom\nas temporary storage for checking credentials. Here's a more complete\nexample, where each character has their own username and password.\nPersistence is left to the user.\n\n```clojure\n(defn on-auth\n  [cli line]\n  (if-let [u (:user @cli)]\n    ;; they've already provided a username\n    (do\n      ;; probably load the character by username from a db or flat file,\n      ;;  then compare the (hashed) password\n      (if-let [ch (validate-and-load u line)]\n        (do\n          (swap! cli assoc :ch ch)\n          ;; note the {W escape sequence; this is for colors! (see below)\n          (send! cli \"Logged in as {W\" (-\u003e ch :name) \"{n\"))))\n        (do\n          (swap! cli dissoc :user)\n          (send! cli \"Invalid username/password combo\"))\n    ;; first input; store as a username and prompt for a password\n    (do\n      (swap! cli assoc :user line)\n      (send! cli \"Password:\"))))\n```\n\nBecause `on-auth` is plug-and-play, you can implement it however you\nlike. You could have a single login with multiple characters, or even\nsupport two-factor auth!  The sky is the limit.\n\n### Communicating with clients\n\nYou've seen some examples of the `send!` function up there, but it can do\na lot more than you might have thought. For example, instead of embedding\ncolors in the string with escape sequences, why not use a [hiccup][4]-inspired\nsyntax?\n\n```clojure\n(send! cli \"Logged in as \" [:W (-\u003e ch :name)])\n```\n\nAll the same colors are supported as keyword tags, but with hiccup they\nautomatically close themselves. They even support nesting:\n\n```clojure\n; these two lines are functionally identical:\n(send! cli \"{YCaptain {rMal{Y Reynolds{n\")\n(send! cli [:Y \"Captain \" [:r \"Mal\"] \" Reynolds\"])\n```\n\nNotice how with hiccup syntax, we didn't need to remember to re-set the yellow\ncolor after the inner red color block!\n\n#### Customizing Hiccup\n\nOf course, in addition to the colors built-in, you can provide your own\nhiccup handlers. A hiccup handler in Rainboots is just a function that\ntakes the client receiving the message and whatever arguments you passed\nto it. For example:\n\n```clojure\n(defn upper [cli text]\n  (str/upper-case text))\n\n(send! cli \"You yell, \" [upper \"Where'd you go?\"])\n```\n\nIf you'd like to declare your handler as a keyword, you can do that too:\n\n```clojure\n(rainboots.hiccup/defhandler :upper\n  [cli text]\n  (str/upper-case text))\n\n(send! cli \"You yell, \" [:upper \"Where'd you go?\"])\n```\n\nSince you're given the receiving client, you can do neat things like customize\nthe output depending on who's receiving it:\n\n```clojure\n(rainboots.hiccup/defhandler :name\n  [cli person]\n  (if (ch-knows? (:ch @cli) person)\n    (:name person)\n    \"Someone\"))\n\n(send! cli [:name sender] \" yells, \" [:upper \"I'm over here!\"])\n```\n\n\n### Command Handling\n\nRainboots comes with a pretty powerful `on-cmd` handler installed by\ndefault.  You should rarely need to replace the default `on-cmd`\nhandler, but you are more than welcome to. As with `on-auth`, the\n`on-cmd` handler is called with a client object, and a line of input.\nSince there are many ways to handle commands, and most of them annoying\nto manage, rainboots provides a convenient way to get started:\n\n```clojure\n(defcmd broadcast\n  \"Send a message to everybody online\"\n  [cli ^:rest text]\n  (send-all! (-\u003e @cli :ch :name) \" broadcasts: \" text))\n```\n\nThis demonstrates a couple things. First, the `(defcmd)` macro, which\ninstalls a command into the default set with a familiar syntax, and\n\"argument types.\" Any logged-in user will be able to type `broadcast\nHi!` to greet the whole mud. In fact, we automatically build out\nshortcuts, so with no other commands def'd, you could just type `b\nHi!`. The `:rest` argument type that annotates the `text` argument to\nget the entire rest of the input does not come with rainboots, but\nwould be easy enough to implement. See below for more on \"argument\ntypes.\"\n\nBy default, user input is destructured into arguments based on\nwhitespace. For example, imagine this command:\n\n```clojure\n(defcmd put\n  \"Put something somewhere\"\n  [cli what where]\n  ; this part is up to you!\n  )\n```\n\nA user-input line \"put gun holster\" will be automatically destructured\nfor you.\n\nCommands don't always follow a single format, however, so `defcmd`\nsupports multi-arity out of the box:\n\n```clojure\n(defcmd look\n  \"Look around you, or at something\"\n  ([cli]\n   (send! cli \"You're on a run-down old ship (But don't call it that!)\"))\n  ([cli thing]\n   (send! cli thing \" isn't very interesting to look at\")))\n```\n\nTo make this really convenient, however, you'll want to make some\nargument types.\n\n#### Argument Types\n\nA powerful feature built into rainboots is the notion of \"argument\ntypes.\" These are basically keyword annotations for command arguments\nwhich transform the user's input into the appropriate object, so\ncommands can just declare what they expect, and rainboots can handle\nvalidating the user's input and providing the objects.\n\nHere's how to implement that `^:rest` type shown above:\n\n```clojure\n(defargtype :rest\n  \"A string of text\"\n  [cli input]\n  [input nil])\n```\n\nWhat? That's it!?\n\n`defargtype` installs the argument type handler globally, and also uses\n`(defn)`-like syntax. Every handler MUST return a vector, whose first\nitem is the resulting object, and whose second item is the remaining\npart of the input to parse. Handlers are provided a client object and\nthe remaining input line at that point. This lets you do fancy things\nlike supporting \"sword in stone\" as a single `^:item` argument.\n\nFor more specificity per-command, you may use the map annotation style\nto provide a parameter to your argtype. For example, you could annotate\na param as `^{:item :on-ground}` if you only want the item if it is on\nthe ground. The argtype def should then look something like:\n\n```clojure\n(defargtype :item\n  \"An item somewhere\"\n  [cli input \u0026 [param]]\n  (cond\n    (= :on-ground param)\n    ;; .. etc\n    :else ;; ...\n    ))\n```\n\nArgtypes may not always \"work,\" however. Even if the user provided\nparse-able input, it might be invalid. Having to handle that everywhere\nyou use an argtype is problematic, so you may return a `Throwable`\ninstead of a value. If any argument is parsed to a `Throwable`, the\nmessage in the first `Throwable` found will be sent to the user, and\nyour command handler will not be called. For example:\n\n```clojure\n(defargtype :item\n  \"An item somewhere\"\n  [cli input]\n  (if-let [[item etc] (find-item cli input)]\n    ;; found it!\n    [item etc]\n    ;; no such item found:\n    [(Exception. (str \"I don't see any \" input)), etc]))\n```\n\nSometimes, you might want an argtype that isn't directly supplied by\nthe user's input, for example if you have combat commands that require\na previously-specified target. Using an argtype is a convenient way to\naccess this, and have the logic to verify the existence of that target\nbe unified in a single place. For such an argtype, the input is\ngenerally not necessary, so you can mark it as `:nilable`, meaning that\nit's okay if it's `nil`—normally, an argtype is only called if there's\nsome input left to handle. For example:\n\n```clojure\n(defargtype :target\n  \"Your current target\"\n  [cli ^:nilable input]\n  [(if-let [target (:target @cli)]\n    target\n    (Exception. \"You must target something first\"))\n    input])  ; return the input unchanged\n```\n\n\n### Command sets\n\nNormally, all commands are added to a default \"command set.\" Sometimes,\nhowever, you may wish to put the user into a special mode where they\nhave access to only specific commands. You can do this via\n`(push-cmds!)` and `(pop-cmds!)`.\n\n`(push-cmds!)` takes a client object, and the command set. You can\ndefine a command set using, you guessed it:\n\n```clojure\n(defcmdset combat-commands\n  (defcmd punch\n    [cli]\n    (send! cli \"You swing a punch!\")))\n```\n\nAny `defcmd`s inside a `defcmdset` will be bound to that set, and only\nvisible after a call to `(push-cmds! cli combat-commands)`.\n\nIn fact, a cmdset is just a function, which looks like `(fn [on-404 cli\ninput])`. `on-404` is the registered \"unknown command\" function\ninstalled on the server, and the rest is as you expect. So, if you want\nfull control over the input and don't wish to use `defcmd` or\n`defcmdset`, you can just `(push-cmds!)` your own function!\n\n### Hooks\n\nHooks allow you to provide information throughout the system without\nhaving any direct dependencies. For example, you might have a hook for\n\"wearing\" an item.  There might be different types of effects\nassociated with an item, such as armor points, or magical properties.\nYou could store these properties as keywords on the item, and apply\nthem by *hooking into* the \"wear\" event.\n\nFor example:\n\n```clojure\n;;\n;; Register hooks\n;;\n\n; magic-items.clj:\n(hook! :wear-item\n  ; Note the use of a named fn here: It's not\n  ; required, but your repl experience will be\n  ; better if you do.\n  (fn wear-magic-item [{:keys [cli item] :as arg}]\n    (when-let [magic (:magic item)]\n      (apply-magic! cli magic))\n    arg))\n\n; armor.clj:\n(hook! :wear-item\n  (fn wear-armor [{:keys [cli item] :as arg}]\n    (when-let [armor (:armor item)]\n      (apply-armor! cli armor))\n    arg))\n\n;;\n;; Execute hooks\n;;\n\n(defcmd wear\n  \"Wear an item\"\n  [cli ^:item item]\n  (when (trigger! :wear-item {:cli cli :item item})\n    ;; You could potentially support returning nil\n    ;;  to indicate that the item couldn't be worn;\n    ;;  the semantics of each hook is up to you!\n    (add-equip! cli item))\n```\n\n#### Hook ordering\n\nBy default, the order in which hooks are triggered is undefined, since\nthe order in which they are added depends on when you load the\nnamespace they're declared in, but you can provide a specific priority\nif you need to make sure that some hook is executed before another:\n\n```clojure\n(hook! :wear-item\n  {:priority 10}\n  (fn wear-magic-item [{:keys [cli item] :as arg}]\n    ; etc\n    )\n```\n\nIf unspecified, a hook's priority will be `0`. Higher-priority hooks\nwill be executed before lower-priority hooks.\n\n#### Stopping early\n\nIf the semantics of your hook are that not every registered hook fn\nneeds to see the input, you may wrap the returned value with `reduced`\nto indicate that the given value **is** the *result*, and that no other\nhook fn needs to run:\n\n```clojure\n(hook! :wear-item\n  {:priority 10}\n  (fn prevent-wearing-unwearables [{:keys [cli item] :as arg}]\n    (if (wearable? item)\n      arg ; proceed\n\n      (do\n        (send! cli \"You can't wear that!\")\n        (reduced {})))))\n```\n\n#### Default hooks\n\nSometimes you want to provide a \"fallback\" hook that only gets called\nwhen nobody else was interested. Rainboots has a couple options here:\n\n- `:when-only`  With this option, the hook fn will *only* be called if\n  no other fn is registered for this hook.\n- `:when-no-result`  With this option, the hook fn will *only* be\n  called if no other fn has produced a *result* (see above).\n\nIf you're only providing one of these options and not priority (they\nare essentially mutually exclusive, so this should be the normal case)\nyou can use a set instead of a map, for example:\n\n```clojure\n(hook! :perform-action\n  #{:when-no-result}\n  (fn default-perform-action [arg]\n    ; etc\n    ))\n```\n\n#### Builtin hooks\n\nRainboots uses hooks internally to provide ways for you to extend or replace\ndefault behaviors.\n\nHook | Description\n---- | -----------\n`:process-send!` | All strings sent with `(send!)` are processed by triggering the `:process-send!` hook with a map containing the recipient as `:cli` and the text to send as `:text`.  This is how the built-in colorization is applied, which means you can fully disable it by doing `(unhook! :process-send! rainbooks.comms/default-colorize-hook)`.\u003cbr\u003e\u003cbr\u003eIn fact, `(send!)`, `(send-if!)`, and `(send-all!)` all support an optional first parameter `process-extras` which is a map whose keys and values will be included in the argument to the `:process-send!` hook, so you can provide extra information to your own custom output processing functions.\n\n### Colors\n\nWhen using the built-in `(send!)` method, strings will automatically be colorized\nwith ansi using some built-in escape sequences. The foreground color can be\nchanged using the `{` character, followed by a color. Here's a table:\n\nSymbol | Color   | Symbol | Color\n-------|---------|--------|------\n`{d`   | ![Dark](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/43c72287b5a987b545b2a81480767527b4d8dab1/ansi-d-dark.svg)    | `{D`   | ![Less-Dark](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/82dad12dbfe04766120883be5d74dd2f87df57fd/ansi-b-dark.svg)\n`{r`   | ![Red](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/43c72287b5a987b545b2a81480767527b4d8dab1/ansi-d-red.svg)     | `{R`   | ![Bright Red](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/2e3d51c6bb96b9f20ead14161edba4c66be57818/ansi-b-red.svg)\n`{g`   | ![Green](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/43c72287b5a987b545b2a81480767527b4d8dab1/ansi-d-green.svg)   | `{G`   | ![Bright-green](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/1ce48bb5af4ba4207ef7469e99a4ef9e3882a677/ansi-b-green.svg)\n`{y`   | ![Yellow](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/43c72287b5a987b545b2a81480767527b4d8dab1/ansi-d-yellow.svg)  | `{Y`   | ![Bright-yellow](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/1ce48bb5af4ba4207ef7469e99a4ef9e3882a677/ansi-b-yellow.svg)\n`{b`   | ![Blue](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/43c72287b5a987b545b2a81480767527b4d8dab1/ansi-d-blue.svg)    | `{B`   | ![Bright-blue](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/2a2b5b6583abe9e60e1eaed141fc585d4db74095/ansi-b-blue.svg)\n`{p`   | ![Magenta](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/43c72287b5a987b545b2a81480767527b4d8dab1/ansi-d-magenta.svg) | `{P`   | ![Bright-Magenta](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/2a2b5b6583abe9e60e1eaed141fc585d4db74095/ansi-b-magenta.svg)\n`{c`   | ![Cyan](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/43c72287b5a987b545b2a81480767527b4d8dab1/ansi-d-cyan.svg)    | `{C`   | ![Bright-cyan](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/2a2b5b6583abe9e60e1eaed141fc585d4db74095/ansi-b-cyan.svg)\n`{w`   | ![Gray](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/43c72287b5a987b545b2a81480767527b4d8dab1/ansi-d-white.svg)    | `{W`   | ![White](https://cdn.rawgit.com/dhleong/cd96df9cb4c58e9db7d2f88fac4a3d29/raw/2a2b5b6583abe9e60e1eaed141fc585d4db74095/ansi-b-white.svg)\n`{n`   | (Reset) | |\n\n## License\n\nCopyright © 2016-2019 Daniel Leong\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n\n[1]: https://en.wikipedia.org/wiki/MUD\n[2]: src/rainboots/sample.clj\n[3]: http://clojure.org/reference/atoms\n[4]: https://github.com/weavejester/hiccup/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdhleong%2Frainboots","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdhleong%2Frainboots","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdhleong%2Frainboots/lists"}