├── _config.yml ├── ext └── db │ └── gwredis │ ├── gwredis_test.go │ └── gwredis.go ├── .codecov.yml ├── examples ├── test_game │ ├── types.go │ ├── Monster.go │ ├── AOITester.go │ ├── OnlineService.go │ ├── test_game.go │ ├── Account.go │ ├── MySpace.go │ ├── MailService.go │ └── SpaceService.go ├── chatroom_demo │ ├── MySpace.go │ ├── chatroom_demo.go │ ├── Avatar.go │ └── Account.go ├── nil_game │ ├── nil_game.go │ ├── Account.go │ └── MySpace.go ├── unity_demo │ ├── unity_demo.go │ ├── OnlineService.go │ ├── Account.go │ ├── Player.go │ ├── MySpace.go │ └── Monster.go └── test_client │ ├── ClientSpace.go │ ├── profile.go │ └── test_client.go ├── cmd └── goworld │ ├── kill.go │ ├── process │ ├── process_unix.go │ ├── process_win.go │ ├── process_test.go │ └── process.go │ ├── platform_unix.go │ ├── platform_windows.go │ ├── error.go │ ├── serverid.go │ ├── pathutil.go │ ├── reload.go │ ├── build.go │ ├── main.go │ ├── stop.go │ ├── detectenv.go │ ├── status.go │ └── start.go ├── engine ├── opmon │ ├── opmon_test.go │ └── opmon.go ├── post │ ├── post_test.go │ └── post.go ├── kvreg │ ├── kvreg_test.go │ └── kvreg.go ├── gwutils │ ├── gwutils_test.go │ └── gwutils.go ├── netutil │ ├── Connection.go │ ├── MsgPacker.go │ ├── MessagePackMsgPacker.go │ ├── netutil_test.go │ ├── netutil.go │ ├── PacketConnection.go │ ├── TCPServer.go │ └── MsgPacker_test.go ├── binutil │ ├── windows.go │ ├── unix.go │ └── binutil.go ├── gwvar │ └── gwvar.go ├── storage │ ├── storage_common │ │ └── storage_common.go │ └── backend │ │ └── mongodb │ │ ├── mongodb_test.go │ │ └── mongodb.go ├── common │ ├── types_test.go │ ├── entityid_set.go │ ├── collections_test.go │ ├── types.go │ ├── hash_test.go │ ├── hash.go │ └── collections.go ├── crontab │ └── crontab_test.go ├── dispatchercluster │ ├── hash.go │ ├── dispatcherclient │ │ ├── DispatcherClient.go │ │ └── DispatcherConnMgr.go │ └── dispatchercluster.go ├── async │ ├── async_test.go │ └── async.go ├── entity │ ├── ISpace.go │ ├── SpaceManager.go │ ├── rpc_desc.go │ ├── migarte_test.go │ ├── attr.go │ ├── space_ops.go │ ├── Vector3.go │ ├── entity_map.go │ ├── attr_test.go │ └── GameClient.go ├── uuid │ ├── uuid_test.go │ └── uuid.go ├── kvdb │ ├── types │ │ └── kvdb_types.go │ ├── kvdb_test.go │ ├── backend │ │ ├── kvdbredis │ │ │ └── kvdb_redis.go │ │ ├── kvdb_mongodb │ │ │ └── mongodb.go │ │ └── kvdbrediscluster │ │ │ └── kvdb_redis_cluster.go │ └── kvdb_backend_test.go ├── gwlog │ ├── gwlog_test.go │ └── gwlog.go ├── gwioutil │ └── gwioutil.go └── config │ └── config_test.go ├── covertest.sh ├── TODO.md ├── .gitignore ├── components ├── game │ ├── restore.go │ └── lbc │ │ └── gamelbc.go ├── dispatcher │ ├── lbcheap.go │ ├── DispatcherClientProxy.go │ └── dispatcher.go └── gate │ ├── ClientProxy.go │ ├── FilterTree.go │ └── gate.go ├── .github └── workflows │ ├── build.yml │ ├── test.yml │ └── test_game.yml ├── rsa.crt ├── rsa.key ├── goworld.ini.sample ├── goworld_actions.ini ├── goworld.ini ├── go.mod └── doc.go /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /ext/db/gwredis/gwredis_test.go: -------------------------------------------------------------------------------- 1 | package gwredis 2 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | 6 | ignore: 7 | - "**/*_test.go" 8 | -------------------------------------------------------------------------------- /examples/test_game/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // MailData is the type of mail data 4 | type MailData map[string]interface{} 5 | -------------------------------------------------------------------------------- /cmd/goworld/kill.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "syscall" 4 | 5 | func kill(sid ServerID) { 6 | stopWithSignal(sid, syscall.SIGKILL) 7 | } 8 | -------------------------------------------------------------------------------- /cmd/goworld/process/process_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package process 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func (p process) Signal(sig syscall.Signal) { 10 | p.Process.SendSignal(sig) 11 | } 12 | -------------------------------------------------------------------------------- /engine/opmon/opmon_test.go: -------------------------------------------------------------------------------- 1 | package opmon 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestOpMon(t *testing.T) { 9 | op := StartOperation("test") 10 | op.Finish(time.Millisecond) 11 | monitor.Dump() 12 | } 13 | -------------------------------------------------------------------------------- /engine/post/post_test.go: -------------------------------------------------------------------------------- 1 | package post 2 | 3 | import "testing" 4 | 5 | func TestPost(t *testing.T) { 6 | var a int 7 | Post(func() { 8 | a = 1 9 | }) 10 | Tick() 11 | if a != 1 { 12 | t.Errorf("t should be 1") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /engine/kvreg/kvreg_test.go: -------------------------------------------------------------------------------- 1 | package kvreg 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/xiaonanln/goworld/engine/gwlog" 7 | ) 8 | 9 | func init() { 10 | gwlog.Infof("init") 11 | } 12 | 13 | func TestOver(t *testing.T) { 14 | gwlog.Infof("fini") 15 | } 16 | -------------------------------------------------------------------------------- /cmd/goworld/process/process_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package process 4 | 5 | import ( 6 | "syscall" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | func (p process) Signal(sig syscall.Signal) { 12 | p.Process.SendSignal(windows.Signal(sig)) 13 | } 14 | -------------------------------------------------------------------------------- /engine/gwutils/gwutils_test.go: -------------------------------------------------------------------------------- 1 | package gwutils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestRunPanicless(t *testing.T) { 9 | RunPanicless(func() { 10 | panic(1) 11 | }) 12 | RunPanicless(func() { 13 | panic(fmt.Errorf("bad")) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/goworld/platform_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | import "syscall" 6 | 7 | const ( 8 | // BinaryExtension extension used on unix 9 | BinaryExtension = "" 10 | // StopSignal syscall used to stop server 11 | StopSignal = syscall.SIGTERM 12 | ) 13 | -------------------------------------------------------------------------------- /examples/chatroom_demo/MySpace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/xiaonanln/goworld/engine/entity" 4 | 5 | // MySpace 是一个自定义的场景类型 6 | // 7 | // 由于聊天服务器没有任何场景逻辑,因此这个类型也没有任何具体的代码实现 8 | type MySpace struct { 9 | entity.Space // 自定义的场景类型必须继承一个引擎所提供的entity.Space类型 10 | } 11 | -------------------------------------------------------------------------------- /examples/nil_game/nil_game.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld" 5 | ) 6 | 7 | func main() { 8 | goworld.RegisterSpace(&MySpace{}) // 注册自定义的Space类型 9 | // 注册Account类型 10 | goworld.RegisterEntity("Account", &Account{}) 11 | // 运行游戏服务器 12 | goworld.Run() 13 | } 14 | -------------------------------------------------------------------------------- /engine/netutil/Connection.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "github.com/xiaonanln/netconnutil" 5 | "net" 6 | ) 7 | 8 | type Connection interface { 9 | netconnutil.FlushableConn 10 | } 11 | 12 | type NetConn struct { 13 | net.Conn 14 | } 15 | 16 | func (n NetConn) Flush() error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /examples/nil_game/Account.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld/engine/entity" 5 | ) 6 | 7 | // Account 是账号对象类型,用于处理注册、登录逻辑 8 | type Account struct { 9 | entity.Entity // 自定义对象类型必须继承entity.Entity 10 | } 11 | 12 | func (a *Account) DescribeEntityType(desc *entity.EntityTypeDesc) { 13 | } 14 | -------------------------------------------------------------------------------- /covertest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | PKG=$(go list ./... | grep -v /vendor/) 7 | 8 | for d in $PKG; do 9 | go test -race -coverprofile=profile.out -covermode=atomic $d 10 | if [ -f profile.out ]; then 11 | cat profile.out >> coverage.txt 12 | rm profile.out 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /cmd/goworld/process/process_test.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import "testing" 4 | 5 | func TestProcesses(t *testing.T) { 6 | ps, err := Processes() 7 | if err != nil { 8 | t.Errorf("ListProcess error: %s", err) 9 | } 10 | 11 | for _, p := range ps { 12 | cmdline, err := p.CmdlineSlice() 13 | t.Logf("process %s, err %v", cmdline, err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cmd/goworld/platform_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | import ( 6 | "syscall" 7 | 8 | _ "github.com/go-ole/go-ole" // so that dep can resolve versions correctly 9 | ) 10 | 11 | const ( 12 | // BinaryExtension extension used on windows 13 | BinaryExtension = ".exe" 14 | // StopSignal syscall used to stop server 15 | StopSignal = syscall.SIGKILL 16 | ) 17 | -------------------------------------------------------------------------------- /engine/binutil/windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package binutil 4 | 5 | import "github.com/xiaonanln/goworld/engine/gwlog" 6 | 7 | type nopRelease int 8 | 9 | func (_ nopRelease) Release() { 10 | 11 | } 12 | 13 | func Daemonize() nopRelease { 14 | // Windows can not daemonize 15 | gwlog.Warnf("can not run in daemon mode in windows, -d ignored") 16 | return nopRelease(0) 17 | } 18 | -------------------------------------------------------------------------------- /engine/netutil/MsgPacker.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | var ( 4 | // MSG_PACKER is used for packing and unpacking network data 5 | MSG_PACKER MsgPacker = MessagePackMsgPacker{} 6 | ) 7 | 8 | // MsgPacker is used to packs and unpacks messages 9 | type MsgPacker interface { 10 | PackMsg(msg interface{}, buf []byte) ([]byte, error) 11 | UnpackMsg(data []byte, msg interface{}) error 12 | } 13 | -------------------------------------------------------------------------------- /examples/test_game/Monster.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/xiaonanln/goworld/engine/entity" 4 | 5 | // Monster type 6 | type Monster struct { 7 | entity.Entity // Entity type should always inherit entity.Entity 8 | } 9 | 10 | func (m *Monster) DescribeEntityType(desc *entity.EntityTypeDesc) { 11 | desc.SetUseAOI(true, 100) 12 | desc.DefineAttr("name", "AllClients") 13 | } 14 | -------------------------------------------------------------------------------- /examples/chatroom_demo/chatroom_demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld" 5 | ) 6 | 7 | func main() { 8 | goworld.RegisterSpace(&MySpace{}) // 注册自定义的Space类型 9 | 10 | // 注册Account类型 11 | goworld.RegisterEntity("Account", &Account{}) 12 | // 注册Avatar类型,并定义属性 13 | goworld.RegisterEntity("Avatar", &Avatar{}) 14 | 15 | // 运行游戏服务器 16 | goworld.Run() 17 | } 18 | -------------------------------------------------------------------------------- /examples/test_game/AOITester.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld" 5 | "github.com/xiaonanln/goworld/engine/entity" 6 | ) 7 | 8 | // AOITester type 9 | type AOITester struct { 10 | goworld.Entity // Entity type should always inherit entity.Entity 11 | } 12 | 13 | func (m *AOITester) DescribeEntityType(desc *entity.EntityTypeDesc) { 14 | desc.SetUseAOI(true, 100) 15 | desc.DefineAttr("name", "AllClients") 16 | } 17 | -------------------------------------------------------------------------------- /examples/nil_game/MySpace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld" 5 | "github.com/xiaonanln/goworld/engine/gwlog" 6 | ) 7 | 8 | // MySpace is the custom space type 9 | type MySpace struct { 10 | goworld.Space // Space type should always inherit from entity.Space 11 | } 12 | 13 | // OnGameReady is called when the game server is ready 14 | func (space *MySpace) OnGameReady() { 15 | gwlog.Infof("Game %d Is Ready", goworld.GetGameID()) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/goworld/error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func showMsgAndQuit(format string, a ...interface{}) { 9 | fmt.Fprintf(os.Stderr, "! "+format+"\n", a...) 10 | os.Exit(2) 11 | } 12 | 13 | func showMsg(format string, a ...interface{}) { 14 | fmt.Fprintf(os.Stderr, "> "+format+"\n", a...) 15 | } 16 | 17 | func checkErrorOrQuit(err error, msg string) { 18 | if err != nil { 19 | fmt.Fprintf(os.Stderr, "! %s: %v\n", msg, err) 20 | os.Exit(2) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /engine/gwvar/gwvar.go: -------------------------------------------------------------------------------- 1 | package gwvar 2 | 3 | import "expvar" 4 | 5 | type Bool struct { 6 | val *expvar.Int 7 | } 8 | 9 | func NewBool(name string) *Bool { 10 | return &Bool{ 11 | val: expvar.NewInt("name"), 12 | } 13 | } 14 | 15 | func (b *Bool) Value() bool { 16 | return b.val.Value() > 0 17 | } 18 | 19 | func (b *Bool) Set(v bool) { 20 | if v { 21 | b.val.Set(1) 22 | } else { 23 | b.val.Set(0) 24 | } 25 | } 26 | 27 | var ( 28 | IsDeploymentReady = NewBool("IsDeploymentReady") 29 | ) 30 | -------------------------------------------------------------------------------- /engine/storage/storage_common/storage_common.go: -------------------------------------------------------------------------------- 1 | package storagecommon 2 | 3 | import "github.com/xiaonanln/goworld/engine/common" 4 | 5 | // EntityStorage defines the interface of entity storage backends 6 | type EntityStorage interface { 7 | List(typeName string) ([]common.EntityID, error) 8 | Write(typeName string, entityID common.EntityID, data interface{}) error 9 | Read(typeName string, entityID common.EntityID) (interface{}, error) 10 | Exists(typeName string, entityID common.EntityID) (bool, error) 11 | Close() 12 | IsEOF(err error) bool 13 | } 14 | -------------------------------------------------------------------------------- /engine/common/types_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "testing" 4 | 5 | func TestEntityID(t *testing.T) { 6 | eid := GenEntityID() 7 | if len(eid) != ENTITYID_LENGTH { 8 | t.Fail() 9 | } 10 | 11 | if eid.IsNil() { 12 | t.Fail() 13 | } 14 | 15 | if !EntityID("").IsNil() { 16 | t.Fail() 17 | } 18 | 19 | } 20 | 21 | func TestClientID(t *testing.T) { 22 | if !ClientID("").IsNil() { 23 | t.Fail() 24 | } 25 | cid := GenClientID() 26 | if cid.IsNil() { 27 | t.Fail() 28 | } 29 | if len(cid) != CLIENTID_LENGTH { 30 | t.Fail() 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /engine/crontab/crontab_test.go: -------------------------------------------------------------------------------- 1 | package crontab 2 | 3 | import "testing" 4 | 5 | func init() { 6 | Initialize() 7 | } 8 | 9 | func TestRegister(t *testing.T) { 10 | Register(-1, -1, -1, -1, -1, func() { 11 | t.Logf("crontab every minute") 12 | }) 13 | check() 14 | } 15 | 16 | func TestUnregister(t *testing.T) { 17 | var h Handle 18 | Register(-1, -1, -1, -1, -1, func() { 19 | t.Logf("crontab every minute 1") 20 | }) 21 | 22 | h = Register(-1, -1, -1, -1, -1, func() { 23 | t.Logf("crontab every minute 2") 24 | h.Unregister() 25 | }) 26 | check() 27 | } 28 | -------------------------------------------------------------------------------- /engine/binutil/unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package binutil 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/sevlyar/go-daemon" 9 | "github.com/xiaonanln/goworld/engine/gwlog" 10 | ) 11 | 12 | func Daemonize() *daemon.Context { 13 | context := new(daemon.Context) 14 | child, err := context.Reborn() 15 | 16 | if err != nil { 17 | // daemonize failed 18 | gwlog.Panicf("daemonize failed: %v", err) 19 | } 20 | 21 | if child != nil { 22 | gwlog.Infof("run in daemon mode") 23 | os.Exit(0) 24 | return nil 25 | } else { 26 | return context 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /engine/dispatchercluster/hash.go: -------------------------------------------------------------------------------- 1 | package dispatchercluster 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld/engine/common" 5 | ) 6 | 7 | func hashEntityID(id common.EntityID) int { 8 | // hash EntityID to dispatcher shard index: use least 2 bytes 9 | b1 := id[14] 10 | b2 := id[15] 11 | return int(b1)*256 + int(b2) 12 | } 13 | 14 | func hashGateID(gateid uint16) int { 15 | return int(gateid - 1) 16 | } 17 | 18 | func hashString(s string) int { 19 | return int(common.HashString(s)) 20 | } 21 | 22 | func hashSrvID(sn string) int { 23 | return hashString(sn) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/goworld/serverid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // ServerID represents a server 10 | type ServerID string 11 | 12 | // Path returns the path to the server 13 | func (sid ServerID) Path() string { 14 | serverPath := strings.Split(string(sid), "/") 15 | serverPath = append([]string{env.GoWorldRoot}, serverPath...) 16 | return filepath.Join(serverPath...) 17 | } 18 | 19 | // Name returns the name of the server 20 | func (sid ServerID) Name() string { 21 | _, file := path.Split(string(sid)) 22 | return file 23 | } 24 | -------------------------------------------------------------------------------- /engine/async/async_test.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "time" 8 | 9 | "github.com/xiaonanln/goworld/engine/post" 10 | ) 11 | 12 | func TestNewAsyncJob(t *testing.T) { 13 | var wait sync.WaitGroup 14 | wait.Add(2) 15 | AppendAsyncJob("1", func() (res interface{}, err error) { 16 | wait.Done() 17 | return 1, nil 18 | }, func(res interface{}, err error) { 19 | println("returns", res.(int), err) 20 | wait.Done() 21 | }) 22 | wait.Wait() 23 | } 24 | 25 | func init() { 26 | go func() { 27 | for { 28 | post.Tick() 29 | time.Sleep(time.Millisecond) 30 | } 31 | }() 32 | } 33 | -------------------------------------------------------------------------------- /examples/unity_demo/unity_demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld" 5 | ) 6 | 7 | var ( 8 | _SERVICE_NAMES = []string{ 9 | "OnlineService", 10 | "SpaceService", 11 | } 12 | ) 13 | 14 | func main() { 15 | goworld.RegisterSpace(&MySpace{}) // 注册自定义的Space类型 16 | 17 | goworld.RegisterService("OnlineService", &OnlineService{}, 3) 18 | goworld.RegisterService("SpaceService", &SpaceService{}, 3) 19 | // 注册Account类型 20 | goworld.RegisterEntity("Account", &Account{}) 21 | // 注册Monster类型 22 | goworld.RegisterEntity("Monster", &Monster{}) 23 | // 注册Avatar类型,并定义属性 24 | goworld.RegisterEntity("Player", &Player{}) 25 | // 运行游戏服务器 26 | goworld.Run() 27 | } 28 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * Run GoWorld server on Docker 4 | 5 | * Adopt Kafka 6 | ** Support For Reliable Call? 7 | 8 | * Run processes in Docker 9 | * Dispatcher, Gate, Game should run in different docker container 10 | * Processes connect each other and other services using Container Network 11 | * Processes discover each other using etcd ? 12 | 13 | * Optimize callall and 'AllClients' attribute broadcasting ? 14 | 15 | * Multiple service entities on multiple Games to remove SPOF in service architecture 16 | 17 | * Service Registry using Etcd 18 | 19 | * Better AOI algorithm that enables entities to have different AOI distances 20 | 21 | * Read config using tag (maybe use yaml) 22 | -------------------------------------------------------------------------------- /cmd/goworld/pathutil.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | 5 | func isfile(path string) bool { 6 | fi, err := os.Stat(path) 7 | if err != nil { 8 | if os.IsNotExist(err) { 9 | return false 10 | } 11 | 12 | panic(err) 13 | } 14 | 15 | return !fi.IsDir() 16 | } 17 | 18 | func isdir(path string) bool { 19 | fi, err := os.Stat(path) 20 | if err != nil { 21 | if os.IsNotExist(err) { 22 | return false 23 | } 24 | 25 | panic(err) 26 | } 27 | 28 | return fi.IsDir() 29 | } 30 | 31 | func isexists(path string) bool { 32 | _, err := os.Stat(path) 33 | if err != nil { 34 | if os.IsNotExist(err) { 35 | return false 36 | } 37 | 38 | panic(err) 39 | } 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /engine/entity/ISpace.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | // ISpace is the space delegate interface 4 | // 5 | // User custom space class can override these functions for their own game logic 6 | type ISpace interface { 7 | IEntity 8 | 9 | OnSpaceInit() // Called when initializing space struct, override to initialize custom space fields 10 | OnSpaceCreated() // Called when space is created 11 | OnSpaceDestroy() // Called just before space is destroyed 12 | // Space Operations 13 | OnEntityEnterSpace(entity *Entity) // Called when any entity enters space 14 | OnEntityLeaveSpace(entity *Entity) // Called when any entity leaves space 15 | // Game releated callbacks on nil space only 16 | OnGameReady() 17 | } 18 | -------------------------------------------------------------------------------- /engine/uuid/uuid_test.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | ) 7 | 8 | func TestGenUUID(t *testing.T) { 9 | for i := 0; i < 100; i++ { 10 | uuid := GenUUID() 11 | t.Logf("GenUUID: %s", uuid) 12 | if len(uuid) != UUID_LENGTH { 13 | t.FailNow() 14 | } 15 | } 16 | } 17 | 18 | func BenchmarkGenUUID(b *testing.B) { 19 | for i := 0; i < b.N; i++ { 20 | GenUUID() 21 | } 22 | } 23 | 24 | func TestGenFixedUUID(t *testing.T) { 25 | for i := 0; i < 100; i++ { 26 | s := strconv.Itoa(i) 27 | u1 := GenFixedUUID([]byte(s)) 28 | u2 := GenFixedUUID([]byte(s)) 29 | if u1 != u2 { 30 | t.Fatalf("GenFixedUUID is not fixed") 31 | } 32 | t.Logf("GenFixedUUID: %v => %v", i, u1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /engine/kvdb/types/kvdb_types.go: -------------------------------------------------------------------------------- 1 | package kvdbtypes 2 | 3 | // KVDBEngine defines the interface of a KVDB engine implementation 4 | type KVDBEngine interface { 5 | Get(key string) (val string, err error) 6 | Put(key string, val string) (err error) 7 | Find(beginKey string, endKey string) (Iterator, error) 8 | Close() 9 | IsConnectionError(err error) bool 10 | } 11 | 12 | // Iterator is the interface for iterators for KVDB 13 | // 14 | // Next should returns the next item with error=nil whenever has next item 15 | // otherwise returns KVItem{}, io.EOF 16 | // When failed, returns KVItem{}, error 17 | type Iterator interface { 18 | Next() (KVItem, error) 19 | } 20 | 21 | // KVItem is the type of KVDB item 22 | type KVItem struct { 23 | Key string 24 | Val string 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | *~ 17 | *.log 18 | *.log.gz 19 | .*.swp 20 | .*.swo 21 | 22 | _entity_storage/ 23 | fc 24 | *.db/ 25 | .idea/ 26 | examples/*/*.sh 27 | *.bat 28 | *.dat 29 | vendor/ 30 | .idea.bak/ 31 | .vscode/ 32 | tests/ 33 | /components/dispatcher/dispatcher 34 | /components/gate/gate 35 | /examples/test_client/test_client 36 | /examples/test_game/test_game 37 | /examples/chatroom_demo/chatroom_demo 38 | /examples/nil_game/nil_game 39 | /examples/unity_demo/unity_demo 40 | /go.sum 41 | -------------------------------------------------------------------------------- /engine/netutil/MessagePackMsgPacker.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/vmihailenco/msgpack" 7 | ) 8 | 9 | // MessagePackMsgPacker packs and unpacks message in MessagePack format 10 | type MessagePackMsgPacker struct{} 11 | 12 | // PackMsg packs message to bytes in MessagePack format 13 | func (mp MessagePackMsgPacker) PackMsg(msg interface{}, buf []byte) ([]byte, error) { 14 | buffer := bytes.NewBuffer(buf) 15 | 16 | encoder := msgpack.NewEncoder(buffer) 17 | err := encoder.Encode(msg) 18 | if err != nil { 19 | return buf, err 20 | } 21 | buf = buffer.Bytes() 22 | return buf, nil 23 | } 24 | 25 | // UnpackMsg unpacksbytes in MessagePack format to message 26 | func (mp MessagePackMsgPacker) UnpackMsg(data []byte, msg interface{}) error { 27 | err := msgpack.Unmarshal(data, msg) 28 | return err 29 | } 30 | -------------------------------------------------------------------------------- /components/game/restore.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "time" 7 | 8 | "github.com/xiaonanln/goworld/engine/entity" 9 | "github.com/xiaonanln/goworld/engine/gwlog" 10 | ) 11 | 12 | func freezeFilename(gameid uint16) string { 13 | return fmt.Sprintf("game%d_freezed.dat", gameid) 14 | } 15 | 16 | func restoreFreezedEntities() error { 17 | t0 := time.Now() 18 | freezeFilename := freezeFilename(gameid) 19 | data, err := ioutil.ReadFile(freezeFilename) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | t1 := time.Now() 25 | var freezeEntity entity.FreezeData 26 | freezePacker.UnpackMsg(data, &freezeEntity) 27 | t2 := time.Now() 28 | 29 | err = entity.RestoreFreezedEntities(&freezeEntity) 30 | t3 := time.Now() 31 | 32 | gwlog.Infof("Restored game service: load = %s, unpack = %s, restore = %s", t1.Sub(t0), t2.Sub(t1), t3.Sub(t2)) 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /engine/netutil/netutil_test.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "fmt" 8 | 9 | "github.com/xiaonanln/goworld/engine/gwioutil" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | ) 12 | 13 | type testEchoTcpServer struct { 14 | } 15 | 16 | func (ts *testEchoTcpServer) ServeTCPConnection(conn net.Conn) { 17 | buf := make([]byte, 1024*1024, 1024*1024) 18 | for { 19 | n, err := conn.Read(buf) 20 | if n > 0 { 21 | gwioutil.WriteAll(conn, buf[:n]) 22 | } 23 | 24 | if err != nil { 25 | if gwioutil.IsTimeoutError(err) { 26 | continue 27 | } else { 28 | gwlog.Errorf("read error: %s", err.Error()) 29 | break 30 | } 31 | } 32 | } 33 | } 34 | 35 | const PORT = 14572 36 | 37 | func init() { 38 | go func() { 39 | ServeTCP(fmt.Sprintf("localhost:%d", PORT), &testEchoTcpServer{}) 40 | }() 41 | time.Sleep(time.Millisecond * 200) 42 | } 43 | -------------------------------------------------------------------------------- /engine/common/entityid_set.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // EntityIDSet is the data structure for a set of entity IDs 4 | type EntityIDSet map[EntityID]struct{} 5 | 6 | // Add adds an entity ID to EntityIDSet 7 | func (es EntityIDSet) Add(id EntityID) { 8 | es[id] = struct{}{} 9 | } 10 | 11 | // Del removes an entity ID from EntityIDSet 12 | func (es EntityIDSet) Del(id EntityID) { 13 | delete(es, id) 14 | } 15 | 16 | // Contains checks if entity ID is in EntityIDSet 17 | func (es EntityIDSet) Contains(id EntityID) bool { 18 | _, ok := es[id] 19 | return ok 20 | } 21 | 22 | // ToList convert EntityIDSet to a slice of entity IDs 23 | func (es EntityIDSet) ToList() []EntityID { 24 | list := make([]EntityID, 0, len(es)) 25 | for eid := range es { 26 | list = append(list, eid) 27 | } 28 | return list 29 | } 30 | 31 | func (es EntityIDSet) ForEach(cb func(eid EntityID) bool) { 32 | for eid := range es { 33 | if !cb(eid) { 34 | break 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /engine/common/collections_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bmizerany/assert" 7 | ) 8 | 9 | func TestStringSet(t *testing.T) { 10 | ss := StringSet{} 11 | ss.Add("1") 12 | ss.Add("2") 13 | assert.T(t, ss.Contains("1"), "should contain") 14 | assert.T(t, ss.Contains("2"), "should contain") 15 | ss.Remove("2") 16 | assert.T(t, !ss.Contains("2"), "should contain") 17 | } 18 | 19 | func TestStringList(t *testing.T) { 20 | ss := StringList{} 21 | ss.Append("1") 22 | assert.T(t, len(ss) == 1, "wrong length") 23 | ss.Append("2") 24 | assert.T(t, len(ss) == 2, "wrong length") 25 | ss.Append("3") 26 | assert.T(t, len(ss) == 3, "wrong length") 27 | ss.Remove("2") 28 | assert.Tf(t, len(ss) == 2, "wrong length: %v", ss) 29 | assert.Tf(t, ss.Find("1") == 0, "wrong index: %d", ss.Find("1")) 30 | assert.Tf(t, ss.Find("2") == -1, "wrong index: %d", ss.Find("2")) 31 | assert.Tf(t, ss.Find("3") == 1, "wrong index: %d", ss.Find("3")) 32 | } 33 | -------------------------------------------------------------------------------- /engine/entity/SpaceManager.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld/engine/common" 5 | ) 6 | 7 | var ( 8 | spaceManager = newSpaceManager() 9 | ) 10 | 11 | type _SpaceManager struct { 12 | spaces map[common.EntityID]*Space 13 | } 14 | 15 | func newSpaceManager() *_SpaceManager { 16 | return &_SpaceManager{ 17 | spaces: map[common.EntityID]*Space{}, 18 | } 19 | } 20 | 21 | func (spmgr *_SpaceManager) putSpace(space *Space) { 22 | spmgr.spaces[space.ID] = space 23 | } 24 | 25 | func (spmgr *_SpaceManager) delSpace(id common.EntityID) { 26 | delete(spmgr.spaces, id) 27 | } 28 | 29 | func (spmgr *_SpaceManager) getSpace(id common.EntityID) *Space { 30 | return spmgr.spaces[id] 31 | } 32 | 33 | // RegisterSpace registers the user custom space type 34 | func RegisterSpace(spacePtr ISpace) { 35 | RegisterEntity(_SPACE_ENTITY_TYPE, spacePtr, false) 36 | } 37 | 38 | func GetSpace(id common.EntityID) *Space { 39 | return spaceManager.spaces[id] 40 | } 41 | -------------------------------------------------------------------------------- /cmd/goworld/reload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/xiaonanln/goworld/engine/binutil" 7 | "github.com/xiaonanln/goworld/engine/config" 8 | ) 9 | 10 | func reload(sid ServerID) { 11 | err := os.Chdir(env.GoWorldRoot) 12 | checkErrorOrQuit(err, "chdir to goworld directory failed") 13 | 14 | ss := detectServerStatus() 15 | showServerStatus(ss) 16 | if !ss.IsRunning() { 17 | // server is not running 18 | showMsgAndQuit("no server is running currently") 19 | } 20 | 21 | if ss.ServerID != "" && ss.ServerID != sid { 22 | showMsgAndQuit("another server is running: %s", ss.ServerID) 23 | } 24 | 25 | if ss.NumGamesRunning == 0 { 26 | showMsgAndQuit("no game is running") 27 | } else if ss.NumGamesRunning != config.GetDeployment().DesiredGames { 28 | showMsgAndQuit("found %d games, but should have %d", ss.NumGamesRunning, config.GetDeployment().DesiredGames) 29 | } 30 | 31 | stopGames(ss, binutil.FreezeSignal) 32 | startGames(sid, true) 33 | } 34 | -------------------------------------------------------------------------------- /examples/test_client/ClientSpace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/xiaonanln/goworld/engine/common" 8 | "github.com/xiaonanln/typeconv" 9 | ) 10 | 11 | // ClientSpace is the space on client 12 | type ClientSpace struct { 13 | sync.Mutex 14 | 15 | owner *ClientBot 16 | ID common.EntityID 17 | Kind int 18 | 19 | Attrs clientAttrs 20 | destroyed bool 21 | } 22 | 23 | func newClientSpace(owner *ClientBot, entityid common.EntityID, data map[string]interface{}) *ClientSpace { 24 | space := &ClientSpace{ 25 | owner: owner, 26 | ID: entityid, 27 | Attrs: data, 28 | } 29 | space.Kind = int(typeconv.Int(data["_K"])) 30 | return space 31 | } 32 | 33 | func (space *ClientSpace) String() string { 34 | return fmt.Sprintf("ClientSpace<%d|%s>", space.Kind, space.ID) 35 | } 36 | 37 | // Destroy the client space 38 | func (space *ClientSpace) Destroy() { 39 | if space.destroyed { 40 | return 41 | } 42 | space.destroyed = true 43 | } 44 | -------------------------------------------------------------------------------- /engine/netutil/netutil.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "net" 7 | 8 | "unsafe" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | var ( 14 | // NETWORK_ENDIAN is the network Endian of connections 15 | NETWORK_ENDIAN = binary.LittleEndian 16 | ) 17 | 18 | // IsConnectionError check if the error is a connection error (close) 19 | func IsConnectionError(_err interface{}) bool { 20 | err, ok := _err.(error) 21 | if !ok { 22 | return false 23 | } 24 | 25 | err = errors.Cause(err) 26 | if err == io.EOF { 27 | return true 28 | } 29 | 30 | neterr, ok := err.(net.Error) 31 | if !ok { 32 | return false 33 | } 34 | if neterr.Timeout() { 35 | return false 36 | } 37 | 38 | return true 39 | } 40 | 41 | // ConnectTCP connects to host:port in TCP 42 | func ConnectTCP(addr string) (net.Conn, error) { 43 | conn, err := net.Dial("tcp", addr) 44 | return conn, err 45 | } 46 | 47 | func PutFloat32(b []byte, f float32) { 48 | NETWORK_ENDIAN.PutUint32(b, *(*uint32)(unsafe.Pointer(&f))) 49 | } 50 | -------------------------------------------------------------------------------- /cmd/goworld/process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "syscall" 5 | 6 | psutil_process "github.com/shirou/gopsutil/process" 7 | ) 8 | 9 | type Process interface { 10 | Pid() int32 11 | Executable() string 12 | Path() (string, error) 13 | CmdlineSlice() ([]string, error) 14 | Cwd() (string, error) 15 | Signal(sig syscall.Signal) 16 | } 17 | 18 | type process struct { 19 | *psutil_process.Process 20 | } 21 | 22 | func (p process) Pid() int32 { 23 | return p.Process.Pid 24 | } 25 | 26 | func (p process) Executable() string { 27 | name, _ := p.Process.Name() 28 | return name 29 | } 30 | 31 | func (p process) Path() (string, error) { 32 | return p.Process.Exe() 33 | } 34 | 35 | func (p process) test() { 36 | } 37 | 38 | func Processes() ([]Process, error) { 39 | var procs []Process 40 | 41 | ps, err := psutil_process.Processes() 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | for _, _p := range ps { 47 | p := &process{_p} 48 | p.test() 49 | procs = append(procs, p) 50 | } 51 | return procs, nil 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | cancel-previous-runs: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: rokroskar/workflow-run-cleanup-action@master 10 | env: 11 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 12 | if: "github.ref != 'refs/heads/master'" 13 | 14 | build: 15 | name: Build (Go ${{ matrix.go }}, ${{ matrix.os }}) 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | go: [1.17, 1.18] 20 | os: ["ubuntu-20.04", "macos-11"] 21 | steps: 22 | - uses: actions/setup-go@v2 23 | with: 24 | go-version: ${{ matrix.go }} 25 | - uses: actions/checkout@v2 26 | - name: Build 27 | run: | 28 | go mod tidy 29 | go install ./cmd/... 30 | goworld build examples/nil_game 31 | goworld build examples/test_game 32 | goworld build examples/test_client 33 | goworld build examples/chatroom_demo 34 | goworld build examples/unity_demo 35 | -------------------------------------------------------------------------------- /components/game/lbc/gamelbc.go: -------------------------------------------------------------------------------- 1 | package gamelbc 2 | 3 | import ( 4 | "os" 5 | 6 | "context" 7 | 8 | "time" 9 | 10 | "github.com/shirou/gopsutil/process" 11 | "github.com/xiaonanln/goworld/engine/dispatchercluster" 12 | "github.com/xiaonanln/goworld/engine/gwlog" 13 | "github.com/xiaonanln/goworld/engine/gwutils" 14 | "github.com/xiaonanln/goworld/engine/proto" 15 | ) 16 | 17 | func Initialize(ctx context.Context, collectInterval time.Duration) { 18 | pid := os.Getpid() 19 | p, err := process.NewProcess(int32(pid)) 20 | if err != nil { 21 | gwlog.Fatalf("can not find game process: pid = %v", pid) 22 | } 23 | gwlog.Infof("gamelbc: found game process: %s", p) 24 | 25 | go gwutils.RepeatUntilPanicless(func() { 26 | for { 27 | time.Sleep(collectInterval) 28 | pcnt, err := p.CPUPercentWithContext(ctx) 29 | if err != nil { 30 | gwlog.Panicf("gamelbc: get process cpu percent failed: %s", err) 31 | } 32 | 33 | gwlog.Debugf("gamelbc: cpu percent is %.3f%%", pcnt) 34 | dispatchercluster.SendGameLBCInfo(proto.GameLBCInfo{ 35 | CPUPercent: pcnt, 36 | }) 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /engine/entity/rpc_desc.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | rfServer = 1 << iota 10 | rfOwnClient 11 | rfOtherClient 12 | ) 13 | 14 | type rpcDesc struct { 15 | Func reflect.Value 16 | Flags uint 17 | MethodType reflect.Type 18 | NumArgs int 19 | } 20 | 21 | type rpcDescMap map[string]*rpcDesc 22 | 23 | func (rdm rpcDescMap) visit(method reflect.Method) { 24 | methodName := method.Name 25 | var flag uint 26 | var rpcName string 27 | if strings.HasSuffix(methodName, "_Client") { 28 | flag |= rfServer + rfOwnClient 29 | rpcName = methodName[:len(methodName)-7] 30 | } else if strings.HasSuffix(methodName, "_AllClients") { 31 | flag |= rfServer + rfOwnClient + rfOtherClient 32 | rpcName = methodName[:len(methodName)-11] 33 | } else { 34 | // server method 35 | flag |= rfServer 36 | rpcName = methodName 37 | } 38 | 39 | methodType := method.Type 40 | rdm[rpcName] = &rpcDesc{ 41 | Func: method.Func, 42 | Flags: flag, 43 | MethodType: methodType, 44 | NumArgs: methodType.NumIn() - 1, // do not count the receiver 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /engine/gwlog/gwlog_test.go: -------------------------------------------------------------------------------- 1 | package gwlog 2 | 3 | import "testing" 4 | 5 | func TestGWLog(t *testing.T) { 6 | SetSource("gwlog_test") 7 | SetOutput([]string{"stderr", "gwlog_test.log"}) 8 | SetLevel(DebugLevel) 9 | 10 | if lv := ParseLevel("debug"); lv != DebugLevel { 11 | t.Fail() 12 | } 13 | if lv := ParseLevel("info"); lv != InfoLevel { 14 | t.Fail() 15 | } 16 | if lv := ParseLevel("warn"); lv != WarnLevel { 17 | t.Fail() 18 | } 19 | if lv := ParseLevel("error"); lv != ErrorLevel { 20 | t.Fail() 21 | } 22 | if lv := ParseLevel("panic"); lv != PanicLevel { 23 | t.Fail() 24 | } 25 | if lv := ParseLevel("fatal"); lv != FatalLevel { 26 | t.Fail() 27 | } 28 | 29 | Debugf("this is a debug %d", 1) 30 | SetLevel(InfoLevel) 31 | Debugf("SHOULD NOT SEE THIS!") 32 | Infof("this is an info %d", 2) 33 | Warnf("this is a warning %d", 3) 34 | TraceError("this is a trace error %d", 4) 35 | func() { 36 | defer func() { 37 | _ = recover() 38 | }() 39 | Panicf("this is a panic %d", 4) 40 | }() 41 | 42 | func() { 43 | defer func() { 44 | _ = recover() 45 | }() 46 | //Fatalf("this is a fatal %d", 5) 47 | }() 48 | } 49 | -------------------------------------------------------------------------------- /engine/gwutils/gwutils.go: -------------------------------------------------------------------------------- 1 | package gwutils 2 | 3 | import "github.com/xiaonanln/goworld/engine/gwlog" 4 | 5 | // CatchPanic calls a function and returns the error if function paniced 6 | func CatchPanic(f func()) (err interface{}) { 7 | defer func() { 8 | err = recover() 9 | if err != nil { 10 | gwlog.TraceError("%s panic: %s", f, err) 11 | } 12 | }() 13 | 14 | f() 15 | return 16 | } 17 | 18 | // RunPanicless calls a function panic-freely 19 | func RunPanicless(f func()) (panicless bool) { 20 | defer func() { 21 | err := recover() 22 | panicless = err == nil 23 | if err != nil { 24 | gwlog.TraceError("%s panic: %s", f, err) 25 | } 26 | }() 27 | 28 | f() 29 | return 30 | } 31 | 32 | // RepeatUntilPanicless runs the function repeatly until there is no panic 33 | func RepeatUntilPanicless(f func()) { 34 | for !RunPanicless(f) { 35 | } 36 | } 37 | 38 | // NextLargerKey finds the next key that is larger than the specified key, 39 | // but smaller than any other keys that is larger than the specified key 40 | func NextLargerKey(key string) string { 41 | return key + "\x00" // the next string that is larger than key, but smaller than any other keys > key 42 | } 43 | -------------------------------------------------------------------------------- /engine/common/types.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld/engine/gwlog" 5 | "github.com/xiaonanln/goworld/engine/uuid" 6 | ) 7 | 8 | // ENTITYID_LENGTH is the length of Entity IDs 9 | const ENTITYID_LENGTH = uuid.UUID_LENGTH 10 | 11 | // EntityID type 12 | type EntityID string 13 | 14 | // IsNil returns if EntityID is nil 15 | func (id EntityID) IsNil() bool { 16 | return id == "" 17 | } 18 | 19 | // GenEntityID generates a new EntityID 20 | func GenEntityID() EntityID { 21 | return EntityID(uuid.GenUUID()) 22 | } 23 | 24 | // MustEntityID assures a string to be EntityID 25 | func MustEntityID(id string) EntityID { 26 | if len(id) != ENTITYID_LENGTH { 27 | gwlog.Panicf("%s of len %d is not a valid entity ID (len=%d)", id, len(id), ENTITYID_LENGTH) 28 | } 29 | return EntityID(id) 30 | } 31 | 32 | // ClientID type 33 | type ClientID string 34 | 35 | // GenClientID generates a new Client ID 36 | func GenClientID() ClientID { 37 | return ClientID(uuid.GenUUID()) 38 | } 39 | 40 | // IsNil returns if ClientID is nil 41 | func (id ClientID) IsNil() bool { 42 | return id == "" 43 | } 44 | 45 | // CLIENTID_LENGTH is the length of Client IDs 46 | const CLIENTID_LENGTH = uuid.UUID_LENGTH 47 | -------------------------------------------------------------------------------- /engine/post/post.go: -------------------------------------------------------------------------------- 1 | package post 2 | 3 | import ( 4 | "sync" 5 | 6 | //"github.com/xiaonanln/goworld/gwlog" 7 | "github.com/xiaonanln/goworld/engine/gwutils" 8 | ) 9 | 10 | // PostCallback is the type of functions to be posted 11 | type PostCallback func() 12 | 13 | var ( 14 | callbacks []PostCallback 15 | lock sync.Mutex 16 | ) 17 | 18 | // Post a callback which will be executed when other things are done in the main game routine 19 | // 20 | // Post might be called from other goroutine, so we use a lock to protect the data 21 | func Post(f PostCallback) { 22 | lock.Lock() 23 | callbacks = append(callbacks, f) 24 | lock.Unlock() 25 | } 26 | 27 | // Tick is called by the main game routine to run all posted functions 28 | func Tick() { 29 | for { // loop until there is no callbacks posted anymore 30 | lock.Lock() // lock to check number of callbacks 31 | if len(callbacks) == 0 { 32 | lock.Unlock() 33 | break // all callbacked executed, quit 34 | } 35 | // switch callbacks in locked section 36 | callbacksCopy := callbacks 37 | callbacks = make([]PostCallback, 0, len(callbacks)) 38 | lock.Unlock() 39 | 40 | for _, f := range callbacksCopy { 41 | gwutils.RunPanicless(f) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ext/db/gwredis/gwredis.go: -------------------------------------------------------------------------------- 1 | package gwredis 2 | 3 | import ( 4 | "github.com/garyburd/redigo/redis" 5 | "github.com/xiaonanln/goworld/engine/async" 6 | ) 7 | 8 | const ( 9 | _REDIS_ASYNC_JOB_GROUP = "_redis" 10 | ) 11 | 12 | type DB struct { 13 | conn redis.Conn 14 | } 15 | 16 | func Dial(network, address string, options []redis.DialOption, ac async.AsyncCallback) { 17 | async.AppendAsyncJob(_REDIS_ASYNC_JOB_GROUP, func() (res interface{}, err error) { 18 | conn, err := redis.Dial(network, address, options...) 19 | if err == nil { 20 | return &DB{conn}, nil 21 | } else { 22 | return nil, err 23 | } 24 | }, ac) 25 | } 26 | 27 | func DialURL(rawurl string, options []redis.DialOption, ac async.AsyncCallback) { 28 | async.AppendAsyncJob(_REDIS_ASYNC_JOB_GROUP, func() (res interface{}, err error) { 29 | conn, err := redis.DialURL(rawurl, options...) 30 | if err == nil { 31 | return &DB{conn}, nil 32 | } else { 33 | return nil, err 34 | } 35 | }, ac) 36 | } 37 | 38 | func (db *DB) Do(commandName string, args []interface{}, ac async.AsyncCallback) { 39 | async.AppendAsyncJob(_REDIS_ASYNC_JOB_GROUP, func() (res interface{}, err error) { 40 | res, err = db.conn.Do(commandName, args...) 41 | return 42 | }, ac) 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | cancel-previous-runs: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: rokroskar/workflow-run-cleanup-action@master 10 | env: 11 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 12 | if: "github.ref != 'refs/heads/master'" 13 | 14 | test: 15 | name: Test (Go ${{ matrix.go }}, ${{ matrix.os }}) 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | go: [1.17, 1.18] 20 | os: ["ubuntu-20.04"] 21 | mongodb-version: [3.6] 22 | steps: 23 | - uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ matrix.go }} 26 | - name: Start MongoDB 27 | uses: supercharge/mongodb-github-action@1.3.0 28 | with: 29 | mongodb-version: ${{ matrix.mongodb-version }} 30 | - uses: shogo82148/actions-setup-redis@v1 31 | with: 32 | redis-version: '4.x' 33 | - uses: actions/checkout@v2 34 | - name: Test 35 | run: | 36 | cp goworld_actions.ini goworld.ini 37 | go mod tidy 38 | bash covertest.sh 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v1 41 | -------------------------------------------------------------------------------- /engine/common/hash_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, Suryandaru Triandana 2 | // All rights reserved. 3 | // 4 | // Use of this source code is governed by a BSD-style license that can be 5 | // found in the LICENSE file. 6 | 7 | package common 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | var hashTests = []struct { 14 | data []byte 15 | seed uint32 16 | hash uint32 17 | }{ 18 | {nil, 0xbc9f1d34, 0xbc9f1d34}, 19 | {[]byte{0x62}, 0xbc9f1d34, 0xef1345c4}, 20 | {[]byte{0xc3, 0x97}, 0xbc9f1d34, 0x5b663814}, 21 | {[]byte{0xe2, 0x99, 0xa5}, 0xbc9f1d34, 0x323c078f}, 22 | {[]byte{0xe1, 0x80, 0xb9, 0x32}, 0xbc9f1d34, 0xed21633a}, 23 | {[]byte{ 24 | 0x01, 0xc0, 0x00, 0x00, 25 | 0x00, 0x00, 0x00, 0x00, 26 | 0x00, 0x00, 0x00, 0x00, 27 | 0x00, 0x00, 0x00, 0x00, 28 | 0x14, 0x00, 0x00, 0x00, 29 | 0x00, 0x00, 0x04, 0x00, 30 | 0x00, 0x00, 0x00, 0x14, 31 | 0x00, 0x00, 0x00, 0x18, 32 | 0x28, 0x00, 0x00, 0x00, 33 | 0x00, 0x00, 0x00, 0x00, 34 | 0x02, 0x00, 0x00, 0x00, 35 | 0x00, 0x00, 0x00, 0x00, 36 | }, 0x12345678, 0xf333dabb}, 37 | } 38 | 39 | func TestHash(t *testing.T) { 40 | for i, x := range hashTests { 41 | h := HashSeed(x.data, x.seed) 42 | if h != x.hash { 43 | t.Fatalf("test-%d: invalid hash, %#x vs %#x", i, h, x.hash) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /engine/common/hash.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, Suryandaru Triandana 2 | // All rights reserved. 3 | // 4 | // Use of this source code is governed by a BSD-style license that can be 5 | // found in the LICENSE file. 6 | 7 | package common 8 | 9 | import ( 10 | "encoding/binary" 11 | ) 12 | 13 | func HashString(s string) uint32 { 14 | return Hash([]byte(s)) 15 | } 16 | 17 | // Hash returns the hash of the given data 18 | func Hash(data []byte) uint32 { 19 | return HashSeed(data, 0xbc9f1d34) 20 | } 21 | 22 | // HashSeed return hash of the given data, using specified seed 23 | func HashSeed(data []byte, seed uint32) uint32 { 24 | // Similar to murmur hash 25 | const ( 26 | m = uint32(0xc6a4a793) 27 | r = uint32(24) 28 | ) 29 | var ( 30 | h = seed ^ (uint32(len(data)) * m) 31 | i int 32 | ) 33 | 34 | for n := len(data) - len(data)%4; i < n; i += 4 { 35 | h += binary.LittleEndian.Uint32(data[i:]) 36 | h *= m 37 | h ^= h >> 16 38 | } 39 | 40 | switch len(data) - i { 41 | default: 42 | panic("not reached") 43 | case 3: 44 | h += uint32(data[i+2]) << 16 45 | fallthrough 46 | case 2: 47 | h += uint32(data[i+1]) << 8 48 | fallthrough 49 | case 1: 50 | h += uint32(data[i]) 51 | h *= m 52 | h ^= h >> r 53 | case 0: 54 | } 55 | 56 | return h 57 | } 58 | -------------------------------------------------------------------------------- /cmd/goworld/build.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | ) 8 | 9 | func build(sid ServerID) { 10 | showMsg("building server %s ...", sid) 11 | 12 | buildServer(sid) 13 | buildDispatcher() 14 | buildGate() 15 | } 16 | 17 | func buildServer(sid ServerID) { 18 | serverPath := sid.Path() 19 | showMsg("server directory is %s ...", serverPath) 20 | if !isdir(serverPath) { 21 | showMsgAndQuit("wrong server id: %s, using '\\' instead of '/'?", sid) 22 | } 23 | 24 | showMsg("go build %s ...", sid) 25 | buildDirectory(serverPath) 26 | } 27 | 28 | func buildDispatcher() { 29 | showMsg("go build dispatcher ...") 30 | buildDirectory(filepath.Join(env.GoWorldRoot, "components", "dispatcher")) 31 | } 32 | 33 | func buildGate() { 34 | showMsg("go build gate ...") 35 | buildDirectory(filepath.Join(env.GoWorldRoot, "components", "gate")) 36 | } 37 | 38 | func buildDirectory(dir string) { 39 | var err error 40 | var curdir string 41 | curdir, err = os.Getwd() 42 | checkErrorOrQuit(err, "") 43 | 44 | err = os.Chdir(dir) 45 | checkErrorOrQuit(err, "") 46 | 47 | defer os.Chdir(curdir) 48 | 49 | cmd := exec.Command("go", "build", ".") 50 | cmd.Stderr = os.Stderr 51 | cmd.Stdout = os.Stdout 52 | cmd.Stdin = os.Stdin 53 | err = cmd.Run() 54 | checkErrorOrQuit(err, "") 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /rsa.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDgTCCAmmgAwIBAgIJAK1h/OZmPYQhMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV 3 | BAYTAkNOMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxEDAOBgNVBAMMB2dvd29ybGQwHhcNMTcxMDExMDgwOTIy 5 | WhcNMjcxMDA5MDgwOTIyWjBXMQswCQYDVQQGEwJDTjETMBEGA1UECAwKU29tZS1T 6 | dGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQD 7 | DAdnb3dvcmxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGRhGJC3 8 | rdh3o8mgKAGmD/+G62UZC9gAb4TXUrQdGU9Ej1YlWH899MahgWqLWeJtF17FLflK 9 | 5iICxLet89ySjsb/9Rq1lPDFurZpCm1mFcChy/nOY/knL0/lUkcXi4cXz36mRBwP 10 | kkjzzPVR3dwTVfJQjPGj467en3Za8muFk/FxK9g2K4YP/CrvvRtdLQi1ZGdpF/34 11 | 5fIdOvKGyIO5iWmYfvMMRbSM+mFDgbMFlBi460MRSc/ZYGG6nXGmGB1VjRF1vFIW 12 | hqMLEuOUdU+sxxIXyJ4E/0Ma6pZ+EL3JoJKeLFy7gBItBuDRyf6UyJZHIe3zNlwl 13 | bNeKVeHripcvKwIDAQABo1AwTjAdBgNVHQ4EFgQUTB13Axas8lIJCwnmXmEZi4t+ 14 | OkEwHwYDVR0jBBgwFoAUTB13Axas8lIJCwnmXmEZi4t+OkEwDAYDVR0TBAUwAwEB 15 | /zANBgkqhkiG9w0BAQsFAAOCAQEAYFTwN/++EtFM4J+QgxNGn1cqVHHn48KMdDVD 16 | vE/9dbCvNpH7Wbip5zSAuaZipf+xDNoB/Tb10cVYGO1kNOudX31Q9xhJhwYQcH6K 17 | UTncaF0cStyGEYlea2P9eFKSxFfxujGHvsUlexN1JKdX17/UvWqQz1uN/IPOmfdP 18 | UXa16xaokXSq+fLjPYEImV3/+uszS7VQqXpQNVO56vOpGe+0gTEj/4r6xmu60d23 19 | rGHce7RnHYyQa79dp6t1qSWH4r5i46gYbLu3PDAz3EUQUcJ7KojgTteeKmqhdQ4s 20 | rkbn6eo0E8ievXIT9Qj/r9l7rBH/jQ3zPj/7GyMBsX71RKerMw== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /engine/gwioutil/gwioutil.go: -------------------------------------------------------------------------------- 1 | package gwioutil 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type timeoutError interface { 10 | Timeout() bool // Is it a timeout error 11 | } 12 | 13 | // IsTimeoutError checks if the error is a timeout error 14 | func IsTimeoutError(err error) bool { 15 | if err == nil { 16 | return false 17 | } 18 | 19 | err = errors.Cause(err) 20 | ne, ok := err.(timeoutError) 21 | return ok && ne.Timeout() 22 | } 23 | 24 | // WriteAll write all bytes of data to the writer 25 | func WriteAll(conn io.Writer, data []byte) error { 26 | left := len(data) 27 | for left > 0 { 28 | n, err := conn.Write(data) 29 | if n == left && err == nil { // handle most common case first 30 | return nil 31 | } 32 | 33 | if n > 0 { 34 | data = data[n:] 35 | left -= n 36 | } 37 | 38 | if err != nil && !IsTimeoutError(err) { 39 | return err 40 | } 41 | } 42 | return nil 43 | } 44 | 45 | // ReadAll reads from the reader until all bytes in data is filled 46 | func ReadAll(conn io.Reader, data []byte) error { 47 | left := len(data) 48 | for left > 0 { 49 | n, err := conn.Read(data) 50 | if n == left && err == nil { // handle most common case first 51 | return nil 52 | } 53 | 54 | if n > 0 { 55 | data = data[n:] 56 | left -= n 57 | } 58 | 59 | if err != nil && !IsTimeoutError(err) { 60 | return err 61 | } 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /engine/entity/migarte_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/xiaonanln/goworld/engine/common" 7 | "github.com/xiaonanln/goworld/engine/netutil" 8 | ) 9 | 10 | type TestEntity struct { 11 | Entity 12 | } 13 | 14 | func (e *TestEntity) DescribeEntityType(*EntityTypeDesc) { 15 | 16 | } 17 | 18 | func TestMigrateData(t *testing.T) { 19 | RegisterEntity("TestEntity", &TestEntity{}, false) 20 | e := CreateEntityLocally("TestEntity", nil) 21 | t.Logf("created entity %s", e) 22 | targetSpaceID := common.GenEntityID() 23 | e.Attrs.SetBool("bool", true) 24 | e.Attrs.SetStr("str", "strval") 25 | md := e.GetMigrateData(targetSpaceID, e.Position) 26 | t.Logf("get migrate data: %+v", md) 27 | data, err := netutil.MSG_PACKER.PackMsg(md, nil) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | t.Logf("pack migrate data: %d bytes", len(data)) 32 | 33 | var umd entityMigrateData 34 | if err := netutil.MSG_PACKER.UnpackMsg(data, &umd); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | t.Logf("Pack Migrate Data: %+v", md) 39 | t.Logf("UnPack Migrate Data: %+v", umd) 40 | if umd.SpaceID != md.SpaceID { 41 | t.Fatalf("SpaceID mismatch: %#v & %#v", umd.SpaceID, md.SpaceID) 42 | } 43 | if sv, ok := umd.Attrs["str"]; !ok || sv.(string) != "strval" { 44 | t.Fatalf("str is not strval") 45 | } 46 | if bv, ok := umd.Attrs["bool"]; !ok || bv.(bool) != true { 47 | t.Fatalf("bool is not true") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /engine/kvreg/kvreg.go: -------------------------------------------------------------------------------- 1 | package kvreg 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/xiaonanln/goworld/engine/dispatchercluster" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | "github.com/xiaonanln/goworld/engine/post" 9 | ) 10 | 11 | type RegisterCallback func(ok bool) 12 | 13 | var ( 14 | kvmap = map[string]string{} 15 | postCallbacks []post.PostCallback 16 | ) 17 | 18 | func Register(key string, val string, force bool) { 19 | gwlog.Infof("kvreg: register %s = %s, force=%v", key, val, force) 20 | dispatchercluster.SendKvregRegister(key, val, force) 21 | } 22 | 23 | func TraverseByPrefix(prefix string, cb func(key string, val string)) { 24 | for key, val := range kvmap { 25 | if strings.HasPrefix(key, prefix) { 26 | cb(key, val) 27 | } 28 | } 29 | } 30 | 31 | func WatchKvregRegister(key string, val string) { 32 | gwlog.Infof("kvreg: watch %s = %s", key, val) 33 | kvmap[key] = val 34 | 35 | for _, c := range postCallbacks { 36 | post.Post(c) 37 | } 38 | } 39 | 40 | func ClearByDispatcher(dispid uint16) { 41 | removeKeys := []string(nil) 42 | for key := range kvmap { 43 | if dispatchercluster.SrvIDToDispatcherID(key) == dispid { 44 | removeKeys = append(removeKeys, key) 45 | } 46 | } 47 | for _, key := range removeKeys { 48 | delete(kvmap, key) 49 | } 50 | 51 | for _, c := range postCallbacks { 52 | post.Post(c) 53 | } 54 | } 55 | 56 | func AddPostCallback(cb post.PostCallback) { 57 | postCallbacks = append(postCallbacks, cb) 58 | } 59 | -------------------------------------------------------------------------------- /engine/dispatchercluster/dispatcherclient/DispatcherClient.go: -------------------------------------------------------------------------------- 1 | package dispatcherclient 2 | 3 | import ( 4 | "github.com/xiaonanln/netconnutil" 5 | "net" 6 | 7 | "github.com/xiaonanln/goworld/engine/consts" 8 | "github.com/xiaonanln/goworld/engine/gwlog" 9 | "github.com/xiaonanln/goworld/engine/proto" 10 | ) 11 | 12 | type DispatcherClientType int 13 | 14 | const ( 15 | GameDispatcherClientType DispatcherClientType = 1 + iota 16 | GateDispatcherClientType 17 | ) 18 | 19 | // DispatcherClient is a client connection to the dispatcher 20 | type DispatcherClient struct { 21 | *proto.GoWorldConnection 22 | dctype DispatcherClientType 23 | isReconnect bool 24 | isRestoreGame bool 25 | } 26 | 27 | func newDispatcherClient(dctype DispatcherClientType, conn net.Conn, isReconnect bool, isRestoreGame bool) *DispatcherClient { 28 | if dctype != GameDispatcherClientType && dctype != GateDispatcherClientType { 29 | gwlog.Fatalf("invalid dispatcher client type: %v", dctype) 30 | } 31 | 32 | conn = netconnutil.NewNoTempErrorConn(conn) 33 | 34 | dc := &DispatcherClient{ 35 | dctype: dctype, 36 | isReconnect: isReconnect, 37 | isRestoreGame: isRestoreGame, 38 | } 39 | 40 | dc.GoWorldConnection = proto.NewGoWorldConnection(netconnutil.NewBufferedConn(conn, consts.BUFFERED_READ_BUFFSIZE, consts.BUFFERED_WRITE_BUFFSIZE), dc) 41 | 42 | return dc 43 | } 44 | 45 | // Close the dispatcher client 46 | func (dc *DispatcherClient) Close() error { 47 | return dc.GoWorldConnection.Close() 48 | } 49 | -------------------------------------------------------------------------------- /examples/test_game/OnlineService.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld/engine/common" 5 | "github.com/xiaonanln/goworld/engine/entity" 6 | "github.com/xiaonanln/goworld/engine/gwlog" 7 | ) 8 | 9 | type avatarInfo struct { 10 | name string 11 | level int 12 | } 13 | 14 | // OnlineService is the service entity for maintain total online avatar infos 15 | type OnlineService struct { 16 | entity.Entity 17 | 18 | avatars map[common.EntityID]*avatarInfo 19 | maxlevel int 20 | } 21 | 22 | func (s *OnlineService) DescribeEntityType(desc *entity.EntityTypeDesc) { 23 | } 24 | 25 | // OnInit initialize OnlineService fields 26 | func (s *OnlineService) OnInit() { 27 | s.avatars = map[common.EntityID]*avatarInfo{} 28 | } 29 | 30 | // OnCreated is called when OnlineService is created 31 | func (s *OnlineService) OnCreated() { 32 | gwlog.Infof("Registering OnlineService ...") 33 | } 34 | 35 | // CheckIn is called when Avatars login 36 | func (s *OnlineService) CheckIn(avatarID common.EntityID, name string, level int) { 37 | s.avatars[avatarID] = &avatarInfo{ 38 | name: name, 39 | level: level, 40 | } 41 | if level > s.maxlevel { 42 | s.maxlevel = level 43 | } 44 | gwlog.Infof("%s CHECK IN: %s %s %d, total online %d", s, avatarID, name, level, len(s.avatars)) 45 | } 46 | 47 | // CheckOut is called when Avatars logout 48 | func (s *OnlineService) CheckOut(avatarID common.EntityID) { 49 | delete(s.avatars, avatarID) 50 | gwlog.Infof("%s CHECK OUT: %s, total online %d", s, avatarID, len(s.avatars)) 51 | } 52 | -------------------------------------------------------------------------------- /examples/unity_demo/OnlineService.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld/engine/common" 5 | "github.com/xiaonanln/goworld/engine/entity" 6 | "github.com/xiaonanln/goworld/engine/gwlog" 7 | ) 8 | 9 | type avatarInfo struct { 10 | name string 11 | level int 12 | } 13 | 14 | // OnlineService is the service entity for maintain total online avatar infos 15 | type OnlineService struct { 16 | entity.Entity 17 | 18 | avatars map[common.EntityID]*avatarInfo 19 | maxlevel int 20 | } 21 | 22 | func (s *OnlineService) DescribeEntityType(desc *entity.EntityTypeDesc) { 23 | } 24 | 25 | // OnInit initialize OnlineService fields 26 | func (s *OnlineService) OnInit() { 27 | s.avatars = map[common.EntityID]*avatarInfo{} 28 | } 29 | 30 | // OnCreated is called when OnlineService is created 31 | func (s *OnlineService) OnCreated() { 32 | gwlog.Infof("Registering OnlineService ...") 33 | } 34 | 35 | // CheckIn is called when Avatars login 36 | func (s *OnlineService) CheckIn(avatarID common.EntityID, name string, level int) { 37 | s.avatars[avatarID] = &avatarInfo{ 38 | name: name, 39 | level: level, 40 | } 41 | if level > s.maxlevel { 42 | s.maxlevel = level 43 | } 44 | gwlog.Infof("%s CHECK IN: %s %s %d, total online %d", s, avatarID, name, level, len(s.avatars)) 45 | } 46 | 47 | // CheckOut is called when Avatars logout 48 | func (s *OnlineService) CheckOut(avatarID common.EntityID) { 49 | delete(s.avatars, avatarID) 50 | gwlog.Infof("%s CHECK OUT: %s, total online %d", s, avatarID, len(s.avatars)) 51 | } 52 | -------------------------------------------------------------------------------- /examples/test_client/profile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | var ( 11 | profLock sync.Mutex 12 | profSectionStartTime time.Time 13 | thingMaxTime map[string]time.Duration 14 | thingMinTime map[string]time.Duration 15 | thingTimes map[string]time.Duration 16 | thingCounts map[string]int 17 | ) 18 | 19 | func recordThingTime(thing string, d time.Duration) { 20 | profLock.Lock() 21 | now := time.Now() 22 | if now.Sub(profSectionStartTime) >= time.Second { 23 | // start new profile section 24 | dumpThingProfile() 25 | 26 | profSectionStartTime = now 27 | thingTimes = map[string]time.Duration{} 28 | thingCounts = map[string]int{} 29 | thingMinTime = map[string]time.Duration{} 30 | thingMaxTime = map[string]time.Duration{} 31 | } 32 | 33 | thingCounts[thing] += 1 34 | thingTimes[thing] += d 35 | if oldMaxTime, ok := thingMaxTime[thing]; !ok || oldMaxTime < d { 36 | thingMaxTime[thing] = d 37 | } 38 | if oldMinTime, ok := thingMaxTime[thing]; !ok || oldMinTime > d { 39 | thingMinTime[thing] = d 40 | } 41 | 42 | profLock.Unlock() 43 | } 44 | 45 | func dumpThingProfile() { 46 | for thing, count := range thingCounts { 47 | totalTime := thingTimes[thing] 48 | fmt.Fprintf(os.Stdout, "> %-32s *%d AVG %s RANGE %s ~ %s\n", thing, count, totalTime/time.Duration(count), thingMinTime[thing], thingMaxTime[thing]) 49 | } 50 | fmt.Fprintln(os.Stdout, "===============================================================================") 51 | } 52 | -------------------------------------------------------------------------------- /cmd/goworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | var arguments struct { 12 | runInDaemonMode bool 13 | } 14 | 15 | func parseArgs() { 16 | //flag.StringVar(&arguments.configFile, "configfile", "", "set config file path") 17 | flag.BoolVar(&arguments.runInDaemonMode, "d", false, "run in daemon mode") 18 | flag.Parse() 19 | } 20 | 21 | func main() { 22 | parseArgs() 23 | args := flag.Args() 24 | showMsg("arguments: %s", strings.Join(args, " ")) 25 | 26 | detectGoWorldPath() 27 | 28 | if len(args) == 0 { 29 | showMsg("no command to execute") 30 | flag.Usage() 31 | fmt.Fprintf(os.Stderr, "\tgoworld [server-id]\n") 32 | os.Exit(1) 33 | } 34 | 35 | cmd := args[0] 36 | 37 | if cmd == "build" || cmd == "start" || cmd == "stop" || cmd == "reload" || cmd == "kill" { 38 | if len(args) != 2 { 39 | showMsgAndQuit("server id is not given") 40 | } 41 | } 42 | 43 | if cmd == "build" { 44 | build(ServerID(args[1])) 45 | } else if cmd == "start" { 46 | start(ServerID(args[1])) 47 | } else if cmd == "stop" { 48 | if runtime.GOOS == "windows" { 49 | showMsgAndQuit("stop does not work on Windows, use kill instead (will lose player data)") 50 | } 51 | 52 | stop(ServerID(args[1])) 53 | } else if cmd == "reload" { 54 | reload(ServerID(args[1])) 55 | } else if cmd == "kill" { 56 | kill(ServerID(args[1])) 57 | } else if cmd == "status" { 58 | status() 59 | } else { 60 | showMsgAndQuit("unknown command: %s", cmd) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /engine/entity/attr.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "github.com/xiaonanln/goworld/engine/gwlog" 4 | 5 | type attrFlag int 6 | 7 | const ( 8 | afClient attrFlag = 1 << iota 9 | afAllClient 10 | ) 11 | 12 | func getPathFromOwner(a interface{}, path []interface{}) []interface{} { 13 | forloop: 14 | for { 15 | switch ma := a.(type) { 16 | case *MapAttr: 17 | if ma.parent != nil { 18 | path = append(path, ma.pkey) 19 | a = ma.parent 20 | } else { 21 | break forloop 22 | } 23 | case *ListAttr: 24 | if ma.parent != nil { 25 | path = append(path, ma.pkey) 26 | a = ma.parent 27 | } else { 28 | break forloop 29 | } 30 | default: 31 | gwlog.Panicf("getPathFromOwner: invalid parent type: %T", a) 32 | } 33 | } 34 | 35 | return path 36 | } 37 | 38 | // uniformAttrType convert v to uniform attr type: int64, float64, bool, string 39 | func uniformAttrType(v interface{}) interface{} { 40 | switch av := v.(type) { 41 | case bool: 42 | return av 43 | case string: 44 | return av 45 | case float64: 46 | return float64(av) 47 | case float32: 48 | return float64(av) 49 | case int64: 50 | return av 51 | case uint64: 52 | return int64(av) 53 | case int: 54 | return int64(av) 55 | case uint: 56 | return int64(av) 57 | case int32: 58 | return int64(av) 59 | case uint32: 60 | return int64(av) 61 | case int16: 62 | return int64(av) 63 | case uint16: 64 | return int64(av) 65 | case int8: 66 | return int64(av) 67 | case byte: 68 | return int64(av) 69 | 70 | default: 71 | gwlog.Panicf("can not uniform attr val %+v type %T", v, v) 72 | return v 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/test_game.yml: -------------------------------------------------------------------------------- 1 | name: Test Game 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | cancel-previous-runs: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: rokroskar/workflow-run-cleanup-action@master 10 | env: 11 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 12 | if: "github.ref != 'refs/heads/master'" 13 | 14 | test_game: 15 | name: Test Game (Go ${{ matrix.go }}, ${{ matrix.os }}) 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | go: [1.17, 1.18] 20 | os: ["ubuntu-20.04"] 21 | mongodb-version: [3.6] 22 | steps: 23 | - uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ matrix.go }} 26 | - name: Start MongoDB 27 | uses: supercharge/mongodb-github-action@1.3.0 28 | with: 29 | mongodb-version: ${{ matrix.mongodb-version }} 30 | - uses: shogo82148/actions-setup-redis@v1 31 | with: 32 | redis-version: '4.x' 33 | - uses: actions/checkout@v2 34 | - name: Test Game 35 | run: | 36 | cp goworld_actions.ini goworld.ini 37 | go mod tidy 38 | go install ./cmd/... 39 | goworld build examples/test_client 40 | goworld build examples/test_game 41 | goworld start examples/test_game 42 | sleep 5 43 | examples/test_client/test_client -N 200 -strict -duration 300 44 | sleep 5 45 | goworld reload examples/test_game 46 | sleep 5 47 | examples/test_client/test_client -N 200 -strict -duration 60 48 | sleep 1 49 | goworld stop examples/test_game 50 | -------------------------------------------------------------------------------- /engine/entity/space_ops.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/xiaonanln/goworld/engine/common" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | "github.com/xiaonanln/goworld/engine/uuid" 9 | ) 10 | 11 | // CreateSpaceLocally creates a space in the local game server 12 | func CreateSpaceLocally(kind int) *Space { 13 | if kind == 0 { 14 | gwlog.Panicf("Can not create nil space with kind=0. Game will create 1 nil space automatically.") 15 | } 16 | e := createEntity(_SPACE_ENTITY_TYPE, nil, Vector3{}, "", map[string]interface{}{ 17 | _SPACE_KIND_ATTR_KEY: kind, 18 | }) 19 | return e.AsSpace() 20 | } 21 | 22 | // CreateSpaceSomewhere creates a space in any game server 23 | func CreateSpaceSomewhere(gameid uint16, kind int) common.EntityID { 24 | if kind == 0 { 25 | gwlog.Panicf("Can not create nil space with kind=0. Game will create 1 nil space automatically.") 26 | } 27 | return createEntitySomewhere(gameid, _SPACE_ENTITY_TYPE, map[string]interface{}{ 28 | _SPACE_KIND_ATTR_KEY: kind, 29 | }) 30 | } 31 | 32 | // CreateNilSpace creates the nil space 33 | func CreateNilSpace(gameid uint16) *Space { 34 | spaceID := GetNilSpaceID(gameid) 35 | e := createEntity(_SPACE_ENTITY_TYPE, nil, Vector3{}, spaceID, map[string]interface{}{ 36 | _SPACE_KIND_ATTR_KEY: 0, 37 | }) 38 | return e.AsSpace() 39 | } 40 | 41 | // GetNilSpaceEntityID returns the EntityID for Nil Space on the specified game 42 | // GoWorld uses fixed EntityID for nil spaces on each game 43 | func GetNilSpaceID(gameid uint16) common.EntityID { 44 | gameidStr := strconv.Itoa(int(gameid)) 45 | return common.EntityID(uuid.GenFixedUUID([]byte(gameidStr))) 46 | } 47 | 48 | func GetNilSpace() *Space { 49 | return nilSpace 50 | } 51 | -------------------------------------------------------------------------------- /engine/netutil/PacketConnection.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/xiaonanln/pktconn" 7 | "net" 8 | ) 9 | 10 | // PacketConnection is a connection that send and receive data packets upon a network stream connection 11 | type PacketConnection pktconn.PacketConn 12 | 13 | // NewPacketConnection creates a packet connection based on network connection 14 | func NewPacketConnection(conn Connection, tag interface{}) *PacketConnection { 15 | config := pktconn.DefaultConfig() 16 | config.Tag = tag 17 | return (*PacketConnection)(pktconn.NewPacketConnWithConfig(context.TODO(), conn, config)) 18 | } 19 | 20 | // NewPacket allocates a new packet (usually for sending) 21 | func (pc *PacketConnection) NewPacket() *Packet { 22 | return NewPacket() 23 | } 24 | 25 | // SendPacket send packets to remote 26 | func (pc *PacketConnection) SendPacket(packet *Packet) { 27 | (*pktconn.PacketConn)(pc).Send((*pktconn.Packet)(packet)) 28 | } 29 | 30 | // RecvPacket receives the next packet 31 | func (pc *PacketConnection) RecvChan(recvChan chan *pktconn.Packet) error { 32 | return (*pktconn.PacketConn)(pc).RecvChan(recvChan) 33 | } 34 | 35 | // Close the connection 36 | func (pc *PacketConnection) Close() error { 37 | return (*pktconn.PacketConn)(pc).Close() 38 | } 39 | 40 | // RemoteAddr return the remote address 41 | func (pc *PacketConnection) RemoteAddr() net.Addr { 42 | return (*pktconn.PacketConn)(pc).RemoteAddr() 43 | } 44 | 45 | // LocalAddr returns the local address 46 | func (pc *PacketConnection) LocalAddr() net.Addr { 47 | return (*pktconn.PacketConn)(pc).LocalAddr() 48 | } 49 | 50 | func (pc *PacketConnection) String() string { 51 | return fmt.Sprintf("[%s >>> %s]", pc.LocalAddr(), pc.RemoteAddr()) 52 | } 53 | -------------------------------------------------------------------------------- /engine/netutil/TCPServer.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/xiaonanln/goworld/engine/gwioutil" 8 | "github.com/xiaonanln/goworld/engine/gwlog" 9 | ) 10 | 11 | const ( 12 | _RESTART_TCP_SERVER_INTERVAL = 3 * time.Second 13 | ) 14 | 15 | // TCPServerDelegate is the implementations that a TCP server should provide 16 | type TCPServerDelegate interface { 17 | ServeTCPConnection(net.Conn) 18 | } 19 | 20 | // ServeTCPForever serves on specified address as TCP server, for ever ... 21 | func ServeTCPForever(listenAddr string, delegate TCPServerDelegate) { 22 | for { 23 | err := serveTCPForeverOnce(listenAddr, delegate) 24 | gwlog.Errorf("server@%s failed with error: %v, will restart after %s", listenAddr, err, _RESTART_TCP_SERVER_INTERVAL) 25 | time.Sleep(_RESTART_TCP_SERVER_INTERVAL) 26 | } 27 | } 28 | 29 | func serveTCPForeverOnce(listenAddr string, delegate TCPServerDelegate) error { 30 | defer func() { 31 | if err := recover(); err != nil { 32 | gwlog.TraceError("serveTCPImpl: paniced with error %s", err) 33 | } 34 | }() 35 | 36 | return ServeTCP(listenAddr, delegate) 37 | 38 | } 39 | 40 | // ServeTCP serves on specified address as TCP server 41 | func ServeTCP(listenAddr string, delegate TCPServerDelegate) error { 42 | ln, err := net.Listen("tcp", listenAddr) 43 | gwlog.Infof("Listening on TCP: %s ...", listenAddr) 44 | 45 | if err != nil { 46 | return err 47 | } 48 | 49 | defer ln.Close() 50 | 51 | for { 52 | conn, err := ln.Accept() 53 | if err != nil { 54 | if gwioutil.IsTimeoutError(err) { 55 | continue 56 | } else { 57 | return err 58 | } 59 | } 60 | 61 | gwlog.Infof("Connection from: %s", conn.RemoteAddr()) 62 | go delegate.ServeTCPConnection(conn) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rsa.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAuGRhGJC3rdh3o8mgKAGmD/+G62UZC9gAb4TXUrQdGU9Ej1Yl 3 | WH899MahgWqLWeJtF17FLflK5iICxLet89ySjsb/9Rq1lPDFurZpCm1mFcChy/nO 4 | Y/knL0/lUkcXi4cXz36mRBwPkkjzzPVR3dwTVfJQjPGj467en3Za8muFk/FxK9g2 5 | K4YP/CrvvRtdLQi1ZGdpF/345fIdOvKGyIO5iWmYfvMMRbSM+mFDgbMFlBi460MR 6 | Sc/ZYGG6nXGmGB1VjRF1vFIWhqMLEuOUdU+sxxIXyJ4E/0Ma6pZ+EL3JoJKeLFy7 7 | gBItBuDRyf6UyJZHIe3zNlwlbNeKVeHripcvKwIDAQABAoIBAEeYAfMtzYOcdueL 8 | U7n02euARXyEZnMozRJ/u7MK5/l5w577zchMr1uo+/Bp0+10cvMOAvVUw/YS4oxK 9 | 3SnATM9PCPa8BiDsU3mpdaNs0qRDhQ7T0YUTqlk+ZkXKAKRWMbKI9Dmhw5IC7VZ5 10 | Me44kvFeAlSXRhETRrEXnTxe7yB/h7+UjyFzBfSU0JdcOkf2e45jayCzWKadVjV+ 11 | ZohDUK9/t6e7l1SEPVZQJhwstkXZFFTc3+68nb1oeSl5uU+GB/1sumkG4ggpl0qL 12 | WpTuvDfnf+eBdvZutlrhzicdmG8DR5ANsrMOJN49XZ6GDZblBAG4Y/+HPPtyUG+2 13 | xAOCTSECgYEA2ewnc/8BwbWkiARxA2OtRlSTvMJKdKJuRhYO6bxaBGRYZ4PLMuXE 14 | yoHZyEoYVxqnp2M72nUVvEYDt3h/ISQdKomlWEwKuKiHh7D0Y54HtAKBOYMFc+Yi 15 | 0gtKDiOFztMXKMH0oC3YG6lzeDN0ATnyJt60unoNBziDiJEBjr11SRkCgYEA2Jxi 16 | j5wQ+lrHdv3QLpCMO3FNsSmhorUO4310r5ep46LmbCmfavMLurf53XwwRuffWaAp 17 | So2xt+n+ffGMnlVVLy8T5/Bv0cY/aiY/tDAuyetsJjbvH294ASUTpS2CB5fxbCqr 18 | r4uagj14+zizOEIs0b4t4E3uxfd9PHEUjylYDuMCgYEAmd9wwCvoTqH2agBQ2CbS 19 | m51ur9K0hgSHPr+mig3vtbgw3+6kVOz+dksXvp/q7d4pUTz1bzxLO6RoTW0svvbk 20 | DTwh3uXakCaXhA1Dku9r2wQFwNktyXdPUOadxLv6aF6OtL23AD8+n9GXceFK4O7M 21 | d/u6uw0hE98oqQ5SfjpyjUkCgYAH4E/ZJvSbFdfw5LPALbNbqfgIPBpLf0fmT7aB 22 | eANOaqr0PeM1EVdY1723Jv/eZCoD6UX3FwXiSRje3XyeeT3atFyF74ExYIMHyhJA 23 | 7AuXff71uMOjIft8FywKsofq3MSDiV2qyWm52KBgiiRCJG3axo6GfG71NdWQ8A+u 24 | UPOJvwKBgQDGYL1pw8mof2KvZBqcoXsI3bKSOE9n2XVCrTAPXgrB3sonInsJVUY4 25 | n2tqxbkDtJVcVHnxtyeuqlQfS0eBPwdHTFISwmiMDDKlGtS4E0bOKUyQQwxDh4WB 26 | MvQTYHuW/lJZYUhb5DW2yP3rxxWunteSOCFC2YOBGM2kHQbpqgbbGg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /engine/entity/Vector3.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Coord is the of coordinations entity position (x, y, z) 9 | type Coord float32 10 | 11 | // Vector3 is type of entity position 12 | type Vector3 struct { 13 | X Coord 14 | Y Coord 15 | Z Coord 16 | } 17 | 18 | func (p Vector3) String() string { 19 | return fmt.Sprintf("(%.2f, %.2f, %.2f)", p.X, p.Y, p.Z) 20 | } 21 | 22 | // DistanceTo calculates distance between two positions 23 | func (p Vector3) DistanceTo(o Vector3) Coord { 24 | dx := p.X - o.X 25 | dy := p.Y - o.Y 26 | dz := p.Z - o.Z 27 | return Coord(math.Sqrt(float64(dx*dx + dy*dy + dz*dz))) 28 | } 29 | 30 | // Sub calculates Vector3 p - Vector3 o 31 | func (p Vector3) Sub(o Vector3) Vector3 { 32 | return Vector3{p.X - o.X, p.Y - o.Y, p.Z - o.Z} 33 | } 34 | 35 | func (p Vector3) Add(o Vector3) Vector3 { 36 | return Vector3{p.X + o.X, p.Y + o.Y, p.Z + o.Z} 37 | } 38 | 39 | // Mul calculates Vector3 p * m 40 | func (p Vector3) Mul(m Coord) Vector3 { 41 | return Vector3{p.X * m, p.Y * m, p.Z * m} 42 | } 43 | 44 | // DirToYaw convert direction represented by Vector3 to Yaw 45 | func (dir Vector3) DirToYaw() Yaw { 46 | dir.Normalize() 47 | 48 | yaw := math.Acos(float64(dir.X)) 49 | if dir.Z < 0 { 50 | yaw = math.Pi*2 - yaw 51 | } 52 | 53 | yaw = yaw / math.Pi * 180 // convert to angle 54 | 55 | if yaw <= 90 { 56 | yaw = 90 - yaw 57 | } else { 58 | yaw = 90 + (360 - yaw) 59 | } 60 | 61 | return Yaw(yaw) 62 | } 63 | 64 | func (p *Vector3) Normalize() { 65 | d := Coord(math.Sqrt(float64(p.X*p.X + p.Y*p.Y + p.Z*p.Z))) 66 | if d == 0 { 67 | return 68 | } 69 | p.X /= d 70 | p.Y /= d 71 | p.Z /= d 72 | } 73 | 74 | func (p Vector3) Normalized() Vector3 { 75 | p.Normalize() 76 | return p 77 | } 78 | -------------------------------------------------------------------------------- /examples/chatroom_demo/Avatar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "regexp" 7 | 8 | "github.com/xiaonanln/goworld/engine/common" 9 | "github.com/xiaonanln/goworld/engine/entity" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | ) 12 | 13 | // Avatar 对象代表一名玩家 14 | type Avatar struct { 15 | entity.Entity 16 | } 17 | 18 | func (a *Avatar) DescribeEntityType(desc *entity.EntityTypeDesc) { 19 | desc.SetPersistent(true).SetUseAOI(true, 100) 20 | desc.DefineAttr("name", "Client", "Persistent") 21 | desc.DefineAttr("chatroom", "Client") 22 | } 23 | 24 | // OnCreated 在Avatar对象创建后被调用 25 | func (a *Avatar) OnCreated() { 26 | a.Entity.OnCreated() 27 | a.setDefaultAttrs() 28 | } 29 | 30 | // setDefaultAttrs 设置玩家的一些默认属性 31 | func (a *Avatar) setDefaultAttrs() { 32 | a.Attrs.SetDefaultStr("name", "noname") 33 | a.SetClientFilterProp("chatroom", "1") 34 | a.Attrs.SetStr("chatroom", "1") 35 | } 36 | 37 | // GetSpaceID 获得玩家的场景ID并发给调用者 38 | func (a *Avatar) GetSpaceID(callerID common.EntityID) { 39 | a.Call(callerID, "OnGetAvatarSpaceID", a.ID, a.Space.ID) 40 | } 41 | 42 | var spaceSep = regexp.MustCompile("\\s") 43 | 44 | // SendChat_Client 是用来发送聊天信息的客户端RPC 45 | func (a *Avatar) SendChat_Client(text string) { 46 | text = strings.TrimSpace(text) 47 | if text[0] == '/' { 48 | // this is a command 49 | cmd := spaceSep.Split(text[1:], -1) 50 | if cmd[0] == "join" { 51 | a.enterRoom(cmd[1]) 52 | } else { 53 | a.CallClient("ShowError", "无法识别的命令:"+cmd[0]) 54 | } 55 | } else { 56 | a.CallFilteredClients("chatroom", "=", a.GetStr("chatroom"), "OnRecvChat", a.GetStr("name"), text) 57 | } 58 | } 59 | 60 | // enterRoom 进入一个聊天室,本质上就是设置Filter属性 61 | func (a *Avatar) enterRoom(name string) { 62 | gwlog.Debugf("%s enter room %s", a, name) 63 | a.SetClientFilterProp("chatroom", name) 64 | a.Attrs.SetStr("chatroom", name) 65 | } 66 | -------------------------------------------------------------------------------- /engine/storage/backend/mongodb/mongodb_test.go: -------------------------------------------------------------------------------- 1 | package entitystoragemongodb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/xiaonanln/goworld/engine/common" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | ) 9 | 10 | func TestMongoDBEntityStorage(t *testing.T) { 11 | es, err := OpenMongoDB("mongodb://localhost:27017/goworld", "goworld") 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | gwlog.Infof("TestMongoDBEntityStorage: %v", es) 16 | entityID := common.GenEntityID() 17 | gwlog.Infof("TESTING ENTITYID: %s", entityID) 18 | data, err := es.Read("Avatar", entityID) 19 | if data != nil { 20 | t.Errorf("should be nil") 21 | } 22 | 23 | testData := map[string]interface{}{ 24 | "a": 1, 25 | "b": "2", 26 | "c": true, 27 | "d": 1.11, 28 | } 29 | es.Write("Avatar", entityID, testData) 30 | 31 | verifyData, err := es.Read("Avatar", entityID) 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | 36 | if verifyData.(map[string]interface{})["a"].(int) != 1 { 37 | t.Errorf("read wrong data: %v", verifyData) 38 | } 39 | if verifyData.(map[string]interface{})["b"].(string) != "2" { 40 | t.Errorf("read wrong data: %v", verifyData) 41 | } 42 | if verifyData.(map[string]interface{})["c"].(bool) != true { 43 | t.Errorf("read wrong data: %v", verifyData) 44 | } 45 | if verifyData.(map[string]interface{})["d"].(float64) != 1.11 { 46 | t.Errorf("read wrong data: %v", verifyData) 47 | } 48 | 49 | avatarIDs, err := es.List("Avatar") 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | if len(avatarIDs) == 0 { 54 | t.Errorf("Avatar IDs is empty!") 55 | } 56 | 57 | gwlog.Infof("Found avatars saved: %v", avatarIDs) 58 | for _, avatarID := range avatarIDs { 59 | data, err := es.Read("Avatar", avatarID) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | t.Logf("Read Avatar %s => %v", avatarID, data) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /engine/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "encoding/json" 7 | 8 | "github.com/bmizerany/assert" 9 | "github.com/xiaonanln/goworld/engine/gwlog" 10 | ) 11 | 12 | func init() { 13 | SetConfigFile("../../goworld.ini.sample") 14 | } 15 | 16 | func TestLoad(t *testing.T) { 17 | config := Get() 18 | gwlog.Debugf("goworld config: \n%s", config) 19 | if config == nil { 20 | t.FailNow() 21 | } 22 | 23 | for dispid, dispatcherConfig := range config._Dispatchers { 24 | if dispatcherConfig.AdvertiseAddr == "" { 25 | t.Errorf("dispatch %d: advertise addr not found", dispid) 26 | } 27 | } 28 | 29 | gwlog.Infof("read goworld config: %v", config) 30 | } 31 | 32 | func TestReload(t *testing.T) { 33 | Get() 34 | config := Reload() 35 | gwlog.Debugf("goworld config: \n%s", config) 36 | } 37 | 38 | func TestGetDeployment(t *testing.T) { 39 | cfg := GetDeployment() 40 | cfgStr, _ := json.Marshal(cfg) 41 | t.Logf("deployment config: %s", string(cfgStr)) 42 | } 43 | 44 | func TestGetDispatcher(t *testing.T) { 45 | cfg := GetDispatcher(1) 46 | cfgStr, _ := json.Marshal(cfg) 47 | t.Logf("dispatcher config: %s", string(cfgStr)) 48 | } 49 | 50 | func TestGetGame(t *testing.T) { 51 | for id := 1; id <= 10; id++ { 52 | cfg := GetGame(uint16(id)) 53 | if cfg == nil { 54 | t.Logf("Game %d not found", id) 55 | } else { 56 | t.Logf("Game %d config: %v", id, cfg) 57 | } 58 | } 59 | } 60 | 61 | func TestGetStorage(t *testing.T) { 62 | cfg := GetStorage() 63 | if cfg == nil { 64 | t.Errorf("storage config not found") 65 | } 66 | gwlog.Infof("storage config:") 67 | t.Logf("%s\n", DumpPretty(cfg)) 68 | } 69 | 70 | func TestGetKVDB(t *testing.T) { 71 | assert.T(t, GetKVDB() != nil, "kvdb config is nil") 72 | } 73 | 74 | func TestGetGate(t *testing.T) { 75 | GetGate(1) 76 | } 77 | 78 | func TestSetConfigFile(t *testing.T) { 79 | SetConfigFile("../../goworld.ini") 80 | } 81 | -------------------------------------------------------------------------------- /components/dispatcher/lbcheap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "container/heap" 5 | 6 | "github.com/xiaonanln/goworld/engine/config" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | "github.com/xiaonanln/goworld/engine/proto" 9 | ) 10 | 11 | type lbcheapentry struct { 12 | gameid uint16 13 | heapidx int // index of this entry in the heap 14 | CPUPercent float64 15 | origCPUPercent float64 16 | } 17 | 18 | func (e *lbcheapentry) update(info proto.GameLBCInfo) { 19 | e.origCPUPercent = info.CPUPercent 20 | e.CPUPercent = info.CPUPercent 21 | } 22 | 23 | type lbcheap []*lbcheapentry 24 | 25 | func (h lbcheap) Len() int { 26 | return len(h) 27 | } 28 | 29 | func (h lbcheap) Less(i, j int) bool { 30 | return h[i].CPUPercent < h[j].CPUPercent 31 | } 32 | 33 | func (h lbcheap) Swap(i, j int) { 34 | // need to swap heapidx 35 | h[i].heapidx, h[j].heapidx = h[j].heapidx, h[i].heapidx 36 | h[i], h[j] = h[j], h[i] 37 | } 38 | 39 | func (h *lbcheap) Push(x interface{}) { 40 | entry := x.(*lbcheapentry) 41 | entry.heapidx = len(*h) 42 | *h = append(*h, entry) 43 | } 44 | 45 | func (h *lbcheap) Pop() interface{} { 46 | old := *h 47 | n := len(old) 48 | x := old[n-1] 49 | *h = old[0 : n-1] 50 | return x 51 | } 52 | 53 | func (h lbcheap) validateHeapIndexes() { 54 | if !config.Debug() { 55 | return 56 | } 57 | 58 | gameids := []uint16{} 59 | for i := 0; i < len(h); i++ { 60 | if h[i].heapidx != i { 61 | gwlog.Fatalf("lbcheap elem at index %d but has heapidx=%d", i, h[i].heapidx) 62 | } 63 | if i > 0 { 64 | if h.Less(i, 0) { 65 | gwlog.Fatalf("lbcheap elem at index 0 is not min") 66 | } 67 | } 68 | gameids = append(gameids, h[i].gameid) 69 | } 70 | //gwlog.Infof("lbcheap: gameids: %v", gameids) 71 | } 72 | func (h *lbcheap) chosen(idx int) { 73 | entry := (*h)[idx] 74 | if entry.CPUPercent < entry.origCPUPercent+10 { 75 | entry.CPUPercent += 0.1 76 | heap.Fix(h, idx) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /components/dispatcher/DispatcherClientProxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/netconnutil" 5 | "net" 6 | 7 | "fmt" 8 | 9 | "github.com/xiaonanln/goworld/engine/consts" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | "github.com/xiaonanln/goworld/engine/netutil" 12 | "github.com/xiaonanln/goworld/engine/post" 13 | "github.com/xiaonanln/goworld/engine/proto" 14 | ) 15 | 16 | type dispatcherClientProxy struct { 17 | *proto.GoWorldConnection 18 | owner *DispatcherService 19 | gameid uint16 20 | gateid uint16 21 | } 22 | 23 | func newDispatcherClientProxy(owner *DispatcherService, conn net.Conn) *dispatcherClientProxy { 24 | conn = netconnutil.NewNoTempErrorConn(conn) 25 | 26 | dcp := &dispatcherClientProxy{ 27 | owner: owner, 28 | } 29 | 30 | dcp.GoWorldConnection = proto.NewGoWorldConnection(netconnutil.NewBufferedConn(conn, consts.BUFFERED_READ_BUFFSIZE, consts.BUFFERED_WRITE_BUFFSIZE), dcp) 31 | 32 | return dcp 33 | } 34 | 35 | func (dcp *dispatcherClientProxy) serve() { 36 | // Serve the dispatcher client from server / gate 37 | defer func() { 38 | dcp.Close() 39 | post.Post(func() { 40 | dcp.owner.handleDispatcherClientDisconnect(dcp) 41 | }) 42 | err := recover() 43 | if err != nil && !netutil.IsConnectionError(err) { 44 | gwlog.TraceError("Client %s paniced with error: %v", dcp, err) 45 | } 46 | }() 47 | 48 | gwlog.Infof("New dispatcher client: %s", dcp) 49 | 50 | err := dcp.GoWorldConnection.RecvChan(dcp.owner.messageQueue) 51 | if err != nil { 52 | gwlog.Panic(err) 53 | } 54 | } 55 | 56 | func (dcp *dispatcherClientProxy) String() string { 57 | if dcp.gameid > 0 { 58 | return fmt.Sprintf("dispatcherClientProxy", dcp.gameid, dcp.RemoteAddr()) 59 | } else if dcp.gateid > 0 { 60 | return fmt.Sprintf("dispatcherClientProxy", dcp.gateid, dcp.RemoteAddr()) 61 | } else { 62 | return fmt.Sprintf("dispatcherClientProxy<%s>", dcp.RemoteAddr()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/test_game/test_game.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/xiaonanln/goTimer" 7 | "github.com/xiaonanln/goworld" 8 | "github.com/xiaonanln/goworld/engine/gwlog" 9 | "github.com/xiaonanln/goworld/ext/pubsub" 10 | ) 11 | 12 | var ( 13 | _SERVICE_NAMES = []string{ 14 | "OnlineService", 15 | "SpaceService", 16 | "MailService", 17 | pubsub.ServiceName, 18 | } 19 | ) 20 | 21 | func init() { 22 | 23 | } 24 | 25 | func main() { 26 | goworld.RegisterSpace(&MySpace{}) // Register the space type 27 | 28 | // Register each entity types 29 | goworld.RegisterEntity("Account", &Account{}) 30 | goworld.RegisterEntity("AOITester", &AOITester{}) 31 | goworld.RegisterService("OnlineService", &OnlineService{}, 3) 32 | goworld.RegisterService("SpaceService", &SpaceService{}, 3) 33 | // todo: implement sharding for MailService. Currently, MailService only allows 1 shard 34 | goworld.RegisterService("MailService", &MailService{}, 1) 35 | 36 | pubsub.RegisterService(3) 37 | 38 | // Register Monster type and define attributes 39 | goworld.RegisterEntity("Monster", &Monster{}) 40 | // Register Avatar type and define attributes 41 | goworld.RegisterEntity("Avatar", &Avatar{}) 42 | 43 | // Run the game server 44 | goworld.Run() 45 | } 46 | 47 | func checkServerStarted() { 48 | ok := isAllServicesReady() 49 | gwlog.Infof("checkServerStarted: %v", ok) 50 | if ok { 51 | onAllServicesReady() 52 | } else { 53 | timer.AddCallback(time.Millisecond*1000, checkServerStarted) 54 | } 55 | } 56 | 57 | func isAllServicesReady() bool { 58 | for _, serviceName := range _SERVICE_NAMES { 59 | if !goworld.CheckServiceEntitiesReady(serviceName) { 60 | gwlog.Infof("%s entities are not ready ...", serviceName) 61 | return false 62 | } 63 | } 64 | return true 65 | } 66 | 67 | func onAllServicesReady() { 68 | gwlog.Infof("ALL SERVICES ARE READY!!!") 69 | goworld.CallNilSpaces("TestCallNilSpaces", 1, "abc", true, 2.3) 70 | } 71 | -------------------------------------------------------------------------------- /engine/kvdb/kvdb_test.go: -------------------------------------------------------------------------------- 1 | package kvdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/xiaonanln/goworld/engine/config" 7 | "github.com/xiaonanln/goworld/engine/kvdb/types" 8 | ) 9 | 10 | func init() { 11 | config.SetConfigFile("../../goworld.ini") 12 | Initialize() 13 | } 14 | 15 | func TestBasic(t *testing.T) { 16 | Put("__key_not_exists__", "", func(err error) { 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | }) 21 | 22 | Get("__key_not_exists__", func(val string, err error) { 23 | if err != nil { 24 | t.Error(err) 25 | return 26 | } 27 | if val != "" { 28 | t.Fail() 29 | } 30 | }) 31 | 32 | Put("a", "111", func(err error) { 33 | if err != nil { 34 | t.Error(err) 35 | return 36 | } 37 | Get("a", func(val string, err error) { 38 | if err != nil { 39 | t.Error(err) 40 | return 41 | } 42 | if val != "111" { 43 | t.Fail() 44 | } 45 | }) 46 | }) 47 | 48 | GetRange("a", "z", func(items []kvdbtypes.KVItem, err error) { 49 | if err != nil { 50 | t.Error(err) 51 | return 52 | } 53 | }) 54 | 55 | GetOrPut("a", "222", func(oldVal string, err error) { 56 | if err != nil { 57 | t.Error(err) 58 | return 59 | } 60 | if oldVal == "" { 61 | t.Errorf("wrong old val: " + oldVal) 62 | return 63 | } 64 | Get("a", func(val string, err error) { 65 | if err != nil { 66 | t.Error(err) 67 | return 68 | } 69 | if val != "111" { 70 | t.Fail() 71 | } 72 | }) 73 | }) 74 | 75 | GetOrPut("__key_not_exists__", "val", func(oldVal string, err error) { 76 | if err != nil { 77 | t.Error(err) 78 | return 79 | } 80 | if oldVal != "" { 81 | t.Errorf("wrong old val: " + oldVal) 82 | return 83 | } 84 | Get("__key_not_exists__", func(val string, err error) { 85 | if err != nil { 86 | t.Error(err) 87 | return 88 | } 89 | if val != "val" { 90 | t.Fail() 91 | } 92 | }) 93 | }) 94 | 95 | Put("__key_not_exists__", "", nil) 96 | } 97 | -------------------------------------------------------------------------------- /goworld.ini.sample: -------------------------------------------------------------------------------- 1 | [debug] 2 | debug = 1 ; set to 0 in production 3 | 4 | [deployment] 5 | desired_dispatchers=1 6 | desired_games=1 7 | desired_gates=1 8 | 9 | [storage] 10 | type=mongodb 11 | url=mongodb://127.0.0.1:27017/ 12 | db=goworld 13 | 14 | [kvdb] 15 | type=mongodb 16 | url=mongodb://127.0.0.1:27017/goworld 17 | db=goworld 18 | collection=__kv__ 19 | ;type=redis 20 | ;url=redis://127.0.0.1:6379 21 | ;db=1 22 | ;type=redis_cluster 23 | ;start_nodes_1=127.0.0.1:6379 24 | ;start_nodes_2=127.0.0.2:6379 25 | 26 | [dispatcher_common] 27 | listen_addr=127.0.0.1:13000 28 | advertise_addr=127.0.0.1:13000 29 | http_addr=127.0.0.1:23000 30 | log_file=dispatcher.log 31 | log_stderr=true 32 | log_level=debug 33 | 34 | [dispatcher1] 35 | listen_addr=127.0.0.1:13001 36 | advertise_addr=127.0.0.1:13001 37 | http_addr=127.0.0.1:23001 38 | [dispatcher2] 39 | listen_addr=127.0.0.1:13002 40 | advertise_addr=127.0.0.1:13002 41 | http_addr=127.0.0.1:23002 42 | 43 | [game_common] 44 | boot_entity=Account 45 | save_interval=600 46 | log_file=game.log 47 | log_stderr=true 48 | http_addr=127.0.0.1:25000 49 | log_level=debug 50 | position_sync_interval_ms=100 ; position sync: server -> client 51 | ; gomaxprocs=0 52 | 53 | [game1] 54 | http_addr=25001 55 | ; ban_boot_entity=false 56 | [game2] 57 | http_addr=25002 58 | ;ban_boot_entity=false 59 | ;[game3] 60 | ;http_addr=25003 61 | ;;ban_boot_entity=false 62 | 63 | [gate_common] 64 | ; gomaxprocs=0 65 | log_file=gate.log 66 | log_stderr=true 67 | http_addr=127.0.0.1:24000 68 | listen_addr=0.0.0.0:14000 69 | log_level=debug 70 | compress_connection=0 71 | encrypt_connection=0 72 | rsa_key=rsa.key 73 | rsa_certificate=rsa.crt 74 | heartbeat_check_interval = 0 75 | position_sync_interval_ms=100 ; position sync: client -> server 76 | 77 | [gate1] 78 | listen_addr=0.0.0.0:14001 79 | http_addr=127.0.0.1:24001 80 | [gate2] 81 | listen_addr=0.0.0.0:14002 82 | http_addr=127.0.0.1:24002 83 | ;[gate3] 84 | ;listen_addr=0.0.0.0:14003 85 | ;http_addr=127.0.0.1:24003 86 | -------------------------------------------------------------------------------- /goworld_actions.ini: -------------------------------------------------------------------------------- 1 | [debug] 2 | debug = 1 ; set to 0 in production 3 | 4 | [deployment] 5 | desired_dispatchers=3 6 | desired_games=3 7 | desired_gates=3 8 | 9 | [storage] 10 | type=mongodb 11 | url=mongodb://127.0.0.1:27017/ 12 | db=goworld 13 | 14 | [kvdb] 15 | type=mongodb 16 | url=mongodb://127.0.0.1:27017/goworld 17 | db=goworld 18 | collection=__kv__ 19 | ;type=redis 20 | ;url=redis://127.0.0.1:6379 21 | ;db=1 22 | ;type=redis_cluster 23 | ;start_nodes_1=127.0.0.1:6379 24 | ;start_nodes_2=127.0.0.2:6379 25 | 26 | [dispatcher_common] 27 | listen_addr=127.0.0.1:13000 28 | advertise_addr=127.0.0.1:13000 29 | http_addr=127.0.0.1:23000 30 | log_file=dispatcher.log 31 | log_stderr=true 32 | log_level=debug 33 | 34 | [dispatcher1] 35 | listen_addr=127.0.0.1:13001 36 | advertise_addr=127.0.0.1:13001 37 | http_addr=127.0.0.1:23001 38 | [dispatcher2] 39 | listen_addr=127.0.0.1:13002 40 | advertise_addr=127.0.0.1:13002 41 | http_addr=127.0.0.1:23002 42 | 43 | [game_common] 44 | boot_entity=Account 45 | save_interval=600 46 | log_file=game.log 47 | log_stderr=true 48 | http_addr=127.0.0.1:25000 49 | log_level=debug 50 | position_sync_interval_ms=100 ; position sync: server -> client 51 | ; gomaxprocs=0 52 | 53 | [game1] 54 | http_addr=25001 55 | ; ban_boot_entity=false 56 | [game2] 57 | http_addr=25002 58 | ;ban_boot_entity=false 59 | ;[game3] 60 | ;http_addr=25003 61 | ;;ban_boot_entity=false 62 | 63 | [gate_common] 64 | ; gomaxprocs=0 65 | log_file=gate.log 66 | log_stderr=true 67 | http_addr=127.0.0.1:24000 68 | listen_addr=0.0.0.0:14000 69 | log_level=debug 70 | compress_connection=1 71 | encrypt_connection=1 72 | rsa_key=rsa.key 73 | rsa_certificate=rsa.crt 74 | heartbeat_check_interval = 0 75 | position_sync_interval_ms=100 ; position sync: client -> server 76 | 77 | [gate1] 78 | listen_addr=0.0.0.0:14001 79 | http_addr=127.0.0.1:24001 80 | [gate2] 81 | listen_addr=0.0.0.0:14002 82 | http_addr=127.0.0.1:24002 83 | ;[gate3] 84 | ;listen_addr=0.0.0.0:14003 85 | ;http_addr=127.0.0.1:24003 86 | -------------------------------------------------------------------------------- /engine/entity/entity_map.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/xiaonanln/goworld/engine/common" 7 | ) 8 | 9 | // EntityMap is the data structure for maintaining entity IDs to entities 10 | type EntityMap map[common.EntityID]*Entity 11 | 12 | // Add adds a new entity to EntityMap 13 | func (em EntityMap) Add(entity *Entity) { 14 | em[entity.ID] = entity 15 | } 16 | 17 | // Del deletes an entity from EntityMap 18 | func (em EntityMap) Del(id common.EntityID) { 19 | delete(em, id) 20 | } 21 | 22 | // Get returns the Entity of specified entity ID in EntityMap 23 | func (em EntityMap) Get(id common.EntityID) *Entity { 24 | return em[id] 25 | } 26 | 27 | // Keys return keys of the EntityMap in a slice 28 | func (em EntityMap) Keys() (keys []common.EntityID) { 29 | for eid := range em { 30 | keys = append(keys, eid) 31 | } 32 | return 33 | } 34 | 35 | // Values return values of the EntityMap in a slice 36 | func (em EntityMap) Values() (vals []*Entity) { 37 | for _, e := range em { 38 | vals = append(vals, e) 39 | } 40 | return 41 | } 42 | 43 | // EntitySet is the data structure for a set of entities 44 | type EntitySet map[*Entity]struct{} 45 | 46 | // Add adds an entity to the EntitySet 47 | func (es EntitySet) Add(entity *Entity) { 48 | es[entity] = struct{}{} 49 | } 50 | 51 | // Del deletes an entity from the EntitySet 52 | func (es EntitySet) Del(entity *Entity) { 53 | delete(es, entity) 54 | } 55 | 56 | // Contains returns if the entity is in the EntitySet 57 | func (es EntitySet) Contains(entity *Entity) bool { 58 | _, ok := es[entity] 59 | return ok 60 | } 61 | 62 | func (es EntitySet) ForEach(f func(e *Entity)) { 63 | for e := range es { 64 | f(e) 65 | } 66 | } 67 | 68 | func (es EntitySet) String() string { 69 | b := bytes.Buffer{} 70 | b.WriteString("{") 71 | first := true 72 | for entity := range es { 73 | if !first { 74 | b.WriteString(", ") 75 | } else { 76 | first = false 77 | } 78 | b.WriteString(entity.String()) 79 | } 80 | b.WriteString("}") 81 | return b.String() 82 | } 83 | -------------------------------------------------------------------------------- /goworld.ini: -------------------------------------------------------------------------------- 1 | [debug] 2 | debug = 1 ; set to 0 in production 3 | 4 | [deployment] 5 | desired_dispatchers=1 6 | desired_games=1 7 | desired_gates=1 8 | 9 | [storage] 10 | type=mongodb 11 | url=mongodb://127.0.0.1:27017/ 12 | db=goworld 13 | 14 | [kvdb] 15 | type=mongodb 16 | url=mongodb://127.0.0.1:27017/goworld 17 | db=goworld 18 | collection=__kv__ 19 | ;type=redis 20 | ;url=redis://127.0.0.1:6379 21 | ;db=1 22 | ;type=redis_cluster 23 | ;start_nodes_1=127.0.0.1:6379 24 | ;start_nodes_2=127.0.0.2:6379 25 | 26 | [dispatcher_common] 27 | listen_addr=127.0.0.1:13000 28 | advertise_addr=127.0.0.1:13000 29 | http_addr=127.0.0.1:23000 30 | log_file=dispatcher.log 31 | log_stderr=true 32 | log_level=debug 33 | 34 | [dispatcher1] 35 | listen_addr=127.0.0.1:13001 36 | advertise_addr=127.0.0.1:13001 37 | http_addr=127.0.0.1:23001 38 | ;[dispatcher2] 39 | ;listen_addr=127.0.0.1:13002 40 | ;advertise_addr=127.0.0.1:13002 41 | ;http_addr=127.0.0.1:23002 42 | ;[dispatcher3] 43 | ;listen_addr=127.0.0.1:13003 44 | ;advertise_addr=127.0.0.1:13003 45 | ;http_addr=127.0.0.1:23003 46 | 47 | [game_common] 48 | boot_entity=Account 49 | save_interval=600 50 | log_file=game.log 51 | log_stderr=true 52 | http_addr=127.0.0.1:25000 53 | log_level=debug 54 | position_sync_interval_ms=100 ; position sync: server -> client 55 | ; gomaxprocs=0 56 | 57 | [game1] 58 | http_addr=25001 59 | ; ban_boot_entity=false 60 | [game2] 61 | http_addr=25002 62 | [game3] 63 | http_addr=25003 64 | ;ban_boot_entity=false 65 | ;[game3] 66 | ;http_addr=25003 67 | ;;ban_boot_entity=false 68 | 69 | [gate_common] 70 | ; gomaxprocs=0 71 | log_file=gate.log 72 | log_stderr=true 73 | http_addr=127.0.0.1:24000 74 | listen_addr=0.0.0.0:14000 75 | log_level=debug 76 | compress_connection=0 77 | encrypt_connection=0 78 | rsa_key=rsa.key 79 | rsa_certificate=rsa.crt 80 | heartbeat_check_interval = 0 81 | position_sync_interval_ms=100 ; position sync: client -> server 82 | 83 | [gate1] 84 | listen_addr=0.0.0.0:14001 85 | http_addr=127.0.0.1:24001 86 | [gate2] 87 | listen_addr=0.0.0.0:14002 88 | http_addr=127.0.0.1:24002 89 | [gate3] 90 | listen_addr=0.0.0.0:14003 91 | http_addr=127.0.0.1:24003 92 | -------------------------------------------------------------------------------- /cmd/goworld/stop.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "time" 7 | 8 | "github.com/xiaonanln/goworld/cmd/goworld/process" 9 | ) 10 | 11 | func stop(sid ServerID) { 12 | stopWithSignal(sid, StopSignal) 13 | } 14 | 15 | func stopWithSignal(sid ServerID, signal syscall.Signal) { 16 | err := os.Chdir(env.GoWorldRoot) 17 | checkErrorOrQuit(err, "chdir to goworld directory failed") 18 | 19 | ss := detectServerStatus() 20 | showServerStatus(ss) 21 | if !ss.IsRunning() { 22 | // server is not running 23 | showMsgAndQuit("no server is running currently") 24 | } 25 | 26 | if ss.ServerID != "" && ss.ServerID != sid { 27 | showMsgAndQuit("another server is running: %s", ss.ServerID) 28 | } 29 | 30 | stopGates(ss, signal) 31 | stopGames(ss, signal) 32 | stopDispatcher(ss, signal) 33 | } 34 | 35 | func stopGames(ss *ServerStatus, signal syscall.Signal) { 36 | if ss.NumGamesRunning == 0 { 37 | return 38 | } 39 | 40 | showMsg("stop %d games ...", ss.NumGamesRunning) 41 | for _, proc := range ss.GameProcs { 42 | stopProc(proc, signal) 43 | } 44 | } 45 | 46 | func stopDispatcher(ss *ServerStatus, signal syscall.Signal) { 47 | if ss.NumDispatcherRunning == 0 { 48 | return 49 | } 50 | 51 | showMsg("stop dispatcher ...") 52 | for _, proc := range ss.DispatcherProcs { 53 | stopProc(proc, signal) 54 | } 55 | } 56 | 57 | func stopGates(ss *ServerStatus, signal syscall.Signal) { 58 | if ss.NumGatesRunning == 0 { 59 | return 60 | } 61 | 62 | showMsg("stop %d gates ...", ss.NumGatesRunning) 63 | for _, proc := range ss.GateProcs { 64 | stopProc(proc, signal) 65 | } 66 | } 67 | 68 | func stopProc(proc process.Process, signal syscall.Signal) { 69 | showMsg("stop process %s pid=%d", proc.Executable(), proc.Pid()) 70 | 71 | proc.Signal(signal) 72 | for { 73 | time.Sleep(time.Millisecond * 100) 74 | if !checkProcessRunning(proc) { 75 | break 76 | } 77 | } 78 | } 79 | 80 | func checkProcessRunning(proc process.Process) bool { 81 | pid := proc.Pid() 82 | procs, err := process.Processes() 83 | checkErrorOrQuit(err, "list processes failed") 84 | for _, _proc := range procs { 85 | if _proc.Pid() == pid { 86 | return true 87 | } 88 | } 89 | return false 90 | } 91 | -------------------------------------------------------------------------------- /engine/kvdb/backend/kvdbredis/kvdb_redis.go: -------------------------------------------------------------------------------- 1 | package kvdbredis 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | "github.com/pkg/errors" 8 | "github.com/xiaonanln/goworld/engine/kvdb/types" 9 | ) 10 | 11 | const ( 12 | keyPrefix = "_KV_" 13 | ) 14 | 15 | type redisKVDB struct { 16 | c redis.Conn 17 | } 18 | 19 | // OpenRedisKVDB opens Redis for KVDB backend 20 | func OpenRedisKVDB(url string, dbindex int) (kvdbtypes.KVDBEngine, error) { 21 | c, err := redis.DialURL(url) 22 | if err != nil { 23 | return nil, errors.Wrap(err, "redis dail failed") 24 | } 25 | 26 | db := &redisKVDB{ 27 | c: c, 28 | } 29 | 30 | if err := db.initialize(dbindex); err != nil { 31 | panic(errors.Wrap(err, "redis kvdb initialize failed")) 32 | } 33 | 34 | return db, nil 35 | } 36 | 37 | func (db *redisKVDB) initialize(dbindex int) error { 38 | if dbindex >= 0 { 39 | if _, err := db.c.Do("SELECT", dbindex); err != nil { 40 | return err 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (db *redisKVDB) isZeroCursor(c interface{}) bool { 48 | return string(c.([]byte)) == "0" 49 | } 50 | 51 | func (db *redisKVDB) Get(key string) (val string, err error) { 52 | r, err := db.c.Do("GET", keyPrefix+key) 53 | if err != nil { 54 | return "", err 55 | } 56 | if r == nil { 57 | return "", nil 58 | } 59 | return string(r.([]byte)), err 60 | } 61 | 62 | func (db *redisKVDB) Put(key string, val string) error { 63 | _, err := db.c.Do("SET", keyPrefix+key, val) 64 | return err 65 | } 66 | 67 | type redisKVDBIterator struct { 68 | db *redisKVDB 69 | leftKeys []string 70 | } 71 | 72 | func (it *redisKVDBIterator) Next() (kvdbtypes.KVItem, error) { 73 | if len(it.leftKeys) == 0 { 74 | return kvdbtypes.KVItem{}, io.EOF 75 | } 76 | 77 | key := it.leftKeys[0] 78 | it.leftKeys = it.leftKeys[1:] 79 | val, err := it.db.Get(key) 80 | if err != nil { 81 | return kvdbtypes.KVItem{}, err 82 | } 83 | 84 | return kvdbtypes.KVItem{key, val}, nil 85 | } 86 | 87 | func (db *redisKVDB) Find(beginKey string, endKey string) (kvdbtypes.Iterator, error) { 88 | return nil, errors.Errorf("operation not supported on redis") 89 | } 90 | 91 | func (db *redisKVDB) Close() { 92 | db.c.Close() 93 | } 94 | 95 | func (db *redisKVDB) IsConnectionError(err error) bool { 96 | return err == io.EOF || err == io.ErrUnexpectedEOF 97 | } 98 | -------------------------------------------------------------------------------- /components/gate/ClientProxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/xiaonanln/netconnutil" 6 | "net" 7 | "time" 8 | 9 | "github.com/xiaonanln/goworld/engine/common" 10 | "github.com/xiaonanln/goworld/engine/config" 11 | "github.com/xiaonanln/goworld/engine/consts" 12 | "github.com/xiaonanln/goworld/engine/gwlog" 13 | "github.com/xiaonanln/goworld/engine/netutil" 14 | "github.com/xiaonanln/goworld/engine/post" 15 | "github.com/xiaonanln/goworld/engine/proto" 16 | ) 17 | 18 | type clientSyncInfo struct { 19 | EntityID common.EntityID 20 | X, Y, Z float32 21 | Yaw float32 22 | } 23 | 24 | func (info *clientSyncInfo) IsEmpty() bool { 25 | return info.EntityID == "" 26 | } 27 | 28 | // ClientProxy is a game client connections managed by gate 29 | type ClientProxy struct { 30 | *proto.GoWorldConnection 31 | clientid common.ClientID 32 | filterProps map[string]string 33 | clientSyncInfo clientSyncInfo 34 | heartbeatTime time.Time 35 | ownerEntityID common.EntityID // owner entity's ID 36 | } 37 | 38 | func newClientProxy(_conn net.Conn, cfg *config.GateConfig) *ClientProxy { 39 | _conn = netconnutil.NewNoTempErrorConn(_conn) 40 | var conn netutil.Connection = netutil.NetConn{_conn} 41 | if cfg.CompressConnection { 42 | conn = netconnutil.NewSnappyConn(conn) 43 | } 44 | conn = netconnutil.NewBufferedConn(conn, consts.BUFFERED_READ_BUFFSIZE, consts.BUFFERED_WRITE_BUFFSIZE) 45 | clientProxy := &ClientProxy{ 46 | clientid: common.GenClientID(), // each client has its unique clientid 47 | filterProps: map[string]string{}, 48 | } 49 | clientProxy.GoWorldConnection = proto.NewGoWorldConnection(conn, clientProxy) 50 | return clientProxy 51 | } 52 | 53 | func (cp *ClientProxy) String() string { 54 | return fmt.Sprintf("ClientProxy<%s@%s>", cp.clientid, cp.RemoteAddr()) 55 | } 56 | 57 | func (cp *ClientProxy) serve() { 58 | defer func() { 59 | cp.Close() 60 | // tell the gate service that this client is down 61 | post.Post(func() { 62 | gateService.onClientProxyClose(cp) 63 | }) 64 | 65 | if err := recover(); err != nil && !netutil.IsConnectionError(err.(error)) { 66 | gwlog.TraceError("%s error: %s", cp, err.(error)) 67 | } else { 68 | gwlog.Debugf("%s disconnected", cp) 69 | } 70 | }() 71 | 72 | err := cp.RecvChan(gateService.clientPacketQueue) 73 | if err != nil { 74 | gwlog.Panic(err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /engine/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "encoding/binary" 8 | "fmt" 9 | "io" 10 | "os" 11 | "sync/atomic" 12 | "time" 13 | ) 14 | 15 | const ( 16 | // UUID_LENGTH is length of a UUID 17 | UUID_LENGTH = 16 18 | encodeUUID = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_." 19 | ) 20 | 21 | var ( 22 | // _UUIDEncoding is encoding for UUID 23 | _UUIDEncoding = base64.NewEncoding(encodeUUID).WithPadding(base64.NoPadding) 24 | ) 25 | 26 | // GenUUID generates a new unique ObjectId. 27 | func GenUUID() string { 28 | var b = make([]byte, 12) 29 | // Timestamp, 4 bytes, big endian 30 | binary.BigEndian.PutUint32(b[:], uint32(time.Now().Unix())) 31 | // Machine, first 3 bytes of md5(hostname) 32 | b[4] = machineId[0] 33 | b[5] = machineId[1] 34 | b[6] = machineId[2] 35 | // Pid, 2 bytes, specs don't specify endianness, but we use big endian. 36 | pid := os.Getpid() 37 | b[7] = byte(pid >> 8) 38 | b[8] = byte(pid) 39 | // Increment, 3 bytes, big endian 40 | i := atomic.AddUint32(&objectIdCounter, 1) 41 | b[9] = byte(i >> 16) 42 | b[10] = byte(i >> 8) 43 | b[11] = byte(i) 44 | 45 | return _UUIDEncoding.EncodeToString(b) 46 | } 47 | 48 | func GenFixedUUID(b []byte) string { 49 | bl := len(b) 50 | if bl > 12 { 51 | b = b[:12] 52 | } else if bl < 12 { 53 | nb := make([]byte, 12) 54 | copy(nb[12-bl:], b) 55 | b = nb 56 | } 57 | 58 | return _UUIDEncoding.EncodeToString(b) 59 | } 60 | 61 | // objectIdCounter is atomically incremented when generating a new ObjectId 62 | // using NewObjectId() function. It's used as a counter part of an id. 63 | var objectIdCounter uint32 64 | 65 | // machineId stores machine id generated once and used in subsequent calls 66 | // to NewObjectId function. 67 | var machineId = readMachineId() 68 | 69 | // readMachineId generates machine id and puts it into the machineId global 70 | // variable. If this function fails to get the hostname, it will cause 71 | // a runtime error. 72 | func readMachineId() []byte { 73 | var sum [3]byte 74 | id := sum[:] 75 | hostname, err1 := os.Hostname() 76 | if err1 != nil { 77 | _, err2 := io.ReadFull(rand.Reader, id) 78 | if err2 != nil { 79 | panic(fmt.Errorf("cannot get hostname: %v; %v", err1, err2)) 80 | } 81 | return id 82 | } 83 | hw := md5.New() 84 | hw.Write([]byte(hostname)) 85 | copy(id, hw.Sum(nil)) 86 | return id 87 | } 88 | -------------------------------------------------------------------------------- /engine/binutil/binutil.go: -------------------------------------------------------------------------------- 1 | package binutil 2 | 3 | import ( 4 | "net/http" 5 | "syscall" 6 | 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | "golang.org/x/net/websocket" 9 | ) 10 | 11 | const ( 12 | // FreezeSignal syscall used to freeze server 13 | FreezeSignal = syscall.SIGHUP 14 | ) 15 | 16 | // SetupHTTPServer starts the HTTP server for go tool pprof and websockets 17 | func SetupHTTPServer(listenAddr string, wsHandler func(ws *websocket.Conn)) { 18 | setupHTTPServer(listenAddr, wsHandler, "", "") 19 | } 20 | 21 | // SetupHTTPServerTLS starts the HTTPs server for go tool pprof and websockets 22 | func SetupHTTPServerTLS(listenAddr string, wsHandler func(ws *websocket.Conn), certFile string, keyFile string) { 23 | setupHTTPServer(listenAddr, wsHandler, certFile, keyFile) 24 | } 25 | 26 | func setupHTTPServer(listenAddr string, wsHandler func(ws *websocket.Conn), certFile string, keyFile string) { 27 | gwlog.Infof("http server listening on %s", listenAddr) 28 | gwlog.Infof("pprof http://%s/debug/pprof/ ... available commands: ", listenAddr) 29 | gwlog.Infof(" go tool pprof http://%s/debug/pprof/heap", listenAddr) 30 | gwlog.Infof(" go tool pprof http://%s/debug/pprof/profile", listenAddr) 31 | if keyFile != "" || certFile != "" { 32 | gwlog.Infof("TLS is enabled on http: key=%s, cert=%s", keyFile, certFile) 33 | } 34 | 35 | if wsHandler != nil { 36 | http.Handle("/ws", websocket.Handler(wsHandler)) 37 | } 38 | 39 | go func() { 40 | if keyFile == "" && certFile == "" { 41 | http.ListenAndServe(listenAddr, nil) 42 | } else { 43 | http.ListenAndServeTLS(listenAddr, certFile, keyFile, nil) 44 | } 45 | }() 46 | } 47 | 48 | // SetupGWLog setup the GoWord log system 49 | func SetupGWLog(component string, logLevel string, logFile string, logStderr bool) { 50 | gwlog.SetSource(component) 51 | gwlog.Infof("Set log level to %s", logLevel) 52 | gwlog.SetLevel(gwlog.ParseLevel(logLevel)) 53 | 54 | var outputs []string 55 | if logStderr { 56 | outputs = append(outputs, "stderr") 57 | } 58 | if logFile != "" { 59 | outputs = append(outputs, logFile) 60 | } 61 | gwlog.SetOutput(outputs) 62 | } 63 | 64 | func PrintSupervisorTag(tag string) { 65 | curlvl := gwlog.GetLevel() 66 | if curlvl != gwlog.DebugLevel && curlvl != gwlog.InfoLevel { 67 | gwlog.SetLevel(gwlog.InfoLevel) 68 | } 69 | gwlog.Infof("%s", tag) 70 | if curlvl != gwlog.DebugLevel && curlvl != gwlog.InfoLevel { 71 | gwlog.SetLevel(curlvl) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xiaonanln/goworld 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 7 | github.com/chasex/redis-go-cluster v1.0.0 8 | github.com/garyburd/redigo v1.6.0 9 | github.com/go-ini/ini v1.67.0 10 | github.com/go-ole/go-ole v1.2.6 11 | github.com/petar/GoLLRB v0.0.0-20190514000832-33fb24c13b99 12 | github.com/pkg/errors v0.9.1 13 | github.com/sevlyar/go-daemon v0.1.6 14 | github.com/shirou/gopsutil v3.21.11+incompatible 15 | github.com/vmihailenco/msgpack v4.0.4+incompatible 16 | github.com/xiaonanln/go-aoi v0.2.0 17 | github.com/xiaonanln/go-trie-tst v0.0.0-20171018095208-5b9678d55438 18 | github.com/xiaonanln/go-xnsyncutil v0.0.5 19 | github.com/xiaonanln/goTimer v0.0.3 20 | github.com/xiaonanln/netconnutil v0.0.0-20200905060227-8faf06e9a365 21 | github.com/xiaonanln/pktconn v0.0.0-20200905130536-8a9529b7c220 22 | github.com/xiaonanln/typeconv v0.0.4 23 | github.com/xtaci/kcp-go v5.4.19+incompatible 24 | go.uber.org/zap v1.22.0 25 | golang.org/x/net v0.0.0-20220812174116-3211cb980234 26 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab 27 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 28 | ) 29 | 30 | require ( 31 | github.com/golang/protobuf v1.3.1 // indirect 32 | github.com/golang/snappy v0.0.1 // indirect 33 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 34 | github.com/klauspost/cpuid v1.2.1 // indirect 35 | github.com/klauspost/reedsolomon v1.9.3 // indirect 36 | github.com/kr/pretty v0.1.0 // indirect 37 | github.com/kr/text v0.1.0 // indirect 38 | github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect 39 | github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b // indirect 40 | github.com/tjfoc/gmsm v1.0.1 // indirect 41 | github.com/tklauser/go-sysconf v0.3.10 // indirect 42 | github.com/tklauser/numcpus v0.5.0 // indirect 43 | github.com/xiaonanln/tickchan v0.0.0-20181130012730-45de2aab1755 // indirect 44 | github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect 45 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 46 | go.uber.org/atomic v1.10.0 // indirect 47 | go.uber.org/multierr v1.8.0 // indirect 48 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect 49 | google.golang.org/appengine v1.6.5 // indirect 50 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 51 | gopkg.in/eapache/queue.v1 v1.1.0 // indirect 52 | gopkg.in/yaml.v2 v2.4.0 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /engine/kvdb/backend/kvdb_mongodb/mongodb.go: -------------------------------------------------------------------------------- 1 | package kvdbmongo 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | 6 | "io" 7 | 8 | "github.com/xiaonanln/goworld/engine/gwlog" 9 | "github.com/xiaonanln/goworld/engine/kvdb/types" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | const ( 14 | _DEFAULT_DB_NAME = "goworld" 15 | _VAL_KEY = "_" 16 | ) 17 | 18 | type mongoKVDB struct { 19 | s *mgo.Session 20 | c *mgo.Collection 21 | } 22 | 23 | // OpenMongoKVDB opens mongodb as KVDB engine 24 | func OpenMongoKVDB(url string, dbname string, collectionName string) (kvdbtypes.KVDBEngine, error) { 25 | gwlog.Debugf("Connecting MongoDB ...") 26 | session, err := mgo.Dial(url) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | session.SetMode(mgo.Monotonic, true) 32 | if dbname == "" { 33 | // if db is not specified, use default 34 | dbname = _DEFAULT_DB_NAME 35 | } 36 | db := session.DB(dbname) 37 | c := db.C(collectionName) 38 | return &mongoKVDB{ 39 | s: session, 40 | c: c, 41 | }, nil 42 | } 43 | 44 | func (kvdb *mongoKVDB) Put(key string, val string) error { 45 | _, err := kvdb.c.UpsertId(key, map[string]string{ 46 | _VAL_KEY: val, 47 | }) 48 | return err 49 | } 50 | 51 | func (kvdb *mongoKVDB) Get(key string) (val string, err error) { 52 | q := kvdb.c.FindId(key) 53 | var doc map[string]string 54 | err = q.One(&doc) 55 | if err != nil { 56 | if err == mgo.ErrNotFound { 57 | err = nil 58 | } 59 | return 60 | } 61 | val = doc[_VAL_KEY] 62 | return 63 | } 64 | 65 | type mongoKVIterator struct { 66 | it *mgo.Iter 67 | } 68 | 69 | func (it *mongoKVIterator) Next() (kvdbtypes.KVItem, error) { 70 | var doc map[string]string 71 | ok := it.it.Next(&doc) 72 | if ok { 73 | return kvdbtypes.KVItem{ 74 | Key: doc["_id"], 75 | Val: doc["_"], 76 | }, nil 77 | } 78 | 79 | err := it.it.Close() 80 | if err != nil { 81 | return kvdbtypes.KVItem{}, err 82 | } 83 | return kvdbtypes.KVItem{}, io.EOF 84 | } 85 | 86 | func (kvdb *mongoKVDB) Find(beginKey string, endKey string) (kvdbtypes.Iterator, error) { 87 | q := kvdb.c.Find(bson.M{"_id": bson.M{"$gte": beginKey, "$lt": endKey}}) 88 | it := q.Iter() 89 | return &mongoKVIterator{ 90 | it: it, 91 | }, nil 92 | } 93 | 94 | func (kvdb *mongoKVDB) Close() { 95 | kvdb.s.Close() 96 | } 97 | 98 | func (kvdb *mongoKVDB) IsConnectionError(err error) bool { 99 | return err == io.EOF || err == io.ErrUnexpectedEOF 100 | } 101 | -------------------------------------------------------------------------------- /examples/test_client/test_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "sync" 7 | 8 | "math/rand" 9 | "time" 10 | 11 | _ "net/http/pprof" 12 | 13 | "os" 14 | 15 | "github.com/xiaonanln/goTimer" 16 | "github.com/xiaonanln/goworld/engine/binutil" 17 | "github.com/xiaonanln/goworld/engine/config" 18 | "github.com/xiaonanln/goworld/engine/gwlog" 19 | ) 20 | 21 | var ( 22 | quiet bool 23 | configFile string 24 | serverHost string 25 | useWebSocket bool 26 | useKCP bool 27 | numClients int 28 | startClientId int 29 | noEntitySync bool 30 | strictMode bool 31 | duration int 32 | loglevel string 33 | ) 34 | 35 | func parseArgs() { 36 | flag.BoolVar(&quiet, "quiet", false, "run client quietly with much less output") 37 | flag.StringVar(&configFile, "configfile", "", "set config file path") 38 | flag.IntVar(&numClients, "N", 1000, "Number of clients") 39 | flag.IntVar(&startClientId, "S", 1, "Start ID of clients") 40 | flag.StringVar(&serverHost, "server", "localhost", "replace server address") 41 | flag.BoolVar(&useWebSocket, "ws", false, "use WebSocket to connect server") 42 | flag.BoolVar(&useKCP, "kcp", false, "use KCP to connect server") 43 | flag.BoolVar(&noEntitySync, "nosync", false, "disable entity sync") 44 | flag.BoolVar(&strictMode, "strict", false, "enable strict mode") 45 | flag.IntVar(&duration, "duration", 0, "run for a specified duration (seconds)") 46 | flag.StringVar(&loglevel, "log", "info", "set log level (info by default)") 47 | flag.Parse() 48 | } 49 | 50 | func main() { 51 | rand.Seed(time.Now().UnixNano()) 52 | parseArgs() 53 | if configFile != "" { 54 | config.SetConfigFile(configFile) 55 | } 56 | 57 | binutil.SetupGWLog("test_client", loglevel, "test_client.log", true) 58 | binutil.SetupHTTPServer("localhost:18888", nil) 59 | if useWebSocket && useKCP { 60 | gwlog.Errorf("Can not use both websocket and KCP") 61 | os.Exit(1) 62 | } 63 | 64 | if useWebSocket { 65 | gwlog.Infof("Using websocket clients ...") 66 | } else if useKCP { 67 | gwlog.Infof("Using KCP clients ...") 68 | } 69 | var wait sync.WaitGroup 70 | var waitAllConnected sync.WaitGroup 71 | wait.Add(numClients) 72 | waitAllConnected.Add(numClients) 73 | for i := 0; i < numClients; i++ { 74 | bot := newClientBot(startClientId+i, useWebSocket, useKCP, noEntitySync, &wait, &waitAllConnected) 75 | go bot.run() 76 | } 77 | timer.StartTicks(time.Millisecond * 100) 78 | if duration > 0 { 79 | timer.AddCallback(time.Second*time.Duration(duration), func() { 80 | os.Exit(0) 81 | }) 82 | } 83 | wait.Wait() 84 | } 85 | -------------------------------------------------------------------------------- /engine/entity/attr_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "strconv" 8 | 9 | "gopkg.in/mgo.v2/bson" 10 | ) 11 | 12 | func TestAttrVals(t *testing.T) { 13 | v := uniformAttrType(float32(1.0)) 14 | t.Logf("uniformAttrType %v %T", v, v) 15 | v = uniformAttrType(int32(1)) 16 | t.Logf("uniformAttrType %v %T", v, v) 17 | } 18 | 19 | func TestMapAttr(t *testing.T) { 20 | m := NewMapAttr() 21 | m.AssignMap(map[string]interface{}{ 22 | "int": int(1), 23 | "int32": int32(32), 24 | "int64": int64(64), 25 | "float32": float32(32.0), 26 | "float64": float64(64.0), 27 | "bool": true, 28 | "string": "xxx", 29 | "subM": map[string]interface{}{ 30 | "a": 1, "b": 2, 31 | "innerM1": map[string]interface{}{ 32 | "a": 1, "b": 2, 33 | }, 34 | }, 35 | "list": []interface{}{1, 2, 3, map[string]interface{}{ 36 | "a": 1, "b": 2, 37 | "innerM1": map[string]interface{}{ 38 | "a": 1, "b": 2, "l": []interface{}{1, "xxx", false}, 39 | }, 40 | }}, 41 | }) 42 | 43 | t.Logf("AssignMap: %s", m) 44 | 45 | if !m.HasKey("int") { 46 | t.Fatalf("should has key") 47 | } 48 | 49 | if m.HasKey("not exist key") { 50 | t.Fatalf("should not has key") 51 | } 52 | 53 | if m.GetInt("int") != 1 { 54 | t.Fatalf("wrong value") 55 | } 56 | if m.GetInt("int32") != 32 { 57 | t.Fatalf("wrong value") 58 | } 59 | if m.GetInt("int64") != 64 { 60 | t.Fatalf("wrong value") 61 | } 62 | if m.GetInt("not exist key") != 0 { 63 | t.Fatalf("wrong value") 64 | } 65 | if m.GetBool("bool") != true { 66 | t.Fatalf("wrong value") 67 | } 68 | if m.GetBool("not exist key") != false { 69 | t.Fatalf("wrong value") 70 | } 71 | if m.GetStr("string") != "xxx" { 72 | t.Fatalf("wrong value") 73 | } 74 | if m.GetStr("not exist key") != "" { 75 | t.Fatalf("wrong value") 76 | } 77 | if math.Abs(m.GetFloat("float32")-32.0) >= 0.000001 { 78 | t.Fatalf("wrong value") 79 | } 80 | if math.Abs(m.GetFloat("float64")-64.0) >= 0.000001 { 81 | t.Fatalf("wrong value") 82 | } 83 | if math.Abs(m.GetFloat("not exist key")-0.0) >= 0.000001 { 84 | t.Fatalf("wrong value") 85 | } 86 | sl := m.GetListAttr("list") 87 | t.Logf("list convert to %v", sl) 88 | } 89 | 90 | func BenchmarkConvertBsonMToMap(b *testing.B) { 91 | m := bson.M{ 92 | "a": 1, 93 | "b": 2, 94 | } 95 | for i := 0; i < 100000; i++ { 96 | m[strconv.Itoa(i)] = i 97 | } 98 | 99 | b.ResetTimer() 100 | for i := 0; i < b.N; i++ { 101 | mm := map[string]interface{}(m) // if there is copy here, it will consume a lot of time 102 | mm["a"] = 1 103 | mm["b"] = 1 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /components/dispatcher/dispatcher.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | 7 | "flag" 8 | 9 | _ "net/http/pprof" 10 | 11 | "os/signal" 12 | 13 | "runtime/debug" 14 | 15 | "github.com/xiaonanln/goworld/engine/binutil" 16 | "github.com/xiaonanln/goworld/engine/config" 17 | "github.com/xiaonanln/goworld/engine/consts" 18 | "github.com/xiaonanln/goworld/engine/gwlog" 19 | "github.com/xiaonanln/goworld/engine/post" 20 | ) 21 | 22 | var ( 23 | dispidArg int 24 | dispid uint16 25 | configFile = "" 26 | logLevel string 27 | runInDaemonMode bool 28 | sigChan = make(chan os.Signal, 1) 29 | dispatcherService *DispatcherService 30 | ) 31 | 32 | func parseArgs() { 33 | flag.IntVar(&dispidArg, "dispid", 0, "set dispatcher ID") 34 | flag.StringVar(&configFile, "configfile", "", "set config file path") 35 | flag.StringVar(&logLevel, "log", "", "set log level, will override log level in config") 36 | flag.BoolVar(&runInDaemonMode, "d", false, "run in daemon mode") 37 | flag.Parse() 38 | dispid = uint16(dispidArg) 39 | } 40 | 41 | func setupGCPercent() { 42 | debug.SetGCPercent(consts.DISPATCHER_GC_PERCENT) 43 | } 44 | 45 | func main() { 46 | parseArgs() 47 | if runInDaemonMode { 48 | daemoncontext := binutil.Daemonize() 49 | defer daemoncontext.Release() 50 | } 51 | 52 | setupGCPercent() 53 | 54 | if configFile != "" { 55 | config.SetConfigFile(configFile) 56 | } 57 | 58 | validDispIds := config.GetDispatcherIDs() 59 | if dispid < validDispIds[0] || dispid > validDispIds[len(validDispIds)-1] { 60 | gwlog.Fatalf("dispatcher ID must be one of %v, but is %v, use -dispid to specify", config.GetDispatcherIDs(), dispid) 61 | } 62 | 63 | dispatcherConfig := config.GetDispatcher(dispid) 64 | 65 | if logLevel == "" { 66 | logLevel = dispatcherConfig.LogLevel 67 | } 68 | binutil.SetupGWLog("dispatcherService", logLevel, dispatcherConfig.LogFile, dispatcherConfig.LogStderr) 69 | binutil.SetupHTTPServer(dispatcherConfig.HTTPAddr, nil) 70 | 71 | dispatcherService = newDispatcherService(dispid) 72 | setupSignals() // call setupSignals to avoid data race on `dispatcherService` 73 | dispatcherService.run() 74 | } 75 | 76 | func setupSignals() { 77 | signal.Ignore(syscall.Signal(10), syscall.Signal(12), syscall.SIGPIPE, syscall.SIGHUP) 78 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 79 | go func() { 80 | for { 81 | sig := <-sigChan 82 | 83 | if sig == syscall.SIGINT || sig == syscall.SIGTERM { 84 | // interrupting, quit dispatcher 85 | post.Post(func() { 86 | dispatcherService.terminate() 87 | }) 88 | } else { 89 | gwlog.Infof("unexcepted signal: %s", sig) 90 | } 91 | } 92 | }() 93 | } 94 | -------------------------------------------------------------------------------- /engine/netutil/MsgPacker_test.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/xiaonanln/goworld/engine/uuid" 7 | ) 8 | 9 | type testMsg struct { 10 | ID string 11 | F1 float64 12 | F2 int 13 | ListField []interface{} 14 | MapField map[string]interface{} 15 | } 16 | 17 | func BenchmarkMessagePackMsgPacker(b *testing.B) { 18 | benchmarkMsgPacker(b, &MessagePackMsgPacker{}) 19 | } 20 | 21 | func benchmarkMsgPacker(b *testing.B, packer MsgPacker) { 22 | b.Logf("Testing MsgPacker %T ...", packer) 23 | msg := testMsg{ 24 | ID: "abc", 25 | F1: 0.123124234, 26 | ListField: []interface{}{1, 2, 3, "abc", "def"}, 27 | MapField: map[string]interface{}{}, 28 | } 29 | for i := 0; i < 100; i++ { 30 | msg.MapField[uuid.GenUUID()] = uuid.GenUUID() 31 | } 32 | 33 | var totalSize int64 34 | for i := 0; i < b.N; i++ { 35 | 36 | buf := make([]byte, 0, 100) 37 | buf, _ = packer.PackMsg(msg, buf) 38 | totalSize += int64(len(buf)) 39 | 40 | var restoreMsg map[string]interface{} 41 | _ = packer.UnpackMsg(buf, &restoreMsg) 42 | //if msg.ID != restoreMsg.ID { 43 | // b.Fail() 44 | //} 45 | } 46 | b.Logf("average size: %d", totalSize/int64(b.N)) 47 | } 48 | 49 | func TestMessagePackMsgPacker_UnpackMsg(t *testing.T) { 50 | msg := map[string]interface{}{ 51 | "a": 1, 52 | "b": 2, 53 | "c": map[string]interface{}{ 54 | "d": 1, 55 | }, 56 | } 57 | buf := make([]byte, 0) 58 | buf, err := MessagePackMsgPacker{}.PackMsg(msg, buf) 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | var outmsg map[string]interface{} 63 | MessagePackMsgPacker{}.UnpackMsg(buf, &outmsg) 64 | t.Logf("outmsg %T %v", outmsg, outmsg) 65 | if _, ok := outmsg["c"].(map[interface{}]interface{}); ok { 66 | t.Errorf("should not unpack with type map[interface{}]interface{}") 67 | } 68 | } 69 | 70 | func BenchmarkMessagePackMsgPacker_PackMsg_Array_AllInOne(b *testing.B) { 71 | packer := MessagePackMsgPacker{} 72 | items := []testMsg{} 73 | for i := 0; i < 3; i++ { 74 | items = append(items, testMsg{ 75 | ID: "abc", 76 | F1: 0.123124234, 77 | ListField: []interface{}{1, 2, 3, "abc", "def"}, 78 | MapField: map[string]interface{}{}, 79 | }) 80 | } 81 | b.ResetTimer() 82 | for i := 0; i < b.N; i++ { 83 | packer.PackMsg(items, []byte{}) 84 | } 85 | } 86 | 87 | func BenchmarkMessagePackMsgPacker_PackMsg_Array_OneByOne(b *testing.B) { 88 | packer := MessagePackMsgPacker{} 89 | items := []testMsg{} 90 | for i := 0; i < 3; i++ { 91 | items = append(items, testMsg{ 92 | ID: "abc", 93 | F1: 0.123124234, 94 | ListField: []interface{}{1, 2, 3, "abc", "def"}, 95 | MapField: map[string]interface{}{}, 96 | }) 97 | } 98 | 99 | b.ResetTimer() 100 | for i := 0; i < b.N; i++ { 101 | for _, item := range items { 102 | packer.PackMsg(item, []byte{}) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /engine/common/collections.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // StringSet is a set of strings 4 | type StringSet map[string]struct{} 5 | 6 | // Contains checks if Stringset contains the string 7 | func (ss StringSet) Contains(elem string) bool { 8 | _, ok := ss[elem] 9 | return ok 10 | } 11 | 12 | // Add adds the string to StringSet 13 | func (ss StringSet) Add(elem string) { 14 | ss[elem] = struct{}{} 15 | } 16 | 17 | // Remove removes the string from StringList 18 | func (ss StringSet) Remove(elem string) { 19 | delete(ss, elem) 20 | } 21 | 22 | // ToList convert StringSet to string slice 23 | func (ss StringSet) ToList() []string { 24 | keys := make([]string, 0, len(ss)) 25 | for s := range ss { 26 | keys = append(keys, s) 27 | } 28 | return keys 29 | } 30 | 31 | // StringList is a list of string (slice) 32 | type StringList []string 33 | 34 | // Remove removes the string from StringList 35 | func (sl *StringList) Remove(elem string) { 36 | widx := 0 37 | cpsl := *sl 38 | for idx, _elem := range cpsl { 39 | if _elem == elem { 40 | // ignore this elem by doing nothing 41 | } else { 42 | if idx != widx { 43 | cpsl[widx] = _elem 44 | } 45 | widx += 1 46 | } 47 | } 48 | 49 | *sl = cpsl[:widx] 50 | } 51 | 52 | // Append add the string to the end of StringList 53 | func (sl *StringList) Append(elem string) { 54 | *sl = append(*sl, elem) 55 | } 56 | 57 | // Find get the index of string in StringList, returns -1 if not found 58 | func (sl *StringList) Find(s string) int { 59 | for idx, elem := range *sl { 60 | if elem == s { 61 | return idx 62 | } 63 | } 64 | return -1 65 | } 66 | 67 | // IntSet is a set of int 68 | type IntSet map[int]struct{} 69 | 70 | // Contains checks if Stringset contains the string 71 | func (is IntSet) Contains(elem int) bool { 72 | _, ok := is[elem] 73 | return ok 74 | } 75 | 76 | // Add adds the string to IntSet 77 | func (is IntSet) Add(elem int) { 78 | is[elem] = struct{}{} 79 | } 80 | 81 | // Remove removes the string from IntSet 82 | func (is IntSet) Remove(elem int) { 83 | delete(is, elem) 84 | } 85 | 86 | // ToList convert IntSet to int slice 87 | func (is IntSet) ToList() []int { 88 | keys := make([]int, 0, len(is)) 89 | for s := range is { 90 | keys = append(keys, s) 91 | } 92 | return keys 93 | } 94 | 95 | // Uint16Set is a set of int 96 | type Uint16Set map[uint16]struct{} 97 | 98 | // Contains checks if Stringset contains the string 99 | func (is Uint16Set) Contains(elem uint16) bool { 100 | _, ok := is[elem] 101 | return ok 102 | } 103 | 104 | // Add adds the string to Uint16Set 105 | func (is Uint16Set) Add(elem uint16) { 106 | is[elem] = struct{}{} 107 | } 108 | 109 | // Remove removes the string from Uint16Set 110 | func (is Uint16Set) Remove(elem uint16) { 111 | delete(is, elem) 112 | } 113 | 114 | // ToList convert Uint16Set to int slice 115 | func (is Uint16Set) ToList() []uint16 { 116 | keys := make([]uint16, 0, len(is)) 117 | for s := range is { 118 | keys = append(keys, s) 119 | } 120 | return keys 121 | } 122 | -------------------------------------------------------------------------------- /cmd/goworld/detectenv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/xiaonanln/goworld/engine/config" 11 | ) 12 | 13 | // Env represents environment variables 14 | type Env struct { 15 | GoWorldRoot string 16 | } 17 | 18 | // GetDispatcherDir returns the path to the dispatcher 19 | func (env *Env) GetDispatcherDir() string { 20 | return filepath.Join(env.GoWorldRoot, "components", "dispatcher") 21 | } 22 | 23 | // GetGateDir returns the path to the gate 24 | func (env *Env) GetGateDir() string { 25 | return filepath.Join(env.GoWorldRoot, "components", "gate") 26 | } 27 | 28 | // GetDispatcherBinary returns the path to the dispatcher binary 29 | func (env *Env) GetDispatcherBinary() string { 30 | return filepath.Join(env.GetDispatcherDir(), "dispatcher"+BinaryExtension) 31 | } 32 | 33 | // GetGateBinary returns the path to the gate binary 34 | func (env *Env) GetGateBinary() string { 35 | return filepath.Join(env.GetGateDir(), "gate"+BinaryExtension) 36 | } 37 | 38 | var env Env 39 | 40 | func getGoSearchPaths() []string { 41 | var paths []string 42 | goroot := os.Getenv("GOROOT") 43 | if goroot != "" { 44 | paths = append(paths, goroot) 45 | } 46 | 47 | gopath := os.Getenv("GOPATH") 48 | for _, p := range strings.Split(gopath, string(os.PathListSeparator)) { 49 | if p != "" { 50 | paths = append(paths, p) 51 | } 52 | } 53 | return paths 54 | } 55 | 56 | type ModuleInfo struct { 57 | Path string `json:"Path"` 58 | Main bool `json:"Main"` 59 | Dir string `json:"Dir"` 60 | GoMod string `json:"GoMod"` 61 | GoVersion string `json:"GoVersion"` 62 | } 63 | 64 | func goListModule() (*ModuleInfo, error) { 65 | cmd := exec.Command("go", "list", "-m", "-json") 66 | 67 | r, err := cmd.StdoutPipe() 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | err = cmd.Start() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | d := json.NewDecoder(r) 78 | var mi ModuleInfo 79 | err = d.Decode(&mi) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | cmd.Wait() 85 | return &mi, err 86 | } 87 | 88 | func _detectGoWorldPath() string { 89 | mi, err := goListModule() 90 | if err == nil { 91 | showMsg("go list -m -json: %+v", *mi) 92 | return mi.Dir 93 | } 94 | 95 | searchPaths := getGoSearchPaths() 96 | showMsg("go search paths: %s", strings.Join(searchPaths, string(os.PathListSeparator))) 97 | for _, sp := range searchPaths { 98 | goworldPath := filepath.Join(sp, "src", "github.com", "xiaonanln", "goworld") 99 | if isdir(goworldPath) { 100 | return goworldPath 101 | } 102 | } 103 | return "" 104 | } 105 | 106 | func detectGoWorldPath() { 107 | env.GoWorldRoot = _detectGoWorldPath() 108 | if env.GoWorldRoot == "" { 109 | showMsgAndQuit("goworld directory is not detected") 110 | } 111 | 112 | showMsg("goworld directory found: %s", env.GoWorldRoot) 113 | configFile := filepath.Join(env.GoWorldRoot, "goworld.ini") 114 | config.SetConfigFile(configFile) 115 | } 116 | -------------------------------------------------------------------------------- /engine/opmon/opmon.go: -------------------------------------------------------------------------------- 1 | package opmon 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "sort" 8 | 9 | "fmt" 10 | "os" 11 | 12 | "github.com/xiaonanln/goworld/engine/consts" 13 | "github.com/xiaonanln/goworld/engine/gwlog" 14 | ) 15 | 16 | var ( 17 | operationAllocPool = sync.Pool{ 18 | New: func() interface{} { 19 | return &Operation{} 20 | }, 21 | } 22 | 23 | monitor = newMonitor() 24 | ) 25 | 26 | func init() { 27 | if consts.OPMON_DUMP_INTERVAL > 0 { 28 | go func() { 29 | for { 30 | time.Sleep(consts.OPMON_DUMP_INTERVAL) 31 | monitor.Dump() 32 | } 33 | }() 34 | } 35 | } 36 | 37 | type _OpInfo struct { 38 | count uint64 39 | totalDuration time.Duration 40 | maxDuration time.Duration 41 | } 42 | 43 | type _Monitor struct { 44 | sync.Mutex 45 | opInfos map[string]*_OpInfo 46 | } 47 | 48 | func newMonitor() *_Monitor { 49 | m := &_Monitor{ 50 | opInfos: map[string]*_OpInfo{}, 51 | } 52 | return m 53 | } 54 | 55 | func (monitor *_Monitor) record(opname string, duration time.Duration) { 56 | monitor.Lock() 57 | info := monitor.opInfos[opname] 58 | if info == nil { 59 | info = &_OpInfo{} 60 | monitor.opInfos[opname] = info 61 | } 62 | info.count += 1 63 | info.totalDuration += duration 64 | if duration > info.maxDuration { 65 | info.maxDuration = duration 66 | } 67 | monitor.Unlock() 68 | } 69 | 70 | func (monitor *_Monitor) Dump() { 71 | type _T struct { 72 | name string 73 | info *_OpInfo 74 | } 75 | var opInfos map[string]*_OpInfo 76 | monitor.Lock() 77 | opInfos = monitor.opInfos 78 | monitor.opInfos = map[string]*_OpInfo{} // clear to be empty 79 | monitor.Unlock() 80 | 81 | var copyOpInfos []_T 82 | for name, opinfo := range opInfos { 83 | copyOpInfos = append(copyOpInfos, _T{name, opinfo}) 84 | } 85 | sort.Slice(copyOpInfos, func(i, j int) bool { 86 | _t1 := copyOpInfos[i] 87 | _t2 := copyOpInfos[j] 88 | return _t1.name < _t2.name 89 | }) 90 | fmt.Fprint(os.Stderr, "=====================================================================================\n") 91 | for _, _t := range copyOpInfos { 92 | opname, opinfo := _t.name, _t.info 93 | fmt.Fprintf(os.Stderr, "%-30sx%-10d AVG %-10s MAX %-10s\n", opname, opinfo.count, opinfo.totalDuration/time.Duration(opinfo.count), opinfo.maxDuration) 94 | } 95 | } 96 | 97 | // Operation is the type of operation to be monitored 98 | type Operation struct { 99 | name string 100 | startTime time.Time 101 | } 102 | 103 | // StartOperation creates a new operation 104 | func StartOperation(operationName string) *Operation { 105 | op := operationAllocPool.Get().(*Operation) 106 | op.name = operationName 107 | op.startTime = time.Now() 108 | return op 109 | } 110 | 111 | // Finish finishes the operation and records the duration of operation 112 | func (op *Operation) Finish(warnThreshold time.Duration) { 113 | takeTime := time.Now().Sub(op.startTime) 114 | monitor.record(op.name, takeTime) 115 | if takeTime >= warnThreshold { 116 | gwlog.Warnf("opmon: operation %s takes %s > %s", op.name, takeTime, warnThreshold) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /components/gate/FilterTree.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/petar/GoLLRB/llrb" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | "github.com/xiaonanln/goworld/engine/gwutils" 9 | "github.com/xiaonanln/goworld/engine/proto" 10 | ) 11 | 12 | type _FilterTree struct { 13 | btree *llrb.LLRB 14 | } 15 | 16 | func newFilterTree() *_FilterTree { 17 | return &_FilterTree{ 18 | btree: llrb.New(), 19 | } 20 | } 21 | 22 | type filterTreeItem struct { 23 | cp *ClientProxy 24 | val string 25 | } 26 | 27 | func (it *filterTreeItem) Less(_other llrb.Item) bool { 28 | other := _other.(*filterTreeItem) 29 | return it.val < other.val || (it.val == other.val && uintptr(unsafe.Pointer(it.cp)) < uintptr(unsafe.Pointer(other.cp))) 30 | } 31 | 32 | func (ft *_FilterTree) Insert(cp *ClientProxy, val string) { 33 | ft.btree.ReplaceOrInsert(&filterTreeItem{ 34 | cp: cp, 35 | val: val, 36 | }) 37 | } 38 | 39 | func (ft *_FilterTree) Remove(cp *ClientProxy, val string) { 40 | //gwlog.Infof("Removing %s %s has %v", id, val, ft.llrb.Has(&filterTreeItem{ 41 | // cp: id, 42 | // val: val, 43 | //})) 44 | 45 | ft.btree.Delete(&filterTreeItem{ 46 | cp: cp, 47 | val: val, 48 | }) 49 | } 50 | 51 | func (ft *_FilterTree) Visit(op proto.FilterClientsOpType, val string, f func(cp *ClientProxy)) { 52 | if op == proto.FILTER_CLIENTS_OP_EQ { 53 | // visit key == val 54 | ft.btree.AscendGreaterOrEqual(&filterTreeItem{nil, val}, func(_item llrb.Item) bool { 55 | item := _item.(*filterTreeItem) 56 | if item.val > val { 57 | return false 58 | } 59 | 60 | f(item.cp) 61 | return true 62 | }) 63 | } else if op == proto.FILTER_CLIENTS_OP_NE { 64 | // visit key != val 65 | // visit key < val first 66 | ft.btree.AscendLessThan(&filterTreeItem{nil, val}, func(_item llrb.Item) bool { 67 | f(_item.(*filterTreeItem).cp) 68 | return true 69 | }) 70 | // then visit key > val 71 | ft.btree.AscendGreaterOrEqual(&filterTreeItem{nil, gwutils.NextLargerKey(val)}, func(_item llrb.Item) bool { 72 | f(_item.(*filterTreeItem).cp) 73 | return true 74 | }) 75 | } else if op == proto.FILTER_CLIENTS_OP_GT { 76 | // visit key > val 77 | ft.btree.AscendGreaterOrEqual(&filterTreeItem{nil, gwutils.NextLargerKey(val)}, func(_item llrb.Item) bool { 78 | f(_item.(*filterTreeItem).cp) 79 | return true 80 | }) 81 | } else if op == proto.FILTER_CLIENTS_OP_GTE { 82 | // visit key >= val 83 | ft.btree.AscendGreaterOrEqual(&filterTreeItem{nil, val}, func(_item llrb.Item) bool { 84 | f(_item.(*filterTreeItem).cp) 85 | return true 86 | }) 87 | } else if op == proto.FILTER_CLIENTS_OP_LT { 88 | // visit key < val 89 | ft.btree.AscendLessThan(&filterTreeItem{nil, val}, func(_item llrb.Item) bool { 90 | f(_item.(*filterTreeItem).cp) 91 | return true 92 | }) 93 | } else if op == proto.FILTER_CLIENTS_OP_LTE { 94 | // visit key <= val 95 | ft.btree.AscendLessThan(&filterTreeItem{nil, gwutils.NextLargerKey(val)}, func(_item llrb.Item) bool { 96 | f(_item.(*filterTreeItem).cp) 97 | return true 98 | }) 99 | } else { 100 | gwlog.Panicf("unknown filter clients op: %s", op) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /engine/async/async.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/xiaonanln/goworld/engine/consts" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | "github.com/xiaonanln/goworld/engine/gwutils" 9 | "github.com/xiaonanln/goworld/engine/post" 10 | ) 11 | 12 | var ( 13 | numAsyncJobWorkersRunning sync.WaitGroup 14 | ) 15 | 16 | // AsyncCallback is a function which will be called after async job is finished with result and error 17 | type AsyncCallback func(res interface{}, err error) 18 | 19 | func (ac AsyncCallback) callback(res interface{}, err error) { 20 | if ac != nil { 21 | post.Post(func() { 22 | ac(res, err) 23 | }) 24 | } 25 | } 26 | 27 | // AsyncRoutine is a function that will be executed in the async goroutine and its result and error will be passed to AsyncCallback 28 | type AsyncRoutine func() (res interface{}, err error) 29 | 30 | type asyncJobWorker struct { 31 | jobQueue chan asyncJobItem 32 | } 33 | 34 | type asyncJobItem struct { 35 | routine AsyncRoutine 36 | callback AsyncCallback 37 | } 38 | 39 | func newAsyncJobWorker() *asyncJobWorker { 40 | ajw := &asyncJobWorker{ 41 | jobQueue: make(chan asyncJobItem, consts.ASYNC_JOB_QUEUE_MAXLEN), 42 | } 43 | numAsyncJobWorkersRunning.Add(1) 44 | go ajw.loop() 45 | return ajw 46 | } 47 | 48 | func (ajw *asyncJobWorker) appendJob(routine AsyncRoutine, callback AsyncCallback) { 49 | ajw.jobQueue <- asyncJobItem{routine, callback} 50 | } 51 | 52 | func (ajw *asyncJobWorker) loop() { 53 | defer numAsyncJobWorkersRunning.Done() 54 | 55 | gwutils.RepeatUntilPanicless(func() { 56 | for item := range ajw.jobQueue { 57 | res, err := item.routine() 58 | item.callback.callback(res, err) 59 | } 60 | }) 61 | } 62 | 63 | var ( 64 | asyncJobWorkersLock sync.RWMutex 65 | asyncJobWorkers = map[string]*asyncJobWorker{} 66 | ) 67 | 68 | func getAsyncJobWorker(group string) (ajw *asyncJobWorker) { 69 | asyncJobWorkersLock.RLock() 70 | ajw = asyncJobWorkers[group] 71 | asyncJobWorkersLock.RUnlock() 72 | 73 | if ajw == nil { 74 | asyncJobWorkersLock.Lock() 75 | ajw = asyncJobWorkers[group] 76 | if ajw == nil { 77 | ajw = newAsyncJobWorker() 78 | asyncJobWorkers[group] = ajw 79 | } 80 | asyncJobWorkersLock.Unlock() 81 | } 82 | return 83 | } 84 | 85 | // AppendAsyncJob append an async job to be executed asyncly (not in the game goroutine) 86 | func AppendAsyncJob(group string, routine AsyncRoutine, callback AsyncCallback) { 87 | ajw := getAsyncJobWorker(group) 88 | ajw.appendJob(routine, callback) 89 | } 90 | 91 | // WaitClear wait for all async job workers to finish (should only be called in the game goroutine) 92 | func WaitClear() bool { 93 | var cleared bool 94 | // Close all job queue workers 95 | gwlog.Infof("Waiting for all async job workers to be cleared ...") 96 | asyncJobWorkersLock.Lock() 97 | if len(asyncJobWorkers) > 0 { 98 | for group, alw := range asyncJobWorkers { 99 | close(alw.jobQueue) 100 | gwlog.Infof("\tclear %s", group) 101 | } 102 | asyncJobWorkers = map[string]*asyncJobWorker{} 103 | cleared = true 104 | } 105 | asyncJobWorkersLock.Unlock() 106 | 107 | // wait for all job workers to quit 108 | numAsyncJobWorkersRunning.Wait() 109 | return cleared 110 | } 111 | -------------------------------------------------------------------------------- /engine/kvdb/backend/kvdbrediscluster/kvdb_redis_cluster.go: -------------------------------------------------------------------------------- 1 | package kvdbrediscluster 2 | 3 | import ( 4 | "io" 5 | 6 | "time" 7 | 8 | "github.com/chasex/redis-go-cluster" 9 | "github.com/pkg/errors" 10 | "github.com/xiaonanln/goworld/engine/kvdb/types" 11 | ) 12 | 13 | const ( 14 | keyPrefix = "_KV_" 15 | ) 16 | 17 | type redisKVDB struct { 18 | c redis.Cluster 19 | } 20 | 21 | // OpenRedisKVDB opens Redis for KVDB backend 22 | func OpenRedisKVDB(startNodes []string) (kvdbtypes.KVDBEngine, error) { 23 | c, err := redis.NewCluster(&redis.Options{ 24 | StartNodes: startNodes, 25 | ConnTimeout: 10 * time.Second, // Connection timeout 26 | ReadTimeout: 60 * time.Second, // Read timeout 27 | WriteTimeout: 60 * time.Second, // Write timeout 28 | KeepAlive: 1, // Maximum keep alive connecion in each node 29 | AliveTime: 10 * time.Minute, // Keep alive timeout 30 | }) 31 | if err != nil { 32 | return nil, errors.Wrap(err, "redis dail failed") 33 | } 34 | 35 | db := &redisKVDB{ 36 | c: c, 37 | } 38 | 39 | return db, nil 40 | } 41 | 42 | func (db *redisKVDB) initialize(dbindex int) error { 43 | if dbindex >= 0 { 44 | if _, err := db.c.Do("SELECT", dbindex); err != nil { 45 | return err 46 | } 47 | } 48 | 49 | //keyMatch := keyPrefix + "*" 50 | //r, err := redis.Values(db.c.Do("SCAN", "0", "MATCH", keyMatch, "COUNT", 10000)) 51 | //if err != nil { 52 | // return err 53 | //} 54 | //for { 55 | // nextCursor := r[0] 56 | // keys, err := redis.Strings(r[1], nil) 57 | // if err != nil { 58 | // return err 59 | // } 60 | // for _, key := range keys { 61 | // key := key[len(keyPrefix):] 62 | // db.keyTree.ReplaceOrInsert(keyTreeItem{key}) 63 | // } 64 | // 65 | // if db.isZeroCursor(nextCursor) { 66 | // break 67 | // } 68 | // r, err = redis.Values(db.c.Do("SCAN", nextCursor, "MATCH", keyMatch, "COUNT", 10000)) 69 | // if err != nil { 70 | // return err 71 | // } 72 | //} 73 | return nil 74 | } 75 | 76 | func (db *redisKVDB) isZeroCursor(c interface{}) bool { 77 | return string(c.([]byte)) == "0" 78 | } 79 | 80 | func (db *redisKVDB) Get(key string) (val string, err error) { 81 | r, err := db.c.Do("GET", keyPrefix+key) 82 | if err != nil { 83 | return "", err 84 | } 85 | if r == nil { 86 | return "", nil 87 | } 88 | return string(r.([]byte)), err 89 | } 90 | 91 | func (db *redisKVDB) Put(key string, val string) error { 92 | _, err := db.c.Do("SET", keyPrefix+key, val) 93 | return err 94 | } 95 | 96 | type redisKVDBIterator struct { 97 | db *redisKVDB 98 | leftKeys []string 99 | } 100 | 101 | func (it *redisKVDBIterator) Next() (kvdbtypes.KVItem, error) { 102 | if len(it.leftKeys) == 0 { 103 | return kvdbtypes.KVItem{}, io.EOF 104 | } 105 | 106 | key := it.leftKeys[0] 107 | it.leftKeys = it.leftKeys[1:] 108 | val, err := it.db.Get(key) 109 | if err != nil { 110 | return kvdbtypes.KVItem{}, err 111 | } 112 | 113 | return kvdbtypes.KVItem{key, val}, nil 114 | } 115 | 116 | func (db *redisKVDB) Find(beginKey string, endKey string) (kvdbtypes.Iterator, error) { 117 | return nil, errors.Errorf("operation not supported on redis") 118 | } 119 | 120 | func (db *redisKVDB) Close() { 121 | } 122 | 123 | func (db *redisKVDB) IsConnectionError(err error) bool { 124 | return err == io.EOF || err == io.ErrUnexpectedEOF 125 | } 126 | -------------------------------------------------------------------------------- /cmd/goworld/status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "fmt" 8 | 9 | "github.com/xiaonanln/goworld/cmd/goworld/process" 10 | "github.com/xiaonanln/goworld/engine/config" 11 | ) 12 | 13 | // ServerStatus represents the status of a server 14 | type ServerStatus struct { 15 | NumDispatcherRunning int 16 | NumGatesRunning int 17 | NumGamesRunning int 18 | 19 | DispatcherProcs []process.Process 20 | GateProcs []process.Process 21 | GameProcs []process.Process 22 | ServerID ServerID 23 | } 24 | 25 | // IsRunning returns if a server is running 26 | func (ss *ServerStatus) IsRunning() bool { 27 | return ss.NumDispatcherRunning > 0 || ss.NumGatesRunning > 0 || ss.NumGamesRunning > 0 28 | } 29 | 30 | func detectServerStatus() *ServerStatus { 31 | ss := &ServerStatus{} 32 | procs, err := process.Processes() 33 | checkErrorOrQuit(err, "list processes failed") 34 | for _, proc := range procs { 35 | path, err := proc.Path() 36 | if err != nil { 37 | continue 38 | } 39 | 40 | if !isexists(path) { 41 | cmdline, err := proc.CmdlineSlice() 42 | if err != nil { 43 | continue 44 | } 45 | path = cmdline[0] 46 | if !filepath.IsAbs(path) { 47 | cwd, err := proc.Cwd() 48 | if err != nil { 49 | continue 50 | } 51 | path = filepath.Join(cwd, path) 52 | } 53 | 54 | } 55 | 56 | relpath, err := filepath.Rel(env.GoWorldRoot, path) 57 | if err != nil || strings.HasPrefix(relpath, "..") { 58 | continue 59 | } 60 | 61 | dir, file := filepath.Split(relpath) 62 | 63 | if file == "dispatcher"+BinaryExtension { 64 | ss.NumDispatcherRunning++ 65 | ss.DispatcherProcs = append(ss.DispatcherProcs, proc) 66 | } else if file == "gate"+BinaryExtension { 67 | ss.NumGatesRunning++ 68 | ss.GateProcs = append(ss.GateProcs, proc) 69 | } else { 70 | if strings.HasSuffix(dir, string(filepath.Separator)) { 71 | dir = dir[:len(dir)-1] 72 | } 73 | serverid := ServerID(strings.Join(strings.Split(dir, string(filepath.Separator)), "/")) 74 | if strings.HasPrefix(string(serverid), "cmd/") || strings.HasPrefix(string(serverid), "components/") || string(serverid) == "examples/test_client" { 75 | // this is a cmd or a component, not a game 76 | continue 77 | } 78 | ss.NumGamesRunning++ 79 | ss.GameProcs = append(ss.GameProcs, proc) 80 | if ss.ServerID == "" { 81 | ss.ServerID = serverid 82 | } 83 | } 84 | } 85 | 86 | return ss 87 | } 88 | 89 | func status() { 90 | ss := detectServerStatus() 91 | showServerStatus(ss) 92 | } 93 | 94 | func showServerStatus(ss *ServerStatus) { 95 | showMsg("%d dispatcher running, %d/%d gates running, %d/%d games (%s) running", ss.NumDispatcherRunning, 96 | ss.NumGatesRunning, config.GetDeployment().DesiredGates, 97 | ss.NumGamesRunning, config.GetDeployment().DesiredGames, 98 | ss.ServerID, 99 | ) 100 | 101 | var listProcs []process.Process 102 | listProcs = append(listProcs, ss.DispatcherProcs...) 103 | listProcs = append(listProcs, ss.GameProcs...) 104 | listProcs = append(listProcs, ss.GateProcs...) 105 | for _, proc := range listProcs { 106 | cmdlineSlice, err := proc.CmdlineSlice() 107 | var cmdline string 108 | if err == nil { 109 | cmdline = strings.Join(cmdlineSlice, " ") 110 | } else { 111 | cmdline = fmt.Sprintf("get cmdline failed: %e", err) 112 | } 113 | 114 | showMsg("\t%-10d%-16s%s", proc.Pid(), proc.Executable(), cmdline) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /examples/test_game/Account.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/xiaonanln/goworld" 8 | "github.com/xiaonanln/goworld/engine/common" 9 | "github.com/xiaonanln/goworld/engine/entity" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | ) 12 | 13 | // Account entity for login process 14 | type Account struct { 15 | entity.Entity // Entity type should always inherit entity.Entity 16 | username string 17 | logining bool 18 | } 19 | 20 | func (a *Account) DescribeEntityType(desc *entity.EntityTypeDesc) { 21 | } 22 | 23 | func (a *Account) getAvatarID(username string, callback func(entityID common.EntityID, err error)) { 24 | goworld.GetKVDB(username, func(val string, err error) { 25 | if a.IsDestroyed() { 26 | return 27 | } 28 | callback(common.EntityID(val), err) 29 | }) 30 | } 31 | 32 | func (a Account) setAvatarID(username string, avatarID common.EntityID) { 33 | goworld.PutKVDB(username, string(avatarID), nil) 34 | } 35 | 36 | // Login_Client is the login RPC for clients 37 | func (a *Account) Login_Client(username string, password string) { 38 | if a.logining { 39 | // logining 40 | gwlog.Errorf("%s is already logining", a) 41 | return 42 | } 43 | 44 | gwlog.Infof("%s logining with username %s password %s ...", a, username, password) 45 | if password != "123456" { 46 | a.CallClient("OnLogin", false) 47 | return 48 | } 49 | 50 | a.logining = true 51 | a.CallClient("OnLogin", true) 52 | a.getAvatarID(username, func(avatarID common.EntityID, err error) { 53 | if err != nil { 54 | gwlog.Panic(err) 55 | } 56 | 57 | gwlog.Debugf("Username %s get avatar id = %s", username, avatarID) 58 | if avatarID.IsNil() { 59 | // avatar not found, create new avatar 60 | avatar := goworld.CreateEntityLocally("Avatar") 61 | avatarID = avatar.ID 62 | a.setAvatarID(username, avatarID) 63 | a.onAvatarEntityFound(avatar) 64 | } else { 65 | goworld.LoadEntityAnywhere("Avatar", avatarID) 66 | a.Call(avatarID, "GetSpaceID", a.ID) // request for avatar space ID 67 | } 68 | }) 69 | } 70 | 71 | // OnGetAvatarSpaceID is called by Avatar to send spaceID 72 | func (a *Account) OnGetAvatarSpaceID(avatarID common.EntityID, spaceID common.EntityID) { 73 | // avatar may be in the same space with account, check again 74 | avatar := goworld.GetEntity(avatarID) 75 | if avatar != nil { 76 | a.onAvatarEntityFound(avatar) 77 | return 78 | } 79 | 80 | a.Attrs.SetStr("loginAvatarID", string(avatarID)) 81 | a.EnterSpace(spaceID, entity.Vector3{}) 82 | } 83 | 84 | func (a *Account) onAvatarEntityFound(avatar *entity.Entity) { 85 | a.GiveClientTo(avatar) 86 | } 87 | 88 | // OnClientDisconnected is triggered when client is disconnected or given 89 | func (a *Account) OnClientDisconnected() { 90 | a.Destroy() 91 | } 92 | 93 | // OnMigrateIn is called when Account entity is migrate in 94 | func (a *Account) OnMigrateIn() { 95 | loginAvatarID := common.EntityID(a.Attrs.GetStr("loginAvatarID")) 96 | avatar := goworld.GetEntity(loginAvatarID) 97 | gwlog.Debugf("%s migrating in, attrs=%v, loginAvatarID=%s, avatar=%v, client=%s", a, a.Attrs.ToMap(), loginAvatarID, avatar, a.GetClient()) 98 | 99 | if avatar != nil { 100 | a.onAvatarEntityFound(avatar) 101 | } else { 102 | // failed ? try again 103 | a.AddCallback(time.Millisecond*time.Duration(rand.Intn(3000)), "RetryLoginToAvatar", loginAvatarID) 104 | } 105 | } 106 | 107 | // RetryLoginToAvatar retry to login 108 | func (a *Account) RetryLoginToAvatar(loginAvatarID common.EntityID) { 109 | goworld.LoadEntityAnywhere("Avatar", loginAvatarID) 110 | a.Call(loginAvatarID, "GetSpaceID", a.ID) // request for avatar space ID 111 | } 112 | -------------------------------------------------------------------------------- /engine/storage/backend/mongodb/mongodb.go: -------------------------------------------------------------------------------- 1 | package entitystoragemongodb 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | 7 | "io" 8 | 9 | "github.com/xiaonanln/goworld/engine/common" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | "github.com/xiaonanln/goworld/engine/storage/storage_common" 12 | ) 13 | 14 | const ( 15 | _DEFAULT_DB_NAME = "goworld" 16 | ) 17 | 18 | var ( 19 | db *mgo.Database 20 | ) 21 | 22 | type mongoDBEntityStorge struct { 23 | db *mgo.Database 24 | } 25 | 26 | // OpenMongoDB opens mongodb as entity storage 27 | func OpenMongoDB(url string, dbname string) (storagecommon.EntityStorage, error) { 28 | gwlog.Debugf("Connecting MongoDB ...") 29 | session, err := mgo.Dial(url) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | session.SetMode(mgo.Monotonic, true) 35 | if dbname == "" { 36 | // if db is not specified, use default 37 | dbname = _DEFAULT_DB_NAME 38 | } 39 | db = session.DB(dbname) 40 | return &mongoDBEntityStorge{ 41 | db: db, 42 | }, nil 43 | } 44 | 45 | func (es *mongoDBEntityStorge) Write(typeName string, entityID common.EntityID, data interface{}) error { 46 | col := es.getCollection(typeName) 47 | _, err := col.UpsertId(entityID, bson.M{ 48 | "data": data, 49 | }) 50 | return err 51 | } 52 | 53 | func (es *mongoDBEntityStorge) Read(typeName string, entityID common.EntityID) (interface{}, error) { 54 | col := es.getCollection(typeName) 55 | q := col.FindId(entityID) 56 | var doc bson.M 57 | err := q.One(&doc) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return es.convertM2Map(doc["data"].(bson.M)), nil 62 | } 63 | 64 | func (es *mongoDBEntityStorge) convertM2Map(m bson.M) map[string]interface{} { 65 | ma := map[string]interface{}(m) 66 | es.convertM2MapInMap(ma) 67 | return ma 68 | } 69 | 70 | func (es *mongoDBEntityStorge) convertM2MapInMap(m map[string]interface{}) { 71 | for k, v := range m { 72 | switch im := v.(type) { 73 | case bson.M: 74 | m[k] = es.convertM2Map(im) 75 | case map[string]interface{}: 76 | es.convertM2MapInMap(im) 77 | case []interface{}: 78 | es.convertM2MapInList(im) 79 | } 80 | } 81 | } 82 | 83 | func (es *mongoDBEntityStorge) convertM2MapInList(l []interface{}) { 84 | for i, v := range l { 85 | switch im := v.(type) { 86 | case bson.M: 87 | l[i] = es.convertM2Map(im) 88 | case map[string]interface{}: 89 | es.convertM2MapInMap(im) 90 | case []interface{}: 91 | es.convertM2MapInList(im) 92 | } 93 | } 94 | } 95 | 96 | func (es *mongoDBEntityStorge) getCollection(typeName string) *mgo.Collection { 97 | return es.db.C(typeName) 98 | } 99 | 100 | func (es *mongoDBEntityStorge) List(typeName string) ([]common.EntityID, error) { 101 | col := es.getCollection(typeName) 102 | var docs []bson.M 103 | err := col.Find(nil).Select(bson.M{"_id": 1}).All(&docs) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | entityIDs := make([]common.EntityID, len(docs)) 109 | for i, doc := range docs { 110 | entityIDs[i] = common.EntityID(doc["_id"].(string)) 111 | } 112 | return entityIDs, nil 113 | } 114 | 115 | func (es *mongoDBEntityStorge) Exists(typeName string, entityID common.EntityID) (bool, error) { 116 | col := es.getCollection(typeName) 117 | query := col.FindId(entityID) 118 | var doc bson.M 119 | err := query.One(&doc) 120 | if err == nil { 121 | // doc found 122 | return true, nil 123 | } else if err == mgo.ErrNotFound { 124 | return false, nil 125 | } else { 126 | return false, err 127 | } 128 | } 129 | 130 | func (es *mongoDBEntityStorge) Close() { 131 | es.db.Session.Close() 132 | } 133 | 134 | func (es *mongoDBEntityStorge) IsEOF(err error) bool { 135 | return err == io.EOF || err == io.ErrUnexpectedEOF 136 | } 137 | -------------------------------------------------------------------------------- /examples/chatroom_demo/Account.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld" 5 | "github.com/xiaonanln/goworld/engine/common" 6 | "github.com/xiaonanln/goworld/engine/entity" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | ) 9 | 10 | // Account 是账号对象类型,用于处理注册、登录逻辑 11 | type Account struct { 12 | entity.Entity // 自定义对象类型必须继承entity.Entity 13 | logining bool 14 | } 15 | 16 | func (a *Account) DescribeEntityType(desc *entity.EntityTypeDesc) { 17 | } 18 | 19 | // Register_Client 是处理玩家注册请求的RPC函数 20 | func (a *Account) Register_Client(username string, password string) { 21 | gwlog.Debugf("Register %s %s", username, password) 22 | goworld.GetKVDB("password$"+username, func(val string, err error) { 23 | if err != nil { 24 | a.CallClient("ShowError", "服务器错误:"+err.Error()) 25 | return 26 | } 27 | 28 | if val != "" { 29 | a.CallClient("ShowError", "这个账号已经存在") 30 | return 31 | } 32 | goworld.PutKVDB("password$"+username, password, func(err error) { 33 | avatar := goworld.CreateEntityLocally("Avatar") // 创建一个Avatar对象然后立刻销毁,产生一次存盘 34 | avatar.Attrs.SetStr("name", username) 35 | avatar.Destroy() 36 | goworld.PutKVDB("avatarID$"+username, string(avatar.ID), func(err error) { 37 | a.CallClient("ShowInfo", "注册成功,请点击登录") 38 | }) 39 | }) 40 | }) 41 | } 42 | 43 | // Login_Client 是处理玩家登录请求的RPC函数 44 | func (a *Account) Login_Client(username string, password string) { 45 | if a.logining { 46 | gwlog.Errorf("%s is already logining", a) 47 | return 48 | } 49 | 50 | gwlog.Infof("%s logining with username %s password %s ...", a, username, password) 51 | a.logining = true 52 | goworld.GetKVDB("password$"+username, func(correctPassword string, err error) { 53 | if err != nil { 54 | a.logining = false 55 | a.CallClient("ShowError", "服务器错误:"+err.Error()) 56 | return 57 | } 58 | 59 | if correctPassword == "" { 60 | a.logining = false 61 | a.CallClient("ShowError", "账号不存在") 62 | return 63 | } 64 | 65 | if password != correctPassword { 66 | a.logining = false 67 | a.CallClient("ShowError", "密码错误") 68 | return 69 | } 70 | 71 | goworld.GetKVDB("avatarID$"+username, func(_avatarID string, err error) { 72 | if err != nil { 73 | a.logining = false 74 | a.CallClient("ShowError", "服务器错误:"+err.Error()) 75 | return 76 | } 77 | avatarID := common.EntityID(_avatarID) 78 | goworld.LoadEntityAnywhere("Avatar", avatarID) 79 | a.Call(avatarID, "GetSpaceID", a.ID) 80 | }) 81 | }) 82 | } 83 | 84 | // OnGetAvatarSpaceID 是用于接收Avatar场景编号的回调函数 85 | func (a *Account) OnGetAvatarSpaceID(avatarID common.EntityID, spaceID common.EntityID) { 86 | // avatar may be in the same space with account, check again 87 | avatar := goworld.GetEntity(avatarID) 88 | if avatar != nil { 89 | a.onAvatarEntityFound(avatar) 90 | return 91 | } 92 | 93 | a.Attrs.SetStr("loginAvatarID", string(avatarID)) 94 | a.EnterSpace(spaceID, entity.Vector3{}) 95 | } 96 | 97 | func (a *Account) onAvatarEntityFound(avatar *entity.Entity) { 98 | a.logining = false 99 | a.GiveClientTo(avatar) // 将Account的客户端移交给Avatar 100 | } 101 | 102 | // OnClientDisconnected 在客户端掉线或者给了Avatar后触发 103 | func (a *Account) OnClientDisconnected() { 104 | a.Destroy() 105 | } 106 | 107 | // OnMigrateIn 在账号迁移到目标服务器的时候调用 108 | func (a *Account) OnMigrateIn() { 109 | loginAvatarID := common.EntityID(a.Attrs.GetStr("loginAvatarID")) 110 | avatar := goworld.GetEntity(loginAvatarID) 111 | gwlog.Debugf("%s migrating in, attrs=%v, loginAvatarID=%s, avatar=%v, client=%s", a, a.Attrs.ToMap(), loginAvatarID, avatar, a.GetClient()) 112 | 113 | if avatar != nil { 114 | a.onAvatarEntityFound(avatar) 115 | } else { 116 | // failed 117 | a.CallClient("ShowError", "登录失败,请重试") 118 | a.logining = false 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /cmd/goworld/start.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/xiaonanln/goworld/engine/config" 14 | "github.com/xiaonanln/goworld/engine/consts" 15 | ) 16 | 17 | func start(sid ServerID) { 18 | err := os.Chdir(env.GoWorldRoot) 19 | checkErrorOrQuit(err, "chdir to goworld directory failed") 20 | 21 | ss := detectServerStatus() 22 | if ss.NumDispatcherRunning > 0 || ss.NumGatesRunning > 0 { 23 | status() 24 | showMsgAndQuit("server is already running, can not start multiple servers") 25 | } 26 | 27 | startDispatchers() 28 | startGames(sid, false) 29 | startGates() 30 | } 31 | 32 | func startDispatchers() { 33 | showMsg("start dispatchers ...") 34 | dispatcherIds := config.GetDispatcherIDs() 35 | showMsg("dispatcher ids: %v", dispatcherIds) 36 | for _, dispid := range dispatcherIds { 37 | startDispatcher(dispid) 38 | } 39 | } 40 | 41 | func startDispatcher(dispid uint16) { 42 | cfg := config.GetDispatcher(dispid) 43 | args := []string{"-dispid", strconv.Itoa(int(dispid))} 44 | if arguments.runInDaemonMode { 45 | args = append(args, "-d") 46 | } 47 | cmd := exec.Command(env.GetDispatcherBinary(), args...) 48 | err := runCmdUntilTag(cmd, cfg.LogFile, consts.DISPATCHER_STARTED_TAG, time.Second*10) 49 | checkErrorOrQuit(err, "start dispatcher failed, see dispatcher.log for error") 50 | } 51 | 52 | func startGames(sid ServerID, isRestore bool) { 53 | showMsg("start games ...") 54 | desiredGames := config.GetDeployment().DesiredGames 55 | showMsg("desired games = %d", desiredGames) 56 | for gameid := uint16(1); int(gameid) <= desiredGames; gameid++ { 57 | startGame(sid, gameid, isRestore) 58 | } 59 | } 60 | 61 | func startGame(sid ServerID, gameid uint16, isRestore bool) { 62 | showMsg("start game %d ...", gameid) 63 | 64 | gameExePath := filepath.Join(sid.Path(), sid.Name()+BinaryExtension) 65 | args := []string{"-gid", strconv.Itoa(int(gameid))} 66 | if isRestore { 67 | args = append(args, "-restore") 68 | } 69 | if arguments.runInDaemonMode { 70 | args = append(args, "-d") 71 | } 72 | cmd := exec.Command(gameExePath, args...) 73 | err := runCmdUntilTag(cmd, config.GetGame(gameid).LogFile, consts.GAME_STARTED_TAG, time.Second*600) 74 | checkErrorOrQuit(err, "start game failed, see game.log for error") 75 | } 76 | 77 | func startGates() { 78 | showMsg("start gates ...") 79 | desiredGates := config.GetDeployment().DesiredGates 80 | showMsg("desired gates = %d", desiredGates) 81 | for gateid := uint16(1); int(gateid) <= desiredGates; gateid++ { 82 | startGate(gateid) 83 | } 84 | } 85 | 86 | func startGate(gateid uint16) { 87 | showMsg("start gate %d ...", gateid) 88 | 89 | args := []string{"-gid", strconv.Itoa(int(gateid))} 90 | if arguments.runInDaemonMode { 91 | args = append(args, "-d") 92 | } 93 | cmd := exec.Command(env.GetGateBinary(), args...) 94 | err := runCmdUntilTag(cmd, config.GetGate(gateid).LogFile, consts.GATE_STARTED_TAG, time.Second*10) 95 | checkErrorOrQuit(err, "start gate failed, see gate.log for error") 96 | } 97 | 98 | func runCmdUntilTag(cmd *exec.Cmd, logFile string, tag string, timeout time.Duration) (err error) { 99 | clearLogFile(logFile) 100 | err = cmd.Start() 101 | if err != nil { 102 | return 103 | } 104 | 105 | timeoutTime := time.Now().Add(timeout) 106 | for time.Now().Before(timeoutTime) { 107 | time.Sleep(time.Millisecond * 200) 108 | if isTagInFile(logFile, tag) { 109 | cmd.Process.Release() 110 | return 111 | } 112 | } 113 | 114 | err = errors.Errorf("wait started tag timeout") 115 | return 116 | } 117 | 118 | func clearLogFile(logFile string) { 119 | ioutil.WriteFile(logFile, []byte{}, 0644) 120 | } 121 | 122 | func isTagInFile(filename string, tag string) bool { 123 | data, err := ioutil.ReadFile(filename) 124 | checkErrorOrQuit(err, "read file error") 125 | return strings.Contains(string(data), tag) 126 | } 127 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | GoWorld is a distributed game server engine. GoWorld adopts a Space-Entity framework for game server programming. 3 | Entities can migrate between spaces by calling `EnterSpace`. Entities can call each other using EntityID which is a 4 | global unique identifier for each entity. Entites can be used to represent game objects like players, monsters, NPCs, etc. 5 | 6 | Multiprocessing 7 | 8 | GoWorld server contains multiple processes. There should be at least 3 processes: 1 dispatcher + 1 gate + 1 game. 9 | The gate process is responsable for handling game client connections. Currently, gate supports multiple 10 | transmission protocol including TCP, KCP or WebSocket. It also support data compression and encryption. 11 | The game process is where game logic actually runs. A Space will always reside in one game process where it is created. 12 | Entities can migrate between multiple game processes by entering spaces on other game processes. 13 | GoWorld server can scale arbitrarily by running more process. 14 | 15 | Package goworld 16 | 17 | goworld package is dedicated to provide GoWorld game engine APIs for developers. Most of time developers should use 18 | functions exported by goworld package to manipulate spaces and entities. Developers can also use public methods of 19 | Space and Entity. 20 | 21 | Run game 22 | 23 | GoWorld does not provide a game executable. Developers have to build their own game program. A common game program looks 24 | like bellow: 25 | 26 | import "goworld" 27 | 28 | func main() { 29 | goworld.RegisterSpace(&MySpace{}) // Register a custom Space type 30 | // Register service entity types 31 | goworld.RegisterService("OnlineService", &OnlineService{}) 32 | goworld.RegisterService("SpaceService", &SpaceService{}) 33 | // Register Account entity type 34 | goworld.RegisterEntity("Account", &Account{}) 35 | // Register Monster entity type 36 | goworld.RegisterEntity("Monster", &Monster{}) 37 | // Register Player entity type 38 | goworld.RegisterEntity("Player", &Player{}) 39 | // Run the game server 40 | goworld.Run() 41 | } 42 | 43 | Basically, you need to register space type, service types and entity types and then start the endless loop of game logic. 44 | 45 | Creating Spaces 46 | 47 | Use goworld.CreateSpace* functions to create spaces. 48 | 49 | Creating Entities 50 | 51 | Use goworld.CreateEntity* functions to create entities. 52 | 53 | Loading Entities 54 | 55 | Use goworld.LoadEntity* functions to load entities from database. 56 | 57 | Entity RPC 58 | 59 | Use goworld.Call* functions to do RPC among entities 60 | 61 | Entity storage and attributes 62 | 63 | Each entity type should override function DescribeEntityType to declare its expected behavior and all attributes, 64 | just like bellow. 65 | 66 | func (a *Avatar) DescribeEntityType(desc *entity.EntityTypeDesc) { 67 | desc.SetPersistent(true).SetUseAOI(true, 100) 68 | desc.DefineAttr("name", "AllClients", "Persistent") 69 | desc.DefineAttr("exp", "Client", "Persistent") 70 | desc.DefineAttr("lastMailID", "Persistent") 71 | desc.DefineAttr("testListField", "AllClients") 72 | desc.DefineAttr("enteringNilSpace") 73 | } 74 | 75 | Function SetPersistent can be used to make entities persistent. Persistent entities' attributes will be marshalled and 76 | saved on Entity Storage (e.g. MongoDB) every configurable minutes. 77 | 78 | Entities use attributes to store related data. Attributes can be synchronized to clients automatically. 79 | An entity's "AllClient" attributes will be synchronized to all clients of entities where this entity is 80 | in its AOI range. "Client" attributes wil be synchronized to own clients of entities. "Persistent" attributes will be 81 | saved on entity storage when entities are saved periodically. 82 | When entity is migrated from one game process to another, all attributes are marshalled and sent to the target game where 83 | the entity will be reconstructed using attribute data. 84 | 85 | Configuration 86 | 87 | GoWorld uses `goworld.ini` as the default config file. Use '-configfile ' to use specified config file for processes. 88 | 89 | */ 90 | package goworld 91 | -------------------------------------------------------------------------------- /examples/unity_demo/Account.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld" 5 | "github.com/xiaonanln/goworld/engine/common" 6 | "github.com/xiaonanln/goworld/engine/entity" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | ) 9 | 10 | // Account 是账号对象类型,用于处理注册、登录逻辑 11 | type Account struct { 12 | entity.Entity // 自定义对象类型必须继承entity.Entity 13 | logIn bool 14 | } 15 | 16 | func (a *Account) DescribeEntityType(desc *entity.EntityTypeDesc) { 17 | } 18 | 19 | // Register_Client 是处理玩家注册请求的RPC函数 20 | func (a *Account) Register_Client(username string, password string) { 21 | gwlog.Debugf("Register %s %s", username, password) 22 | goworld.GetOrPutKVDB("password$"+username, password, func(oldVal string, err error) { 23 | if err != nil { 24 | a.CallClient("ShowError", "Server Error: "+err.Error()) // 服务器错误 25 | return 26 | } 27 | 28 | if oldVal == "" { 29 | 30 | player := goworld.CreateEntityLocally("Player") // 创建一个Player对象然后立刻销毁,产生一次存盘 31 | player.Attrs.SetStr("name", username) 32 | player.Destroy() 33 | 34 | goworld.PutKVDB("playerID$"+username, string(player.ID), func(err error) { 35 | a.CallClient("ShowInfo", "Registered Successfully, please click login.") // 注册成功,请点击登录 36 | }) 37 | } else { 38 | a.CallClient("ShowError", "Sorry, this account aready exists.") // 抱歉,这个账号已经存在 39 | } 40 | }) 41 | } 42 | 43 | // Login_Client 是处理玩家登录请求的RPC函数 44 | func (a *Account) Login_Client(username string, password string) { 45 | gwlog.Debugf("%s.Login: username=%s, password=%s", a, username, password) 46 | if a.logIn { 47 | // logining 48 | gwlog.Errorf("%s has already started to log in.", a) 49 | return 50 | } 51 | 52 | gwlog.Infof("%s started log in with username %s password %s ...", a, username, password) 53 | a.logIn = true 54 | goworld.GetKVDB("password$"+username, func(correctPassword string, err error) { 55 | if err != nil { 56 | a.logIn = false 57 | a.CallClient("ShowError", "Server Error: "+err.Error()) // 服务器错误 58 | return 59 | } 60 | 61 | if correctPassword == "" { 62 | a.logIn = false 63 | a.CallClient("ShowError", "Account does not exist.") // 账号不存在 64 | return 65 | } 66 | 67 | if password != correctPassword { 68 | a.logIn = false 69 | a.CallClient("ShowError", "Invalid password or username") // 密码错误 70 | return 71 | } 72 | 73 | goworld.GetKVDB("playerID$"+username, func(_playerID string, err error) { 74 | if err != nil { 75 | a.logIn = false 76 | a.CallClient("ShowError", "Server Error:"+err.Error()) // 服务器错误 77 | return 78 | } 79 | playerID := common.EntityID(_playerID) 80 | goworld.LoadEntityAnywhere("Player", playerID) 81 | a.Call(playerID, "GetSpaceID", a.ID) 82 | }) 83 | }) 84 | } 85 | 86 | // OnGetPlayerSpaceID 是用于接收Player场景编号的回调函数 87 | func (a *Account) OnGetPlayerSpaceID(playerID common.EntityID, spaceID common.EntityID) { 88 | // player may be in the same space with account, check again 89 | player := goworld.GetEntity(playerID) 90 | if player != nil { 91 | a.onPlayerEntityFound(player) 92 | return 93 | } 94 | 95 | a.Attrs.SetStr("loginPlayerID", string(playerID)) 96 | a.EnterSpace(spaceID, entity.Vector3{}) 97 | } 98 | 99 | func (a *Account) onPlayerEntityFound(player *entity.Entity) { 100 | gwlog.Infof("Player %s is found, giving client to ...", player) 101 | a.logIn = false 102 | a.GiveClientTo(player) // 将Account的客户端移交给Player 103 | } 104 | 105 | // OnClientDisconnected 在客户端掉线或者给了Player后触发 106 | func (a *Account) OnClientDisconnected() { 107 | gwlog.Debugf("destroying %s ...", a) 108 | a.Destroy() 109 | } 110 | 111 | // OnMigrateIn 在账号迁移到目标服务器的时候调用 112 | func (a *Account) OnMigrateIn() { 113 | loginPlayerID := common.EntityID(a.Attrs.GetStr("loginPlayerID")) 114 | player := goworld.GetEntity(loginPlayerID) 115 | gwlog.Debugf("%s migrating in, attrs=%v, loginPlayerID=%s, player=%v, client=%s", a, a.Attrs.ToMap(), loginPlayerID, player, a.GetClient()) 116 | 117 | if player != nil { 118 | a.onPlayerEntityFound(player) 119 | } else { 120 | // failed 121 | a.CallClient("ShowError", "登录失败,请重试") 122 | a.logIn = false 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /examples/unity_demo/Player.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld" 5 | "github.com/xiaonanln/goworld/engine/common" 6 | "github.com/xiaonanln/goworld/engine/consts" 7 | "github.com/xiaonanln/goworld/engine/entity" 8 | "github.com/xiaonanln/goworld/engine/gwlog" 9 | "strconv" 10 | ) 11 | 12 | // Player 对象代表一名玩家 13 | type Player struct { 14 | entity.Entity 15 | } 16 | 17 | func (a *Player) DescribeEntityType(desc *entity.EntityTypeDesc) { 18 | desc.SetPersistent(true).SetUseAOI(true, 100) 19 | desc.DefineAttr("name", "AllClients", "Persistent") 20 | desc.DefineAttr("lv", "AllClients", "Persistent") 21 | desc.DefineAttr("hp", "AllClients") 22 | desc.DefineAttr("hpmax", "AllClients") 23 | desc.DefineAttr("action", "AllClients") 24 | desc.DefineAttr("spaceKind", "Persistent") 25 | } 26 | 27 | // OnCreated 在Player对象创建后被调用 28 | func (a *Player) OnCreated() { 29 | a.Entity.OnCreated() 30 | a.setDefaultAttrs() 31 | } 32 | 33 | // setDefaultAttrs 设置玩家的一些默认属性 34 | func (a *Player) setDefaultAttrs() { 35 | a.Attrs.SetDefaultInt("spaceKind", 1) 36 | a.Attrs.SetDefaultStr("name", "noname") 37 | a.Attrs.SetDefaultInt("lv", 1) 38 | a.Attrs.SetDefaultInt("hp", 100) 39 | a.Attrs.SetDefaultInt("hpmax", 100) 40 | a.Attrs.SetDefaultStr("action", "idle") 41 | 42 | a.SetClientSyncing(true) 43 | } 44 | 45 | // GetSpaceID 获得玩家的场景ID并发给调用者 46 | func (a *Player) GetSpaceID(callerID common.EntityID) { 47 | a.Call(callerID, "OnGetPlayerSpaceID", a.ID, a.Space.ID) 48 | } 49 | 50 | func (p *Player) enterSpace(spaceKind int) { 51 | if p.Space.Kind == spaceKind { 52 | return 53 | } 54 | if consts.DEBUG_SPACES { 55 | gwlog.Infof("%s enter space from %d => %d", p, p.Space.Kind, spaceKind) 56 | } 57 | goworld.CallServiceShardKey("SpaceService", strconv.Itoa(spaceKind), "EnterSpace", p.ID, spaceKind) 58 | } 59 | 60 | // OnClientConnected is called when client is connected 61 | func (a *Player) OnClientConnected() { 62 | gwlog.Infof("%s client connected", a) 63 | a.enterSpace(int(a.GetInt("spaceKind"))) 64 | } 65 | 66 | // OnClientDisconnected is called when client is lost 67 | func (a *Player) OnClientDisconnected() { 68 | gwlog.Infof("%s client disconnected", a) 69 | a.Destroy() 70 | } 71 | 72 | // EnterSpace_Client is enter space RPC for client 73 | func (a *Player) EnterSpace_Client(kind int) { 74 | a.enterSpace(kind) 75 | } 76 | 77 | // DoEnterSpace is called by SpaceService to notify avatar entering specified space 78 | func (a *Player) DoEnterSpace(kind int, spaceID common.EntityID) { 79 | // let the avatar enter space with spaceID 80 | a.EnterSpace(spaceID, entity.Vector3{}) 81 | } 82 | 83 | //func (a *Player) randomPosition() entity.Vector3 { 84 | // minCoord, maxCoord := -400, 400 85 | // return entity.Vector3{ 86 | // X: entity.Coord(minCoord + rand.Intn(maxCoord-minCoord)), 87 | // Y: 0, 88 | // Z: entity.Coord(minCoord + rand.Intn(maxCoord-minCoord)), 89 | // } 90 | //} 91 | 92 | // OnEnterSpace is called when avatar enters a space 93 | func (a *Player) OnEnterSpace() { 94 | gwlog.Infof("%s ENTER SPACE %s", a, a.Space) 95 | a.SetClientSyncing(true) 96 | } 97 | 98 | func (a *Player) SetAction_Client(action string) { 99 | if a.GetInt("hp") <= 0 { // dead already 100 | return 101 | } 102 | 103 | a.Attrs.SetStr("action", action) 104 | } 105 | 106 | func (a *Player) ShootMiss_Client() { 107 | a.CallAllClients("Shoot") 108 | } 109 | 110 | func (a *Player) ShootHit_Client(victimID common.EntityID) { 111 | a.CallAllClients("Shoot") 112 | victim := a.Space.GetEntity(victimID) 113 | if victim == nil { 114 | gwlog.Warnf("Shoot %s, but monster not found", victimID) 115 | return 116 | } 117 | 118 | if victim.Attrs.GetInt("hp") <= 0 { 119 | return 120 | } 121 | 122 | monster := victim.I.(*Monster) 123 | monster.TakeDamage(50) 124 | } 125 | 126 | func (player *Player) TakeDamage(damage int64) { 127 | hp := player.GetInt("hp") 128 | if hp <= 0 { 129 | return 130 | } 131 | 132 | hp = hp - damage 133 | if hp < 0 { 134 | hp = 0 135 | } 136 | 137 | player.Attrs.SetInt("hp", hp) 138 | 139 | if hp <= 0 { 140 | // now player dead ... 141 | player.Attrs.SetStr("action", "death") 142 | player.SetClientSyncing(false) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /examples/test_game/MySpace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | timer "github.com/xiaonanln/goTimer" 8 | "github.com/xiaonanln/goworld" 9 | "github.com/xiaonanln/goworld/engine/consts" 10 | "github.com/xiaonanln/goworld/engine/entity" 11 | "github.com/xiaonanln/goworld/engine/gwlog" 12 | ) 13 | 14 | const ( 15 | _SPACE_DESTROY_CHECK_INTERVAL = time.Minute * 5 16 | ) 17 | 18 | // MySpace is the custom space type 19 | type MySpace struct { 20 | entity.Space // Space type should always inherit from entity.Space 21 | 22 | destroyCheckTimer entity.EntityTimerID 23 | } 24 | 25 | // OnSpaceCreated is called when the space is created 26 | func (space *MySpace) OnSpaceCreated() { 27 | // notify the SpaceService that it's ok 28 | space.EnableAOI(100) 29 | 30 | goworld.CallServiceShardKey("SpaceService", strconv.Itoa(space.Kind), "NotifySpaceLoaded", space.Kind, space.ID) 31 | 32 | M := 10 33 | for i := 0; i < M; i++ { 34 | space.CreateEntity("Monster", entity.Vector3{}) 35 | } 36 | } 37 | 38 | // OnEntityEnterSpace is called when entity enters space 39 | func (space *MySpace) OnEntityEnterSpace(entity *entity.Entity) { 40 | if entity.TypeName == "Avatar" { 41 | space.onAvatarEnterSpace(entity) 42 | } 43 | } 44 | 45 | func (space *MySpace) onAvatarEnterSpace(entity *entity.Entity) { 46 | space.clearDestroyCheckTimer() 47 | } 48 | 49 | // OnEntityLeaveSpace is called when entity leaves space 50 | func (space *MySpace) OnEntityLeaveSpace(entity *entity.Entity) { 51 | if entity.TypeName == "Avatar" { 52 | space.onAvatarLeaveSpace(entity) 53 | } 54 | } 55 | 56 | func (space *MySpace) onAvatarLeaveSpace(entity *entity.Entity) { 57 | if consts.DEBUG_SPACES { 58 | gwlog.Infof("Avatar %s leave space %s, left avatar count %d", entity, space, space.CountEntities("Avatar")) 59 | } 60 | if space.CountEntities("Avatar") == 0 { 61 | // no avatar left, start destroying space 62 | space.setDestroyCheckTimer() 63 | } 64 | } 65 | 66 | func (space *MySpace) setDestroyCheckTimer() { 67 | if space.destroyCheckTimer != 0 { 68 | return 69 | } 70 | 71 | space.destroyCheckTimer = space.AddTimer(_SPACE_DESTROY_CHECK_INTERVAL, "CheckForDestroy") 72 | } 73 | 74 | // CheckForDestroy checks if the space should be destroyed 75 | func (space *MySpace) CheckForDestroy() { 76 | avatarCount := space.CountEntities("Avatar") 77 | if avatarCount != 0 { 78 | gwlog.Panicf("Avatar count should be 0, but is %d", avatarCount) 79 | } 80 | 81 | goworld.CallServiceShardKey("SpaceService", strconv.Itoa(space.Kind), "RequestDestroy", space.Kind, space.ID) 82 | } 83 | 84 | func (space *MySpace) clearDestroyCheckTimer() { 85 | if space.destroyCheckTimer == 0 { 86 | return 87 | } 88 | 89 | space.CancelTimer(space.destroyCheckTimer) 90 | space.destroyCheckTimer = 0 91 | } 92 | 93 | // ConfirmRequestDestroy is called by SpaceService to confirm that the space 94 | func (space *MySpace) ConfirmRequestDestroy(ok bool) { 95 | if ok { 96 | if space.CountEntities("Avatar") != 0 { 97 | gwlog.Panicf("%s ConfirmRequestDestroy: avatar count is %d", space, space.CountEntities("Avatar")) 98 | } 99 | space.Destroy() 100 | } 101 | } 102 | 103 | // OnGameReady is called when the game server is ready 104 | func (space *MySpace) OnGameReady() { 105 | gwlog.Infof("%s on game ready", space) 106 | 107 | //if goworld.GetGameID() == 1 { // Create services on just 1 server 108 | // for _, serviceName := range _SERVICE_NAMES { 109 | // serviceName := serviceName 110 | // goworld.ListEntityIDs(serviceName, func(eids []common.EntityID, err error) { 111 | // gwlog.Infof("Found saved %s ids: %v", serviceName, eids) 112 | // 113 | // if len(eids) == 0 { 114 | // goworld.CreateEntityAnywhere(serviceName) 115 | // } else { 116 | // // already exists 117 | // serviceID := eids[0] 118 | // goworld.LoadEntityAnywhere(serviceName, serviceID) 119 | // } 120 | // }) 121 | // } 122 | //} 123 | 124 | timer.AddCallback(time.Millisecond*1000, checkServerStarted) 125 | } 126 | 127 | func (space *MySpace) TestCallNilSpaces(a, b, c, d interface{}) { 128 | gwlog.Infof("TestCallNilSpaces %v %v %v %v: CallNilSpaces works in game %d", a, b, c, d, goworld.GetGameID()) 129 | } 130 | -------------------------------------------------------------------------------- /examples/test_game/MailService.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "strconv" 7 | 8 | "github.com/xiaonanln/goworld/engine/common" 9 | "github.com/xiaonanln/goworld/engine/entity" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | "github.com/xiaonanln/goworld/engine/kvdb" 12 | "github.com/xiaonanln/goworld/engine/kvdb/types" 13 | "github.com/xiaonanln/goworld/engine/netutil" 14 | ) 15 | 16 | const ( 17 | // END_MAIL_ID is the max possible Mail ID 18 | END_MAIL_ID = 9999999999 19 | ) 20 | 21 | // MailService to handle mail sending & receiving 22 | type MailService struct { 23 | entity.Entity // Entity type should always inherit entity.Entity 24 | mailPacker netutil.MsgPacker 25 | lastMailID int 26 | } 27 | 28 | func (s *MailService) DescribeEntityType(desc *entity.EntityTypeDesc) { 29 | } 30 | 31 | // OnInit is called when initializing MailService 32 | func (s *MailService) OnInit() { 33 | s.mailPacker = netutil.MessagePackMsgPacker{} 34 | s.lastMailID = -1 35 | } 36 | 37 | // OnCreated is called when MailService is created 38 | func (s *MailService) OnCreated() { 39 | gwlog.Infof("Registering MailService ...") 40 | kvdb.GetOrPut("MailService:lastMailID", "0", func(oldVal string, err error) { 41 | if oldVal == "" { 42 | s.lastMailID = 0 43 | } else { 44 | var err error 45 | s.lastMailID, err = strconv.Atoi(oldVal) 46 | if err != nil { 47 | gwlog.Panicf("MailService: lastMailID is invalid: %#v", oldVal) 48 | } 49 | } 50 | }) 51 | } 52 | 53 | // SendMail handles send mail requests from avatars 54 | func (s *MailService) SendMail(senderID common.EntityID, senderName string, targetID common.EntityID, data MailData) { 55 | gwlog.Debugf("%s.SendMail: sender=%s,%s, target=%s, mail=%v", s, senderID, senderName, targetID, data) 56 | 57 | mailID := s.genMailID() 58 | mailKey := s.getMailKey(mailID, targetID) 59 | 60 | mail := map[string]interface{}{ 61 | "senderID": senderID, 62 | "senderName": senderName, 63 | "targetID": targetID, 64 | "data": data, 65 | } 66 | mailBytes, err := s.mailPacker.PackMsg(mail, nil) 67 | if err != nil { 68 | gwlog.Panicf("Pack mail failed: %s", err) 69 | s.Call(senderID, "OnSendMail", false) 70 | } 71 | 72 | kvdb.Put(mailKey, string(mailBytes), func(err error) { 73 | if err != nil { 74 | gwlog.Panicf("Put mail to kvdb failed: %s", err) 75 | s.Call(senderID, "OnSendMail", false) 76 | } 77 | gwlog.Debugf("Put mail %s to KVDB succeed", mailKey) 78 | s.Call(senderID, "OnSendMail", true) 79 | // tell the target that you have got a mail 80 | s.Call(targetID, "NotifyReceiveMail") 81 | }) 82 | } 83 | 84 | // GetMails handle get mails requests from avatars 85 | func (s *MailService) GetMails(avatarID common.EntityID, lastMailID int) { 86 | beginMailKey := s.getMailKey(lastMailID+1, avatarID) 87 | endMailKey := s.getMailKey(END_MAIL_ID, avatarID) 88 | 89 | kvdb.GetRange(beginMailKey, endMailKey, func(items []kvdbtypes.KVItem, err error) { 90 | s.PanicOnError(err) 91 | 92 | var mails []interface{} 93 | for _, item := range items { // Parse the mails 94 | _, mailId := s.parseMailKey(item.Key) // eid should always equal to avatarID 95 | mails = append(mails, []interface{}{ 96 | mailId, item.Val, // val is the marshalled mail 97 | }) 98 | } 99 | 100 | s.Call(avatarID, "OnGetMails", lastMailID, mails) 101 | }) 102 | } 103 | 104 | func (s *MailService) genMailID() int { 105 | if s.lastMailID < 0 { 106 | gwlog.Panicf("MailService: lastMailId=%v (not loaded successfully)", s.lastMailID) 107 | } 108 | 109 | s.lastMailID += 1 110 | kvdb.Put("MailService:lastMailID", strconv.Itoa(s.lastMailID), func(err error) { 111 | if err != nil { 112 | gwlog.Panicf("MailService: save lastMailID failed: %+v", err) 113 | } else { 114 | gwlog.Debugf("MailService: save lastMailID = %+v", s.lastMailID) 115 | } 116 | }) 117 | return s.lastMailID 118 | } 119 | 120 | func (s *MailService) getMailKey(mailID int, targetID common.EntityID) string { 121 | return fmt.Sprintf("MailService:mail$%s$%010d", targetID, mailID) 122 | } 123 | 124 | func (s *MailService) parseMailKey(mailKey string) (common.EntityID, int) { 125 | // mail$WVKLioYW8i5wAAD9$0000020969 126 | eid := common.EntityID(mailKey[5 : 5+common.ENTITYID_LENGTH]) 127 | mailIdStr := mailKey[5+common.ENTITYID_LENGTH+1:] 128 | mailId, err := strconv.Atoi(mailIdStr) 129 | s.PanicOnError(err) 130 | return eid, mailId 131 | } 132 | -------------------------------------------------------------------------------- /examples/unity_demo/MySpace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/xiaonanln/goTimer" 8 | "github.com/xiaonanln/goworld" 9 | "github.com/xiaonanln/goworld/engine/entity" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | ) 12 | 13 | const ( 14 | _SPACE_DESTROY_CHECK_INTERVAL = time.Minute * 5 15 | ) 16 | 17 | // MySpace is the custom space type 18 | type MySpace struct { 19 | goworld.Space // Space type should always inherit from entity.Space 20 | 21 | destroyCheckTimer entity.EntityTimerID 22 | } 23 | 24 | // OnSpaceCreated is called when the space is created 25 | func (space *MySpace) OnSpaceCreated() { 26 | // notify the SpaceService that it's ok 27 | space.EnableAOI(100) 28 | 29 | goworld.CallServiceShardKey("SpaceService", strconv.Itoa(space.Kind), "NotifySpaceLoaded", space.Kind, space.ID) 30 | space.AddTimer(time.Second*5, "DumpEntityStatus") 31 | space.AddTimer(time.Second*5, "SummonMonsters") 32 | //M := 10 33 | //for i := 0; i < M; i++ { 34 | // space.CreateEntity("Monster", entity.Vector3{}) 35 | //} 36 | } 37 | 38 | func (space *MySpace) DumpEntityStatus() { 39 | space.ForEachEntity(func(e *entity.Entity) { 40 | gwlog.Debugf(">>> %s @ position %s, neighbors=%d", e, e.GetPosition(), len(e.InterestedIn)) 41 | }) 42 | } 43 | 44 | func (space *MySpace) SummonMonsters() { 45 | if space.CountEntities("Monster") < space.CountEntities("Player")*2 { 46 | space.CreateEntity("Monster", entity.Vector3{}) 47 | } 48 | } 49 | 50 | // OnEntityEnterSpace is called when entity enters space 51 | func (space *MySpace) OnEntityEnterSpace(entity *entity.Entity) { 52 | if entity.TypeName == "Player" { 53 | space.onPlayerEnterSpace(entity) 54 | } 55 | } 56 | 57 | func (space *MySpace) onPlayerEnterSpace(entity *entity.Entity) { 58 | gwlog.Debugf("Player %s enter space %s, total avatar count %d", entity, space, space.CountEntities("Player")) 59 | space.clearDestroyCheckTimer() 60 | } 61 | 62 | // OnEntityLeaveSpace is called when entity leaves space 63 | func (space *MySpace) OnEntityLeaveSpace(entity *entity.Entity) { 64 | if entity.TypeName == "Player" { 65 | space.onPlayerLeaveSpace(entity) 66 | } 67 | } 68 | 69 | func (space *MySpace) onPlayerLeaveSpace(entity *entity.Entity) { 70 | gwlog.Infof("Player %s leave space %s, left avatar count %d", entity, space, space.CountEntities("Player")) 71 | if space.CountEntities("Player") == 0 { 72 | // no avatar left, start destroying space 73 | space.setDestroyCheckTimer() 74 | } 75 | } 76 | 77 | func (space *MySpace) setDestroyCheckTimer() { 78 | if space.destroyCheckTimer != 0 { 79 | return 80 | } 81 | 82 | space.destroyCheckTimer = space.AddTimer(_SPACE_DESTROY_CHECK_INTERVAL, "CheckForDestroy") 83 | } 84 | 85 | // CheckForDestroy checks if the space should be destroyed 86 | func (space *MySpace) CheckForDestroy() { 87 | avatarCount := space.CountEntities("Player") 88 | if avatarCount != 0 { 89 | gwlog.Panicf("Player count should be 0, but is %d", avatarCount) 90 | } 91 | 92 | goworld.CallServiceShardKey("SpaceService", strconv.Itoa(space.Kind), "RequestDestroy", space.Kind, space.ID) 93 | } 94 | 95 | func (space *MySpace) clearDestroyCheckTimer() { 96 | if space.destroyCheckTimer == 0 { 97 | return 98 | } 99 | 100 | space.CancelTimer(space.destroyCheckTimer) 101 | space.destroyCheckTimer = 0 102 | } 103 | 104 | // ConfirmRequestDestroy is called by SpaceService to confirm that the space 105 | func (space *MySpace) ConfirmRequestDestroy(ok bool) { 106 | if ok { 107 | if space.CountEntities("Player") != 0 { 108 | gwlog.Panicf("%s ConfirmRequestDestroy: avatar count is %d", space, space.CountEntities("Player")) 109 | } 110 | space.Destroy() 111 | } 112 | } 113 | 114 | // OnGameReady is called when the game server is ready 115 | func (space *MySpace) OnGameReady() { 116 | timer.AddCallback(time.Millisecond*1000, checkServerStarted) 117 | } 118 | 119 | func checkServerStarted() { 120 | ok := isAllServicesReady() 121 | gwlog.Infof("checkServerStarted: %v", ok) 122 | if ok { 123 | onAllServicesReady() 124 | } else { 125 | timer.AddCallback(time.Millisecond*1000, checkServerStarted) 126 | } 127 | } 128 | 129 | func isAllServicesReady() bool { 130 | for _, serviceName := range _SERVICE_NAMES { 131 | if !goworld.CheckServiceEntitiesReady(serviceName) { 132 | gwlog.Infof("%s entities are not ready ...", serviceName) 133 | return false 134 | } 135 | } 136 | return true 137 | } 138 | 139 | func onAllServicesReady() { 140 | gwlog.Infof("All services are ready!") 141 | } 142 | -------------------------------------------------------------------------------- /engine/gwlog/gwlog.go: -------------------------------------------------------------------------------- 1 | package gwlog 2 | 3 | import ( 4 | "runtime/debug" 5 | 6 | "strings" 7 | 8 | "encoding/json" 9 | 10 | "time" 11 | 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | ) 15 | 16 | var ( 17 | // DebugLevel level 18 | DebugLevel = Level(zap.DebugLevel) 19 | // InfoLevel level 20 | InfoLevel = Level(zap.InfoLevel) 21 | // WarnLevel level 22 | WarnLevel = Level(zap.WarnLevel) 23 | // ErrorLevel level 24 | ErrorLevel = Level(zap.ErrorLevel) 25 | // PanicLevel level 26 | PanicLevel = Level(zap.PanicLevel) 27 | // FatalLevel level 28 | FatalLevel = Level(zap.FatalLevel) 29 | ) 30 | 31 | type logFormatFunc func(format string, args ...interface{}) 32 | 33 | // Level is type of log levels 34 | type Level = zapcore.Level 35 | 36 | var ( 37 | cfg zap.Config 38 | logger *zap.Logger 39 | sugar *zap.SugaredLogger 40 | source string 41 | currentLevel Level 42 | ) 43 | 44 | func init() { 45 | var err error 46 | cfgJson := []byte(`{ 47 | "level": "debug", 48 | "outputPaths": ["stderr"], 49 | "errorOutputPaths": ["stderr"], 50 | "encoding": "console", 51 | "encoderConfig": { 52 | "messageKey": "message", 53 | "levelKey": "level", 54 | "levelEncoder": "lowercase" 55 | } 56 | }`) 57 | currentLevel = DebugLevel 58 | 59 | if err = json.Unmarshal(cfgJson, &cfg); err != nil { 60 | panic(err) 61 | } 62 | cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 63 | rebuildLoggerFromCfg() 64 | } 65 | 66 | // SetSource sets the component name (dispatcher/gate/game) of gwlog module 67 | func SetSource(source_ string) { 68 | source = source_ 69 | rebuildLoggerFromCfg() 70 | } 71 | 72 | // SetLevel sets the log level 73 | func SetLevel(lv Level) { 74 | currentLevel = lv 75 | cfg.Level.SetLevel(lv) 76 | } 77 | 78 | // GetLevel get the current log level 79 | func GetLevel() Level { 80 | return currentLevel 81 | } 82 | 83 | // TraceError prints the stack and error 84 | func TraceError(format string, args ...interface{}) { 85 | Error(string(debug.Stack())) 86 | Errorf(format, args...) 87 | } 88 | 89 | // SetOutput sets the output writer 90 | func SetOutput(outputs []string) { 91 | cfg.OutputPaths = outputs 92 | rebuildLoggerFromCfg() 93 | } 94 | 95 | // ParseLevel converts string to Levels 96 | func ParseLevel(s string) Level { 97 | if strings.ToLower(s) == "debug" { 98 | return DebugLevel 99 | } else if strings.ToLower(s) == "info" { 100 | return InfoLevel 101 | } else if strings.ToLower(s) == "warn" || strings.ToLower(s) == "warning" { 102 | return WarnLevel 103 | } else if strings.ToLower(s) == "error" { 104 | return ErrorLevel 105 | } else if strings.ToLower(s) == "panic" { 106 | return PanicLevel 107 | } else if strings.ToLower(s) == "fatal" { 108 | return FatalLevel 109 | } 110 | Errorf("ParseLevel: unknown level: %s", s) 111 | return DebugLevel 112 | } 113 | 114 | func rebuildLoggerFromCfg() { 115 | if newLogger, err := cfg.Build(); err == nil { 116 | if logger != nil { 117 | logger.Sync() 118 | } 119 | logger = newLogger 120 | //logger = logger.With(zap.Time("ts", time.Now())) 121 | if source != "" { 122 | logger = logger.With(zap.String("source", source)) 123 | } 124 | setSugar(logger.Sugar()) 125 | } else { 126 | panic(err) 127 | } 128 | } 129 | 130 | func Debugf(format string, args ...interface{}) { 131 | sugar.With(zap.Time("ts", time.Now())).Debugf(format, args...) 132 | } 133 | 134 | func Infof(format string, args ...interface{}) { 135 | sugar.With(zap.Time("ts", time.Now())).Infof(format, args...) 136 | } 137 | 138 | func Warnf(format string, args ...interface{}) { 139 | sugar.With(zap.Time("ts", time.Now())).Warnf(format, args...) 140 | } 141 | 142 | func Errorf(format string, args ...interface{}) { 143 | sugar.With(zap.Time("ts", time.Now())).Errorf(format, args...) 144 | } 145 | 146 | func Panicf(format string, args ...interface{}) { 147 | sugar.With(zap.Time("ts", time.Now())).Panicf(format, args...) 148 | } 149 | 150 | func Fatalf(format string, args ...interface{}) { 151 | debug.PrintStack() 152 | sugar.With(zap.Time("ts", time.Now())).Fatalf(format, args...) 153 | } 154 | 155 | func Error(args ...interface{}) { 156 | sugar.With(zap.Time("ts", time.Now())).Error(args...) 157 | } 158 | 159 | func Panic(args ...interface{}) { 160 | sugar.With(zap.Time("ts", time.Now())).Panic(args...) 161 | } 162 | 163 | func Fatal(args ...interface{}) { 164 | sugar.With(zap.Time("ts", time.Now())).Fatal(args...) 165 | } 166 | 167 | func setSugar(sugar_ *zap.SugaredLogger) { 168 | sugar = sugar_ 169 | } 170 | -------------------------------------------------------------------------------- /engine/entity/GameClient.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/xiaonanln/goworld/engine/common" 7 | "github.com/xiaonanln/goworld/engine/consts" 8 | "github.com/xiaonanln/goworld/engine/dispatchercluster" 9 | "github.com/xiaonanln/goworld/engine/dispatchercluster/dispatcherclient" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | ) 12 | 13 | // GameClient represents the game Client of entity 14 | // 15 | // Each entity can have at most one GameClient, and GameClient can be given to other entities 16 | type GameClient struct { 17 | clientid common.ClientID 18 | gateid uint16 19 | ownerid common.EntityID 20 | } 21 | 22 | // MakeGameClient creates a GameClient object using Client ID and Game ID 23 | func MakeGameClient(clientid common.ClientID, gateid uint16) *GameClient { 24 | return &GameClient{ 25 | clientid: clientid, 26 | gateid: gateid, 27 | } 28 | } 29 | 30 | func (client *GameClient) String() string { 31 | if client == nil { 32 | return "GameClient" 33 | } 34 | return fmt.Sprintf("GameClient<%s@%d>", client.clientid, client.gateid) 35 | } 36 | 37 | func (client *GameClient) sendCreateEntity(entity *Entity, isPlayer bool) { 38 | if client == nil { 39 | return 40 | } 41 | 42 | var clientData map[string]interface{} 43 | if !isPlayer { 44 | clientData = entity.getAllClientData() 45 | } else { 46 | clientData = entity.getClientData() 47 | } 48 | 49 | pos := entity.Position 50 | yaw := entity.yaw 51 | client.selectDispatcher().SendCreateEntityOnClient(client.gateid, client.clientid, entity.TypeName, entity.ID, isPlayer, 52 | clientData, float32(pos.X), float32(pos.Y), float32(pos.Z), float32(yaw)) 53 | } 54 | 55 | func (client *GameClient) sendDestroyEntity(entity *Entity) { 56 | if client != nil { 57 | client.selectDispatcher().SendDestroyEntityOnClient(client.gateid, client.clientid, entity.TypeName, entity.ID) 58 | } 59 | } 60 | 61 | func (client *GameClient) call(entityID common.EntityID, method string, args []interface{}) { 62 | if client != nil { 63 | client.selectDispatcher().SendCallEntityMethodOnClient(client.gateid, client.clientid, entityID, method, args) 64 | } 65 | } 66 | 67 | // sendNotifyMapAttrChange updates MapAttr change to Client entity 68 | func (client *GameClient) sendNotifyMapAttrChange(entityID common.EntityID, path []interface{}, key string, val interface{}) { 69 | if client != nil { 70 | client.selectDispatcher().SendNotifyMapAttrChangeOnClient(client.gateid, client.clientid, entityID, path, key, val) 71 | } 72 | } 73 | 74 | // sendNotifyMapAttrDel updates MapAttr delete to Client entity 75 | func (client *GameClient) sendNotifyMapAttrDel(entityID common.EntityID, path []interface{}, key string) { 76 | if client != nil { 77 | client.selectDispatcher().SendNotifyMapAttrDelOnClient(client.gateid, client.clientid, entityID, path, key) 78 | } 79 | } 80 | 81 | func (client *GameClient) sendNotifyMapAttrClear(entityID common.EntityID, path []interface{}) { 82 | if client != nil { 83 | client.selectDispatcher().SendNotifyMapAttrClearOnClient(client.gateid, client.clientid, entityID, path) 84 | } 85 | } 86 | 87 | // sendNotifyListAttrChange notifies Client of ListAttr item changing 88 | func (client *GameClient) sendNotifyListAttrChange(entityID common.EntityID, path []interface{}, index uint32, val interface{}) { 89 | if client != nil { 90 | client.selectDispatcher().SendNotifyListAttrChangeOnClient(client.gateid, client.clientid, entityID, path, index, val) 91 | } 92 | } 93 | 94 | // sendNotifyListAttrPop notify Client of ListAttr popping 95 | func (client *GameClient) sendNotifyListAttrPop(entityID common.EntityID, path []interface{}) { 96 | if client != nil { 97 | client.selectDispatcher().SendNotifyListAttrPopOnClient(client.gateid, client.clientid, entityID, path) 98 | } 99 | } 100 | 101 | // sendNotifyListAttrAppend notify entity of ListAttr appending 102 | func (client *GameClient) sendNotifyListAttrAppend(entityID common.EntityID, path []interface{}, val interface{}) { 103 | if client != nil { 104 | client.selectDispatcher().SendNotifyListAttrAppendOnClient(client.gateid, client.clientid, entityID, path, val) 105 | } 106 | } 107 | 108 | func (client *GameClient) sendSetClientFilterProp(key, val string) { 109 | if client != nil { 110 | client.selectDispatcher().SendSetClientFilterProp(client.gateid, client.clientid, key, val) 111 | } 112 | } 113 | 114 | func (client *GameClient) selectDispatcher() *dispatcherclient.DispatcherClient { 115 | if consts.DEBUG_MODE { 116 | if client.ownerid == "" { 117 | gwlog.Panicf("%s select dispatcher failed: ownerid is nil", client) 118 | } 119 | } 120 | return dispatchercluster.SelectByEntityID(client.ownerid) 121 | } 122 | -------------------------------------------------------------------------------- /examples/unity_demo/Monster.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/xiaonanln/goworld/engine/entity" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | ) 9 | 10 | // Monster type 11 | type Monster struct { 12 | entity.Entity // Entity type should always inherit entity.Entity 13 | movingToTarget *entity.Entity 14 | attackingTarget *entity.Entity 15 | lastTickTime time.Time 16 | 17 | attackCD time.Duration 18 | lastAttackTime time.Time 19 | } 20 | 21 | func (monster *Monster) DescribeEntityType(desc *entity.EntityTypeDesc) { 22 | desc.SetUseAOI(true, 100) 23 | desc.DefineAttr("name", "AllClients") 24 | desc.DefineAttr("lv", "AllClients") 25 | desc.DefineAttr("hp", "AllClients") 26 | desc.DefineAttr("hpmax", "AllClients") 27 | desc.DefineAttr("action", "AllClients") 28 | } 29 | 30 | func (monster *Monster) OnEnterSpace() { 31 | monster.setDefaultAttrs() 32 | monster.AddTimer(time.Millisecond*100, "AI") 33 | monster.lastTickTime = time.Now() 34 | monster.AddTimer(time.Millisecond*30, "Tick") 35 | } 36 | 37 | func (monster *Monster) setDefaultAttrs() { 38 | monster.Attrs.SetDefaultStr("name", "minion") 39 | monster.Attrs.SetDefaultInt("lv", 1) 40 | monster.Attrs.SetDefaultInt("hpmax", 100) 41 | monster.Attrs.SetDefaultInt("hp", 100) 42 | monster.Attrs.SetDefaultStr("action", "idle") 43 | 44 | monster.attackCD = time.Second 45 | monster.lastAttackTime = time.Now() 46 | } 47 | 48 | func (monster *Monster) AI() { 49 | var nearestPlayer *entity.Entity 50 | for entity := range monster.InterestedIn { 51 | 52 | if entity.TypeName != "Player" { 53 | continue 54 | } 55 | 56 | if entity.GetInt("hp") <= 0 { 57 | // dead 58 | continue 59 | } 60 | 61 | if nearestPlayer == nil || nearestPlayer.DistanceTo(&monster.Entity) > entity.DistanceTo(&monster.Entity) { 62 | nearestPlayer = entity 63 | } 64 | } 65 | 66 | if nearestPlayer == nil { 67 | monster.Idling() 68 | return 69 | } 70 | 71 | if nearestPlayer.DistanceTo(&monster.Entity) > monster.GetAttackRange() { 72 | monster.MovingTo(nearestPlayer) 73 | } else { 74 | monster.Attacking(nearestPlayer) 75 | } 76 | } 77 | 78 | func (monster *Monster) Tick() { 79 | if monster.attackingTarget != nil && monster.IsInterestedIn(monster.attackingTarget) { 80 | now := time.Now() 81 | if !now.Before(monster.lastAttackTime.Add(monster.attackCD)) { 82 | monster.FaceTo(monster.attackingTarget) 83 | monster.attack(monster.attackingTarget.I.(*Player)) 84 | monster.lastAttackTime = now 85 | } 86 | return 87 | } 88 | 89 | if monster.movingToTarget != nil && monster.IsInterestedIn(monster.movingToTarget) { 90 | mypos := monster.GetPosition() 91 | direction := monster.movingToTarget.GetPosition().Sub(mypos) 92 | direction.Y = 0 93 | 94 | t := direction.Normalized().Mul(monster.GetSpeed() * 30 / 1000.0) 95 | monster.SetPosition(mypos.Add(t)) 96 | monster.FaceTo(monster.movingToTarget) 97 | return 98 | } 99 | 100 | } 101 | 102 | func (monster *Monster) GetSpeed() entity.Coord { 103 | return 2 104 | } 105 | 106 | func (monster *Monster) GetAttackRange() entity.Coord { 107 | return 3 108 | } 109 | 110 | func (monster *Monster) Idling() { 111 | if monster.movingToTarget == nil && monster.attackingTarget == nil { 112 | return 113 | } 114 | 115 | monster.movingToTarget = nil 116 | monster.attackingTarget = nil 117 | monster.Attrs.SetStr("action", "idle") 118 | } 119 | 120 | func (monster *Monster) MovingTo(player *entity.Entity) { 121 | if monster.movingToTarget == player { 122 | // moving target not changed 123 | return 124 | } 125 | 126 | monster.movingToTarget = player 127 | monster.attackingTarget = nil 128 | monster.Attrs.SetStr("action", "move") 129 | } 130 | 131 | func (monster *Monster) Attacking(player *entity.Entity) { 132 | if monster.attackingTarget == player { 133 | return 134 | } 135 | 136 | monster.movingToTarget = nil 137 | monster.attackingTarget = player 138 | monster.Attrs.SetStr("action", "move") 139 | } 140 | 141 | func (monster *Monster) attack(player *Player) { 142 | monster.CallAllClients("DisplayAttack", player.ID) 143 | 144 | if player.GetInt("hp") <= 0 { 145 | return 146 | } 147 | 148 | player.TakeDamage(monster.GetDamage()) 149 | } 150 | 151 | func (monster *Monster) GetDamage() int64 { 152 | return 10 153 | } 154 | 155 | func (monster *Monster) TakeDamage(damage int64) { 156 | hp := monster.GetInt("hp") 157 | hp = hp - damage 158 | if hp < 0 { 159 | hp = 0 160 | } 161 | 162 | monster.Attrs.SetInt("hp", hp) 163 | gwlog.Infof("%s TakeDamage %d => hp=%d", monster, damage, hp) 164 | if hp <= 0 { 165 | monster.Attrs.SetStr("action", "death") 166 | monster.Destroy() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /components/gate/gate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/xiaonanln/pktconn" 6 | 7 | "math/rand" 8 | "time" 9 | 10 | "os" 11 | 12 | _ "net/http/pprof" 13 | 14 | "runtime" 15 | 16 | "os/signal" 17 | 18 | "syscall" 19 | 20 | "fmt" 21 | 22 | "path" 23 | 24 | "github.com/xiaonanln/goworld/engine/binutil" 25 | "github.com/xiaonanln/goworld/engine/common" 26 | "github.com/xiaonanln/goworld/engine/config" 27 | "github.com/xiaonanln/goworld/engine/dispatchercluster" 28 | "github.com/xiaonanln/goworld/engine/dispatchercluster/dispatcherclient" 29 | "github.com/xiaonanln/goworld/engine/gwlog" 30 | "github.com/xiaonanln/goworld/engine/post" 31 | ) 32 | 33 | var ( 34 | args struct { 35 | gateid uint16 36 | configFile string 37 | logLevel string 38 | runInDaemonMode bool 39 | //listenAddr string 40 | } 41 | gateService *GateService 42 | signalChan = make(chan os.Signal, 1) 43 | ) 44 | 45 | func parseArgs() { 46 | var gateIdArg int 47 | flag.IntVar(&gateIdArg, "gid", 0, "set gateid") 48 | flag.StringVar(&args.configFile, "configfile", "", "set config file path") 49 | flag.StringVar(&args.logLevel, "log", "", "set log level, will override log level in config") 50 | flag.BoolVar(&args.runInDaemonMode, "d", false, "run in daemon mode") 51 | //flag.StringVar(&args.listenAddr, "listen-addr", "", "set listen address for gate, overriding listen_addr in config file") 52 | flag.Parse() 53 | args.gateid = uint16(gateIdArg) 54 | } 55 | 56 | func main() { 57 | rand.Seed(time.Now().UnixNano()) 58 | parseArgs() 59 | 60 | if args.runInDaemonMode { 61 | daemoncontext := binutil.Daemonize() 62 | defer daemoncontext.Release() 63 | } 64 | 65 | if args.configFile != "" { 66 | config.SetConfigFile(args.configFile) 67 | } 68 | 69 | if args.gateid <= 0 { 70 | gwlog.Errorf("gateid %d is not valid, should be positive", args.gateid) 71 | os.Exit(1) 72 | } 73 | 74 | gateConfig := config.GetGate(args.gateid) 75 | verifyGateConfig(gateConfig) 76 | if gateConfig.GoMaxProcs > 0 { 77 | gwlog.Infof("SET GOMAXPROCS = %d", gateConfig.GoMaxProcs) 78 | runtime.GOMAXPROCS(gateConfig.GoMaxProcs) 79 | } 80 | logLevel := args.logLevel 81 | if logLevel == "" { 82 | logLevel = gateConfig.LogLevel 83 | } 84 | binutil.SetupGWLog(fmt.Sprintf("gate%d", args.gateid), logLevel, gateConfig.LogFile, gateConfig.LogStderr) 85 | 86 | gateService = newGateService() 87 | if gateConfig.EncryptConnection { 88 | cfgdir := config.GetConfigDir() 89 | rsaCert := path.Join(cfgdir, gateConfig.RSACertificate) 90 | rsaKey := path.Join(cfgdir, gateConfig.RSAKey) 91 | binutil.SetupHTTPServerTLS(gateConfig.HTTPAddr, gateService.handleWebSocketConn, rsaCert, rsaKey) 92 | } else { 93 | binutil.SetupHTTPServer(gateConfig.HTTPAddr, gateService.handleWebSocketConn) 94 | } 95 | 96 | dispatchercluster.Initialize(args.gateid, dispatcherclient.GateDispatcherClientType, false, false, &gateDispatcherClientDelegate{}) 97 | //dispatcherclient.Initialize(&gateDispatcherClientDelegate{}, true) 98 | setupSignals() 99 | gateService.run() // run gate service in another goroutine 100 | } 101 | 102 | func verifyGateConfig(gateConfig *config.GateConfig) { 103 | } 104 | 105 | func setupSignals() { 106 | gwlog.Infof("Setup signals ...") 107 | signal.Ignore(syscall.Signal(10), syscall.Signal(12), syscall.SIGPIPE, syscall.SIGHUP) 108 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 109 | 110 | go func() { 111 | for { 112 | sig := <-signalChan 113 | if sig == syscall.SIGINT || sig == syscall.SIGTERM { 114 | // terminating gate ... 115 | gwlog.Infof("Terminating gate service ...") 116 | post.Post(func() { 117 | gateService.terminate() 118 | }) 119 | 120 | gateService.terminated.Wait() 121 | gwlog.Infof("Gate %d terminated gracefully.", args.gateid) 122 | os.Exit(0) 123 | } else { 124 | gwlog.Errorf("unexpected signal: %s", sig) 125 | } 126 | } 127 | }() 128 | } 129 | 130 | type gateDispatcherClientDelegate struct { 131 | } 132 | 133 | func (delegate *gateDispatcherClientDelegate) GetDispatcherClientPacketQueue() chan *pktconn.Packet { 134 | return gateService.dispatcherClientPacketQueue 135 | } 136 | 137 | func (delegate *gateDispatcherClientDelegate) HandleDispatcherClientDisconnect() { 138 | //gwlog.Errorf("Disconnected from dispatcher, try reconnecting ...") 139 | // if gate is disconnected from dispatcher, we just quit 140 | gwlog.Infof("Disconnected from dispatcher, gate has to quit.") 141 | signalChan <- syscall.SIGTERM // let gate quit 142 | } 143 | 144 | func (deleget *gateDispatcherClientDelegate) GetEntityIDsForDispatcher(dispid uint16) []common.EntityID { 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /engine/dispatchercluster/dispatchercluster.go: -------------------------------------------------------------------------------- 1 | package dispatchercluster 2 | 3 | import ( 4 | "github.com/xiaonanln/goworld/engine/common" 5 | "github.com/xiaonanln/goworld/engine/config" 6 | "github.com/xiaonanln/goworld/engine/dispatchercluster/dispatcherclient" 7 | "github.com/xiaonanln/goworld/engine/gwlog" 8 | "github.com/xiaonanln/goworld/engine/netutil" 9 | "github.com/xiaonanln/goworld/engine/proto" 10 | ) 11 | 12 | var ( 13 | dispatcherConns []*dispatcherclient.DispatcherConnMgr 14 | dispatcherNum int 15 | gid uint16 16 | ) 17 | 18 | func Initialize(_gid uint16, dctype dispatcherclient.DispatcherClientType, isRestoreGame, isBanBootEntity bool, delegate dispatcherclient.IDispatcherClientDelegate) { 19 | gid = _gid 20 | if gid == 0 { 21 | gwlog.Fatalf("gid is 0") 22 | } 23 | 24 | dispIds := config.GetDispatcherIDs() 25 | dispatcherNum = len(dispIds) 26 | if dispatcherNum == 0 { 27 | gwlog.Fatalf("dispatcher number is 0") 28 | } 29 | 30 | dispatcherConns = make([]*dispatcherclient.DispatcherConnMgr, dispatcherNum) 31 | for _, dispid := range dispIds { 32 | dispatcherConns[dispid-1] = dispatcherclient.NewDispatcherConnMgr(gid, dctype, dispid, isRestoreGame, isBanBootEntity, delegate) 33 | } 34 | for _, dispConn := range dispatcherConns { 35 | dispConn.Connect() 36 | } 37 | } 38 | 39 | func SendNotifyDestroyEntity(id common.EntityID) { 40 | SelectByEntityID(id).SendNotifyDestroyEntity(id) 41 | } 42 | 43 | func SendMigrateRequest(entityID common.EntityID, spaceID common.EntityID, spaceGameID uint16) { 44 | SelectByEntityID(entityID).SendMigrateRequest(entityID, spaceID, spaceGameID) 45 | } 46 | 47 | func SendRealMigrate(eid common.EntityID, targetGame uint16, data []byte) { 48 | SelectByEntityID(eid).SendRealMigrate(eid, targetGame, data) 49 | } 50 | func SendCallFilterClientProxies(op proto.FilterClientsOpType, key, val string, method string, args []interface{}) { 51 | pkt := proto.AllocCallFilterClientProxiesPacket(op, key, val, method, args) 52 | broadcast(pkt) 53 | pkt.Release() 54 | return 55 | } 56 | 57 | func broadcast(packet *netutil.Packet) { 58 | for _, dcm := range dispatcherConns { 59 | dcm.GetDispatcherClientForSend().SendPacket(packet) 60 | } 61 | } 62 | 63 | func SendNotifyCreateEntity(id common.EntityID) { 64 | if gid != 0 { 65 | SelectByEntityID(id).SendNotifyCreateEntity(id) 66 | } else { 67 | // goes here when creating nil space or restoring freezed entities 68 | } 69 | } 70 | 71 | func SendLoadEntityAnywhere(typeName string, entityID common.EntityID) { 72 | SelectByEntityID(entityID).SendLoadEntitySomewhere(typeName, entityID, 0) 73 | } 74 | 75 | func SendLoadEntityOnGame(typeName string, entityID common.EntityID, gameid uint16) { 76 | SelectByEntityID(entityID).SendLoadEntitySomewhere(typeName, entityID, gameid) 77 | } 78 | 79 | func SendCreateEntitySomewhere(gameid uint16, entityid common.EntityID, typeName string, data map[string]interface{}) { 80 | SelectByEntityID(entityid).SendCreateEntitySomewhere(gameid, entityid, typeName, data) 81 | } 82 | 83 | func SendGameLBCInfo(lbcinfo proto.GameLBCInfo) { 84 | packet := proto.AllocGameLBCInfoPacket(lbcinfo) 85 | broadcast(packet) 86 | packet.Release() 87 | } 88 | 89 | func SendStartFreezeGame() { 90 | pkt := proto.AllocStartFreezeGamePacket() 91 | broadcast(pkt) 92 | pkt.Release() 93 | return 94 | } 95 | 96 | func SendKvregRegister(srvid string, info string, force bool) { 97 | SelectBySrvID(srvid).SendKvregRegister(srvid, info, force) 98 | } 99 | 100 | func SendCallNilSpaces(exceptGameID uint16, method string, args []interface{}) { 101 | // construct one packet for multiple sending 102 | packet := proto.AllocCallNilSpacesPacket(exceptGameID, method, args) 103 | broadcast(packet) 104 | packet.Release() 105 | } 106 | 107 | func EntityIDToDispatcherID(entityid common.EntityID) uint16 { 108 | return uint16((hashEntityID(entityid) % dispatcherNum) + 1) 109 | } 110 | 111 | func SrvIDToDispatcherID(srvid string) uint16 { 112 | return uint16((hashSrvID(srvid) % dispatcherNum) + 1) 113 | } 114 | 115 | func SelectByEntityID(entityid common.EntityID) *dispatcherclient.DispatcherClient { 116 | idx := hashEntityID(entityid) % dispatcherNum 117 | return dispatcherConns[idx].GetDispatcherClientForSend() 118 | } 119 | 120 | func SelectByGateID(gateid uint16) *dispatcherclient.DispatcherClient { 121 | idx := hashGateID(gateid) % dispatcherNum 122 | return dispatcherConns[idx].GetDispatcherClientForSend() 123 | } 124 | 125 | func SelectByDispatcherID(dispid uint16) *dispatcherclient.DispatcherClient { 126 | return dispatcherConns[dispid-1].GetDispatcherClientForSend() 127 | } 128 | 129 | func SelectBySrvID(srvid string) *dispatcherclient.DispatcherClient { 130 | idx := hashSrvID(srvid) % dispatcherNum 131 | return dispatcherConns[idx].GetDispatcherClientForSend() 132 | } 133 | 134 | func Select(dispidx int) *dispatcherclient.DispatcherClient { 135 | return dispatcherConns[dispidx].GetDispatcherClientForSend() 136 | } 137 | -------------------------------------------------------------------------------- /engine/kvdb/kvdb_backend_test.go: -------------------------------------------------------------------------------- 1 | package kvdb 2 | 3 | import ( 4 | "math/rand" 5 | "strconv" 6 | "testing" 7 | 8 | "fmt" 9 | "io" 10 | 11 | "github.com/xiaonanln/goworld/engine/kvdb/backend/kvdb_mongodb" 12 | "github.com/xiaonanln/goworld/engine/kvdb/backend/kvdbredis" 13 | . "github.com/xiaonanln/goworld/engine/kvdb/types" 14 | ) 15 | 16 | func TestMongoBackendSet(t *testing.T) { 17 | testKVDBBackendSet(t, openTestMongoKVDB(t)) 18 | } 19 | 20 | func TestRedisBackendSet(t *testing.T) { 21 | testKVDBBackendSet(t, openTestRedisKVDB(t)) 22 | } 23 | 24 | func testKVDBBackendSet(t *testing.T, kvdb KVDBEngine) { 25 | val, err := kvdb.Get("__key_not_exists__") 26 | if err != nil || val != "" { 27 | t.Fatal(err) 28 | } 29 | 30 | for i := 0; i < 100; i++ { 31 | key := strconv.Itoa(rand.Intn(10000)) 32 | val := strconv.Itoa(rand.Intn(10000)) 33 | err = kvdb.Put(key, val) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | var verifyVal string 38 | verifyVal, err = kvdb.Get(key) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if verifyVal != val { 44 | t.Errorf("%s != %s", val, verifyVal) 45 | } 46 | } 47 | 48 | } 49 | 50 | func TestMongoBackendFind(t *testing.T) { 51 | testBackendFind(t, openTestMongoKVDB(t)) 52 | } 53 | 54 | //func TestRedisBackendFind(t *testing.T) { 55 | // testBackendFind(t, openTestRedisKVDB(t)) 56 | //} 57 | 58 | func testBackendFind(t *testing.T, kvdb KVDBEngine) { 59 | beginKey := strconv.Itoa(1000 + rand.Intn(2000-1000)) 60 | if len(beginKey) != 4 { 61 | t.Fatalf("wrong begin key: %s", beginKey) 62 | } 63 | 64 | endKey := strconv.Itoa(5000 + rand.Intn(5000)) 65 | 66 | if len(endKey) != 4 { 67 | t.Fatalf("wrong end key: %s", endKey) 68 | } 69 | if err := kvdb.Put(beginKey, beginKey); err != nil { 70 | t.Error(err) 71 | } 72 | if err := kvdb.Put(endKey, endKey); err != nil { 73 | t.Error(err) 74 | } 75 | 76 | it, err := kvdb.Find(beginKey, endKey) 77 | if err != nil { 78 | t.Error(err) 79 | return 80 | } 81 | 82 | oldKey := "" 83 | beginKeyFound, endKeyFound := false, false 84 | //println("testBackendFind", beginKey, endKey) 85 | for { 86 | item, err := it.Next() 87 | if err == io.EOF { 88 | break 89 | } 90 | 91 | if err != nil { 92 | t.Error(err) 93 | break 94 | } 95 | 96 | if item.Key <= oldKey { // the keys should be increasing 97 | t.Errorf("old key is %s, new key is %s, should be increasing", oldKey, item.Key) 98 | } 99 | 100 | //println("visit", item.Key) 101 | if item.Key == beginKey { 102 | beginKeyFound = true 103 | } else if item.Key == endKey { 104 | endKeyFound = true 105 | } 106 | 107 | //println(item.Key, item.Val) 108 | oldKey = item.Key 109 | } 110 | if !beginKeyFound { 111 | t.Errorf("begin key is not found") 112 | } 113 | if endKeyFound { 114 | t.Errorf("end key is found") 115 | } 116 | } 117 | 118 | func BenchmarkMongoBackendGetSet(b *testing.B) { 119 | benchmarkBackendGetSet(b, openTestMongoKVDB(b)) 120 | } 121 | 122 | func BenchmarkRedisBackendGetSet(b *testing.B) { 123 | benchmarkBackendGetSet(b, openTestRedisKVDB(b)) 124 | } 125 | 126 | func benchmarkBackendGetSet(b *testing.B, kvdb KVDBEngine) { 127 | key := "testkey" 128 | 129 | for i := 0; i < b.N; i++ { 130 | val := strconv.Itoa(rand.Intn(1000)) 131 | kvdb.Put(key, val) 132 | getval, err := kvdb.Get(key) 133 | if err != nil { 134 | b.Error(err) 135 | } 136 | 137 | if getval != val { 138 | b.Errorf("put %s but get %s", val, getval) 139 | } 140 | } 141 | } 142 | 143 | func BenchmarkMongoBackendFind(b *testing.B) { 144 | benchmarkBackendFind(b, openTestMongoKVDB(b)) 145 | } 146 | 147 | func BenchmarkRedisBackendFind(b *testing.B) { 148 | benchmarkBackendFind(b, openTestRedisKVDB(b)) 149 | } 150 | 151 | func benchmarkBackendFind(b *testing.B, kvdb KVDBEngine) { 152 | var keys []string 153 | for i := 1; i <= 10; i++ { 154 | keys = append(keys, fmt.Sprintf("%03d", i)) 155 | } 156 | for _, key := range keys { 157 | kvdb.Put(key, key) 158 | } 159 | 160 | //fmt.Printf("keys %v\n", keys) 161 | beginKey, endKey := keys[0], keys[len(keys)-1] 162 | b.ResetTimer() 163 | for i := 0; i < b.N; i++ { 164 | it, err := kvdb.Find(beginKey, endKey) 165 | if err != nil { 166 | b.Error(err) 167 | continue 168 | } 169 | 170 | for { 171 | _, err := it.Next() 172 | if err == io.EOF { 173 | break 174 | } 175 | if err != nil { 176 | b.Error(err) 177 | } 178 | //println(item.Key, item.Val) 179 | } 180 | } 181 | } 182 | 183 | type _Fataler interface { 184 | Fatal(args ...interface{}) 185 | } 186 | 187 | func openTestMongoKVDB(f _Fataler) KVDBEngine { 188 | kvdb, err := kvdbmongo.OpenMongoKVDB("mongodb://127.0.0.1:27017/goworld", "goworld", "__kv__") 189 | if err != nil { 190 | f.Fatal(err) 191 | } 192 | return kvdb 193 | } 194 | 195 | func openTestRedisKVDB(f _Fataler) KVDBEngine { 196 | kvdb, err := kvdbredis.OpenRedisKVDB("redis://127.0.0.1:6379", 0) 197 | if err != nil { 198 | f.Fatal(err) 199 | } 200 | return kvdb 201 | } 202 | -------------------------------------------------------------------------------- /engine/dispatchercluster/dispatcherclient/DispatcherConnMgr.go: -------------------------------------------------------------------------------- 1 | package dispatcherclient 2 | 3 | import ( 4 | "github.com/xiaonanln/pktconn" 5 | "time" 6 | 7 | "net" 8 | 9 | "fmt" 10 | 11 | "sync/atomic" 12 | "unsafe" 13 | 14 | "github.com/xiaonanln/goworld/engine/common" 15 | "github.com/xiaonanln/goworld/engine/config" 16 | "github.com/xiaonanln/goworld/engine/consts" 17 | "github.com/xiaonanln/goworld/engine/gwlog" 18 | "github.com/xiaonanln/goworld/engine/gwutils" 19 | "github.com/xiaonanln/goworld/engine/netutil" 20 | ) 21 | 22 | const ( 23 | _LOOP_DELAY_ON_DISPATCHER_CLIENT_ERROR = time.Second 24 | ) 25 | 26 | type DispatcherConnMgr struct { 27 | gid uint16 // gateid or gameid 28 | dctype DispatcherClientType 29 | dispid uint16 30 | _dispatcherClient *DispatcherClient 31 | isReconnect, isRestoreGame, isBanBootEntity bool // more properties for Game 32 | delegate IDispatcherClientDelegate 33 | } 34 | 35 | func NewDispatcherConnMgr(gid uint16, dctype DispatcherClientType, dispid uint16, isRestoreGame, isBanBootEntity bool, delegate IDispatcherClientDelegate) *DispatcherConnMgr { 36 | return &DispatcherConnMgr{ 37 | gid: gid, 38 | dctype: dctype, 39 | dispid: dispid, 40 | isRestoreGame: isRestoreGame, 41 | isBanBootEntity: isBanBootEntity, 42 | delegate: delegate, 43 | } 44 | } 45 | 46 | func (dcm *DispatcherConnMgr) getDispatcherClient() *DispatcherClient { // atomic 47 | addr := (*uintptr)(unsafe.Pointer(&dcm._dispatcherClient)) 48 | return (*DispatcherClient)(unsafe.Pointer(atomic.LoadUintptr(addr))) 49 | } 50 | 51 | func (dcm *DispatcherConnMgr) setDispatcherClient(dispatcherClient *DispatcherClient) { // atomic 52 | addr := (*uintptr)(unsafe.Pointer(&dcm._dispatcherClient)) 53 | atomic.StoreUintptr(addr, uintptr(unsafe.Pointer(dispatcherClient))) 54 | } 55 | 56 | func (dcm *DispatcherConnMgr) String() string { 57 | return fmt.Sprintf("DispatcherConnMgr<%d>", dcm.dispid) 58 | } 59 | 60 | func (dcm *DispatcherConnMgr) assureConnected() *DispatcherClient { 61 | //gwlog.Debugf("assureConnected: _dispatcherClient", _dispatcherClient) 62 | var err error 63 | dc := dcm.getDispatcherClient() 64 | for dc == nil || dc.IsClosed() { 65 | dc, err = dcm.connectDispatchClient() 66 | if err != nil { 67 | gwlog.Errorf("Connect to dispatcher%d failed: %s", dcm.dispid, err.Error()) 68 | time.Sleep(_LOOP_DELAY_ON_DISPATCHER_CLIENT_ERROR) 69 | continue 70 | } 71 | dcm.setDispatcherClient(dc) 72 | if dcm.dctype == GameDispatcherClientType { 73 | dc.SendSetGameID(dcm.gid, dcm.isReconnect, dcm.isRestoreGame, dcm.isBanBootEntity, dcm.delegate.GetEntityIDsForDispatcher(dcm.dispid)) 74 | } else { 75 | dc.SendSetGateID(dcm.gid) 76 | } 77 | dcm.isReconnect = true 78 | 79 | gwlog.Infof("dispatcher_client: connected to dispatcher: %s", dc) 80 | } 81 | return dc 82 | } 83 | 84 | func (dcm *DispatcherConnMgr) connectDispatchClient() (*DispatcherClient, error) { 85 | dispatcherConfig := config.GetDispatcher(dcm.dispid) 86 | conn, err := netutil.ConnectTCP(dispatcherConfig.AdvertiseAddr) 87 | if err != nil { 88 | return nil, err 89 | } 90 | tcpConn := conn.(*net.TCPConn) 91 | tcpConn.SetReadBuffer(consts.DISPATCHER_CLIENT_READ_BUFFER_SIZE) 92 | tcpConn.SetWriteBuffer(consts.DISPATCHER_CLIENT_WRITE_BUFFER_SIZE) 93 | dc := newDispatcherClient(dcm.dctype, conn, dcm.isReconnect, dcm.isRestoreGame) 94 | return dc, nil 95 | } 96 | 97 | // IDispatcherClientDelegate defines functions that should be implemented by dispatcher clients 98 | type IDispatcherClientDelegate interface { 99 | GetDispatcherClientPacketQueue() chan *pktconn.Packet 100 | HandleDispatcherClientDisconnect() 101 | GetEntityIDsForDispatcher(dispid uint16) []common.EntityID 102 | } 103 | 104 | // Initialize the dispatcher client, only called by engine 105 | func (dcm *DispatcherConnMgr) Connect() { 106 | dcm.assureConnected() 107 | go gwutils.RepeatUntilPanicless(dcm.serveDispatcherClient) // start the recv routine 108 | } 109 | 110 | // GetDispatcherClientForSend returns the current dispatcher client for sending messages 111 | func (dcm *DispatcherConnMgr) GetDispatcherClientForSend() *DispatcherClient { 112 | dispatcherClient := dcm.getDispatcherClient() 113 | return dispatcherClient 114 | } 115 | 116 | // serve the dispatcher client, receive RESPs from dispatcher and process 117 | func (dcm *DispatcherConnMgr) serveDispatcherClient() { 118 | gwlog.Debugf("%s.serveDispatcherClient: start serving dispatcher client ...", dcm) 119 | for { 120 | dc := dcm.assureConnected() 121 | 122 | err := dc.GoWorldConnection.RecvChan(dcm.delegate.GetDispatcherClientPacketQueue()) 123 | 124 | gwlog.TraceError("serveDispatcherClient: RecvMsgPacket error: %s", err.Error()) 125 | dc.Close() 126 | 127 | dcm.delegate.HandleDispatcherClientDisconnect() 128 | time.Sleep(_LOOP_DELAY_ON_DISPATCHER_CLIENT_ERROR) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /examples/test_game/SpaceService.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/xiaonanln/goworld" 7 | "github.com/xiaonanln/goworld/engine/common" 8 | "github.com/xiaonanln/goworld/engine/consts" 9 | "github.com/xiaonanln/goworld/engine/entity" 10 | "github.com/xiaonanln/goworld/engine/gwlog" 11 | ) 12 | 13 | const ( 14 | _MAX_AVATAR_COUNT_PER_SPACE = 100 15 | ) 16 | 17 | type enterSpaceReq struct { 18 | avatarId common.EntityID 19 | kind int 20 | } 21 | 22 | type _SpaceKindInfo struct { 23 | spaceEntities map[common.EntityID]*_SpaceEntityInfo 24 | } 25 | 26 | func (ki *_SpaceKindInfo) choose() *_SpaceEntityInfo { 27 | var best *_SpaceEntityInfo 28 | 29 | for _, ei := range ki.spaceEntities { 30 | if ei.AvatarNum >= _MAX_AVATAR_COUNT_PER_SPACE { // space is full 31 | continue 32 | } 33 | 34 | if best == nil || best.AvatarNum < ei.AvatarNum { // choose the space with more avatars 35 | best = ei 36 | } 37 | } 38 | return best 39 | } 40 | 41 | func (ki *_SpaceKindInfo) remove(spaceID common.EntityID) { 42 | delete(ki.spaceEntities, spaceID) 43 | } 44 | 45 | type _SpaceEntityInfo struct { 46 | EntityID common.EntityID 47 | Kind int 48 | LastEnterTime time.Time 49 | AvatarNum int 50 | } 51 | 52 | // SpaceService is the service entity for space management 53 | type SpaceService struct { 54 | entity.Entity 55 | 56 | spaceKinds map[int]*_SpaceKindInfo 57 | pendingRequests []enterSpaceReq 58 | } 59 | 60 | func (s *SpaceService) DescribeEntityType(desc *entity.EntityTypeDesc) { 61 | } 62 | 63 | func (s *SpaceService) getSpaceKindInfo(kind int) *_SpaceKindInfo { 64 | ki := s.spaceKinds[kind] 65 | if ki == nil { 66 | ki = &_SpaceKindInfo{ 67 | spaceEntities: map[common.EntityID]*_SpaceEntityInfo{}, 68 | } 69 | s.spaceKinds[kind] = ki 70 | } 71 | return ki 72 | } 73 | 74 | func (s *SpaceService) getSpaceEntityInfo(kind int, spaceID common.EntityID) *_SpaceEntityInfo { 75 | kindinfo := s.getSpaceKindInfo(kind) 76 | return kindinfo.spaceEntities[spaceID] 77 | } 78 | 79 | // OnInit initializes SpaceService 80 | func (s *SpaceService) OnInit() { 81 | s.spaceKinds = map[int]*_SpaceKindInfo{} 82 | s.pendingRequests = []enterSpaceReq{} 83 | } 84 | 85 | // OnCreated is called when entity is created 86 | func (s *SpaceService) OnCreated() { 87 | gwlog.Infof("Registering SpaceService ...") 88 | } 89 | 90 | // EnterSpace is called by avatar to enter space by kind 91 | func (s *SpaceService) EnterSpace(avatarId common.EntityID, kind int) { 92 | if consts.DEBUG_SPACES { 93 | gwlog.Infof("%s.EnterSpace: avatar=%s, kind=%d", s, avatarId, kind) 94 | } 95 | 96 | spaceKindInfo := s.getSpaceKindInfo(kind) 97 | spaceInfo := spaceKindInfo.choose() 98 | if spaceInfo != nil { 99 | // space already exists, tell the avatar 100 | spaceInfo.LastEnterTime = time.Now() 101 | s.Call(avatarId, "DoEnterSpace", kind, spaceInfo.EntityID) 102 | } else { 103 | s.pendingRequests = append(s.pendingRequests, enterSpaceReq{ 104 | avatarId, kind, 105 | }) 106 | // create the space 107 | goworld.CreateSpaceAnywhere(kind) 108 | } 109 | } 110 | 111 | // NotifySpaceLoaded is called when space is loaded 112 | func (s *SpaceService) NotifySpaceLoaded(loadKind int, loadSpaceID common.EntityID) { 113 | if consts.DEBUG_SPACES { 114 | gwlog.Infof("%s: space is loaded: kind=%d, loadSpaceID=%s", s, loadKind, loadSpaceID) 115 | } 116 | spaceKindInfo := s.getSpaceKindInfo(loadKind) 117 | 118 | spaceKindInfo.spaceEntities[loadSpaceID] = &_SpaceEntityInfo{ 119 | Kind: loadKind, 120 | EntityID: loadSpaceID, 121 | LastEnterTime: time.Now(), 122 | } 123 | 124 | // notify all pending requests 125 | leftPendingReqs := []enterSpaceReq{} 126 | satisfyingReqs := []enterSpaceReq{} 127 | for _, req := range s.pendingRequests { 128 | if req.kind == loadKind { 129 | // this req can be satisfied 130 | satisfyingReqs = append(satisfyingReqs, req) 131 | } else { 132 | // this req can not be satisfied 133 | leftPendingReqs = append(leftPendingReqs, req) 134 | } 135 | } 136 | 137 | if len(satisfyingReqs) > 0 { 138 | // if some req is satisfied 139 | s.pendingRequests = leftPendingReqs 140 | for _, req := range satisfyingReqs { 141 | s.Call(req.avatarId, "DoEnterSpace", loadKind, loadSpaceID) 142 | } 143 | } 144 | } 145 | 146 | // RequestDestroy is RPC request for Spaces to request for destroying self 147 | func (s *SpaceService) RequestDestroy(kind int, spaceID common.EntityID) { 148 | if consts.DEBUG_SPACES { 149 | gwlog.Infof("Space %s kind %d is requesting destroy ...", spaceID, kind) 150 | } 151 | //spaceKindInfo := s.getSpaceKindInfo(kind) 152 | spaceInfo := s.getSpaceEntityInfo(kind, spaceID) 153 | 154 | if spaceInfo == nil { // You don't exists 155 | s.Call(spaceID, "ConfirmRequestDestroy", true) 156 | return 157 | } 158 | 159 | if time.Now().After(spaceInfo.LastEnterTime.Add(time.Second * 60)) { 160 | s.getSpaceKindInfo(kind).remove(spaceID) 161 | s.Call(spaceID, "ConfirmRequestDestroy", true) 162 | return 163 | } 164 | } 165 | --------------------------------------------------------------------------------