├── types ├── types_test.go ├── coordinate.go ├── color.go └── types.go ├── session ├── session_test.go ├── mapbuilder.go ├── session.go └── actions.go ├── .gitignore ├── bots ├── start_bots.sh ├── create_unit_users.py └── bot.py ├── bin ├── get_pexpect └── coverage ├── TODO ├── README ├── utils ├── set.go ├── menu_test.go ├── naturalsort │ ├── naturalsort_test.go │ └── naturalsort.go ├── menu.go ├── utils.go └── utils_test.go ├── database ├── zone.go ├── store.go ├── area.go ├── container.go ├── world.go ├── skill.go ├── dbobject.go ├── mongowrapper.go ├── effect.go ├── item.go ├── user.go ├── room.go ├── database.go ├── character.go └── dbtest │ └── dbtest.go ├── kmud.go ├── events ├── events_test.go └── events.go ├── LICENSE ├── datastore └── datastore.go ├── combat ├── combat_test.go └── combat.go ├── engine ├── engine.go └── pathing.go ├── testutils ├── testutils.go └── mocks.go ├── model ├── model_test.go └── model.go ├── telnet ├── telnet_test.go └── telnet.go └── server └── server.go /types/types_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | -------------------------------------------------------------------------------- /session/session_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | server.exe 2 | kmud 3 | *.swp 4 | *.swo 5 | html 6 | coverage/ 7 | -------------------------------------------------------------------------------- /bots/start_bots.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "`dirname "$0"`" 4 | 5 | for i in {1..10}; do 6 | ./bot.py localhost 8945 unit$i unit123 & 7 | done 8 | -------------------------------------------------------------------------------- /bin/get_pexpect: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl -OL http://pexpect.sourceforge.net/pexpect-2.3.tar.gz 4 | tar -xvzf pexpect-2.3.tar.gz 5 | cd pexpect-2.3 6 | sudo python ./setup.py install 7 | sudo rm -rf pexpect-2.3 pexpect-2.3.tar.gz 8 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * More unit tests 2 | * NPC conversation topics 3 | * Currency giving, dropping 4 | * Trading 5 | * Custom room views 6 | * Custom room actions 7 | * Input speed limit (at all input possibilities) 8 | * Spell checking 9 | * Party/grouping 10 | * Skills 11 | * Classes 12 | * Stats 13 | * Item templating 14 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | go get github.com/Cristofori/kmud 2 | go install github.com/Cristofori/kmud 3 | 4 | 5 | Dependencies 6 | ============ 7 | Google Go v1.2 8 | MongoDB: www.mongodb.org 9 | 10 | mgo: http://labix.org/mgo 11 | go get gopkg.in/mgo.v2 12 | 13 | go check: http://labix.org/gocheck 14 | go get gopkg.in/check.v1 15 | -------------------------------------------------------------------------------- /utils/set.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Set map[string]bool 4 | 5 | func (self Set) Contains(key string) bool { 6 | _, found := self[key] 7 | return found 8 | } 9 | 10 | func (self Set) Insert(key string) { 11 | self[key] = true 12 | } 13 | 14 | func (self Set) Remove(key string) { 15 | delete(self, key) 16 | } 17 | 18 | func (self Set) Size() int { 19 | return len(self) 20 | } 21 | -------------------------------------------------------------------------------- /database/zone.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "github.com/Cristofori/kmud/utils" 4 | 5 | type Zone struct { 6 | DbObject `bson:",inline"` 7 | 8 | Name string 9 | } 10 | 11 | func NewZone(name string) *Zone { 12 | zone := &Zone{ 13 | Name: utils.FormatName(name), 14 | } 15 | 16 | dbinit(zone) 17 | return zone 18 | } 19 | 20 | func (self *Zone) GetName() string { 21 | self.ReadLock() 22 | defer self.ReadUnlock() 23 | return self.Name 24 | } 25 | 26 | func (self *Zone) SetName(name string) { 27 | self.writeLock(func() { 28 | self.Name = utils.FormatName(name) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /kmud.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "os" 6 | "os/signal" 7 | "runtime" 8 | "time" 9 | 10 | "github.com/Cristofori/kmud/server" 11 | ) 12 | 13 | func main() { 14 | runtime.GOMAXPROCS(runtime.NumCPU()) 15 | rand.Seed(time.Now().UnixNano()) 16 | 17 | go signalHandler() 18 | 19 | var s server.Server 20 | s.Exec() 21 | } 22 | 23 | func signalHandler() { 24 | c := make(chan os.Signal, 1) 25 | signal.Notify(c, os.Interrupt) 26 | 27 | for { 28 | <-c 29 | // stack := make([]byte, 1024*10) 30 | // runtime.Stack(stack, true) 31 | // os.Stderr.Write(stack) 32 | os.Exit(0) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/store.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/Cristofori/kmud/types" 5 | "github.com/Cristofori/kmud/utils" 6 | ) 7 | 8 | type Store struct { 9 | Container `bson:",inline"` 10 | 11 | Name string 12 | RoomId types.Id 13 | } 14 | 15 | func NewStore(name string, roomId types.Id) *Store { 16 | store := &Store{ 17 | Name: utils.FormatName(name), 18 | RoomId: roomId, 19 | } 20 | 21 | dbinit(store) 22 | return store 23 | } 24 | 25 | func (self *Store) GetName() string { 26 | self.ReadLock() 27 | defer self.ReadUnlock() 28 | 29 | return self.Name 30 | } 31 | 32 | func (self *Store) SetName(name string) { 33 | self.writeLock(func() { 34 | self.Name = utils.FormatName(name) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /database/area.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/Cristofori/kmud/types" 5 | "github.com/Cristofori/kmud/utils" 6 | ) 7 | 8 | type Area struct { 9 | DbObject `bson:",inline"` 10 | Name string 11 | ZoneId types.Id 12 | } 13 | 14 | func NewArea(name string, zone types.Id) *Area { 15 | area := &Area{ 16 | ZoneId: zone, 17 | Name: utils.FormatName(name), 18 | } 19 | dbinit(area) 20 | return area 21 | } 22 | 23 | func (self *Area) GetName() string { 24 | self.ReadLock() 25 | defer self.ReadUnlock() 26 | return self.Name 27 | } 28 | 29 | func (self *Area) SetName(name string) { 30 | self.writeLock(func() { 31 | self.Name = name 32 | }) 33 | } 34 | 35 | func (self *Area) GetZoneId() types.Id { 36 | self.ReadLock() 37 | defer self.ReadUnlock() 38 | return self.ZoneId 39 | } 40 | -------------------------------------------------------------------------------- /database/container.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | type Container struct { 4 | DbObject `bson:",inline"` 5 | Cash int 6 | Capacity int 7 | Weight int 8 | } 9 | 10 | func (self *Container) SetCash(cash int) { 11 | self.writeLock(func() { 12 | self.Cash = cash 13 | }) 14 | } 15 | 16 | func (self *Container) GetCash() int { 17 | self.ReadLock() 18 | defer self.ReadUnlock() 19 | return self.Cash 20 | } 21 | 22 | func (self *Container) AddCash(amount int) { 23 | self.SetCash(self.GetCash() + amount) 24 | } 25 | 26 | func (self *Container) RemoveCash(amount int) { 27 | self.SetCash(self.GetCash() - amount) 28 | } 29 | 30 | func (self *Container) GetCapacity() int { 31 | self.ReadLock() 32 | defer self.ReadUnlock() 33 | return self.Capacity 34 | } 35 | 36 | func (self *Container) SetCapacity(limit int) { 37 | self.writeLock(func() { 38 | self.Capacity = limit 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /bin/coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")"/.. 4 | mkdir -p coverage/html 5 | 6 | PROJECT="github.com/Cristofori/kmud" 7 | HTML_INDEX="coverage/index.html" 8 | 9 | PACKAGES=" 10 | database 11 | datastore 12 | engine 13 | events 14 | model 15 | server 16 | session 17 | testutils 18 | types 19 | utils 20 | " 21 | 22 | for package in $PACKAGES; do 23 | go test -coverprofile="coverage/$package.out" $PROJECT/$package 24 | go tool cover -html="coverage/$package.out" -o coverage/html/$package.html 25 | done 26 | 27 | cat < $HTML_INDEX 28 | 29 | Code coverage report for kmud 30 | 31 | 40 | 41 | 42 | EOF 43 | -------------------------------------------------------------------------------- /database/world.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/Cristofori/kmud/types" 8 | ) 9 | 10 | type World struct { 11 | DbObject `bson:",inline"` 12 | Time time.Time 13 | } 14 | 15 | func NewWorld() *World { 16 | world := &World{Time: time.Now()} 17 | 18 | dbinit(world) 19 | return world 20 | } 21 | 22 | type _time struct { 23 | hour int 24 | min int 25 | sec int 26 | } 27 | 28 | func (self _time) String() string { 29 | return fmt.Sprintf("%02d:%02d:%02d", self.hour, self.min, self.sec) 30 | } 31 | 32 | const _TIME_MULTIPLIER = 3 33 | 34 | func (self *World) GetTime() types.Time { 35 | self.ReadLock() 36 | defer self.ReadUnlock() 37 | 38 | hour, min, sec := self.Time.Clock() 39 | return _time{hour: hour, min: min, sec: sec} 40 | } 41 | 42 | func (self *World) AdvanceTime() { 43 | self.writeLock(func() { 44 | self.Time = self.Time.Add(3 * time.Second) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /events/events_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Cristofori/kmud/testutils" 8 | . "gopkg.in/check.v1" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type EventSuite struct{} 14 | 15 | var _ = Suite(&EventSuite{}) 16 | 17 | func (s *EventSuite) TestEventLoop(c *C) { 18 | 19 | char := testutils.NewMockPC() 20 | 21 | eventChannel := Register(char) 22 | 23 | message := "hey how are yah" 24 | Broadcast(TellEvent{char, char, message}) 25 | 26 | select { 27 | case event := <-eventChannel: 28 | gotTellEvent := false 29 | 30 | switch e := event.(type) { 31 | case TellEvent: 32 | gotTellEvent = true 33 | c.Assert(e.Message, Equals, message) 34 | 35 | } 36 | 37 | if gotTellEvent == false { 38 | c.Fatalf("Didn't get a Tell event back") 39 | } 40 | case <-time.After(3 * time.Second): 41 | c.Fatalf("Timed out waiting for tell event") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Chris Thatcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /datastore/datastore.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/Cristofori/kmud/types" 7 | ) 8 | 9 | var _data map[types.Id]types.Object 10 | var _mutex sync.RWMutex 11 | 12 | func init() { 13 | ClearAll() 14 | } 15 | 16 | func Get(id types.Id) types.Object { 17 | _mutex.RLock() 18 | defer _mutex.RUnlock() 19 | 20 | return _data[id] 21 | } 22 | 23 | func Contains(obj types.Identifiable) bool { 24 | _mutex.RLock() 25 | defer _mutex.RUnlock() 26 | 27 | _, found := _data[obj.GetId()] 28 | return found 29 | } 30 | 31 | func ContainsId(id types.Id) bool { 32 | _mutex.RLock() 33 | defer _mutex.RUnlock() 34 | 35 | _, found := _data[id] 36 | return found 37 | } 38 | 39 | func Set(obj types.Object) { 40 | _mutex.Lock() 41 | defer _mutex.Unlock() 42 | 43 | _data[obj.GetId()] = obj 44 | } 45 | 46 | func Remove(obj types.Identifiable) { 47 | RemoveId(obj.GetId()) 48 | } 49 | 50 | func RemoveId(id types.Id) { 51 | _mutex.Lock() 52 | defer _mutex.Unlock() 53 | 54 | delete(_data, id) 55 | } 56 | 57 | func ClearAll() { 58 | _mutex.Lock() 59 | defer _mutex.Unlock() 60 | 61 | _data = map[types.Id]types.Object{} 62 | } 63 | -------------------------------------------------------------------------------- /database/skill.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/Cristofori/kmud/types" 5 | "github.com/Cristofori/kmud/utils" 6 | ) 7 | 8 | type Skill struct { 9 | DbObject `bson:",inline"` 10 | 11 | Effects utils.Set 12 | Name string 13 | } 14 | 15 | func NewSkill(name string) *Skill { 16 | skill := &Skill{ 17 | Name: utils.FormatName(name), 18 | } 19 | 20 | dbinit(skill) 21 | return skill 22 | } 23 | 24 | func (self *Skill) GetName() string { 25 | self.ReadLock() 26 | defer self.ReadUnlock() 27 | return self.Name 28 | } 29 | 30 | func (self *Skill) SetName(name string) { 31 | self.writeLock(func() { 32 | self.Name = utils.FormatName(name) 33 | }) 34 | } 35 | 36 | func (self *Skill) AddEffect(id types.Id) { 37 | self.writeLock(func() { 38 | if self.Effects == nil { 39 | self.Effects = utils.Set{} 40 | } 41 | self.Effects.Insert(id.Hex()) 42 | }) 43 | } 44 | 45 | func (self *Skill) RemoveEffect(id types.Id) { 46 | self.writeLock(func() { 47 | self.Effects.Remove(id.Hex()) 48 | }) 49 | } 50 | 51 | func (self *Skill) GetEffects() []types.Id { 52 | self.ReadLock() 53 | defer self.ReadUnlock() 54 | return idSetToList(self.Effects) 55 | } 56 | 57 | func (self *Skill) HasEffect(id types.Id) bool { 58 | self.ReadLock() 59 | defer self.ReadUnlock() 60 | return self.Effects.Contains(id.Hex()) 61 | } 62 | -------------------------------------------------------------------------------- /database/dbobject.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/Cristofori/kmud/types" 7 | "github.com/Cristofori/kmud/utils" 8 | "gopkg.in/mgo.v2/bson" 9 | ) 10 | 11 | type DbObject struct { 12 | Id types.Id `bson:"_id"` 13 | 14 | mutex sync.RWMutex 15 | destroyed bool 16 | } 17 | 18 | func (self *DbObject) SetId(id types.Id) { 19 | self.Id = id 20 | } 21 | 22 | func (self *DbObject) GetId() types.Id { 23 | return self.Id 24 | } 25 | 26 | func (self *DbObject) ReadLock() { 27 | self.mutex.RLock() 28 | } 29 | 30 | func (self *DbObject) ReadUnlock() { 31 | self.mutex.RUnlock() 32 | } 33 | 34 | func (self *DbObject) WriteLock() { 35 | self.mutex.Lock() 36 | } 37 | 38 | func (self *DbObject) writeLock(worker func()) { 39 | self.WriteLock() 40 | defer self.WriteUnlock() 41 | defer self.modified() 42 | worker() 43 | } 44 | 45 | func (self *DbObject) WriteUnlock() { 46 | self.mutex.Unlock() 47 | } 48 | 49 | func (self *DbObject) Destroy() { 50 | self.WriteLock() 51 | defer self.WriteUnlock() 52 | 53 | self.destroyed = true 54 | } 55 | 56 | func (self *DbObject) IsDestroyed() bool { 57 | self.ReadLock() 58 | defer self.ReadUnlock() 59 | 60 | return self.destroyed 61 | } 62 | 63 | func (self *DbObject) modified() { 64 | modifiedObjectChannel <- self.Id 65 | } 66 | 67 | func (self *DbObject) syncModified() { 68 | commitObject(self.Id) 69 | } 70 | 71 | func idSetToList(set utils.Set) []types.Id { 72 | ids := make([]types.Id, len(set)) 73 | 74 | i := 0 75 | for id := range set { 76 | ids[i] = bson.ObjectIdHex(id) 77 | i++ 78 | } 79 | 80 | return ids 81 | } 82 | -------------------------------------------------------------------------------- /utils/menu_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/Cristofori/kmud/testutils" 8 | "github.com/Cristofori/kmud/types" 9 | ) 10 | 11 | func Test_Menu(t *testing.T) { 12 | var comm testutils.TestCommunicable 13 | comm.ToRead = "a" 14 | 15 | called := false 16 | handled := false 17 | onExitCalled := false 18 | 19 | ExecMenu("Menu", &comm, func(menu *Menu) { 20 | called = true 21 | 22 | menu.AddAction("a", "Apple", func() { 23 | handled = true 24 | menu.Exit() 25 | }) 26 | 27 | menu.OnExit(func() { 28 | onExitCalled = true 29 | }) 30 | }) 31 | 32 | testutils.Assert(called == true, t, "Failed to exec menu") 33 | testutils.Assert(handled == true, t, "Failed to handle menu action") 34 | testutils.Assert(onExitCalled == true, t, "Failed to call the OnExit handler") 35 | } 36 | 37 | func Test_Search(t *testing.T) { 38 | var comm1 testutils.TestCommunicable 39 | var comm2 testutils.TestCommunicable 40 | var menu1 Menu 41 | var menu2 Menu 42 | 43 | title := "Menu" 44 | menu1.SetTitle(title) 45 | menu2.SetTitle(title) 46 | 47 | menu1.AddActionI(0, "Action One", func() { 48 | menu1.Exit() 49 | }) 50 | menu2.AddActionI(0, "Action One", func() { 51 | menu2.Exit() 52 | }) 53 | 54 | menu2.AddActionI(1, "Action Two", func() { 55 | menu1.Exit() 56 | }) 57 | 58 | filter := "one" 59 | menu1.Print(&comm1, 0, filter) 60 | menu2.Print(&comm2, 0, filter) 61 | 62 | testutils.Assert(comm1.Wrote == comm2.Wrote, t, fmt.Sprintf("Failed to correctly filter menu, got: \n%s, expected: \n%s", 63 | types.StripColors(comm2.Wrote), types.StripColors(comm1.Wrote))) 64 | } 65 | -------------------------------------------------------------------------------- /utils/naturalsort/naturalsort_test.go: -------------------------------------------------------------------------------- 1 | package naturalsort 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func Test_piecesOf(t *testing.T) { 11 | var tests = []struct { 12 | input string 13 | output []string 14 | }{ 15 | {"", []string{}}, 16 | {"foo", []string{"foo"}}, 17 | {"123", []string{"123"}}, 18 | {"foo1bar", []string{"foo", "1", "bar"}}, 19 | {"2bar3foo", []string{"2", "bar", "3", "foo"}}, 20 | } 21 | 22 | for _, test := range tests { 23 | result := piecesOf(test.input) 24 | 25 | if reflect.DeepEqual(result, test.output) == false { 26 | t.Errorf("piecesOf(%v) == %v. Want %v", test.input, result, test.output) 27 | } 28 | } 29 | } 30 | 31 | func Test_sort(t *testing.T) { 32 | input := SortableStrings{ 33 | "1", 34 | "a", 35 | "10", 36 | "pony", 37 | "11", 38 | "3", 39 | "went", 40 | "2", 41 | "to", 42 | "4", 43 | "market", 44 | "y10", 45 | "y1", 46 | "y20", 47 | "y2", 48 | "z10x10", 49 | "z10x2", 50 | "z10x4", 51 | "z10x3", 52 | "z10x44", 53 | "z10x33", 54 | "z10x1", 55 | } 56 | 57 | output := SortableStrings{ 58 | "1", 59 | "2", 60 | "3", 61 | "4", 62 | "10", 63 | "11", 64 | "a", 65 | "market", 66 | "pony", 67 | "to", 68 | "went", 69 | "y1", 70 | "y2", 71 | "y10", 72 | "y20", 73 | "z10x1", 74 | "z10x2", 75 | "z10x3", 76 | "z10x4", 77 | "z10x10", 78 | "z10x33", 79 | "z10x44", 80 | } 81 | 82 | sort.Sort(input) 83 | 84 | if reflect.DeepEqual(input, output) == false { 85 | t.Errorf("Undeisred sorting result: %v\n", strings.Join(input, ", ")) 86 | t.Errorf("Wanted : %v\n", strings.Join(output, ", ")) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /utils/naturalsort/naturalsort.go: -------------------------------------------------------------------------------- 1 | package naturalsort 2 | 3 | import ( 4 | "strconv" 5 | "unicode" 6 | ) 7 | 8 | // SortableStrings implements the sort.Interface for a []string. It uses "natural" sort 9 | // order rather than asciibetical sort order. 10 | type SortableStrings []string 11 | 12 | func (s SortableStrings) Len() int { 13 | return len(s) 14 | } 15 | 16 | func (s SortableStrings) Swap(i, j int) { 17 | s[i], s[j] = s[j], s[i] 18 | } 19 | 20 | func (s SortableStrings) Less(i, j int) bool { 21 | return NaturalLessThan(s[i], s[j]) 22 | } 23 | 24 | func NaturalLessThan(str1, str2 string) bool { 25 | pieces1 := piecesOf(str1) 26 | pieces2 := piecesOf(str2) 27 | 28 | for i, piece1 := range pieces1 { 29 | if i >= len(pieces2) { 30 | return true 31 | } 32 | 33 | piece2 := pieces2[i] 34 | 35 | if piece1 != piece2 { 36 | if unicode.IsDigit(rune(piece1[0])) && unicode.IsDigit(rune(piece2[0])) { 37 | num1, _ := strconv.Atoi(piece1) 38 | num2, _ := strconv.Atoi(piece2) 39 | return num1 < num2 40 | } else { 41 | return piece1 < piece2 42 | } 43 | } 44 | } 45 | 46 | return false 47 | } 48 | 49 | func piecesOf(str string) []string { 50 | pieces := []string{} 51 | 52 | if len(str) == 0 { 53 | return pieces 54 | } 55 | 56 | type Mode int 57 | const ( 58 | CharMode = iota 59 | NumMode = iota 60 | ) 61 | 62 | currentMode := CharMode 63 | 64 | if unicode.IsDigit(rune(str[0])) { 65 | currentMode = NumMode 66 | } 67 | 68 | begin := 0 69 | 70 | for i, c := range str { 71 | newMode := CharMode 72 | if unicode.IsDigit(c) { 73 | newMode = NumMode 74 | } 75 | 76 | if newMode != currentMode { 77 | pieces = append(pieces, str[begin:i]) 78 | begin = i 79 | currentMode = newMode 80 | } 81 | } 82 | 83 | pieces = append(pieces, str[begin:]) 84 | 85 | return pieces 86 | } 87 | -------------------------------------------------------------------------------- /bots/create_unit_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import pexpect, sys, exceptions 4 | 5 | host = "localhost" 6 | port = 8945 7 | 8 | baseUsername = "unit" 9 | password = "unit123" 10 | 11 | def usage(): 12 | print "Usage: %s [count] [host] [port]" % sys.argv[0] 13 | sys.exit(1) 14 | 15 | if len(sys.argv) < 2: 16 | usage() 17 | 18 | try: 19 | userCount = int(sys.argv[1]) 20 | except exceptions.ValueError: 21 | usage() 22 | 23 | if len(sys.argv) > 2: 24 | host = sys.argv[2] 25 | 26 | if len(sys.argv) > 3: 27 | try: 28 | port = int(sys.argv[3]) 29 | except exceptions.ValueError: 30 | usage() 31 | 32 | telnetCommand = 'telnet %s %s' % (host, port) 33 | 34 | print 'Connecting...' 35 | telnet = pexpect.spawn(telnetCommand, timeout=5) 36 | 37 | patterns = telnet.compile_pattern_list(['> $', 'Desired username', 'unavailable', 'Desired password', 'Confirm password', pexpect.TIMEOUT]) 38 | 39 | for i in range(userCount): 40 | username = baseUsername + str(i+1) 41 | while True: 42 | index = telnet.expect(patterns) 43 | 44 | if index == 0: 45 | telnet.sendline("n") 46 | elif index == 1: 47 | telnet.sendline(username) 48 | print 'Creating user %s' % username 49 | elif index == 2: 50 | break # User already exists, move on 51 | elif index == 3: 52 | telnet.sendline(password) 53 | elif index == 4: 54 | telnet.sendline(password) 55 | 56 | while True: 57 | charPatterns = telnet.compile_pattern_list(['> $', 'Desired character name', pexpect.TIMEOUT]) 58 | index = telnet.expect(charPatterns) 59 | 60 | if index == 0: 61 | telnet.sendline("n") 62 | elif index == 1: 63 | telnet.sendline(username) 64 | print 'Creating character %s' % username 65 | telnet.sendline("x") 66 | telnet.sendline("x") 67 | break 68 | 69 | break 70 | 71 | else: 72 | print 'Timeout, %s' % username 73 | break 74 | 75 | -------------------------------------------------------------------------------- /database/mongowrapper.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | ) 6 | 7 | func NewMongoSession(session *mgo.Session) *MongoSession { 8 | var mongoSession MongoSession 9 | mongoSession.session = session 10 | return &mongoSession 11 | } 12 | 13 | type MongoSession struct { 14 | session *mgo.Session 15 | } 16 | 17 | func (ms MongoSession) DB(dbName string) Database { 18 | var db MongoDatabase 19 | db.database = ms.session.DB(dbName) 20 | return &db 21 | } 22 | 23 | type MongoDatabase struct { 24 | database *mgo.Database 25 | } 26 | 27 | func (md MongoDatabase) C(collectionName string) Collection { 28 | return &MongoCollection{collection: md.database.C(collectionName)} 29 | } 30 | 31 | type MongoCollection struct { 32 | collection *mgo.Collection 33 | } 34 | 35 | func (mc MongoCollection) FindId(id interface{}) Query { 36 | return &MongoQuery{query: mc.collection.FindId(id)} 37 | } 38 | 39 | func (mc MongoCollection) Find(selector interface{}) Query { 40 | return &MongoQuery{query: mc.collection.Find(selector)} 41 | } 42 | 43 | func (mc MongoCollection) RemoveId(id interface{}) error { 44 | return mc.collection.RemoveId(id) 45 | } 46 | 47 | func (mc MongoCollection) Remove(selector interface{}) error { 48 | return mc.collection.Remove(selector) 49 | } 50 | 51 | func (mc MongoCollection) DropCollection() error { 52 | return mc.collection.DropCollection() 53 | } 54 | 55 | func (mc MongoCollection) UpdateId(id interface{}, change interface{}) error { 56 | return mc.collection.UpdateId(id, change) 57 | } 58 | 59 | func (mc MongoCollection) UpsertId(id interface{}, change interface{}) error { 60 | _, err := mc.collection.UpsertId(id, change) 61 | return err 62 | } 63 | 64 | type MongoQuery struct { 65 | query *mgo.Query 66 | } 67 | 68 | func (mq MongoQuery) Count() (int, error) { 69 | return mq.query.Count() 70 | } 71 | 72 | func (mq MongoQuery) One(result interface{}) error { 73 | return mq.query.One(result) 74 | } 75 | 76 | func (mq MongoQuery) Iter() Iterator { 77 | return &MongoIterator{iterator: mq.query.Iter()} 78 | } 79 | 80 | type MongoIterator struct { 81 | iterator *mgo.Iter 82 | } 83 | 84 | func (mi MongoIterator) All(result interface{}) error { 85 | return mi.iterator.All(result) 86 | } 87 | -------------------------------------------------------------------------------- /combat/combat_test.go: -------------------------------------------------------------------------------- 1 | package combat 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Cristofori/kmud/events" 8 | "github.com/Cristofori/kmud/testutils" 9 | . "gopkg.in/check.v1" 10 | ) 11 | 12 | func Test(t *testing.T) { TestingT(t) } 13 | 14 | type CombatSuite struct{} 15 | 16 | var _ = Suite(&CombatSuite{}) 17 | 18 | func init() { 19 | combatInterval = 10 * time.Millisecond 20 | } 21 | 22 | func (s *CombatSuite) TestCombatLoop(c *C) { 23 | char1 := testutils.NewMockPC() 24 | char2 := testutils.NewMockPC() 25 | char1.RoomId = char2.RoomId 26 | 27 | eventChannel1 := events.Register(char1) 28 | eventChannel2 := events.Register(char2) 29 | 30 | StartFight(char1, nil, char2) 31 | 32 | c.Assert(InCombat(char1), Equals, true) 33 | c.Assert(InCombat(char2), Equals, true) 34 | 35 | verifyEvents := func(channel chan events.Event) { 36 | gotCombatEvent := false 37 | gotStartEvent := false 38 | 39 | Loop: 40 | for { 41 | select { 42 | case event := <-channel: 43 | switch event.(type) { 44 | case events.TickEvent: 45 | case events.CombatEvent: 46 | gotCombatEvent = true 47 | case events.CombatStartEvent: 48 | gotStartEvent = true 49 | default: 50 | c.FailNow() 51 | } 52 | case <-time.After(30 * time.Millisecond): 53 | c.Fatalf("Timed out waiting for combat event") 54 | break Loop 55 | } 56 | 57 | if gotCombatEvent && gotStartEvent { 58 | break 59 | } 60 | } 61 | } 62 | verifyEvents(eventChannel1) 63 | verifyEvents(eventChannel2) 64 | 65 | StopFight(char1) 66 | 67 | e := <-eventChannel1 68 | switch e.(type) { 69 | case events.CombatStopEvent: 70 | default: 71 | c.Fatalf("Didn't get a combat stop event (channel 1)") 72 | } 73 | 74 | e = <-eventChannel2 75 | switch e.(type) { 76 | case events.CombatStopEvent: 77 | default: 78 | c.Fatalf("Didn't get a combat stop event (channel 1)") 79 | } 80 | 81 | select { 82 | case e := <-eventChannel1: 83 | c.Fatalf("Shouldn't have gotten any combat events after stopping combat (channel 1) - %s", e) 84 | case e := <-eventChannel2: 85 | c.Fatalf("Shouldn't have gotten any combat events after stopping combat (channel 2) - %s", e) 86 | case <-time.After(20 * time.Millisecond): 87 | } 88 | 89 | StartFight(char1, nil, char2) 90 | <-eventChannel1 91 | <-eventChannel2 92 | } 93 | -------------------------------------------------------------------------------- /database/effect.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/Cristofori/kmud/types" 5 | "github.com/Cristofori/kmud/utils" 6 | ) 7 | 8 | type Effect struct { 9 | DbObject `bson:",inline"` 10 | 11 | Type types.EffectKind 12 | Name string 13 | Power int 14 | Cost int 15 | Variance int 16 | Speed int 17 | Time int 18 | } 19 | 20 | func NewEffect(name string) types.Effect { 21 | effect := &Effect{ 22 | Name: utils.FormatName(name), 23 | Power: 1, 24 | Type: types.HitpointEffect, 25 | Variance: 0, 26 | Time: 1, 27 | } 28 | 29 | dbinit(effect) 30 | return effect 31 | } 32 | 33 | func (self *Effect) GetName() string { 34 | self.ReadLock() 35 | defer self.ReadUnlock() 36 | return self.Name 37 | } 38 | 39 | func (self *Effect) SetName(name string) { 40 | self.writeLock(func() { 41 | self.Name = utils.FormatName(name) 42 | }) 43 | } 44 | 45 | func (self *Effect) GetType() types.EffectKind { 46 | self.ReadLock() 47 | defer self.ReadUnlock() 48 | return self.Type 49 | } 50 | 51 | func (self *Effect) SetType(effectKind types.EffectKind) { 52 | self.writeLock(func() { 53 | self.Type = effectKind 54 | }) 55 | } 56 | 57 | func (self *Effect) GetPower() int { 58 | self.ReadLock() 59 | defer self.ReadUnlock() 60 | return self.Power 61 | } 62 | 63 | func (self *Effect) SetPower(power int) { 64 | self.writeLock(func() { 65 | self.Power = power 66 | }) 67 | } 68 | 69 | func (self *Effect) GetCost() int { 70 | self.ReadLock() 71 | defer self.ReadUnlock() 72 | return self.Cost 73 | } 74 | 75 | func (self *Effect) SetCost(cost int) { 76 | self.writeLock(func() { 77 | self.Cost = cost 78 | }) 79 | } 80 | 81 | func (self *Effect) GetVariance() int { 82 | self.ReadLock() 83 | defer self.ReadUnlock() 84 | return self.Variance 85 | } 86 | 87 | func (self *Effect) SetVariance(variance int) { 88 | self.writeLock(func() { 89 | self.Variance = variance 90 | }) 91 | } 92 | 93 | func (self *Effect) GetSpeed() int { 94 | self.ReadLock() 95 | defer self.ReadUnlock() 96 | return self.Speed 97 | } 98 | 99 | func (self *Effect) SetSpeed(speed int) { 100 | self.writeLock(func() { 101 | self.Speed = speed 102 | }) 103 | } 104 | 105 | func (self *Effect) GetTime() int { 106 | self.ReadLock() 107 | defer self.ReadUnlock() 108 | return self.Time 109 | } 110 | 111 | func (self *Effect) SetTime(speed int) { 112 | self.writeLock(func() { 113 | self.Time = speed 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Cristofori/kmud/combat" 7 | "github.com/Cristofori/kmud/events" 8 | "github.com/Cristofori/kmud/model" 9 | "github.com/Cristofori/kmud/types" 10 | "github.com/Cristofori/kmud/utils" 11 | ) 12 | 13 | const ( 14 | RoamingProperty = "roaming" 15 | ) 16 | 17 | func Start() { 18 | manageWorld() 19 | 20 | for _, npc := range model.GetNpcs() { 21 | manageNpc(npc) 22 | } 23 | 24 | for _, spawner := range model.GetSpawners() { 25 | manageSpawner(spawner) 26 | } 27 | } 28 | 29 | func manageWorld() { 30 | world := model.GetWorld() 31 | 32 | wer := &events.SimpleReceiver{} 33 | 34 | eventChannel := events.Register(wer) 35 | 36 | go func() { 37 | defer events.Unregister(wer) 38 | for { 39 | event := <-eventChannel 40 | switch event.(type) { 41 | case events.TickEvent: 42 | world.AdvanceTime() 43 | } 44 | } 45 | }() 46 | } 47 | 48 | func manageNpc(npc types.NPC) { 49 | eventChannel := events.Register(npc) 50 | 51 | go func() { 52 | defer events.Unregister(npc) 53 | 54 | for { 55 | event := <-eventChannel 56 | switch e := event.(type) { 57 | case events.TickEvent: 58 | if npc.GetRoaming() { 59 | room := model.GetRoom(npc.GetRoomId()) 60 | exits := room.GetExits() 61 | exitToTake := utils.Random(0, len(exits)-1) 62 | model.MoveCharacter(npc, exits[exitToTake]) 63 | } 64 | case events.CombatStartEvent: 65 | if npc == e.Defender { 66 | combat.StartFight(npc, nil, e.Attacker) 67 | } 68 | case events.CombatStopEvent: 69 | if npc == e.Defender { 70 | combat.StopFight(npc) 71 | } 72 | case events.DeathEvent: 73 | if npc == e.Character { 74 | model.DeleteCharacter(npc.GetId()) 75 | return 76 | } 77 | } 78 | } 79 | }() 80 | } 81 | 82 | func manageSpawner(spawner types.Spawner) { 83 | throttler := utils.NewThrottler(5 * time.Second) 84 | go func() { 85 | for { 86 | rooms := model.GetAreaRooms(spawner.GetAreaId()) 87 | 88 | if len(rooms) > 0 { 89 | npcs := model.GetSpawnerNpcs(spawner.GetId()) 90 | diff := spawner.GetCount() - len(npcs) 91 | 92 | for diff > 0 && len(rooms) > 0 { 93 | room := rooms[utils.Random(0, len(rooms)-1)] 94 | npc := model.CreateNpc(spawner.GetName(), room.GetId(), spawner.GetId()) 95 | npc.SetHealth(spawner.GetHealth()) 96 | manageNpc(npc) 97 | diff-- 98 | } 99 | } 100 | 101 | throttler.Sync() 102 | } 103 | }() 104 | } 105 | -------------------------------------------------------------------------------- /engine/pathing.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/Cristofori/kmud/model" 7 | "github.com/Cristofori/kmud/types" 8 | "github.com/Cristofori/kmud/utils" 9 | ) 10 | 11 | func costEstimate(start, goal types.Room) int { 12 | c1 := start.GetLocation() 13 | c2 := goal.GetLocation() 14 | 15 | return utils.Abs(c1.X-c2.X) + utils.Abs(c1.Y-c2.Y) + utils.Abs(c1.Z-c2.Z) 16 | } 17 | 18 | type roomSet map[types.Room]bool 19 | 20 | func lowest(rooms roomSet, scores map[types.Room]int) types.Room { 21 | var lowest types.Room 22 | lowestValue := math.MaxInt32 23 | 24 | for room := range rooms { 25 | value, found := scores[room] 26 | if found && value < lowestValue { 27 | lowest = room 28 | lowestValue = value 29 | } 30 | } 31 | 32 | return lowest 33 | } 34 | 35 | func reconstruct(cameFrom map[types.Room]types.Room, current types.Room) []types.Room { 36 | path := []types.Room{current} 37 | 38 | for { 39 | found := false 40 | current, found = cameFrom[current] 41 | if !found { 42 | break 43 | } 44 | 45 | path = append([]types.Room{current}, path...) 46 | } 47 | 48 | return path 49 | 50 | } 51 | 52 | // A* pathfinding algorithm, adapted from wikipedia's pseudocode 53 | // TODO - Find out what happens if the rooms aren't in the same zone 54 | func FindPath(start, goal types.Room) []types.Room { 55 | /* 56 | t1 := time.Now() 57 | defer func() { 58 | fmt.Printf("Took %v to find a path from %v to %v in %s\n", time.Since(t1), start.GetLocation(), goal.GetLocation(), model.GetZone(start.GetZoneId()).GetName()) 59 | }() 60 | */ 61 | 62 | evaluated := roomSet{} 63 | 64 | unevaluated := roomSet{} 65 | unevaluated[start] = true 66 | 67 | cameFrom := map[types.Room]types.Room{} 68 | 69 | g_score := map[types.Room]int{} 70 | g_score[start] = 0 71 | 72 | f_score := map[types.Room]int{} 73 | f_score[start] = costEstimate(start, goal) 74 | 75 | for { 76 | if len(unevaluated) == 0 { 77 | break 78 | } 79 | 80 | current := lowest(unevaluated, f_score) 81 | if current == goal { 82 | return reconstruct(cameFrom, goal) 83 | } 84 | 85 | delete(unevaluated, current) 86 | evaluated[current] = true 87 | 88 | neighbors := model.GetNeighbors(current) 89 | 90 | for _, neighbor := range neighbors { 91 | _, found := evaluated[neighbor] 92 | if found { 93 | continue 94 | } 95 | 96 | _, found = g_score[neighbor] 97 | if !found { 98 | g_score[neighbor] = math.MaxInt32 99 | } 100 | 101 | tentative_g_score := g_score[current] + 1 // TODO - Update this when there is a travel penalty between rooms 102 | 103 | _, found = unevaluated[neighbor] 104 | 105 | if !found { 106 | unevaluated[neighbor] = true 107 | } else if tentative_g_score >= g_score[neighbor] { 108 | continue 109 | } 110 | 111 | cameFrom[neighbor] = current 112 | g_score[neighbor] = tentative_g_score 113 | f_score[neighbor] = g_score[neighbor] + costEstimate(neighbor, goal) 114 | } 115 | } 116 | 117 | return []types.Room{} 118 | } 119 | -------------------------------------------------------------------------------- /database/item.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/Cristofori/kmud/types" 5 | "github.com/Cristofori/kmud/utils" 6 | ) 7 | 8 | type Template struct { 9 | DbObject `bson:",inline"` 10 | Name string 11 | Value int 12 | Weight int 13 | Capacity int 14 | } 15 | 16 | type Item struct { 17 | Container `bson:",inline"` 18 | 19 | TemplateId types.Id 20 | Locked bool 21 | ContainerId types.Id 22 | } 23 | 24 | func NewTemplate(name string) *Template { 25 | template := &Template{ 26 | Name: utils.FormatName(name), 27 | } 28 | dbinit(template) 29 | return template 30 | } 31 | 32 | func NewItem(templateId types.Id) *Item { 33 | item := &Item{ 34 | TemplateId: templateId, 35 | } 36 | dbinit(item) 37 | return item 38 | } 39 | 40 | // Template 41 | 42 | func (self *Template) GetName() string { 43 | self.ReadLock() 44 | defer self.ReadUnlock() 45 | 46 | return self.Name 47 | } 48 | 49 | func (self *Template) SetName(name string) { 50 | self.writeLock(func() { 51 | self.Name = utils.FormatName(name) 52 | }) 53 | } 54 | 55 | func (self *Template) SetValue(value int) { 56 | self.writeLock(func() { 57 | self.Value = value 58 | }) 59 | } 60 | 61 | func (self *Template) GetValue() int { 62 | self.ReadLock() 63 | defer self.ReadUnlock() 64 | return self.Value 65 | } 66 | 67 | func (self *Template) GetWeight() int { 68 | self.ReadLock() 69 | defer self.ReadUnlock() 70 | return self.Weight 71 | } 72 | 73 | func (self *Template) SetWeight(weight int) { 74 | self.writeLock(func() { 75 | self.Weight = weight 76 | }) 77 | } 78 | 79 | func (self *Template) GetCapacity() int { 80 | self.ReadLock() 81 | defer self.ReadUnlock() 82 | return self.Capacity 83 | } 84 | 85 | func (self *Template) SetCapacity(capacity int) { 86 | self.writeLock(func() { 87 | self.Capacity = capacity 88 | }) 89 | } 90 | 91 | // Item 92 | 93 | func (self *Item) GetTemplateId() types.Id { 94 | self.ReadLock() 95 | defer self.ReadUnlock() 96 | return self.TemplateId 97 | } 98 | 99 | func (self *Item) GetTemplate() types.Template { 100 | self.ReadLock() 101 | defer self.ReadUnlock() 102 | return Retrieve(self.TemplateId, types.TemplateType).(types.Template) 103 | } 104 | 105 | func (self *Item) GetName() string { 106 | return self.GetTemplate().GetName() 107 | } 108 | 109 | func (self *Item) GetValue() int { 110 | return self.GetTemplate().GetValue() 111 | } 112 | 113 | func (self *Item) GetCapacity() int { 114 | return self.GetTemplate().GetCapacity() 115 | } 116 | 117 | func (self *Item) IsLocked() bool { 118 | self.ReadLock() 119 | defer self.ReadUnlock() 120 | 121 | return self.Locked 122 | } 123 | 124 | func (self *Item) SetLocked(locked bool) { 125 | self.writeLock(func() { 126 | self.Locked = locked 127 | }) 128 | } 129 | 130 | func (self *Item) GetContainerId() types.Id { 131 | self.ReadLock() 132 | defer self.ReadUnlock() 133 | return self.ContainerId 134 | } 135 | 136 | func (self *Item) SetContainerId(id types.Id, from types.Id) bool { 137 | self.WriteLock() 138 | if from != self.ContainerId { 139 | self.WriteUnlock() 140 | return false 141 | } 142 | self.ContainerId = id 143 | self.WriteUnlock() 144 | self.syncModified() 145 | return true 146 | } 147 | -------------------------------------------------------------------------------- /testutils/testutils.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | type TestWriter struct { 12 | Wrote string 13 | } 14 | 15 | func (self *TestWriter) Write(p []byte) (n int, err error) { 16 | self.Wrote += string(p) 17 | return len(p), nil 18 | } 19 | 20 | type TestReader struct { 21 | ToRead string 22 | err error 23 | } 24 | 25 | func (self *TestReader) Read(p []byte) (n int, err error) { 26 | if self.err != nil { 27 | return 0, self.err 28 | } 29 | 30 | for i := 0; i < len(self.ToRead); i++ { 31 | p[i] = self.ToRead[i] 32 | } 33 | 34 | p[len(self.ToRead)] = '\n' 35 | 36 | return len(self.ToRead) + 1, nil 37 | } 38 | 39 | func (self *TestReader) SetError(err error) { 40 | self.err = err 41 | } 42 | 43 | type TestReadWriter struct { 44 | TestReader 45 | TestWriter 46 | } 47 | 48 | type TestCommunicable struct { 49 | TestReadWriter 50 | } 51 | 52 | func (self *TestCommunicable) Write(text string) { 53 | self.TestReadWriter.Write([]byte(text)) 54 | } 55 | 56 | func (self *TestCommunicable) WriteLine(line string, a ...interface{}) { 57 | self.Write(fmt.Sprintf(line, a...) + "\n") 58 | } 59 | 60 | func (self *TestCommunicable) GetInput(prompt string) string { 61 | self.Write(prompt) 62 | buf := make([]byte, 1024) 63 | n, _ := self.Read(buf) 64 | return strings.ToLower(strings.TrimSpace(string(buf[:n]))) 65 | } 66 | 67 | func (self *TestCommunicable) GetWindowSize() (int, int) { 68 | return 80, 40 69 | } 70 | 71 | func TestSettersAndGetters(object interface{}, t *testing.T) bool { 72 | objType := reflect.TypeOf(object) 73 | 74 | regex, _ := regexp.Compile("^Get(.+)") 75 | 76 | getterToSetter := make(map[string]string) 77 | 78 | for i := 0; i < objType.NumMethod(); i++ { 79 | method := objType.Method(i) 80 | 81 | findMatchingFunctions := func(prefix1, prefix2 string) string { 82 | if strings.HasPrefix(method.Name, prefix1) { 83 | result := regex.FindStringSubmatch(method.Name) 84 | 85 | if result != nil { 86 | pairName := "Set" + result[1] 87 | _, found := objType.MethodByName(pairName) 88 | 89 | if !found { 90 | t.Logf("Unable to find matching setter/getter for %s.%s", objType.String(), method.Name) 91 | return "" 92 | } 93 | 94 | return pairName 95 | } 96 | } 97 | 98 | return "" 99 | } 100 | 101 | pairedMethodName := findMatchingFunctions("Get", "Set") 102 | if pairedMethodName != "" { 103 | getterToSetter[method.Name] = pairedMethodName 104 | } 105 | 106 | findMatchingFunctions("Set", "Get") 107 | } 108 | 109 | v := reflect.ValueOf(object) 110 | 111 | for g, s := range getterToSetter { 112 | getterValue := v.MethodByName(g) 113 | setterValue := v.MethodByName(s) 114 | 115 | getterType := getterValue.Type() 116 | setterType := setterValue.Type() 117 | 118 | if getterType.NumOut() != setterType.NumIn() { 119 | t.Errorf("In/out mismatch: %s:%v, %s:%v", g, getterType.NumOut(), s, setterType.NumIn()) 120 | } else { 121 | vals := make([]reflect.Value, setterType.NumIn()) 122 | 123 | for i := 0; i < len(vals); i++ { 124 | inType := setterType.In(i) 125 | t.Log("inType:", inType) 126 | vals[i] = reflect.New(inType) 127 | } 128 | 129 | setterValue.Call(vals) 130 | } 131 | } 132 | 133 | return true 134 | } 135 | 136 | func Assert(condition bool, t *testing.T, failMessage ...interface{}) { 137 | if !condition { 138 | t.Error(failMessage...) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /model/model_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Cristofori/kmud/database" 7 | "github.com/Cristofori/kmud/datastore" 8 | "github.com/Cristofori/kmud/types" 9 | . "gopkg.in/check.v1" 10 | "gopkg.in/mgo.v2" 11 | ) 12 | 13 | func Test(t *testing.T) { TestingT(t) } 14 | 15 | type ModelSuite struct{} 16 | 17 | var _ = Suite(&ModelSuite{}) 18 | 19 | var _db *mgo.Database 20 | 21 | func (s *ModelSuite) SetUpSuite(c *C) { 22 | session, err := mgo.Dial("localhost") 23 | c.Assert(err, Equals, nil) 24 | 25 | if err != nil { 26 | return 27 | } 28 | 29 | dbName := "unit_model_test" 30 | 31 | _db = session.DB(dbName) 32 | 33 | session.DB(dbName).DropDatabase() 34 | 35 | database.Init(database.NewMongoSession(session), dbName) 36 | } 37 | 38 | func (s *ModelSuite) TearDownSuite(c *C) { 39 | _db.DropDatabase() 40 | datastore.ClearAll() 41 | } 42 | 43 | func (s *ModelSuite) TestUserFunctions(c *C) { 44 | name1 := "Test_name1" 45 | password1 := "test_password2" 46 | 47 | user1 := CreateUser(name1, password1, false) 48 | 49 | c.Assert(user1.GetName(), Equals, name1) 50 | c.Assert(user1.VerifyPassword(password1), Equals, true) 51 | 52 | userByName := GetUserByName(name1) 53 | c.Assert(userByName, Equals, user1) 54 | 55 | userByName = GetUserByName("foobar") 56 | c.Assert(userByName, Equals, nil) 57 | 58 | zone, _ := CreateZone("testZone") 59 | room, _ := CreateRoom(zone, types.Coordinate{X: 0, Y: 0, Z: 0}) 60 | CreatePlayerCharacter("testPlayer", user1.GetId(), room) 61 | 62 | DeleteUser(user1.GetId()) 63 | userByName = GetUserByName(name1) 64 | c.Assert(userByName, Equals, nil) 65 | c.Assert(GetUserCharacters(user1.GetId()), HasLen, 0) 66 | } 67 | 68 | func (s *ModelSuite) TestZoneFunctions(c *C) { 69 | name := "zone1" 70 | zone1, err1 := CreateZone(name) 71 | 72 | c.Assert(zone1, Not(Equals), nil) 73 | c.Assert(err1, Equals, nil) 74 | 75 | zoneByName := GetZoneByName(name) 76 | c.Assert(zoneByName, Equals, zone1) 77 | 78 | zone2, err2 := CreateZone("zone2") 79 | c.Assert(zone2, Not(Equals), nil) 80 | c.Assert(err2, Equals, nil) 81 | 82 | zone3, err3 := CreateZone("zone3") 83 | c.Assert(zone3, Not(Equals), nil) 84 | c.Assert(err3, Equals, nil) 85 | 86 | zoneById := GetZone(zone1.GetId()) 87 | c.Assert(zoneById, Equals, zone1) 88 | 89 | _, err := CreateZone("zone3") 90 | c.Assert(err, Not(Equals), nil) 91 | } 92 | 93 | func (s *ModelSuite) TestRoomFunctions(c *C) { 94 | zone, err := CreateZone("zone") 95 | c.Assert(zone, Not(Equals), nil) 96 | c.Assert(err, Equals, nil) 97 | 98 | room1, err1 := CreateRoom(zone, types.Coordinate{X: 0, Y: 0, Z: 0}) 99 | c.Assert(room1, Not(Equals), nil) 100 | c.Assert(err1, Equals, nil) 101 | 102 | badRoom, shouldError := CreateRoom(zone, types.Coordinate{X: 0, Y: 0, Z: 0}) 103 | c.Assert(badRoom, Equals, nil) 104 | c.Assert(shouldError, Not(Equals), nil) 105 | 106 | room2, err2 := CreateRoom(zone, types.Coordinate{X: 0, Y: 1, Z: 0}) 107 | c.Assert(room2, Not(Equals), nil) 108 | c.Assert(err2, Equals, nil) 109 | 110 | room1.SetExitEnabled(types.DirectionSouth, true) 111 | room2.SetExitEnabled(types.DirectionNorth, true) 112 | 113 | c.Assert(room2.HasExit(types.DirectionNorth), Equals, true) 114 | DeleteRoom(room1) 115 | c.Assert(room2.HasExit(types.DirectionNorth), Equals, false) 116 | } 117 | 118 | func (s *ModelSuite) TestRoomAndZoneFunctions(c *C) { 119 | // ZoneCorners 120 | // GetRoomsInZone 121 | } 122 | 123 | func (s *ModelSuite) TestCharFunctions(c *C) { 124 | //user := CreateUser("user1", "") 125 | //playerName1 := "player1" 126 | //player1 := CreatePlayer(name1, user 127 | } 128 | -------------------------------------------------------------------------------- /types/coordinate.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "strings" 4 | 5 | type Coordinate struct { 6 | X int 7 | Y int 8 | Z int 9 | } 10 | 11 | type Direction string 12 | 13 | const ( 14 | DirectionNorth Direction = "North" 15 | DirectionNorthEast Direction = "NorthEast" 16 | DirectionEast Direction = "East" 17 | DirectionSouthEast Direction = "SouthEast" 18 | DirectionSouth Direction = "South" 19 | DirectionSouthWest Direction = "SouthWest" 20 | DirectionWest Direction = "West" 21 | DirectionNorthWest Direction = "NorthWest" 22 | DirectionUp Direction = "Up" 23 | DirectionDown Direction = "Down" 24 | DirectionNone Direction = "None" 25 | ) 26 | 27 | func StringToDirection(str string) Direction { 28 | dirStr := strings.ToLower(str) 29 | switch dirStr { 30 | case "n": 31 | fallthrough 32 | case "north": 33 | return DirectionNorth 34 | case "ne": 35 | return DirectionNorthEast 36 | case "e": 37 | fallthrough 38 | case "east": 39 | return DirectionEast 40 | case "se": 41 | return DirectionSouthEast 42 | case "s": 43 | fallthrough 44 | case "south": 45 | return DirectionSouth 46 | case "sw": 47 | return DirectionSouthWest 48 | case "w": 49 | fallthrough 50 | case "west": 51 | return DirectionWest 52 | case "nw": 53 | return DirectionNorthWest 54 | case "u": 55 | fallthrough 56 | case "up": 57 | return DirectionUp 58 | case "d": 59 | fallthrough 60 | case "down": 61 | return DirectionDown 62 | } 63 | 64 | return DirectionNone 65 | } 66 | 67 | func (dir Direction) ToString() string { 68 | switch dir { 69 | case DirectionNorth: 70 | return "North" 71 | case DirectionNorthEast: 72 | return "NorthEast" 73 | case DirectionEast: 74 | return "East" 75 | case DirectionSouthEast: 76 | return "SouthEast" 77 | case DirectionSouth: 78 | return "South" 79 | case DirectionSouthWest: 80 | return "SouthWest" 81 | case DirectionWest: 82 | return "West" 83 | case DirectionNorthWest: 84 | return "NorthWest" 85 | case DirectionUp: 86 | return "Up" 87 | case DirectionDown: 88 | return "Down" 89 | case DirectionNone: 90 | return "None" 91 | } 92 | 93 | panic("Unexpected code path") 94 | } 95 | 96 | func (self Direction) Opposite() Direction { 97 | switch self { 98 | case DirectionNorth: 99 | return DirectionSouth 100 | case DirectionNorthEast: 101 | return DirectionSouthWest 102 | case DirectionEast: 103 | return DirectionWest 104 | case DirectionSouthEast: 105 | return DirectionNorthWest 106 | case DirectionSouth: 107 | return DirectionNorth 108 | case DirectionSouthWest: 109 | return DirectionNorthEast 110 | case DirectionWest: 111 | return DirectionEast 112 | case DirectionNorthWest: 113 | return DirectionSouthEast 114 | case DirectionUp: 115 | return DirectionDown 116 | case DirectionDown: 117 | return DirectionUp 118 | } 119 | 120 | return DirectionNone 121 | } 122 | 123 | func (self *Coordinate) Next(direction Direction) Coordinate { 124 | newCoord := *self 125 | switch direction { 126 | case DirectionNorth: 127 | newCoord.Y -= 1 128 | case DirectionNorthEast: 129 | newCoord.Y -= 1 130 | newCoord.X += 1 131 | case DirectionEast: 132 | newCoord.X += 1 133 | case DirectionSouthEast: 134 | newCoord.Y += 1 135 | newCoord.X += 1 136 | case DirectionSouth: 137 | newCoord.Y += 1 138 | case DirectionSouthWest: 139 | newCoord.Y += 1 140 | newCoord.X -= 1 141 | case DirectionWest: 142 | newCoord.X -= 1 143 | case DirectionNorthWest: 144 | newCoord.Y -= 1 145 | newCoord.X -= 1 146 | case DirectionUp: 147 | newCoord.Z -= 1 148 | case DirectionDown: 149 | newCoord.Z += 1 150 | } 151 | return newCoord 152 | } 153 | -------------------------------------------------------------------------------- /testutils/mocks.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "github.com/Cristofori/kmud/types" 5 | "gopkg.in/mgo.v2/bson" 6 | ) 7 | 8 | type MockId string 9 | 10 | func (self MockId) String() string { 11 | return string(self) 12 | } 13 | 14 | func (self MockId) Hex() string { 15 | return string(self) 16 | } 17 | 18 | type MockIdentifiable struct { 19 | Id types.Id 20 | } 21 | 22 | func (self MockIdentifiable) GetId() types.Id { 23 | return self.Id 24 | } 25 | 26 | func (self MockObject) SetId(types.Id) { 27 | } 28 | 29 | type MockNameable struct { 30 | Name string 31 | } 32 | 33 | func (self MockNameable) GetName() string { 34 | return self.Name 35 | } 36 | 37 | func (self *MockNameable) SetName(name string) { 38 | self.Name = name 39 | } 40 | 41 | type MockDestroyable struct { 42 | } 43 | 44 | func (MockDestroyable) Destroy() { 45 | } 46 | 47 | func (self MockDestroyable) IsDestroyed() bool { 48 | return false 49 | } 50 | 51 | type MockReadLocker struct { 52 | } 53 | 54 | func (*MockReadLocker) ReadLock() { 55 | } 56 | 57 | func (*MockReadLocker) ReadUnlock() { 58 | } 59 | 60 | type MockObject struct { 61 | MockIdentifiable 62 | MockReadLocker 63 | MockDestroyable 64 | } 65 | 66 | type MockZone struct { 67 | MockIdentifiable 68 | } 69 | 70 | func NewMockZone() *MockZone { 71 | return &MockZone{ 72 | MockIdentifiable{Id: bson.NewObjectId()}, 73 | } 74 | } 75 | 76 | type MockRoom struct { 77 | MockIdentifiable 78 | } 79 | 80 | func NewMockRoom() *MockRoom { 81 | return &MockRoom{ 82 | MockIdentifiable{Id: bson.NewObjectId()}, 83 | } 84 | } 85 | 86 | type MockUser struct { 87 | MockIdentifiable 88 | } 89 | 90 | func NewMockUser() *MockUser { 91 | return &MockUser{ 92 | MockIdentifiable{Id: bson.NewObjectId()}, 93 | } 94 | } 95 | 96 | type MockContainer struct { 97 | } 98 | 99 | func (*MockContainer) AddCash(int) { 100 | } 101 | 102 | func (*MockContainer) GetCash() int { 103 | return 0 104 | } 105 | 106 | func (*MockContainer) RemoveCash(int) { 107 | } 108 | 109 | func (*MockContainer) AddItem(types.Id) { 110 | } 111 | 112 | func (*MockContainer) RemoveItem(types.Id) bool { 113 | return true 114 | } 115 | 116 | func (*MockContainer) GetCapacity() int { 117 | return 0 118 | } 119 | 120 | func (*MockContainer) SetCapacity(int) { 121 | } 122 | 123 | func (*MockContainer) GetItems() types.ItemList { 124 | return types.ItemList{} 125 | } 126 | 127 | type MockCharacter struct { 128 | MockObject 129 | MockNameable 130 | MockContainer 131 | } 132 | 133 | func (*MockCharacter) GetHealth() int { 134 | return 1 135 | } 136 | 137 | func (*MockCharacter) SetHealth(int) { 138 | } 139 | 140 | func (*MockCharacter) GetHitPoints() int { 141 | return 1 142 | } 143 | 144 | func (*MockCharacter) Heal(int) { 145 | } 146 | 147 | func (*MockCharacter) Hit(int) { 148 | } 149 | 150 | func (*MockCharacter) SetHitPoints(int) { 151 | } 152 | 153 | func (*MockCharacter) GetWeight() int { 154 | return 0 155 | } 156 | 157 | type MockPC struct { 158 | MockCharacter 159 | RoomId types.Id 160 | } 161 | 162 | func NewMockPC() *MockPC { 163 | return &MockPC{ 164 | MockCharacter: MockCharacter{ 165 | MockObject: MockObject{ 166 | MockIdentifiable: MockIdentifiable{Id: bson.NewObjectId()}, 167 | }, 168 | MockNameable: MockNameable{Name: "Mock PC"}, 169 | }, 170 | RoomId: bson.NewObjectId(), 171 | } 172 | } 173 | 174 | func (self MockPC) GetRoomId() types.Id { 175 | return self.RoomId 176 | } 177 | 178 | func (self MockPC) IsOnline() bool { 179 | return true 180 | } 181 | 182 | func (self MockPC) SetRoomId(types.Id) { 183 | } 184 | 185 | func (self MockPC) GetSkills() []types.Id { 186 | return []types.Id{} 187 | } 188 | 189 | func (self MockPC) AddSkill(types.Id) { 190 | } 191 | -------------------------------------------------------------------------------- /database/user.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "io" 7 | "net" 8 | "reflect" 9 | 10 | "github.com/Cristofori/kmud/types" 11 | "github.com/Cristofori/kmud/utils" 12 | ) 13 | 14 | type User struct { 15 | DbObject `bson:",inline"` 16 | 17 | Name string 18 | ColorMode types.ColorMode 19 | Password []byte 20 | Admin bool 21 | 22 | online bool 23 | conn net.Conn 24 | windowWidth int 25 | windowHeight int 26 | terminalType string 27 | } 28 | 29 | func NewUser(name string, password string, admin bool) *User { 30 | user := &User{ 31 | Name: utils.FormatName(name), 32 | Password: hash(password), 33 | ColorMode: types.ColorModeNone, 34 | Admin: admin, 35 | online: false, 36 | windowWidth: 80, 37 | windowHeight: 40, 38 | } 39 | 40 | dbinit(user) 41 | return user 42 | } 43 | 44 | func (self *User) GetName() string { 45 | self.ReadLock() 46 | defer self.ReadUnlock() 47 | 48 | return self.Name 49 | } 50 | 51 | func (self *User) SetName(name string) { 52 | self.writeLock(func() { 53 | self.Name = utils.FormatName(name) 54 | }) 55 | } 56 | 57 | func (self *User) SetOnline(online bool) { 58 | self.online = online 59 | 60 | if !online { 61 | self.conn = nil 62 | } 63 | } 64 | 65 | func (self *User) IsOnline() bool { 66 | return self.online 67 | } 68 | 69 | func (self *User) SetColorMode(cm types.ColorMode) { 70 | self.writeLock(func() { 71 | self.ColorMode = cm 72 | }) 73 | } 74 | 75 | func (self *User) GetColorMode() types.ColorMode { 76 | self.ReadLock() 77 | defer self.ReadUnlock() 78 | return self.ColorMode 79 | } 80 | 81 | func hash(data string) []byte { 82 | h := sha1.New() 83 | io.WriteString(h, data) 84 | return h.Sum(nil) 85 | } 86 | 87 | // SetPassword SHA1 hashes the password before saving it to the database 88 | func (self *User) SetPassword(password string) { 89 | self.writeLock(func() { 90 | self.Password = hash(password) 91 | }) 92 | } 93 | 94 | func (self *User) VerifyPassword(password string) bool { 95 | hashed := hash(password) 96 | return reflect.DeepEqual(hashed, self.GetPassword()) 97 | } 98 | 99 | // GetPassword returns the SHA1 of the user's password 100 | func (self *User) GetPassword() []byte { 101 | self.ReadLock() 102 | defer self.ReadUnlock() 103 | return self.Password 104 | } 105 | 106 | func (self *User) SetConnection(conn net.Conn) { 107 | self.conn = conn 108 | } 109 | 110 | func (self *User) GetConnection() net.Conn { 111 | return self.conn 112 | } 113 | 114 | func (self *User) SetWindowSize(width int, height int) { 115 | self.windowWidth = width 116 | self.windowHeight = height 117 | } 118 | 119 | const MinWidth = 60 120 | const MinHeight = 20 121 | 122 | func (self *User) GetWindowSize() (width int, height int) { 123 | return utils.Max(self.windowWidth, MinWidth), 124 | utils.Max(self.windowHeight, MinHeight) 125 | } 126 | 127 | func (self *User) SetTerminalType(tt string) { 128 | self.terminalType = tt 129 | } 130 | 131 | func (self *User) GetTerminalType() string { 132 | return self.terminalType 133 | } 134 | 135 | func (self *User) GetInput(prompt string) string { 136 | return utils.GetUserInput(self.conn, prompt, self.GetColorMode()) 137 | } 138 | 139 | func (self *User) WriteLine(line string, a ...interface{}) { 140 | utils.WriteLine(self.conn, fmt.Sprintf(line, a...), self.GetColorMode()) 141 | } 142 | 143 | func (self *User) Write(text string) { 144 | utils.Write(self.conn, text, self.GetColorMode()) 145 | } 146 | 147 | func (self *User) SetAdmin(admin bool) { 148 | self.writeLock(func() { 149 | self.Admin = admin 150 | }) 151 | } 152 | 153 | func (self *User) IsAdmin() bool { 154 | self.ReadLock() 155 | defer self.ReadUnlock() 156 | return self.Admin 157 | } 158 | -------------------------------------------------------------------------------- /bots/bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import pexpect, sys, re, random, exceptions 4 | 5 | host = "localhost" 6 | port = 8945 7 | user = "unit1" 8 | password = "unit1" 9 | 10 | def usage(): 11 | print "Usage: %s [host] [port] [username] [password]" % sys.argv[0] 12 | sys.exit(1) 13 | 14 | if len(sys.argv) > 1: 15 | host = sys.argv[1] 16 | 17 | if len(sys.argv) > 2: 18 | try: 19 | port = int(sys.argv[2]) 20 | except exceptions.ValueError: 21 | usage() 22 | 23 | if len(sys.argv) > 3: 24 | user = sys.argv[3] 25 | 26 | if len(sys.argv) > 4: 27 | password = sys.argv[4] 28 | 29 | telnetCommand = 'telnet %s %s' % (host, port) 30 | 31 | print '%s: Connecting...' % user 32 | 33 | telnet = pexpect.spawn(telnetCommand, timeout=5) 34 | 35 | def login(user, password): 36 | patterns = telnet.compile_pattern_list(['> $', 'Username: $', 'Password: $', 'already online', 'User not found', pexpect.TIMEOUT]) 37 | 38 | while True: 39 | try: 40 | index = telnet.expect(patterns) 41 | except pexpect.EOF: 42 | print '%s: Lost connection to server' % user 43 | exit(0) 44 | 45 | if index == 0: 46 | telnet.sendline("l") 47 | elif index == 1: 48 | print '%s: Logging in' % user 49 | telnet.sendline(user) 50 | elif index == 2: 51 | print '%s: Sending password' % user 52 | telnet.sendline(password) 53 | telnet.sendline("1") 54 | break 55 | elif index == 3: 56 | print '%s: Login failed, already online' % user 57 | sys.exit(2) 58 | elif index == 4: 59 | print '%s: User not found' % user 60 | sys.exit(3) 61 | else: 62 | print '%s: Login timeout' % user 63 | break 64 | 65 | def runaround(): 66 | print '%s: Running around' % user 67 | exits = re.compile('Exits: (\[N\]orth)? ?(\[NE\]North East)? ?(\[E\]ast)? ?(\[SE\]South East)? ?(\[S\]outh)? ?(\[SW\]South West)? ?(\[W\]est)? ?(\[NW\]North West)?') 68 | patterns = telnet.compile_pattern_list([exits, pexpect.TIMEOUT]) 69 | 70 | while True: 71 | try: 72 | index = telnet.expect(patterns) 73 | except pexpect.EOF: 74 | print '%s: Lost connection to server' % user 75 | exit(0) 76 | 77 | if index == 0: 78 | m = telnet.match 79 | 80 | exitList = [] 81 | 82 | for i in range(len(m.groups())): 83 | cap = m.group(i) 84 | if cap != None: 85 | if i == 1: # N 86 | exitList.append("N") 87 | elif i == 2: # NE 88 | exitList.append("NE") 89 | elif i == 3: # E 90 | exitList.append("E") 91 | elif i == 4: # SE 92 | exitList.append("SE") 93 | elif i == 5: # S 94 | exitList.append("S") 95 | elif i == 6: # SW 96 | exitList.append("SW") 97 | elif i == 7: # W 98 | exitList.append("W") 99 | elif i == 8: # NW 100 | exitList.append("NW") 101 | 102 | if len(exitList) == 0: 103 | print '%s: Error, no exits' % user 104 | print telnet.before 105 | print telnet.after 106 | sys.exit(4) 107 | 108 | index = random.randint(0, len(exitList) - 1) 109 | direction = exitList[index] 110 | # print "%s: Moving %s" % (user, direction) 111 | telnet.sendline(direction) 112 | 113 | elif index == 1: 114 | print '%s: Runaround timeout' % user 115 | print telnet.before 116 | print telnet.after 117 | telnet.sendline("l") 118 | 119 | 120 | login(user, password) 121 | runaround() 122 | 123 | -------------------------------------------------------------------------------- /combat/combat.go: -------------------------------------------------------------------------------- 1 | package combat 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Cristofori/kmud/events" 7 | "github.com/Cristofori/kmud/model" 8 | "github.com/Cristofori/kmud/types" 9 | "github.com/Cristofori/kmud/utils" 10 | ) 11 | 12 | var combatInterval = 3 * time.Second 13 | 14 | var combatMessages chan interface{} 15 | 16 | type combatInfo struct { 17 | Defender types.Character 18 | Skill types.Skill 19 | } 20 | 21 | var fights map[types.Character]combatInfo 22 | 23 | type combatStart struct { 24 | Attacker types.Character 25 | Defender types.Character 26 | Skill types.Skill 27 | } 28 | 29 | type combatStop struct { 30 | Attacker types.Character 31 | } 32 | 33 | type combatQuery struct { 34 | Character types.Character 35 | Ret chan bool 36 | } 37 | 38 | type combatTick struct{} 39 | 40 | func StartFight(attacker types.Character, skill types.Skill, defender types.Character) { 41 | combatMessages <- combatStart{Attacker: attacker, Defender: defender, Skill: skill} 42 | } 43 | 44 | func StopFight(attacker types.Character) { 45 | combatMessages <- combatStop{Attacker: attacker} 46 | } 47 | 48 | func InCombat(character types.Character) bool { 49 | query := combatQuery{Character: character, Ret: make(chan bool)} 50 | combatMessages <- query 51 | return <-query.Ret 52 | } 53 | 54 | func init() { 55 | fights = map[types.Character]combatInfo{} 56 | 57 | combatMessages = make(chan interface{}, 1) 58 | 59 | go func() { 60 | defer func() { recover() }() 61 | throttler := utils.NewThrottler(combatInterval) 62 | for { 63 | throttler.Sync() 64 | combatMessages <- combatTick{} 65 | } 66 | }() 67 | 68 | go func() { 69 | for message := range combatMessages { 70 | Switch: 71 | switch m := message.(type) { 72 | case combatTick: 73 | for a, info := range fights { 74 | d := info.Defender 75 | 76 | if a.GetRoomId() == d.GetRoomId() { 77 | power := 0 78 | skill := info.Skill 79 | 80 | if skill == nil { 81 | power = utils.Random(1, 10) 82 | } else { 83 | effects := model.GetEffects(skill.GetEffects()) 84 | variance := 0 85 | for _, e := range effects { 86 | power += e.GetPower() 87 | variance += e.GetVariance() 88 | } 89 | power += utils.Random(-variance, variance) 90 | } 91 | 92 | d.Hit(power) 93 | events.Broadcast(events.CombatEvent{Attacker: a, Defender: d, Skill: skill, Power: power}) 94 | 95 | if d.GetHitPoints() <= 0 { 96 | Kill(d) 97 | } 98 | } else { 99 | doCombatStop(a) 100 | } 101 | } 102 | case combatStart: 103 | oldInfo, found := fights[m.Attacker] 104 | 105 | if m.Defender == oldInfo.Defender { 106 | break 107 | } 108 | 109 | if found { 110 | doCombatStop(m.Attacker) 111 | } 112 | 113 | fights[m.Attacker] = combatInfo{ 114 | Defender: m.Defender, 115 | Skill: m.Skill, 116 | } 117 | 118 | events.Broadcast(events.CombatStartEvent{Attacker: m.Attacker, Defender: m.Defender}) 119 | case combatStop: 120 | doCombatStop(m.Attacker) 121 | case combatQuery: 122 | _, found := fights[m.Character] 123 | 124 | if found { 125 | m.Ret <- true 126 | } else { 127 | for _, info := range fights { 128 | if info.Defender == m.Character { 129 | m.Ret <- true 130 | break Switch 131 | } 132 | } 133 | m.Ret <- false 134 | } 135 | 136 | default: 137 | panic("Unhandled combat message") 138 | } 139 | } 140 | }() 141 | } 142 | 143 | func Kill(char types.Character) { 144 | clearCombat(char) 145 | events.Broadcast(events.DeathEvent{Character: char}) 146 | } 147 | 148 | func clearCombat(char types.Character) { 149 | _, found := fights[char] 150 | 151 | if found { 152 | doCombatStop(char) 153 | } 154 | 155 | for a, info := range fights { 156 | if info.Defender == char { 157 | doCombatStop(a) 158 | } 159 | } 160 | } 161 | 162 | func doCombatStop(attacker types.Character) { 163 | info := fights[attacker] 164 | 165 | if info.Defender != nil { 166 | delete(fights, attacker) 167 | events.Broadcast(events.CombatStopEvent{Attacker: attacker, Defender: info.Defender}) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /types/color.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var ColorRegex = regexp.MustCompile("([@#][0-6]|@@|##)") 10 | 11 | type ColorMode int 12 | 13 | const ( 14 | ColorModeLight ColorMode = iota 15 | ColorModeDark ColorMode = iota 16 | ColorModeNone ColorMode = iota 17 | ) 18 | 19 | type Color string 20 | 21 | const ( 22 | ColorRed Color = "@0" 23 | ColorGreen Color = "@1" 24 | ColorYellow Color = "@2" 25 | ColorBlue Color = "@3" 26 | ColorMagenta Color = "@4" 27 | ColorCyan Color = "@5" 28 | ColorWhite Color = "@6" 29 | 30 | ColorDarkRed Color = "#0" 31 | ColorDarkGreen Color = "#1" 32 | ColorDarkYellow Color = "#2" 33 | ColorDarkBlue Color = "#3" 34 | ColorDarkMagenta Color = "#4" 35 | ColorDarkCyan Color = "#5" 36 | ColorBlack Color = "#6" 37 | 38 | ColorGray Color = "@@" 39 | ColorNormal Color = "##" 40 | ) 41 | 42 | type colorCode string 43 | 44 | const ( 45 | red colorCode = "\033[01;31m" 46 | green colorCode = "\033[01;32m" 47 | yellow colorCode = "\033[01;33m" 48 | blue colorCode = "\033[01;34m" 49 | magenta colorCode = "\033[01;35m" 50 | cyan colorCode = "\033[01;36m" 51 | white colorCode = "\033[01;37m" 52 | 53 | darkRed colorCode = "\033[22;31m" 54 | darkGreen colorCode = "\033[22;32m" 55 | darkYellow colorCode = "\033[22;33m" 56 | darkBlue colorCode = "\033[22;34m" 57 | darkMagenta colorCode = "\033[22;35m" 58 | darkCyan colorCode = "\033[22;36m" 59 | black colorCode = "\033[22;30m" 60 | 61 | gray colorCode = "\033[22;37m" 62 | normal colorCode = "\033[0m" 63 | ) 64 | 65 | func getAnsiCode(mode ColorMode, color Color) string { 66 | if mode == ColorModeNone { 67 | return "" 68 | } 69 | 70 | var code colorCode 71 | switch color { 72 | case ColorNormal: 73 | code = normal 74 | case ColorRed: 75 | code = red 76 | case ColorGreen: 77 | code = green 78 | case ColorYellow: 79 | code = yellow 80 | case ColorBlue: 81 | code = blue 82 | case ColorMagenta: 83 | code = magenta 84 | case ColorCyan: 85 | code = cyan 86 | case ColorWhite: 87 | code = white 88 | case ColorDarkRed: 89 | code = darkRed 90 | case ColorDarkGreen: 91 | code = darkGreen 92 | case ColorDarkYellow: 93 | code = darkYellow 94 | case ColorDarkBlue: 95 | code = darkBlue 96 | case ColorDarkMagenta: 97 | code = darkMagenta 98 | case ColorDarkCyan: 99 | code = darkCyan 100 | case ColorBlack: 101 | code = black 102 | case ColorGray: 103 | code = gray 104 | } 105 | 106 | if mode == ColorModeDark { 107 | if code == white { 108 | return string(black) 109 | } else if code == black { 110 | return string(white) 111 | } else if strings.Contains(string(code), "01") { 112 | return strings.Replace(string(code), "01", "22", 1) 113 | } else { 114 | return strings.Replace(string(code), "22", "01", 1) 115 | } 116 | } 117 | 118 | return string(code) 119 | } 120 | 121 | // Wraps the given text in the given color, followed by a color reset 122 | func Colorize(color Color, text string) string { 123 | return fmt.Sprintf("%s%s%s", string(color), text, string(ColorNormal)) 124 | } 125 | 126 | var Lookup = map[Color]bool{ 127 | ColorRed: true, 128 | ColorGreen: true, 129 | ColorYellow: true, 130 | ColorBlue: true, 131 | ColorMagenta: true, 132 | ColorCyan: true, 133 | ColorWhite: true, 134 | ColorDarkRed: true, 135 | ColorDarkGreen: true, 136 | ColorDarkYellow: true, 137 | ColorDarkBlue: true, 138 | ColorDarkMagenta: true, 139 | ColorDarkCyan: true, 140 | ColorBlack: true, 141 | ColorGray: true, 142 | ColorNormal: true, 143 | } 144 | 145 | // Strips MUD color codes and replaces them with ansi color codes 146 | func ProcessColors(text string, cm ColorMode) string { 147 | replace := func(match string) string { 148 | found := Lookup[Color(match)] 149 | 150 | if found { 151 | return getAnsiCode(cm, Color(match)) 152 | } 153 | 154 | return match 155 | } 156 | 157 | after := ColorRegex.ReplaceAllStringFunc(text, replace) 158 | return after 159 | } 160 | 161 | func StripColors(text string) string { 162 | return ColorRegex.ReplaceAllString(text, "") 163 | } 164 | -------------------------------------------------------------------------------- /database/room.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "github.com/Cristofori/kmud/types" 4 | 5 | type Exit struct { 6 | Locked bool 7 | } 8 | 9 | type Room struct { 10 | Container `bson:",inline"` 11 | 12 | ZoneId types.Id 13 | AreaId types.Id `bson:",omitempty"` 14 | Title string 15 | Description string 16 | Links map[string]types.Id 17 | Location types.Coordinate 18 | 19 | Exits map[types.Direction]*Exit 20 | } 21 | 22 | func NewRoom(zoneId types.Id, location types.Coordinate) *Room { 23 | room := &Room{ 24 | Title: "The Void", 25 | Description: "You are floating in the blackness of space. Complete darkness surrounds " + 26 | "you in all directions. There is no escape, there is no hope, just the emptiness. " + 27 | "You are likely to be eaten by a grue.", 28 | Location: location, 29 | ZoneId: zoneId, 30 | } 31 | 32 | dbinit(room) 33 | return room 34 | } 35 | 36 | func (self *Room) HasExit(dir types.Direction) bool { 37 | self.ReadLock() 38 | defer self.ReadUnlock() 39 | 40 | _, found := self.Exits[dir] 41 | return found 42 | } 43 | 44 | func (self *Room) SetExitEnabled(dir types.Direction, enabled bool) { 45 | self.writeLock(func() { 46 | if self.Exits == nil { 47 | self.Exits = map[types.Direction]*Exit{} 48 | } 49 | if enabled { 50 | self.Exits[dir] = &Exit{} 51 | } else { 52 | delete(self.Exits, dir) 53 | } 54 | }) 55 | } 56 | 57 | func (self *Room) SetLink(name string, roomId types.Id) { 58 | self.writeLock(func() { 59 | if self.Links == nil { 60 | self.Links = map[string]types.Id{} 61 | } 62 | self.Links[name] = roomId 63 | }) 64 | } 65 | 66 | func (self *Room) RemoveLink(name string) { 67 | self.writeLock(func() { 68 | delete(self.Links, name) 69 | }) 70 | } 71 | 72 | func (self *Room) GetLinks() map[string]types.Id { 73 | self.ReadLock() 74 | defer self.ReadUnlock() 75 | return self.Links 76 | } 77 | 78 | func (self *Room) LinkNames() []string { 79 | names := make([]string, len(self.GetLinks())) 80 | 81 | i := 0 82 | for name := range self.Links { 83 | names[i] = name 84 | i++ 85 | } 86 | return names 87 | } 88 | 89 | func (self *Room) SetTitle(title string) { 90 | self.writeLock(func() { 91 | self.Title = title 92 | }) 93 | } 94 | 95 | func (self *Room) GetTitle() string { 96 | self.ReadLock() 97 | defer self.ReadUnlock() 98 | return self.Title 99 | } 100 | 101 | func (self *Room) SetDescription(description string) { 102 | self.writeLock(func() { 103 | self.Description = description 104 | }) 105 | } 106 | 107 | func (self *Room) GetDescription() string { 108 | self.ReadLock() 109 | defer self.ReadUnlock() 110 | return self.Description 111 | } 112 | 113 | func (self *Room) SetLocation(location types.Coordinate) { 114 | self.writeLock(func() { 115 | self.Location = location 116 | }) 117 | } 118 | 119 | func (self *Room) GetLocation() types.Coordinate { 120 | self.ReadLock() 121 | defer self.ReadUnlock() 122 | return self.Location 123 | } 124 | 125 | func (self *Room) SetZoneId(zoneId types.Id) { 126 | self.writeLock(func() { 127 | self.ZoneId = zoneId 128 | }) 129 | } 130 | 131 | func (self *Room) GetZoneId() types.Id { 132 | self.ReadLock() 133 | defer self.ReadUnlock() 134 | return self.ZoneId 135 | } 136 | 137 | func (self *Room) SetAreaId(areaId types.Id) { 138 | self.writeLock(func() { 139 | self.AreaId = areaId 140 | }) 141 | } 142 | 143 | func (self *Room) GetAreaId() types.Id { 144 | self.ReadLock() 145 | defer self.ReadUnlock() 146 | return self.AreaId 147 | } 148 | 149 | func (self *Room) NextLocation(direction types.Direction) types.Coordinate { 150 | loc := self.GetLocation() 151 | return loc.Next(direction) 152 | } 153 | 154 | func (self *Room) GetExits() []types.Direction { 155 | self.ReadLock() 156 | defer self.ReadUnlock() 157 | 158 | exits := make([]types.Direction, len(self.Exits)) 159 | 160 | i := 0 161 | for dir := range self.Exits { 162 | exits[i] = dir 163 | i++ 164 | } 165 | 166 | return exits 167 | } 168 | 169 | func (self *Room) SetLocked(dir types.Direction, locked bool) { 170 | self.writeLock(func() { 171 | if self.HasExit(dir) { 172 | self.Exits[dir].Locked = locked 173 | } 174 | }) 175 | } 176 | 177 | func (self *Room) IsLocked(dir types.Direction) bool { 178 | self.ReadLock() 179 | defer self.ReadUnlock() 180 | 181 | if self.HasExit(dir) { 182 | return self.Exits[dir].Locked 183 | } 184 | 185 | return false 186 | } 187 | -------------------------------------------------------------------------------- /session/mapbuilder.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "github.com/Cristofori/kmud/types" 5 | "github.com/Cristofori/kmud/utils" 6 | ) 7 | 8 | type mapBuilder struct { 9 | width int 10 | height int 11 | depth int 12 | data [][][]mapTile 13 | userRoom types.Room 14 | } 15 | 16 | type mapTile struct { 17 | char rune 18 | color types.Color 19 | } 20 | 21 | func (self *mapTile) toString() string { 22 | if self.char == ' ' { 23 | return string(self.char) 24 | } 25 | 26 | return types.Colorize(self.color, string(self.char)) 27 | } 28 | 29 | func newMapBuilder(width int, height int, depth int) mapBuilder { 30 | var builder mapBuilder 31 | 32 | // Double the X/Y axis to account for extra space to draw exits 33 | width *= 2 34 | height *= 2 35 | 36 | builder.data = make([][][]mapTile, depth) 37 | 38 | for z := 0; z < depth; z++ { 39 | builder.data[z] = make([][]mapTile, height) 40 | for y := 0; y < height; y++ { 41 | builder.data[z][y] = make([]mapTile, width) 42 | } 43 | } 44 | 45 | builder.width = width 46 | builder.height = height 47 | builder.depth = depth 48 | 49 | for z := 0; z < depth; z++ { 50 | for y := 0; y < height; y++ { 51 | for x := 0; x < width; x++ { 52 | builder.data[z][y][x].char = ' ' 53 | } 54 | } 55 | } 56 | 57 | return builder 58 | } 59 | 60 | func (self *mapBuilder) setUserRoom(room types.Room) { 61 | self.userRoom = room 62 | } 63 | 64 | func (self *mapBuilder) addRoom(room types.Room, x int, y int, z int) { 65 | x = x * 2 66 | y = y * 2 67 | 68 | addIfExists := func(dir types.Direction, x int, y int) { 69 | if x < 0 || y < 0 { 70 | return 71 | } 72 | 73 | if room.HasExit(dir) { 74 | self.data[z][y][x].addExit(dir) 75 | } 76 | } 77 | 78 | if self.userRoom.GetId() == room.GetId() { 79 | self.data[z][y][x].char = '*' 80 | self.data[z][y][x].color = types.ColorRed 81 | } else { 82 | self.data[z][y][x].color = types.ColorMagenta 83 | if room.HasExit(types.DirectionUp) && room.HasExit(types.DirectionDown) { 84 | self.data[z][y][x].char = '+' 85 | } else if room.HasExit(types.DirectionUp) { 86 | self.data[z][y][x].char = '^' 87 | } else if room.HasExit(types.DirectionDown) { 88 | self.data[z][y][x].char = 'v' 89 | } else { 90 | char := '#' 91 | 92 | /* 93 | count := len(model.CharactersIn(room.GetId())) 94 | if count < 10 { 95 | char = rune(strconv.Itoa(count)[0]) 96 | } 97 | */ 98 | 99 | self.data[z][y][x].char = char 100 | self.data[z][y][x].color = types.ColorWhite 101 | } 102 | } 103 | 104 | addIfExists(types.DirectionNorth, x, y-1) 105 | addIfExists(types.DirectionNorthEast, x+1, y-1) 106 | addIfExists(types.DirectionEast, x+1, y) 107 | addIfExists(types.DirectionSouthEast, x+1, y+1) 108 | addIfExists(types.DirectionSouth, x, y+1) 109 | addIfExists(types.DirectionSouthWest, x-1, y+1) 110 | addIfExists(types.DirectionWest, x-1, y) 111 | addIfExists(types.DirectionNorthWest, x-1, y-1) 112 | } 113 | 114 | func (self *mapBuilder) toString() string { 115 | str := "" 116 | 117 | for z := 0; z < self.depth; z++ { 118 | var rows []string 119 | for y := 0; y < self.height; y++ { 120 | row := "" 121 | for x := 0; x < self.width; x++ { 122 | tile := self.data[z][y][x].toString() 123 | row = row + tile 124 | } 125 | rows = append(rows, row) 126 | } 127 | 128 | rows = utils.TrimLowerRows(rows) 129 | 130 | if self.depth > 1 { 131 | divider := types.Colorize(types.ColorWhite, "================================================================================\r\n") 132 | rows = append(rows, divider) 133 | } 134 | 135 | for _, row := range rows { 136 | str = str + row + "\r\n" 137 | } 138 | } 139 | 140 | return str 141 | } 142 | 143 | func (self *mapTile) addExit(dir types.Direction) { 144 | combineChars := func(r1 rune, r2 rune, r3 rune) { 145 | if self.char == r1 { 146 | self.char = r2 147 | } else { 148 | self.char = r3 149 | } 150 | } 151 | 152 | self.color = types.ColorBlue 153 | 154 | switch dir { 155 | case types.DirectionNorth: 156 | combineChars('|', '|', '|') 157 | case types.DirectionNorthEast: 158 | combineChars('\\', 'X', '/') 159 | case types.DirectionEast: 160 | combineChars('-', '-', '-') 161 | case types.DirectionSouthEast: 162 | combineChars('/', 'X', '\\') 163 | case types.DirectionSouth: 164 | combineChars('|', '|', '|') 165 | case types.DirectionSouthWest: 166 | combineChars('\\', 'X', '/') 167 | case types.DirectionWest: 168 | combineChars('-', '-', '-') 169 | case types.DirectionNorthWest: 170 | combineChars('/', 'X', '\\') 171 | default: 172 | panic("Unexpected direction given to mapTile::addExit()") 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /telnet/telnet_test.go: -------------------------------------------------------------------------------- 1 | package telnet 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type fakeConn struct { 11 | data []byte 12 | } 13 | 14 | func (self *fakeConn) Write(p []byte) (int, error) { 15 | self.data = append(self.data, p...) 16 | return len(p), nil 17 | } 18 | 19 | func (self *fakeConn) Read(p []byte) (int, error) { 20 | n := 0 21 | 22 | for i := 0; i < len(p) && i < len(self.data); i++ { 23 | p[i] = self.data[i] 24 | n++ 25 | } 26 | 27 | self.data = self.data[n:] 28 | 29 | return n, nil 30 | } 31 | 32 | func (self *fakeConn) Close() error { 33 | return nil 34 | } 35 | 36 | func (self *fakeConn) LocalAddr() net.Addr { 37 | return nil 38 | } 39 | 40 | func (self *fakeConn) RemoteAddr() net.Addr { 41 | return nil 42 | } 43 | 44 | func (self *fakeConn) SetDeadline(t time.Time) error { 45 | return nil 46 | } 47 | 48 | func (self *fakeConn) SetReadDeadline(t time.Time) error { 49 | return nil 50 | } 51 | 52 | func (self *fakeConn) SetWriteDeadline(t time.Time) error { 53 | return nil 54 | } 55 | 56 | func compareData(d1 []byte, d2 []byte) bool { 57 | if len(d1) != len(d2) { 58 | return false 59 | } 60 | 61 | for i, b := range d1 { 62 | if b != d2[i] { 63 | return false 64 | } 65 | } 66 | 67 | return true 68 | } 69 | 70 | func Test_Processor(t *testing.T) { 71 | var fc fakeConn 72 | fc.data = []byte{} 73 | 74 | telnet := NewTelnet(&fc) 75 | testStr := "test" 76 | readBuffer := make([]byte, 1024) 77 | 78 | data := []byte(testStr) 79 | telnet.Write(data) 80 | n, err := telnet.Read(readBuffer) 81 | result := readBuffer[:n] 82 | 83 | if compareData(result, []byte(testStr)) == false || err != nil { 84 | t.Errorf("Process(%s) == '%s', want '%s'", data, result, testStr) 85 | } 86 | 87 | subdataResult := telnet.Data(WS) 88 | if subdataResult != nil { 89 | t.Errorf("Subdata should have been nil") 90 | } 91 | 92 | data = append(data, BuildCommand(WILL, ECHO)...) 93 | telnet.Write(data) 94 | n, err = telnet.Read(readBuffer) 95 | result = readBuffer[:n] 96 | 97 | if compareData(result, []byte(testStr)) == false || err != nil { 98 | t.Errorf("Process(%s) == '%s', want '%s'", data, result, testStr) 99 | } 100 | 101 | if telnet.processor.subdata != nil { 102 | t.Errorf("Subdata should have been nil") 103 | } 104 | 105 | data = append(data, []byte(" another test")...) 106 | testStr = testStr + " another test" 107 | telnet.Write(data) 108 | n, err = telnet.Read(readBuffer) 109 | result = readBuffer[:n] 110 | 111 | if compareData(result, []byte(testStr)) == false { 112 | t.Errorf("Process(%s) == '%s', want '%s'", data, result, testStr) 113 | } 114 | 115 | if telnet.processor.subdata != nil { 116 | t.Errorf("Subdata should have been nil") 117 | } 118 | 119 | subData := []byte{'\x00', '\x12', '\x99'} 120 | 121 | data = append(data, BuildCommand(SB, WS)...) 122 | data = append(data, subData...) 123 | data = append(data, BuildCommand(SE)...) 124 | 125 | telnet.Write(data) 126 | n, err = telnet.Read(readBuffer) 127 | result = readBuffer[:n] 128 | 129 | if compareData(result, []byte(testStr)) == false { 130 | t.Errorf("Process(%s) == '%s', want '%s'", data, result, testStr) 131 | } 132 | 133 | if compareData(telnet.Data(WS), subData) == false { 134 | t.Errorf("Process(%s), Subdata == %v, want %v", data, telnet.Data(WS), subData) 135 | } 136 | 137 | data = append(data, []byte(" again")...) 138 | testStr = testStr + " again" 139 | 140 | telnet.Write(data) 141 | n, err = telnet.Read(readBuffer) 142 | result = readBuffer[:n] 143 | 144 | if compareData(result, []byte(testStr)) == false { 145 | t.Errorf("Process(%s) == '%s', want '%s'", data, result, testStr) 146 | } 147 | 148 | if compareData(telnet.Data(WS), subData) == false { 149 | t.Errorf("Process(%s), Subdata == %v, want %v", data, telnet.Data(WS), subData) 150 | } 151 | 152 | // Interpret escaped FF bytes properly 153 | subData = []byte{'\x00', '\x12', '\x99', '\xFF', '\xFF', '\x42'} 154 | wantedSubData := []byte{'\x00', '\x12', '\x99', '\xFF', '\x42'} 155 | 156 | data = append(data, BuildCommand(SB, WS)...) 157 | data = append(data, subData...) 158 | data = append(data, BuildCommand(SE)...) 159 | 160 | telnet.Write(data) 161 | n, err = telnet.Read(readBuffer) 162 | result = readBuffer[:n] 163 | 164 | if compareData(result, []byte(testStr)) == false { 165 | t.Errorf("Process(%s) == '%s', want '%s'", data, result, testStr) 166 | } 167 | 168 | if compareData(telnet.Data(WS), wantedSubData) == false { 169 | t.Errorf("Process(%s), Subdata == %v, want %v", data, telnet.Data(WS), wantedSubData) 170 | } 171 | 172 | // Test with bufio 173 | testStr = "bufio test\n" 174 | data = []byte(testStr) 175 | 176 | reader := bufio.NewReader(telnet) 177 | telnet.Write(data) 178 | 179 | bytes, err := reader.ReadBytes('\n') 180 | 181 | if compareData(bytes, data) == false { 182 | t.Errorf("Bufio failure %v != %v", bytes, data) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /utils/menu.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/Cristofori/kmud/types" 9 | ) 10 | 11 | const decorator = "-=-=-" 12 | 13 | type Menu struct { 14 | actions []action 15 | title string 16 | exit bool 17 | exitHandler func() 18 | } 19 | 20 | func ExecMenu(title string, comm types.Communicable, build func(*Menu)) { 21 | pageIndex := 0 22 | pageCount := 1 23 | filter := "" 24 | 25 | for { 26 | var menu Menu 27 | menu.title = title 28 | menu.exit = false 29 | build(&menu) 30 | 31 | pageIndex = Bound(pageIndex, 0, pageCount-1) 32 | pageCount = menu.Print(comm, pageIndex, filter) 33 | filter = "" 34 | 35 | prompt := "" 36 | if pageCount > 1 { 37 | prompt = fmt.Sprintf("Page %v of %v (<, >, <<, >>)\r\n> ", pageIndex+1, pageCount) 38 | } else { 39 | prompt = "> " 40 | } 41 | 42 | input := comm.GetInput(types.Colorize(types.ColorWhite, prompt)) 43 | 44 | if input == "" { 45 | if menu.exitHandler != nil { 46 | menu.exitHandler() 47 | } 48 | return 49 | } 50 | 51 | if input == ">" { 52 | pageIndex++ 53 | } else if input == "<" { 54 | pageIndex-- 55 | } else if input == ">>" { 56 | pageIndex = pageCount - 1 57 | } else if input == "<<" { 58 | pageIndex = 0 59 | } else if input[0] == '/' { 60 | filter = input[1:] 61 | } else { 62 | action := menu.getAction(input) 63 | 64 | if action.handler != nil { 65 | action.handler() 66 | if menu.exit { 67 | if menu.exitHandler != nil { 68 | menu.exitHandler() 69 | } 70 | return 71 | } 72 | } else if input != "?" && input != "help" { 73 | comm.WriteLine(types.Colorize(types.ColorRed, "Invalid selection")) 74 | } 75 | } 76 | } 77 | } 78 | 79 | type action struct { 80 | key string 81 | text string 82 | data types.Id 83 | handler func() 84 | } 85 | 86 | func (self *Menu) AddAction(key string, text string, handler func()) { 87 | if self.HasAction(key) { 88 | panic(fmt.Sprintf("Duplicate action added to menu: %s %s", key, text)) 89 | } 90 | 91 | self.actions = append(self.actions, 92 | action{key: strings.ToLower(key), 93 | text: text, 94 | handler: handler, 95 | }) 96 | } 97 | 98 | func (self *Menu) AddActionI(index int, text string, handler func()) { 99 | self.AddAction(strconv.Itoa(index+1), text, handler) 100 | } 101 | 102 | func (self *Menu) SetTitle(title string) { 103 | self.title = title 104 | } 105 | 106 | func (self *Menu) Exit() { 107 | self.exit = true 108 | } 109 | 110 | func (self *Menu) OnExit(handler func()) { 111 | self.exitHandler = handler 112 | } 113 | 114 | func (self *Menu) GetData(choice string) types.Id { 115 | for _, action := range self.actions { 116 | if action.key == choice { 117 | return action.data 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (self *Menu) getAction(key string) action { 125 | key = strings.ToLower(key) 126 | 127 | for _, action := range self.actions { 128 | if action.key == key { 129 | return action 130 | } 131 | } 132 | return action{} 133 | } 134 | 135 | func (self *Menu) HasAction(key string) bool { 136 | action := self.getAction(key) 137 | return action.key != "" 138 | } 139 | 140 | func filterActions(actions []action, filter string) []action { 141 | var filtered []action 142 | 143 | for _, action := range actions { 144 | if FilterItem(action.text, filter) { 145 | filtered = append(filtered, action) 146 | } 147 | } 148 | 149 | return filtered 150 | } 151 | 152 | func (self *Menu) Print(comm types.Communicable, page int, filter string) int { 153 | border := types.Colorize(types.ColorWhite, decorator) 154 | title := types.Colorize(types.ColorBlue, self.title) 155 | header := fmt.Sprintf("%s %s %s", border, title, border) 156 | 157 | if filter != "" { 158 | header = fmt.Sprintf("%s (/%s)", header, filter) 159 | } 160 | 161 | comm.WriteLine(header) 162 | 163 | filteredActions := filterActions(self.actions, filter) 164 | options := make([]string, len(filteredActions)) 165 | 166 | for i, action := range filteredActions { 167 | index := strings.Index(strings.ToLower(action.text), action.key) 168 | 169 | actionText := "" 170 | 171 | if index == -1 { 172 | actionText = fmt.Sprintf("%s[%s%s%s]%s%s", 173 | types.ColorDarkBlue, 174 | types.ColorBlue, 175 | strings.ToUpper(action.key), 176 | types.ColorDarkBlue, 177 | types.ColorWhite, 178 | action.text) 179 | } else { 180 | keyLength := len(action.key) 181 | actionText = fmt.Sprintf("%s%s[%s%s%s]%s%s", 182 | action.text[:index], 183 | types.ColorDarkBlue, 184 | types.ColorBlue, 185 | action.text[index:index+keyLength], 186 | types.ColorDarkBlue, 187 | types.ColorWhite, 188 | action.text[index+keyLength:]) 189 | } 190 | 191 | options[i] = fmt.Sprintf(" %s", actionText) 192 | } 193 | 194 | width, height := comm.GetWindowSize() 195 | pages := Paginate(options, width, height/2) 196 | 197 | if len(options) == 0 && filter != "" { 198 | comm.WriteLine("No items match your search") 199 | } else { 200 | comm.Write(pages[page]) 201 | } 202 | 203 | return len(pages) 204 | } 205 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Cristofori/kmud/datastore" 10 | "github.com/Cristofori/kmud/types" 11 | "github.com/Cristofori/kmud/utils" 12 | "gopkg.in/mgo.v2/bson" 13 | ) 14 | 15 | type Session interface { 16 | DB(string) Database 17 | } 18 | 19 | type Database interface { 20 | C(string) Collection 21 | } 22 | 23 | type Collection interface { 24 | Find(interface{}) Query 25 | FindId(interface{}) Query 26 | RemoveId(interface{}) error 27 | Remove(interface{}) error 28 | DropCollection() error 29 | UpdateId(interface{}, interface{}) error 30 | UpsertId(interface{}, interface{}) error 31 | } 32 | 33 | type Query interface { 34 | Count() (int, error) 35 | One(interface{}) error 36 | Iter() Iterator 37 | } 38 | 39 | type Iterator interface { 40 | All(interface{}) error 41 | } 42 | 43 | var modifiedObjects = map[types.Id]bool{} 44 | var modifiedObjectChannel chan types.Id 45 | 46 | var _session Session 47 | var _dbName string 48 | 49 | func init() { 50 | modifiedObjectChannel = make(chan types.Id, 1) 51 | watchModifiedObjects() 52 | } 53 | 54 | func Init(session Session, dbName string) { 55 | _session = session 56 | _dbName = dbName 57 | } 58 | 59 | func dbinit(obj types.Object) { 60 | obj.SetId(bson.NewObjectId()) 61 | datastore.Set(obj) 62 | commitObject(obj.GetId()) 63 | } 64 | 65 | func watchModifiedObjects() { 66 | go func() { 67 | timeout := make(chan bool) 68 | 69 | startTimeout := func() { 70 | go func() { 71 | time.Sleep(1 * time.Second) 72 | timeout <- true 73 | }() 74 | } 75 | 76 | startTimeout() 77 | 78 | for { 79 | select { 80 | case id := <-modifiedObjectChannel: 81 | modifiedObjects[id] = true 82 | case <-timeout: 83 | for id := range modifiedObjects { 84 | go commitObject(id) 85 | } 86 | modifiedObjects = map[types.Id]bool{} 87 | startTimeout() 88 | } 89 | } 90 | }() 91 | } 92 | 93 | func getCollection(collection types.ObjectType) Collection { 94 | return _session.DB(_dbName).C(string(collection)) 95 | } 96 | 97 | func getCollectionOfObject(obj types.Object) Collection { 98 | name := reflect.TypeOf(obj).String() 99 | parts := strings.Split(name, ".") 100 | name = parts[len(parts)-1] 101 | 102 | return getCollection(types.ObjectType(name)) 103 | } 104 | 105 | func Retrieve(id types.Id, typ types.ObjectType) types.Object { 106 | if datastore.ContainsId(id) { 107 | return datastore.Get(id) 108 | } 109 | 110 | var object types.Object 111 | 112 | switch typ { 113 | case types.PcType: 114 | object = &Pc{} 115 | case types.NpcType: 116 | object = &Npc{} 117 | case types.SpawnerType: 118 | object = &Spawner{} 119 | case types.UserType: 120 | object = &User{} 121 | case types.ZoneType: 122 | object = &Zone{} 123 | case types.AreaType: 124 | object = &Area{} 125 | case types.RoomType: 126 | object = &Room{} 127 | case types.TemplateType: 128 | object = &Template{} 129 | case types.ItemType: 130 | object = &Item{} 131 | case types.SkillType: 132 | object = &Skill{} 133 | case types.EffectType: 134 | object = &Effect{} 135 | case types.StoreType: 136 | object = &Store{} 137 | case types.WorldType: 138 | object = &World{} 139 | default: 140 | panic(fmt.Sprintf("unrecognized object type: %v", typ)) 141 | } 142 | 143 | c := getCollectionOfObject(object) 144 | err := c.FindId(id).One(object) 145 | 146 | if err != nil || object == nil { 147 | return nil 148 | } 149 | 150 | datastore.Set(object) 151 | return object 152 | } 153 | 154 | func RetrieveObjects(t types.ObjectType, objects interface{}) { 155 | c := getCollection(t) 156 | err := c.Find(nil).Iter().All(objects) 157 | utils.HandleError(err) 158 | } 159 | 160 | func Find(t types.ObjectType, query bson.M) []bson.ObjectId { 161 | return find(t, query) 162 | } 163 | 164 | func FindOne(t types.ObjectType, query bson.M) types.Id { 165 | var result bson.M 166 | find_helper(t, query).One(&result) 167 | id, found := result["_id"] 168 | if found { 169 | return id.(bson.ObjectId) 170 | } 171 | return nil 172 | } 173 | 174 | func FindAll(t types.ObjectType) []bson.ObjectId { 175 | return find(t, nil) 176 | } 177 | 178 | func find(t types.ObjectType, query interface{}) []bson.ObjectId { 179 | var results []bson.M 180 | find_helper(t, query).Iter().All(&results) 181 | 182 | var ids []bson.ObjectId 183 | for _, result := range results { 184 | ids = append(ids, result["_id"].(bson.ObjectId)) 185 | } 186 | 187 | return ids 188 | } 189 | 190 | func find_helper(t types.ObjectType, query interface{}) Query { 191 | c := getCollection(t) 192 | return c.Find(query) 193 | } 194 | 195 | func DeleteObject(id types.Id) { 196 | object := datastore.Get(id) 197 | datastore.Remove(object) 198 | 199 | object.Destroy() 200 | 201 | c := getCollectionOfObject(object) 202 | utils.HandleError(c.RemoveId(object.GetId())) 203 | } 204 | 205 | func commitObject(id types.Id) { 206 | object := datastore.Get(id) 207 | 208 | if object == nil || object.IsDestroyed() { 209 | return 210 | } 211 | 212 | c := getCollectionOfObject(object) 213 | 214 | object.ReadLock() 215 | err := c.UpsertId(object.GetId(), object) 216 | object.ReadUnlock() 217 | 218 | if err != nil { 219 | fmt.Println("Update failed", object.GetId()) 220 | } 221 | 222 | utils.HandleError(err) 223 | } 224 | -------------------------------------------------------------------------------- /database/character.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Cristofori/kmud/types" 7 | "github.com/Cristofori/kmud/utils" 8 | ) 9 | 10 | type Character struct { 11 | Container `bson:",inline"` 12 | 13 | RoomId types.Id `bson:",omitempty"` 14 | Name string 15 | HitPoints int 16 | Skills utils.Set 17 | 18 | Strength int 19 | Vitality int 20 | } 21 | 22 | type Pc struct { 23 | Character `bson:",inline"` 24 | 25 | UserId types.Id 26 | online bool 27 | } 28 | 29 | type Npc struct { 30 | Character `bson:",inline"` 31 | 32 | SpawnerId types.Id `bson:",omitempty"` 33 | 34 | Roaming bool 35 | Conversation string 36 | } 37 | 38 | type Spawner struct { 39 | Character `bson:",inline"` 40 | 41 | AreaId types.Id 42 | Count int 43 | } 44 | 45 | func NewPc(name string, userId types.Id, roomId types.Id) *Pc { 46 | pc := &Pc{ 47 | UserId: userId, 48 | online: false, 49 | } 50 | 51 | pc.initCharacter(name, types.PcType, roomId) 52 | dbinit(pc) 53 | return pc 54 | } 55 | 56 | func NewNpc(name string, roomId types.Id, spawnerId types.Id) *Npc { 57 | npc := &Npc{ 58 | SpawnerId: spawnerId, 59 | } 60 | 61 | npc.initCharacter(name, types.NpcType, roomId) 62 | dbinit(npc) 63 | return npc 64 | } 65 | 66 | func NewSpawner(name string, areaId types.Id) *Spawner { 67 | spawner := &Spawner{ 68 | AreaId: areaId, 69 | Count: 1, 70 | } 71 | 72 | spawner.initCharacter(name, types.SpawnerType, nil) 73 | dbinit(spawner) 74 | return spawner 75 | } 76 | 77 | func (self *Character) initCharacter(name string, objType types.ObjectType, roomId types.Id) { 78 | self.RoomId = roomId 79 | self.Cash = 0 80 | self.HitPoints = 100 81 | self.Name = utils.FormatName(name) 82 | 83 | self.Strength = 10 84 | self.Vitality = 100 85 | } 86 | 87 | func (self *Character) GetName() string { 88 | self.ReadLock() 89 | defer self.ReadUnlock() 90 | 91 | return self.Name 92 | } 93 | 94 | func (self *Character) SetName(name string) { 95 | self.writeLock(func() { 96 | self.Name = utils.FormatName(name) 97 | }) 98 | } 99 | 100 | func (self *Character) GetCapacity() int { 101 | return self.GetStrength() * 10 102 | } 103 | 104 | func (self *Character) GetStrength() int { 105 | self.ReadLock() 106 | defer self.ReadUnlock() 107 | return self.Strength 108 | } 109 | 110 | func (self *Pc) SetOnline(online bool) { 111 | self.WriteLock() 112 | defer self.WriteUnlock() 113 | self.online = online 114 | } 115 | 116 | func (self *Pc) IsOnline() bool { 117 | self.ReadLock() 118 | defer self.ReadUnlock() 119 | return self.online 120 | } 121 | 122 | func (self *Character) SetRoomId(id types.Id) { 123 | self.writeLock(func() { 124 | self.RoomId = id 125 | }) 126 | } 127 | 128 | func (self *Character) GetRoomId() types.Id { 129 | self.ReadLock() 130 | defer self.ReadUnlock() 131 | return self.RoomId 132 | } 133 | 134 | func (self *Pc) SetUserId(id types.Id) { 135 | self.writeLock(func() { 136 | self.UserId = id 137 | }) 138 | } 139 | 140 | func (self *Pc) GetUserId() types.Id { 141 | self.ReadLock() 142 | defer self.ReadUnlock() 143 | return self.UserId 144 | } 145 | 146 | func (self *Character) AddSkill(id types.Id) { 147 | self.writeLock(func() { 148 | if self.Skills == nil { 149 | self.Skills = utils.Set{} 150 | } 151 | self.Skills.Insert(id.Hex()) 152 | }) 153 | } 154 | 155 | func (self *Character) RemoveSkill(id types.Id) { 156 | self.writeLock(func() { 157 | self.Skills.Remove(id.Hex()) 158 | }) 159 | } 160 | 161 | func (self *Character) HasSkill(id types.Id) bool { 162 | self.ReadLock() 163 | defer self.ReadUnlock() 164 | return self.Skills.Contains(id.Hex()) 165 | } 166 | 167 | func (self *Character) GetSkills() []types.Id { 168 | self.ReadLock() 169 | defer self.ReadUnlock() 170 | return idSetToList(self.Skills) 171 | } 172 | 173 | func (self *Npc) SetConversation(conversation string) { 174 | self.writeLock(func() { 175 | self.Conversation = conversation 176 | }) 177 | } 178 | 179 | func (self *Npc) GetConversation() string { 180 | self.ReadLock() 181 | defer self.ReadUnlock() 182 | return self.Conversation 183 | } 184 | 185 | func (self *Npc) PrettyConversation() string { 186 | conv := self.GetConversation() 187 | 188 | if conv == "" { 189 | return fmt.Sprintf("%s has nothing to say", self.GetName()) 190 | } 191 | 192 | return fmt.Sprintf("%s%s", 193 | types.Colorize(types.ColorBlue, self.GetName()), 194 | types.Colorize(types.ColorWhite, ": "+conv)) 195 | } 196 | 197 | func (self *Character) SetHealth(health int) { 198 | self.writeLock(func() { 199 | self.Vitality = health 200 | if self.HitPoints > self.Vitality { 201 | self.HitPoints = self.Vitality 202 | } 203 | }) 204 | } 205 | 206 | func (self *Character) GetHealth() int { 207 | self.ReadLock() 208 | defer self.ReadUnlock() 209 | return self.Vitality 210 | } 211 | 212 | func (self *Character) SetHitPoints(hitpoints int) { 213 | self.writeLock(func() { 214 | if hitpoints > self.Vitality { 215 | hitpoints = self.Vitality 216 | } 217 | self.HitPoints = hitpoints 218 | }) 219 | } 220 | 221 | func (self *Character) GetHitPoints() int { 222 | self.ReadLock() 223 | defer self.ReadUnlock() 224 | return self.HitPoints 225 | } 226 | 227 | func (self *Character) Hit(hitpoints int) { 228 | self.SetHitPoints(self.GetHitPoints() - hitpoints) 229 | } 230 | 231 | func (self *Character) Heal(hitpoints int) { 232 | self.SetHitPoints(self.GetHitPoints() + hitpoints) 233 | } 234 | 235 | func (self *Npc) GetRoaming() bool { 236 | self.ReadLock() 237 | defer self.ReadUnlock() 238 | return self.Roaming 239 | } 240 | 241 | func (self *Npc) SetRoaming(roaming bool) { 242 | self.writeLock(func() { 243 | self.Roaming = roaming 244 | }) 245 | } 246 | 247 | func (self *Spawner) SetCount(count int) { 248 | self.writeLock(func() { 249 | self.Count = count 250 | }) 251 | } 252 | 253 | func (self *Spawner) GetCount() int { 254 | self.ReadLock() 255 | defer self.ReadUnlock() 256 | return self.Count 257 | } 258 | 259 | func (self *Spawner) GetAreaId() types.Id { 260 | self.ReadLock() 261 | defer self.ReadUnlock() 262 | return self.AreaId 263 | } 264 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/Cristofori/kmud/utils/naturalsort" 7 | ) 8 | 9 | type Id interface { 10 | String() string 11 | Hex() string 12 | } 13 | 14 | type ObjectType string 15 | 16 | const ( 17 | NpcType ObjectType = "Npc" 18 | PcType ObjectType = "Pc" 19 | SpawnerType ObjectType = "Spawner" 20 | UserType ObjectType = "User" 21 | ZoneType ObjectType = "Zone" 22 | AreaType ObjectType = "Area" 23 | RoomType ObjectType = "Room" 24 | TemplateType ObjectType = "Template" 25 | ItemType ObjectType = "Item" 26 | SkillType ObjectType = "Skill" 27 | EffectType ObjectType = "Effect" 28 | StoreType ObjectType = "Store" 29 | WorldType ObjectType = "World" 30 | ) 31 | 32 | type Identifiable interface { 33 | GetId() Id 34 | } 35 | 36 | type ReadLockable interface { 37 | ReadLock() 38 | ReadUnlock() 39 | } 40 | 41 | type Destroyable interface { 42 | Destroy() 43 | IsDestroyed() bool 44 | } 45 | 46 | type Locateable interface { 47 | GetRoomId() Id 48 | } 49 | 50 | type Nameable interface { 51 | GetName() string 52 | SetName(string) 53 | } 54 | 55 | type Loginable interface { 56 | IsOnline() bool 57 | SetOnline(bool) 58 | } 59 | 60 | type Container interface { 61 | AddCash(int) 62 | RemoveCash(int) 63 | GetCash() int 64 | SetCapacity(int) 65 | GetCapacity() int 66 | } 67 | 68 | type Object interface { 69 | Identifiable 70 | ReadLockable 71 | Destroyable 72 | SetId(Id) 73 | } 74 | 75 | type Character interface { 76 | Object 77 | Nameable 78 | Locateable 79 | Container 80 | SetRoomId(Id) 81 | Hit(int) 82 | Heal(int) 83 | GetHitPoints() int 84 | SetHitPoints(int) 85 | GetHealth() int 86 | SetHealth(int) 87 | GetSkills() []Id 88 | AddSkill(Id) 89 | } 90 | 91 | type CharacterList []Character 92 | 93 | func (self CharacterList) Names() []string { 94 | names := make([]string, len(self)) 95 | for i, char := range self { 96 | names[i] = char.GetName() 97 | } 98 | return names 99 | } 100 | 101 | type PC interface { 102 | Character 103 | Loginable 104 | } 105 | 106 | type PCList []PC 107 | 108 | func (self PCList) Characters() CharacterList { 109 | chars := make(CharacterList, len(self)) 110 | for i, pc := range self { 111 | chars[i] = pc 112 | } 113 | return chars 114 | } 115 | 116 | type NPC interface { 117 | Character 118 | SetRoaming(bool) 119 | GetRoaming() bool 120 | SetConversation(string) 121 | GetConversation() string 122 | PrettyConversation() string 123 | } 124 | 125 | type NPCList []NPC 126 | 127 | type Spawner interface { 128 | Character 129 | GetAreaId() Id 130 | SetCount(int) 131 | GetCount() int 132 | } 133 | 134 | type SpawnerList []Spawner 135 | 136 | func (self NPCList) Characters() CharacterList { 137 | chars := make(CharacterList, len(self)) 138 | for i, npc := range self { 139 | chars[i] = npc 140 | } 141 | return chars 142 | } 143 | 144 | type Room interface { 145 | Object 146 | Container 147 | GetZoneId() Id 148 | GetAreaId() Id 149 | SetAreaId(Id) 150 | GetLocation() Coordinate 151 | SetExitEnabled(Direction, bool) 152 | HasExit(Direction) bool 153 | NextLocation(Direction) Coordinate 154 | GetExits() []Direction 155 | GetTitle() string 156 | SetTitle(string) 157 | GetDescription() string 158 | SetDescription(string) 159 | SetLink(string, Id) 160 | RemoveLink(string) 161 | GetLinks() map[string]Id 162 | LinkNames() []string 163 | SetLocked(Direction, bool) 164 | IsLocked(Direction) bool 165 | } 166 | 167 | type RoomList []Room 168 | 169 | type Area interface { 170 | Object 171 | Nameable 172 | } 173 | 174 | type AreaList []Area 175 | 176 | type Zone interface { 177 | Object 178 | Nameable 179 | } 180 | 181 | type ZoneList []Zone 182 | 183 | type Time interface { 184 | String() string 185 | } 186 | 187 | type World interface { 188 | GetTime() Time 189 | AdvanceTime() 190 | } 191 | 192 | type Communicable interface { 193 | WriteLine(string, ...interface{}) 194 | Write(string) 195 | GetInput(prompt string) string 196 | GetWindowSize() (int, int) 197 | } 198 | 199 | type User interface { 200 | Object 201 | Nameable 202 | Loginable 203 | Communicable 204 | VerifyPassword(string) bool 205 | SetConnection(net.Conn) 206 | GetConnection() net.Conn 207 | SetWindowSize(int, int) 208 | SetTerminalType(string) 209 | GetTerminalType() string 210 | GetColorMode() ColorMode 211 | SetColorMode(ColorMode) 212 | IsAdmin() bool 213 | SetAdmin(bool) 214 | } 215 | 216 | type UserList []User 217 | 218 | func (self UserList) Len() int { 219 | return len(self) 220 | } 221 | 222 | func (self UserList) Less(i, j int) bool { 223 | return naturalsort.NaturalLessThan(self[i].GetName(), self[j].GetName()) 224 | } 225 | 226 | func (self UserList) Swap(i, j int) { 227 | self[i], self[j] = self[j], self[i] 228 | } 229 | 230 | type Template interface { 231 | Object 232 | Nameable 233 | SetValue(int) 234 | GetValue() int 235 | SetWeight(int) 236 | GetWeight() int 237 | GetCapacity() int 238 | SetCapacity(int) 239 | } 240 | 241 | type TemplateList []Template 242 | 243 | func (self TemplateList) Names() []string { 244 | names := make([]string, len(self)) 245 | for i, item := range self { 246 | names[i] = item.GetName() 247 | } 248 | return names 249 | } 250 | 251 | func (self TemplateList) Len() int { 252 | return len(self) 253 | } 254 | 255 | func (self TemplateList) Less(i, j int) bool { 256 | return naturalsort.NaturalLessThan(self[i].GetName(), self[j].GetName()) 257 | } 258 | 259 | func (self TemplateList) Swap(i, j int) { 260 | self[i], self[j] = self[j], self[i] 261 | } 262 | 263 | type Item interface { 264 | Object 265 | Container 266 | GetTemplateId() Id 267 | GetName() string 268 | GetValue() int 269 | SetLocked(bool) 270 | IsLocked() bool 271 | GetContainerId() Id 272 | SetContainerId(Id, Id) bool 273 | } 274 | 275 | type ItemList []Item 276 | 277 | func (self ItemList) Names() []string { 278 | names := make([]string, len(self)) 279 | for i, item := range self { 280 | names[i] = item.GetName() 281 | } 282 | return names 283 | } 284 | 285 | func (self ItemList) Len() int { 286 | return len(self) 287 | } 288 | 289 | func (self ItemList) Less(i, j int) bool { 290 | return naturalsort.NaturalLessThan(self[i].GetName(), self[j].GetName()) 291 | } 292 | 293 | func (self ItemList) Swap(i, j int) { 294 | self[i], self[j] = self[j], self[i] 295 | } 296 | 297 | type Skill interface { 298 | Object 299 | Nameable 300 | GetEffects() []Id 301 | AddEffect(Id) 302 | RemoveEffect(Id) 303 | HasEffect(Id) bool 304 | } 305 | 306 | type SkillList []Skill 307 | 308 | type EffectKind string 309 | 310 | const ( 311 | HitpointEffect EffectKind = "hitpoint" 312 | SilenceEffect EffectKind = "silence" 313 | StunEffect EffectKind = "stun" 314 | ) 315 | 316 | func (self SkillList) Names() []string { 317 | names := make([]string, len(self)) 318 | for i, skill := range self { 319 | names[i] = skill.GetName() 320 | } 321 | return names 322 | } 323 | 324 | type Effect interface { 325 | Object 326 | Nameable 327 | SetPower(int) 328 | GetPower() int 329 | SetCost(int) 330 | GetCost() int 331 | GetType() EffectKind 332 | SetType(EffectKind) 333 | GetVariance() int 334 | SetVariance(int) 335 | GetSpeed() int 336 | SetSpeed(int) 337 | GetTime() int 338 | SetTime(int) 339 | } 340 | 341 | type EffectList []Effect 342 | 343 | func (self EffectList) Names() []string { 344 | names := make([]string, len(self)) 345 | for i, skill := range self { 346 | names[i] = skill.GetName() 347 | } 348 | return names 349 | } 350 | 351 | type Store interface { 352 | Object 353 | Nameable 354 | Container 355 | } 356 | 357 | type Purchaser interface { 358 | GetId() Id 359 | AddCash(int) 360 | RemoveCash(int) 361 | } 362 | -------------------------------------------------------------------------------- /database/dbtest/dbtest.go: -------------------------------------------------------------------------------- 1 | package dbtest 2 | 3 | import ( 4 | "runtime" 5 | "strconv" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/Cristofori/kmud/database" 10 | "github.com/Cristofori/kmud/testutils" 11 | "github.com/Cristofori/kmud/types" 12 | "gopkg.in/mgo.v2/bson" 13 | ) 14 | 15 | type TestSession struct { 16 | } 17 | 18 | func (ms TestSession) DB(dbName string) database.Database { 19 | return &TestDatabase{} 20 | } 21 | 22 | type TestDatabase struct { 23 | } 24 | 25 | func (md TestDatabase) C(collectionName string) database.Collection { 26 | return &TestCollection{} 27 | } 28 | 29 | type TestCollection struct { 30 | } 31 | 32 | func (mc TestCollection) Find(selector interface{}) database.Query { 33 | return &TestQuery{} 34 | } 35 | 36 | func (mc TestCollection) FindId(selector interface{}) database.Query { 37 | return &TestQuery{} 38 | } 39 | 40 | func (mc TestCollection) RemoveId(id interface{}) error { 41 | return nil 42 | } 43 | 44 | func (mc TestCollection) Remove(selector interface{}) error { 45 | return nil 46 | } 47 | 48 | func (mc TestCollection) DropCollection() error { 49 | return nil 50 | } 51 | 52 | func (mc TestCollection) UpdateId(id interface{}, change interface{}) error { 53 | return nil 54 | } 55 | 56 | func (mc TestCollection) UpsertId(id interface{}, change interface{}) error { 57 | return nil 58 | } 59 | 60 | type TestQuery struct { 61 | } 62 | 63 | func (mq TestQuery) Count() (int, error) { 64 | return 0, nil 65 | } 66 | 67 | func (mq TestQuery) One(result interface{}) error { 68 | return nil 69 | } 70 | 71 | func (mq TestQuery) Iter() database.Iterator { 72 | return &TestIterator{} 73 | } 74 | 75 | type TestIterator struct { 76 | } 77 | 78 | func (mi TestIterator) All(result interface{}) error { 79 | return nil 80 | } 81 | 82 | func Test_ThreadSafety(t *testing.T) { 83 | runtime.GOMAXPROCS(2) 84 | database.Init(&TestSession{}, "unit_dbtest") 85 | 86 | char := database.NewPc("test", testutils.MockId(""), testutils.MockId("")) 87 | 88 | var wg sync.WaitGroup 89 | wg.Add(2) 90 | 91 | go func() { 92 | for i := 0; i < 100; i++ { 93 | char.SetName(strconv.Itoa(i)) 94 | } 95 | wg.Done() 96 | }() 97 | 98 | go func() { 99 | for i := 0; i < 100; i++ { 100 | } 101 | wg.Done() 102 | }() 103 | 104 | wg.Wait() 105 | } 106 | 107 | func Test_User(t *testing.T) { 108 | user := database.NewUser("testuser", "", false) 109 | 110 | if user.IsOnline() { 111 | t.Errorf("Newly created user shouldn't be online") 112 | } 113 | 114 | user.SetOnline(true) 115 | 116 | testutils.Assert(user.IsOnline(), t, "Call to SetOnline(true) failed") 117 | testutils.Assert(user.GetColorMode() == types.ColorModeNone, t, "Newly created user should have a color mode of None") 118 | 119 | user.SetColorMode(types.ColorModeLight) 120 | 121 | testutils.Assert(user.GetColorMode() == types.ColorModeLight, t, "Call to SetColorMode(types.ColorModeLight) failed") 122 | 123 | user.SetColorMode(types.ColorModeDark) 124 | 125 | testutils.Assert(user.GetColorMode() == types.ColorModeDark, t, "Call to SetColorMode(types.ColorModeDark) failed") 126 | 127 | pw := "password" 128 | user.SetPassword(pw) 129 | 130 | testutils.Assert(user.VerifyPassword(pw), t, "User password verification failed") 131 | 132 | width := 11 133 | height := 12 134 | user.SetWindowSize(width, height) 135 | 136 | testWidth, testHeight := user.GetWindowSize() 137 | 138 | testutils.Assert(testWidth == width && testHeight == height, t, "Call to SetWindowSize() failed") 139 | 140 | terminalType := "fake terminal type" 141 | user.SetTerminalType(terminalType) 142 | 143 | testutils.Assert(terminalType == user.GetTerminalType(), t, "Call to SetTerminalType() failed") 144 | } 145 | 146 | func Test_PlayerCharacter(t *testing.T) { 147 | fakeId := bson.ObjectId("12345") 148 | character := database.NewPc("testcharacter", fakeId, fakeId) 149 | 150 | testutils.Assert(character.GetUserId() == fakeId, t, "Call to character.SetUser() failed", fakeId, character.GetUserId()) 151 | testutils.Assert(!character.IsOnline(), t, "Player-Characters should be offline by default") 152 | 153 | character.SetOnline(true) 154 | 155 | testutils.Assert(character.IsOnline(), t, "Call to character.SetOnline(true) failed") 156 | 157 | character.SetRoomId(fakeId) 158 | 159 | testutils.Assert(character.GetRoomId() == fakeId, t, "Call to character.SetRoom() failed", fakeId, character.GetRoomId()) 160 | 161 | cashAmount := 1234 162 | character.SetCash(cashAmount) 163 | 164 | testutils.Assert(character.GetCash() == cashAmount, t, "Call to character.GetCash() failed", cashAmount, character.GetCash()) 165 | 166 | character.AddCash(cashAmount) 167 | 168 | testutils.Assert(character.GetCash() == cashAmount*2, t, "Call to character.AddCash() failed", cashAmount*2, character.GetCash()) 169 | 170 | // conversation := "this is a fake conversation that is made up for the unit test" 171 | 172 | // character.SetConversation(conversation) 173 | 174 | // testutils.Assert(character.GetConversation() == conversation, t, "Call to character.SetConversation() failed") 175 | 176 | health := 123 177 | 178 | character.SetHealth(health) 179 | 180 | testutils.Assert(character.GetHealth() == health, t, "Call to character.SetHealth() failed") 181 | 182 | hitpoints := health - 10 183 | 184 | character.SetHitPoints(hitpoints) 185 | 186 | testutils.Assert(character.GetHitPoints() == hitpoints, t, "Call to character.SetHitPoints() failed") 187 | 188 | character.SetHitPoints(health + 10) 189 | 190 | testutils.Assert(character.GetHitPoints() == health, t, "Shouldn't be able to set a character's hitpoints to be greater than its maximum health", health, character.GetHitPoints()) 191 | 192 | character.SetHealth(character.GetHealth() - 10) 193 | 194 | testutils.Assert(character.GetHitPoints() == character.GetHealth(), t, "Lowering health didn't lower the hitpoint count along with it", character.GetHitPoints(), character.GetHealth()) 195 | 196 | character.SetHealth(100) 197 | character.SetHitPoints(100) 198 | 199 | hitAmount := 51 200 | character.Hit(hitAmount) 201 | 202 | testutils.Assert(character.GetHitPoints() == character.GetHealth()-hitAmount, t, "Call to character.Hit() failed", hitAmount, character.GetHitPoints()) 203 | 204 | character.Heal(hitAmount) 205 | 206 | testutils.Assert(character.GetHitPoints() == character.GetHealth(), t, "Call to character.Heal() failed", hitAmount, character.GetHitPoints()) 207 | } 208 | 209 | func Test_Zone(t *testing.T) { 210 | zoneName := "testzone" 211 | zone := database.NewZone(zoneName) 212 | 213 | testutils.Assert(zone.GetName() == zoneName, t, "Zone didn't have correct name upon creation", zoneName, zone.GetName()) 214 | } 215 | 216 | func Test_Room(t *testing.T) { 217 | fakeZoneId := bson.ObjectId("!2345") 218 | room := database.NewRoom(fakeZoneId, types.Coordinate{X: 0, Y: 0, Z: 0}) 219 | 220 | testutils.Assert(room.GetZoneId() == fakeZoneId, t, "Room didn't have correct zone ID upon creation", fakeZoneId, room.GetZoneId()) 221 | 222 | fakeZoneId2 := bson.ObjectId("11111") 223 | room.SetZoneId(fakeZoneId2) 224 | testutils.Assert(room.GetZoneId() == fakeZoneId2, t, "Call to room.SetZoneId() failed") 225 | 226 | directionList := make([]types.Direction, 10) 227 | directionCount := 10 228 | 229 | for i := 0; i < directionCount; i++ { 230 | directionList[i] = types.Direction(i) 231 | } 232 | 233 | for _, dir := range directionList { 234 | testutils.Assert(!room.HasExit(dir), t, "Room shouldn't have any exits enabled by default", dir) 235 | room.SetExitEnabled(dir, true) 236 | testutils.Assert(room.HasExit(dir), t, "Call to room.SetExitEnabled(true) failed") 237 | room.SetExitEnabled(dir, false) 238 | testutils.Assert(!room.HasExit(dir), t, "Call to room.SetExitEnabled(false) failed") 239 | } 240 | 241 | title := "Test Title" 242 | room.SetTitle(title) 243 | testutils.Assert(title == room.GetTitle(), t, "Call to room.SetTitle() failed", title, room.GetTitle()) 244 | 245 | description := "This is a fake description" 246 | room.SetDescription(description) 247 | testutils.Assert(description == room.GetDescription(), t, "Call to room.SetDescription() failed", description, room.GetDescription()) 248 | 249 | coord := types.Coordinate{X: 1, Y: 2, Z: 3} 250 | room.SetLocation(coord) 251 | testutils.Assert(coord == room.GetLocation(), t, "Call to room.SetLocation() failed", coord, room.GetLocation()) 252 | } 253 | 254 | func Test_Item(t *testing.T) { 255 | name := "test_item" 256 | template := database.NewTemplate(name) 257 | item := database.NewItem(template.GetId()) 258 | 259 | testutils.Assert(item.GetName() == name, t, "Item didn't get created with correct name", name, item.GetName()) 260 | } 261 | -------------------------------------------------------------------------------- /events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/Cristofori/kmud/database" 8 | "github.com/Cristofori/kmud/types" 9 | "github.com/Cristofori/kmud/utils" 10 | ) 11 | 12 | type EventReceiver interface { 13 | types.Identifiable 14 | types.Locateable 15 | } 16 | 17 | type SimpleReceiver struct { 18 | } 19 | 20 | func (*SimpleReceiver) GetId() types.Id { 21 | return nil 22 | } 23 | 24 | func (*SimpleReceiver) GetRoomId() types.Id { 25 | return nil 26 | } 27 | 28 | type eventListener struct { 29 | Channel chan Event 30 | Receiver EventReceiver 31 | } 32 | 33 | var _listeners map[EventReceiver]chan Event 34 | 35 | var eventMessages chan interface{} 36 | 37 | type register eventListener 38 | 39 | type unregister struct { 40 | Receiver EventReceiver 41 | } 42 | 43 | type broadcast struct { 44 | Event Event 45 | } 46 | 47 | func Register(receiver EventReceiver) chan Event { 48 | listener := eventListener{Receiver: receiver, Channel: make(chan Event)} 49 | eventMessages <- register(listener) 50 | return listener.Channel 51 | } 52 | 53 | func Unregister(char EventReceiver) { 54 | eventMessages <- unregister{char} 55 | } 56 | 57 | func Broadcast(event Event) { 58 | eventMessages <- broadcast{event} 59 | } 60 | 61 | func init() { 62 | _listeners = map[EventReceiver]chan Event{} 63 | eventMessages = make(chan interface{}, 1) 64 | 65 | go func() { 66 | for message := range eventMessages { 67 | switch msg := message.(type) { 68 | case register: 69 | _listeners[msg.Receiver] = msg.Channel 70 | case unregister: 71 | delete(_listeners, msg.Receiver) 72 | case broadcast: 73 | for char, channel := range _listeners { 74 | if msg.Event.IsFor(char) { 75 | go func(c chan Event) { 76 | c <- msg.Event 77 | }(channel) 78 | } 79 | } 80 | default: 81 | panic("Unhandled event message") 82 | } 83 | } 84 | 85 | _listeners = nil 86 | }() 87 | 88 | go func() { 89 | throttler := utils.NewThrottler(1 * time.Second) 90 | 91 | for { 92 | throttler.Sync() 93 | Broadcast(TickEvent{}) 94 | } 95 | }() 96 | } 97 | 98 | type Event interface { 99 | ToString(receiver EventReceiver) string 100 | IsFor(receiver EventReceiver) bool 101 | } 102 | 103 | type TickEvent struct{} 104 | 105 | type CreateEvent struct { 106 | Object *database.DbObject 107 | } 108 | 109 | type DestroyEvent struct { 110 | Object *database.DbObject 111 | } 112 | 113 | type DeathEvent struct { 114 | Character types.Character 115 | } 116 | 117 | type BroadcastEvent struct { 118 | Character types.Character 119 | Message string 120 | } 121 | 122 | type SayEvent struct { 123 | Character types.Character 124 | Message string 125 | } 126 | 127 | type EmoteEvent struct { 128 | Character types.Character 129 | Emote string 130 | } 131 | 132 | type TellEvent struct { 133 | From types.Character 134 | To types.Character 135 | Message string 136 | } 137 | 138 | type EnterEvent struct { 139 | Character types.Character 140 | RoomId types.Id 141 | Direction types.Direction 142 | } 143 | 144 | type LeaveEvent struct { 145 | Character types.Character 146 | RoomId types.Id 147 | Direction types.Direction 148 | } 149 | 150 | type RoomUpdateEvent struct { 151 | Room *database.Room 152 | } 153 | 154 | type LoginEvent struct { 155 | Character types.Character 156 | } 157 | 158 | type LogoutEvent struct { 159 | Character types.Character 160 | } 161 | 162 | type CombatStartEvent struct { 163 | Attacker types.Character 164 | Defender types.Character 165 | } 166 | 167 | type CombatStopEvent struct { 168 | Attacker types.Character 169 | Defender types.Character 170 | } 171 | 172 | type CombatEvent struct { 173 | Attacker types.Character 174 | Defender types.Character 175 | Skill types.Skill 176 | Power int 177 | } 178 | 179 | type LockEvent struct { 180 | RoomId types.Id 181 | Exit types.Direction 182 | Locked bool 183 | } 184 | 185 | func (self BroadcastEvent) ToString(receiver EventReceiver) string { 186 | return types.Colorize(types.ColorCyan, "Broadcast from "+self.Character.GetName()+": ") + 187 | types.Colorize(types.ColorWhite, self.Message) 188 | } 189 | 190 | func (self BroadcastEvent) IsFor(receiver EventReceiver) bool { 191 | return true 192 | } 193 | 194 | // Say 195 | func (self SayEvent) ToString(receiver EventReceiver) string { 196 | who := "" 197 | if receiver == self.Character { 198 | who = "You say" 199 | } else { 200 | who = self.Character.GetName() + " says" 201 | } 202 | 203 | return types.Colorize(types.ColorBlue, who+", ") + 204 | types.Colorize(types.ColorWhite, "\""+self.Message+"\"") 205 | } 206 | 207 | func (self SayEvent) IsFor(receiver EventReceiver) bool { 208 | return receiver.GetRoomId() == self.Character.GetRoomId() 209 | } 210 | 211 | // Emote 212 | func (self EmoteEvent) ToString(receiver EventReceiver) string { 213 | return types.Colorize(types.ColorYellow, self.Character.GetName()+" "+self.Emote) 214 | } 215 | 216 | func (self EmoteEvent) IsFor(receiver EventReceiver) bool { 217 | return receiver.GetRoomId() == self.Character.GetRoomId() 218 | } 219 | 220 | // Tell 221 | func (self TellEvent) ToString(receiver EventReceiver) string { 222 | if receiver == self.To { 223 | return types.Colorize(types.ColorMagenta, 224 | fmt.Sprintf("Message from %s: %s", self.From.GetName(), types.Colorize(types.ColorWhite, self.Message))) 225 | } else { 226 | return types.Colorize(types.ColorMagenta, 227 | fmt.Sprintf("Message to %s: %s", self.To.GetName(), types.Colorize(types.ColorWhite, self.Message))) 228 | } 229 | } 230 | 231 | func (self TellEvent) IsFor(receiver EventReceiver) bool { 232 | return receiver == self.To || receiver == self.From 233 | } 234 | 235 | // Enter 236 | func (self EnterEvent) ToString(receiver EventReceiver) string { 237 | message := fmt.Sprintf("%v%s %vhas entered the room", types.ColorBlue, self.Character.GetName(), types.ColorWhite) 238 | if self.Direction != types.DirectionNone { 239 | message = fmt.Sprintf("%s from the %s", message, self.Direction.ToString()) 240 | } 241 | return message 242 | } 243 | 244 | func (self EnterEvent) IsFor(receiver EventReceiver) bool { 245 | return self.RoomId == receiver.GetRoomId() && receiver != self.Character 246 | } 247 | 248 | // Leave 249 | func (self LeaveEvent) ToString(receiver EventReceiver) string { 250 | message := fmt.Sprintf("%v%s %vhas left the room", types.ColorBlue, self.Character.GetName(), types.ColorWhite) 251 | if self.Direction != types.DirectionNone { 252 | message = fmt.Sprintf("%s to the %s", message, self.Direction.ToString()) 253 | } 254 | return message 255 | } 256 | 257 | func (self LeaveEvent) IsFor(receiver EventReceiver) bool { 258 | return self.RoomId == receiver.GetRoomId() 259 | } 260 | 261 | // RoomUpdate 262 | func (self RoomUpdateEvent) ToString(receiver EventReceiver) string { 263 | return types.Colorize(types.ColorWhite, "This room has been modified") 264 | } 265 | 266 | func (self RoomUpdateEvent) IsFor(receiver EventReceiver) bool { 267 | return receiver.GetRoomId() == self.Room.GetId() 268 | } 269 | 270 | // Login 271 | func (self LoginEvent) ToString(receiver EventReceiver) string { 272 | return types.Colorize(types.ColorBlue, self.Character.GetName()) + 273 | types.Colorize(types.ColorWhite, " has connected") 274 | } 275 | 276 | func (self LoginEvent) IsFor(receiver EventReceiver) bool { 277 | return receiver != self.Character 278 | } 279 | 280 | // Logout 281 | func (self LogoutEvent) ToString(receiver EventReceiver) string { 282 | return fmt.Sprintf("%s has disconnected", self.Character.GetName()) 283 | } 284 | 285 | func (self LogoutEvent) IsFor(receiver EventReceiver) bool { 286 | return true 287 | } 288 | 289 | // CombatStart 290 | func (self CombatStartEvent) ToString(receiver EventReceiver) string { 291 | if receiver == self.Attacker { 292 | return types.Colorize(types.ColorRed, fmt.Sprintf("You are attacking %s!", self.Defender.GetName())) 293 | } else if receiver == self.Defender { 294 | return types.Colorize(types.ColorRed, fmt.Sprintf("%s is attacking you!", self.Attacker.GetName())) 295 | } 296 | 297 | return "" 298 | } 299 | 300 | func (self CombatStartEvent) IsFor(receiver EventReceiver) bool { 301 | return receiver == self.Attacker || receiver == self.Defender 302 | } 303 | 304 | // CombatStop 305 | func (self CombatStopEvent) ToString(receiver EventReceiver) string { 306 | if receiver == self.Attacker { 307 | return types.Colorize(types.ColorGreen, fmt.Sprintf("You stopped attacking %s", self.Defender.GetName())) 308 | } else if receiver == self.Defender { 309 | return types.Colorize(types.ColorGreen, fmt.Sprintf("%s has stopped attacking you", self.Attacker.GetName())) 310 | } 311 | 312 | return "" 313 | } 314 | 315 | func (self CombatStopEvent) IsFor(receiver EventReceiver) bool { 316 | return receiver == self.Attacker || receiver == self.Defender 317 | } 318 | 319 | // Combat 320 | func (self CombatEvent) ToString(receiver EventReceiver) string { 321 | skillMsg := "" 322 | if self.Skill != nil { 323 | skillMsg = fmt.Sprintf(" with %s", self.Skill.GetName()) 324 | } 325 | 326 | if receiver == self.Attacker { 327 | return types.Colorize(types.ColorRed, fmt.Sprintf("You hit %s%s for %v damage", self.Defender.GetName(), skillMsg, self.Power)) 328 | } else if receiver == self.Defender { 329 | return types.Colorize(types.ColorRed, fmt.Sprintf("%s hits you%s for %v damage", self.Attacker.GetName(), skillMsg, self.Power)) 330 | } 331 | 332 | return "" 333 | } 334 | 335 | func (self CombatEvent) IsFor(receiver EventReceiver) bool { 336 | return receiver == self.Attacker || receiver == self.Defender 337 | } 338 | 339 | // Timer 340 | func (self TickEvent) ToString(receiver EventReceiver) string { 341 | return "" 342 | } 343 | 344 | func (self TickEvent) IsFor(receiver EventReceiver) bool { 345 | return true 346 | } 347 | 348 | // Create 349 | func (self CreateEvent) ToString(receiver EventReceiver) string { 350 | return "" 351 | } 352 | 353 | func (self CreateEvent) IsFor(receiver EventReceiver) bool { 354 | return true 355 | } 356 | 357 | // Destroy 358 | func (self DestroyEvent) ToString(receiver EventReceiver) string { 359 | return "" 360 | } 361 | 362 | func (self DestroyEvent) IsFor(receiver EventReceiver) bool { 363 | return true 364 | } 365 | 366 | // Death 367 | func (self DeathEvent) IsFor(receiver EventReceiver) bool { 368 | return receiver == self.Character || 369 | receiver.GetRoomId() == self.Character.GetRoomId() 370 | } 371 | 372 | func (self DeathEvent) ToString(receiver EventReceiver) string { 373 | if receiver == self.Character { 374 | return types.Colorize(types.ColorRed, ">> You have died") 375 | } 376 | 377 | return types.Colorize(types.ColorRed, fmt.Sprintf(">> %s has died", self.Character.GetName())) 378 | } 379 | 380 | // Lock 381 | func (self LockEvent) IsFor(receiver EventReceiver) bool { 382 | return receiver.GetRoomId() == self.RoomId 383 | } 384 | 385 | func (self LockEvent) ToString(receiver EventReceiver) string { 386 | status := "unlocked" 387 | if self.Locked { 388 | status = "locked" 389 | } 390 | 391 | return types.Colorize(types.ColorBlue, 392 | fmt.Sprintf("The exit to the %s has been %s", self.Exit.ToString(), 393 | types.Colorize(types.ColorWhite, status))) 394 | } 395 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "runtime/debug" 8 | "sort" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/Cristofori/kmud/database" 13 | "github.com/Cristofori/kmud/engine" 14 | "github.com/Cristofori/kmud/model" 15 | "github.com/Cristofori/kmud/session" 16 | "github.com/Cristofori/kmud/telnet" 17 | "github.com/Cristofori/kmud/types" 18 | "github.com/Cristofori/kmud/utils" 19 | "gopkg.in/mgo.v2" 20 | ) 21 | 22 | type Server struct { 23 | listener net.Listener 24 | } 25 | 26 | type connectionHandler struct { 27 | user types.User 28 | pc types.PC 29 | conn *wrappedConnection 30 | } 31 | 32 | type wrappedConnection struct { 33 | telnet.Telnet 34 | watcher *utils.WatchableReadWriter 35 | } 36 | 37 | func (s *wrappedConnection) Write(p []byte) (int, error) { 38 | return s.watcher.Write(p) 39 | } 40 | 41 | func (s *wrappedConnection) Read(p []byte) (int, error) { 42 | return s.watcher.Read(p) 43 | } 44 | 45 | func login(conn *wrappedConnection) types.User { 46 | for { 47 | username := utils.GetUserInput(conn, "Username: ", types.ColorModeNone) 48 | 49 | if username == "" { 50 | return nil 51 | } 52 | 53 | user := model.GetUserByName(username) 54 | 55 | if user == nil { 56 | utils.WriteLine(conn, "User not found", types.ColorModeNone) 57 | } else if user.IsOnline() { 58 | utils.WriteLine(conn, "That user is already online", types.ColorModeNone) 59 | } else { 60 | attempts := 1 61 | conn.WillEcho() 62 | for { 63 | password := utils.GetRawUserInputSuffix(conn, "Password: ", "\r\n", types.ColorModeNone) 64 | 65 | // TODO - Disabling password verification to make development easier 66 | if user.VerifyPassword(password) || true { 67 | break 68 | } 69 | 70 | if attempts >= 3 { 71 | utils.WriteLine(conn, "Too many failed login attempts", types.ColorModeNone) 72 | conn.Close() 73 | panic("Booted user due to too many failed logins (" + user.GetName() + ")") 74 | } 75 | 76 | attempts++ 77 | 78 | time.Sleep(2 * time.Second) 79 | utils.WriteLine(conn, "Invalid password", types.ColorModeNone) 80 | } 81 | conn.WontEcho() 82 | 83 | return user 84 | } 85 | } 86 | } 87 | 88 | func newUser(conn *wrappedConnection) types.User { 89 | for { 90 | name := utils.GetUserInput(conn, "Desired username: ", types.ColorModeNone) 91 | 92 | if name == "" { 93 | return nil 94 | } 95 | 96 | user := model.GetUserByName(name) 97 | password := "" 98 | 99 | if user != nil { 100 | utils.WriteLine(conn, "That name is unavailable", types.ColorModeNone) 101 | } else if err := utils.ValidateName(name); err != nil { 102 | utils.WriteLine(conn, err.Error(), types.ColorModeNone) 103 | } else { 104 | conn.WillEcho() 105 | for { 106 | pass1 := utils.GetRawUserInputSuffix(conn, "Desired password: ", "\r\n", types.ColorModeNone) 107 | 108 | if len(pass1) < 7 { 109 | utils.WriteLine(conn, "Passwords must be at least 7 letters in length", types.ColorModeNone) 110 | continue 111 | } 112 | 113 | pass2 := utils.GetRawUserInputSuffix(conn, "Confirm password: ", "\r\n", types.ColorModeNone) 114 | 115 | if pass1 != pass2 { 116 | utils.WriteLine(conn, "Passwords do not match", types.ColorModeNone) 117 | continue 118 | } 119 | 120 | password = pass1 121 | 122 | break 123 | } 124 | conn.WontEcho() 125 | 126 | admin := model.UserCount() == 0 127 | user = model.CreateUser(name, password, admin) 128 | return user 129 | } 130 | } 131 | } 132 | 133 | func (self *connectionHandler) newPlayer() types.PC { 134 | // TODO: character slot limit 135 | const SizeLimit = 12 136 | for { 137 | name := self.user.GetInput("Desired character name: ") 138 | 139 | if name == "" { 140 | return nil 141 | } 142 | 143 | char := model.GetCharacterByName(name) 144 | 145 | if char != nil { 146 | self.user.WriteLine("That name is unavailable") 147 | } else if err := utils.ValidateName(name); err != nil { 148 | self.user.WriteLine(err.Error()) 149 | } else { 150 | room := model.GetRooms()[0] // TODO: Better way to pick an initial character location 151 | return model.CreatePlayerCharacter(name, self.user.GetId(), room) 152 | } 153 | } 154 | } 155 | 156 | func (self *connectionHandler) WriteLine(line string, a ...interface{}) { 157 | utils.WriteLine(self.conn, fmt.Sprintf(line, a...), types.ColorModeNone) 158 | } 159 | 160 | func (self *connectionHandler) Write(text string) { 161 | utils.Write(self.conn, text, types.ColorModeNone) 162 | } 163 | 164 | func (self *connectionHandler) GetInput(prompt string) string { 165 | return utils.GetUserInput(self.conn, prompt, types.ColorModeNone) 166 | } 167 | 168 | func (sefl *connectionHandler) GetWindowSize() (int, int) { 169 | return 80, 80 170 | } 171 | 172 | func (self *connectionHandler) mainMenu() { 173 | utils.ExecMenu( 174 | "MUD", 175 | self, 176 | func(menu *utils.Menu) { 177 | menu.AddAction("l", "Login", func() { 178 | self.user = login(self.conn) 179 | self.loggedIn() 180 | }) 181 | 182 | menu.AddAction("n", "New user", func() { 183 | self.user = newUser(self.conn) 184 | self.loggedIn() 185 | }) 186 | 187 | menu.OnExit(func() { 188 | utils.WriteLine(self.conn, "Take luck!", types.ColorModeNone) 189 | self.conn.Close() 190 | }) 191 | }) 192 | } 193 | 194 | func (self *connectionHandler) userMenu() { 195 | utils.ExecMenu( 196 | self.user.GetName(), 197 | self.user, 198 | func(menu *utils.Menu) { 199 | menu.OnExit(func() { 200 | self.user.SetOnline(false) 201 | self.user = nil 202 | }) 203 | 204 | if self.user.IsAdmin() { 205 | menu.AddAction("a", "Admin", func() { 206 | self.adminMenu() 207 | }) 208 | } 209 | 210 | menu.AddAction("n", "New character", func() { 211 | self.pc = self.newPlayer() 212 | }) 213 | 214 | // TODO: Sort character list 215 | chars := model.GetUserCharacters(self.user.GetId()) 216 | 217 | if len(chars) > 0 { 218 | menu.AddAction("d", "Delete character", func() { 219 | self.deleteMenu() 220 | }) 221 | } 222 | 223 | for i, char := range chars { 224 | c := char 225 | menu.AddAction(strconv.Itoa(i+1), char.GetName(), func() { 226 | self.pc = c 227 | self.launchSession() 228 | }) 229 | } 230 | }) 231 | } 232 | 233 | func (self *connectionHandler) deleteMenu() { 234 | utils.ExecMenu( 235 | "Delete character", 236 | self.user, 237 | func(menu *utils.Menu) { 238 | // TODO: Sort character list 239 | chars := model.GetUserCharacters(self.user.GetId()) 240 | for i, char := range chars { 241 | c := char 242 | menu.AddAction(strconv.Itoa(i+1), char.GetName(), func() { 243 | // TODO: Delete confirmation 244 | model.DeleteCharacter(c.GetId()) 245 | }) 246 | } 247 | }) 248 | } 249 | 250 | func (self *connectionHandler) adminMenu() { 251 | utils.ExecMenu( 252 | "Admin", 253 | self.user, 254 | func(menu *utils.Menu) { 255 | menu.AddAction("u", "Users", func() { 256 | self.userAdminMenu() 257 | }) 258 | }) 259 | } 260 | 261 | func (self *connectionHandler) userAdminMenu() { 262 | utils.ExecMenu("User Admin", self.user, func(menu *utils.Menu) { 263 | users := model.GetUsers() 264 | sort.Sort(users) 265 | 266 | for i, user := range users { 267 | online := "" 268 | if user.IsOnline() { 269 | online = "*" 270 | } 271 | 272 | u := user 273 | menu.AddAction(strconv.Itoa(i+1), user.GetName()+online, func() { 274 | self.specificUserMenu(u) 275 | }) 276 | } 277 | }) 278 | } 279 | 280 | func (self *connectionHandler) specificUserMenu(user types.User) { 281 | suffix := "" 282 | if user.IsOnline() { 283 | suffix = "(Online)" 284 | } else { 285 | suffix = "(Offline)" 286 | } 287 | 288 | utils.ExecMenu( 289 | fmt.Sprintf("User: %s %s", user.GetName(), suffix), 290 | self.user, 291 | func(menu *utils.Menu) { 292 | menu.AddAction("d", "Delete", func() { 293 | model.DeleteUser(user.GetId()) 294 | menu.Exit() 295 | }) 296 | 297 | menu.AddAction("a", fmt.Sprintf("Admin - %v", user.IsAdmin()), func() { 298 | u := model.GetUser(user.GetId()) 299 | u.SetAdmin(!u.IsAdmin()) 300 | }) 301 | 302 | if user.IsOnline() { 303 | menu.AddAction("w", "Watch", func() { 304 | if user == self.user { 305 | self.user.WriteLine("You can't watch yourself!") 306 | } else { 307 | userConn := user.GetConnection().(*wrappedConnection) 308 | 309 | userConn.watcher.AddWatcher(self.conn) 310 | utils.GetRawUserInput(self.conn, "Type anything to stop watching\r\n", self.user.GetColorMode()) 311 | userConn.watcher.RemoveWatcher(self.conn) 312 | } 313 | }) 314 | } 315 | }) 316 | } 317 | 318 | func (self *connectionHandler) Handle() { 319 | go func() { 320 | defer self.conn.Close() 321 | 322 | defer func() { 323 | r := recover() 324 | 325 | username := "" 326 | charname := "" 327 | 328 | if self.user != nil { 329 | self.user.SetOnline(false) 330 | username = self.user.GetName() 331 | } 332 | 333 | if self.pc != nil { 334 | self.pc.SetOnline(false) 335 | charname = self.pc.GetName() 336 | } 337 | 338 | if r != io.EOF { 339 | debug.PrintStack() 340 | } 341 | 342 | fmt.Printf("Lost connection to client (%v/%v): %v, %v\n", 343 | username, 344 | charname, 345 | self.conn.RemoteAddr(), 346 | r) 347 | }() 348 | 349 | self.mainMenu() 350 | }() 351 | } 352 | 353 | func (self *connectionHandler) loggedIn() { 354 | if self.user == nil { 355 | return 356 | } 357 | 358 | self.user.SetOnline(true) 359 | self.user.SetConnection(self.conn) 360 | 361 | self.conn.DoWindowSize() 362 | self.conn.DoTerminalType() 363 | 364 | self.conn.Listen(func(code telnet.TelnetCode, data []byte) { 365 | switch code { 366 | case telnet.WS: 367 | if len(data) != 4 { 368 | fmt.Println("Malformed window size data:", data) 369 | return 370 | } 371 | 372 | width := int((255 * data[0])) + int(data[1]) 373 | height := int((255 * data[2])) + int(data[3]) 374 | self.user.SetWindowSize(width, height) 375 | 376 | case telnet.TT: 377 | self.user.SetTerminalType(string(data)) 378 | } 379 | }) 380 | 381 | self.userMenu() 382 | } 383 | 384 | func (self *connectionHandler) launchSession() { 385 | if self.pc == nil { 386 | return 387 | } 388 | 389 | session := session.NewSession(self.conn, self.user, self.pc) 390 | session.Exec() 391 | self.pc = nil 392 | } 393 | 394 | func (self *Server) Start() { 395 | fmt.Printf("Connecting to database... ") 396 | session, err := mgo.Dial("localhost") 397 | 398 | utils.HandleError(err) 399 | 400 | fmt.Println("done.") 401 | 402 | self.listener, err = net.Listen("tcp", ":8945") 403 | utils.HandleError(err) 404 | 405 | database.Init(database.NewMongoSession(session.Copy()), "mud") 406 | } 407 | 408 | func (self *Server) Bootstrap() { 409 | // Create the world object if necessary 410 | model.GetWorld() 411 | 412 | // If there are no rooms at all create one 413 | rooms := model.GetRooms() 414 | if len(rooms) == 0 { 415 | zones := model.GetZones() 416 | 417 | var zone types.Zone 418 | 419 | if len(zones) == 0 { 420 | zone, _ = model.CreateZone("Default") 421 | } else { 422 | zone = zones[0] 423 | } 424 | 425 | model.CreateRoom(zone, types.Coordinate{X: 0, Y: 0, Z: 0}) 426 | } 427 | } 428 | 429 | func (self *Server) Listen() { 430 | for { 431 | conn, err := self.listener.Accept() 432 | utils.HandleError(err) 433 | fmt.Println("Client connected:", conn.RemoteAddr()) 434 | t := telnet.NewTelnet(conn) 435 | 436 | wc := utils.NewWatchableReadWriter(t) 437 | 438 | ch := connectionHandler{ 439 | conn: &wrappedConnection{Telnet: *t, watcher: wc}, 440 | } 441 | 442 | ch.Handle() 443 | } 444 | } 445 | 446 | func (self *Server) Exec() { 447 | self.Start() 448 | self.Bootstrap() 449 | engine.Start() 450 | self.Listen() 451 | } 452 | -------------------------------------------------------------------------------- /session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | "strconv" 8 | 9 | "github.com/Cristofori/kmud/combat" 10 | "github.com/Cristofori/kmud/events" 11 | "github.com/Cristofori/kmud/model" 12 | "github.com/Cristofori/kmud/types" 13 | "github.com/Cristofori/kmud/utils" 14 | // "log" 15 | // "os" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | type Session struct { 21 | conn io.ReadWriter 22 | user types.User 23 | pc types.PC 24 | 25 | prompt string 26 | states map[string]string 27 | 28 | userInputChannel chan string 29 | inputModeChannel chan userInputMode 30 | prompterChannel chan utils.Prompter 31 | panicChannel chan interface{} 32 | eventChannel chan events.Event 33 | 34 | silentMode bool 35 | replyId types.Id 36 | lastInput string 37 | 38 | // logger *log.Logger 39 | } 40 | 41 | func NewSession(conn io.ReadWriter, user types.User, pc types.PC) *Session { 42 | var session Session 43 | session.conn = conn 44 | session.user = user 45 | session.pc = pc 46 | 47 | session.prompt = "%h/%H> " 48 | session.states = map[string]string{} 49 | 50 | session.userInputChannel = make(chan string) 51 | session.inputModeChannel = make(chan userInputMode) 52 | session.prompterChannel = make(chan utils.Prompter) 53 | session.panicChannel = make(chan interface{}) 54 | session.eventChannel = events.Register(pc) 55 | 56 | session.silentMode = false 57 | 58 | // file, err := os.OpenFile(pc.GetName()+".log", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) 59 | // utils.PanicIfError(err) 60 | 61 | // session.logger = log.New(file, pc.GetName()+" ", log.LstdFlags) 62 | 63 | model.Login(pc) 64 | 65 | return &session 66 | } 67 | 68 | type userInputMode int 69 | 70 | const ( 71 | CleanUserInput userInputMode = iota 72 | RawUserInput userInputMode = iota 73 | ) 74 | 75 | func (self *Session) Exec() { 76 | defer events.Unregister(self.pc) 77 | defer model.Logout(self.pc) 78 | 79 | self.WriteLine("Welcome, " + self.pc.GetName()) 80 | self.PrintRoom() 81 | 82 | // Main routine in charge of actually reading input from the connection object, 83 | // also has built in throttling to limit how fast we are allowed to process 84 | // commands from the user. 85 | go func() { 86 | defer func() { 87 | self.panicChannel <- recover() 88 | }() 89 | 90 | throttler := utils.NewThrottler(200 * time.Millisecond) 91 | 92 | for { 93 | mode := <-self.inputModeChannel 94 | prompter := <-self.prompterChannel 95 | input := "" 96 | 97 | switch mode { 98 | case CleanUserInput: 99 | input = utils.GetUserInputP(self.conn, prompter, self.user.GetColorMode()) 100 | case RawUserInput: 101 | input = utils.GetRawUserInputP(self.conn, prompter, self.user.GetColorMode()) 102 | default: 103 | panic("Unhandled case in switch statement (userInputMode)") 104 | } 105 | 106 | throttler.Sync() 107 | self.userInputChannel <- input 108 | } 109 | }() 110 | 111 | // Main loop 112 | for { 113 | input := self.getUserInputP(RawUserInput, self) 114 | if input == "" || input == "logout" || input == "quit" { 115 | return 116 | } 117 | 118 | if input == "." { 119 | self.WriteLine(self.lastInput) 120 | input = self.lastInput 121 | } 122 | 123 | self.lastInput = input 124 | 125 | if strings.HasPrefix(input, "/") { 126 | self.handleCommand(utils.Argify(input[1:])) 127 | } else { 128 | self.handleAction(utils.Argify(input)) 129 | } 130 | } 131 | } 132 | 133 | func (self *Session) WriteLinef(line string, a ...interface{}) { 134 | self.WriteLineColor(types.ColorWhite, line, a...) 135 | } 136 | 137 | func (self *Session) WriteLine(line string, a ...interface{}) { 138 | self.WriteLineColor(types.ColorWhite, line, a...) 139 | } 140 | 141 | func (self *Session) WriteLineColor(color types.Color, line string, a ...interface{}) { 142 | self.printLine(types.Colorize(color, fmt.Sprintf(line, a...))) 143 | } 144 | 145 | func (self *Session) printLine(line string, a ...interface{}) { 146 | self.Write(fmt.Sprintf(line+"\r\n", a...)) 147 | } 148 | 149 | func (self *Session) Write(text string) { 150 | self.user.Write(text) 151 | } 152 | 153 | func (self *Session) printError(err string, a ...interface{}) { 154 | self.WriteLineColor(types.ColorRed, err, a...) 155 | } 156 | 157 | func (self *Session) clearLine() { 158 | utils.ClearLine(self.conn) 159 | } 160 | 161 | func (self *Session) asyncMessage(message string) { 162 | self.clearLine() 163 | self.WriteLine(message) 164 | } 165 | 166 | func (self *Session) GetInput(prompt string) string { 167 | return self.getUserInput(CleanUserInput, prompt) 168 | } 169 | 170 | func (self *Session) GetWindowSize() (int, int) { 171 | return self.user.GetWindowSize() 172 | } 173 | 174 | // getUserInput allows us to retrieve user input in a way that doesn't block the 175 | // event loop by using channels and a separate Go routine to grab 176 | // either the next user input or the next event. 177 | func (self *Session) getUserInputP(inputMode userInputMode, prompter utils.Prompter) string { 178 | self.inputModeChannel <- inputMode 179 | self.prompterChannel <- prompter 180 | 181 | for { 182 | select { 183 | case input := <-self.userInputChannel: 184 | return input 185 | case event := <-self.eventChannel: 186 | if self.silentMode { 187 | continue 188 | } 189 | 190 | switch e := event.(type) { 191 | case events.TellEvent: 192 | self.replyId = e.From.GetId() 193 | case events.TickEvent: 194 | if !combat.InCombat(self.pc) { 195 | oldHps := self.pc.GetHitPoints() 196 | self.pc.Heal(5) 197 | newHps := self.pc.GetHitPoints() 198 | 199 | if oldHps != newHps { 200 | self.clearLine() 201 | self.Write(prompter.GetPrompt()) 202 | } 203 | } 204 | } 205 | 206 | message := event.ToString(self.pc) 207 | if message != "" { 208 | self.asyncMessage(message) 209 | self.Write(prompter.GetPrompt()) 210 | } 211 | 212 | case quitMessage := <-self.panicChannel: 213 | panic(quitMessage) 214 | } 215 | } 216 | } 217 | 218 | func (self *Session) getUserInput(inputMode userInputMode, prompt string) string { 219 | return self.getUserInputP(inputMode, utils.SimplePrompter(prompt)) 220 | } 221 | 222 | func (self *Session) getCleanUserInput(prompt string) string { 223 | return self.getUserInput(CleanUserInput, prompt) 224 | } 225 | 226 | func (self *Session) getRawUserInput(prompt string) string { 227 | return self.getUserInput(RawUserInput, prompt) 228 | } 229 | 230 | func (self *Session) getConfirmation(prompt string) bool { 231 | answer := self.getCleanUserInput(types.Colorize(types.ColorWhite, prompt)) 232 | return answer == "y" || answer == "yes" 233 | } 234 | 235 | func (self *Session) getInt(prompt string, min, max int) (int, bool) { 236 | for { 237 | input := self.getRawUserInput(prompt) 238 | if input == "" { 239 | return 0, false 240 | } 241 | 242 | val, err := utils.Atoir(input, min, max) 243 | 244 | if err != nil { 245 | self.printError(err.Error()) 246 | } else { 247 | return val, true 248 | } 249 | } 250 | } 251 | 252 | func (self *Session) GetPrompt() string { 253 | prompt := self.prompt 254 | prompt = strings.Replace(prompt, "%h", strconv.Itoa(self.pc.GetHitPoints()), -1) 255 | prompt = strings.Replace(prompt, "%H", strconv.Itoa(self.pc.GetHealth()), -1) 256 | 257 | if len(self.states) > 0 { 258 | states := make([]string, len(self.states)) 259 | 260 | i := 0 261 | for key, value := range self.states { 262 | states[i] = fmt.Sprintf("%s:%s", key, value) 263 | i++ 264 | } 265 | 266 | prompt = fmt.Sprintf("%s %s", states, prompt) 267 | } 268 | 269 | return types.Colorize(types.ColorWhite, prompt) 270 | } 271 | 272 | func (self *Session) currentZone() types.Zone { 273 | return model.GetZone(self.GetRoom().GetZoneId()) 274 | } 275 | 276 | func (self *Session) handleAction(action string, arg string) { 277 | if arg == "" { 278 | direction := types.StringToDirection(action) 279 | 280 | if direction != types.DirectionNone { 281 | if self.GetRoom().HasExit(direction) { 282 | err := model.MoveCharacter(self.pc, direction) 283 | if err == nil { 284 | self.PrintRoom() 285 | } else { 286 | self.printError(err.Error()) 287 | } 288 | 289 | } else { 290 | self.printError("You can't go that way") 291 | } 292 | 293 | return 294 | } 295 | } 296 | 297 | handler, found := actions[action] 298 | 299 | if found { 300 | if handler.alias != "" { 301 | handler = actions[handler.alias] 302 | } 303 | handler.exec(self, arg) 304 | } else { 305 | self.printError("You can't do that") 306 | } 307 | } 308 | 309 | func (self *Session) handleCommand(name string, arg string) { 310 | if len(name) == 0 { 311 | return 312 | } 313 | 314 | if name[0] == '/' && self.user.IsAdmin() { 315 | quickRoom(self, name[1:]) 316 | return 317 | } 318 | 319 | command, found := commands[name] 320 | 321 | if found { 322 | if command.alias != "" { 323 | command = commands[command.alias] 324 | } 325 | 326 | if command.admin && !self.user.IsAdmin() { 327 | self.printError("You don't have permission to do that") 328 | } else { 329 | command.exec(command, self, arg) 330 | } 331 | } else { 332 | self.printError("Unrecognized command: %s", name) 333 | } 334 | } 335 | 336 | func (self *Session) GetRoom() types.Room { 337 | return model.GetRoom(self.pc.GetRoomId()) 338 | } 339 | 340 | func (self *Session) PrintRoom() { 341 | self.printRoom(self.GetRoom()) 342 | } 343 | 344 | func (self *Session) printRoom(room types.Room) { 345 | pcs := model.PlayerCharactersIn(self.pc.GetRoomId(), self.pc.GetId()) 346 | npcs := model.NpcsIn(room.GetId()) 347 | items := model.ItemsIn(room.GetId()) 348 | store := model.StoreIn(room.GetId()) 349 | 350 | var area types.Area 351 | if room.GetAreaId() != nil { 352 | area = model.GetArea(room.GetAreaId()) 353 | } 354 | 355 | var str string 356 | 357 | areaStr := "" 358 | if area != nil { 359 | areaStr = fmt.Sprintf("%s - ", area.GetName()) 360 | } 361 | 362 | str = fmt.Sprintf("\r\n %v>>> %v%s%s %v<<< %v(%v %v %v)\r\n\r\n %v%s\r\n\r\n", 363 | types.ColorWhite, types.ColorBlue, 364 | areaStr, room.GetTitle(), 365 | types.ColorWhite, types.ColorBlue, 366 | room.GetLocation().X, room.GetLocation().Y, room.GetLocation().Z, 367 | types.ColorWhite, 368 | room.GetDescription()) 369 | 370 | if store != nil { 371 | str = fmt.Sprintf("%s Store: %s\r\n\r\n", str, types.Colorize(types.ColorBlue, store.GetName())) 372 | } 373 | 374 | extraNewLine := "" 375 | 376 | if len(pcs) > 0 { 377 | str = fmt.Sprintf("%s %sAlso here:", str, types.ColorBlue) 378 | 379 | names := make([]string, len(pcs)) 380 | for i, char := range pcs { 381 | names[i] = types.Colorize(types.ColorWhite, char.GetName()) 382 | } 383 | str = fmt.Sprintf("%s %s \r\n", str, strings.Join(names, types.Colorize(types.ColorBlue, ", "))) 384 | 385 | extraNewLine = "\r\n" 386 | } 387 | 388 | if len(npcs) > 0 { 389 | str = fmt.Sprintf("%s %s", str, types.Colorize(types.ColorBlue, "NPCs: ")) 390 | 391 | names := make([]string, len(npcs)) 392 | for i, npc := range npcs { 393 | names[i] = types.Colorize(types.ColorWhite, npc.GetName()) 394 | } 395 | str = fmt.Sprintf("%s %s \r\n", str, strings.Join(names, types.Colorize(types.ColorBlue, ", "))) 396 | 397 | extraNewLine = "\r\n" 398 | } 399 | 400 | if len(items) > 0 { 401 | itemMap := make(map[string]int) 402 | var nameList []string 403 | 404 | for _, item := range items { 405 | if item == nil { 406 | continue 407 | } 408 | 409 | _, found := itemMap[item.GetName()] 410 | if !found { 411 | nameList = append(nameList, item.GetName()) 412 | } 413 | itemMap[item.GetName()]++ 414 | } 415 | 416 | sort.Strings(nameList) 417 | 418 | str = str + " " + types.Colorize(types.ColorBlue, "Items: ") 419 | 420 | var names []string 421 | for _, name := range nameList { 422 | if itemMap[name] > 1 { 423 | name = fmt.Sprintf("%s x%v", name, itemMap[name]) 424 | } 425 | names = append(names, types.Colorize(types.ColorWhite, name)) 426 | } 427 | str = str + strings.Join(names, types.Colorize(types.ColorBlue, ", ")) + "\r\n" 428 | 429 | extraNewLine = "\r\n" 430 | } 431 | 432 | str = str + extraNewLine + " " + types.Colorize(types.ColorBlue, "Exits: ") 433 | 434 | var exitList []string 435 | for _, direction := range room.GetExits() { 436 | exitList = append(exitList, utils.DirectionToExitString(direction)) 437 | } 438 | 439 | if len(exitList) == 0 { 440 | str = str + types.Colorize(types.ColorWhite, "None") 441 | } else { 442 | str = str + strings.Join(exitList, " ") 443 | } 444 | 445 | if len(room.GetLinks()) > 0 { 446 | str = fmt.Sprintf("%s\r\n\r\n %s %s", 447 | str, 448 | types.Colorize(types.ColorBlue, "Other exits:"), 449 | types.Colorize(types.ColorWhite, strings.Join(room.LinkNames(), ", ")), 450 | ) 451 | } 452 | 453 | str = str + "\r\n" 454 | 455 | self.WriteLine(str) 456 | } 457 | 458 | func (s *Session) execMenu(title string, build func(*utils.Menu)) { 459 | utils.ExecMenu(title, s, build) 460 | } 461 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/zlib" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "math/rand" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "time" 16 | "unicode" 17 | 18 | "github.com/Cristofori/kmud/types" 19 | ) 20 | 21 | type Prompter interface { 22 | GetPrompt() string 23 | } 24 | 25 | type simplePrompter struct { 26 | prompt string 27 | } 28 | 29 | func (sp simplePrompter) GetPrompt() string { 30 | return sp.prompt 31 | } 32 | 33 | // SimpleRompter returns a Prompter that always returns the given string as its prompt 34 | func SimplePrompter(prompt string) Prompter { 35 | var prompter simplePrompter 36 | prompter.prompt = prompt 37 | return &prompter 38 | } 39 | 40 | func Write(conn io.Writer, text string, cm types.ColorMode) { 41 | _, err := conn.Write([]byte(types.ProcessColors(text, cm))) 42 | HandleError(err) 43 | } 44 | 45 | func WriteLine(conn io.Writer, line string, cm types.ColorMode) { 46 | Write(conn, line+"\r\n", cm) 47 | } 48 | 49 | // ClearLine sends the VT100 code for erasing the line followed by a carriage 50 | // return to move the cursor back to the beginning of the line 51 | func ClearLine(conn io.Writer) { 52 | clearline := "\x1B[2K" 53 | Write(conn, clearline+"\r", types.ColorModeNone) 54 | } 55 | 56 | func Simplify(str string) string { 57 | simpleStr := strings.TrimSpace(str) 58 | simpleStr = strings.ToLower(simpleStr) 59 | return simpleStr 60 | } 61 | 62 | func GetRawUserInputSuffix(conn io.ReadWriter, prompt string, suffix string, cm types.ColorMode) string { 63 | return GetRawUserInputSuffixP(conn, SimplePrompter(prompt), suffix, cm) 64 | } 65 | 66 | func GetRawUserInputSuffixP(conn io.ReadWriter, prompter Prompter, suffix string, cm types.ColorMode) string { 67 | scanner := bufio.NewScanner(conn) 68 | 69 | for { 70 | Write(conn, prompter.GetPrompt(), cm) 71 | 72 | if !scanner.Scan() { 73 | err := scanner.Err() 74 | if err == nil { 75 | err = io.EOF 76 | } 77 | 78 | panic(err) 79 | } 80 | 81 | input := scanner.Text() 82 | Write(conn, suffix, cm) 83 | 84 | if input == "x" || input == "X" { 85 | return "" 86 | } else if input != "" { 87 | return input 88 | } 89 | } 90 | } 91 | 92 | func GetRawUserInputP(conn io.ReadWriter, prompter Prompter, cm types.ColorMode) string { 93 | return GetRawUserInputSuffixP(conn, prompter, "", cm) 94 | } 95 | 96 | func GetRawUserInput(conn io.ReadWriter, prompt string, cm types.ColorMode) string { 97 | return GetRawUserInputP(conn, SimplePrompter(prompt), cm) 98 | } 99 | 100 | func GetUserInputP(conn io.ReadWriter, prompter Prompter, cm types.ColorMode) string { 101 | input := GetRawUserInputP(conn, prompter, cm) 102 | return Simplify(input) 103 | } 104 | 105 | func GetUserInput(conn io.ReadWriter, prompt string, cm types.ColorMode) string { 106 | input := GetUserInputP(conn, SimplePrompter(prompt), cm) 107 | return Simplify(input) 108 | } 109 | 110 | func HandleError(err error) { 111 | if err != nil { 112 | log.Printf("Error: %s", err) 113 | panic(err) 114 | } 115 | } 116 | 117 | func FormatName(name string) string { 118 | if name == "" { 119 | return name 120 | } 121 | 122 | fields := strings.Fields(name) 123 | for i, field := range fields { 124 | runes := []rune(strings.ToLower(field)) 125 | runes[0] = unicode.ToUpper(runes[0]) 126 | fields[i] = string(runes) 127 | } 128 | 129 | return strings.Join(fields, " ") 130 | } 131 | 132 | func Argify(data string) (string, string) { 133 | fields := strings.Fields(data) 134 | 135 | if len(fields) == 0 { 136 | return "", "" 137 | } 138 | 139 | command := Simplify(fields[0]) 140 | params := strings.TrimSpace(data[len(command):]) 141 | 142 | return command, params 143 | } 144 | 145 | func rowEmpty(row string) bool { 146 | for _, char := range row { 147 | if char != ' ' { 148 | return false 149 | } 150 | } 151 | return true 152 | } 153 | 154 | func TrimUpperRows(rows []string) []string { 155 | for _, row := range rows { 156 | if !rowEmpty(row) { 157 | break 158 | } 159 | 160 | rows = rows[1:] 161 | } 162 | 163 | return rows 164 | } 165 | 166 | func TrimLowerRows(rows []string) []string { 167 | for i := len(rows) - 1; i >= 0; i -= 1 { 168 | row := rows[i] 169 | if !rowEmpty(row) { 170 | break 171 | } 172 | rows = rows[:len(rows)-1] 173 | } 174 | 175 | return rows 176 | } 177 | 178 | func TrimEmptyRows(str string) string { 179 | rows := strings.Split(str, "\r\n") 180 | return strings.Join(TrimLowerRows(TrimUpperRows(rows)), "\r\n") 181 | } 182 | 183 | func ValidateName(name string) error { 184 | const MinSize = 3 185 | const MaxSize = 12 186 | 187 | if len(name) < MinSize || len(name) > MaxSize { 188 | return errors.New(fmt.Sprintf("Names must be between %v and %v letters long", MinSize, MaxSize)) 189 | } 190 | 191 | regex := regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]*$") 192 | 193 | if !regex.MatchString(name) { 194 | return errors.New("Names may only contain letters or numbers (A-Z, 0-9), and must begin with a letter") 195 | } 196 | 197 | return nil 198 | } 199 | 200 | func MonitorChannel() { 201 | // TODO: See if there's a way to take in a generic channel and see how close it is to being full 202 | } 203 | 204 | // BestMatch searches the given list for the given pattern, the index of the 205 | // longest match that starts with the given pattern is returned. Returns -1 if 206 | // no match was found, -2 if the result is ambiguous. The search is case 207 | // insensitive 208 | func BestMatch(pattern string, searchList []string) int { 209 | pattern = strings.ToLower(pattern) 210 | 211 | index := -1 212 | 213 | for i, searchItem := range searchList { 214 | searchItem = strings.ToLower(searchItem) 215 | 216 | if searchItem == pattern { 217 | return i 218 | } 219 | 220 | if strings.HasPrefix(searchItem, pattern) { 221 | if index != -1 { 222 | return -2 223 | } 224 | 225 | index = i 226 | } 227 | } 228 | 229 | return index 230 | } 231 | 232 | func compress(data []byte) []byte { 233 | var b bytes.Buffer 234 | w := zlib.NewWriter(&b) 235 | w.Write(data) 236 | w.Close() 237 | return b.Bytes() 238 | } 239 | 240 | type WatchableReadWriter struct { 241 | rw io.ReadWriter 242 | watchers []io.ReadWriter 243 | } 244 | 245 | func NewWatchableReadWriter(rw io.ReadWriter) *WatchableReadWriter { 246 | var watchable WatchableReadWriter 247 | watchable.rw = rw 248 | return &watchable 249 | } 250 | 251 | func (w *WatchableReadWriter) Read(p []byte) (int, error) { 252 | n, err := w.rw.Read(p) 253 | 254 | for _, watcher := range w.watchers { 255 | watcher.Write(p[:n]) 256 | } 257 | 258 | return n, err 259 | } 260 | 261 | func (w *WatchableReadWriter) Write(p []byte) (int, error) { 262 | for _, watcher := range w.watchers { 263 | watcher.Write(p) 264 | } 265 | 266 | return w.rw.Write(p) 267 | } 268 | 269 | func (w *WatchableReadWriter) AddWatcher(rw io.ReadWriter) { 270 | w.watchers = append(w.watchers, rw) 271 | } 272 | 273 | func (w *WatchableReadWriter) RemoveWatcher(rw io.ReadWriter) { 274 | for i, watcher := range w.watchers { 275 | if watcher == rw { 276 | // TODO: Potential memory leak. See http://code.google.com/p/go-wiki/wiki/SliceTricks 277 | w.watchers = append(w.watchers[:i], w.watchers[i+1:]...) 278 | return 279 | } 280 | } 281 | } 282 | 283 | // Case-insensitive string comparison 284 | func Compare(str1, str2 string) bool { 285 | return strings.ToLower(str1) == strings.ToLower(str2) 286 | } 287 | 288 | // Throttler is a simple utility class that allows events to occur on a 289 | // deterministic recurring basis. Every call to Sync() will block until the 290 | // duration of the Throttler's interval has passed since the last call to 291 | // Sync() 292 | type Throttler struct { 293 | lastTime time.Time 294 | interval time.Duration 295 | } 296 | 297 | func NewThrottler(interval time.Duration) *Throttler { 298 | var throttler Throttler 299 | throttler.lastTime = time.Now() 300 | throttler.interval = interval 301 | return &throttler 302 | } 303 | 304 | func (self *Throttler) Sync() { 305 | diff := time.Since(self.lastTime) 306 | if diff < self.interval { 307 | time.Sleep(self.interval - diff) 308 | } 309 | self.lastTime = time.Now() 310 | } 311 | 312 | // Random returns a random integer between low and high, inclusive 313 | func Random(low, high int) int { 314 | if high < low { 315 | high, low = low, high 316 | } 317 | 318 | diff := high - low 319 | 320 | if diff == 0 { 321 | return low 322 | } 323 | 324 | result := rand.Int() % (diff + 1) 325 | result += low 326 | 327 | return result 328 | } 329 | 330 | func DirectionToExitString(direction types.Direction) string { 331 | letterColor := types.ColorBlue 332 | bracketColor := types.ColorDarkBlue 333 | textColor := types.ColorWhite 334 | 335 | colorize := func(letters string, text string) string { 336 | return fmt.Sprintf("%s%s%s%s", 337 | types.Colorize(bracketColor, "["), 338 | types.Colorize(letterColor, letters), 339 | types.Colorize(bracketColor, "]"), 340 | types.Colorize(textColor, text)) 341 | } 342 | 343 | switch direction { 344 | case types.DirectionNorth: 345 | return colorize("N", "orth") 346 | case types.DirectionNorthEast: 347 | return colorize("NE", "North East") 348 | case types.DirectionEast: 349 | return colorize("E", "ast") 350 | case types.DirectionSouthEast: 351 | return colorize("SE", "South East") 352 | case types.DirectionSouth: 353 | return colorize("S", "outh") 354 | case types.DirectionSouthWest: 355 | return colorize("SW", "South West") 356 | case types.DirectionWest: 357 | return colorize("W", "est") 358 | case types.DirectionNorthWest: 359 | return colorize("NW", "North West") 360 | case types.DirectionUp: 361 | return colorize("U", "p") 362 | case types.DirectionDown: 363 | return colorize("D", "own") 364 | case types.DirectionNone: 365 | return types.Colorize(types.ColorWhite, "None") 366 | } 367 | 368 | panic("Unexpected code path") 369 | } 370 | 371 | func Paginate(list []string, width, height int) []string { 372 | itemLength := func(item string) int { 373 | return len(types.StripColors(item)) 374 | } 375 | 376 | columns := [][]string{} 377 | widths := []int{} 378 | totalWidth := 0 379 | 380 | index := 0 381 | for { 382 | column := []string{} 383 | for ; index < (height*(len(columns)+1)) && index < len(list); index++ { 384 | column = append(column, list[index]) 385 | } 386 | 387 | columnWidth := 0 388 | for _, item := range column { 389 | length := itemLength(item) 390 | if length > columnWidth { 391 | columnWidth = length 392 | } 393 | } 394 | columnWidth += 2 // Padding between columns 395 | 396 | if (columnWidth + totalWidth) > width { 397 | // Column doesn't fit, drop it 398 | index -= len(column) 399 | break 400 | } 401 | 402 | totalWidth += columnWidth 403 | widths = append(widths, columnWidth) 404 | columns = append(columns, column) 405 | 406 | if index >= len(list) { 407 | break 408 | } 409 | } 410 | 411 | page := "" 412 | 413 | for i := range columns[0] { 414 | for j := range columns { 415 | column := columns[j] 416 | 417 | if i < len(column) { 418 | item := column[i] 419 | page += item + strings.Repeat(" ", widths[j]-itemLength(item)) 420 | } 421 | } 422 | 423 | page += "\r\n" 424 | } 425 | 426 | pages := []string{page} 427 | 428 | if index < len(list) { 429 | pages = append(pages, Paginate(list[index:], width, height)...) 430 | } 431 | 432 | return pages 433 | } 434 | 435 | func Atois(strings []string) ([]int, error) { 436 | ints := make([]int, len(strings)) 437 | for i, str := range strings { 438 | val, err := strconv.Atoi(str) 439 | if err != nil { 440 | return ints, err 441 | } 442 | ints[i] = val 443 | } 444 | 445 | return ints, nil 446 | } 447 | 448 | func Atoir(str string, min, max int) (int, error) { 449 | val, err := strconv.Atoi(str) 450 | if err != nil { 451 | return val, fmt.Errorf("%v is not a valid number", str) 452 | } 453 | 454 | if val < min || val > max { 455 | return val, fmt.Errorf("Value out of range: %v (%v - %v)", val, min, max) 456 | } 457 | 458 | return val, nil 459 | } 460 | 461 | func Min(x, y int) int { 462 | if x < y { 463 | return x 464 | } 465 | return y 466 | } 467 | 468 | func Max(x, y int) int { 469 | if x > y { 470 | return x 471 | } 472 | return y 473 | } 474 | 475 | func Abs(x int) int { 476 | if x < 0 { 477 | x = -x 478 | } 479 | return x 480 | } 481 | 482 | func Bound(x, lower, upper int) int { 483 | if x < lower { 484 | return lower 485 | } 486 | if x > upper { 487 | return upper 488 | } 489 | return x 490 | } 491 | 492 | func Filter(list []string, pattern string) []string { 493 | if pattern == "" { 494 | return list 495 | } 496 | filtered := []string{} 497 | 498 | for _, item := range list { 499 | if FilterItem(item, pattern) { 500 | filtered = append(filtered, item) 501 | } 502 | } 503 | 504 | return filtered 505 | } 506 | 507 | func FilterItem(item, pattern string) bool { 508 | return strings.Contains(strings.ToLower(types.StripColors(item)), strings.ToLower(pattern)) 509 | } 510 | -------------------------------------------------------------------------------- /telnet/telnet.go: -------------------------------------------------------------------------------- 1 | package telnet 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | // RFC 854: http://tools.ietf.org/html/rfc854, http://support.microsoft.com/kb/231866 11 | 12 | var byteToCode map[byte]TelnetCode 13 | var codeToByte map[TelnetCode]byte 14 | 15 | type TelnetCode int 16 | 17 | func init() { 18 | byteToCode = map[byte]TelnetCode{} 19 | codeToByte = map[TelnetCode]byte{} 20 | 21 | codeToByte[NUL] = '\x00' 22 | codeToByte[ECHO] = '\x01' 23 | codeToByte[SGA] = '\x03' 24 | codeToByte[ST] = '\x05' 25 | codeToByte[TM] = '\x06' 26 | codeToByte[BEL] = '\x07' 27 | codeToByte[BS] = '\x08' 28 | codeToByte[HT] = '\x09' 29 | codeToByte[LF] = '\x0a' 30 | codeToByte[FF] = '\x0c' 31 | codeToByte[CR] = '\x0d' 32 | codeToByte[TT] = '\x18' 33 | codeToByte[WS] = '\x1F' 34 | codeToByte[TS] = '\x20' 35 | codeToByte[RFC] = '\x21' 36 | codeToByte[LM] = '\x22' 37 | codeToByte[EV] = '\x24' 38 | codeToByte[SE] = '\xf0' 39 | codeToByte[NOP] = '\xf1' 40 | codeToByte[DM] = '\xf2' 41 | codeToByte[BRK] = '\xf3' 42 | codeToByte[IP] = '\xf4' 43 | codeToByte[AO] = '\xf5' 44 | codeToByte[AYT] = '\xf6' 45 | codeToByte[EC] = '\xf7' 46 | codeToByte[EL] = '\xf8' 47 | codeToByte[GA] = '\xf9' 48 | codeToByte[SB] = '\xfa' 49 | codeToByte[WILL] = '\xfb' 50 | codeToByte[WONT] = '\xfc' 51 | codeToByte[DO] = '\xfd' 52 | codeToByte[DONT] = '\xfe' 53 | codeToByte[IAC] = '\xff' 54 | 55 | codeToByte[CMP1] = '\x55' 56 | codeToByte[CMP2] = '\x56' 57 | codeToByte[AARD] = '\x66' 58 | codeToByte[ATCP] = '\xc8' 59 | codeToByte[GMCP] = '\xc9' 60 | 61 | for enum, code := range codeToByte { 62 | byteToCode[code] = enum 63 | } 64 | } 65 | 66 | // Telnet wraps the given connection object, processing telnet codes from its byte 67 | // stream and interpreting them as necessary, making it possible to hand the connection 68 | // object off to other code so that it doesn't have to worry about telnet escape sequences 69 | // being found in its data. 70 | type Telnet struct { 71 | conn net.Conn 72 | err error 73 | 74 | processor *telnetProcessor 75 | } 76 | 77 | func NewTelnet(conn net.Conn) *Telnet { 78 | var t Telnet 79 | t.conn = conn 80 | t.processor = newTelnetProcessor() 81 | return &t 82 | } 83 | 84 | func (t *Telnet) Write(p []byte) (int, error) { 85 | return t.conn.Write(p) 86 | } 87 | 88 | func (t *Telnet) Read(p []byte) (int, error) { 89 | for { 90 | t.fill() 91 | if t.err != nil { 92 | return 0, t.err 93 | } 94 | 95 | n, err := t.processor.Read(p) 96 | if n > 0 { 97 | return n, err 98 | } 99 | } 100 | } 101 | 102 | func (t *Telnet) Data(code TelnetCode) []byte { 103 | return t.processor.subdata[code] 104 | } 105 | 106 | func (t *Telnet) Listen(listenFunc func(TelnetCode, []byte)) { 107 | t.processor.listenFunc = listenFunc 108 | } 109 | 110 | // Idea/name for this function shamelessly stolen from bufio 111 | func (t *Telnet) fill() { 112 | buf := make([]byte, 1024) 113 | n, err := t.conn.Read(buf) 114 | t.err = err 115 | t.processor.addBytes(buf[:n]) 116 | } 117 | 118 | func (t *Telnet) Close() error { 119 | return t.conn.Close() 120 | } 121 | 122 | func (t *Telnet) LocalAddr() net.Addr { 123 | return t.conn.LocalAddr() 124 | } 125 | 126 | func (t *Telnet) RemoteAddr() net.Addr { 127 | return t.conn.RemoteAddr() 128 | } 129 | 130 | func (t *Telnet) SetDeadline(dl time.Time) error { 131 | return t.conn.SetDeadline(dl) 132 | } 133 | 134 | func (t *Telnet) SetReadDeadline(dl time.Time) error { 135 | return t.conn.SetReadDeadline(dl) 136 | } 137 | 138 | func (t *Telnet) SetWriteDeadline(dl time.Time) error { 139 | return t.conn.SetWriteDeadline(dl) 140 | } 141 | 142 | func (t *Telnet) WillEcho() { 143 | t.SendCommand(WILL, ECHO) 144 | } 145 | 146 | func (t *Telnet) WontEcho() { 147 | t.SendCommand(WONT, ECHO) 148 | } 149 | 150 | func (t *Telnet) DoWindowSize() { 151 | t.SendCommand(DO, WS) 152 | } 153 | 154 | func (t *Telnet) DoTerminalType() { 155 | // This is really supposed to be two commands, one to ask if they'll send a 156 | // terminal type, and another to indicate that they should send it if 157 | // they've expressed a "willingness" to send it. For the time being this 158 | // works well enough. 159 | 160 | // See http://tools.ietf.org/html/rfc884 161 | 162 | t.SendCommand(DO, TT, IAC, SB, TT, 1, IAC, SE) // 1 = SEND 163 | } 164 | 165 | func (t *Telnet) SendCommand(codes ...TelnetCode) { 166 | t.conn.Write(BuildCommand(codes...)) 167 | } 168 | 169 | func BuildCommand(codes ...TelnetCode) []byte { 170 | command := make([]byte, len(codes)+1) 171 | command[0] = codeToByte[IAC] 172 | 173 | for i, code := range codes { 174 | command[i+1] = codeToByte[code] 175 | } 176 | 177 | return command 178 | } 179 | 180 | const ( 181 | NUL TelnetCode = iota // NULL, no operation 182 | ECHO TelnetCode = iota // Echo 183 | SGA TelnetCode = iota // Suppress go ahead 184 | ST TelnetCode = iota // Status 185 | TM TelnetCode = iota // Timing mark 186 | BEL TelnetCode = iota // Bell 187 | BS TelnetCode = iota // Backspace 188 | HT TelnetCode = iota // Horizontal tab 189 | LF TelnetCode = iota // Line feed 190 | FF TelnetCode = iota // Form feed 191 | CR TelnetCode = iota // Carriage return 192 | TT TelnetCode = iota // Terminal type 193 | WS TelnetCode = iota // Window size 194 | TS TelnetCode = iota // Terminal speed 195 | RFC TelnetCode = iota // Remote flow control 196 | LM TelnetCode = iota // Line mode 197 | EV TelnetCode = iota // Environment variables 198 | SE TelnetCode = iota // End of subnegotiation parameters. 199 | NOP TelnetCode = iota // No operation. 200 | DM TelnetCode = iota // Data Mark. The data stream portion of a Synch. This should always be accompanied by a TCP Urgent notification. 201 | BRK TelnetCode = iota // Break. NVT character BRK. 202 | IP TelnetCode = iota // Interrupt Process 203 | AO TelnetCode = iota // Abort output 204 | AYT TelnetCode = iota // Are you there 205 | EC TelnetCode = iota // Erase character 206 | EL TelnetCode = iota // Erase line 207 | GA TelnetCode = iota // Go ahead signal 208 | SB TelnetCode = iota // Indicates that what follows is subnegotiation of the indicated option. 209 | WILL TelnetCode = iota // Indicates the desire to begin performing, or confirmation that you are now performing, the indicated option. 210 | WONT TelnetCode = iota // Indicates the refusal to perform, or continue performing, the indicated option. 211 | DO TelnetCode = iota // Indicates the request that the other party perform, or confirmation that you are expecting the other party to perform, the indicated option. 212 | DONT TelnetCode = iota // Indicates the demand that the other party stop performing, or confirmation that you are no longer expecting the other party to perform, the indicated option. 213 | IAC TelnetCode = iota // Interpret as command 214 | 215 | // Non-standard codes: 216 | CMP1 TelnetCode = iota // MCCP Compress 217 | CMP2 TelnetCode = iota // MCCP Compress2 218 | AARD TelnetCode = iota // Aardwolf MUD out of band communication, http://www.aardwolf.com/blog/2008/07/10/telnet-negotiation-control-mud-client-interaction/ 219 | ATCP TelnetCode = iota // Achaea Telnet Client Protocol, http://www.ironrealms.com/rapture/manual/files/FeatATCP-txt.html 220 | GMCP TelnetCode = iota // Generic Mud Communication Protocol 221 | ) 222 | 223 | type processorState int 224 | 225 | const ( 226 | stateBase processorState = iota 227 | stateInIAC processorState = iota 228 | stateInSB processorState = iota 229 | stateCapSB processorState = iota 230 | stateEscIAC processorState = iota 231 | ) 232 | 233 | // telnetProcessor implements a state machine that reads input one byte at a time 234 | // and processes it according to the telnet spec. It is designed to read a raw telnet 235 | // stream, from which it will extract telnet escape codes and subnegotiation data. 236 | // The processor can then be read from with all of the telnet codes removed, leaving 237 | // the pure user input stream. 238 | type telnetProcessor struct { 239 | state processorState 240 | currentSB TelnetCode 241 | 242 | capturedBytes []byte 243 | subdata map[TelnetCode][]byte 244 | cleanData string 245 | listenFunc func(TelnetCode, []byte) 246 | 247 | debug bool 248 | } 249 | 250 | func newTelnetProcessor() *telnetProcessor { 251 | var tp telnetProcessor 252 | tp.state = stateBase 253 | tp.debug = false 254 | tp.currentSB = NUL 255 | 256 | return &tp 257 | } 258 | 259 | func (self *telnetProcessor) Read(p []byte) (int, error) { 260 | maxLen := len(p) 261 | 262 | n := 0 263 | 264 | if maxLen >= len(self.cleanData) { 265 | n = len(self.cleanData) 266 | } else { 267 | n = maxLen 268 | } 269 | 270 | for i := 0; i < n; i++ { 271 | p[i] = self.cleanData[i] 272 | } 273 | 274 | self.cleanData = self.cleanData[n:] // TODO: Memory leak? 275 | 276 | return n, nil 277 | } 278 | 279 | func (self *telnetProcessor) capture(b byte) { 280 | if self.debug { 281 | fmt.Println("Captured:", ByteToCodeString(b)) 282 | } 283 | 284 | self.capturedBytes = append(self.capturedBytes, b) 285 | } 286 | 287 | func (self *telnetProcessor) dontCapture(b byte) { 288 | self.cleanData = self.cleanData + string(b) 289 | } 290 | 291 | func (self *telnetProcessor) resetSubDataField(code TelnetCode) { 292 | if self.subdata == nil { 293 | self.subdata = map[TelnetCode][]byte{} 294 | } 295 | 296 | self.subdata[code] = []byte{} 297 | } 298 | 299 | func (self *telnetProcessor) captureSubData(code TelnetCode, b byte) { 300 | if self.debug { 301 | fmt.Println("Captured subdata:", CodeToString(code), b) 302 | } 303 | 304 | if self.subdata == nil { 305 | self.subdata = map[TelnetCode][]byte{} 306 | } 307 | 308 | self.subdata[code] = append(self.subdata[code], b) 309 | } 310 | 311 | func (self *telnetProcessor) addBytes(bytes []byte) { 312 | for _, b := range bytes { 313 | self.addByte(b) 314 | } 315 | } 316 | 317 | func (self *telnetProcessor) addByte(b byte) { 318 | code := byteToCode[b] 319 | 320 | switch self.state { 321 | case stateBase: 322 | if code == IAC { 323 | self.state = stateInIAC 324 | self.capture(b) 325 | } else { 326 | self.dontCapture(b) 327 | } 328 | 329 | case stateInIAC: 330 | if code == WILL || code == WONT || code == DO || code == DONT { 331 | // Stay in this state 332 | } else if code == SB { 333 | self.state = stateInSB 334 | } else { 335 | self.state = stateBase 336 | } 337 | self.capture(b) 338 | 339 | case stateInSB: 340 | self.capture(b) 341 | self.currentSB = code 342 | self.state = stateCapSB 343 | self.resetSubDataField(code) 344 | 345 | case stateCapSB: 346 | if code == IAC { 347 | self.state = stateEscIAC 348 | } else { 349 | self.captureSubData(self.currentSB, b) 350 | } 351 | 352 | case stateEscIAC: 353 | if code == IAC { 354 | self.state = stateCapSB 355 | self.captureSubData(self.currentSB, b) 356 | } else { 357 | self.subDataFinished(self.currentSB) 358 | self.currentSB = NUL 359 | self.state = stateBase 360 | self.addByte(codeToByte[IAC]) 361 | self.addByte(b) 362 | } 363 | } 364 | } 365 | 366 | func (self *telnetProcessor) subDataFinished(code TelnetCode) { 367 | if self.listenFunc != nil { 368 | self.listenFunc(code, self.subdata[code]) 369 | } 370 | } 371 | 372 | func ToString(bytes []byte) string { 373 | str := "" 374 | for _, b := range bytes { 375 | 376 | if str != "" { 377 | str = str + " " 378 | } 379 | 380 | str = str + ByteToCodeString(b) 381 | } 382 | 383 | return str 384 | } 385 | 386 | func ByteToCodeString(b byte) string { 387 | code, found := byteToCode[b] 388 | 389 | if !found { 390 | return "??(" + strconv.Itoa(int(b)) + ")" 391 | } 392 | 393 | return CodeToString(code) 394 | } 395 | 396 | func CodeToString(code TelnetCode) string { 397 | switch code { 398 | case NUL: 399 | return "NUL" 400 | case ECHO: 401 | return "ECHO" 402 | case SGA: 403 | return "SGA" 404 | case ST: 405 | return "ST" 406 | case TM: 407 | return "TM" 408 | case BEL: 409 | return "BEL" 410 | case BS: 411 | return "BS" 412 | case HT: 413 | return "HT" 414 | case LF: 415 | return "LF" 416 | case FF: 417 | return "FF" 418 | case CR: 419 | return "CR" 420 | case TT: 421 | return "TT" 422 | case WS: 423 | return "WS" 424 | case TS: 425 | return "TS" 426 | case RFC: 427 | return "RFC" 428 | case LM: 429 | return "LM" 430 | case EV: 431 | return "EV" 432 | case SE: 433 | return "SE" 434 | case NOP: 435 | return "NOP" 436 | case DM: 437 | return "DM" 438 | case BRK: 439 | return "BRK" 440 | case IP: 441 | return "IP" 442 | case AO: 443 | return "AO" 444 | case AYT: 445 | return "AYT" 446 | case EC: 447 | return "EC" 448 | case EL: 449 | return "EL" 450 | case GA: 451 | return "GA" 452 | case SB: 453 | return "SB" 454 | case WILL: 455 | return "WILL" 456 | case WONT: 457 | return "WONT" 458 | case DO: 459 | return "DO" 460 | case DONT: 461 | return "DONT" 462 | case IAC: 463 | return "IAC" 464 | case CMP1: 465 | return "CMP1" 466 | case CMP2: 467 | return "CMP2" 468 | case AARD: 469 | return "AARD" 470 | case ATCP: 471 | return "ATCP" 472 | case GMCP: 473 | return "GMCP" 474 | } 475 | 476 | return "" 477 | } 478 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "math/rand" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/Cristofori/kmud/testutils" 12 | "github.com/Cristofori/kmud/types" 13 | ) 14 | 15 | func Test_WriteLine(t *testing.T) { 16 | writer := &testutils.TestWriter{} 17 | 18 | line := "This is a line" 19 | want := line + "\r\n" 20 | 21 | WriteLine(writer, line, types.ColorModeNone) 22 | 23 | if writer.Wrote != want { 24 | t.Errorf("WriteLine(%q) == %q, want %q", line, writer.Wrote, want) 25 | } 26 | } 27 | 28 | func Test_Simplify(t *testing.T) { 29 | tests := []struct { 30 | s, want string 31 | }{ 32 | {" Test1", "test1"}, 33 | {"TesT2 ", "test2"}, 34 | {" tESt3 ", "test3"}, 35 | {"\tTeSt4\t", "test4"}, 36 | {" teSt5 \n", "test5"}, 37 | {" \t \t TEST6 \n \t", "test6"}, 38 | } 39 | 40 | for _, c := range tests { 41 | got := Simplify(c.s) 42 | if got != c.want { 43 | t.Errorf("Simplify(%q) == %q, want %q", c.s, got, c.want) 44 | } 45 | } 46 | } 47 | 48 | func Test_GetRawUserInput(t *testing.T) { 49 | readWriter := &testutils.TestReadWriter{} 50 | 51 | tests := []struct { 52 | input, output string 53 | }{ 54 | {"Test1", "Test1"}, 55 | {"TEST2", "TEST2"}, 56 | {" test3 ", " test3 "}, 57 | {"\tTeSt4\t", "\tTeSt4\t"}, 58 | {"x", ""}, 59 | {"X", ""}, 60 | } 61 | 62 | for _, test := range tests { 63 | readWriter.ToRead = test.input 64 | line := GetRawUserInput(readWriter, ">", types.ColorModeNone) 65 | if line != test.output { 66 | t.Errorf("GetRawUserInput(%q) == %q, want %q", test.input, line, test.output) 67 | } 68 | } 69 | } 70 | 71 | func Test_GetUserInput(t *testing.T) { 72 | readWriter := &testutils.TestReadWriter{} 73 | 74 | tests := []struct { 75 | input, output string 76 | }{ 77 | {"Test1", "test1"}, 78 | {"TEST2", "test2"}, 79 | {"\tTeSt3\t", "test3"}, 80 | {" teSt4 \n", "test4"}, 81 | {" \t \t TEST5 \n \t", "test5"}, 82 | {"x", ""}, 83 | {"X", ""}, 84 | } 85 | 86 | for _, test := range tests { 87 | readWriter.ToRead = test.input 88 | line := GetUserInput(readWriter, ">", types.ColorModeNone) 89 | if line != test.output { 90 | t.Errorf("GetUserInput(%q) == %q, want %q", test.input, line, test.output) 91 | } 92 | } 93 | } 94 | 95 | func Test_GetUserInputPanicOnEOF(t *testing.T) { 96 | readWriter := &testutils.TestReadWriter{} 97 | readWriter.SetError(io.EOF) 98 | 99 | defer func() { 100 | r := recover() 101 | if r == nil { 102 | t.Errorf("GetUserInput() didn't panic on EOF") 103 | } 104 | }() 105 | 106 | GetUserInput(readWriter, "", types.ColorModeNone) 107 | } 108 | 109 | func Test_HandleError(t *testing.T) { 110 | // TODO try using recover 111 | HandleError(nil) 112 | } 113 | 114 | func Test_FormatName(t *testing.T) { 115 | tests := []struct { 116 | input string 117 | output string 118 | }{ 119 | {"", ""}, 120 | {"joe", "Joe"}, 121 | {"ASDF", "Asdf"}, 122 | {"Bob", "Bob"}, 123 | {"aBcDeFg", "Abcdefg"}, 124 | {"foo bar", "Foo Bar"}, 125 | {"foO bAr", "Foo Bar"}, 126 | {"foo \n bar", "Foo Bar"}, 127 | } 128 | 129 | for _, test := range tests { 130 | result := FormatName(test.input) 131 | if result != test.output { 132 | t.Errorf("FormatName(%s) == %s, want %s", test.input, result, test.output) 133 | } 134 | } 135 | } 136 | 137 | func Test_ValidateName(t *testing.T) { 138 | tests := []struct { 139 | input string 140 | output bool 141 | }{ 142 | {"t", false}, 143 | {"te", false}, 144 | {"tes", true}, 145 | {"te1", true}, 146 | {"test", true}, 147 | {"testing", true}, 148 | {"*(!(@#*$", false}, 149 | {"Abc1abc", true}, 150 | {"123456789012", false}, 151 | {"aslsidjfljll", true}, 152 | {"1slsidjfljll", false}, 153 | {"aslsidjfljl3", true}, 154 | } 155 | 156 | for _, test := range tests { 157 | result := (ValidateName(test.input) == nil) 158 | if result != test.output { 159 | t.Errorf("ValidateName(%s) == %v, want %v", test.input, result, test.output) 160 | } 161 | } 162 | } 163 | 164 | func Test_BestMatch(t *testing.T) { 165 | searchList := []string{"", "Foo", "Bar", "Joe", "Bob", "Abcdef", "Abc", "QrStUv"} 166 | 167 | tests := []struct { 168 | input string 169 | output int 170 | }{ 171 | {"f", 1}, 172 | {"B", -2}, 173 | {"alseifjlfji", -1}, 174 | {"AB", -2}, 175 | {"aBc", 6}, 176 | {"AbCd", 5}, 177 | {"q", 7}, 178 | {"jo", 3}, 179 | } 180 | 181 | for _, test := range tests { 182 | result := BestMatch(test.input, searchList) 183 | if result != test.output { 184 | t.Errorf("BestMatch(%v) == %v, want %v", test.input, result, test.output) 185 | } 186 | } 187 | } 188 | 189 | func Test_Argify(t *testing.T) { 190 | tests := []struct { 191 | input string 192 | output1 string 193 | output2 string 194 | }{ 195 | {"", "", ""}, 196 | {"test", "test", ""}, 197 | {"test two", "test", "two"}, 198 | {"test one two", "test", "one two"}, 199 | {"this is a somewhat longer test that should also work", 200 | "this", "is a somewhat longer test that should also work"}, 201 | } 202 | 203 | for _, test := range tests { 204 | result1, result2 := Argify(test.input) 205 | 206 | if result1 != test.output1 || reflect.DeepEqual(result2, test.output2) == false { 207 | t.Errorf("Argify(%v) == %v, %v. Want %v, %v", test.input, result1, result2, test.output1, test.output2) 208 | } 209 | } 210 | } 211 | 212 | func Test_TrimUpperRows(t *testing.T) { 213 | emptyRow1 := " " 214 | emptyRow2 := " " 215 | emptyRow3 := "" 216 | 217 | nonEmptyRow1 := "A" 218 | nonEmptyRow2 := "A " 219 | nonEmptyRow3 := "A B" 220 | nonEmptyRow4 := " B" 221 | nonEmptyRow5 := " C " 222 | 223 | tests := []struct { 224 | input []string 225 | output []string 226 | }{ 227 | {[]string{}, []string{}}, 228 | {[]string{nonEmptyRow1}, []string{nonEmptyRow1}}, 229 | {[]string{emptyRow1}, []string{}}, 230 | {[]string{emptyRow1, emptyRow2, emptyRow3}, []string{}}, 231 | {[]string{nonEmptyRow4, nonEmptyRow3, nonEmptyRow2}, []string{nonEmptyRow4, nonEmptyRow3, nonEmptyRow2}}, 232 | {[]string{emptyRow3, nonEmptyRow5}, []string{nonEmptyRow5}}, 233 | {[]string{nonEmptyRow1, nonEmptyRow2, emptyRow1, emptyRow2, emptyRow3}, []string{nonEmptyRow1, nonEmptyRow2, emptyRow1, emptyRow2, emptyRow3}}, 234 | } 235 | 236 | for _, test := range tests { 237 | result := TrimUpperRows(test.input) 238 | 239 | if reflect.DeepEqual(result, test.output) == false { 240 | t.Errorf("TrimUpperRows(\n%v) == \n%v,\nWanted: \n%v", test.input, result, test.output) 241 | } 242 | } 243 | } 244 | 245 | func Test_TrimLowerRows(t *testing.T) { 246 | emptyRow1 := " " 247 | emptyRow2 := " " 248 | emptyRow3 := "" 249 | 250 | nonEmptyRow1 := "A" 251 | nonEmptyRow2 := "A " 252 | nonEmptyRow3 := "A B" 253 | nonEmptyRow4 := " B" 254 | nonEmptyRow5 := " C " 255 | 256 | tests := []struct { 257 | input []string 258 | output []string 259 | }{ 260 | {[]string{}, []string{}}, 261 | {[]string{nonEmptyRow1}, []string{nonEmptyRow1}}, 262 | {[]string{emptyRow1}, []string{}}, 263 | {[]string{emptyRow1, emptyRow2, emptyRow3}, []string{}}, 264 | {[]string{nonEmptyRow4, nonEmptyRow3, nonEmptyRow2}, []string{nonEmptyRow4, nonEmptyRow3, nonEmptyRow2}}, 265 | {[]string{emptyRow3, nonEmptyRow5}, []string{emptyRow3, nonEmptyRow5}}, 266 | {[]string{nonEmptyRow1, nonEmptyRow2, emptyRow1, emptyRow2, emptyRow3}, []string{nonEmptyRow1, nonEmptyRow2}}, 267 | } 268 | 269 | for _, test := range tests { 270 | result := TrimLowerRows(test.input) 271 | 272 | if reflect.DeepEqual(result, test.output) == false { 273 | t.Errorf("TrimLowerRows(\n%v) == \n%v,\nWanted: \n%v", test.input, result, test.output) 274 | } 275 | } 276 | } 277 | 278 | func Test_TrimEmptyRows(t *testing.T) { 279 | emptyRow1 := " " 280 | emptyRow2 := " " 281 | emptyRow3 := "" 282 | 283 | nonEmptyRow1 := "A" 284 | nonEmptyRow2 := "A " 285 | nonEmptyRow3 := "A B" 286 | nonEmptyRow4 := " B" 287 | nonEmptyRow5 := " C " 288 | 289 | NL := "\r\n" 290 | 291 | tests := []struct { 292 | input string 293 | output string 294 | }{ 295 | {"", ""}, 296 | {strings.Join([]string{nonEmptyRow1}, NL), 297 | strings.Join([]string{nonEmptyRow1}, NL)}, 298 | {strings.Join([]string{emptyRow1}, NL), 299 | strings.Join([]string{}, NL)}, 300 | {strings.Join([]string{emptyRow1, emptyRow2, emptyRow3}, NL), 301 | strings.Join([]string{}, NL)}, 302 | {strings.Join([]string{nonEmptyRow4, nonEmptyRow3, nonEmptyRow2}, NL), 303 | strings.Join([]string{nonEmptyRow4, nonEmptyRow3, nonEmptyRow2}, NL)}, 304 | {strings.Join([]string{emptyRow3, nonEmptyRow5}, NL), 305 | strings.Join([]string{nonEmptyRow5}, NL)}, 306 | {strings.Join([]string{nonEmptyRow1, nonEmptyRow2, emptyRow1, emptyRow2, emptyRow3}, NL), 307 | strings.Join([]string{nonEmptyRow1, nonEmptyRow2}, NL)}, 308 | {strings.Join([]string{emptyRow1, emptyRow2, emptyRow3, nonEmptyRow1, nonEmptyRow2, emptyRow1, emptyRow2, emptyRow3}, NL), 309 | strings.Join([]string{nonEmptyRow1, nonEmptyRow2}, NL)}, 310 | } 311 | 312 | for i, test := range tests { 313 | result := TrimEmptyRows(test.input) 314 | 315 | if result != test.output { 316 | t.Errorf("%v: TrimEmptyRows(\n%v) == \n%v,\nWanted: \n%v", i, test.input, result, test.output) 317 | } 318 | } 319 | } 320 | 321 | func Test_Random(t *testing.T) { 322 | tests := []struct { 323 | low int 324 | high int 325 | }{ 326 | {0, 0}, 327 | {0, 1}, 328 | {-10, 0}, 329 | {1, 2}, 330 | {1000, 2000}, 331 | } 332 | 333 | for i := 0; i < 100; i++ { 334 | rand.Seed(int64(i)) 335 | for _, test := range tests { 336 | result := Random(test.low, test.high) 337 | 338 | if result < test.low || result > test.high { 339 | t.Errorf("Random number was out of range %v-%v, got %v", test.low, test.high, result) 340 | } 341 | } 342 | } 343 | } 344 | 345 | func Test_Atois(t *testing.T) { 346 | tests := []struct { 347 | input []string 348 | output []int 349 | err error 350 | }{ 351 | {[]string{"0"}, []int{0}, nil}, 352 | {[]string{"0", "1", "3"}, []int{0, 1, 3}, nil}, 353 | {[]string{"asdf"}, []int{}, errors.New("not nil")}, 354 | {[]string{"1", "2", "asdf"}, []int{}, errors.New("not nil")}, 355 | } 356 | 357 | for _, test := range tests { 358 | output, err := Atois(test.input) 359 | if (err == nil && test.err != nil) || (err != nil && test.err == nil) { 360 | t.Errorf("Error flags did not match: %v, got %v", test.input, err) 361 | } else if err == nil { 362 | if len(output) != len(test.output) { 363 | t.Errorf("Mismatched length: %v, %v", output, test.output) 364 | } else { 365 | for i, o := range output { 366 | if o != test.output[i] { 367 | t.Errorf("Expected: %v, got: %v", test.output, output) 368 | } 369 | } 370 | } 371 | } 372 | } 373 | } 374 | 375 | func Test_Atoir(t *testing.T) { 376 | tests := []struct { 377 | input string 378 | min int 379 | max int 380 | output int 381 | err error 382 | }{ 383 | {"1", 0, 10, 1, nil}, 384 | {"asdf", 0, 10, 0, errors.New("error")}, 385 | {"999999", -5, 5, 0, errors.New("error")}, 386 | {"-10", -5, 5, 0, errors.New("error")}, 387 | {"-3", -5, 5, -3, nil}, 388 | {"3", -5, 5, 3, nil}, 389 | } 390 | 391 | for _, test := range tests { 392 | output, err := Atoir(test.input, test.min, test.max) 393 | if (err == nil && test.err != nil) || (err != nil && test.err == nil) { 394 | t.Errorf("Error flags did not match: %v, got %v", test.input, err) 395 | } else if err == nil { 396 | if test.output != output { 397 | t.Errorf("Expected %v, got %v", test.output, output) 398 | } 399 | } 400 | } 401 | } 402 | 403 | func Test_Min(t *testing.T) { 404 | tests := []struct { 405 | x int 406 | y int 407 | output int 408 | }{ 409 | {0, 1, 0}, 410 | {-10, 10, -10}, 411 | {100, 200, 100}, 412 | {200, 100, 100}, 413 | {0, 0, 0}, 414 | {1, 1, 1}, 415 | {-1, -1, -1}, 416 | } 417 | 418 | for _, test := range tests { 419 | output := Min(test.x, test.y) 420 | if output != test.output { 421 | t.Errorf("Min(%v, %v) = %v, expected %v", test.x, test.y, output, test.output) 422 | } 423 | } 424 | } 425 | 426 | func Test_Max(t *testing.T) { 427 | tests := []struct { 428 | x int 429 | y int 430 | output int 431 | }{ 432 | {0, 1, 1}, 433 | {-10, 10, 10}, 434 | {100, 200, 200}, 435 | {200, 100, 200}, 436 | {0, 0, 0}, 437 | {1, 1, 1}, 438 | {-1, -1, -1}, 439 | } 440 | 441 | for _, test := range tests { 442 | output := Max(test.x, test.y) 443 | if output != test.output { 444 | t.Errorf("Max(%v, %v) = %v, expected %v", test.x, test.y, output, test.output) 445 | } 446 | } 447 | } 448 | 449 | func Test_Bound(t *testing.T) { 450 | tests := []struct { 451 | input int 452 | lower int 453 | upper int 454 | output int 455 | }{ 456 | {5, 0, 10, 5}, 457 | {-10, 0, 10, 0}, 458 | {20, 0, 10, 10}, 459 | {-15, -20, -10, -15}, 460 | {19, 10, 20, 19}, 461 | } 462 | 463 | for _, test := range tests { 464 | output := Bound(test.input, test.lower, test.upper) 465 | if output != test.output { 466 | t.Errorf("Bound(%v, %v, %v) = %v, expected %v", test.input, test.lower, test.upper, output, test.output) 467 | } 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /session/actions.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Cristofori/kmud/combat" 8 | "github.com/Cristofori/kmud/events" 9 | "github.com/Cristofori/kmud/model" 10 | "github.com/Cristofori/kmud/types" 11 | "github.com/Cristofori/kmud/utils" 12 | ) 13 | 14 | type action struct { 15 | alias string 16 | exec func(*Session, string) 17 | } 18 | 19 | func aAlias(name string) action { 20 | return action{alias: name} 21 | } 22 | 23 | var actions = map[string]action{ 24 | "l": aAlias("look"), 25 | "look": { 26 | exec: func(s *Session, arg string) { 27 | if arg == "" { 28 | s.PrintRoom() 29 | } else { 30 | dir := types.StringToDirection(arg) 31 | 32 | if dir == types.DirectionNone { 33 | charList := model.CharactersIn(s.pc.GetRoomId()) 34 | index := utils.BestMatch(arg, charList.Names()) 35 | 36 | if index == -2 { 37 | s.printError("Which one do you mean?") 38 | } else if index != -1 { 39 | char := charList[index] 40 | s.WriteLinef("Looking at: %s", char.GetName()) 41 | s.WriteLinef(" Health: %v/%v", char.GetHitPoints(), char.GetHealth()) 42 | } else { 43 | itemList := model.ItemsIn(s.GetRoom().GetId()) 44 | index = utils.BestMatch(arg, itemList.Names()) 45 | 46 | if index == -1 { 47 | s.WriteLine("Nothing to see") 48 | } else if index == -2 { 49 | s.printError("Which one do you mean?") 50 | } else { 51 | item := itemList[index] 52 | s.WriteLinef("Looking at: %s", item.GetName()) 53 | contents := model.ItemsIn(item.GetId()) 54 | if len(contents) > 0 { 55 | s.WriteLinef("Contents: %s", strings.Join(contents.Names(), ", ")) 56 | } else { 57 | s.WriteLine("(empty)") 58 | } 59 | } 60 | } 61 | } else { 62 | if s.GetRoom().HasExit(dir) { 63 | loc := s.GetRoom().NextLocation(dir) 64 | roomToSee := model.GetRoomByLocation(loc, s.GetRoom().GetZoneId()) 65 | if roomToSee != nil { 66 | s.printRoom(roomToSee) 67 | } else { 68 | s.WriteLine("Nothing to see") 69 | } 70 | } else { 71 | s.printError("You can't look in that direction") 72 | } 73 | } 74 | } 75 | }, 76 | }, 77 | "a": aAlias("attack"), 78 | "attack": { 79 | exec: func(s *Session, arg string) { 80 | charList := model.CharactersIn(s.pc.GetRoomId()) 81 | index := utils.BestMatch(arg, charList.Names()) 82 | 83 | if index == -1 { 84 | s.printError("Not found") 85 | } else if index == -2 { 86 | s.printError("Which one do you mean?") 87 | } else { 88 | defender := charList[index] 89 | if defender.GetId() == s.pc.GetId() { 90 | s.printError("You can't attack yourself") 91 | } else { 92 | combat.StartFight(s.pc, nil, defender) 93 | } 94 | } 95 | }, 96 | }, 97 | "c": aAlias("cast"), 98 | "cast": { 99 | exec: func(s *Session, arg string) { 100 | usage := func() { 101 | s.printError("Usage: cast ") 102 | } 103 | 104 | spell, targetName := utils.Argify(arg) 105 | 106 | if spell == "" || targetName == "" { 107 | usage() 108 | return 109 | } 110 | 111 | var skill types.Skill 112 | skills := model.GetSkills(s.pc.GetSkills()) 113 | index := utils.BestMatch(spell, skills.Names()) 114 | 115 | if index == -1 { 116 | s.printError("Skill not found") 117 | } else if index == -2 { 118 | s.printError("Which skill do you mean?") 119 | } else { 120 | skill = skills[index] 121 | } 122 | 123 | if skill != nil { 124 | var target types.Character 125 | 126 | charList := model.CharactersIn(s.pc.GetRoomId()) 127 | index := utils.BestMatch(targetName, charList.Names()) 128 | 129 | if index == -1 { 130 | s.printError("Target not found") 131 | } else if index == -2 { 132 | s.printError("Which target do you mean?") 133 | } else { 134 | target = charList[index] 135 | } 136 | 137 | if target != nil { 138 | s.WriteLineColor(types.ColorRed, "Casting %s on %s", skill.GetName(), target.GetName()) 139 | combat.StartFight(s.pc, skill, target) 140 | } 141 | } 142 | }, 143 | }, 144 | "sb": aAlias("skillbook"), 145 | "skillbook": { 146 | exec: func(s *Session, arg string) { 147 | s.execMenu("Skill Book", func(menu *utils.Menu) { 148 | menu.AddAction("a", "Add", func() { 149 | s.execMenu("Select a skill to add", func(menu *utils.Menu) { 150 | for i, skill := range model.GetAllSkills() { 151 | sk := skill 152 | menu.AddActionI(i, skill.GetName(), func() { 153 | s.pc.AddSkill(sk.GetId()) 154 | }) 155 | } 156 | }) 157 | }) 158 | 159 | skills := model.GetSkills(s.pc.GetSkills()) 160 | for i, skill := range skills { 161 | sk := skill 162 | menu.AddActionI(i, skill.GetName(), func() { 163 | s.WriteLine("Skill: %v", sk.GetName()) 164 | }) 165 | } 166 | }) 167 | }, 168 | }, 169 | "talk": { 170 | exec: func(s *Session, arg string) { 171 | if arg == "" { 172 | s.printError("Usage: talk ") 173 | return 174 | } 175 | 176 | npcList := model.NpcsIn(s.pc.GetRoomId()) 177 | index := utils.BestMatch(arg, npcList.Characters().Names()) 178 | 179 | if index == -1 { 180 | s.printError("Not found") 181 | } else if index == -2 { 182 | s.printError("Which one do you mean?") 183 | } else { 184 | npc := npcList[index] 185 | s.WriteLine(npc.PrettyConversation()) 186 | } 187 | }, 188 | }, 189 | "drop": { 190 | exec: func(s *Session, arg string) { 191 | dropUsage := func() { 192 | s.printError("Usage: drop ") 193 | } 194 | 195 | if arg == "" { 196 | dropUsage() 197 | return 198 | } 199 | 200 | characterItems := model.ItemsIn(s.pc.GetId()) 201 | index := utils.BestMatch(arg, characterItems.Names()) 202 | 203 | if index == -1 { 204 | s.printError("Not found") 205 | } else if index == -2 { 206 | s.printError("Which one do you mean?") 207 | } else { 208 | item := characterItems[index] 209 | if item.SetContainerId(s.GetRoom().GetId(), s.pc.GetId()) { 210 | s.WriteLine("Dropped %s", item.GetName()) 211 | } else { 212 | s.printError("Not found") 213 | } 214 | } 215 | }, 216 | }, 217 | "take": aAlias("get"), 218 | "t": aAlias("get"), 219 | "g": aAlias("g"), 220 | "get": { 221 | exec: func(s *Session, arg string) { 222 | takeUsage := func() { 223 | s.printError("Usage: take ") 224 | } 225 | 226 | if arg == "" { 227 | takeUsage() 228 | return 229 | } 230 | 231 | itemsInRoom := model.ItemsIn(s.GetRoom().GetId()) 232 | index := utils.BestMatch(arg, itemsInRoom.Names()) 233 | 234 | if index == -2 { 235 | s.printError("Which one do you mean?") 236 | } else if index == -1 { 237 | s.printError("Not found") 238 | } else { 239 | item := itemsInRoom[index] 240 | if item.SetContainerId(s.pc.GetId(), s.GetRoom().GetId()) { 241 | s.WriteLine("Picked up %s", item.GetName()) 242 | } else { 243 | s.printError("Not found") 244 | } 245 | } 246 | }, 247 | }, 248 | "i": aAlias("inventory"), 249 | "inv": aAlias("inventory"), 250 | "inventory": { 251 | exec: func(s *Session, arg string) { 252 | items := model.ItemsIn(s.pc.GetId()) 253 | 254 | if len(items) == 0 { 255 | s.WriteLinef("You aren't carrying anything") 256 | } else { 257 | names := make([]string, len(items)) 258 | for i, item := range items { 259 | template := model.GetTemplate(item.GetTemplateId()) 260 | names[i] = fmt.Sprintf("%s (%v)", item.GetName(), template.GetWeight()) 261 | } 262 | s.WriteLinef("You are carrying: %s", strings.Join(names, ", ")) 263 | } 264 | 265 | s.WriteLinef("Cash: %v", s.pc.GetCash()) 266 | s.WriteLinef("Weight: %v/%v", model.CharacterWeight(s.pc), s.pc.GetCapacity()) 267 | }, 268 | }, 269 | "help": { 270 | exec: func(s *Session, arg string) { 271 | s.WriteLine("HELP!") 272 | }, 273 | }, 274 | "ls": { 275 | exec: func(s *Session, arg string) { 276 | s.WriteLine("Where do you think you are?!") 277 | }, 278 | }, 279 | "stop": { 280 | exec: func(s *Session, arg string) { 281 | combat.StopFight(s.pc) 282 | }, 283 | }, 284 | "go": { 285 | exec: func(s *Session, arg string) { 286 | if arg == "" { 287 | s.printError("Usage: go ") 288 | return 289 | } 290 | 291 | links := s.GetRoom().GetLinks() 292 | linkNames := s.GetRoom().LinkNames() 293 | index := utils.BestMatch(arg, linkNames) 294 | 295 | if index == -2 { 296 | s.printError("Which one do you mean?") 297 | } else if index == -1 { 298 | s.printError("Exit %s not found", arg) 299 | } else { 300 | destId := links[linkNames[index]] 301 | newRoom := model.GetRoom(destId) 302 | model.MoveCharacterToRoom(s.pc, newRoom) 303 | s.PrintRoom() 304 | } 305 | }, 306 | }, 307 | "lock": { 308 | exec: func(s *Session, arg string) { 309 | if arg == "" { 310 | s.printError("Usage: lock ") 311 | } 312 | handleLock(s, arg, true) 313 | }, 314 | }, 315 | "unlock": { 316 | exec: func(s *Session, arg string) { 317 | if arg == "" { 318 | s.printError("Usage: unlock ") 319 | } 320 | handleLock(s, arg, false) 321 | }, 322 | }, 323 | "store": aAlias("shop"), 324 | "buy": aAlias("shop"), 325 | "sell": aAlias("shop"), 326 | "shop": { 327 | exec: func(s *Session, arg string) { 328 | store := model.StoreIn(s.pc.GetRoomId()) 329 | if store == nil { 330 | s.printError("There is no store here") 331 | return 332 | } 333 | 334 | s.execMenu("", func(menu *utils.Menu) { 335 | menu.SetTitle(fmt.Sprintf("%s - $%v", store.GetName(), store.GetCash())) 336 | menu.AddAction("b", "Buy", func() { 337 | if model.CountItemsIn(store.GetId()) == 0 { 338 | s.printError("This store has nothing to sell") 339 | } else { 340 | s.execMenu("Buy Items", func(menu *utils.Menu) { 341 | items := model.ItemsIn(store.GetId()) 342 | for i, item := range items { 343 | menu.AddActionI(i, item.GetName(), func() { 344 | confirmed := s.getConfirmation(fmt.Sprintf("Buy %s for %v? ", item.GetName(), item.GetValue())) 345 | if confirmed && sellItem(s, store, s.pc, item) { 346 | s.WriteLineColor(types.ColorGreen, "Bought %s", item.GetName()) 347 | } 348 | if len(model.ItemsIn(store.GetId())) == 0 { 349 | menu.Exit() 350 | } 351 | }) 352 | } 353 | }) 354 | } 355 | }) 356 | 357 | menu.AddAction("s", "Sell", func() { 358 | if model.CountItemsIn(s.pc.GetId()) == 0 { 359 | s.printError("You have nothing to sell") 360 | } else { 361 | s.execMenu("Sell Items", func(menu *utils.Menu) { 362 | items := model.ItemsIn(s.pc.GetId()) 363 | for i, item := range items { 364 | menu.AddActionI(i, item.GetName(), func() { 365 | confirmed := s.getConfirmation(fmt.Sprintf("Sell %s for %v? ", item.GetName(), item.GetValue())) 366 | if confirmed && sellItem(s, s.pc, store, item) { 367 | s.WriteLineColor(types.ColorGreen, "Sold %s", item.GetName()) 368 | } 369 | if len(model.ItemsIn(s.pc.GetId())) == 0 { 370 | menu.Exit() 371 | } 372 | }) 373 | } 374 | }) 375 | } 376 | }) 377 | }) 378 | }, 379 | }, 380 | "o": aAlias("open"), 381 | "open": { 382 | exec: func(s *Session, arg string) { 383 | items := model.ItemsIn(s.GetRoom().GetId()) 384 | containers := types.ItemList{} 385 | 386 | for _, item := range items { 387 | if item.GetCapacity() > 0 { 388 | containers = append(containers, item) 389 | } 390 | } 391 | 392 | if len(containers) == 0 { 393 | s.printError("There's nothing here to open") 394 | } else { 395 | index := utils.BestMatch(arg, containers.Names()) 396 | 397 | if index == -2 { 398 | s.printError("Which one do you mean?") 399 | } else if index != -1 { 400 | container := containers[index] 401 | 402 | s.execMenu(container.GetName(), func(menu *utils.Menu) { 403 | menu.AddAction("d", "Deposit", func() { 404 | if model.CountItemsIn(s.pc.GetId()) == 0 { 405 | s.printError("You have nothing to deposit") 406 | } else { 407 | s.execMenu(fmt.Sprintf("Deposit into %s", container.GetName()), func(menu *utils.Menu) { 408 | for i, item := range model.ItemsIn(s.pc.GetId()) { 409 | locItem := item 410 | menu.AddActionI(i, item.GetName(), func() { 411 | if locItem.SetContainerId(container.GetId(), s.pc.GetId()) { 412 | s.WriteLine("Item deposited") 413 | } else { 414 | s.printError("Failed to deposit item") 415 | } 416 | if model.CountItemsIn(s.pc.GetId()) == 0 { 417 | menu.Exit() 418 | } 419 | }) 420 | } 421 | }) 422 | } 423 | }) 424 | 425 | for i, item := range model.ItemsIn(container.GetId()) { 426 | locItem := item 427 | menu.AddActionI(i, item.GetName(), func() { 428 | if locItem.SetContainerId(s.pc.GetId(), container.GetId()) { 429 | s.WriteLine("Took %s from %s", locItem.GetName(), container.GetName()) 430 | } else { 431 | s.printError("Failed to take item") 432 | } 433 | }) 434 | } 435 | }) 436 | } 437 | } 438 | }, 439 | }, 440 | } 441 | 442 | func handleLock(s *Session, arg string, locked bool) { 443 | dir := types.StringToDirection(arg) 444 | 445 | if dir == types.DirectionNone { 446 | s.printError("Invalid direction") 447 | } else { 448 | s.GetRoom().SetLocked(dir, locked) 449 | 450 | events.Broadcast(events.LockEvent{ 451 | RoomId: s.pc.GetRoomId(), 452 | Exit: dir, 453 | Locked: locked, 454 | }) 455 | 456 | // Lock on both sides 457 | location := s.GetRoom().NextLocation(dir) 458 | otherRoom := model.GetRoomByLocation(location, s.GetRoom().GetZoneId()) 459 | 460 | if otherRoom != nil { 461 | otherRoom.SetLocked(dir.Opposite(), locked) 462 | 463 | events.Broadcast(events.LockEvent{ 464 | RoomId: otherRoom.GetId(), 465 | Exit: dir.Opposite(), 466 | Locked: locked, 467 | }) 468 | } 469 | } 470 | } 471 | 472 | func sellItem(s *Session, seller types.Purchaser, buyer types.Purchaser, item types.Item) bool { 473 | // TODO - Transferring the item and money needs be guarded in some kind of atomic transaction 474 | if item.SetContainerId(buyer.GetId(), seller.GetId()) { 475 | buyer.RemoveCash(item.GetValue()) 476 | seller.AddCash(item.GetValue()) 477 | return true 478 | } else { 479 | s.printError("Transaction failed") 480 | } 481 | 482 | return false 483 | } 484 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | 8 | db "github.com/Cristofori/kmud/database" 9 | "github.com/Cristofori/kmud/events" 10 | "github.com/Cristofori/kmud/types" 11 | "github.com/Cristofori/kmud/utils" 12 | "gopkg.in/mgo.v2/bson" 13 | ) 14 | 15 | func FindObjectByName(name string, objectType types.ObjectType) types.Id { 16 | return db.FindOne(objectType, bson.M{"name": utils.FormatName(name)}) 17 | } 18 | 19 | func CreateUser(name string, password string, admin bool) types.User { 20 | return db.NewUser(name, password, admin) 21 | } 22 | 23 | func GetUsers() types.UserList { 24 | ids := db.FindAll(types.UserType) 25 | users := make(types.UserList, len(ids)) 26 | 27 | for i, id := range ids { 28 | users[i] = GetUser(id) 29 | } 30 | 31 | return users 32 | } 33 | 34 | func UserCount() int { 35 | return len(db.FindAll(types.UserType)) 36 | } 37 | 38 | func GetUserByName(username string) types.User { 39 | id := FindObjectByName(username, types.UserType) 40 | if id != nil { 41 | return GetUser(id) 42 | } 43 | return nil 44 | } 45 | 46 | func DeleteUser(userId types.Id) { 47 | for _, character := range GetUserCharacters(userId) { 48 | DeleteCharacter(character.GetId()) 49 | } 50 | 51 | db.DeleteObject(userId) 52 | } 53 | 54 | func GetPlayerCharacter(id types.Id) types.PC { 55 | return db.Retrieve(id, types.PcType).(types.PC) 56 | } 57 | 58 | func GetNpc(id types.Id) types.NPC { 59 | return db.Retrieve(id, types.NpcType).(types.NPC) 60 | } 61 | 62 | func GetCharacterByName(name string) types.Character { 63 | char := GetPlayerCharacterByName(name) 64 | 65 | if char != nil { 66 | return char 67 | } 68 | 69 | npc := GetNpcByName(name) 70 | 71 | if npc != nil { 72 | return npc 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func GetPlayerCharacterByName(name string) types.PC { 79 | id := FindObjectByName(name, types.PcType) 80 | if id != nil { 81 | return GetPlayerCharacter(id) 82 | } 83 | return nil 84 | } 85 | 86 | func GetNpcByName(name string) types.NPC { 87 | id := FindObjectByName(name, types.NpcType) 88 | if id != nil { 89 | return GetNpc(id) 90 | } 91 | return nil 92 | } 93 | 94 | func GetNpcs() types.NPCList { 95 | ids := db.FindAll(types.NpcType) 96 | npcs := make(types.NPCList, len(ids)) 97 | 98 | for i, id := range ids { 99 | npcs[i] = GetNpc(id) 100 | } 101 | 102 | return npcs 103 | } 104 | 105 | func GetUserCharacters(userId types.Id) types.PCList { 106 | ids := db.Find(types.PcType, bson.M{"userid": userId}) 107 | pcs := make(types.PCList, len(ids)) 108 | 109 | for i, id := range ids { 110 | pcs[i] = GetPlayerCharacter(id) 111 | } 112 | 113 | return pcs 114 | } 115 | 116 | func CharactersIn(roomId types.Id) types.CharacterList { 117 | var characters types.CharacterList 118 | 119 | players := PlayerCharactersIn(roomId, nil) 120 | npcs := NpcsIn(roomId) 121 | 122 | characters = append(characters, players.Characters()...) 123 | characters = append(characters, npcs.Characters()...) 124 | 125 | return characters 126 | } 127 | 128 | func PlayerCharactersIn(roomId types.Id, except types.Id) types.PCList { 129 | ids := db.Find(types.PcType, bson.M{"roomid": roomId}) 130 | var pcs types.PCList 131 | 132 | for _, id := range ids { 133 | pc := GetPlayerCharacter(id) 134 | 135 | if pc.IsOnline() && id != except { 136 | pcs = append(pcs, pc) 137 | } 138 | } 139 | 140 | return pcs 141 | } 142 | 143 | func NpcsIn(roomId types.Id) types.NPCList { 144 | ids := db.Find(types.NpcType, bson.M{"roomid": roomId}) 145 | npcs := make(types.NPCList, len(ids)) 146 | 147 | for i, id := range ids { 148 | npcs[i] = GetNpc(id) 149 | } 150 | 151 | return npcs 152 | } 153 | 154 | func GetOnlinePlayerCharacters() []types.PC { 155 | var pcs []types.PC 156 | 157 | for _, id := range db.FindAll(types.PcType) { 158 | pc := GetPlayerCharacter(id) 159 | if pc.IsOnline() { 160 | pcs = append(pcs, pc) 161 | } 162 | } 163 | 164 | return pcs 165 | } 166 | 167 | func CreatePlayerCharacter(name string, userId types.Id, startingRoom types.Room) types.PC { 168 | pc := db.NewPc(name, userId, startingRoom.GetId()) 169 | events.Broadcast(events.EnterEvent{Character: pc, RoomId: startingRoom.GetId(), Direction: types.DirectionNone}) 170 | return pc 171 | } 172 | 173 | func GetOrCreatePlayerCharacter(name string, userId types.Id, startingRoom types.Room) types.PC { 174 | player := GetPlayerCharacterByName(name) 175 | npc := GetNpcByName(name) 176 | 177 | if player == nil && npc == nil { 178 | player = CreatePlayerCharacter(name, userId, startingRoom) 179 | } else if npc != nil { 180 | return nil 181 | } 182 | 183 | return player 184 | } 185 | 186 | func CreateNpc(name string, roomId types.Id, spawnerId types.Id) types.NPC { 187 | npc := db.NewNpc(name, roomId, spawnerId) 188 | events.Broadcast(events.EnterEvent{Character: npc, RoomId: roomId, Direction: types.DirectionNone}) 189 | return npc 190 | } 191 | 192 | func DeleteCharacter(charId types.Id) { 193 | deleteContainer(charId) 194 | } 195 | 196 | func CreateRoom(zone types.Zone, location types.Coordinate) (types.Room, error) { 197 | existingRoom := GetRoomByLocation(location, zone.GetId()) 198 | if existingRoom != nil { 199 | return nil, errors.New("A room already exists at that location") 200 | } 201 | 202 | return db.NewRoom(zone.GetId(), location), nil 203 | } 204 | 205 | func GetRoom(id types.Id) types.Room { 206 | return db.Retrieve(id, types.RoomType).(types.Room) 207 | } 208 | 209 | func GetRooms() types.RoomList { 210 | ids := db.FindAll(types.RoomType) 211 | rooms := make(types.RoomList, len(ids)) 212 | 213 | for i, id := range ids { 214 | rooms[i] = GetRoom(id) 215 | } 216 | 217 | return rooms 218 | } 219 | 220 | func GetRoomsInZone(zoneId types.Id) types.RoomList { 221 | zone := GetZone(zoneId) 222 | ids := db.Find(types.RoomType, bson.M{"zoneid": zone.GetId()}) 223 | rooms := make(types.RoomList, len(ids)) 224 | 225 | for i, id := range ids { 226 | rooms[i] = GetRoom(id) 227 | } 228 | 229 | return rooms 230 | } 231 | 232 | func GetRoomByLocation(coordinate types.Coordinate, zoneId types.Id) types.Room { 233 | id := db.FindOne(types.RoomType, bson.M{ 234 | "zoneid": zoneId, 235 | "location": coordinate, 236 | }) 237 | if id != nil { 238 | return GetRoom(id) 239 | } 240 | return nil 241 | } 242 | 243 | func GetNeighbors(room types.Room) []types.Room { 244 | neighbors := []types.Room{} 245 | 246 | for _, dir := range room.GetExits() { 247 | coords := room.NextLocation(dir) 248 | neighbor := GetRoomByLocation(coords, room.GetZoneId()) 249 | if neighbor != nil { 250 | neighbors = append(neighbors, neighbor) 251 | } 252 | } 253 | 254 | for _, id := range room.GetLinks() { 255 | neighbor := GetRoom(id) 256 | if neighbor != nil { 257 | neighbors = append(neighbors, neighbor) 258 | } 259 | } 260 | 261 | return neighbors 262 | } 263 | 264 | func GetZone(id types.Id) types.Zone { 265 | return db.Retrieve(id, types.ZoneType).(types.Zone) 266 | } 267 | 268 | func GetZones() types.ZoneList { 269 | ids := db.FindAll(types.ZoneType) 270 | zones := make(types.ZoneList, len(ids)) 271 | 272 | for i, id := range ids { 273 | zones[i] = GetZone(id) 274 | } 275 | 276 | return zones 277 | } 278 | 279 | func CreateZone(name string) (types.Zone, error) { 280 | if GetZoneByName(name) != nil { 281 | return nil, errors.New("A zone with that name already exists") 282 | } 283 | 284 | return db.NewZone(name), nil 285 | } 286 | 287 | func DeleteZone(zoneId types.Id) { 288 | rooms := GetRoomsInZone(zoneId) 289 | 290 | for _, room := range rooms { 291 | DeleteRoom(room) 292 | } 293 | 294 | db.DeleteObject(zoneId) 295 | } 296 | 297 | func GetZoneByName(name string) types.Zone { 298 | id := FindObjectByName(name, types.ZoneType) 299 | if id != nil { 300 | return GetZone(id) 301 | } 302 | return nil 303 | } 304 | 305 | func GetAreas(zone types.Zone) types.AreaList { 306 | ids := db.FindAll(types.AreaType) 307 | areas := make(types.AreaList, len(ids)) 308 | for i, id := range ids { 309 | areas[i] = GetArea(id) 310 | } 311 | 312 | return areas 313 | } 314 | 315 | func GetArea(id types.Id) types.Area { 316 | return db.Retrieve(id, types.AreaType).(types.Area) 317 | } 318 | 319 | func CreateArea(name string, zone types.Zone) (types.Area, error) { 320 | if GetAreaByName(name) != nil { 321 | return nil, errors.New("An area with that name already exists") 322 | } 323 | 324 | return db.NewArea(name, zone.GetId()), nil 325 | } 326 | 327 | func GetAreaByName(name string) types.Area { 328 | id := FindObjectByName(name, types.AreaType) 329 | if id != nil { 330 | return GetArea(id) 331 | } 332 | return nil 333 | } 334 | 335 | func DeleteArea(areaId types.Id) { 336 | rooms := GetAreaRooms(areaId) 337 | for _, room := range rooms { 338 | room.SetAreaId(nil) 339 | } 340 | 341 | spawners := GetAreaSpawners(areaId) 342 | for _, spawner := range spawners { 343 | db.DeleteObject(spawner.GetId()) 344 | } 345 | 346 | db.DeleteObject(areaId) 347 | } 348 | 349 | func GetAreaRooms(areaId types.Id) types.RoomList { 350 | ids := db.Find(types.RoomType, bson.M{"areaid": areaId}) 351 | rooms := make(types.RoomList, len(ids)) 352 | for i, id := range ids { 353 | rooms[i] = GetRoom(id) 354 | } 355 | return rooms 356 | 357 | } 358 | 359 | func DeleteRoom(room types.Room) { 360 | db.DeleteObject(room.GetId()) 361 | 362 | // Disconnect all exits leading to this room 363 | loc := room.GetLocation() 364 | 365 | updateRoom := func(dir types.Direction) { 366 | next := loc.Next(dir) 367 | room := GetRoomByLocation(next, room.GetZoneId()) 368 | 369 | if room != nil { 370 | room.SetExitEnabled(dir.Opposite(), false) 371 | } 372 | } 373 | 374 | updateRoom(types.DirectionNorth) 375 | updateRoom(types.DirectionNorthEast) 376 | updateRoom(types.DirectionEast) 377 | updateRoom(types.DirectionSouthEast) 378 | updateRoom(types.DirectionSouth) 379 | updateRoom(types.DirectionSouthWest) 380 | updateRoom(types.DirectionWest) 381 | updateRoom(types.DirectionNorthWest) 382 | updateRoom(types.DirectionUp) 383 | updateRoom(types.DirectionDown) 384 | } 385 | 386 | func GetUser(id types.Id) types.User { 387 | return db.Retrieve(id, types.UserType).(types.User) 388 | } 389 | 390 | func CreateTemplate(name string) types.Template { 391 | return db.NewTemplate(name) 392 | } 393 | 394 | func GetAllTemplates() types.TemplateList { 395 | ids := db.FindAll(types.TemplateType) 396 | templates := make(types.TemplateList, len(ids)) 397 | for i, id := range ids { 398 | templates[i] = GetTemplate(id) 399 | } 400 | sort.Sort(templates) 401 | return templates 402 | } 403 | 404 | func GetTemplate(id types.Id) types.Template { 405 | return db.Retrieve(id, types.TemplateType).(types.Template) 406 | } 407 | 408 | func DeleteTemplate(id types.Id) { 409 | DeleteItems(GetTemplateItems(id)) 410 | // db.DeleteObject(id) 411 | } 412 | 413 | func GetTemplateItems(templateId types.Id) types.ItemList { 414 | ids := db.Find(types.ItemType, bson.M{"templateid": templateId}) 415 | items := make(types.ItemList, len(ids)) 416 | for i, id := range ids { 417 | items[i] = db.Retrieve(id, types.ItemType).(types.Item) 418 | } 419 | return items 420 | } 421 | 422 | func CreateItem(templateId types.Id) types.Item { 423 | return db.NewItem(templateId) 424 | } 425 | 426 | func GetItem(id types.Id) types.Item { 427 | return db.Retrieve(id, types.ItemType).(types.Item) 428 | } 429 | 430 | func DeleteItem(itemId types.Id) { 431 | db.DeleteObject(itemId) 432 | } 433 | 434 | func DeleteItems(items types.ItemList) { 435 | for _, item := range items { 436 | DeleteItem(item.GetId()) 437 | } 438 | } 439 | 440 | func ItemsIn(containerId types.Id) types.ItemList { 441 | ids := db.Find(types.ItemType, bson.M{"containerid": containerId}) 442 | items := make(types.ItemList, len(ids)) 443 | 444 | for i, id := range ids { 445 | items[i] = GetItem(id) 446 | } 447 | 448 | return items 449 | } 450 | 451 | func CountItemsIn(containerId types.Id) int { 452 | return len(db.Find(types.ItemType, bson.M{"containerid": containerId})) 453 | } 454 | 455 | func ItemWeight(item types.Item) int { 456 | template := GetTemplate(item.GetTemplateId()) 457 | weight := template.GetWeight() 458 | items := ItemsIn(item.GetId()) 459 | for _, item := range items { 460 | weight += ItemWeight(item) 461 | } 462 | 463 | return weight 464 | } 465 | 466 | func CharacterWeight(character types.Character) int { 467 | items := ItemsIn(character.GetId()) 468 | weight := 0 469 | for _, item := range items { 470 | weight += ItemWeight(item) 471 | } 472 | return weight 473 | } 474 | 475 | func MoveCharacterToRoom(character types.Character, newRoom types.Room) { 476 | oldRoomId := character.GetRoomId() 477 | character.SetRoomId(newRoom.GetId()) 478 | 479 | oldRoom := GetRoom(oldRoomId) 480 | 481 | // Leave 482 | dir := DirectionBetween(oldRoom, newRoom) 483 | events.Broadcast(events.LeaveEvent{Character: character, RoomId: oldRoomId, Direction: dir}) 484 | 485 | // Enter 486 | dir = DirectionBetween(newRoom, oldRoom) 487 | events.Broadcast(events.EnterEvent{Character: character, RoomId: newRoom.GetId(), Direction: dir}) 488 | } 489 | 490 | func MoveCharacter(character types.Character, direction types.Direction) error { 491 | room := GetRoom(character.GetRoomId()) 492 | 493 | if room == nil { 494 | return errors.New("Character doesn't appear to be in any room") 495 | } 496 | 497 | if !room.HasExit(direction) { 498 | return errors.New("Attempted to move through an exit that the room does not contain") 499 | } 500 | 501 | if room.IsLocked(direction) { 502 | return errors.New("That way is locked") 503 | } 504 | 505 | newLocation := room.NextLocation(direction) 506 | newRoom := GetRoomByLocation(newLocation, room.GetZoneId()) 507 | 508 | if newRoom == nil { 509 | zone := GetZone(room.GetZoneId()) 510 | fmt.Printf("No room found at location %v %v, creating a new one (%s)\n", zone.GetName(), newLocation, character.GetName()) 511 | 512 | var err error 513 | newRoom, err = CreateRoom(GetZone(room.GetZoneId()), newLocation) 514 | newRoom.SetTitle(room.GetTitle()) 515 | newRoom.SetDescription(room.GetDescription()) 516 | 517 | if err != nil { 518 | return err 519 | } 520 | 521 | switch direction { 522 | case types.DirectionNorth: 523 | newRoom.SetExitEnabled(types.DirectionSouth, true) 524 | case types.DirectionNorthEast: 525 | newRoom.SetExitEnabled(types.DirectionSouthWest, true) 526 | case types.DirectionEast: 527 | newRoom.SetExitEnabled(types.DirectionWest, true) 528 | case types.DirectionSouthEast: 529 | newRoom.SetExitEnabled(types.DirectionNorthWest, true) 530 | case types.DirectionSouth: 531 | newRoom.SetExitEnabled(types.DirectionNorth, true) 532 | case types.DirectionSouthWest: 533 | newRoom.SetExitEnabled(types.DirectionNorthEast, true) 534 | case types.DirectionWest: 535 | newRoom.SetExitEnabled(types.DirectionEast, true) 536 | case types.DirectionNorthWest: 537 | newRoom.SetExitEnabled(types.DirectionSouthEast, true) 538 | case types.DirectionUp: 539 | newRoom.SetExitEnabled(types.DirectionDown, true) 540 | case types.DirectionDown: 541 | newRoom.SetExitEnabled(types.DirectionUp, true) 542 | default: 543 | panic("Unexpected code path") 544 | } 545 | } 546 | 547 | MoveCharacterToRoom(character, newRoom) 548 | return nil 549 | } 550 | 551 | func BroadcastMessage(from types.Character, message string) { 552 | events.Broadcast(events.BroadcastEvent{Character: from, Message: message}) 553 | } 554 | 555 | func Tell(from types.Character, to types.Character, message string) { 556 | events.Broadcast(events.TellEvent{From: from, To: to, Message: message}) 557 | } 558 | 559 | func Say(from types.Character, message string) { 560 | events.Broadcast(events.SayEvent{Character: from, Message: message}) 561 | } 562 | 563 | func Emote(from types.Character, message string) { 564 | events.Broadcast(events.EmoteEvent{Character: from, Emote: message}) 565 | } 566 | 567 | func Login(character types.PC) { 568 | character.SetOnline(true) 569 | events.Broadcast(events.LoginEvent{Character: character}) 570 | } 571 | 572 | func Logout(character types.PC) { 573 | character.SetOnline(false) 574 | events.Broadcast(events.LogoutEvent{Character: character}) 575 | } 576 | 577 | func ZoneCorners(zone types.Zone) (types.Coordinate, types.Coordinate) { 578 | var top int 579 | var bottom int 580 | var left int 581 | var right int 582 | var high int 583 | var low int 584 | 585 | rooms := GetRoomsInZone(zone.GetId()) 586 | 587 | for _, room := range rooms { 588 | top = room.GetLocation().Y 589 | bottom = room.GetLocation().Y 590 | left = room.GetLocation().X 591 | right = room.GetLocation().X 592 | high = room.GetLocation().Z 593 | low = room.GetLocation().Z 594 | break 595 | } 596 | 597 | for _, room := range rooms { 598 | if room.GetLocation().Z < high { 599 | high = room.GetLocation().Z 600 | } 601 | 602 | if room.GetLocation().Z > low { 603 | low = room.GetLocation().Z 604 | } 605 | 606 | if room.GetLocation().Y < top { 607 | top = room.GetLocation().Y 608 | } 609 | 610 | if room.GetLocation().Y > bottom { 611 | bottom = room.GetLocation().Y 612 | } 613 | 614 | if room.GetLocation().X < left { 615 | left = room.GetLocation().X 616 | } 617 | 618 | if room.GetLocation().X > right { 619 | right = room.GetLocation().X 620 | } 621 | } 622 | 623 | return types.Coordinate{X: left, Y: top, Z: high}, 624 | types.Coordinate{X: right, Y: bottom, Z: low} 625 | } 626 | 627 | func DirectionBetween(from, to types.Room) types.Direction { 628 | for _, exit := range from.GetExits() { 629 | nextLocation := from.NextLocation(exit) 630 | nextRoom := GetRoomByLocation(nextLocation, from.GetZoneId()) 631 | 632 | if nextRoom == to { 633 | return exit 634 | } 635 | } 636 | 637 | return types.DirectionNone 638 | } 639 | 640 | func CreateSpawner(name string, areaId types.Id) types.Spawner { 641 | return db.NewSpawner(name, areaId) 642 | } 643 | 644 | func GetSpawners() types.SpawnerList { 645 | ids := db.FindAll(types.SpawnerType) 646 | spawners := make(types.SpawnerList, len(ids)) 647 | 648 | for i, id := range ids { 649 | spawners[i] = GetSpawner(id) 650 | } 651 | 652 | return spawners 653 | } 654 | 655 | func GetSpawner(id types.Id) types.Spawner { 656 | return db.Retrieve(id, types.SpawnerType).(types.Spawner) 657 | } 658 | 659 | func GetAreaSpawners(areaId types.Id) types.SpawnerList { 660 | ids := db.Find(types.SpawnerType, bson.M{"areaid": areaId}) 661 | spawners := make(types.SpawnerList, len(ids)) 662 | for i, id := range ids { 663 | spawners[i] = GetSpawner(id) 664 | } 665 | return spawners 666 | } 667 | 668 | func GetSpawnerNpcs(spawnerId types.Id) types.NPCList { 669 | ids := db.Find(types.NpcType, bson.M{"spawnerid": spawnerId}) 670 | npcs := make(types.NPCList, len(ids)) 671 | for i, id := range ids { 672 | npcs[i] = GetNpc(id) 673 | } 674 | return npcs 675 | } 676 | 677 | func GetSkill(id types.Id) types.Skill { 678 | return db.Retrieve(id, types.SkillType).(types.Skill) 679 | } 680 | 681 | func GetSkillByName(name string) types.Skill { 682 | id := FindObjectByName(name, types.SkillType) 683 | if id != nil { 684 | return GetSkill(id) 685 | } 686 | return nil 687 | } 688 | 689 | func GetAllSkills() types.SkillList { 690 | ids := db.FindAll(types.SkillType) 691 | skills := make(types.SkillList, len(ids)) 692 | for i, id := range ids { 693 | skills[i] = GetSkill(id) 694 | } 695 | return skills 696 | } 697 | 698 | func GetSkills(SkillIds []types.Id) types.SkillList { 699 | skills := make(types.SkillList, len(SkillIds)) 700 | for i, id := range SkillIds { 701 | skills[i] = GetSkill(id) 702 | } 703 | return skills 704 | } 705 | 706 | func CreateSkill(name string) types.Skill { 707 | return db.NewSkill(name) 708 | } 709 | 710 | func DeleteSkill(id types.Id) { 711 | db.DeleteObject(id) 712 | } 713 | 714 | func CreateEffect(name string) types.Effect { 715 | return db.NewEffect(name) 716 | } 717 | 718 | func DeleteEffect(id types.Id) { 719 | db.DeleteObject(id) 720 | } 721 | 722 | func GetAllEffects() types.EffectList { 723 | ids := db.FindAll(types.EffectType) 724 | effects := make(types.EffectList, len(ids)) 725 | for i, id := range ids { 726 | effects[i] = GetEffect(id) 727 | } 728 | return effects 729 | } 730 | 731 | func GetEffects(EffectIds []types.Id) types.EffectList { 732 | effects := make(types.EffectList, len(EffectIds)) 733 | for i, id := range EffectIds { 734 | effects[i] = GetEffect(id) 735 | } 736 | return effects 737 | } 738 | 739 | func GetEffect(id types.Id) types.Effect { 740 | return db.Retrieve(id, types.EffectType).(types.Effect) 741 | } 742 | 743 | func GetEffectByName(name string) types.Effect { 744 | id := FindObjectByName(name, types.EffectType) 745 | if id != nil { 746 | return GetEffect(id) 747 | } 748 | return nil 749 | } 750 | 751 | func StoreIn(roomId types.Id) types.Store { 752 | id := db.FindOne(types.StoreType, bson.M{"roomid": roomId}) 753 | 754 | if id != nil { 755 | return GetStore(id) 756 | } 757 | return nil 758 | } 759 | 760 | func GetStore(id types.Id) types.Store { 761 | return db.Retrieve(id, types.StoreType).(types.Store) 762 | } 763 | 764 | func CreateStore(name string, roomId types.Id) types.Store { 765 | return db.NewStore(name, roomId) 766 | } 767 | 768 | func DeleteStore(id types.Id) { 769 | deleteContainer(id) 770 | } 771 | 772 | func GetWorld() types.World { 773 | id := db.FindOne(types.WorldType, bson.M{}) 774 | if id == nil { 775 | return db.NewWorld() 776 | } 777 | return db.Retrieve(id, types.WorldType).(types.World) 778 | } 779 | 780 | func deleteContainer(id types.Id) { 781 | items := ItemsIn(id) 782 | for _, item := range items { 783 | db.DeleteObject(item.GetId()) 784 | } 785 | db.DeleteObject(id) 786 | } 787 | --------------------------------------------------------------------------------