├── .gitignore ├── .goreleaser.yml ├── AUTHORS ├── DESIGN.md ├── LICENSE ├── README.md ├── auth ├── auth.go ├── auth_test.go └── user.go ├── claymud.go ├── data ├── commands.toml ├── location.template ├── mobs │ ├── 0.json │ ├── 103.json │ ├── 105.json │ ├── 106.json │ ├── 108.json │ ├── 11.json │ ├── 112.json │ ├── 113.json │ ├── 115.json │ ├── 116.json │ ├── 118.json │ ├── 119.json │ ├── 12.json │ ├── 120.json │ ├── 122.json │ ├── 127.json │ ├── 128.json │ ├── 13.json │ ├── 130.json │ ├── 131.json │ ├── 140.json │ ├── 141.json │ ├── 142.json │ ├── 145.json │ ├── 146.json │ ├── 15.json │ ├── 150.json │ ├── 151.json │ ├── 156.json │ ├── 158.json │ ├── 16.json │ ├── 160.json │ ├── 161.json │ ├── 162.json │ ├── 163.json │ ├── 17.json │ ├── 172.json │ ├── 173.json │ ├── 179.json │ ├── 18.json │ ├── 181.json │ ├── 183.json │ ├── 184.json │ ├── 185.json │ ├── 187.json │ ├── 189.json │ ├── 19.json │ ├── 190.json │ ├── 196.json │ ├── 197.json │ ├── 2.json │ ├── 20.json │ ├── 203.json │ ├── 205.json │ ├── 206.json │ ├── 208.json │ ├── 210.json │ ├── 217.json │ ├── 22.json │ ├── 221.json │ ├── 222.json │ ├── 223.json │ ├── 225.json │ ├── 226.json │ ├── 227.json │ ├── 23.json │ ├── 230.json │ ├── 235.json │ ├── 237.json │ ├── 238.json │ ├── 242.json │ ├── 243.json │ ├── 245.json │ ├── 247.json │ ├── 248.json │ ├── 25.json │ ├── 250.json │ ├── 251.json │ ├── 255.json │ ├── 256.json │ ├── 258.json │ ├── 259.json │ ├── 26.json │ ├── 260.json │ ├── 261.json │ ├── 262.json │ ├── 27.json │ ├── 270.json │ ├── 271.json │ ├── 272.json │ ├── 273.json │ ├── 29.json │ ├── 3.json │ ├── 30.json │ ├── 31.json │ ├── 32.json │ ├── 33.json │ ├── 34.json │ ├── 36.json │ ├── 38.json │ ├── 40.json │ ├── 41.json │ ├── 42.json │ ├── 43.json │ ├── 45.json │ ├── 46.json │ ├── 47.json │ ├── 48.json │ ├── 49.json │ ├── 5.json │ ├── 50.json │ ├── 51.json │ ├── 52.json │ ├── 53.json │ ├── 54.json │ ├── 56.json │ ├── 59.json │ ├── 6.json │ ├── 60.json │ ├── 61.json │ ├── 64.json │ ├── 65.json │ ├── 66.json │ ├── 7.json │ ├── 70.json │ ├── 73.json │ ├── 75.json │ ├── 79.json │ ├── 8.json │ ├── 82.json │ ├── 83.json │ ├── 84.json │ ├── 85.json │ ├── 86.json │ ├── 88.json │ ├── 9.json │ ├── 90.json │ ├── 93.json │ ├── 95.json │ ├── 96.json │ ├── 98.json │ └── 99.json ├── mud.toml ├── rooms │ ├── 0.json │ ├── 103.json │ ├── 105.json │ ├── 106.json │ ├── 108.json │ ├── 11.json │ ├── 112.json │ ├── 113.json │ ├── 115.json │ ├── 116.json │ ├── 118.json │ ├── 119.json │ ├── 12.json │ ├── 120.json │ ├── 122.json │ ├── 127.json │ ├── 128.json │ ├── 13.json │ ├── 130.json │ ├── 131.json │ ├── 140.json │ ├── 141.json │ ├── 142.json │ ├── 145.json │ ├── 146.json │ ├── 15.json │ ├── 150.json │ ├── 151.json │ ├── 156.json │ ├── 158.json │ ├── 16.json │ ├── 160.json │ ├── 161.json │ ├── 162.json │ ├── 163.json │ ├── 17.json │ ├── 172.json │ ├── 173.json │ ├── 179.json │ ├── 18.json │ ├── 181.json │ ├── 183.json │ ├── 184.json │ ├── 185.json │ ├── 187.json │ ├── 189.json │ ├── 19.json │ ├── 190.json │ ├── 196.json │ ├── 197.json │ ├── 2.json │ ├── 20.json │ ├── 203.json │ ├── 205.json │ ├── 206.json │ ├── 208.json │ ├── 210.json │ ├── 217.json │ ├── 22.json │ ├── 221.json │ ├── 222.json │ ├── 223.json │ ├── 225.json │ ├── 226.json │ ├── 227.json │ ├── 23.json │ ├── 230.json │ ├── 235.json │ ├── 237.json │ ├── 238.json │ ├── 242.json │ ├── 243.json │ ├── 245.json │ ├── 247.json │ ├── 248.json │ ├── 25.json │ ├── 250.json │ ├── 251.json │ ├── 255.json │ ├── 256.json │ ├── 258.json │ ├── 259.json │ ├── 26.json │ ├── 260.json │ ├── 261.json │ ├── 262.json │ ├── 27.json │ ├── 270.json │ ├── 271.json │ ├── 272.json │ ├── 273.json │ ├── 29.json │ ├── 3.json │ ├── 30.json │ ├── 31.json │ ├── 32.json │ ├── 33.json │ ├── 34.json │ ├── 36.json │ ├── 38.json │ ├── 40.json │ ├── 41.json │ ├── 42.json │ ├── 43.json │ ├── 45.json │ ├── 46.json │ ├── 47.json │ ├── 48.json │ ├── 49.json │ ├── 5.json │ ├── 50.json │ ├── 51.json │ ├── 52.json │ ├── 53.json │ ├── 54.json │ ├── 56.json │ ├── 59.json │ ├── 6.json │ ├── 60.json │ ├── 61.json │ ├── 64.json │ ├── 65.json │ ├── 66.json │ ├── 7.json │ ├── 70.json │ ├── 73.json │ ├── 75.json │ ├── 79.json │ ├── 8.json │ ├── 82.json │ ├── 83.json │ ├── 84.json │ ├── 85.json │ ├── 86.json │ ├── 88.json │ ├── 9.json │ ├── 90.json │ ├── 93.json │ ├── 95.json │ ├── 96.json │ ├── 98.json │ └── 99.json ├── scripts │ ├── teleport.star │ └── wind.star ├── socials.toml └── zones │ ├── 0.json │ ├── 103.json │ ├── 105.json │ ├── 106.json │ ├── 108.json │ ├── 11.json │ ├── 112.json │ ├── 113.json │ ├── 115.json │ ├── 116.json │ ├── 118.json │ ├── 119.json │ ├── 12.json │ ├── 120.json │ ├── 122.json │ ├── 127.json │ ├── 128.json │ ├── 13.json │ ├── 130.json │ ├── 131.json │ ├── 140.json │ ├── 141.json │ ├── 142.json │ ├── 145.json │ ├── 146.json │ ├── 15.json │ ├── 150.json │ ├── 151.json │ ├── 156.json │ ├── 158.json │ ├── 16.json │ ├── 160.json │ ├── 161.json │ ├── 162.json │ ├── 163.json │ ├── 17.json │ ├── 172.json │ ├── 173.json │ ├── 179.json │ ├── 18.json │ ├── 181.json │ ├── 183.json │ ├── 184.json │ ├── 185.json │ ├── 187.json │ ├── 189.json │ ├── 19.json │ ├── 190.json │ ├── 196.json │ ├── 197.json │ ├── 2.json │ ├── 20.json │ ├── 203.json │ ├── 205.json │ ├── 206.json │ ├── 208.json │ ├── 210.json │ ├── 217.json │ ├── 22.json │ ├── 221.json │ ├── 222.json │ ├── 223.json │ ├── 225.json │ ├── 226.json │ ├── 227.json │ ├── 23.json │ ├── 230.json │ ├── 235.json │ ├── 237.json │ ├── 238.json │ ├── 242.json │ ├── 243.json │ ├── 245.json │ ├── 247.json │ ├── 248.json │ ├── 25.json │ ├── 250.json │ ├── 251.json │ ├── 255.json │ ├── 256.json │ ├── 258.json │ ├── 259.json │ ├── 26.json │ ├── 260.json │ ├── 261.json │ ├── 262.json │ ├── 27.json │ ├── 270.json │ ├── 271.json │ ├── 272.json │ ├── 273.json │ ├── 29.json │ ├── 3.json │ ├── 30.json │ ├── 31.json │ ├── 32.json │ ├── 33.json │ ├── 34.json │ ├── 36.json │ ├── 38.json │ ├── 40.json │ ├── 41.json │ ├── 42.json │ ├── 43.json │ ├── 45.json │ ├── 46.json │ ├── 47.json │ ├── 48.json │ ├── 49.json │ ├── 5.json │ ├── 50.json │ ├── 51.json │ ├── 52.json │ ├── 53.json │ ├── 54.json │ ├── 56.json │ ├── 59.json │ ├── 6.json │ ├── 60.json │ ├── 61.json │ ├── 64.json │ ├── 65.json │ ├── 66.json │ ├── 7.json │ ├── 70.json │ ├── 73.json │ ├── 75.json │ ├── 79.json │ ├── 8.json │ ├── 82.json │ ├── 83.json │ ├── 84.json │ ├── 85.json │ ├── 86.json │ ├── 88.json │ ├── 9.json │ ├── 90.json │ ├── 93.json │ ├── 95.json │ ├── 96.json │ ├── 98.json │ └── 99.json ├── db ├── bolt.go ├── credentials.go ├── db.go ├── db_test.go ├── errors.go ├── players.go ├── players_test.go ├── users.go └── users_test.go ├── game ├── dice.go ├── direction.go ├── gender.go ├── position.go ├── social │ ├── parsing.go │ ├── social_test.go │ └── socials.go └── worker.go ├── magefile.go ├── server ├── config │ └── config.go └── run.go ├── testutil └── testutil.go ├── util ├── id.go ├── id_test.go ├── query.go ├── query_test.go └── util.go └── world ├── actions.go ├── area.go ├── command.go ├── commands.go ├── exit.go ├── global_test.go ├── init.go ├── load_world.go ├── location.go ├── mob.go ├── player.go └── player_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | data/logs/*.log 24 | data/mud.db 25 | .vscode 26 | claymud 27 | dist/ -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: claymud 2 | release: 3 | github: 4 | owner: natefinch 5 | name: claymud 6 | draft: true 7 | build: 8 | binary: claymud 9 | main: . 10 | ldflags: -s -w -X github.com/natefinch/claymud/server.timestamp={{.Date}} -X github.com/natefinch/claymud/server.commitHash={{.Commit}} -X github.com/natefinch/claymud/server.gitTag={{.Version}} 11 | goos: 12 | - darwin 13 | - linux 14 | - windows 15 | - freebsd 16 | - netbsd 17 | - openbsd 18 | - dragonfly 19 | goarch: 20 | - amd64 21 | - 386 22 | - arm 23 | - arm64 24 | ignore: 25 | - goos: openbsd 26 | goarch: arm 27 | goarm: 6 28 | env: 29 | - CGO_ENABLED=0 30 | archive: 31 | name_template: "{{.Binary}}_{{.Version}}_{{.Os}}-{{.Arch}}" 32 | replacements: 33 | amd64: 64bit 34 | 386: 32bit 35 | arm: ARM 36 | arm64: ARM64 37 | darwin: macOS 38 | linux: Linux 39 | windows: Windows 40 | openbsd: OpenBSD 41 | netbsd: NetBSD 42 | freebsd: FreeBSD 43 | dragonfly: DragonFlyBSD 44 | format: tar.gz 45 | format_overrides: 46 | - goos: windows 47 | format: zip 48 | files: 49 | - LICENSE 50 | snapshot: 51 | name_template: SNAPSHOT-{{ .Commit }} 52 | checksum: 53 | name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt' 54 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Nate Finch -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | ClayMUD Design Notes 2 | ==================== 3 | 4 | ClayMUD is intended to be extremely configurable with almost zero programming 5 | knowledge. Configuration files use the user-friendly 6 | [toml](https://github.com/toml-lang/toml) configuration language. 7 | 8 | ## Goroutines 9 | 10 | ### Players 11 | 12 | Connections have their own goroutine, which ends up being one per in-game 13 | player. These do the command parsing. 14 | 15 | ### Workers 16 | 17 | Each zone has its own goroutine "worker", plus there's a global worker. Each 18 | worker contains an event channel protected by a "gate" that is a readwrite lock. 19 | Players send "local" events (events that only affect things in the same zone as 20 | them) to the worker for the zone they're in. They readlock and then read-unlock 21 | the gate mutex before sending on the channel, ensuring that if they gate is 22 | closed they will block before the send. Every "tick" (100ms), the worker wakes 23 | up, write-locks the gate (shutting out any new events) and then drains the 24 | channel of events. Once finished, it unlocks the gate and sleeps until the next 25 | tick. This gate ensures that a fast player can't get a second event on the 26 | worker queue while the worker is consuming from the queue (otherwise the queue 27 | might never empty). 28 | 29 | The workers ensure that all writes to global state are synchronized without race 30 | conditions or too much lock contention. 31 | 32 | ## DB 33 | 34 | ClayMUD uses BoltDB to store data. This removes any dependency on an outside 35 | application to store data. 36 | 37 | Entities from the game are stored in the db using json encoded bytes. 38 | ### Users 39 | 40 | User passwords are stored as bcrypt hashes in the db, keyed by username. The 41 | bcrypt cost is configurable by the administrator. 42 | 43 | ### Characters 44 | 45 | Characters (called Players in the code) are stored in the DB with their lowercased name as 46 | the key, ensuring we don't have duplicate names that look similar. 47 | 48 | ## Locations, Mobs, Items 49 | 50 | Permanent Location, mob, and item data such as name, description, etc are 51 | loaded as json from files on disk at startup. Even with thousands of rooms and 52 | mobs, the memory overhead is trivial. 53 | 54 | Ephemeral Location data such as what players, mobs, and items that are in a 55 | Location is stored in memory. 56 | 57 | ## Scripting 58 | 59 | ClayMUD supports extensive, dynamic scripting via an embedded Python dialect 60 | known as [starlark](https://github.com/google/starlark-go). 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | .d8888b. 888 888b d888 888 888 8888888b. 3 | d88P Y88b 888 8888b d8888 888 888 888 "Y88b 4 | 888 888 888 88888b.d88888 888 888 888 888 5 | 888 888 8888b. 888 888 888Y88888P888 888 888 888 888 6 | 888 888 "88b 888 888 888 Y888P 888 888 888 888 888 7 | 888 888 888 .d888888 888 888 888 Y8P 888 888 888 888 888 8 | Y88b d88P 888 888 888 Y88b 888 888 " 888 Y88b. .d88P 888 .d88P 9 | "Y8888P" 888 "Y888888 "Y88888 888 888 "Y88888P" 8888888P" 10 | 888 11 | Y8b d88P 12 | "Y88P" 13 | ``` 14 | 15 | ClayMUD is a highly configurable, highly performant 16 | [MUD](https://en.wikipedia.org/wiki/MUD) implemented in the Go programming 17 | language. 18 | 19 | Currently it is in active development, but the overarching premise is that a MUD 20 | should be configurable and runnable without any programming knowledge. Too many 21 | MUD systems require you to write code to change how they work. Not only is that 22 | very likely to introduce bugs, it also restricts MUDs to be run by people who 23 | know how to code. 24 | 25 | ClayMUD is intended to be fully configurable through text files - everything 26 | from the name of the MUD, to what socials are available, to how ability scores 27 | and powers work, so that anyone can create their own unique game. 28 | 29 | 30 | Status 31 | ----------- 32 | 33 | The game is functioning to the point where you can connect, create an account, 34 | create one or more players, and walk around, talk, and social. You can 35 | configure the socials and the pronouns used for gender (e.g. he his her). There 36 | is a "chatmode" toggle that will switch the interface from commands-by-default 37 | (typical MUD), and talk-by-default (like slack or discord, where typing produces 38 | textual output). While in chatmode, directional commands will work normally, 39 | but any other command must be prefixed with a configurable prefix (like /). 40 | 41 | 42 | To build and run 43 | ----------------------- 44 | 45 | ```shell 46 | go get -d github.com/natefinch/claymud 47 | claymud 48 | ``` 49 | 50 | This will run the mud on port 8888 of your current machine. To change the port, 51 | use -port 52 | 53 | To run with version info embedded in the binary (recommended), you'll need the 54 | [mage](magfile.org) build tool. From the root directory of this repo: 55 | 56 | ```shell 57 | go get github.com/magefile/mage 58 | mage run 59 | ``` 60 | 61 | 62 | Configuration 63 | ----------- 64 | Claymud makes extensive use of configuration files. Working examples live in 65 | the `data` directory of this repo, with comments explaining how they work and 66 | what the values mean. The main configuration file is mud.toml. 67 | 68 | Storage 69 | ------------- 70 | 71 | ClayMUD stores data as json serialized documents in 72 | [boltdb](https://github.com/boltdb/bolt), an embedded key/value store that saves 73 | to a file on disk. 74 | 75 | 76 | License 77 | ------------- 78 | 79 | MIT License 80 | -------------------------------------------------------------------------------- /auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | -------------------------------------------------------------------------------- /auth/user.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "io" 5 | "math/big" 6 | 7 | "github.com/natefinch/claymud/util" 8 | ) 9 | 10 | // UFlag represents a flag (bit) set on a User. 11 | type UFlag int 12 | 13 | // All possible user flags. 14 | // 15 | // DO NOT REARRANGE OR COMMENT OUT VALUES. If you need to deprecate a value, 16 | // append _DEPRECATED on the end of the name. New values must be appended to 17 | // this list, never inserted anywhere else. 18 | const ( 19 | UFlagAdmin UFlag = iota 20 | ) 21 | 22 | // An user is a username and password and connection info. 23 | type User struct { 24 | ID util.ID 25 | Username string 26 | Players []string 27 | bits *big.Int 28 | io.Closer 29 | util.WriteScanner 30 | } 31 | 32 | // Flag reports if the given flag has been set to true for the user. 33 | func (u *User) Flag(f UFlag) bool { 34 | return u.bits.Bit(int(f)) == 1 35 | } 36 | 37 | // SetFlag sets the given flag to true for the user. 38 | func (u *User) SetFlag(f UFlag) { 39 | u.bits.SetBit(u.bits, int(f), 1) 40 | } 41 | 42 | // UnsetFlag sets the given flag to false for the user. 43 | func (u *User) UnsetFlag(f UFlag) { 44 | u.bits.SetBit(u.bits, int(f), 0) 45 | } 46 | -------------------------------------------------------------------------------- /claymud.go: -------------------------------------------------------------------------------- 1 | // Package main is the entry point for ClayMUD. This defines the command line 2 | // args and listens for incoming connections. 3 | package main 4 | 5 | import ( 6 | "log" 7 | "os" 8 | 9 | "github.com/natefinch/claymud/server" 10 | ) 11 | 12 | func main() { 13 | if err := server.Main(); err != nil { 14 | log.Print(err) 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /data/commands.toml: -------------------------------------------------------------------------------- 1 | # This file contains the list of commmands you can type in the mud, and what the in-game 2 | # command names are. Command names cannot be duplicated (there will be an error at 3 | # startup if you do.) Exit directions take precedence over command names, so 4 | # keep that in mind when naming commands and directions. 5 | 6 | [Look] 7 | Command = "look" # the primary command name players will type to run the command 8 | Aliases = ["l"] # additional command names that will also work (usually abbreviations) 9 | Help = "looks at the room, or something in the room if given a target" # short help text shown in main output of `help` 10 | 11 | 12 | [Who] 13 | Command = "who" 14 | # you can leave out aliases if you don't want to specify them for a command. 15 | Help = "show a list of logged-on players" 16 | 17 | [Tell] 18 | Command = "tell" 19 | Aliases = ["t"] 20 | Help = "send a message to someone directly" 21 | 22 | [Quit] 23 | Command = "quit" 24 | Help = "leave the game" 25 | 26 | [Say] 27 | Command = "say" 28 | Aliases = ["'"] 29 | Help = "speak to everyone in the room" 30 | 31 | [Help] 32 | Command = "help" 33 | Aliases = ["?"] 34 | Help = "get help about commands" 35 | 36 | [Uptime] 37 | Command = "uptime" 38 | Help = "show how long the MUD has been running since the last reboot" 39 | 40 | [ChatMode] 41 | Command = "chatmode" 42 | Help = "turn on or off chatmode" 43 | 44 | [Goto] 45 | Command = "goto" 46 | Help = "admin command to go directly to a room by number or player by name" 47 | 48 | [Zones] 49 | Command = "zones" 50 | Aliases = [] 51 | Help = "shows a list of all zones in the world" 52 | -------------------------------------------------------------------------------- /data/location.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | This template defines how rooms will be displayed. 3 | */ -}} 4 | {{ .Name }} 5 | 6 | {{ .Desc }} 7 | 8 | [Exits] 9 | {{- range .Exits }} 10 | {{ .Name }} - {{ .Destination.Name }} 11 | {{- else }} 12 | There are no exits! 13 | {{ end }} 14 | {{if gt (len .Players) 1 -}} 15 | [Players] 16 | {{- range .Players }} 17 | {{- if ne $.Actor.ID .ID }} 18 | {{.Desc}} 19 | {{- end }} 20 | {{- end }} 21 | {{- end}} -------------------------------------------------------------------------------- /data/mobs/116.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 11600, 5 | "Aliases": [ 6 | "guard", 7 | "armed", 8 | "mental", 9 | "ward" 10 | ], 11 | "ShortDesc": "the armed guard", 12 | "LongDesc": "The armed guard stands here guarding the doors the the mental ward.\n", 13 | "DetailedDesc": "This guard looks to be a bit on the dumb side. Wonder if you can \noutsmart him?\n", 14 | "Actions": [ 15 | "SENTINEL", 16 | "ISNPC" 17 | ], 18 | "Affections": [], 19 | "Alignment": 300, 20 | "Level": 32, 21 | "THAC0": 12, 22 | "AC": 5, 23 | "HP": "10d10+300", 24 | "Damage": "5d5+20", 25 | "Gold": 3000, 26 | "XP": 8000, 27 | "LoadPosition": "POSITION_STANDING", 28 | "DefaultPosition": "POSITION_STANDING", 29 | "Gender": "Male" 30 | }, 31 | { 32 | "Number": 11601, 33 | "Aliases": [ 34 | "sarah", 35 | "connor", 36 | "woman" 37 | ], 38 | "ShortDesc": "Sarah Connor", 39 | "LongDesc": "Sarah Connor is standing here.\n", 40 | "DetailedDesc": "She is a strong woman and is very smart. But she's a little nuts. Why else\nwould she be here? She is very skilful. She could probly break out any time\nshe wanted to.\n", 41 | "Actions": [ 42 | "SPEC", 43 | "SENTINEL", 44 | "SCAVENGER", 45 | "ISNPC", 46 | "STAY_ZONE", 47 | "MEMORY", 48 | "NOCHARM" 49 | ], 50 | "Affections": [ 51 | "SANCTUARY" 52 | ], 53 | "Alignment": 1000, 54 | "Level": 127, 55 | "THAC0": 75, 56 | "AC": -25, 57 | "HP": "5d5+14000", 58 | "Damage": "10d9+50", 59 | "Gold": 50000, 60 | "XP": 500000, 61 | "LoadPosition": "POSITION_STANDING", 62 | "DefaultPosition": "POSITION_STANDING", 63 | "Gender": "Female" 64 | }, 65 | { 66 | "Number": 11602, 67 | "Aliases": [ 68 | "factory", 69 | "worker" 70 | ], 71 | "ShortDesc": "a factory worker", 72 | "LongDesc": "A factory worker stands here nervously doing his job.\n", 73 | "DetailedDesc": "This person looks to be scared of their supervisor. Wonder if you \ncould get them fired and have their job?\n", 74 | "Actions": [ 75 | "ISNPC", 76 | "STAY_ZONE", 77 | "MEMORY", 78 | "HELPER" 79 | ], 80 | "Affections": [ 81 | "DETECT_INVIS" 82 | ], 83 | "Alignment": 1000, 84 | "Level": 43, 85 | "THAC0": 18, 86 | "AC": -15, 87 | "HP": "20d10+200", 88 | "Damage": "4d4+12", 89 | "Gold": 5, 90 | "XP": 60000, 91 | "LoadPosition": "POSITION_STANDING", 92 | "DefaultPosition": "POSITION_STANDING", 93 | "Gender": "Neutral" 94 | }, 95 | { 96 | "Number": 11603, 97 | "Aliases": [ 98 | "Nurse", 99 | "R.N.", 100 | "hospital", 101 | "personel" 102 | ], 103 | "ShortDesc": "a Nurse, who works at county hospital", 104 | "LongDesc": "A nurse is standing here waiting to take someones temperature.\n", 105 | "DetailedDesc": "This nurse looks like she doesn't take no for an answer when the Doctor \norders something. She appears capable and knowledgeable in her field.\n", 106 | "Actions": [ 107 | "ISNPC", 108 | "AGGR_EVIL", 109 | "MEMORY", 110 | "NOCHARM", 111 | "NOSLEEP" 112 | ], 113 | "Affections": [ 114 | "BLIND", 115 | "SANCTUARY" 116 | ], 117 | "Alignment": 1000, 118 | "Level": 70, 119 | "THAC0": 40, 120 | "AC": -2, 121 | "HP": "5d5+2800", 122 | "Damage": "1d1+35", 123 | "Gold": 5000, 124 | "XP": 20000, 125 | "LoadPosition": "POSITION_STANDING", 126 | "DefaultPosition": "POSITION_STANDING", 127 | "Gender": "Neutral" 128 | }, 129 | { 130 | "Number": 11604, 131 | "Aliases": [ 132 | "Dr.", 133 | "Doctor", 134 | "personel", 135 | "0" 136 | ], 137 | "ShortDesc": "a Doctor who practices at county hospital", 138 | "LongDesc": "A Doctor stands here look at his patients chart.\n", 139 | "DetailedDesc": "This Doctor looks to be very in tune to his patients needs. He looks\nlike he has been here all night and he looks like he needs some sleep.\n", 140 | "Actions": [ 141 | "SPEC", 142 | "ISNPC", 143 | "AGGR_EVIL", 144 | "MEMORY", 145 | "NOCHARM", 146 | "NOSLEEP" 147 | ], 148 | "Affections": [ 149 | "BLIND", 150 | "SANCTUARY" 151 | ], 152 | "Alignment": 100, 153 | "Level": 127, 154 | "THAC0": 65, 155 | "AC": -26, 156 | "HP": "1d100+13000", 157 | "Damage": "10d10+70", 158 | "Gold": 5000, 159 | "XP": 100000, 160 | "LoadPosition": "POSITION_STANDING", 161 | "DefaultPosition": "POSITION_STANDING", 162 | "Gender": "Neutral" 163 | }, 164 | { 165 | "Number": 11605, 166 | "Aliases": [ 167 | "patient" 168 | ], 169 | "ShortDesc": "a Patient at the county hospital", 170 | "LongDesc": "A patient at the county hospital is sleeping here.\n", 171 | "DetailedDesc": "This patient is very tired and appears to need the sleep they are getting.\nPlease be quiet and let them sleep.\n", 172 | "Actions": [ 173 | "ISNPC", 174 | "AGGR_EVIL", 175 | "MEMORY", 176 | "NOCHARM", 177 | "NOSLEEP" 178 | ], 179 | "Affections": [ 180 | "BLIND", 181 | "SANCTUARY" 182 | ], 183 | "Alignment": 1000, 184 | "Level": 41, 185 | "THAC0": 25, 186 | "AC": -2, 187 | "HP": "10d10+500", 188 | "Damage": "3d4+20", 189 | "Gold": 5000, 190 | "XP": 20000, 191 | "LoadPosition": "POSITION_SLEEPING", 192 | "DefaultPosition": "POSITION_SLEEPING", 193 | "Gender": "Neutral" 194 | } 195 | ] 196 | } -------------------------------------------------------------------------------- /data/mobs/142.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 14297, 5 | "Aliases": [ 6 | "king", 7 | "rancoth" 8 | ], 9 | "ShortDesc": "king Rancoth", 10 | "LongDesc": "The king of Graven is here, damn he's ugly\n", 11 | "DetailedDesc": "", 12 | "Actions": [ 13 | "SPEC", 14 | "SENTINEL", 15 | "ISNPC", 16 | "AWARE", 17 | "MEMORY", 18 | "NOCHARM", 19 | "NOSUMMON", 20 | "NOSLEEP", 21 | "NOBASH", 22 | "NOBLIND" 23 | ], 24 | "Affections": [ 25 | "DETECT_INVIS", 26 | "SENSE_LIFE", 27 | "SANCTUARY", 28 | "INFRAVISION" 29 | ], 30 | "Alignment": 0, 31 | "Level": 175, 32 | "THAC0": 76, 33 | "AC": -32, 34 | "HP": "100d100+28000", 35 | "Damage": "10d15+80", 36 | "Gold": 1000000, 37 | "XP": 1000000, 38 | "LoadPosition": "POSITION_STANDING", 39 | "DefaultPosition": "POSITION_STANDING", 40 | "Gender": "Male" 41 | }, 42 | { 43 | "Number": 14298, 44 | "Aliases": [ 45 | "green", 46 | "gelatinous", 47 | "blob" 48 | ], 49 | "ShortDesc": "the green gelatinous blob", 50 | "LongDesc": "An oozing green gelatinous blob is here, sucking in bits of debris.\n", 51 | "DetailedDesc": "A horrid looking thing; it's huge, greenish, and looks like the blob.\n", 52 | "Actions": [ 53 | "SPEC", 54 | "SENTINEL", 55 | "ISNPC", 56 | "WIMPY", 57 | "MEMORY", 58 | "HELPER" 59 | ], 60 | "Affections": [ 61 | "DETECT_INVIS" 62 | ], 63 | "Alignment": 0, 64 | "Level": 12, 65 | "THAC0": 75, 66 | "AC": -17, 67 | "HP": "2d5+10100", 68 | "Damage": "2d4+25", 69 | "Gold": 1000, 70 | "XP": 11000, 71 | "LoadPosition": "POSITION_STANDING", 72 | "DefaultPosition": "POSITION_STANDING", 73 | "Gender": "Neutral" 74 | }, 75 | { 76 | "Number": 14299, 77 | "Aliases": [ 78 | "a", 79 | "BLUE", 80 | "blob" 81 | ], 82 | "ShortDesc": "a huge BLUE blob", 83 | "LongDesc": "A BLUE gelatinous blob is here watching it's followers suck in bits of\ndebris.\n", 84 | "DetailedDesc": "A huge Blue gelantenous blob is about all you can describe it as. This one\nappears to be very intelligent (as far as blobs go). Hmm. Perhaps not a real\ngood idea to attack it.\n", 85 | "Actions": [ 86 | "SPEC", 87 | "ISNPC", 88 | "AWARE", 89 | "MEMORY", 90 | "NOSUMMON", 91 | "NOBLIND" 92 | ], 93 | "Affections": [ 94 | "DETECT_INVIS", 95 | "SENSE_LIFE", 96 | "SANCTUARY", 97 | "PROTECT_GOOD", 98 | "NOTRACK" 99 | ], 100 | "Alignment": 0, 101 | "Level": 150, 102 | "THAC0": 75, 103 | "AC": -25, 104 | "HP": "100d100+25000", 105 | "Damage": "8d10+5", 106 | "Gold": 0, 107 | "XP": 500000, 108 | "LoadPosition": "POSITION_STANDING", 109 | "DefaultPosition": "POSITION_STANDING", 110 | "Gender": "Neutral" 111 | } 112 | ] 113 | } -------------------------------------------------------------------------------- /data/mobs/162.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 16200, 5 | "Aliases": [ 6 | "Elyas", 7 | "Machera" 8 | ], 9 | "ShortDesc": "Elyas Machera", 10 | "LongDesc": "A lean man tends a camp fire here.\n", 11 | "DetailedDesc": "You see a lean sunbrowned man with long graying brown hair hung to his waist\nand gathered at the back of his neck with a cord. A thick brown beard lays\nacross his chest like a carpet. His eyes glow bright yellow, like a piece of\ngold in the sunlight. As you near him, you see a wolf cuts you off. Its\nobvious that this man has some power over the wolfs of the forest.\n", 12 | "Actions": [ 13 | "SPEC", 14 | "SENTINEL", 15 | "ISNPC" 16 | ], 17 | "Affections": [ 18 | "SANCTUARY", 19 | "INFRAVISION", 20 | "PROTECT_EVIL" 21 | ], 22 | "Alignment": 600, 23 | "Level": 100, 24 | "THAC0": 25, 25 | "AC": 0, 26 | "HP": "15d25+3000", 27 | "Damage": "1d10+25", 28 | "Gold": 1000, 29 | "XP": 260000, 30 | "LoadPosition": "POSITION_STANDING", 31 | "DefaultPosition": "POSITION_STANDING", 32 | "Gender": "Male" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /data/mobs/163.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/181.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/197.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/238.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 23801, 5 | "Aliases": [ 6 | "Skeleton", 7 | "T-Rex", 8 | "Dinosaur", 9 | "Lich" 10 | ], 11 | "ShortDesc": "a HUGE skeletal T-Rex", 12 | "LongDesc": "An ENOURMOUS skeletal T-Rex stands here waiting for lunch.\n", 13 | "DetailedDesc": "As you look at what appears to be a huge pile of bones it begins to move.\nYou cant help wondering how something this huge and intimidating was slain. \nAs you sit there and ponder the skeletal figure comes to life and attacks.\nWith quicksilver speed, your life flashes in front of your eyes as you \nrealize you are going to die!\n", 14 | "Actions": [ 15 | "SPEC", 16 | "SENTINEL", 17 | "ISNPC", 18 | "AWARE", 19 | "STAY_ZONE", 20 | "AGGR_GOOD", 21 | "AGGR_NEUTRAL", 22 | "NOCHARM", 23 | "NOSLEEP" 24 | ], 25 | "Affections": [ 26 | "DETECT_INVIS", 27 | "SENSE_LIFE", 28 | "SANCTUARY", 29 | "INFRAVISION", 30 | "UNUSED20" 31 | ], 32 | "Alignment": -990, 33 | "Level": 200, 34 | "THAC0": 90, 35 | "AC": -34, 36 | "HP": "10d10+10000", 37 | "Damage": "18d9+60", 38 | "Gold": 20000, 39 | "XP": 100000, 40 | "LoadPosition": "POSITION_STANDING", 41 | "DefaultPosition": "POSITION_STANDING", 42 | "Gender": "Neutral" 43 | }, 44 | { 45 | "Number": 23802, 46 | "Aliases": [ 47 | "lizard", 48 | "small", 49 | "mean" 50 | ], 51 | "ShortDesc": "lizard damage nuff said", 52 | "LongDesc": "A Spitting Lizard stands here ready to ruin your new clothes.\n", 53 | "DetailedDesc": "", 54 | "Actions": [ 55 | "ISNPC", 56 | "AGGRESSIVE", 57 | "HELPER", 58 | "NOSLEEP" 59 | ], 60 | "Affections": [], 61 | "Alignment": 0, 62 | "Level": 51, 63 | "THAC0": 50, 64 | "AC": -1, 65 | "HP": "5d5+1150", 66 | "Damage": "3d5+10", 67 | "Gold": 100, 68 | "XP": 100000, 69 | "LoadPosition": "POSITION_STANDING", 70 | "DefaultPosition": "POSITION_STANDING", 71 | "Gender": "Neutral" 72 | }, 73 | { 74 | "Number": 23803, 75 | "Aliases": [ 76 | "pter", 77 | "pteradactyl", 78 | "dinosaur", 79 | "skeleton", 80 | "lich" 81 | ], 82 | "ShortDesc": "Pteradactyl", 83 | "LongDesc": "A Skeletal Pteradactyl stands here.\n", 84 | "DetailedDesc": "A skeletal pteradactyl swoops down from the ceiling to attack you! You \nwonder if the terrible creaking of those leathery wings is the last thing\nYou'll ever hear. Probably!\n", 85 | "Actions": [ 86 | "SPEC", 87 | "SENTINEL", 88 | "ISNPC", 89 | "AWARE", 90 | "HELPER", 91 | "NOSLEEP" 92 | ], 93 | "Affections": [ 94 | "DETECT_INVIS", 95 | "SANCTUARY" 96 | ], 97 | "Alignment": -500, 98 | "Level": 120, 99 | "THAC0": 80, 100 | "AC": -30, 101 | "HP": "20d10+14000", 102 | "Damage": "10d10+55", 103 | "Gold": 25000, 104 | "XP": 100000, 105 | "LoadPosition": "POSITION_STANDING", 106 | "DefaultPosition": "POSITION_STANDING", 107 | "Gender": "Neutral" 108 | }, 109 | { 110 | "Number": 23804, 111 | "Aliases": [ 112 | "Stegosaurus", 113 | "skeleton", 114 | "lich", 115 | "steg" 116 | ], 117 | "ShortDesc": "Stegosaurus", 118 | "LongDesc": "The Skeletal Stegosaurus lashes its tail at you with full force!\n", 119 | "DetailedDesc": "This large beast has spikes all over its body. From the looks of them\nyou would think he doesnt have many enemies.\n", 120 | "Actions": [ 121 | "SPEC", 122 | "SENTINEL", 123 | "ISNPC", 124 | "AWARE", 125 | "AGGRESSIVE", 126 | "MEMORY", 127 | "NOSLEEP" 128 | ], 129 | "Affections": [ 130 | "SANCTUARY" 131 | ], 132 | "Alignment": 50, 133 | "Level": 200, 134 | "THAC0": 90, 135 | "AC": -36, 136 | "HP": "20d10+14000", 137 | "Damage": "18d9+75", 138 | "Gold": 100000, 139 | "XP": 150000, 140 | "LoadPosition": "POSITION_STANDING", 141 | "DefaultPosition": "POSITION_STANDING", 142 | "Gender": "Neutral" 143 | }, 144 | { 145 | "Number": 23805, 146 | "Aliases": [ 147 | "Raptor", 148 | "lich", 149 | "skeleton" 150 | ], 151 | "ShortDesc": "a Raptor", 152 | "LongDesc": "The Skeletal Raptor lays here in ambush for someone.\n", 153 | "DetailedDesc": "Even the mere skeleton of this fearsome hunter is impressive. You \ncan't help staring at it. What was that? Did you just hear something\nbehind you? TOO LATE BUDDY, your gunna die!\n", 154 | "Actions": [ 155 | "SPEC", 156 | "SENTINEL", 157 | "ISNPC", 158 | "AWARE", 159 | "HELPER", 160 | "NOSLEEP" 161 | ], 162 | "Affections": [ 163 | "DETECT_INVIS", 164 | "SANCTUARY" 165 | ], 166 | "Alignment": 0, 167 | "Level": 120, 168 | "THAC0": 60, 169 | "AC": -8, 170 | "HP": "10d10+10000", 171 | "Damage": "8d7+38", 172 | "Gold": 50000, 173 | "XP": 90000, 174 | "LoadPosition": "POSITION_STANDING", 175 | "DefaultPosition": "POSITION_STANDING", 176 | "Gender": "Neutral" 177 | }, 178 | { 179 | "Number": 23806, 180 | "Aliases": [ 181 | "Raptor", 182 | "dino", 183 | "dinosaur" 184 | ], 185 | "ShortDesc": "the sneaky one", 186 | "LongDesc": "A Raptor hides behind the bushes here, WATCH OUT!\n", 187 | "DetailedDesc": "This is the one that'll get ya. While you stare at his counterpart\nthis little fella is sneaking up to attack from behind!\n", 188 | "Actions": [ 189 | "SPEC", 190 | "SENTINEL", 191 | "ISNPC", 192 | "AWARE", 193 | "AGGRESSIVE", 194 | "HELPER", 195 | "NOSLEEP" 196 | ], 197 | "Affections": [ 198 | "SANCTUARY", 199 | "HIDE" 200 | ], 201 | "Alignment": -700, 202 | "Level": 135, 203 | "THAC0": 87, 204 | "AC": -30, 205 | "HP": "20d10+7000", 206 | "Damage": "10d10+70", 207 | "Gold": 35000, 208 | "XP": 50000, 209 | "LoadPosition": "POSITION_STANDING", 210 | "DefaultPosition": "POSITION_STANDING", 211 | "Gender": "Neutral" 212 | }, 213 | { 214 | "Number": 23807, 215 | "Aliases": [ 216 | "lich", 217 | "skeleton", 218 | "triceratops", 219 | "dinosaur", 220 | "dino" 221 | ], 222 | "ShortDesc": "Triceratops", 223 | "LongDesc": "The Skeletal Triceratops stands here encased in its calcium armor.\n", 224 | "DetailedDesc": "This looks like one beast you would not want to mess with. The Triceratops\ndoes not need offensive claws or teeth, this relatively docile beast is so\nencased in bone armor that it would be nearly impossible to hurt it.\n", 225 | "Actions": [ 226 | "SPEC", 227 | "SENTINEL", 228 | "SCAVENGER", 229 | "ISNPC", 230 | "AWARE" 231 | ], 232 | "Affections": [ 233 | "SANCTUARY" 234 | ], 235 | "Alignment": 0, 236 | "Level": 127, 237 | "THAC0": 76, 238 | "AC": -28, 239 | "HP": "10d7+7500", 240 | "Damage": "12d10+25", 241 | "Gold": 90000, 242 | "XP": 150000, 243 | "LoadPosition": "POSITION_STANDING", 244 | "DefaultPosition": "POSITION_STANDING", 245 | "Gender": "Neutral" 246 | } 247 | ] 248 | } -------------------------------------------------------------------------------- /data/mobs/25.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 2500, 5 | "Aliases": [ 6 | "barrel" 7 | ], 8 | "ShortDesc": "a barrel", 9 | "LongDesc": "A large barrel rests on the ground.\n", 10 | "DetailedDesc": "It's a barrel. It's large. Get the picture?\n", 11 | "Actions": [ 12 | "SENTINEL", 13 | "ISNPC", 14 | "NOCHARM", 15 | "NOSLEEP", 16 | "NOBLIND" 17 | ], 18 | "Affections": [], 19 | "Alignment": 0, 20 | "Level": 0, 21 | "THAC0": 0, 22 | "AC": 10, 23 | "HP": "0d0+1", 24 | "Damage": "0d0+0", 25 | "Gold": 0, 26 | "XP": 0, 27 | "LoadPosition": "POSITION_STANDING", 28 | "DefaultPosition": "POSITION_STANDING", 29 | "Gender": "Neutral" 30 | }, 31 | { 32 | "Number": 2501, 33 | "Aliases": [ 34 | "zombie" 35 | ], 36 | "ShortDesc": "a zombie", 37 | "LongDesc": "A zombie shambles towards you, its arms out-stretched.\n", 38 | "DetailedDesc": "This thing is a walking corpse; pieces are falling off everywhere.\n", 39 | "Actions": [ 40 | "SPEC", 41 | "ISNPC" 42 | ], 43 | "Affections": [ 44 | "DETECT_INVIS", 45 | "SENSE_LIFE" 46 | ], 47 | "Alignment": 0, 48 | "Level": 28, 49 | "THAC0": 4, 50 | "AC": 0, 51 | "HP": "100d4+1", 52 | "Damage": "2d3+5", 53 | "Gold": 65, 54 | "XP": 16000, 55 | "LoadPosition": "POSITION_STANDING", 56 | "DefaultPosition": "POSITION_STANDING", 57 | "Gender": "Neutral" 58 | }, 59 | { 60 | "Number": 2502, 61 | "Aliases": [ 62 | "skeleton" 63 | ], 64 | "ShortDesc": "a skeleton", 65 | "LongDesc": "A skeleton wielding an axe is coming right at you.\n", 66 | "DetailedDesc": "White bones, big axe. 'nuff said.\n", 67 | "Actions": [ 68 | "SPEC", 69 | "ISNPC" 70 | ], 71 | "Affections": [ 72 | "DETECT_INVIS", 73 | "SENSE_LIFE" 74 | ], 75 | "Alignment": 0, 76 | "Level": 20, 77 | "THAC0": 9, 78 | "AC": 10, 79 | "HP": "80d4+1", 80 | "Damage": "2d6+5", 81 | "Gold": 65, 82 | "XP": 16000, 83 | "LoadPosition": "POSITION_STANDING", 84 | "DefaultPosition": "POSITION_STANDING", 85 | "Gender": "Neutral" 86 | }, 87 | { 88 | "Number": 2503, 89 | "Aliases": [ 90 | "scavenger" 91 | ], 92 | "ShortDesc": "a scavenger", 93 | "LongDesc": "A small scuttling scavenger comes over to gnaw on your leg.\n", 94 | "DetailedDesc": "This thing is all red, running on two legs hunched over. It scuttles around\nfrom one place to the next, attacking and then running away. It's large teeth\nlike to rip through flesh and bone.\n", 95 | "Actions": [ 96 | "SPEC", 97 | "ISNPC", 98 | "STAY_ZONE" 99 | ], 100 | "Affections": [], 101 | "Alignment": 0, 102 | "Level": 20, 103 | "THAC0": 8, 104 | "AC": 3, 105 | "HP": "1d100+150", 106 | "Damage": "5d3+10", 107 | "Gold": 75, 108 | "XP": 14000, 109 | "LoadPosition": "POSITION_STANDING", 110 | "DefaultPosition": "POSITION_STANDING", 111 | "Gender": "Neutral" 112 | }, 113 | { 114 | "Number": 2504, 115 | "Aliases": [ 116 | "goblin", 117 | "fallen", 118 | "one" 119 | ], 120 | "ShortDesc": "a Fallen One", 121 | "LongDesc": "A strange little goblin like creature comes up and attacks you.\n", 122 | "DetailedDesc": "This little creature looks sort of like a goblin. It is wielding a spear and\nthrusts it at you menacingly. Its body is barely covered by the rags it\nwears, and it bares its teeth at you as it attacks.\n", 123 | "Actions": [ 124 | "SPEC", 125 | "ISNPC", 126 | "STAY_ZONE", 127 | "WIMPY" 128 | ], 129 | "Affections": [], 130 | "Alignment": 0, 131 | "Level": 20, 132 | "THAC0": 6, 133 | "AC": 3, 134 | "HP": "1d100+150", 135 | "Damage": "4d3+5", 136 | "Gold": 75, 137 | "XP": 14000, 138 | "LoadPosition": "POSITION_STANDING", 139 | "DefaultPosition": "POSITION_STANDING", 140 | "Gender": "Neutral" 141 | } 142 | ] 143 | } -------------------------------------------------------------------------------- /data/mobs/258.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/259.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/26.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/260.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/261.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/262.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/27.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/270.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/271.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/272.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/273.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/33.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 3300, 5 | "Aliases": [ 6 | "Diablo", 7 | "devil", 8 | "demon" 9 | ], 10 | "ShortDesc": "Diablo", 11 | "LongDesc": "Diablo grins at you evilly and walks slowly towards you.\n", 12 | "DetailedDesc": "Diablo is the essence of evil in this world. He is about 10 feet tall,\ncovered in red scales with a spikes protruding from his back and arms. Ram's\nhorns grow from his head, and his hands end in wicked looking claws. His\nmuscles bulge under the thick armor of his skin. The room warms noticibly as\nhe approaches, and you wonder how anyone could stand to be near him for long\nenough to kill him.\n", 13 | "Actions": [ 14 | "SPEC", 15 | "SENTINEL", 16 | "ISNPC", 17 | "AWARE", 18 | "AGGRESSIVE", 19 | "STAY_ZONE", 20 | "AGGR_EVIL", 21 | "AGGR_GOOD", 22 | "AGGR_NEUTRAL", 23 | "MEMORY", 24 | "NOSLEEP", 25 | "NOBASH" 26 | ], 27 | "Affections": [ 28 | "DETECT_INVIS", 29 | "SENSE_LIFE", 30 | "NOTRACK" 31 | ], 32 | "Alignment": -1000, 33 | "Level": 200, 34 | "THAC0": 86, 35 | "AC": -39, 36 | "HP": "100d20+30000", 37 | "Damage": "20d20+127", 38 | "Gold": 1000000, 39 | "XP": 1000000, 40 | "LoadPosition": "POSITION_STANDING", 41 | "DefaultPosition": "POSITION_STANDING", 42 | "Gender": "Neutral" 43 | }, 44 | { 45 | "Number": 3301, 46 | "Aliases": [ 47 | "knight", 48 | "blood" 49 | ], 50 | "ShortDesc": "a Blood Knight", 51 | "LongDesc": "A Blood Knight charges you and attacks!\n", 52 | "DetailedDesc": "This knight is about 7 feet tall and wearing a complete set of armor.\nHe has a huge sword which he wields in one hand, and advances on you\nmenacingly.\n", 53 | "Actions": [ 54 | "ISNPC" 55 | ], 56 | "Affections": [], 57 | "Alignment": 0, 58 | "Level": 127, 59 | "THAC0": 73, 60 | "AC": -36, 61 | "HP": "0d0+6000", 62 | "Damage": "12d12+80", 63 | "Gold": 0, 64 | "XP": 0, 65 | "LoadPosition": "POSITION_STANDING", 66 | "DefaultPosition": "POSITION_STANDING", 67 | "Gender": "Neutral" 68 | }, 69 | { 70 | "Number": 3302, 71 | "Aliases": [ 72 | "advocate", 73 | "mage" 74 | ], 75 | "ShortDesc": "an Advocate", 76 | "LongDesc": "An Advocate points it's staff at you and lets loose a ball of magic.\n", 77 | "DetailedDesc": "This mage is wearing grey robes and holds a long staff in his right\nhand. His head looks like a skull with horns, and the evil grin never\nleaves it's face. This thing fairly emanates evil.\n", 78 | "Actions": [ 79 | "ISNPC" 80 | ], 81 | "Affections": [], 82 | "Alignment": 0, 83 | "Level": 127, 84 | "THAC0": 35, 85 | "AC": -20, 86 | "HP": "0d0+6000", 87 | "Damage": "8d8+40", 88 | "Gold": 100000, 89 | "XP": 100000, 90 | "LoadPosition": "POSITION_STANDING", 91 | "DefaultPosition": "POSITION_STANDING", 92 | "Gender": "Neutral" 93 | }, 94 | { 95 | "Number": 3303, 96 | "Aliases": [ 97 | "drake", 98 | "azure", 99 | "snake" 100 | ], 101 | "ShortDesc": "an Azure Drake", 102 | "LongDesc": "A huge snake-like being attacks you!\n", 103 | "DetailedDesc": "This thing seems to be a huge snake with four arms and horns growing\nout of its head. It is about 10 feet tall, and towers above you as\nit rears back to attack you.\n", 104 | "Actions": [ 105 | "ISNPC" 106 | ], 107 | "Affections": [], 108 | "Alignment": 0, 109 | "Level": 127, 110 | "THAC0": 73, 111 | "AC": -35, 112 | "HP": "0d0+6000", 113 | "Damage": "10d10+60", 114 | "Gold": 0, 115 | "XP": 0, 116 | "LoadPosition": "POSITION_STANDING", 117 | "DefaultPosition": "POSITION_STANDING", 118 | "Gender": "Neutral" 119 | } 120 | ] 121 | } -------------------------------------------------------------------------------- /data/mobs/34.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 3400, 5 | "Aliases": [ 6 | "monica", 7 | "lewinsky", 8 | "girl" 9 | ], 10 | "ShortDesc": "Monica Lewinsky", 11 | "LongDesc": "Monica Lewinsky is here woofing a box of rare Twinkies.\n", 12 | "DetailedDesc": "Spokesperson for Jenny Craig? I guess those marketing people know something\nwe don't. Sheaa right. This little chunk of a girl is here waiting for a\none on one meeting with the president.\n", 13 | "Actions": [ 14 | "SPEC", 15 | "SENTINEL", 16 | "ISNPC", 17 | "AWARE", 18 | "STAY_ZONE", 19 | "MEMORY", 20 | "NOCHARM", 21 | "NOSUMMON", 22 | "NOSLEEP", 23 | "NOBASH", 24 | "NOBLIND" 25 | ], 26 | "Affections": [ 27 | "DETECT_INVIS", 28 | "SENSE_LIFE", 29 | "SANCTUARY", 30 | "INFRAVISION" 31 | ], 32 | "Alignment": 0, 33 | "Level": 150, 34 | "THAC0": 71, 35 | "AC": -12, 36 | "HP": "0d0+20000", 37 | "Damage": "5d5+35", 38 | "Gold": 5000, 39 | "XP": 1, 40 | "LoadPosition": "POSITION_STANDING", 41 | "DefaultPosition": "POSITION_STANDING", 42 | "Gender": "Female" 43 | }, 44 | { 45 | "Number": 3401, 46 | "Aliases": [ 47 | "al", 48 | "gore", 49 | "vp" 50 | ], 51 | "ShortDesc": "Al Gore", 52 | "LongDesc": "Al Gore is here whispering something to Ken.\n", 53 | "DetailedDesc": "This guy's willing to anything to be president...even hang out with Ken\nStarr. You know him as the guy who invented and created the internet and who\nsaves the enviroment on a daily basis. Oh, i think he single handedly won\nWorld War II as well.\n", 54 | "Actions": [ 55 | "SPEC", 56 | "SENTINEL", 57 | "ISNPC", 58 | "AWARE", 59 | "STAY_ZONE", 60 | "MEMORY", 61 | "HELPER", 62 | "NOCHARM", 63 | "NOSUMMON", 64 | "NOSLEEP", 65 | "NOBASH", 66 | "NOBLIND" 67 | ], 68 | "Affections": [ 69 | "DETECT_INVIS", 70 | "SENSE_LIFE", 71 | "SANCTUARY", 72 | "INFRAVISION" 73 | ], 74 | "Alignment": 0, 75 | "Level": 150, 76 | "THAC0": 61, 77 | "AC": -12, 78 | "HP": "0d0+12000", 79 | "Damage": "5d5+50", 80 | "Gold": 5000, 81 | "XP": 1, 82 | "LoadPosition": "POSITION_STANDING", 83 | "DefaultPosition": "POSITION_STANDING", 84 | "Gender": "Male" 85 | }, 86 | { 87 | "Number": 3402, 88 | "Aliases": [ 89 | "ken", 90 | "starr" 91 | ], 92 | "ShortDesc": "Ken Starr", 93 | "LongDesc": "Ken Starr is here soaking in some useless info Al is feeding him.\n", 94 | "DetailedDesc": "Just when you thought you saw the last of this guy. Here he is again. He\nlooks confused as usual. And those eyes look tired behind his glasses. He\nwould do anything to take down Clinton.\n", 95 | "Actions": [ 96 | "SPEC", 97 | "SENTINEL", 98 | "ISNPC", 99 | "AWARE", 100 | "STAY_ZONE", 101 | "MEMORY", 102 | "HELPER", 103 | "NOCHARM", 104 | "NOSUMMON", 105 | "NOSLEEP", 106 | "NOBASH", 107 | "NOBLIND" 108 | ], 109 | "Affections": [ 110 | "DETECT_INVIS", 111 | "SENSE_LIFE", 112 | "SANCTUARY", 113 | "INFRAVISION" 114 | ], 115 | "Alignment": 0, 116 | "Level": 150, 117 | "THAC0": 75, 118 | "AC": -12, 119 | "HP": "0d0+12000", 120 | "Damage": "30d8+25", 121 | "Gold": 5000, 122 | "XP": 1, 123 | "LoadPosition": "POSITION_STANDING", 124 | "DefaultPosition": "POSITION_STANDING", 125 | "Gender": "Male" 126 | }, 127 | { 128 | "Number": 3403, 129 | "Aliases": [ 130 | "bill", 131 | "clinton", 132 | "president" 133 | ], 134 | "ShortDesc": "Bill Clinton", 135 | "LongDesc": "Bill Clinton is here daydreaming about....well nevermind that.\n", 136 | "DetailedDesc": "Here here is. This guy is a complete embarassment to the people of his home\nstate of Arkansas. Well ok, maybe he isn't. But he is to the rest of the\npeople in the other 49 states. Do the world a favor (especially Hillary) and\nend our suffering. =)\n", 137 | "Actions": [ 138 | "SPEC", 139 | "SENTINEL", 140 | "ISNPC", 141 | "AWARE", 142 | "MEMORY", 143 | "NOCHARM", 144 | "NOSUMMON", 145 | "NOSLEEP", 146 | "NOBASH", 147 | "NOBLIND" 148 | ], 149 | "Affections": [ 150 | "DETECT_INVIS", 151 | "SENSE_LIFE", 152 | "SANCTUARY", 153 | "INFRAVISION", 154 | "NOTRACK" 155 | ], 156 | "Alignment": -400, 157 | "Level": 200, 158 | "THAC0": 78, 159 | "AC": -23, 160 | "HP": "0d0+30000", 161 | "Damage": "12d19+70", 162 | "Gold": 1000000, 163 | "XP": 1, 164 | "LoadPosition": "POSITION_STANDING", 165 | "DefaultPosition": "POSITION_STANDING", 166 | "Gender": "Male" 167 | }, 168 | { 169 | "Number": 3404, 170 | "Aliases": [ 171 | "secret", 172 | "service", 173 | "agent" 174 | ], 175 | "ShortDesc": "A Secret Service Agent", 176 | "LongDesc": "A member of the Secret Service is here.\n", 177 | "DetailedDesc": "Dark glasses, earpiece...you know the guy. Try and make him laugh. He needs\na break from all this serious business.\n", 178 | "Actions": [ 179 | "SPEC", 180 | "SENTINEL", 181 | "ISNPC", 182 | "AWARE", 183 | "STAY_ZONE", 184 | "MEMORY", 185 | "NOCHARM", 186 | "NOSUMMON", 187 | "NOSLEEP", 188 | "NOBASH", 189 | "NOBLIND" 190 | ], 191 | "Affections": [ 192 | "INVISIBLE", 193 | "DETECT_INVIS", 194 | "SENSE_LIFE", 195 | "SANCTUARY", 196 | "INFRAVISION", 197 | "HIDE" 198 | ], 199 | "Alignment": 0, 200 | "Level": 200, 201 | "THAC0": 72, 202 | "AC": 10, 203 | "HP": "0d0+30000", 204 | "Damage": "5d5+70", 205 | "Gold": 5000, 206 | "XP": 1, 207 | "LoadPosition": "POSITION_STANDING", 208 | "DefaultPosition": "POSITION_STANDING", 209 | "Gender": "Male" 210 | } 211 | ] 212 | } -------------------------------------------------------------------------------- /data/mobs/54.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/79.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 7900, 5 | "Aliases": [ 6 | "aggressive", 7 | "test", 8 | "monster" 9 | ], 10 | "ShortDesc": "an aggressive", 11 | "LongDesc": "An aggressive monster is here, he ATTACKS!.\n", 12 | "DetailedDesc": "You see a mob of blank look, he just attacks cause that is how he is\nprogrammed.\n", 13 | "Actions": [ 14 | "SPEC", 15 | "ISNPC", 16 | "AGGRESSIVE", 17 | "WIMPY", 18 | "MEMORY", 19 | "NOCHARM", 20 | "NOSUMMON" 21 | ], 22 | "Affections": [ 23 | "SANCTUARY", 24 | "SNEAK", 25 | "HIDE" 26 | ], 27 | "Alignment": -1000, 28 | "Level": 55, 29 | "THAC0": 40, 30 | "AC": -15, 31 | "HP": "10d10+1000", 32 | "Damage": "7d7+50", 33 | "Gold": 100, 34 | "XP": 100000, 35 | "LoadPosition": "POSITION_STANDING", 36 | "DefaultPosition": "POSITION_STANDING", 37 | "Gender": "Neutral" 38 | }, 39 | { 40 | "Number": 7909, 41 | "Aliases": [ 42 | "grand", 43 | "knight", 44 | "paladin" 45 | ], 46 | "ShortDesc": "the Grand Knight of Paladins", 47 | "LongDesc": "The Grand Knight is standing here, waiting for someone to help.\n", 48 | "DetailedDesc": "The Knight is standing here, smiling at you. He is dressed all in white, blue\nand silver. He looks VERY strong, as he stands here, ready to help the\ninnocent.\n", 49 | "Actions": [ 50 | "SPEC", 51 | "SENTINEL", 52 | "SCAVENGER", 53 | "ISNPC", 54 | "AGGRESSIVE", 55 | "AGGR_EVIL", 56 | "MEMORY", 57 | "HELPER" 58 | ], 59 | "Affections": [ 60 | "DETECT_INVIS", 61 | "SANCTUARY" 62 | ], 63 | "Alignment": 1000, 64 | "Level": 51, 65 | "THAC0": 20, 66 | "AC": -2, 67 | "HP": "16d8+2916", 68 | "Damage": "2d9+35", 69 | "Gold": 31570, 70 | "XP": 100000, 71 | "LoadPosition": "POSITION_STANDING", 72 | "DefaultPosition": "POSITION_STANDING", 73 | "Gender": "Male" 74 | }, 75 | { 76 | "Number": 7911, 77 | "Aliases": [ 78 | "statue" 79 | ], 80 | "ShortDesc": "a small statue of Odin", 81 | "LongDesc": "A statue of Odin is here. It seems to be focussing some energy from the sun.\n", 82 | "DetailedDesc": "You see a small statue, worn by pollution and acid rain.\n", 83 | "Actions": [ 84 | "SPEC", 85 | "SENTINEL", 86 | "SCAVENGER", 87 | "ISNPC", 88 | "MEMORY" 89 | ], 90 | "Affections": [ 91 | "SANCTUARY" 92 | ], 93 | "Alignment": 1000, 94 | "Level": 55, 95 | "THAC0": 40, 96 | "AC": -15, 97 | "HP": "1d100+1000", 98 | "Damage": "20d2+100", 99 | "Gold": 0, 100 | "XP": 10000, 101 | "LoadPosition": "POSITION_STANDING", 102 | "DefaultPosition": "POSITION_STANDING", 103 | "Gender": "Male" 104 | }, 105 | { 106 | "Number": 7912, 107 | "Aliases": [ 108 | "lady", 109 | "redferne" 110 | ], 111 | "ShortDesc": "Lady Redferne", 112 | "LongDesc": "Lady Redferne, follower of her Lord, is sleeping here.\n", 113 | "DetailedDesc": "You see a young woman, with sun in her eyes, and love in her heart.\n", 114 | "Actions": [ 115 | "SPEC", 116 | "SENTINEL", 117 | "SCAVENGER", 118 | "ISNPC", 119 | "MEMORY" 120 | ], 121 | "Affections": [ 122 | "SANCTUARY" 123 | ], 124 | "Alignment": 1000, 125 | "Level": 100, 126 | "THAC0": 40, 127 | "AC": -15, 128 | "HP": "100d100+2000", 129 | "Damage": "20d2+40", 130 | "Gold": 250000, 131 | "XP": 100000, 132 | "LoadPosition": "POSITION_RESTING", 133 | "DefaultPosition": "POSITION_STANDING", 134 | "Gender": "Female" 135 | }, 136 | { 137 | "Number": 7913, 138 | "Aliases": [ 139 | "lord", 140 | "redferne", 141 | "lordredferne" 142 | ], 143 | "ShortDesc": "Lord Redferne", 144 | "LongDesc": "Lord Redferne, follower of Odin, is here.\n", 145 | "DetailedDesc": "You see an old wise man who is thinking of eternity.\n", 146 | "Actions": [ 147 | "SPEC", 148 | "SENTINEL", 149 | "SCAVENGER", 150 | "ISNPC", 151 | "AGGR_EVIL", 152 | "MEMORY" 153 | ], 154 | "Affections": [ 155 | "DETECT_INVIS", 156 | "SENSE_LIFE", 157 | "SANCTUARY", 158 | "INFRAVISION", 159 | "UNUSED16" 160 | ], 161 | "Alignment": 1000, 162 | "Level": 150, 163 | "THAC0": 85, 164 | "AC": -35, 165 | "HP": "1d100+17500", 166 | "Damage": "40d2+100", 167 | "Gold": 20000, 168 | "XP": 50000, 169 | "LoadPosition": "POSITION_STANDING", 170 | "DefaultPosition": "POSITION_STANDING", 171 | "Gender": "Male" 172 | } 173 | ] 174 | } -------------------------------------------------------------------------------- /data/mobs/8.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [] 3 | } -------------------------------------------------------------------------------- /data/mobs/99.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobs": [ 3 | { 4 | "Number": 9900, 5 | "Aliases": [ 6 | "trooper", 7 | "assault", 8 | "alien" 9 | ], 10 | "ShortDesc": "an alien assault trooper", 11 | "LongDesc": "An alien assault trooper is hovering high above trying to eliminate human scum!\n", 12 | "DetailedDesc": "These troopers are the first offensive wave against earth's defenses.\nThese guards are brown colored and wear blue jackets. Though moderately\narmored, some troopers have exhibited high resistance to injury.\n", 13 | "Actions": [ 14 | "SPEC", 15 | "ISNPC", 16 | "AWARE", 17 | "AGGRESSIVE", 18 | "MEMORY", 19 | "NOCHARM" 20 | ], 21 | "Affections": [ 22 | "DETECT_INVIS", 23 | "SENSE_LIFE", 24 | "INFRAVISION" 25 | ], 26 | "Alignment": 0, 27 | "Level": 200, 28 | "THAC0": 60, 29 | "AC": -30, 30 | "HP": "0d0+6000", 31 | "Damage": "10d6+50", 32 | "Gold": 250000, 33 | "XP": 0, 34 | "LoadPosition": "POSITION_STANDING", 35 | "DefaultPosition": "POSITION_STANDING", 36 | "Gender": "Neutral" 37 | }, 38 | { 39 | "Number": 9901, 40 | "Aliases": [ 41 | "captain", 42 | "assault", 43 | "alien" 44 | ], 45 | "ShortDesc": "an Assault Captain", 46 | "LongDesc": "An Assault Captain has teleported to your location.\n", 47 | "DetailedDesc": "These troopers are the first offensive wave against earth's defenses.\nThese guards are brown colored and wear blue jackets. Though moderately\narmored, some troopers have exhibited high resistance to injury.\n", 48 | "Actions": [ 49 | "SPEC", 50 | "SENTINEL", 51 | "ISNPC", 52 | "AWARE", 53 | "AGGRESSIVE", 54 | "MEMORY", 55 | "NOCHARM" 56 | ], 57 | "Affections": [ 58 | "DETECT_INVIS", 59 | "SENSE_LIFE", 60 | "SANCTUARY", 61 | "INFRAVISION" 62 | ], 63 | "Alignment": 0, 64 | "Level": 200, 65 | "THAC0": 80, 66 | "AC": -30, 67 | "HP": "0d0+18000", 68 | "Damage": "30d6+120", 69 | "Gold": 250000, 70 | "XP": 0, 71 | "LoadPosition": "POSITION_STANDING", 72 | "DefaultPosition": "POSITION_STANDING", 73 | "Gender": "Neutral" 74 | }, 75 | { 76 | "Number": 9902, 77 | "Aliases": [ 78 | "pig", 79 | "cop", 80 | "alien" 81 | ], 82 | "ShortDesc": "a Pig Cop", 83 | "LongDesc": "A Pig Cop cannot wait to blast you into oblivion.\n", 84 | "DetailedDesc": "Pig Cops are mutated alien operatives positioned to supress residual human\nopposition and to police the new centers of alien power on Earth. They\nhave extremely high intolerance to the presence of humans, and exhibit blind\nrage when they detect human scent.\n", 85 | "Actions": [ 86 | "SPEC", 87 | "SENTINEL", 88 | "ISNPC", 89 | "AWARE", 90 | "AGGRESSIVE", 91 | "MEMORY", 92 | "NOCHARM" 93 | ], 94 | "Affections": [ 95 | "DETECT_INVIS", 96 | "SENSE_LIFE", 97 | "SANCTUARY", 98 | "INFRAVISION" 99 | ], 100 | "Alignment": 0, 101 | "Level": 200, 102 | "THAC0": 85, 103 | "AC": -30, 104 | "HP": "0d0+22000", 105 | "Damage": "30d6+120", 106 | "Gold": 250000, 107 | "XP": 0, 108 | "LoadPosition": "POSITION_STANDING", 109 | "DefaultPosition": "POSITION_STANDING", 110 | "Gender": "Neutral" 111 | }, 112 | { 113 | "Number": 9903, 114 | "Aliases": [ 115 | "pig", 116 | "cop", 117 | "alien" 118 | ], 119 | "ShortDesc": "a Pig Cop", 120 | "LongDesc": "A Pig Cop cannot wait to blast you into oblivion.\n", 121 | "DetailedDesc": "Pig Cops are mutated alien operatives positioned to supress residual human\nopposition and to police the new centers of alien power on Earth. They\nhave extremely high intolerance to the presence of humans, and exhibit blind\nrage when they detect human scent.\n", 122 | "Actions": [ 123 | "SPEC", 124 | "SENTINEL", 125 | "ISNPC", 126 | "AWARE", 127 | "AGGRESSIVE", 128 | "MEMORY", 129 | "NOCHARM" 130 | ], 131 | "Affections": [ 132 | "DETECT_INVIS", 133 | "SENSE_LIFE", 134 | "SANCTUARY", 135 | "INFRAVISION" 136 | ], 137 | "Alignment": 0, 138 | "Level": 200, 139 | "THAC0": 85, 140 | "AC": -30, 141 | "HP": "0d0+25000", 142 | "Damage": "30d6+120", 143 | "Gold": 250000, 144 | "XP": 0, 145 | "LoadPosition": "POSITION_STANDING", 146 | "DefaultPosition": "POSITION_STANDING", 147 | "Gender": "Neutral" 148 | }, 149 | { 150 | "Number": 9904, 151 | "Aliases": [ 152 | "duke", 153 | "nukem" 154 | ], 155 | "ShortDesc": "Duke Nukem", 156 | "LongDesc": "Duke Nukem is here holding the key to end this insanity.\n", 157 | "DetailedDesc": "He stands about 7 feet tall and weighs about 350 pounds. With those\nbulging muscles, spiked blond hair, and shades...he loves to look at\nhimself in the mirror and tell himself how pretty he is. Sporting\nnumerous hard core weapons and an endless supply of ammo...he isn't\ngoing down easy...if at all.\n", 158 | "Actions": [ 159 | "SPEC", 160 | "SENTINEL", 161 | "ISNPC", 162 | "AWARE", 163 | "MEMORY", 164 | "NOCHARM", 165 | "NOSLEEP" 166 | ], 167 | "Affections": [ 168 | "DETECT_INVIS", 169 | "SENSE_LIFE", 170 | "SANCTUARY", 171 | "INFRAVISION" 172 | ], 173 | "Alignment": 1000, 174 | "Level": 200, 175 | "THAC0": 80, 176 | "AC": -25, 177 | "HP": "0d0+30000", 178 | "Damage": "20d15+75", 179 | "Gold": 500000, 180 | "XP": 0, 181 | "LoadPosition": "POSITION_STANDING", 182 | "DefaultPosition": "POSITION_STANDING", 183 | "Gender": "Male" 184 | }, 185 | { 186 | "Number": 9905, 187 | "Aliases": [ 188 | "redcoat", 189 | "british", 190 | "soldier", 191 | "army", 192 | "man" 193 | ], 194 | "ShortDesc": "a British Redcoat soldier", 195 | "LongDesc": "A soldier of the British Redcoat Army is here to fight you to the death. \n", 196 | "DetailedDesc": "Since 1656 the British Monarch has had a personal bodyguard, now known as the household\ndivision. One of the most enduring images of British Military History is that of the \nlong, thin line of red coated infantry, pouring out its deadly fire against whichever\nenemy troops had dared to challenge its supremacy on the field of battle. The Redcoat\nhas fought, marched and died on all five continents of the world, leaving a trail of\nbrave deeds, sacrifice tragedy and heroism unmatched by any other army in the world.\n", 197 | "Actions": [ 198 | "SPEC", 199 | "ISNPC", 200 | "AWARE", 201 | "MEMORY", 202 | "NOCHARM" 203 | ], 204 | "Affections": [ 205 | "DETECT_INVIS", 206 | "SENSE_LIFE", 207 | "INFRAVISION", 208 | "NOTRACK" 209 | ], 210 | "Alignment": -400, 211 | "Level": 200, 212 | "THAC0": 127, 213 | "AC": 20, 214 | "HP": "0d0+4000", 215 | "Damage": "1d5+5", 216 | "Gold": 50000, 217 | "XP": 100000, 218 | "LoadPosition": "POSITION_STANDING", 219 | "DefaultPosition": "POSITION_STANDING", 220 | "Gender": "Male" 221 | } 222 | ] 223 | } -------------------------------------------------------------------------------- /data/mud.toml: -------------------------------------------------------------------------------- 1 | StartRoom = 3001 # the room number where people will appear after logging in. 2 | 3 | # MainTitle defines the string people see when they first connect to your mud. 4 | MainTitle = """ 5 | 6 | .d8888b. 888 888b d888 888 888 8888888b. 7 | d88P Y88b 888 8888b d8888 888 888 888 "Y88b 8 | 888 888 888 88888b.d88888 888 888 888 888 9 | 888 888 8888b. 888 888 888Y88888P888 888 888 888 888 10 | 888 888 "88b 888 888 888 Y888P 888 888 888 888 888 11 | 888 888 888 .d888888 888 888 888 Y8P 888 888 888 888 888 12 | Y88b d88P 888 888 888 Y88b 888 888 " 888 Y88b. .d88P 888 .d88P 13 | "Y8888P" 888 "Y888888 "Y88888 888 888 "Y88888P" 8888888P" 14 | 888 15 | Y8b d88P 16 | "Y88P" 17 | 18 | 19 | """ 20 | 21 | 22 | # BcryptCost defines the encryption strength of your password hash. it also 23 | # determines how much work the computer has to do to verify passwords, so be 24 | # careful, make this too high, and people can DOS your server. 25 | BcryptCost = 10 26 | 27 | # With chatmode on, words typed into the mud are considered commands. like "look 28 | # north". With chatmode off, words typed into the mud are considered text the 29 | # character is saying. The exception is direction commands (if not followed by other 30 | # text). Other commands must be prefixed with a string so the mud knows you're typing 31 | # a command (like "/look north"). 32 | [ChatMode] 33 | # Enabled sets the global policy for usig chatmode. "require" means chatmode is 34 | # always on. "deny" means chatmode is always off. "allow" means chatmode may be 35 | # turned on or off. 36 | Enabled = "allow" # allow or deny or require 37 | Default = false # if true, chatmode defaults on. ignored except when Enabled = "allow" 38 | Prefix = "/" # the prefix to tell the mud you're typing a command. Ignored when not in chatmode. 39 | 40 | 41 | [Logging] 42 | # This configures how logs behave in ClayMUD. ClayMUD uses a 43 | # rolling/rotating log system. What that means is, once the current log 44 | # file gets too big, ClayMUD will create a new file to log to and optionally 45 | # clean up old log files. 46 | 47 | # filename controls where the log file is written. By default logs are 48 | # written to the ClayMUD data directory in a subdirectory called logs in a 49 | # file called mud.log. You may uncomment this setting to change the 50 | # default. filename = "/path/to/your/foo.log" 51 | 52 | # maxsize is the maximum size in megabytes of the logfile before it gets 53 | # rotated. 54 | maxsize = 100 55 | 56 | # maxbackups controls how many old log files are allowed to be retained. If 57 | # 0 or not specified, there's no maximum on the number of log files that are 58 | # retained. 59 | maxbackups = 3 60 | 61 | # maxage is the cutoff for deleting old log files based on their last 62 | # modified date. Note that the last modified date will almost certainly be 63 | # different than the date encoded in the log file's name. If 0 or not 64 | # specified, there's no maximum age for old log files. 65 | maxage = 365 66 | 67 | # localtime, if true, causes the log file name to use the local time for the 68 | # format. If false or not specified, UTC time will be used. 69 | localtime = true 70 | 71 | 72 | # Directions define the exits in a room and directions you can move. The order here 73 | # determines the order they'll be displayed in, in rooms. Note that direction names 74 | # and aliases take precedence over command names, so don't duplicate them. 75 | [[Direction]] 76 | Name = "North" # displayed on exits and the main command to move in that direction 77 | From = "the North" # displayed when someone enters from that direction e.g. "foo enters from the North" 78 | Aliases = ["n"] # Aliases other than the name that will move you in that direction 79 | 80 | [[Direction]] 81 | Name = "South" 82 | From = "the South" 83 | Aliases = ["s"] 84 | 85 | [[Direction]] 86 | Name = "East" 87 | From = "the East" 88 | Aliases = ["e"] 89 | 90 | [[Direction]] 91 | Name = "West" 92 | From = "the West" 93 | Aliases = ["w"] 94 | 95 | [[Direction]] 96 | Name = "Northeast" 97 | From = "the Northeast" 98 | Aliases = ["ne"] 99 | 100 | [[Direction]] 101 | Name = "Northwest" 102 | From = "the Northwest" 103 | Aliases = ["nw"] 104 | 105 | [[Direction]] 106 | Name = "Southeast" 107 | From = "the Southeast" 108 | Aliases = ["se"] 109 | 110 | [[Direction]] 111 | Name = "Southwest" 112 | From = "the Southwest" 113 | Aliases = ["sw"] 114 | 115 | [[Direction]] 116 | Name = "Up" 117 | From = "above" 118 | Aliases = ["u"] 119 | 120 | [[Direction]] 121 | Name = "Down" 122 | From = "below" 123 | Aliases = ["d"] 124 | 125 | 126 | # Genders define the genders people can choose in your game and their pronouns, used 127 | # mostly for socials. 128 | [[Gender]] 129 | Name = "male" # displayed in gender chooser 130 | Xself = "himself" # reflexive pronoun 131 | Xe = "he" # subjective pronoun 132 | Xim = "him" # objective pronound 133 | Xis = "his" # possessive pronoun 134 | 135 | [[Gender]] 136 | Name = "female" 137 | Xself = "herself" 138 | Xe = "she" 139 | Xim = "her" 140 | Xis = "hers" 141 | 142 | [[Gender]] 143 | Name = "non-binary" 144 | Xself = "themselves" 145 | Xe = "they" 146 | Xim = "them" 147 | Xis = "theirs" 148 | 149 | [[Gender]] 150 | Name = "none" 151 | Xself = "itself" 152 | Xe = "it" 153 | Xim = "it" 154 | Xis = "its" 155 | -------------------------------------------------------------------------------- /data/rooms/163.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/258.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/259.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/26.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": [ 3 | { 4 | "ID": 2550, 5 | "Zone": 26, 6 | "Name": "A room in the dungeons", 7 | "Description": "", 8 | "Bits": [], 9 | "Sector": "INSIDE", 10 | "Exits": null, 11 | "ExtraDescs": null 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /data/rooms/260.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/261.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/262.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/270.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/271.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/272.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/273.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/rooms/54.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": [ 3 | { 4 | "ID": 5400, 5 | "Zone": 54, 6 | "Name": "Connection room", 7 | "Description": "", 8 | "Bits": [], 9 | "Sector": "INSIDE", 10 | "Exits": [ 11 | { 12 | "Direction": "North", 13 | "Description": "", 14 | "Keywords": [], 15 | "DoorFlags": null, 16 | "KeyID": -1, 17 | "Destination": 5401 18 | } 19 | ], 20 | "ExtraDescs": null 21 | }, 22 | { 23 | "ID": 5401, 24 | "Zone": 54, 25 | "Name": "Connection room", 26 | "Description": "", 27 | "Bits": [], 28 | "Sector": "INSIDE", 29 | "Exits": [ 30 | { 31 | "Direction": "North", 32 | "Description": "", 33 | "Keywords": [], 34 | "DoorFlags": null, 35 | "KeyID": -1, 36 | "Destination": 5402 37 | } 38 | ], 39 | "ExtraDescs": null 40 | }, 41 | { 42 | "ID": 5402, 43 | "Zone": 54, 44 | "Name": "Start room", 45 | "Description": "", 46 | "Bits": [], 47 | "Sector": "INSIDE", 48 | "Exits": null, 49 | "ExtraDescs": null 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /data/rooms/8.json: -------------------------------------------------------------------------------- 1 | { 2 | "rooms": null 3 | } -------------------------------------------------------------------------------- /data/scripts/teleport.star: -------------------------------------------------------------------------------- 1 | actor.WriteString("You say \"shazam!\"\nA tornado rises up around you, obscuring your vision.\nAs the tornado clears, you find yourself somewhere else.\n\n") 2 | around(actor, actor.Name() + " says \"shazam!\"\nA tornado rises up around " + actor.Gender().Xim + ".\nWhen the tornado clears, " + actor.Gender().Xe + " is gone.\n") 3 | actor.Relocate(location.Exits[0].Destination) 4 | -------------------------------------------------------------------------------- /data/scripts/wind.star: -------------------------------------------------------------------------------- 1 | def do(): 2 | for name, p in location.Players.items(): 3 | if p.ID != actor.ID: 4 | p.WriteString(actor.Name() + " pushes a button on the wall.\n") 5 | p.WriteString("A cold breeze whistles through the room.\n") 6 | p.WriteString("A quiet voice whispers \"Hello " + actor.Name() + ".\"\n") 7 | 8 | actor.WriteString("The button slowly depresses.\n") 9 | do() 10 | -------------------------------------------------------------------------------- /data/socials.toml: -------------------------------------------------------------------------------- 1 | # This is an example social configuration file. For those that know what it 2 | # means, it is written in TOML. If you don't know what that means, don't worry, 3 | # it's easy to figure out. 4 | # 5 | # Each social starts with the section header [[social]]. Under that you define 6 | # the social's name. Note that names must be unique. 7 | # 8 | # Below that, there are three sub-sections, which may exist in any order. Each 9 | # section defines the behavior of the social when it is used in a particular way. 10 | # 11 | # ToSelf is what happens when you use an social targetted at yourself. 12 | # For example, "smile " 13 | # 14 | # ToNoOne is what happens when you use an social without a target. 15 | # For example "smile" 16 | # 17 | # ToOther is what happens when you use an social with a target that is not you. 18 | # For example "smile Bob". In this case, the target would be Bob. 19 | # 20 | # If you leave out a sub-section, you effectively disable using the social in 21 | # that way, and if someone tries to use the social in that way, they will get a 22 | # message like "You cannot do that." This is useful for socials that don't make 23 | # sense in certain circumstances. 24 | # 25 | # Each section has values which are templates for the text that will be output 26 | # to people in the same location as the person performing the social. 27 | # 28 | # self - The text to output to the person performing the social. 29 | # target - The text to output to the person who was the target of the social. 30 | # (only applicable in the ToOther section) 31 | # around - The text to output to everyone else in the room. 32 | # (not including the person performing the action, or the target if 33 | # there is one) 34 | # 35 | # There are a few special fields that you can put into social text, which will be 36 | # filled out with data from the game, all are enclosed in double squiggly 37 | # braces and start with a period, for example: {{.Actor.Name}}. Note that these are 38 | # case sensitive. 39 | # 40 | # Actor - The name of the person performing the social. 41 | # Target - The name of the target of the social. 42 | # (this value is only usable in the ToOther section). 43 | # Xself - "himself", "herself", or "itself" 44 | # (depending on the gender of the person performing the social) 45 | # 46 | 47 | # arrival defines what it looks like when a player is added to the world at the 48 | # starting location. It uses the social structure, but is by definition an social 49 | # ToNoOne, so you only need to define "self" and "around". 50 | [arrival] 51 | self = ".\n.\n.\n.\nYou arrive in a puff of smoke." 52 | around = "{{.Actor.Name}} arrives in a puff of smoke." 53 | 54 | [[social]] 55 | name = "smile" 56 | 57 | [social.toSelf] 58 | self = "You smile to yourself." 59 | around = "{{.Actor.Name}} smiles to {{.Actor.Gender.Xself}}." 60 | 61 | [social.toNoOne] 62 | self = "You smile." 63 | around = "{{.Actor.Name}} smiles." 64 | 65 | [social.toOther] 66 | self = "You smile at {{.Target.Name}}." 67 | target = "{{.Actor.Name}} smiles at you." 68 | around = "{{.Actor.Name}} smiles at {{.Target.Name}}." 69 | 70 | [[social]] 71 | name = "evil" 72 | 73 | [social.toSelf] 74 | self = "You grin evilly to yourself." 75 | around = "{{.Actor.Name}} grins evilly to {{.Actor.Gender.Xself}}." 76 | 77 | [social.toNoOne] 78 | self = "You grin evilly." 79 | around = "{{.Actor.Name}} grins evilly." 80 | 81 | [social.toOther] 82 | self = "You grin evilly at {{.Target.Name}}." 83 | target = "{{.Actor.Name}} grins evilly at you." 84 | around = "{{.Actor.Name}} grins evilly at {{.Target.Name}}." 85 | 86 | 87 | [[social]] 88 | name = "jump" 89 | 90 | # note there is no ToSelf section, since jumping yourself doesn't really make 91 | # sense, so if you do "jump " you will get a message like "You cannot 92 | # do that." 93 | 94 | [social.toNoOne] 95 | self = "You jump around like a crazy person." 96 | around = "{{.Actor.Name}} jumps around like a crazy person." 97 | 98 | [social.toOther] 99 | self = "You jump {{.Target.Name}}." 100 | target = "{{.Actor.Name}} jumps you." 101 | around = "{{.Actor.Name}} jumps {{.Target.Name}}." -------------------------------------------------------------------------------- /data/zones/0.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 0, 3 | "Name": "Limbo", 4 | "BottomNumber": 0, 5 | "TopNumber": 199, 6 | "Lifespan": 1, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/103.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 103, 3 | "Name": "Marvel Universe (Bloodseeker)", 4 | "BottomNumber": 10300, 5 | "TopNumber": 10499, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/105.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 105, 3 | "Name": "Ancalador", 4 | "BottomNumber": 10500, 5 | "TopNumber": 10599, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/106.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 106, 3 | "Name": "The Great Chessboard", 4 | "BottomNumber": 10600, 5 | "TopNumber": 10666, 6 | "Lifespan": 3, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/108.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 108, 3 | "Name": "Quest Zone 2", 4 | "BottomNumber": 10800, 5 | "TopNumber": 10899, 6 | "Lifespan": 0, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/11.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 11, 3 | "Name": "The Shire", 4 | "BottomNumber": 1100, 5 | "TopNumber": 1199, 6 | "Lifespan": 15, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/112.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 112, 3 | "Name": "Shannara A", 4 | "BottomNumber": 11200, 5 | "TopNumber": 11299, 6 | "Lifespan": 13, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/113.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 113, 3 | "Name": "Redwall (Matthias)", 4 | "BottomNumber": 11300, 5 | "TopNumber": 11399, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/115.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 115, 3 | "Name": "The Haunted Castle", 4 | "BottomNumber": 11500, 5 | "TopNumber": 11599, 6 | "Lifespan": 7, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/116.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 116, 3 | "Name": "Ttor 2 II (DH, Teddy)", 4 | "BottomNumber": 11600, 5 | "TopNumber": 11699, 6 | "Lifespan": 22, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/118.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 118, 3 | "Name": "Holiday Hell(Teddy/Sheol/etc)", 4 | "BottomNumber": 11800, 5 | "TopNumber": 11899, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/119.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 119, 3 | "Name": "Quest shop zone", 4 | "BottomNumber": 11900, 5 | "TopNumber": 11999, 6 | "Lifespan": 300, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/12.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 12, 3 | "Name": "God Rooms", 4 | "BottomNumber": 1200, 5 | "TopNumber": 1299, 6 | "Lifespan": 1, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/120.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 120, 3 | "Name": "Rome", 4 | "BottomNumber": 12000, 5 | "TopNumber": 12099, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/122.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 122, 3 | "Name": "Astral Plane -(Raul)", 4 | "BottomNumber": 12200, 5 | "TopNumber": 12299, 6 | "Lifespan": 33, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/127.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 127, 3 | "Name": "The Slums", 4 | "BottomNumber": 12700, 5 | "TopNumber": 12799, 6 | "Lifespan": 33, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/128.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 128, 3 | "Name": "The Bazaar (Gru)", 4 | "BottomNumber": 12800, 5 | "TopNumber": 12899, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/13.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 13, 3 | "Name": "High Tower of Sorcery", 4 | "BottomNumber": 1300, 5 | "TopNumber": 1499, 6 | "Lifespan": 33, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/130.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 130, 3 | "Name": "Magic: The Gathering (Shifter)", 4 | "BottomNumber": 13000, 5 | "TopNumber": 13099, 6 | "Lifespan": 4, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/131.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 131, 3 | "Name": "Midgaard: 2439", 4 | "BottomNumber": 13100, 5 | "TopNumber": 13199, 6 | "Lifespan": 13, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/140.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 140, 3 | "Name": "X-Men Mansion", 4 | "BottomNumber": 14000, 5 | "TopNumber": 14099, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/141.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 141, 3 | "Name": "Grimwall I (Rigbyd)", 4 | "BottomNumber": 14100, 5 | "TopNumber": 14199, 6 | "Lifespan": 10, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/142.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 142, 3 | "Name": "Grimwall II (Rigbyd)", 4 | "BottomNumber": 14200, 5 | "TopNumber": 14299, 6 | "Lifespan": 10, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/145.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 145, 3 | "Name": "Addams Family Mansion (Booof)", 4 | "BottomNumber": 14500, 5 | "TopNumber": 14599, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/146.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 146, 3 | "Name": "Addams Family II (Booof)", 4 | "BottomNumber": 14600, 5 | "TopNumber": 14699, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/15.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 15, 3 | "Name": "The God Museum (MidKnight)", 4 | "BottomNumber": 1500, 5 | "TopNumber": 1599, 6 | "Lifespan": 22, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/150.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 150, 3 | "Name": "King's Castle", 4 | "BottomNumber": 15000, 5 | "TopNumber": 15055, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/151.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 151, 3 | "Name": "The Land", 4 | "BottomNumber": 15100, 5 | "TopNumber": 15199, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/156.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 156, 3 | "Name": "Newbie Plateau of Prairie", 4 | "BottomNumber": 15600, 5 | "TopNumber": 15699, 6 | "Lifespan": 22, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/158.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 158, 3 | "Name": "Newbie Forest (Bloodseeker)", 4 | "BottomNumber": 15800, 5 | "TopNumber": 15899, 6 | "Lifespan": 5, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/16.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 16, 3 | "Name": "Xanth: paths,forest,gap(Midknight)", 4 | "BottomNumber": 1600, 5 | "TopNumber": 1699, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/160.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 160, 3 | "Name": "Gilligan's Island - Rapead", 4 | "BottomNumber": 16000, 5 | "TopNumber": 16099, 6 | "Lifespan": 29, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/161.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 161, 3 | "Name": "The Wheel Turns (HigherAnarchy)", 4 | "BottomNumber": 16100, 5 | "TopNumber": 16199, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/162.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 162, 3 | "Name": "New zone", 4 | "BottomNumber": 16200, 5 | "TopNumber": 16299, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/163.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 163, 3 | "Name": "New zone", 4 | "BottomNumber": 16300, 5 | "TopNumber": 16399, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/17.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 17, 3 | "Name": "Grymourne/Xanth", 4 | "BottomNumber": 1700, 5 | "TopNumber": 1799, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/172.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 172, 3 | "Name": "LagLand (DonGiovanni)", 4 | "BottomNumber": 17200, 5 | "TopNumber": 17299, 6 | "Lifespan": 24, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/173.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 173, 3 | "Name": "LagLand II (DonGiovanni)", 4 | "BottomNumber": 17300, 5 | "TopNumber": 17399, 6 | "Lifespan": 24, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/179.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 179, 3 | "Name": "Dragonlance (Sarnoth)", 4 | "BottomNumber": 17900, 5 | "TopNumber": 18099, 6 | "Lifespan": 38, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/18.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 18, 3 | "Name": "Hitchhiker (Arthur)", 4 | "BottomNumber": 1800, 5 | "TopNumber": 1899, 6 | "Lifespan": 21, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/181.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 181, 3 | "Name": "Dragonlance (Sarnoth)", 4 | "BottomNumber": 18100, 5 | "TopNumber": 18199, 6 | "Lifespan": 8, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/183.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 183, 3 | "Name": "Durrany's Vale MK", 4 | "BottomNumber": 18300, 5 | "TopNumber": 18399, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/184.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 184, 3 | "Name": "Green Reaches MK", 4 | "BottomNumber": 18400, 5 | "TopNumber": 18499, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/185.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 185, 3 | "Name": "Midkemia", 4 | "BottomNumber": 18500, 5 | "TopNumber": 18599, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/187.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 187, 3 | "Name": "Endless Sea (Bloodseeker)", 4 | "BottomNumber": 18700, 5 | "TopNumber": 18799, 6 | "Lifespan": 44, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/189.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 189, 3 | "Name": "Midkemia", 4 | "BottomNumber": 18900, 5 | "TopNumber": 18999, 6 | "Lifespan": 0, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/19.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 19, 3 | "Name": "The Blair Witch Project (Hysteria)", 4 | "BottomNumber": 1900, 5 | "TopNumber": 1999, 6 | "Lifespan": 120, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/190.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 190, 3 | "Name": "BundyLand! (Xorin)", 4 | "BottomNumber": 19000, 5 | "TopNumber": 19099, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/196.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 196, 3 | "Name": "Final Fantasy (Azuth)", 4 | "BottomNumber": 19600, 5 | "TopNumber": 19699, 6 | "Lifespan": 3, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/197.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 197, 3 | "Name": "Final Fantasy (Azuth)", 4 | "BottomNumber": 19700, 5 | "TopNumber": 19799, 6 | "Lifespan": 3, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 2, 3 | "Name": "Mortal Kombat", 4 | "BottomNumber": 200, 5 | "TopNumber": 299, 6 | "Lifespan": 39, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/20.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 20, 3 | "Name": "Camelot (Lazarus)", 4 | "BottomNumber": 2000, 5 | "TopNumber": 2199, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/203.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 203, 3 | "Name": "Raiders of the lost Fridge", 4 | "BottomNumber": 20300, 5 | "TopNumber": 20399, 6 | "Lifespan": 16, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/205.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 205, 3 | "Name": "Zoo Tv", 4 | "BottomNumber": 20500, 5 | "TopNumber": 20599, 6 | "Lifespan": 14, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/206.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 206, 3 | "Name": "The MTV Zone", 4 | "BottomNumber": 20600, 5 | "TopNumber": 20699, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/208.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 208, 3 | "Name": "Quest Zone (Sheol)", 4 | "BottomNumber": 20800, 5 | "TopNumber": 20899, 6 | "Lifespan": 5, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/210.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 210, 3 | "Name": "New Sparta", 4 | "BottomNumber": 21000, 5 | "TopNumber": 21199, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/217.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 217, 3 | "Name": "Giant Caves (Dyrewulf)", 4 | "BottomNumber": 21700, 5 | "TopNumber": 21799, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/22.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 22, 3 | "Name": "Draconia", 4 | "BottomNumber": 2200, 5 | "TopNumber": 2299, 6 | "Lifespan": 41, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/221.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 221, 3 | "Name": "Assassin's Arena", 4 | "BottomNumber": 21900, 5 | "TopNumber": 22199, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/222.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 222, 3 | "Name": "Acme Acres/Animaniacs/Anime BigH", 4 | "BottomNumber": 22200, 5 | "TopNumber": 22299, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/223.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 223, 3 | "Name": "Caddyshack", 4 | "BottomNumber": 22300, 5 | "TopNumber": 22399, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/225.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 225, 3 | "Name": "City of Agrabah", 4 | "BottomNumber": 22500, 5 | "TopNumber": 22599, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/226.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 226, 3 | "Name": "Palace of Agrabah", 4 | "BottomNumber": 22600, 5 | "TopNumber": 22699, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/227.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 227, 3 | "Name": "Skavenblight (Gru)", 4 | "BottomNumber": 22700, 5 | "TopNumber": 22899, 6 | "Lifespan": 90, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/23.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 23, 3 | "Name": "The Keep of Mahn-Tor", 4 | "BottomNumber": 2300, 5 | "TopNumber": 2399, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/230.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 230, 3 | "Name": "Ruins of Ozymandias (Xorin)", 4 | "BottomNumber": 23000, 5 | "TopNumber": 23099, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/235.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 235, 3 | "Name": "Frost Giants (Bloodseeker)", 4 | "BottomNumber": 23500, 5 | "TopNumber": 23599, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/237.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 237, 3 | "Name": "Jurassic Park (Sandtiger, Rand)", 4 | "BottomNumber": 23700, 5 | "TopNumber": 23799, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/238.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 238, 3 | "Name": "JP Visitor's Center (Lapo)", 4 | "BottomNumber": 23800, 5 | "TopNumber": 23899, 6 | "Lifespan": 248, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/242.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 242, 3 | "Name": "Shaolin Temple (Bloodseeker)", 4 | "BottomNumber": 24200, 5 | "TopNumber": 24299, 6 | "Lifespan": 240, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/243.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 243, 3 | "Name": "The X-Files (Bloodseeker)", 4 | "BottomNumber": 24300, 5 | "TopNumber": 24399, 6 | "Lifespan": 23, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/245.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 245, 3 | "Name": "Star Wars", 4 | "BottomNumber": 24500, 5 | "TopNumber": 24699, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/247.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 247, 3 | "Name": "Mall of Death (Velvet \u0026 Zed)", 4 | "BottomNumber": 24700, 5 | "TopNumber": 24799, 6 | "Lifespan": 0, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/248.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 248, 3 | "Name": "Hollywood (Darkheart)", 4 | "BottomNumber": 24800, 5 | "TopNumber": 24999, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/25.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 25, 3 | "Name": "Diablo Lvl 1 (Dungeons)", 4 | "BottomNumber": 2500, 5 | "TopNumber": 2549, 6 | "Lifespan": 16, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/250.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 250, 3 | "Name": "Warhammer 40k (Arken \u0026 Xorin)", 4 | "BottomNumber": 25000, 5 | "TopNumber": 25099, 6 | "Lifespan": 38, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/251.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 251, 3 | "Name": "Space Hulk (Xorin)", 4 | "BottomNumber": 25100, 5 | "TopNumber": 25199, 6 | "Lifespan": 0, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/255.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 255, 3 | "Name": "Death Star (Wimpy/Sargon)", 4 | "BottomNumber": 25500, 5 | "TopNumber": 25599, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/256.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 256, 3 | "Name": "Grimwall", 4 | "BottomNumber": 25600, 5 | "TopNumber": 25609, 6 | "Lifespan": 10, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/258.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 258, 3 | "Name": "New zone", 4 | "BottomNumber": 25620, 5 | "TopNumber": 25629, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/259.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 259, 3 | "Name": "New zone", 4 | "BottomNumber": 25630, 5 | "TopNumber": 25639, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/26.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 26, 3 | "Name": "Diablo Lvl 3 (Caves)", 4 | "BottomNumber": 2550, 5 | "TopNumber": 2599, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/260.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 260, 3 | "Name": "New zone", 4 | "BottomNumber": 25640, 5 | "TopNumber": 25649, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/261.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 261, 3 | "Name": "New zone", 4 | "BottomNumber": 25650, 5 | "TopNumber": 25659, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/262.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 262, 3 | "Name": "New zone", 4 | "BottomNumber": 25660, 5 | "TopNumber": 25669, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/27.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 27, 3 | "Name": "Diablo Lvl 2 (Catacombs)", 4 | "BottomNumber": 2600, 5 | "TopNumber": 2649, 6 | "Lifespan": 16, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/270.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 270, 3 | "Name": "New zone", 4 | "BottomNumber": 25740, 5 | "TopNumber": 25749, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/271.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 271, 3 | "Name": "New zone", 4 | "BottomNumber": 25750, 5 | "TopNumber": 25759, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/272.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 272, 3 | "Name": "New zone", 4 | "BottomNumber": 25760, 5 | "TopNumber": 25769, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/273.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 273, 3 | "Name": "New zone", 4 | "BottomNumber": 25770, 5 | "TopNumber": 25779, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/29.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 29, 3 | "Name": "Diablo Town (MidKnight)", 4 | "BottomNumber": 2900, 5 | "TopNumber": 2950, 6 | "Lifespan": 24, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 3, 3 | "Name": "DC Comics (Hysteria, Asita)", 4 | "BottomNumber": 300, 5 | "TopNumber": 499, 6 | "Lifespan": 45, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/30.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 30, 3 | "Name": "Northern Midgaard Main City", 4 | "BottomNumber": 2951, 5 | "TopNumber": 3099, 6 | "Lifespan": 15, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/31.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 31, 3 | "Name": "Southern part of Midgaard", 4 | "BottomNumber": 3100, 5 | "TopNumber": 3199, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/32.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 32, 3 | "Name": "Great Temple of Asgaard and River", 4 | "BottomNumber": 3200, 5 | "TopNumber": 3299, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/33.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 33, 3 | "Name": "Diablo Lvl 4 (Hell)", 4 | "BottomNumber": 3300, 5 | "TopNumber": 3399, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/34.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 34, 3 | "Name": "Quest Maze", 4 | "BottomNumber": 3400, 5 | "TopNumber": 3499, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/36.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 36, 3 | "Name": "Land of Oz (Booof)", 4 | "BottomNumber": 3600, 5 | "TopNumber": 3799, 6 | "Lifespan": 44, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/38.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 38, 3 | "Name": "Devil's Wood (Herb and Linna)", 4 | "BottomNumber": 3800, 5 | "TopNumber": 3899, 6 | "Lifespan": 15, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/40.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 40, 3 | "Name": "Moria, levels 1, 2, and 5", 4 | "BottomNumber": 4000, 5 | "TopNumber": 4099, 6 | "Lifespan": 19, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/41.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 41, 3 | "Name": "Moria, levels 3-4, 6-9", 4 | "BottomNumber": 4100, 5 | "TopNumber": 4199, 6 | "Lifespan": 39, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/42.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 42, 3 | "Name": "Newbie builder school (Teddy)", 4 | "BottomNumber": 4200, 5 | "TopNumber": 4299, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/43.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 43, 3 | "Name": "MidKnight's Castle", 4 | "BottomNumber": 4300, 5 | "TopNumber": 4499, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/45.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 45, 3 | "Name": "Elemental Canyon (Drazzst)", 4 | "BottomNumber": 4550, 5 | "TopNumber": 4599, 6 | "Lifespan": 13, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/46.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 46, 3 | "Name": "New zone", 4 | "BottomNumber": 4600, 5 | "TopNumber": 4699, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/47.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 47, 3 | "Name": "Poke'mon I (Hysteria)", 4 | "BottomNumber": 4700, 5 | "TopNumber": 4799, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/48.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 48, 3 | "Name": "Poke'mon II (Hysteria)", 4 | "BottomNumber": 4800, 5 | "TopNumber": 4899, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/49.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 49, 3 | "Name": "Toy Story", 4 | "BottomNumber": 4900, 5 | "TopNumber": 4999, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/5.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 5, 3 | "Name": "Wamphyri", 4 | "BottomNumber": 500, 5 | "TopNumber": 599, 6 | "Lifespan": 44, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/50.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 50, 3 | "Name": "The Great Eastern Desert", 4 | "BottomNumber": 5000, 5 | "TopNumber": 5099, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/51.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 51, 3 | "Name": "Drow City (Dru)", 4 | "BottomNumber": 5100, 5 | "TopNumber": 5199, 6 | "Lifespan": 32, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/52.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 52, 3 | "Name": "The City of Thalos", 4 | "BottomNumber": 5200, 5 | "TopNumber": 5299, 6 | "Lifespan": 15, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/53.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 53, 3 | "Name": "The Pyramid", 4 | "BottomNumber": 5300, 5 | "TopNumber": 5399, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/54.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 54, 3 | "Name": "Sarnoth's zone", 4 | "BottomNumber": 5400, 5 | "TopNumber": 5499, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/56.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 56, 3 | "Name": "The Odyssey (Asita)", 4 | "BottomNumber": 5600, 5 | "TopNumber": 5699, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/59.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 59, 3 | "Name": "Night Court (Akiya, Myan, Stretch)", 4 | "BottomNumber": 5900, 5 | "TopNumber": 5999, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/6.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 6, 3 | "Name": "Chicago 1920's (Cati, Panther)", 4 | "BottomNumber": 600, 5 | "TopNumber": 699, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/60.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 60, 3 | "Name": "Haon-Dor (Light)", 4 | "BottomNumber": 6000, 5 | "TopNumber": 6099, 6 | "Lifespan": 13, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/61.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 61, 3 | "Name": "Haon-Dor (Dark)", 4 | "BottomNumber": 6100, 5 | "TopNumber": 6199, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/64.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 64, 3 | "Name": "Disney Land", 4 | "BottomNumber": 6400, 5 | "TopNumber": 6499, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/65.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 65, 3 | "Name": "The Dwarven Kingdom", 4 | "BottomNumber": 6500, 5 | "TopNumber": 6599, 6 | "Lifespan": 43, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/66.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 66, 3 | "Name": "Blade Runner (Booof)", 4 | "BottomNumber": 6600, 5 | "TopNumber": 6799, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/7.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 7, 3 | "Name": "Chicago 1920's (Cati, Panther)", 4 | "BottomNumber": 700, 5 | "TopNumber": 799, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/70.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 70, 3 | "Name": "Sewer", 4 | "BottomNumber": 7000, 5 | "TopNumber": 7099, 6 | "Lifespan": 15, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/73.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 73, 3 | "Name": "MudSchool", 4 | "BottomNumber": 7300, 5 | "TopNumber": 7399, 6 | "Lifespan": 10, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/75.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 75, 3 | "Name": "Planet Mars (Bloodseeker)", 4 | "BottomNumber": 7500, 5 | "TopNumber": 7599, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/79.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 79, 3 | "Name": "Redferne's Residence", 4 | "BottomNumber": 7900, 5 | "TopNumber": 7999, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/8.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 8, 3 | "Name": "midgaard breather", 4 | "BottomNumber": 800, 5 | "TopNumber": 899, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/82.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 82, 3 | "Name": "Xanth:Roogna\u0026Humphreys (Midknight)", 4 | "BottomNumber": 8200, 5 | "TopNumber": 8299, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/83.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 83, 3 | "Name": "Xanth:Humfrey-2,Villages (Midknight)", 4 | "BottomNumber": 8300, 5 | "TopNumber": 8399, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/84.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 84, 3 | "Name": "Forgotten Realms (Shifter)", 4 | "BottomNumber": 8400, 5 | "TopNumber": 8499, 6 | "Lifespan": 8, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/85.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 85, 3 | "Name": "Forgotten Realms 2 (Shifter)", 4 | "BottomNumber": 8500, 5 | "TopNumber": 8599, 6 | "Lifespan": 18, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/86.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 86, 3 | "Name": "Forgotten Realms 3 (Shifter)", 4 | "BottomNumber": 8600, 5 | "TopNumber": 8699, 6 | "Lifespan": 38, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/88.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 88, 3 | "Name": "Muppets (DonGiovanni)", 4 | "BottomNumber": 8800, 5 | "TopNumber": 8899, 6 | "Lifespan": 15, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/9.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 9, 3 | "Name": "Wolfenstein (Sheol)", 4 | "BottomNumber": 900, 5 | "TopNumber": 999, 6 | "Lifespan": 32, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/90.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 90, 3 | "Name": "Hills of Hell (Arkon)", 4 | "BottomNumber": 9000, 5 | "TopNumber": 9099, 6 | "Lifespan": 35, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/93.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 93, 3 | "Name": "Galaxy V 2.0(Sheol)", 4 | "BottomNumber": 9300, 5 | "TopNumber": 9399, 6 | "Lifespan": 30, 7 | "ResetMode": "RESET_EMPTY", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/95.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 95, 3 | "Name": "Terminator 2 (Darkheart)", 4 | "BottomNumber": 9500, 5 | "TopNumber": 9599, 6 | "Lifespan": 20, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/96.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 96, 3 | "Name": "Lost Valley (Thyra)", 4 | "BottomNumber": 9600, 5 | "TopNumber": 9699, 6 | "Lifespan": 25, 7 | "ResetMode": "RESET_ALWAYS", 8 | "Closed": false 9 | } -------------------------------------------------------------------------------- /data/zones/98.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 98, 3 | "Name": "Statue Museum (Daxx)", 4 | "BottomNumber": 9800, 5 | "TopNumber": 9899, 6 | "Lifespan": 40, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /data/zones/99.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 99, 3 | "Name": "Duke Nukem", 4 | "BottomNumber": 9900, 5 | "TopNumber": 9999, 6 | "Lifespan": 300, 7 | "ResetMode": "RESET_NEVER", 8 | "Closed": true 9 | } -------------------------------------------------------------------------------- /db/bolt.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/boltdb/bolt" 7 | ) 8 | 9 | // get gob decodes the value for key in the given bucket into val. It reports 10 | // if a value with that key exists, and any error in retrieving or decoding the 11 | // value. 12 | func get(b *bolt.Bucket, key []byte, val interface{}) (bool, error) { 13 | v := b.Get(key) 14 | if v == nil { 15 | return false, nil 16 | } 17 | return true, json.Unmarshal(v, val) 18 | } 19 | 20 | // put gob encodes the value and puts it in the given bucket with the given key. 21 | // This function assumes b was created from a writeable transaction. 22 | func put(b *bolt.Bucket, key []byte, val interface{}) error { 23 | bytes, err := json.Marshal(val) 24 | if err != nil { 25 | return err 26 | } 27 | return b.Put(key, bytes) 28 | } 29 | -------------------------------------------------------------------------------- /db/credentials.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "github.com/boltdb/bolt" 4 | 5 | var credsBucket = []byte("credentials") 6 | 7 | // An Credentials object holds a user's login credentials. 8 | type Credentials struct { 9 | Username string 10 | PwdHash []byte 11 | } 12 | 13 | // FindCreds returns the user's credentials. 14 | func (st *Store) FindCreds(username string) (Credentials, error) { 15 | var c Credentials 16 | err := st.db.View(func(tx *bolt.Tx) error { 17 | b := tx.Bucket(credsBucket) 18 | if b == nil { 19 | return ErrNoBucket("credentials") 20 | } 21 | exists, err := get(b, []byte(username), &c) 22 | if err != nil { 23 | return err 24 | } 25 | if !exists { 26 | return ErrNotFound("credentials") 27 | } 28 | return nil 29 | }) 30 | return c, err 31 | } 32 | 33 | // SaveCreds saves the user's credentials to the db. 34 | func (st *Store) SaveCreds(c Credentials) error { 35 | return st.db.Update(func(tx *bolt.Tx) error { 36 | return saveCreds(tx, c) 37 | }) 38 | } 39 | 40 | func saveCreds(tx *bolt.Tx, c Credentials) error { 41 | b := tx.Bucket(credsBucket) 42 | if b == nil { 43 | return ErrNoBucket("credentials") 44 | } 45 | return put(b, []byte(c.Username), c) 46 | } 47 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/boltdb/bolt" 8 | ) 9 | 10 | // Store contains all the functionality of persistence. 11 | type Store struct { 12 | db *bolt.DB 13 | } 14 | 15 | // Initialize sets up the application's configuration directory. 16 | func Init(dir string) (*Store, error) { 17 | path := filepath.Join(dir, "mud.db") 18 | db, err := bolt.Open(path, 0644, nil) 19 | if err != nil { 20 | return nil, fmt.Errorf("Error opening database file %q: %s", path, err) 21 | } 22 | err = db.Update(func(tx *bolt.Tx) error { 23 | _, err := tx.CreateBucketIfNotExists(playersBucket) 24 | if err != nil { 25 | return err 26 | } 27 | _, err = tx.CreateBucketIfNotExists(usersBucket) 28 | if err != nil { 29 | return err 30 | } 31 | _, err = tx.CreateBucketIfNotExists(credsBucket) 32 | return err 33 | }) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return &Store{db: db}, nil 38 | } 39 | 40 | // IsSetup returns true if the database has been setup. 41 | func (st *Store) IsSetup() (bool, error) { 42 | var setup bool 43 | err := st.db.View(func(tx *bolt.Tx) error { 44 | users := tx.Bucket(usersBucket) 45 | if users == nil { 46 | // bucket doesn't exist 47 | return ErrNoBucket("users") 48 | } 49 | k, v := users.Cursor().First() 50 | setup = k != nil && v != nil 51 | return nil 52 | }) 53 | return setup, err 54 | } 55 | -------------------------------------------------------------------------------- /db/db_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func tmpStore(t *testing.T) (st *Store, cleanup func()) { 10 | dir, err := ioutil.TempDir("", "") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | defer os.RemoveAll(dir) 15 | store, err := Init(dir) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | return store, func() { 20 | st.db.Close() 21 | os.RemoveAll(dir) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /db/errors.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "fmt" 4 | 5 | // ErrNotFound indicates the thing doesn't exist. 6 | type ErrNotFound string 7 | 8 | // Error implements the error interface. 9 | func (e ErrNotFound) Error() string { 10 | return string(e) + " doesn't exist" 11 | } 12 | 13 | // ErrExists indicates the thing already exists. 14 | type ErrExists string 15 | 16 | // Error implements the error interface. 17 | func (e ErrExists) Error() string { 18 | return string(e) + " already exists" 19 | } 20 | 21 | // ErrNoBucket is the error returned if we trying to reference a bucket that 22 | // doesn't exist. 23 | type ErrNoBucket string 24 | 25 | // Error implements the error interface. 26 | func (e ErrNoBucket) Error() string { 27 | return fmt.Sprintf("bucket %q doesn't exist", string(e)) 28 | } 29 | -------------------------------------------------------------------------------- /db/players.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "math/big" 5 | "strings" 6 | 7 | "github.com/boltdb/bolt" 8 | "github.com/natefinch/claymud/game" 9 | "github.com/natefinch/claymud/util" 10 | ) 11 | 12 | var playersBucket = []byte("players") 13 | 14 | // Player is the structure that is stored in the database for a Player. 15 | type Player struct { 16 | Name string 17 | Description string 18 | ID util.ID 19 | Gender game.Gender 20 | Flags *big.Int 21 | } 22 | 23 | // FindPlayer returns the player with the given name. This is a 24 | // case-insensitive check. 25 | func (st *Store) FindPlayer(name string) (*Player, error) { 26 | var p Player 27 | err := st.db.View(func(tx *bolt.Tx) error { 28 | b := tx.Bucket(playersBucket) 29 | if b == nil { 30 | return ErrNoBucket("players") 31 | } 32 | key := []byte(strings.ToLower(name)) 33 | exists, err := get(b, key, &p) 34 | if err != nil { 35 | return err 36 | } 37 | if !exists { 38 | return ErrNotFound("player") 39 | } 40 | return nil 41 | }) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return &p, nil 46 | } 47 | 48 | // PlayerExists reports whether a player with the given name exists. This is a 49 | // case-insensitive check. 50 | func (st *Store) PlayerExists(name string) (bool, error) { 51 | exists := false 52 | err := st.db.View(func(tx *bolt.Tx) error { 53 | b := tx.Bucket(playersBucket) 54 | if b == nil { 55 | return ErrNoBucket("players") 56 | } 57 | val := b.Get([]byte(strings.ToLower(name))) 58 | exists = val != nil 59 | return nil 60 | }) 61 | return exists, err 62 | } 63 | 64 | // SavePlayer saves the player's data to the db. 65 | func (st *Store) SavePlayer(p *Player) error { 66 | return st.db.Update(func(tx *bolt.Tx) error { 67 | b := tx.Bucket(playersBucket) 68 | if b == nil { 69 | return ErrNoBucket("players") 70 | } 71 | return put(b, []byte(strings.ToLower(p.Name)), p) 72 | }) 73 | } 74 | 75 | // CreatePlayer saves the player only if it does not already exist. 76 | func (st *Store) CreatePlayer(username string, p *Player) error { 77 | return st.db.Update(func(tx *bolt.Tx) error { 78 | players := tx.Bucket(playersBucket) 79 | if players == nil { 80 | return ErrNoBucket("players") 81 | } 82 | key := []byte(strings.ToLower(p.Name)) 83 | val := players.Get(key) 84 | if val != nil { 85 | return ErrExists("player") 86 | } 87 | id, err := players.NextSequence() 88 | if err != nil { 89 | return err 90 | } 91 | p.ID = util.ID(id) 92 | if err := put(players, key, p); err != nil { 93 | return err 94 | } 95 | 96 | u, err := getUser(tx, username) 97 | if err != nil { 98 | return err 99 | } 100 | u.Players = append(u.Players, p.Name) 101 | return saveUser(tx, u) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /db/players_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "math/big" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/gofrs/uuid" 9 | "github.com/natefinch/claymud/game" 10 | ) 11 | 12 | func TestSaveLoadPlayer(t *testing.T) { 13 | st, cleanup := tmpStore(t) 14 | defer cleanup() 15 | u := createFakeUser(t, st) 16 | p := fakePlayer(t) 17 | err := st.CreatePlayer(u.Username, p) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | found, err := st.FindPlayer(p.Name) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if !reflect.DeepEqual(p, found) { 27 | t.Fatalf("expected %#v, got %#v", p, found) 28 | } 29 | } 30 | 31 | func TestDupePlayer(t *testing.T) { 32 | st, cleanup := tmpStore(t) 33 | defer cleanup() 34 | u := createFakeUser(t, st) 35 | p := fakePlayer(t) 36 | if err := st.CreatePlayer(u.Username, p); err != nil { 37 | t.Fatal(err) 38 | } 39 | err := st.CreatePlayer(u.Username, p) 40 | if _, ok := err.(ErrExists); !ok { 41 | t.Fatalf("expected to get ErrExists, but got %#v", err) 42 | } 43 | } 44 | 45 | func TestFindPlayerNotFound(t *testing.T) { 46 | st, cleanup := tmpStore(t) 47 | defer cleanup() 48 | u, err := uuid.NewV4() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | _, err = st.FindPlayer(u.String()) 53 | if _, ok := err.(ErrNotFound); !ok { 54 | t.Fatalf("expected to get ErrNotFound but got %#v", err) 55 | } 56 | } 57 | 58 | func fakePlayer(t *testing.T) *Player { 59 | name, err := uuid.NewV4() 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | return &Player{ 65 | Name: name.String(), 66 | Description: "bar", 67 | Gender: game.Gender{ 68 | Name: "foo", 69 | Xself: "fooself", 70 | Xe: "fooe", 71 | Xim: "fooim", 72 | Xis: "foois", 73 | }, 74 | Flags: big.NewInt(17), 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /db/users.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/boltdb/bolt" 8 | "github.com/natefinch/claymud/util" 9 | ) 10 | 11 | var usersBucket = []byte("users") 12 | 13 | // User is the structure that is stored in the database for a User. 14 | type User struct { 15 | ID util.ID 16 | Username string 17 | LastIP string 18 | LastLogin time.Time 19 | Players []string 20 | Flags *big.Int 21 | } 22 | 23 | // UserExists reports whether a user with the username exists. 24 | func (st *Store) UserExists(username string) (bool, error) { 25 | exists := false 26 | err := st.db.View(func(tx *bolt.Tx) error { 27 | b := tx.Bucket(usersBucket) 28 | if b == nil { 29 | return ErrNoBucket("users") 30 | } 31 | exists = b.Get([]byte(username)) != nil 32 | return nil 33 | }) 34 | return exists, err 35 | } 36 | 37 | // FindUser returns the user with the username. 38 | func (st *Store) FindUser(username string) (*User, error) { 39 | var u *User 40 | err := st.db.View(func(tx *bolt.Tx) error { 41 | var err error 42 | u, err = getUser(tx, username) 43 | return err 44 | }) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return u, nil 49 | } 50 | 51 | func getUser(tx *bolt.Tx, username string) (*User, error) { 52 | var u User 53 | b := tx.Bucket(usersBucket) 54 | if b == nil { 55 | return nil, ErrNoBucket("users") 56 | } 57 | exists, err := get(b, []byte(username), &u) 58 | if err != nil { 59 | return nil, err 60 | } 61 | if !exists { 62 | return nil, ErrNotFound("user") 63 | } 64 | return &u, nil 65 | } 66 | 67 | // SaveUser saves the user to the db. 68 | func (st *Store) SaveUser(u *User) error { 69 | return st.db.Update(func(tx *bolt.Tx) error { 70 | return saveUser(tx, u) 71 | }) 72 | } 73 | 74 | func saveUser(tx *bolt.Tx, u *User) error { 75 | b := tx.Bucket(usersBucket) 76 | if b == nil { 77 | return ErrNoBucket("users") 78 | } 79 | return put(b, []byte(u.Username), u) 80 | } 81 | 82 | // CreateUser creates the user only if it does not exist. 83 | func (st *Store) CreateUser(u *User, pwdHash []byte) error { 84 | return st.db.Update(func(tx *bolt.Tx) error { 85 | users := tx.Bucket(usersBucket) 86 | if users == nil { 87 | return ErrNoBucket("users") 88 | } 89 | if users.Get([]byte(u.Username)) != nil { 90 | return ErrExists("user") 91 | } 92 | id, err := users.NextSequence() 93 | if err != nil { 94 | return err 95 | } 96 | u.ID = util.ID(id) 97 | if err := put(users, []byte(u.Username), u); err != nil { 98 | return err 99 | } 100 | return saveCreds(tx, Credentials{Username: u.Username, PwdHash: pwdHash}) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /db/users_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "bytes" 5 | "math/big" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gofrs/uuid" 11 | ) 12 | 13 | func TestSaveLoadUser(t *testing.T) { 14 | st, cleanup := tmpStore(t) 15 | defer cleanup() 16 | u := fakeUser(t) 17 | hash := []byte("secret") 18 | if err := st.CreateUser(u, hash); err != nil { 19 | t.Fatal(err) 20 | } 21 | found, err := st.FindUser(u.Username) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | usersEqual(t, u, found) 26 | creds, err := st.FindCreds(u.Username) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if creds.Username != u.Username { 31 | t.Errorf("Expected creds username %q but got %q", u.Username, creds.Username) 32 | } 33 | if !bytes.Equal(hash, creds.PwdHash) { 34 | t.Errorf("Expected password hash %x, but got %x", hash, creds.PwdHash) 35 | } 36 | } 37 | 38 | func TestDupeUser(t *testing.T) { 39 | st, cleanup := tmpStore(t) 40 | defer cleanup() 41 | u := fakeUser(t) 42 | hash := []byte("secret") 43 | if err := st.CreateUser(u, hash); err != nil { 44 | t.Fatal(err) 45 | } 46 | err := st.CreateUser(u, hash) 47 | if _, ok := err.(ErrExists); !ok { 48 | t.Fatalf("expected to get ErrExists, but got %#v", err) 49 | } 50 | } 51 | 52 | func TestFindUserNotFound(t *testing.T) { 53 | st, cleanup := tmpStore(t) 54 | defer cleanup() 55 | u, err := uuid.NewV4() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | _, err = st.FindUser(u.String()) 60 | if _, ok := err.(ErrNotFound); !ok { 61 | t.Fatalf("expected to get ErrNotFound but got %#v", err) 62 | } 63 | } 64 | 65 | func usersEqual(t *testing.T, expected, got *User) { 66 | expectedT := expected.LastLogin 67 | gotT := got.LastLogin 68 | 69 | // zero out the comparison for times because they don't round trip in a comparable fashion. 70 | expected.LastLogin = got.LastLogin 71 | if !expectedT.Equal(gotT) { 72 | t.Errorf("expected time %v, but got %v", expectedT, gotT) 73 | } 74 | if !reflect.DeepEqual(*expected, *got) { 75 | t.Fatalf("expected %#v, got %#v", *expected, *got) 76 | } 77 | } 78 | 79 | func fakeUser(t *testing.T) *User { 80 | u, err := uuid.NewV4() 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | return &User{ 85 | Username: u.String(), 86 | LastIP: "bar", 87 | LastLogin: time.Now(), 88 | Players: []string{"bill", "jane"}, 89 | Flags: big.NewInt(12), 90 | } 91 | } 92 | 93 | func createFakeUser(t *testing.T, st *Store) *User { 94 | u := fakeUser(t) 95 | hash := []byte("secret") 96 | if err := st.CreateUser(u, hash); err != nil { 97 | t.Fatal(err) 98 | } 99 | return u 100 | } 101 | -------------------------------------------------------------------------------- /game/dice.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Dice represents a randomized value based on a number of dice, e.g. 3d6+5. 11 | type Dice struct { 12 | Count, Size, Modifier int 13 | } 14 | 15 | // Roll rolls the dice and returns the result. 16 | func (d Dice) Roll() int { 17 | result := 0 18 | for i := 0; i < d.Count; i++ { 19 | result += rand.Intn(d.Size) + 1 20 | } 21 | result += d.Modifier 22 | return result 23 | } 24 | 25 | // MakeDice converts an #d#+# formatted string to a Dice struct. 26 | func MakeDice(s string) (Dice, error) { 27 | vals := strings.Split(s, "d") 28 | if len(vals) != 2 { 29 | return Dice{}, fmt.Errorf("expected dice to be #d#+-# but was %v", s) 30 | } 31 | count, err := strconv.Atoi(vals[0]) 32 | if err != nil { 33 | return Dice{}, fmt.Errorf("count of dice is not a number: %v", vals[0]) 34 | } 35 | sep := strings.IndexAny(vals[1], "+-") 36 | if sep == -1 { 37 | size, err := strconv.Atoi(vals[1]) 38 | if err != nil { 39 | return Dice{}, fmt.Errorf("size of dice is not a number: %v", vals[1]) 40 | } 41 | return Dice{Count: count, Size: size}, nil 42 | } 43 | s = vals[1][:sep] 44 | size, err := strconv.Atoi(s) 45 | if err != nil { 46 | return Dice{}, fmt.Errorf("size of dice is not a number: %v", s) 47 | } 48 | s = vals[1][sep+1:] 49 | mod, err := strconv.Atoi(s) 50 | if err != nil { 51 | return Dice{}, fmt.Errorf("modifier on dice is not a number: %v", s) 52 | } 53 | return Dice{Count: count, Size: size, Modifier: mod}, nil 54 | } 55 | -------------------------------------------------------------------------------- /game/direction.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | dirMap = map[string]Direction{} 10 | dirList []Direction 11 | ) 12 | 13 | // InitDirs initializes the configured directions. Order matters, the order becomes 14 | // how they'll get displayed in the list of exits. 15 | func InitDirs(dirs []Direction) { 16 | if len(dirs) == 0 { 17 | panic(fmt.Errorf("no directions defined in config")) 18 | } 19 | for i := range dirs { 20 | dirs[i].ID = i 21 | } 22 | // make a map of names and aliases to the directions 23 | // the names and aliases will become commands to move in the world 24 | for _, dir := range dirs { 25 | addDir(dir) 26 | } 27 | dirList = dirs 28 | } 29 | 30 | // Direction is a direction that a standard exit can use 31 | type Direction struct { 32 | ID int 33 | // Name is what will be shown in room descriptions. 34 | Name string 35 | // From is used when displaying enter/exit notifications for players. 36 | From string 37 | // Aliases is a list of alternate names for the direction. 38 | Aliases []string 39 | } 40 | 41 | // FindDir will find a direction by name or alias. This method is not case 42 | // sensitive. 43 | func FindDirection(alias string) (dir Direction, found bool) { 44 | dir, found = dirMap[strings.ToLower(alias)] 45 | return dir, found 46 | } 47 | 48 | // AllDirections returns a list of all the directions that exist in the world. 49 | func AllDirections() []Direction { 50 | return dirList 51 | } 52 | 53 | // addDir adds the given direction to the global map of directions 54 | // 55 | // This function uses the name and aliases as ids to find the direction 56 | func addDir(dir Direction) { 57 | name := strings.ToLower(dir.Name) 58 | if _, ok := dirMap[name]; ok { 59 | panic(fmt.Errorf("direction with name/alias %q already exists", name)) 60 | } 61 | dirMap[name] = dir 62 | for _, alias := range dir.Aliases { 63 | alias = strings.ToLower(alias) 64 | if _, ok := dirMap[alias]; ok { 65 | panic(fmt.Errorf("direction with name/alias %q already exists", alias)) 66 | } 67 | dirMap[alias] = dir 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /game/gender.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import "log" 4 | 5 | // Genders is the list of globally available genders. 6 | var Genders []Gender 7 | 8 | // Gender defines a person's pronouns. 9 | type Gender struct { 10 | Name string 11 | Xself string 12 | Xe string 13 | Xim string 14 | Xis string 15 | } 16 | 17 | // InitGenders sets up the globally available genders. 18 | func InitGenders(g []Gender) { 19 | if len(g) == 0 { 20 | log.Println("WARNING: no genders defined") 21 | Genders = []Gender{ 22 | { 23 | Name: "none", 24 | Xself: "itself", 25 | Xe: "it", 26 | Xim: "it", 27 | Xis: "its", 28 | }, 29 | } 30 | } 31 | Genders = g 32 | } 33 | -------------------------------------------------------------------------------- /game/position.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | // Position represents the physical state of a mob or player. 4 | type Position int 5 | 6 | // All the positions 7 | const ( 8 | PositionDead Position = iota 9 | PositionMortallyWounded 10 | PositionIncapacitated 11 | PositionStunned 12 | PositionSleeping 13 | PositionResting 14 | PositionSitting 15 | PositionFighting 16 | PositionStanding 17 | ) 18 | -------------------------------------------------------------------------------- /game/social/parsing.go: -------------------------------------------------------------------------------- 1 | package social 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "text/template" 10 | 11 | "github.com/BurntSushi/toml" 12 | ) 13 | 14 | const ( 15 | templFile = "socials.toml" 16 | ) 17 | 18 | var arrival *noTarget 19 | 20 | // DoArrival runs the standard social that occurs when you 21 | func DoArrival(actor Person, others io.Writer) { 22 | performToNoOne("arrival", arrival, socialData{Actor: actor}, actor, others) 23 | } 24 | 25 | // Initialize creates the socialTemplate map and loads socials into it. 26 | func Initialize(dir string) error { 27 | filename := filepath.Join(dir, templFile) 28 | 29 | f, err := os.Open(filename) 30 | if err != nil { 31 | return fmt.Errorf("Error reading social config file: %s", err) 32 | } 33 | defer f.Close() 34 | cfg, err := decodeConfig(f) 35 | if err != nil { 36 | return err 37 | } 38 | arrival = &cfg.Arrival 39 | if arrival.Self.Template == nil { 40 | arrival.Self.Template = template.Must(template.New("arrival.self").Parse("You arrive in a puff of smoke.")) 41 | } 42 | if arrival.Around.Template == nil { 43 | arrival.Around.Template = template.Must(template.New("arrival.around").Parse("{{Actor}} arrives in a puff of smoke.")) 44 | } 45 | 46 | if err := loadSocials(cfg.Socials); err != nil { 47 | return err 48 | } 49 | 50 | log.Printf("Loaded socials: %v", Names) 51 | return nil 52 | } 53 | 54 | // socialConfig is a struct for getting the social templates out of a config. 55 | type socialConfig struct { 56 | // the social for a player arriving in the world at the starting location. 57 | Arrival noTarget 58 | 59 | // yes, the toml is social, singular. This lets the section header be 60 | // [[social]] instead of [[socials]] which is a lot clearer. 61 | Socials []social `toml:"social"` 62 | } 63 | 64 | // decodeSocials parses the data from the reader into a list of socials. 65 | func decodeConfig(r io.Reader) (*socialConfig, error) { 66 | cfg := socialConfig{} 67 | 68 | res, err := toml.DecodeReader(r, &cfg) 69 | if err != nil { 70 | return nil, fmt.Errorf("Error parsing social config file: %s", err) 71 | } 72 | 73 | if und := res.Undecoded(); len(und) > 0 { 74 | log.Printf("WARNING: Unknown values in social config file: %v", und) 75 | } 76 | return &cfg, nil 77 | } 78 | 79 | // loadSocials populates the game's list of socials and checks for duplicates. 80 | func loadSocials(em []social) error { 81 | socials = make(map[string]social, len(em)) 82 | Names = make([]string, len(em)) 83 | for i, social := range em { 84 | if _, ok := socials[social.Name]; ok { 85 | return fmt.Errorf("Duplicate social defined: %q", social.Name) 86 | } 87 | 88 | Names[i] = social.Name 89 | socials[social.Name] = social 90 | } 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /game/social/social_test.go: -------------------------------------------------------------------------------- 1 | package social 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/natefinch/claymud/game" 9 | ) 10 | 11 | func TestParse(t *testing.T) { 12 | cfg, err := decodeConfig(strings.NewReader(data)) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | ems := cfg.Socials 17 | if len(ems) != 2 { 18 | t.Fatalf("expected len 2, got %#v", ems) 19 | } 20 | 21 | e := ems[0] 22 | if e.Name != "smile" { 23 | t.Fatalf("expected to see smile, got %#v", e) 24 | } 25 | 26 | e = ems[1] 27 | if e.Name != "jump" { 28 | t.Fatalf("expected jump, got %#v", e) 29 | } 30 | } 31 | 32 | var female = game.Gender{ 33 | Name: "female", 34 | Xself: "herself", 35 | Xe: "she", 36 | Xim: "her", 37 | Xis: "hers", 38 | } 39 | 40 | func TestPerformOther(t *testing.T) { 41 | cfg, err := decodeConfig(strings.NewReader(data)) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | err = loadSocials(cfg.Socials) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | a := testActor{ 51 | name: "fooName", 52 | gender: female, 53 | buf: &bytes.Buffer{}, 54 | } 55 | b := testActor{ 56 | name: "fooName2", 57 | gender: female, 58 | buf: &bytes.Buffer{}, 59 | } 60 | 61 | others := &bytes.Buffer{} 62 | found := Perform("smile", a, b, others) 63 | if !found { 64 | t.Fatal("smile social not found") 65 | } 66 | expected := "You smile at fooName2." 67 | if a.buf.String() != expected { 68 | t.Errorf("expected actor to get %q, but got %q", expected, a.buf.String()) 69 | } 70 | expected = "fooName smiles at you." 71 | if b.buf.String() != expected { 72 | t.Errorf("expected actor to get %q, but got %q", expected, b.buf.String()) 73 | } 74 | 75 | } 76 | 77 | /* 78 | func (*Tests) TestParse(c *C) { 79 | ems, err := decodeSocials(strings.NewReader(data)) 80 | c.Assert(err, IsNil) 81 | c.Assert(ems, HasLen, 2) 82 | 83 | e := ems[0] 84 | c.Assert(e.Name, Equals, "smile") 85 | c.Assert(e.ToSelf, NotNil) 86 | c.Assert(e.ToOther, NotNil) 87 | c.Assert(e.ToNoOne, NotNil) 88 | 89 | e = ems[1] 90 | c.Assert(e.Name, Equals, "jump") 91 | // note that there's no jump toself, so this should be nil 92 | c.Assert(e.ToSelf, IsNil) 93 | c.Assert(e.ToOther, NotNil) 94 | c.Assert(e.ToNoOne, NotNil) 95 | } 96 | 97 | func (*Tests) TestLoadSocials(c *C) { 98 | ems, err := decodeSocials(strings.NewReader(data)) 99 | c.Assert(err, IsNil) 100 | c.Assert(ems, HasLen, 2) 101 | 102 | err = loadSocials(ems) 103 | c.Assert(err, IsNil) 104 | c.Assert(Names, HasLen, 2) 105 | c.Assert(socials, HasLen, 2) 106 | } 107 | 108 | func (*Tests) TestDupeSocials(c *C) { 109 | ems, err := decodeSocials(strings.NewReader(dupes)) 110 | c.Assert(err, IsNil) 111 | c.Assert(ems, HasLen, 2) 112 | 113 | err = loadSocials(ems) 114 | c.Assert(err, ErrorMatches, `Duplicate social defined: "smile"`) 115 | } 116 | 117 | func (*Tests) TestPerformSelf(c *C) { 118 | defer patchGender(c)() 119 | 120 | ems, err := decodeSocials(strings.NewReader(data)) 121 | c.Assert(err, IsNil) 122 | c.Assert(ems, HasLen, 2) 123 | 124 | err = loadSocials(ems) 125 | c.Assert(err, IsNil) 126 | 127 | a := testActor{ 128 | name: "fooName", 129 | sex: gender.Male, 130 | buf: &bytes.Buffer{}, 131 | } 132 | 133 | others := &bytes.Buffer{} 134 | Perform("smile", a, a, others) 135 | 136 | c.Assert(a.buf.String(), Equals, "You smile to yourself.") 137 | c.Assert(others.String(), Equals, "fooName smiles to himself.") 138 | } 139 | */ 140 | 141 | var _ Person = testActor{} 142 | 143 | type testActor struct { 144 | name string 145 | gender game.Gender 146 | buf *bytes.Buffer 147 | } 148 | 149 | func (a testActor) Name() string { 150 | return a.name 151 | } 152 | 153 | func (a testActor) Gender() game.Gender { 154 | return a.gender 155 | } 156 | 157 | func (a testActor) Write(b []byte) (int, error) { 158 | return a.buf.Write(b) 159 | } 160 | 161 | var data = ` 162 | [[social]] 163 | name = "smile" 164 | 165 | [social.toSelf] 166 | self = "You smile to yourself." 167 | around = "{{.Actor.Name}} smiles to {{.Actor.Gender.Xself}}." 168 | 169 | [social.toNoOne] 170 | self = "You smile." 171 | around = "{{.Actor.Name}} smiles." 172 | 173 | [social.toOther] 174 | self = "You smile at {{.Target.Name}}." 175 | target = "{{.Actor.Name}} smiles at you." 176 | around = "{{.Actor.Name}} smiles at {{.Target.Name}}." 177 | 178 | [[social]] 179 | name = "jump" 180 | 181 | # note there is no ToSelf section. 182 | 183 | [social.toNoOne] 184 | self = "You jump around like a crazy person." 185 | around = "{{.Actor.Name}} jumps around like a crazy person." 186 | 187 | [social.toOther] 188 | self = "You jump {{.Target.Name}}." 189 | target = "{{.Actor.Name}} jumps you." 190 | around = "{{.Actor.Name}} jumps {{.Target.Name}}." 191 | 192 | ` 193 | 194 | var dupes = ` 195 | [[social]] 196 | name = "smile" 197 | 198 | [social.toSelf] 199 | self = "You smile to yourself." 200 | around = "{{.Actor}} smiles to {{.Actor.Gender.Xself}}." 201 | 202 | [social.toNoOne] 203 | self = "You smile." 204 | around = "{{.Actor}} smiles." 205 | 206 | [social.toOther] 207 | self = "You smile at {{.Target}}." 208 | target = "{{.Actor}} smiles at you." 209 | around = "{{.Actor}} smiles at {{.Target}}." 210 | 211 | [[social]] 212 | name = "smile" 213 | 214 | [social.toSelf] 215 | self = "You smile to yourself." 216 | around = "{{.Actor}} smiles to {{.Actor.Gender.Xself}}." 217 | 218 | [social.toNoOne] 219 | self = "You smile." 220 | around = "{{.Actor}} smiles." 221 | 222 | [social.toOther] 223 | self = "You smile at {{.Target}}." 224 | target = "{{.Actor}} smiles at you." 225 | around = "{{.Actor}} smiles at {{.Target}}." 226 | 227 | ` 228 | -------------------------------------------------------------------------------- /game/social/socials.go: -------------------------------------------------------------------------------- 1 | package social 2 | 3 | import ( 4 | "io" 5 | "log" 6 | 7 | "github.com/natefinch/claymud/game" 8 | "github.com/natefinch/claymud/util" 9 | ) 10 | 11 | var ( 12 | socials map[string]social 13 | ) 14 | 15 | // Exists reports whether the given social exists as a command in the game. 16 | func Exists(name string) bool { 17 | _, ok := socials[name] 18 | return ok 19 | } 20 | 21 | // Names is a list of the names of the available socials in the game 22 | var Names []string 23 | 24 | // noTarget is a collection of templates for an social that doesn't have a 25 | // target. 26 | type noTarget struct { 27 | Self util.Template 28 | Around util.Template 29 | } 30 | 31 | // withTarget is a collection of templates for an social that has a target. 32 | type withTarget struct { 33 | noTarget 34 | Target util.Template 35 | } 36 | 37 | // social is a struct that holds data about an social. 38 | type social struct { 39 | Name string 40 | ToSelf *noTarget 41 | ToNoOne *noTarget 42 | ToOther *withTarget 43 | } 44 | 45 | func (e social) String() string { 46 | return e.Name 47 | } 48 | 49 | // Person is an interface that is used when filling the messages from an 50 | // SocialTemplate. 51 | type Person interface { 52 | Name() string 53 | Gender() game.Gender 54 | io.Writer 55 | } 56 | 57 | // socialData is the data we pass into the templates to generate the text. 58 | type socialData struct { 59 | Actor Person 60 | Target Person 61 | } 62 | 63 | // Perform attempts to perform the social named by cmd given the actor and target. 64 | // Target may be nil if no target was specified. 65 | // If the social exists, the output will be written to each of the writers. 66 | // Perform reports whether the social was found. 67 | func Perform(cmd string, actor Person, target Person, others io.Writer) bool { 68 | social, ok := socials[cmd] 69 | if !ok { 70 | return false 71 | } 72 | 73 | // TODO: Support more params? Him/He/etc? 74 | data := socialData{ 75 | Actor: actor, 76 | Target: target, 77 | } 78 | 79 | switch { 80 | case target == nil: 81 | performToNoOne(cmd, social.ToNoOne, data, actor, others) 82 | case actor == target: 83 | performToSelf(cmd, social.ToSelf, data, actor, others) 84 | default: 85 | performToOther(cmd, social.ToOther, data, actor, target, others) 86 | } 87 | return true 88 | } 89 | 90 | func performToSelf(name string, toSelf *noTarget, data socialData, actor Person, others io.Writer) { 91 | if toSelf == nil { 92 | _, _ = io.WriteString(actor, "You can't do that to yourself.") 93 | return 94 | } 95 | err := toSelf.Self.Template.Execute(actor, data) 96 | if err != nil { 97 | logFillErr(name, "ToSelf.Self", data, err) 98 | // if there's an error running the social to the actor, just bail early. 99 | return 100 | } 101 | around(name, ".ToSelf", toSelf.Around, data, others) 102 | } 103 | 104 | func around(name, to string, tmpl util.Template, data socialData, others io.Writer) { 105 | err := tmpl.Execute(others, data) 106 | if err != nil { 107 | logFillErr(name, to+".Around", data, err) 108 | return 109 | } 110 | io.WriteString(others, "\n") 111 | } 112 | 113 | func performToNoOne(name string, toNoOne *noTarget, data socialData, actor Person, others io.Writer) { 114 | if toNoOne == nil { 115 | _, _ = io.WriteString(actor, "You can't do that.\n") 116 | return 117 | } 118 | err := toNoOne.Self.Template.Execute(actor, data) 119 | io.WriteString(actor, "\n") 120 | if err != nil { 121 | logFillErr(name, "ToNoOne.Self", data, err) 122 | // if there's an error running the social to the actor, just bail early. 123 | return 124 | } 125 | 126 | around(name, "ToNoOne", toNoOne.Around, data, others) 127 | } 128 | 129 | func performToOther(name string, toOther *withTarget, data socialData, actor Person, target Person, others io.Writer) { 130 | if toOther == nil { 131 | _, _ = io.WriteString(actor, "You can't do that to someone else.") 132 | return 133 | } 134 | err := toOther.Self.Template.Execute(actor, data) 135 | if err != nil { 136 | logFillErr(name, "ToOther.Self", data, err) 137 | // if there's an error running the social to the actor, just bail early. 138 | return 139 | } 140 | 141 | err = toOther.Target.Template.Execute(target, data) 142 | if err != nil { 143 | logFillErr(name, "ToOther.Target", data, err) 144 | } 145 | 146 | around(name, "ToOther", toOther.Around, data, others) 147 | } 148 | 149 | func logFillErr(name string, template string, data socialData, err error) { 150 | log.Printf("ERROR: filling social template %q for %s with data %v: %s", name, template, data, err) 151 | } 152 | -------------------------------------------------------------------------------- /game/worker.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | const tickLen = 100 * time.Millisecond 9 | 10 | // SpawnWorker creates a long-lived goroutine that handles work until the 11 | // shutdown channel is closed. 12 | // Zone-local workers are spawned with a readlocker 13 | // so they can run in parallel. A single global worker is spawned with a write 14 | // locker to ensure that none of the other workers are processing events when it 15 | // is (to avoid data races). Closing the shutdown channel will stop the worker 16 | // as soon as possible. Waiting on the waitgroup will unblock when all workers 17 | // have exited. 18 | func SpawnWorker(runLock sync.Locker, shutdown <-chan struct{}, wg *sync.WaitGroup) *Worker { 19 | w := &Worker{ 20 | runLock: runLock, 21 | shutdown: shutdown, 22 | wg: wg, 23 | events: make(chan func()), 24 | next: time.Now().Add(tickLen), 25 | eventGate: &sync.RWMutex{}, 26 | } 27 | wg.Add(1) 28 | go w.run() 29 | return w 30 | } 31 | 32 | // Worker is a single-threaded event loop that serializes actions in the world. 33 | // All actions in a zone are handled by the same worker, eliminating race 34 | // conditions and obviating the need for most locks. Coordination of actions 35 | // that take place across zones is done by a single "global" worker. Since few 36 | // actions have that trait, this is less of a performance burden. 37 | type Worker struct { 38 | shutdown <-chan struct{} 39 | wg *sync.WaitGroup 40 | next time.Time // Next tick 41 | runLock sync.Locker // exclusive lock between zone and global workers 42 | eventGate *sync.RWMutex // exclusive lock between worker and event sources 43 | events chan func() 44 | } 45 | 46 | // Handle takes an event from somewhere in the world and executes it. This 47 | // method is thread safe, so users on their own threads can call it without 48 | // worry. 49 | func (w *Worker) Handle(event func()) { 50 | // This is a gate that ensures each event source can only put one event on 51 | // the worker's queue. 52 | // 53 | // The worker write locks when it starts to process so that no new items can 54 | // be put on the goroutine until it is finished processing. This prevents a 55 | // fast thread from having its event handled and then getting a second one 56 | // put back on the queue before the worker is finished. We use a reader lock 57 | // for other threads so they won't contend. We unlock immediately, otherwise 58 | // we'd have a deadlock between this readlock being locked, waiting on the 59 | // channel, and the worker trying to write lock before it drains the 60 | // channel. 61 | w.eventGate.RLock() 62 | w.eventGate.RUnlock() 63 | w.events <- event 64 | } 65 | 66 | // run is the goroutine for the worker. 67 | func (w *Worker) run() { 68 | defer w.wg.Done() 69 | 70 | for { 71 | if w.closed() { 72 | return 73 | } 74 | func() { 75 | defer w.runLock.Unlock() 76 | w.runLock.Lock() 77 | defer w.eventGate.Unlock() 78 | w.eventGate.Lock() 79 | for { 80 | select { 81 | case e := <-w.events: 82 | e() 83 | default: 84 | return 85 | } 86 | } 87 | }() 88 | if w.closed() { 89 | return 90 | } 91 | time.Sleep(time.Until(w.next)) 92 | if w.closed() { 93 | return 94 | } 95 | // we recalculate next based on when we *should* have woken up, since 96 | // the actual wake up time may vary slightly. 97 | w.next = w.next.Add(tickLen) 98 | } 99 | } 100 | 101 | // closed returns true if we should exit. 102 | func (w *Worker) closed() bool { 103 | select { 104 | case <-w.shutdown: 105 | return true 106 | default: 107 | return false 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | //+build mage 2 | 3 | // This is the build script for Mage. The install target is all you really need. 4 | // The release target is for generating offial releases and is really only 5 | // useful to project admins. 6 | package main 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "os" 14 | "regexp" 15 | "strings" 16 | "time" 17 | 18 | "github.com/magefile/mage/mg" 19 | "github.com/magefile/mage/sh" 20 | ) 21 | 22 | // Runs "go build" and generates the version info the binary. 23 | func Build() error { 24 | os.Setenv("GOOS", os.Getenv("GOOSE")) 25 | return sh.RunV("go", "build", "-ldflags="+flags(), "github.com/natefinch/claymud") 26 | } 27 | 28 | // Runs the binary after building. 29 | func Run() error { 30 | mg.Deps(Build) 31 | return sh.RunV("./claymud") 32 | } 33 | 34 | var releaseTag = regexp.MustCompile(`^v0\.[0-9]+\.[0-9]+$`) 35 | 36 | // Generates a new release. Expects the TAG environment variable to be set, 37 | // which will create a new tag with that name. 38 | func Release() (err error) { 39 | tag := os.Getenv("TAG") 40 | if !releaseTag.MatchString(tag) { 41 | return errors.New("TAG environment variable must be in semver v1.x.x format, but was " + tag) 42 | } 43 | 44 | if err := sh.RunV("git", "tag", "-a", tag, "-m", tag); err != nil { 45 | return err 46 | } 47 | if err := sh.RunV("git", "push", "origin", tag); err != nil { 48 | return err 49 | } 50 | defer func() { 51 | if err != nil { 52 | sh.RunV("git", "tag", "--delete", "$TAG") 53 | sh.RunV("git", "push", "--delete", "origin", "$TAG") 54 | } 55 | }() 56 | return sh.RunV("goreleaser") 57 | } 58 | 59 | // Remove the temporarily generated files from Release. 60 | func Clean() error { 61 | return sh.Rm("dist") 62 | } 63 | 64 | // Creates a new in-mud command. Expects the CMD environment variable to be set. 65 | func NewCmd() error { 66 | cmd := os.Getenv("CMD") 67 | if cmd == "" { 68 | return errors.New("missing CMD environment variable") 69 | } 70 | title := strings.Title(cmd) 71 | 72 | const commandsGo = "./world/commands.go" 73 | b, err := ioutil.ReadFile(commandsGo) 74 | if err != nil { 75 | return err 76 | } 77 | s := string(b) 78 | const cmdConfig = "type Commands struct {\n" 79 | idx := strings.Index(s, cmdConfig) 80 | if idx == -1 { 81 | return fmt.Errorf("missing command struct!") 82 | } 83 | insertPt := idx + len(cmdConfig) 84 | s = s[:insertPt] + "\t" + title + ",\n" + s[insertPt:] 85 | 86 | const initCmds = "func initCommands(cfg Commands) {\n" 87 | idx = strings.Index(s, initCmds) 88 | if idx == -1 { 89 | return fmt.Errorf("missing initCommands function!") 90 | } 91 | insertPt = idx + len(initCmds) 92 | s = fmt.Sprintf("%s\tregister(%s, cfg.%s)\n%s", s[:insertPt], cmd, title, s[insertPt:]) 93 | 94 | s = s + fmt.Sprintf("\nfunc %s(c *Command) {\n}\n", cmd) 95 | 96 | if err := ioutil.WriteFile(commandsGo, []byte(s), 0644); err != nil { 97 | return err 98 | } 99 | const commandsToml = "./data/commands.toml" 100 | f, err := os.OpenFile(commandsToml, os.O_APPEND|os.O_WRONLY, 0644) 101 | if err != nil { 102 | return err 103 | } 104 | defer f.Close() 105 | _, err = io.WriteString(f, fmt.Sprintf(` 106 | [%s] 107 | Command = "%s" 108 | Aliases = [] 109 | Help = "" 110 | `, title, cmd)) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func flags() string { 119 | timestamp := time.Now().Format(time.RFC3339) 120 | hash := hash() 121 | tag := tag() 122 | if tag == "" { 123 | tag = "dev" 124 | } 125 | return fmt.Sprintf(`-X "github.com/natefinch/claymud/server.timestamp=%s" -X "github.com/natefinch/claymud/server.commitHash=%s" -X "github.com/natefinch/claymud/server.gitTag=%s"`, timestamp, hash, tag) 126 | } 127 | 128 | // tag returns the git tag for the current branch or "" if none. 129 | func tag() string { 130 | s, _ := sh.Output("git", "describe", "--tags") 131 | return s 132 | } 133 | 134 | // hash returns the git hash for the current repo or "" if none. 135 | func hash() string { 136 | hash, _ := sh.Output("git", "rev-parse", "--short", "HEAD") 137 | return hash 138 | } 139 | -------------------------------------------------------------------------------- /server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/natefinch/claymud/world" 13 | 14 | "github.com/BurntSushi/toml" 15 | "github.com/natefinch/claymud/game" 16 | "gopkg.in/natefinch/lumberjack.v2" 17 | ) 18 | 19 | // Init sets up the application's configuration directory. 20 | func Init() (*Config, error) { 21 | dataDir := getDataDir() 22 | 23 | _, err := os.Stat(dataDir) 24 | if os.IsNotExist(err) { 25 | return nil, fmt.Errorf("can't find data directory: %s", dataDir) 26 | } 27 | if err != nil { 28 | return nil, fmt.Errorf("can't read data directory %q: %s", dataDir, err) 29 | } 30 | 31 | logfile := filepath.Join(dataDir, "logs", "mud.log") 32 | 33 | // set some defaults 34 | cfg := Config{ 35 | BcryptCost: 10, 36 | Logging: &lumberjack.Logger{Filename: logfile}, 37 | } 38 | cfg.ChatMode.Enabled = "allow" 39 | cfgFile := filepath.Join(dataDir, "mud.toml") 40 | md, err := toml.DecodeFile(cfgFile, &cfg) 41 | if err != nil { 42 | return nil, fmt.Errorf("error parsing config file %q: %v", cfgFile, err) 43 | } 44 | 45 | switch strings.ToLower(cfg.ChatMode.Enabled) { 46 | case "allow", "deny", "require": 47 | default: 48 | return nil, fmt.Errorf("ChatMode.Enabled must be allow, deny, or require, but got %q", cfg.ChatMode.Enabled) 49 | } 50 | if len(md.Undecoded()) > 0 { 51 | log.Printf("WARNING: unrecognized values in mud.toml: %v", md.Undecoded()) 52 | } 53 | 54 | // ignore any data dir specified in the config... you can't really set it there 55 | cfg.DataDir = dataDir 56 | 57 | cmdFile := filepath.Join(dataDir, "commands.toml") 58 | md, err = toml.DecodeFile(cmdFile, &cfg.Commands) 59 | if err != nil { 60 | return nil, fmt.Errorf("error parsing config file %q: %v", cfgFile, err) 61 | } 62 | if len(md.Undecoded()) > 0 { 63 | log.Printf("WARNING: unrecognized values in commands.toml: %v", md.Undecoded()) 64 | } 65 | 66 | if err := configLogging(cfg.Logging); err != nil { 67 | return nil, err 68 | } 69 | log.Printf("Using data directory %s", dataDir) 70 | 71 | return &cfg, nil 72 | } 73 | 74 | // Config contains all the general configuration parameters for the mud. 75 | type Config struct { 76 | DataDir string // config and data directory 77 | StartRoom int // the starting room number 78 | MainTitle string // title screen 79 | BcryptCost int // work factor for auth 80 | Logging *lumberjack.Logger 81 | ChatMode struct { 82 | Enabled string // "allow" "deny" or "require" 83 | Default bool // whether chatmode starts enabled or not 84 | Prefix string // if not "deny", commands other than movement must start with a prefix 85 | } 86 | Direction []game.Direction 87 | Gender []game.Gender 88 | Commands world.Commands 89 | } 90 | 91 | // getDataDir returns the platform-specific data directory. 92 | func getDataDir() string { 93 | v := os.Getenv("CLAYMUD_DATADIR") 94 | if v != "" { 95 | return v 96 | } 97 | 98 | if runtime.GOOS == "windows" { 99 | return filepath.Join(os.Getenv("USERPROFILE"), "ClayMUD") 100 | } 101 | 102 | return filepath.Join(os.Getenv("HOME"), ".config", "claymud") 103 | } 104 | 105 | func configLogging(lj *lumberjack.Logger) error { 106 | log.Printf("Logging to %s", lj.Filename) 107 | log.SetOutput(io.MultiWriter(lj, os.Stdout)) 108 | 109 | log.Println("******************* ClayMUD Starting *******************") 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /server/run.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "path/filepath" 9 | "runtime" 10 | "strconv" 11 | "sync" 12 | "time" 13 | 14 | "github.com/natefinch/claymud/auth" 15 | "github.com/natefinch/claymud/db" 16 | "github.com/natefinch/claymud/game" 17 | "github.com/natefinch/claymud/game/social" 18 | "github.com/natefinch/claymud/server/config" 19 | "github.com/natefinch/claymud/util" 20 | "github.com/natefinch/claymud/world" 21 | ) 22 | 23 | // set by ldflags when you "mage build" 24 | var ( 25 | commitHash = "" 26 | timestamp = "" 27 | gitTag = "" 28 | ) 29 | 30 | // Main is the main entrypoint to the server 31 | func Main() error { 32 | var port int 33 | var version bool 34 | flag.IntVar(&port, "port", 8888, "specifies the port the server listens on") 35 | flag.BoolVar(&version, "version", false, "show version info") 36 | flag.Parse() 37 | 38 | if version { 39 | fmt.Println("ClayMUD", gitTag) 40 | fmt.Println("Build Date:", timestamp) 41 | fmt.Println("Commit:", commitHash) 42 | fmt.Println("built with:", runtime.Version()) 43 | return nil 44 | } 45 | 46 | // config must be first! 47 | cfg, err := config.Init() 48 | if err != nil { 49 | return err 50 | } 51 | log.Println("ClayMUD", gitTag) 52 | log.Println("Build Date:", timestamp) 53 | log.Println("Commit:", commitHash) 54 | log.Println("built with:", runtime.Version()) 55 | 56 | dir := cfg.DataDir 57 | game.InitGenders(cfg.Gender) 58 | game.InitDirs(cfg.Direction) 59 | if err := social.Initialize(dir); err != nil { 60 | return err 61 | } 62 | auth.Init(cfg.MainTitle, cfg.BcryptCost) 63 | 64 | // db must be before world! 65 | st, err := db.Init(dir) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | shutdown := make(chan struct{}) 71 | wg := &sync.WaitGroup{} 72 | defer func() { 73 | close(shutdown) 74 | done := make(chan struct{}) 75 | go func() { 76 | wg.Wait() 77 | close(done) 78 | }() 79 | select { 80 | case <-done: 81 | case <-time.After(10 * time.Second): 82 | log.Print("Timed out waiting for all goroutines to clean up. Killing process.") 83 | } 84 | }() 85 | wc := world.Config{ 86 | Commands: cfg.Commands, 87 | StartRoom: cfg.StartRoom, 88 | } 89 | wc.ChatMode.Default = cfg.ChatMode.Default 90 | wc.ChatMode.Prefix = cfg.ChatMode.Prefix 91 | switch cfg.ChatMode.Enabled { 92 | case "allow": 93 | wc.ChatMode.Mode = world.ChatModeAllow 94 | case "deny": 95 | wc.ChatMode.Mode = world.ChatModeDeny 96 | case "require": 97 | wc.ChatMode.Mode = world.ChatModeRequire 98 | default: 99 | // we already checked this, but belt and suspenders is ok 100 | return fmt.Errorf("Expected allow, deny, or require for ChatMode.Enabled, got %q", cfg.ChatMode.Enabled) 101 | } 102 | 103 | lock := &sync.RWMutex{} 104 | 105 | // World needs to be last. 106 | if err := world.Init(wc, dir, lock.RLocker(), shutdown, wg); err != nil { 107 | return err 108 | } 109 | if err := world.SetStart(util.ID(cfg.StartRoom)); err != nil { 110 | return err 111 | } 112 | if err := world.InitActions(filepath.Join(dir, "scripts")); err != nil { 113 | return err 114 | } 115 | global := game.SpawnWorker(lock, shutdown, wg) 116 | 117 | host := net.JoinHostPort("", strconv.Itoa(port)) 118 | log.Printf("Running ClayMUD on %v", host) 119 | 120 | addr, err := net.ResolveTCPAddr("tcp", host) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | listener, err := net.ListenTCP("tcp", addr) 126 | if err != nil { 127 | return err 128 | } 129 | for { 130 | conn, err := listener.AcceptTCP() 131 | if err != nil { 132 | log.Printf("Error accepting TCP connection: %v", err) 133 | continue 134 | } 135 | conn.SetKeepAlive(false) 136 | conn.SetLinger(0) 137 | 138 | go func() { 139 | log.Printf("New connection from %v", conn.RemoteAddr()) 140 | user, err := auth.Login(st, conn, conn.RemoteAddr()) 141 | if err != nil { 142 | log.Printf("error logging in user: ") 143 | } 144 | if err := world.SpawnPlayer(st, user, global); err != nil { 145 | log.Printf("error during spawn player for user %s: %s", user.Username, err) 146 | } 147 | }() 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /testutil/testutil.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func PatchEnv(key, value string) func() { 8 | orig := os.Getenv(key) 9 | os.Setenv(key, value) 10 | return func() { 11 | os.Setenv(key, orig) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /util/id.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // ID is a type that allows for unique identification of an object 9 | type ID uint64 10 | 11 | // Key returns a byte representation of this ID for use with a key value database. 12 | func (i ID) Key() []byte { 13 | buf := make([]byte, binary.MaxVarintLen64) 14 | binary.PutUvarint(buf, uint64(i)) 15 | 16 | return buf 17 | } 18 | 19 | // ToID converts a key/value db key into an Id. 20 | func ToID(key []byte) (ID, error) { 21 | i, err := binary.ReadUvarint(bytes.NewReader(key)) 22 | if err != nil { 23 | return 0, err 24 | } 25 | return ID(i), nil 26 | } 27 | -------------------------------------------------------------------------------- /util/id_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIdRoundTrip(t *testing.T) { 8 | var id ID = 45 9 | key := id.Key() 10 | id2, err := ToID(key) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | if id != id2 { 15 | t.Fatalf("expected %v, but got %v", id, id2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /util/query.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "strings" 8 | "unicode/utf8" 9 | ) 10 | 11 | // WriteScanner merges an io.Writer and a bufio.Scanner. 12 | type WriteScanner interface { 13 | io.Writer 14 | Scanner 15 | } 16 | 17 | // Scanner represents a bufio.Scanner. 18 | type Scanner interface { 19 | Scan() bool 20 | Err() error 21 | Text() string 22 | Bytes() []byte 23 | } 24 | 25 | // Query writes the question to rw and waits for an answer. 26 | func Query(ws WriteScanner, question string) (answer string, err error) { 27 | // need this because scan can panic if you send it too much stuff 28 | defer func() { 29 | panicErr := recover() 30 | if panicErr == nil { 31 | return 32 | } 33 | if e, ok := panicErr.(error); ok { 34 | err = e 35 | return 36 | } 37 | err = fmt.Errorf("%v", panicErr) 38 | }() 39 | _, err = io.WriteString(ws, question) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | if !ws.Scan() { 45 | if err = ws.Err(); err != nil { 46 | return "", err 47 | } 48 | return "", fmt.Errorf("Connection closed") 49 | } 50 | return ws.Text(), nil 51 | } 52 | 53 | // Query writes a question to rw and waits for an answer. It will pass the 54 | // answer into the verify function. Verify should check the answer for 55 | // validity, returning a failure reason as a string, or an empty string if the 56 | // answer is valid. 57 | func QueryVerify( 58 | ws WriteScanner, 59 | question string, 60 | verify func(string) (string, error), 61 | ) (answer string, err error) { 62 | // need this because scan can panic if you send it too much stuff 63 | defer func() { 64 | panicErr := recover() 65 | if panicErr == nil { 66 | return 67 | } 68 | if e, ok := panicErr.(error); ok { 69 | err = e 70 | return 71 | } 72 | err = fmt.Errorf("%v", panicErr) 73 | }() 74 | for { 75 | _, err = io.WriteString(ws, question) 76 | if err != nil { 77 | return "", err 78 | } 79 | if !strings.HasSuffix(question, " ") { 80 | _, err := io.WriteString(ws, " ") 81 | if err != nil { 82 | return "", err 83 | } 84 | } 85 | if !ws.Scan() { 86 | if err = ws.Err(); err != nil { 87 | return "", err 88 | } 89 | return "", fmt.Errorf("Connection closed") 90 | } 91 | answer = ws.Text() 92 | failure, err := verify(answer) 93 | if err != nil { 94 | return "", err 95 | } 96 | if failure == "" { 97 | return answer, nil 98 | } 99 | _, err = fmt.Fprintln(ws, failure) 100 | if err != nil { 101 | return "", err 102 | } 103 | } 104 | } 105 | 106 | // Opt represents an option you can choose from a list 107 | type Opt struct { 108 | Key rune 109 | Text string 110 | } 111 | 112 | // QueryStrings generates an automatic options list from the given options 113 | // and returns the chosen index. 114 | func QueryStrings( 115 | ws WriteScanner, 116 | question string, 117 | defaultIndex int, 118 | options ...string, 119 | ) (index int, err error) { 120 | defer func() { 121 | panicErr := recover() 122 | if panicErr == nil { 123 | return 124 | } 125 | if e, ok := panicErr.(error); ok { 126 | err = e 127 | return 128 | } 129 | err = fmt.Errorf("%v", panicErr) 130 | }() 131 | _, err = io.WriteString(ws, question) 132 | if err != nil { 133 | return -1, err 134 | } 135 | if !strings.HasSuffix(question, "\n") { 136 | _, err := io.WriteString(ws, "\n") 137 | if err != nil { 138 | return -1, err 139 | } 140 | } 141 | 142 | for i, s := range options { 143 | if i == defaultIndex { 144 | _, err = fmt.Fprintf(ws, "%d - %s (default)\n", i+1, s) 145 | if err != nil { 146 | return -1, err 147 | } 148 | } else { 149 | _, err = fmt.Fprintf(ws, "%d - %s\n", i+1, s) 150 | if err != nil { 151 | return -1, err 152 | } 153 | } 154 | } 155 | 156 | for { 157 | _, err := io.WriteString(ws, "\nPlease choose one of the options above: ") 158 | if err != nil { 159 | return -1, err 160 | } 161 | if !ws.Scan() { 162 | if err = ws.Err(); err != nil { 163 | return -1, err 164 | } 165 | return -1, fmt.Errorf("Connection closed") 166 | } 167 | choice := ws.Text() 168 | if len(choice) == 0 { 169 | if defaultIndex > -1 { 170 | return defaultIndex, nil 171 | } 172 | continue 173 | } 174 | i, err := strconv.Atoi(choice) 175 | if err != nil { 176 | continue 177 | } 178 | if i < 1 || i > len(options) { 179 | continue 180 | } 181 | return i - 1, nil 182 | } 183 | } 184 | 185 | // QueryOptions writes a question to rw and waits for an answer. If Default is 186 | // not 0, the given option is returns if the user hits enter. 187 | func QueryOptions( 188 | ws WriteScanner, 189 | question string, 190 | Default rune, 191 | options ...Opt, 192 | ) (answer rune, err error) { 193 | // need this because scan can panic if you send it too much stuff 194 | defer func() { 195 | panicErr := recover() 196 | if panicErr == nil { 197 | return 198 | } 199 | if e, ok := panicErr.(error); ok { 200 | err = e 201 | return 202 | } 203 | err = fmt.Errorf("%v", panicErr) 204 | }() 205 | 206 | _, err = io.WriteString(ws, question) 207 | if err != nil { 208 | return utf8.RuneError, err 209 | } 210 | if !strings.HasSuffix(question, "\n") { 211 | _, err := io.WriteString(ws, "\n") 212 | if err != nil { 213 | return utf8.RuneError, err 214 | } 215 | } 216 | for _, opt := range options { 217 | if opt.Key == Default { 218 | _, err = fmt.Fprintf(ws, "%c - %s (default)\n", opt.Key, opt.Text) 219 | if err != nil { 220 | return utf8.RuneError, err 221 | } 222 | } else { 223 | _, err = fmt.Fprintf(ws, "%c - %s\n", opt.Key, opt.Text) 224 | if err != nil { 225 | return utf8.RuneError, err 226 | } 227 | } 228 | } 229 | 230 | for { 231 | _, err := io.WriteString(ws, "\nPlease choose one of the options above: ") 232 | if err != nil { 233 | return utf8.RuneError, err 234 | } 235 | if !ws.Scan() { 236 | if err = ws.Err(); err != nil { 237 | return utf8.RuneError, err 238 | } 239 | return utf8.RuneError, fmt.Errorf("Connection closed") 240 | } 241 | b := ws.Bytes() 242 | if len(b) == 0 { 243 | if Default != 0 { 244 | return Default, nil 245 | } 246 | continue 247 | } 248 | r, _ := utf8.DecodeRune(b) 249 | if r == utf8.RuneError { 250 | continue 251 | } 252 | for _, o := range options { 253 | if r == o.Key { 254 | return r, nil 255 | } 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /util/query_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestQuery(t *testing.T) { 12 | scanner, buf := scanner("nate\n") 13 | answer, err := Query(scanner, "hi!") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | if buf.String() != "hi!" { 18 | t.Fatalf(`expected output "hi!" but got %q`, buf.String()) 19 | } 20 | if answer != "nate" { 21 | t.Fatalf(`expected answer "nate", but got %q`, answer) 22 | } 23 | } 24 | 25 | // scanner returns an io.ReadWriter that reads from input and outputs to the returned 26 | // buffer. 27 | func scanner(input string) (WriteScanner, *bytes.Buffer) { 28 | scanner := bufio.NewScanner(strings.NewReader(input)) 29 | buf := &bytes.Buffer{} 30 | 31 | return struct { 32 | *bufio.Scanner 33 | io.Writer 34 | }{ 35 | Scanner: scanner, 36 | Writer: buf, 37 | }, buf 38 | } 39 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | // Package util holds basic utility methods 2 | package util 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "text/template" 8 | ) 9 | 10 | const ( 11 | // Telnet color codes 12 | BLACK = "\033[30m" 13 | RED = "\033[31m" 14 | GREEN = "\033[32m" 15 | YELLOW = "\033[33m" 16 | BLUE = "\033[34m" 17 | MAGENTA = "\033[35m" 18 | CYAN = "\033[36m" 19 | WHITE = "\033[37m" 20 | ) 21 | 22 | // templ is a struct that lets us unmarshal directly into a template. 23 | type Template struct { 24 | *template.Template 25 | } 26 | 27 | // UnmarshalText implements TextUnmarshaler.UnmarshalText. 28 | func (e *Template) UnmarshalText(text []byte) error { 29 | var err error 30 | e.Template, err = template.New("template").Parse(string(text)) 31 | if err != nil { 32 | return fmt.Errorf("can't parse template %q: %#v", text, err) 33 | } 34 | return nil 35 | } 36 | 37 | type SafeWriter struct { 38 | Writer io.Writer 39 | OnErr func(error) 40 | } 41 | 42 | func (s SafeWriter) Write(b []byte) (int, error) { 43 | n, err := s.Writer.Write(b) 44 | if err != nil { 45 | s.OnErr(err) 46 | } 47 | return n, nil 48 | } 49 | 50 | func (s SafeWriter) WriteString(str string) (int, error) { 51 | n, err := io.WriteString(s.Writer, str) 52 | if err != nil { 53 | s.OnErr(err) 54 | } 55 | return n, nil 56 | } 57 | -------------------------------------------------------------------------------- /world/actions.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | 7 | "github.com/hippogryph/skyhook" 8 | "go.starlark.net/starlark" 9 | ) 10 | 11 | var allActions = map[string]*starlark.Program{} 12 | 13 | var actionDir string 14 | 15 | // An action is a script that can be run. 16 | type Action struct { 17 | Filename string 18 | IsGlobal bool 19 | } 20 | 21 | func InitActions(dir string) error { 22 | actionDir = dir 23 | // files, err := ioutil.ReadDir(dir) 24 | // if err != nil { 25 | // return fmt.Errorf("error reading script directory: %v", err) 26 | // } 27 | // for _, f := range files { 28 | // if f.IsDir() { 29 | // continue 30 | // } 31 | // start := time.Now() 32 | // predec := func(s string) bool { 33 | // return s == "echo" 34 | // } 35 | // filename := filepath.Join(dir, f.Name()) 36 | // log.Printf("loading action script %v", filename) 37 | // _, p, err := starlark.SourceProgram(filename, nil, predec) 38 | // if err != nil { 39 | // return fmt.Errorf("error parsing %q: %v", filename, err) 40 | // } 41 | // log.Println("time to parse a skylark file:", time.Since(start)) 42 | // allActions[f.Name()] = p 43 | // } 44 | return nil 45 | } 46 | 47 | func runLocAction(name string, actor *Player, loc *Location) error { 48 | b, err := ioutil.ReadFile(filepath.Join(actionDir, name)) 49 | if err != nil { 50 | return err 51 | } 52 | dict := map[string]interface{}{ 53 | "echo": func(msg string) { 54 | echo(loc, msg) 55 | }, 56 | "around": around, 57 | "actor": actor, 58 | "location": loc, 59 | } 60 | _, err = skyhook.Eval(b, dict) 61 | return err 62 | } 63 | 64 | // say something to all the people in the room 65 | func echo(loc *Location, msg string) { 66 | for _, p := range loc.Players { 67 | p.WriteString(msg + "\n") 68 | } 69 | } 70 | 71 | func around(player *Player, msg string) { 72 | for _, p := range player.loc.Players { 73 | if p.ID != player.ID { 74 | p.WriteString(msg + "\n") 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /world/area.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/natefinch/claymud/game" 7 | "github.com/natefinch/claymud/util" 8 | ) 9 | 10 | // Area is a small collection of related locations, such as the rooms in a 11 | // hotel. 12 | type Area struct { 13 | ID util.ID 14 | Name string 15 | Zone *Zone 16 | Locations []*Location 17 | LocByID map[util.ID]*Location 18 | } 19 | 20 | // Add adds the location to this area. 21 | func (a *Area) Add(l *Location) { 22 | l.Area = a 23 | a.Locations = append(a.Locations, l) 24 | a.LocByID[l.ID] = l 25 | } 26 | 27 | // Zone is a collection of Areas that represent one large and logically distinct 28 | // section of the mud, such as a town. 29 | type Zone struct { 30 | ID util.ID 31 | Name string 32 | Closed bool 33 | Areas []*Area 34 | *game.Worker 35 | } 36 | 37 | func (z *Zone) String() string { 38 | if z == nil { 39 | return "" 40 | } 41 | return fmt.Sprintf("", z.Name, z.ID) 42 | } 43 | 44 | // Add adds the area to this zone. 45 | func (z *Zone) Add(a *Area) { 46 | z.Areas = append(z.Areas, a) 47 | a.Zone = z 48 | } 49 | -------------------------------------------------------------------------------- /world/command.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "strings" 7 | 8 | "github.com/natefinch/claymud/game/social" 9 | ) 10 | 11 | var chatMode ChatMode 12 | 13 | // Command represents a command sent by a player. 14 | type Command struct { 15 | Actor *Player 16 | Loc *Location 17 | Cmd []string 18 | } 19 | 20 | // Action returns the keyword of the command (always lowercase). 21 | // 22 | // This is just a shortcut to access the first token in the command 23 | func (c *Command) Action() string { 24 | if len(c.Cmd) > 0 { 25 | return strings.ToLower(c.Cmd[0]) 26 | } 27 | return "" 28 | } 29 | 30 | // Target returns the string that indicates the target of the command (if any) (always lowercase) 31 | // 32 | // This is just a shortcut to access the second token in the command 33 | func (c *Command) Target() string { 34 | if len(c.Cmd) > 1 { 35 | return strings.ToLower(c.Cmd[1]) 36 | } 37 | return "" 38 | } 39 | 40 | // Text returns the text of the command, not including the keyword (or target if hasTarget is false) 41 | func (c *Command) Text(hasTarget bool) string { 42 | if hasTarget { 43 | return strings.Join(c.Cmd[2:], " ") 44 | } 45 | return strings.Join(c.Cmd[1:], " ") 46 | } 47 | 48 | // Handle executes the command, if possible. 49 | func (c *Command) Handle() { 50 | // everything in this function MUST be run through either the location's 51 | // handler or the actor's global handler. 52 | 53 | // order is important here, since earlier parsing overrides later 54 | // exits should always override anything else 55 | // socials are least important (so if you configure an social named "north" you won't 56 | // prevent yourc from going north... your social just won't work 57 | 58 | actionName := strings.Join(c.Cmd, " ") 59 | action, ok := c.Actor.loc.Actions[actionName] 60 | if ok { 61 | f := func() { 62 | if err := runLocAction(action.Filename, c.Actor, c.Actor.loc); err != nil { 63 | log.Printf("error running loc action %q in room %v: %v", actionName, c.Actor.loc.ID, err) 64 | } 65 | } 66 | if action.IsGlobal { 67 | c.Actor.HandleGlobal(f) 68 | } else { 69 | c.Actor.HandleLocal(f) 70 | } 71 | return 72 | } 73 | 74 | if !c.Actor.Flag(PFlagChatmode) { 75 | if c.handleExit() { 76 | return 77 | } 78 | if c.run() { 79 | return 80 | } 81 | if !c.handleSocial() { 82 | c.Actor.HandleLocal(func() { 83 | c.Actor.WriteString(`"` + c.Action() + `"` + " is not a valid command.") 84 | }) 85 | } 86 | return 87 | } 88 | // chatmode is on, so we run directions if they are standalone, 89 | // otherwise all commands must be prefixed by the chatmode prefix 90 | 91 | isCmd := strings.HasPrefix(c.Action(), chatMode.Prefix) 92 | if !isCmd { 93 | if c.Target() == "" && c.handleExit() { 94 | return 95 | } 96 | // default to "say" 97 | chatModeSay(c) 98 | return 99 | } 100 | // strip prefix off the command name, so the rest of our string checks work 101 | c.Cmd[0] = c.Cmd[0][len(chatMode.Prefix):] 102 | if c.run() { 103 | return 104 | } 105 | if !c.handleSocial() { 106 | c.Actor.HandleLocal(func() { 107 | c.Actor.WriteString(`"` + c.Action() + `"` + " is not a valid command.") 108 | }) 109 | } 110 | } 111 | 112 | func (c *Command) run() bool { 113 | f, ok := commands[c.Action()] 114 | if !ok { 115 | return false 116 | } 117 | f(c) 118 | return true 119 | } 120 | 121 | // Check to see if the command corresponds to an exit in the room 122 | func (c *Command) handleExit() (handled bool) { 123 | // TODO: Handle custom exits 124 | // TODO: do we reject directions with a target, like "north Bob"? 125 | valid, room := c.Loc.Exits.Find(c.Action()) 126 | if !valid { 127 | return false 128 | } 129 | 130 | if room != nil { 131 | c.Actor.Move(room) 132 | } else { 133 | c.Actor.HandleLocal(func() { 134 | c.Actor.WriteString("You can't go that way!") 135 | }) 136 | } 137 | return true 138 | } 139 | 140 | // handleSocial checks if the command is an existing social, and if so, handles 141 | // it. 142 | func (c *Command) handleSocial() (handled bool) { 143 | if !social.Exists(c.Action()) { 144 | return false 145 | } 146 | c.Actor.HandleLocal(func() { 147 | target := c.Loc.Target(c.Target()) 148 | others := []io.Writer{} 149 | for _, p := range c.Loc.Players { 150 | if !p.Is(c.Actor) { 151 | others = append(others, p) 152 | } 153 | } 154 | var t social.Person 155 | if target != nil { 156 | t = target 157 | } 158 | social.Perform(c.Action(), c.Actor, t, io.MultiWriter(others...)) 159 | }) 160 | return true 161 | } 162 | -------------------------------------------------------------------------------- /world/commands.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "text/tabwriter" 10 | "time" 11 | 12 | "github.com/natefinch/claymud/auth" 13 | "github.com/natefinch/claymud/game" 14 | "github.com/natefinch/claymud/game/social" 15 | "github.com/natefinch/claymud/util" 16 | ) 17 | 18 | var started = time.Now() 19 | 20 | var commands = map[string]func(*Command){} 21 | var allCommands []CommandCfg 22 | 23 | // Commands lets you configure how the commands get named. The list of strings for 24 | // each contain the aliases, they must all be unique. 25 | type Commands struct { 26 | Zones, 27 | Look, 28 | Who, 29 | Tell, 30 | Quit, 31 | Say, 32 | Help, 33 | Uptime, 34 | ChatMode, 35 | Goto CommandCfg 36 | } 37 | 38 | // CommandCfg defines how a command gets run, its aliases, and its help text. 39 | type CommandCfg struct { 40 | Command string 41 | Aliases []string 42 | Help string 43 | } 44 | 45 | var ( 46 | helptext string 47 | movementHelp string 48 | socialsHelp string 49 | ) 50 | 51 | // initCommands sets up the command names. 52 | func initCommands(cfg Commands) { 53 | register(zones, cfg.Zones) 54 | register(look, cfg.Look) 55 | register(who, cfg.Who) 56 | register(tell, cfg.Tell) 57 | register(quit, cfg.Quit) 58 | register(say, cfg.Say) 59 | register(help, cfg.Help) 60 | register(uptime, cfg.Uptime) 61 | register(gotoCmd, cfg.Goto) 62 | 63 | // this is a special "command" that just handles when someone hits enter without typing 64 | // anything. 65 | commands[""] = prompt 66 | 67 | // only register the chatmode command if it's allowed to be run 68 | if chatMode.Mode == ChatModeAllow { 69 | register(chatmode, cfg.ChatMode) 70 | } 71 | 72 | sort.SliceStable(allCommands, func(i, j int) bool { return allCommands[i].Command < allCommands[j].Command }) 73 | 74 | var lines []string 75 | lines = append(lines, "-- Commands --") 76 | 77 | buf := &bytes.Buffer{} 78 | w := tabwriter.NewWriter(buf, 0, 0, 0, ' ', 0) 79 | 80 | for _, c := range allCommands { 81 | aliases := append([]string{c.Command}, c.Aliases...) 82 | fmt.Fprintf(w, "%s\t %s\n", strings.Join(aliases, ", "), c.Help) 83 | } 84 | if err := w.Flush(); err != nil { 85 | // should be impossible 86 | panic(err) 87 | } 88 | lines = append(lines, buf.String()) 89 | lines = append(lines, "-- Additional Help Topics --") 90 | lines = append(lines, "socials, movement") 91 | lines = append(lines, "") 92 | 93 | helptext = strings.Join(lines, "\n") 94 | 95 | lines = []string{"-- Movement --"} 96 | for _, dir := range game.AllDirections() { 97 | lines = append(lines, fmt.Sprintf("%v, %v", dir.Name, strings.Join(dir.Aliases, ", "))) 98 | } 99 | lines = append(lines, "") 100 | movementHelp = strings.Join(lines, "\n") 101 | 102 | socialsHelp = ` 103 | Socials are special commands which will display a predetermined message. 104 | Socials can be performed standalone, directed at someone in the room (or 105 | everyone), or directed at yourself. The same social command may have 106 | different messages depending on the target of the social. The display of 107 | a social may be different for the user, the target, and any other 108 | bystanders watching the interaction take place. 109 | 110 | -- Socials -- 111 | ` + strings.Join(social.Names, "\n") + "\n" 112 | } 113 | 114 | func register(f func(*Command), cmd CommandCfg) { 115 | names := append(cmd.Aliases, cmd.Command) 116 | for _, n := range names { 117 | if _, ok := commands[n]; ok { 118 | panic(fmt.Errorf("duplicate command name: %v %#v", n, cmd)) 119 | } 120 | commands[n] = f 121 | } 122 | allCommands = append(allCommands, cmd) 123 | } 124 | 125 | // look handles the look command 126 | func look(c *Command) { 127 | c.Actor.HandleLocal(func() { 128 | if c.Target() == "" { 129 | c.Loc.ShowRoom(c.Actor) 130 | return 131 | } 132 | desc, ok := c.Loc.LookTarget(c.Target()) 133 | if ok { 134 | c.Actor.WriteString(desc) 135 | } else { 136 | c.Actor.WriteString("You don't see that here.\n") 137 | } 138 | }) 139 | } 140 | 141 | // say is when someone types the "say" command 142 | func say(c *Command) { 143 | c.Actor.HandleLocal(func() { 144 | // message is everything but the command name 145 | msg := strings.Join(c.Cmd[1:], " ") 146 | doChat(msg, c) 147 | }) 148 | } 149 | 150 | // gotoCmd is an admin command that lets you go to any room by room number or to the 151 | // room a person is in. 152 | func gotoCmd(c *Command) { 153 | if c.Target() == "" { 154 | c.Actor.HandleLocal(func() { 155 | c.Actor.WriteString("Goto where?") 156 | }) 157 | return 158 | } 159 | num, err := strconv.Atoi(c.Target()) 160 | if err == nil { 161 | loc, ok := locMap[util.ID(num)] 162 | if !ok { 163 | c.Actor.HandleLocal(func() { 164 | c.Actor.WriteString("There is no room with that number.") 165 | }) 166 | return 167 | } 168 | c.Actor.Move(loc) 169 | return 170 | } 171 | c.Actor.HandleGlobal(func() { 172 | if p, ok := playerMap[c.Target()]; ok { 173 | c.Actor.Relocate(p.loc) 174 | return 175 | } 176 | c.Actor.WriteString("There is player with that name.") 177 | }) 178 | } 179 | 180 | // chatModeSay is when someone types non-command text in chatmode. 181 | func chatModeSay(c *Command) { 182 | c.Actor.HandleLocal(func() { 183 | // message includes first word 184 | msg := strings.Join(c.Cmd, " ") 185 | doChat(msg, c) 186 | }) 187 | } 188 | 189 | func doChat(msg string, c *Command) { 190 | toOthers := c.Actor.Name() + ": " + msg 191 | for _, p := range c.Loc.Players { 192 | if !p.Is(c.Actor) { 193 | p.WriteString(toOthers) 194 | } 195 | } 196 | c.Actor.WriteString(toOthers) 197 | } 198 | 199 | func tell(c *Command) { 200 | c.Actor.HandleGlobal(func() { 201 | target, ok := FindPlayer(c.Target()) 202 | if ok { 203 | msg := strings.Join(c.Cmd[2:], " ") 204 | target.Printf("%v tells you: %v", c.Actor.Name(), msg) 205 | target.prompt() 206 | c.Actor.Printf("You tell %v: %v", target.Name(), msg) 207 | } else { 208 | c.Actor.WriteString("No one with that name exists.") 209 | } 210 | }) 211 | } 212 | 213 | func help(c *Command) { 214 | c.Actor.HandleLocal(func() { 215 | if c.Target() != "" { 216 | c.helpdetails(c.Target()) 217 | } else { 218 | c.Actor.WriteString(helptext) 219 | } 220 | }) 221 | } 222 | 223 | func who(c *Command) { 224 | c.Actor.HandleGlobal(func() { 225 | c.Actor.WriteString("[Players]\n") 226 | for _, p := range *playerList { 227 | c.Actor.WriteString(p.Name() + "\n") 228 | } 229 | }) 230 | } 231 | 232 | func prompt(c *Command) { 233 | c.Actor.reprompt() 234 | } 235 | 236 | func chatmode(c *Command) { 237 | c.Actor.HandleLocal(func() { 238 | switch c.Target() { 239 | case "?": 240 | if c.Actor.Flag(PFlagChatmode) { 241 | c.Actor.WriteString("chat mode is on") 242 | } else { 243 | c.Actor.WriteString("chat mode is off") 244 | } 245 | case "": 246 | if c.Actor.Flag(PFlagChatmode) { 247 | c.Actor.UnsetFlag(PFlagChatmode) 248 | c.Actor.WriteString("chat mode is now off") 249 | } else { 250 | c.Actor.SetFlag(PFlagChatmode) 251 | c.Actor.WriteString("chat mode is now on") 252 | } 253 | default: 254 | c.Actor.Printf("unknown command target %v", c.Target()) 255 | } 256 | }) 257 | } 258 | 259 | func uptime(c *Command) { 260 | c.Actor.HandleLocal(func() { 261 | d := time.Since(started) 262 | switch { 263 | case d > 48*time.Hour: 264 | c.Actor.Printf("%d days", int(d.Hours()/24)) 265 | case d > 2*time.Hour: 266 | c.Actor.Printf("%v", d.Round(time.Hour)) 267 | case d > 5*time.Minute: 268 | c.Actor.Printf("%v", d.Round(time.Minute)) 269 | default: 270 | c.Actor.Printf("%v", d.Round(time.Second)) 271 | } 272 | }) 273 | } 274 | 275 | func quit(c *Command) { 276 | // this must be done on the player's goroutine. 277 | c.Actor.handleQuit() 278 | } 279 | 280 | func (c *Command) helpdetails(command string) { 281 | switch strings.ToLower(command) { 282 | case "socials": 283 | c.Actor.WriteString(socialsHelp) 284 | case "movement": 285 | c.Actor.WriteString(movementHelp) 286 | default: 287 | for _, cmd := range allCommands { 288 | if command == cmd.Command || contains(command, cmd.Aliases) { 289 | c.Actor.WriteString(cmd.Help) 290 | break 291 | } 292 | } 293 | } 294 | } 295 | 296 | func contains(s string, vals []string) bool { 297 | for i := range vals { 298 | if s == vals[i] { 299 | return true 300 | } 301 | } 302 | return false 303 | } 304 | 305 | func zones(c *Command) { 306 | c.Actor.WriteString("-- Zones --\n") 307 | for _, z := range allZones { 308 | if c.Actor.User.Flag(auth.UFlagAdmin) { 309 | c.Actor.WriteString(z.Name) 310 | if z.Closed { 311 | c.Actor.WriteString(" [closed]") 312 | } 313 | c.Actor.WriteString("\n") 314 | } else { 315 | if !z.Closed { 316 | c.Actor.WriteString(z.Name + "\n") 317 | } 318 | } 319 | } 320 | c.Actor.WriteString("\n") 321 | } 322 | -------------------------------------------------------------------------------- /world/exit.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import "github.com/natefinch/claymud/game" 4 | 5 | // Exit is a direction that connects to a location. 6 | type Exit struct { 7 | game.Direction 8 | Desc string 9 | Destination *Location 10 | } 11 | 12 | // Exits is a sorted list of exits for a location. 13 | type Exits []Exit 14 | 15 | // Len implements sort.Interface.Len. 16 | func (e Exits) Len() int { 17 | return len(e) 18 | } 19 | 20 | // Swap implements sort.Interface.Swap. 21 | func (e Exits) Swap(i, j int) { 22 | e[i], e[j] = e[j], e[i] 23 | } 24 | 25 | // Less implements sort.Interface.Less. 26 | func (e Exits) Less(i, j int) bool { 27 | return e[i].ID < e[j].ID 28 | } 29 | 30 | // Find returns the room that exists in the given direction. Returns valid == false if 31 | // the alias is not a valid direction alias. Returns dest == nil if there's no exit in 32 | // that direction. 33 | func (e Exits) Find(alias string) (valid bool, dest *Location) { 34 | dir, found := game.FindDirection(alias) 35 | if !found { 36 | return false, nil 37 | } 38 | for _, exit := range e { 39 | if exit.Direction.ID == dir.ID { 40 | return true, exit.Destination 41 | } 42 | } 43 | return true, nil 44 | } 45 | -------------------------------------------------------------------------------- /world/global_test.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSortedPlayers(t *testing.T) { 9 | sp := sortedPlayers{} 10 | bob := &Player{ID: 1, name: "Bob"} 11 | alice := &Player{ID: 2, name: "Alice"} 12 | chris := &Player{ID: 3, name: "Chris"} 13 | sp.add(bob) 14 | sp.add(alice) 15 | sp.add(chris) 16 | 17 | order := fmt.Sprintf("%v%v%v", sp[0].ID, sp[1].ID, sp[2].ID) 18 | expected := "213" 19 | if order != expected { 20 | t.Fatalf("after add, should be %v, was %v", expected, order) 21 | } 22 | sp.remove(bob) 23 | if len(sp) != 2 { 24 | t.Errorf("len should be 2, was %v", len(sp)) 25 | } 26 | order = fmt.Sprintf("%v%v", sp[0].ID, sp[1].ID) 27 | expected = "23" 28 | if order != expected { 29 | t.Fatalf("after remove, should be %v, was %v", expected, order) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /world/init.go: -------------------------------------------------------------------------------- 1 | // Package world holds most of the MUD code, including locations, players, etc 2 | package world 3 | 4 | import ( 5 | "sync" 6 | ) 7 | 8 | // ChatModeMode determines whether ChatMode is allowed to be on, required to be on, or not allowed to be on. 9 | type ChatModeMode int 10 | 11 | // Allow, disallow, or require chatmode. 12 | const ( 13 | ChatModeAllow ChatModeMode = iota 14 | ChatModeDeny 15 | ChatModeRequire 16 | ) 17 | 18 | // ChatMode defines if and how users enter chatmode. 19 | type ChatMode struct { 20 | Default bool // Whether players default to being in chat mode 21 | Prefix string // prefix required for commands in chatmode 22 | Mode ChatModeMode 23 | } 24 | 25 | // Config determines the configuration of the world. 26 | type Config struct { 27 | StartRoom int // ID of room players start in 28 | Commands Commands // command names 29 | ChatMode ChatMode 30 | } 31 | 32 | // Init spawns the zones and their attendant workers, creates all areas 33 | // and locations. 34 | func Init(cfg Config, datadir string, zoneLock sync.Locker, shutdown <-chan struct{}, wg *sync.WaitGroup) error { 35 | if err := loadLocTempl(datadir); err != nil { 36 | return err 37 | } 38 | 39 | chatMode = cfg.ChatMode 40 | 41 | // ensure that require or deny have the corresponding on or off default 42 | switch cfg.ChatMode.Mode { 43 | case ChatModeRequire: 44 | chatMode.Default = true 45 | case ChatModeDeny: 46 | chatMode.Default = false 47 | default: 48 | // whatever the config set is fine. 49 | } 50 | initCommands(cfg.Commands) 51 | return loadWorld(datadir, zoneLock, shutdown, wg) 52 | } 53 | -------------------------------------------------------------------------------- /world/location.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "path/filepath" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/natefinch/claymud/util" 12 | ) 13 | 14 | var ( 15 | locMap = map[util.ID]*Location{} 16 | start *Location 17 | ) 18 | 19 | // SetStart sets the starting room of the mud. 20 | func SetStart(room util.ID) error { 21 | loc, exists := locMap[room] 22 | if !exists { 23 | return fmt.Errorf("starting room %v does not exist", room) 24 | } 25 | start = loc 26 | return nil 27 | } 28 | 29 | // Start returns the start room of the MUD, where players appear 30 | // TODO: multiple / configurable start rooms 31 | func Start() *Location { 32 | return start 33 | } 34 | 35 | var locTemplate *template.Template 36 | 37 | // A Location in the mud, such as a room 38 | type Location struct { 39 | ID util.ID 40 | Name string 41 | Desc string 42 | Exits 43 | Area *Area 44 | Players map[string]*Player 45 | Descriptions map[string]string 46 | 47 | // LocalActions is a map of command phrases to script names that get run in a zone-local thread. 48 | Actions map[string]Action 49 | } 50 | 51 | // returns a string representation of this location (primarily for logging) 52 | func (l *Location) String() string { 53 | return fmt.Sprintf("%v [%v]", l.Name, l.ID) 54 | } 55 | 56 | // LocalTo returns true if the other location uses the same Worker as this 57 | // location. 58 | func (l *Location) LocalTo(other *Location) bool { 59 | return l.Area.Zone.Worker == other.Area.Zone.Worker 60 | } 61 | 62 | // AddPlayer syncs the player's location with the location's player list. 63 | // TODO: Handle enter/exit notifications for others in the room 64 | func (l *Location) AddPlayer(p *Player) { 65 | l.Players[strings.ToLower(p.Name())] = p 66 | } 67 | 68 | // Handle handles a zone-local event. 69 | func (l *Location) Handle(event func()) { 70 | l.Area.Zone.Handle(event) 71 | } 72 | 73 | // RemovePlayer removes a player from this room. 74 | func (l *Location) RemovePlayer(p *Player) { 75 | delete(l.Players, strings.ToLower(p.Name())) 76 | } 77 | 78 | // Target returns a target from the room with the given name or nil if none. 79 | func (l *Location) Target(target string) *Player { 80 | return l.Players[target] 81 | } 82 | 83 | // LookTarget returns the description of the target in the room, and if a target was 84 | // found. 85 | func (l *Location) LookTarget(target string) (string, bool) { 86 | p, ok := l.Players[target] 87 | if ok { 88 | return p.Desc, true 89 | } 90 | desc, ok := l.Descriptions[target] 91 | if ok { 92 | return desc, true 93 | } 94 | return "", false 95 | } 96 | 97 | // ShowRoom displays the room description from the point of view of the given 98 | // actor. 99 | func (l *Location) ShowRoom(actor *Player) { 100 | locTemplate.Execute(actor, locData{actor, l}) 101 | } 102 | 103 | func loadLocTempl(datadir string) error { 104 | path := filepath.Join(datadir, "location.template") 105 | log.Printf("Loading location template from %s", path) 106 | 107 | b, err := ioutil.ReadFile(path) 108 | if err != nil { 109 | return fmt.Errorf("error reading location template file: %s", err) 110 | } 111 | 112 | locTemplate, err = template.New("location.template").Parse(string(b)) 113 | if err != nil { 114 | return fmt.Errorf("can't parse location template: %s", err) 115 | } 116 | return nil 117 | } 118 | 119 | type locData struct { 120 | Actor *Player 121 | *Location 122 | } 123 | -------------------------------------------------------------------------------- /world/mob.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "github.com/natefinch/claymud/game" 5 | "github.com/natefinch/claymud/util" 6 | ) 7 | 8 | type Mob struct { 9 | ID util.ID 10 | Aliases []string 11 | Name string 12 | LongDesc string 13 | DetailedDesc string 14 | //Actions *big.Int 15 | //Affections *big.Int 16 | Alignment int 17 | Level int 18 | THAC0 int 19 | AC int 20 | HP game.Dice // xdy+z 21 | Damage game.Dice // xdy+z 22 | Gold int 23 | XP int 24 | //LoadPosition game.Position 25 | //DefaultPosition game.Position 26 | Gender game.Gender 27 | } 28 | -------------------------------------------------------------------------------- /world/player_test.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import () 4 | 5 | var () 6 | --------------------------------------------------------------------------------