Let's start with commands. As I already mentioned, there are silent and loud commands, that use different prefix. Commands are handled by the 'handle-command' function. This function takes user input and returns whatever should be broadcasted to the server.
Silent commands always return empty string, as writing to the user only is done while handling the command. Loud commands were supposed to use this broadcasting system, but instead I also handle all broadcasting inside and return an empty string, so it only ever returns anything for regular messages. As far as the input processing goes, I'm using srfi-13. There will also be special prefix for interacting with objects, but that is yet to be implemented.
The following commands currently exist:
silent commands /help /list-colors /set-description <description> /describe <username> /look-around /list-worlds loud commands: !exit !set-name <new-username> !set-color <color> !yell <text> !goto <pathway> !runto <pathway>
I think most are self-descriptive. We will get to '!yell' later. All users start with the default username 'anon' in green color. There are no accounts. You always start like that and anyone can configure whatever, even if such username already exists. This is why describe can handle multiple users of the same name.
If you'd wish to always have some default config, there are multiple ways to achieve that. While nc-chat is meant to be used via nc(1), there is nothing stopping you from writing your own client. It should actually be very easy, as there is no complex protocol. Such client can handle some initial configuration for you. If you don't want to handle entire client, you can feed some initial commands to nc via a simple script, such as:
#!/bin/sh { cat << EOF !set-name de-alchmst !set-color bred /set-description I'm still alive, somehow EOF cat; } | nc localhost 9999
Description should technically allow multiple lines, but I did not manage to convince nc to send a newline, as it uses it to split text into messages. It should be possible to do via a custom client tho. I might add some escape parsing for that.
Changing your name or color will inform the entire world, as it is quite useful to know if someone got renamed.
Speaking of which...
In nc-chat, you will be able to separate the server into multiple worlds. Each world will be further separated into multiple places, connected via pathways. This is configured in scheme files, that will be loaded at runtime, opposed to the compiled core.
Currently, the nc-chat binary expects a 'data' directory next to it. It will then recursively walk through it and loading everything ending in '.ss', '.scm' or '.so' (as you could technically also provide some compiled code yourself).
Turns out recursively processing files in such way is so common, that chicken includes a function for just that: 'find-files'. (no link, as chicken finds it very funny to include parenthesis in it's url and I can't be bothered to fix my site parser to support that)
'find-files' looks something like this:
(find-files DIRECTORY #!key test action seed limit dotfiles follow-symlinks)You can read more about it on the website, but in short, you only select files that match certain regex and apply 'action' on them. '#!key' means, that the following are named arguments.
For loading the config, I'm using the following file:
;; THIS FILE IS USED TO ISOLATE LOADED CODE FROM THE REST OF CODEBASE ;; IT SHOULD NOT BE NEEDED, BUT I'M STILL AFRAID... (module load-worlds (motd worlds symbol->value) (import scheme (chicken base) (chicken process-context) (chicken file) (chicken irregex) (chicken pathname)) (find-files (make-pathname (pathname-directory (executable-pathname)) "data") #:test (irregex ".+\.(?:ss|scm|so)") #:limit #f #:dotifles #t #:follow-symlinks #t #:seed '() #:action (lambda (file _) (load file))) ;; dark magic required to get the symbol to module scope (define motd (eval 'motd)) (define worlds (eval 'worlds)) (define (symbol->value sym) (eval sym)))
The loaded symbols are present in the module, but for some reason, they won't get exported. For that, I have to use the eval trick.
'worlds' is a list of names of all the worlds.
Now I'm realising that some people might not know the basic concepts of Lisp. In Lisp, variables consist of two parts, a name and a value. In some Lisps, variables can have both normal values and a function bind to it at the same time, but in scheme, they share the same slot.
The point is, that the name is a value upon itself. It's called a symbol and you can do some fun stuff with them.
An example world might look like this:
(import (chicken time posix)) (define hub `((description "The main central area of this place") (places (base (welcome "Welcome to the HUB!") (interactives (clock "An old clock hangs on the wall" ,(lambda () (string-append "it's " (seconds->string))))) (pathways (gallery "This door leads to the gallery..." gallery))) (gallery (welcome "Welcome to the gallery!") (interactives) (pathways (base "This door leads to the main area." base))))))
The backtick denotes a quasi-quoted list. In quoted list, everything is stored as a value, instead of being evaluated. This means, that all the names will be stored as symbols, instead of their values.
This being a quasi-quoted list means, that you can also mix in some evaluated values by prefixing them with comma, such as with the lambda in 'clock'. There are noral quoted lists that are denoted with apostrophe and do not allow for any such exceptions.
I came up with this structure. First item of list acts as a key, while the rest acts as a list bound to that key. Think of it a JSON, except in scheme and with functions. Scheme allows you to do things like this.
So now that I have this structure in memory, it's time to parse it. At first I made a function, that is able to search for any key, no matter how deep it is. I have done this as follows:
(define (find-item itm lst) (cond ;; atom? so that I don't need to make checks in 'else' ((or (null? lst) (atom? lst)) '()) ((equal? (car lst) itm) (cdr lst)) ;; if value is '(), it will search the entire tree anyways ;; but it should return '() in the end, so it works (else (let ((val (find-item itm (car lst)))) (if (null? (cdr lst)) val (if (null? val) (find-item itm (cdr lst)) val)))))))
It worked on the first try, which made me happy. Happiness didn't last long however, as I realised that this doesn't allow for multiple keys of the same name on different levels.
Due to this, I rewrote it like so:
;; with the key (define (find-item* itm lst) (cond ((null? lst) '()) ((and (list? (car lst)) (not (null? (car lst))) (equal? (caar lst) itm)) (car lst)) (else (find-item* itm (cdr lst))))) ;; without the key (define (find-item itm lst) (let ((out (find-item* itm lst))) (if (null? out) '() (cdr out))))
This can only search in one level. In scheme, ending name with '*' means that it is a slightly different variant (such a 'let' and 'let*'). Originally, I always dropped the key from the outcome. Later when working with places, I realised that sometimes I would actually like to keep the key as well, so I added the 'find-item*' variant.
Lists are actually linked lists. You can access the first item with 'car' and the rest of the list with 'cdr'. For convenience, they can be written into one function like so:
(car (cdr lst)) ;; is the same as (cadr lst)
'null?' tests if a list is empty.
You should be able to understand the code now.
To make it a bit less annoying to write, I added the following macros:
;; passing world as both symbol and tree can be useful, so this makes it easy (define (world->tree world) (if (symbol? world) (symbol->value world) world)) ;; for easier treversal (define-syntax at-tree-path (syntax-rules () [(at-tree-path tree sym) (find-item sym tree)] [(at-tree-path tree syms ... sym) (find-item sym (at-tree-path tree syms ...))])) (define-syntax at-world-path (syntax-rules () [(at-world-path world syms ...) (at-tree-path (world->tree world) syms ...)]))
As you can notice, I'm keeping the Pascal tradition of having two empty lines between functions.
Ok, so now we have worlds. Users have two extra slots. One for world and one for place in that world. Each takes just the tree, tho the place including the key. As it's all linked lists, they can be compared based on identity via 'eq?'.
This becomes useful, as I also had to to rework broadcasts. I have tree broadcast functions with differing broadcast domains: 'broadcast-server', 'broadcast-world' and 'broadcast-place'. Communication works on place basis. Changing username or color is alerted on world basis.
If you want to message the entire world, you use the '!yell' command.
I should probably also mention '!goto' vs '!runto'. '!goto' will move you from one place to another, and will also call '/look-around', which will list place description and all the interactives, pathways and users. '!runto' will just move you there without the listing.
Well, I have once again written way more than I wanted to. When I was working on it, it didn't seem like I have done much, but looks like I was wrong.
Well the project is almost finished now.
Mainly I just have to do interactions with items and travel between worlds and it should be done. I also wanted to do some sort of inventory system, but I just can't come up with any system that seems right.
Well, we'll see.