├── bin ├── DB │ └── .gitignore ├── SDB │ └── .gitignore ├── .gdbinit ├── .gitignore ├── debug ├── welcome.txt ├── makedistro.sh ├── readme.md ├── config.ini └── mongo.js ├── src ├── superchunk │ ├── .gitignore │ ├── super_test.go │ └── superchunk.go ├── cmd │ ├── shell │ │ ├── .gitignore │ │ ├── shell.go │ │ ├── connection.go │ │ └── ListenForMessages.go │ ├── database │ │ ├── .gitignore │ │ ├── chunkdata.go │ │ └── setup.go │ ├── performance │ │ ├── .gitignore │ │ └── perf.go │ ├── server │ │ ├── .gitignore │ │ ├── parse.go │ │ ├── server_test.go │ │ ├── worldcache.go │ │ ├── commtest.go │ │ ├── config.go │ │ ├── globalUser.go │ │ ├── procs.go │ │ ├── createchunk.go │ │ ├── player.go │ │ ├── pheenadv.go │ │ ├── combat.go │ │ ├── objects.go │ │ ├── specialcommands.go │ │ └── listener.go │ └── clientsimulator │ │ ├── .gitignore │ │ ├── main.go │ │ ├── SimulatePlayer.go │ │ └── ListenForMessages.go ├── twof │ └── twof.go ├── ephenationdb │ └── ephenationdb.go ├── keys │ └── keys.go ├── traffic │ └── traffic.go ├── chunkdb │ └── chunkdb.go ├── score │ ├── score_test.go │ └── score.go ├── timerstats │ └── timerstats.go ├── license │ └── license.go ├── DynamicBuffer │ └── DynamicBuffer.go ├── client_prot │ └── client_prot.go └── quadtree │ └── quadtree.go ├── .gitignore ├── AUTHORS.md ├── README.md └── dumpfile.sql /bin/DB/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /bin/SDB/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /bin/.gdbinit: -------------------------------------------------------------------------------- 1 | set history save 2 | -------------------------------------------------------------------------------- /src/superchunk/.gitignore: -------------------------------------------------------------------------------- 1 | SDB 2 | -------------------------------------------------------------------------------- /src/cmd/shell/.gitignore: -------------------------------------------------------------------------------- 1 | shell 2 | -------------------------------------------------------------------------------- /src/cmd/database/.gitignore: -------------------------------------------------------------------------------- 1 | database 2 | -------------------------------------------------------------------------------- /src/cmd/performance/.gitignore: -------------------------------------------------------------------------------- 1 | performance 2 | -------------------------------------------------------------------------------- /src/cmd/server/.gitignore: -------------------------------------------------------------------------------- 1 | server 2 | DB 3 | SDB 4 | -------------------------------------------------------------------------------- /src/cmd/clientsimulator/.gitignore: -------------------------------------------------------------------------------- 1 | clientsimulator 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | linux_386/ 2 | linux_amd64/ 3 | _obj/ 4 | *.6 5 | *.8 6 | *.orig 7 | *.e64 8 | *~ 9 | nohup.out 10 | -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | /clientsimulator 2 | /performance 3 | /server 4 | /shell 5 | /database 6 | /worldserver.log 7 | /database.log 8 | /.gdb_history 9 | -------------------------------------------------------------------------------- /bin/debug: -------------------------------------------------------------------------------- 1 | # 2 | # Start the server with appropriate flags for testing 3 | # 4 | go install -v ../src/cmd/server && ./server -v=2 -s -testuser -license.v=1 $* 5 | -------------------------------------------------------------------------------- /bin/welcome.txt: -------------------------------------------------------------------------------- 1 | !!Ephenation 2 | !Press ESC for help menu. 3 | ! 4 | !This message will be shown when a player logs in. 5 | !Using exclamation marks makes it go into a pop-up dialog. 6 | 7 | -------------------------------------------------------------------------------- /bin/makedistro.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cp ../dumpfile.sql . 3 | strip server shell clientsimulator 4 | tar cvfz distro-linux64-`date +%F`.gz server shell clientsimulator dumpfile.sql readme.md config.ini 5 | rm dumpfile.sql 6 | -------------------------------------------------------------------------------- /bin/readme.md: -------------------------------------------------------------------------------- 1 | Ephenation server 2 | =============== 3 | 4 | 1. Create two sub folders, DB and SDB 5 | 1. To be able to save characters, a MySQL database has to be setup 6 | 1. Update config.ini as needed 7 | 1. Stat server with ```./server -v=2 -s -testuser``` 8 | 1. Test connection with "./shell localhost" and command "/status" 9 | 10 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ###### Notice 2 | 3 | *This is the official list of **goconfig** authors for copyright purposes.* 4 | 5 | *This file is distinct from the CONTRIBUTORS file. See the latter for an 6 | explanation.* 7 | 8 | *Names should be added to this file as:* 9 | 10 | `Organization` or `Name ` 11 | 12 | *Please keep the list sorted.* 13 | 14 | * * * 15 | 16 | Lars Pensjö 17 | Mikael Grah 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ephenation-server 2 | ================= 3 | 4 | Ephenation is a MMORPG client-server game. 5 | The purpose of the game is to support adventures in an unlimited world and for players to create their your own adventures. 6 | The server is designed to handle large amount (~10000) of players. 7 | 8 | The server is designed to run on a Linux platform, and the client is designed to run on both Windows or Linux 9 | (based on OpenGL 3.3). 10 | 11 | See [Ephenation wiki](https://github.com/larspensjo/ephenation-server/wiki) for more documentation. 12 | There is a page about the game progress at [Google+](https://plus.google.com/u/0/b/116961322217479341351/116961322217479341351/posts) 13 | and a blog about some of the OpenGL experiences [Ephenation OpenGL](http://ephenationopengl.blogspot.se/). 14 | 15 | 16 | ![View](https://lh3.googleusercontent.com/-dGiXwRVTXgA/UGcwRDzfIrI/AAAAAAAAAUs/EHObZSWYJp4/w732-h296-n-k/View_2012-09-30.jpeg) 17 | -------------------------------------------------------------------------------- /bin/config.ini: -------------------------------------------------------------------------------- 1 | # 2 | # This is the configuration file for the Ephenation server 3 | # Some of the keys are loaded every time they are checked. 4 | # 5 | 6 | # Information on how to connect to the database. Leave '[db]' commented 7 | # to skip the timeout from opening the connection. 8 | # [db] 9 | DatabaseServer = localhost:47831 10 | DatabaseName = ephenation 11 | DatabaseLogin = ephenation 12 | DatabasePassword = dummy777 13 | 14 | [client] 15 | # This defines the major and minor version number of the current client version. 16 | # It can be updated live, without restarting the server. 17 | major=4 18 | minor=2 19 | 20 | [login] 21 | # Enable or disable login with test players. Default is false. 22 | testplayer = true 23 | 24 | # testip is a comma separated list of IP addresses from where login as --testuser is allowed. 25 | # Remove this to allow connections from anywhere. Automatic testing simulates from 127.0.0.1. 26 | testip = 127.0.0.1 27 | 28 | # Define a salt used for the md5 encryption of passwords 29 | salt = 30 | -------------------------------------------------------------------------------- /src/twof/twof.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package twof 19 | 20 | import ( 21 | "fmt" 22 | "math" 23 | ) 24 | 25 | type TwoF [2]float64 26 | 27 | func (tf *TwoF) String() string { 28 | return fmt.Sprintf("(%.2f,%.2f)", tf[0], tf[1]) 29 | } 30 | 31 | // Compute the distance from one coordinate to another 32 | func (p1 *TwoF) Dist(p2 *TwoF) float64 { 33 | dx, dy := p2[0]-p1[0], p2[1]-p1[1] 34 | return math.Sqrt(dx*dx + dy*dy) 35 | } 36 | -------------------------------------------------------------------------------- /src/cmd/database/chunkdata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "labix.org/v2/mgo" 22 | "log" 23 | ) 24 | 25 | func chunkdata(c *mgo.Collection) { 26 | var err error 27 | // Enable a compund key for the x, y and z coordinate. 28 | index := mgo.Index{ 29 | Key: []string{"x", "y", "z"}, // Set a compound index 30 | Unique: true, 31 | DropDups: true, 32 | } 33 | err = c.EnsureIndex(index) 34 | if err != nil { 35 | log.Println("chunkdb EnsureIndex compound", err) 36 | } 37 | 38 | // Define another key for the avatar ID 39 | err = c.EnsureIndexKey("avatarID") 40 | if err != nil { 41 | log.Println("chunkdb EnsureIndex avatarID", err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cmd/shell/shell.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "client_prot" 22 | "flag" 23 | "fmt" 24 | "os" 25 | ) 26 | 27 | var uFlag *string = flag.String("u", "test0", "Name prefix of players") 28 | var aFlag *string = flag.String("a", "127.0.0.1:57862", "The network address") 29 | var vFlag *int = flag.Int("v", 0, "Verbose") 30 | 31 | func main() { 32 | flag.Parse() 33 | user := *uFlag 34 | addr := *aFlag 35 | 36 | conn := connect(addr, user) 37 | go ListenForServerMessages(conn, user) 38 | 39 | b := make([]byte, 1000) 40 | for { 41 | n, err := os.Stdin.Read(b) 42 | if err != nil { 43 | fmt.Println(err) 44 | os.Exit(1) 45 | } 46 | if n == 1 { 47 | continue // Empoty line, only trailing LF 48 | } 49 | if b[0] == '/' { 50 | SendMsg(conn, []byte{byte(n + 2), 0, client_prot.CMD_DEBUG}) 51 | SendMsg(conn, b[:n-1]) // Skip the trailing newline 52 | continue 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/cmd/shell/connection.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "client_prot" 22 | "fmt" 23 | "net" 24 | "os" 25 | ) 26 | 27 | func SendMsg(conn net.Conn, b []byte) { 28 | if *vFlag > 1 { 29 | fmt.Printf("SendMsg: %v\n", b) 30 | } 31 | n, err := conn.Write(b) 32 | if err != nil { 33 | fmt.Printf("Failed to write %d bytes to connection: %s\n", len(b), err) 34 | os.Exit(1) 35 | } 36 | if n != len(b) { 37 | fmt.Printf("Could only write %d bytes out of %d\n", n, len(b)) 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | func connect(addr string, user string) net.Conn { 43 | conn, err := net.Dial("tcp", addr) 44 | if err != nil { 45 | fmt.Printf("Connection to %s failed: %v\n", addr, err) 46 | os.Exit(1) 47 | } 48 | cmd := string(len(user)+3) + "\000" 49 | login_cmd := []byte(cmd + string(client_prot.CMD_LOGIN) + user) 50 | // fmt.Printf("Login command: %v\n", login_cmd) 51 | SendMsg(conn, login_cmd) 52 | return conn 53 | } 54 | -------------------------------------------------------------------------------- /bin/mongo.js: -------------------------------------------------------------------------------- 1 | // 2 | // This script will prepare the ephenation database in MongoDB. It is destructive, 3 | // which means it will destroy any existing data! 4 | // 5 | // The following is not mandatory, just a recommendation. 6 | // 7 | // 1. Initialize the MongoDB. Proposal is as follows (from the 'mongo localhost' command): 8 | // use admin 9 | // db.addUser("admin", "somegoodpassword") 10 | // use ephenation 11 | // db.addUser("ephenation", "anothergoodpassword") 12 | // 13 | // 2. Restart the MongoDB server with 'mongod --auth --port port', which will enforce authentication and using port 'port'. 14 | // 15 | // 3. Now run this script with the command: 16 | // 17 | // mongo -u ephenation -p anothergoodpassword host:port/ephenation mongo.js 18 | // 19 | // 'host': The name of the host where the DB is, e.g. 'localhost' 20 | // 'port': The port number used. Can be removed if default. 21 | // 22 | 23 | // The counters collection of documents is used to povide unique IDs. 24 | db.counters.drop() 25 | db.counters.insert({_id: "avatarId", c: 1}) // A document to produce avatar IDs. 0 is reserved. 26 | db.counters.insert({_id: "newsId", c: 0}) // A document to produce news IDs 27 | 28 | // Avatars: _id is used for the numerical avatar Id. 29 | db.avatars.drop() 30 | db.avatars.ensureIndex({"name":1}, {unique:true}) // Avatar name must be unique 31 | db.avatars.ensureIndex({"email":1}, {unique:true}) // Only one avatar per owner 32 | db.avatars.ensureIndex({"level":1}, {unique:false}) // Used for sorting 33 | db.avatars.ensureIndex({"timeonline":1}, {unique:false}) // Used for sorting 34 | db.avatars.ensureIndex({"tscoretotal":1}, {unique:false}) // Used for sorting 35 | 36 | // News: _id is used for the numerical unique id. 37 | db.news.drop() 38 | -------------------------------------------------------------------------------- /src/ephenationdb/ephenationdb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package ephenationdb 19 | 20 | // 21 | // The purpose of this package is to provide Mongo db access. 22 | // This also provides opportunities to re-use connections, as creating new ones take time. 23 | // 24 | 25 | import ( 26 | // "log" 27 | // "fmt" 28 | "labix.org/v2/mgo" 29 | ) 30 | 31 | var ( 32 | session *mgo.Session 33 | ) 34 | 35 | // Initialize with connection data. 36 | // The argument is a function that shall provide necessary data for the connection. 37 | func SetConnection(f func(string) string) error { 38 | login := f("DatabaseLogin") 39 | pwd := f("DatabasePassword") 40 | server := f("DatabaseServer") 41 | database := f("DatabaseName") 42 | var err error 43 | session, err = mgo.Dial("mongodb://" + login + ":" + pwd + "@" + server + "/" + database) 44 | if err == nil { 45 | session.SetMode(mgo.Strong, true) 46 | } 47 | return err 48 | } 49 | 50 | // Get a connection to the default database 51 | func New() *mgo.Database { 52 | if session != nil { 53 | return session.DB("") 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /src/keys/keys.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package keys 19 | 20 | // This package manages keys. The keys are maintained in a key ring, which has a limited size. 21 | // If new keys are added, old are dropped. 22 | 23 | const ( 24 | keyRingMaxSize = 10 25 | ) 26 | 27 | type keyDefinition struct { 28 | Uid uint32 // The territory owner that created the key. 29 | Kid uint // The unique id of the key, for the given territory owner. 30 | Descr string // A one line description of the key. 31 | View uint // How it looks. This is used by the client to choose a display. 32 | } 33 | 34 | // The list of keys 35 | type KeyRing []*keyDefinition 36 | 37 | // Add a key to the key ring 38 | func (kr KeyRing) Add(key *keyDefinition) KeyRing { 39 | if kr.Test(key.Uid, key.Kid) { 40 | // Already have the key 41 | return kr 42 | } 43 | if len(kr) >= keyRingMaxSize { 44 | // The keyring is now too big. Throw away the oldest one. 45 | kr = kr[1:] 46 | } 47 | kr = append(kr, key) 48 | return kr 49 | } 50 | 51 | // Test if a key is in the key ring 52 | func (kr KeyRing) Test(Uid uint32, Kid uint) bool { 53 | for _, key := range kr { 54 | if key.Kid == Kid && key.Uid == Uid { 55 | return true 56 | } 57 | } 58 | return false 59 | } 60 | 61 | // Make a key 62 | func Make(uid uint32, kid uint, descr string, view uint) *keyDefinition { 63 | return &keyDefinition{uid, kid, descr, view} 64 | } 65 | -------------------------------------------------------------------------------- /src/traffic/traffic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package traffic 19 | 20 | import ( 21 | "fmt" 22 | "time" 23 | ) 24 | 25 | // 26 | // This package will simply keep track of amount of data received and sent, 27 | // as well as provide statistics. 28 | // 29 | 30 | func New() *stat { 31 | ret := new(stat) 32 | go ret.computeAverage() 33 | return ret 34 | } 35 | 36 | type stat struct { 37 | totalSent, totalReceived int64 38 | avgSent, avgRec float32 39 | } 40 | 41 | func (this *stat) AddSend(amount int) { 42 | this.totalSent += int64(amount) 43 | } 44 | 45 | func (this *stat) AddReceived(amount int) { 46 | this.totalReceived += int64(amount) 47 | } 48 | 49 | func (this *stat) String() string { 50 | return fmt.Sprintf("Received: %.2f MB (avg %d/s), Sent: %.2f MB (avg %d/s)", float64(this.totalReceived)/1e6, int(this.avgRec), float64(this.totalSent)/1e6, int(this.avgSent)) 51 | } 52 | 53 | func (this *stat) computeAverage() { 54 | const sleepTime = 3e10 55 | for { 56 | prevSent := this.totalSent 57 | prevRec := this.totalReceived 58 | time.Sleep(sleepTime) 59 | diffSent := this.totalSent - prevSent 60 | diffRec := this.totalReceived - prevRec 61 | decay := float32(0.1) // TODO: Should use a decay computed correctly from the sleepTime 62 | this.avgSent = this.avgSent*decay + float32(diffSent)/sleepTime*1e9*(1-decay) 63 | this.avgRec = this.avgRec*decay + float32(diffRec)/sleepTime*1e9*(1-decay) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/cmd/database/setup.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "ephenationdb" 22 | "flag" 23 | "github.com/larspensjo/config" 24 | "log" 25 | "os" 26 | ) 27 | 28 | var ( 29 | configFileName = flag.String("configfile", "config.ini", "General configuration file") 30 | logOnStdout = flag.Bool("s", false, "Send log file to standard otput") 31 | logFileName = flag.String("log", "database.log", "Log file name") 32 | ) 33 | 34 | func main() { 35 | if !*logOnStdout { 36 | logFile, _ := os.OpenFile(*logFileName, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) 37 | log.SetOutput(logFile) 38 | } 39 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) 40 | 41 | cnfg, err := config.ReadDefault(*configFileName) 42 | if err != nil { 43 | log.Println("Fail to find", *configFileName, err) 44 | return 45 | } 46 | configSection := "db" 47 | if cnfg.HasSection(configSection) { 48 | f := func(key string) string { 49 | value, err := cnfg.String(configSection, key) 50 | if err != nil { 51 | log.Println("Config file", *configFileName, "Failt to find key", key, err) 52 | return "" 53 | } 54 | return value 55 | } 56 | err = ephenationdb.SetConnection(f) 57 | if err != nil { 58 | log.Println("main: open DB:", err) 59 | return 60 | } 61 | } else { 62 | log.Println("Config file", configFileName, "missing, or no section 'db'") 63 | } 64 | db := ephenationdb.New() 65 | chunkdata(db.C("chunkdata")) 66 | } 67 | -------------------------------------------------------------------------------- /src/superchunk/super_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package superchunk 19 | 20 | import ( 21 | . "chunkdb" 22 | "os" 23 | "testing" 24 | // "time" 25 | ) 26 | 27 | const SCH_SUBFOLDER = "SDB" 28 | 29 | func TestSuperchunk(t *testing.T) { 30 | 31 | stat, err := os.Stat(SCH_SUBFOLDER) 32 | if err != nil || !stat.Mode().IsDir() { 33 | // The folder didn't exist, so create it. 34 | os.Mkdir(SCH_SUBFOLDER, os.ModePerm) 35 | } 36 | 37 | scm := New(SCH_SUBFOLDER) 38 | 39 | // Loading non existing chunk shall fail 40 | unused := CC{1, 1, 1} 41 | s := scm.load(&unused) 42 | if s != nil { 43 | t.Error("Unexpected finding super chunk at illegal address") 44 | } 45 | 46 | cc := CC{11, 12, 13} // Counting on this chunk not existing 47 | _, _, _, ok := scm.GetTeleport(&cc) 48 | if ok { 49 | t.Error("Load", cc, "should have failed") 50 | } 51 | 52 | scm.SetTeleport(&unused, 1, 2, 3) 53 | x, y, z, ok := scm.GetTeleport(&unused) 54 | if !ok { 55 | t.Error("Failed to read back", unused) 56 | } 57 | if x != 1 || y != 2 || z != 3 { 58 | t.Error("Got bad data back", x, y, z) 59 | } 60 | 61 | scm.RemoveTeleport(&unused) 62 | _, _, _, ok = scm.GetTeleport(&unused) 63 | if ok { 64 | t.Error("Still set after remove", unused) 65 | } 66 | 67 | ccBase := CC{trunc(unused.X), trunc(unused.Y), trunc(unused.Z)} 68 | fn := scm.functionName(&ccBase) 69 | err = os.Remove(fn) 70 | if err != nil { 71 | t.Error("Failed to remove", fn, err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/chunkdb/chunkdb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package chunkdb 19 | 20 | // 21 | // The purpose of this package is to manage access to the chunk list table ("chunkdata"). 22 | // This table contains information about what users own what chunks. 23 | // 24 | 25 | // A chunk coordinate, an address of any chunk in the world. This will limit the size of the world 26 | // to 0x100000000 * 32 = 1,37E11 blocks 27 | type CC struct { 28 | // Values are scaled by CHUNK_SIZE to get block coordinates 29 | X, Y, Z int32 // Relative world center 30 | } 31 | 32 | // Given only the LSB of the chunk coordinate, compute the full coordinate, relative to 33 | // a given coordinate. A requirement is that the distance from the relative chunk is small. 34 | func (this CC) UpdateLSB(x, y, z uint8) (ret CC) { 35 | ret = this 36 | ret.Z = (ret.Z & ^0xFF) | int32(z) // Replace LSB 37 | ret.X = (ret.X & ^0xFF) | int32(x) // Replace LSB 38 | ret.Y = (ret.Y & ^0xFF) | int32(y) // Replace LSB 39 | // Check for wrap around, which can happen near byte boundary 40 | if this.X-ret.X > 127 { 41 | ret.X += 0x100 42 | } 43 | if this.Y-ret.Y > 127 { 44 | ret.Y += 0x100 45 | } 46 | if this.Z-ret.Z > 127 { 47 | ret.Z += 0x100 48 | } 49 | if ret.X-this.X > 127 { 50 | ret.X -= 0x100 51 | } 52 | if ret.Y-this.Y > 127 { 53 | ret.Y -= 0x100 54 | } 55 | if ret.Z-this.Z > 127 { 56 | ret.Z -= 0x100 57 | } 58 | return 59 | } 60 | 61 | // Compare two chunk coordinates for equality 62 | func (chunk CC) Equal(chunk2 CC) bool { 63 | return chunk.X == chunk2.X && chunk.Y == chunk2.Y && chunk.Z == chunk2.Z 64 | } 65 | -------------------------------------------------------------------------------- /src/cmd/clientsimulator/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "client_prot" 22 | "flag" 23 | "fmt" 24 | "math" 25 | "math/rand" 26 | "net" 27 | "sync" 28 | "time" 29 | ) 30 | 31 | var nFlag *int = flag.Int("n", 1, "Number of instances") 32 | var uFlag *string = flag.String("u", "test", "Name prefix of players") 33 | var aFlag *string = flag.String("a", "127.0.0.1", "The network address") 34 | var vFlag *int = flag.Int("v", 0, "Verbose") 35 | var pFlag *string = flag.String("p", "57862", "The network port") 36 | var oFlag *int = flag.Int("o", 1, "Offset number on player name") 37 | 38 | // This lock will allow only one login at a time. 39 | var waitForAck sync.Mutex 40 | 41 | func main() { 42 | flag.Parse() 43 | var i int 44 | user := *uFlag 45 | num_instances := *nFlag 46 | ipaddr := *aFlag 47 | port := *pFlag 48 | addr := ipaddr + ":" + port 49 | 50 | rand.Seed(int64(time.Now().Nanosecond())) 51 | 52 | for i = 0; i < num_instances; i++ { 53 | go spawn_simulator(addr, user+fmt.Sprint(i+*oFlag)) 54 | } 55 | 56 | fmt.Printf("Started %d instances\n", num_instances) 57 | 58 | time.Sleep(math.MaxInt64 / 2) // More or less for ever 59 | } 60 | 61 | func spawn_simulator(addr string, user string) { 62 | conn, err := net.Dial("tcp", addr) 63 | if err != nil { 64 | fmt.Printf("Connection to %s failed: %v\n", addr, err) 65 | return 66 | } 67 | cmd := string(len(user)+3) + "\000" 68 | login_cmd := []byte(cmd + string(client_prot.CMD_LOGIN) + user) 69 | waitForAck.Lock() // Will be unlocked by the login acknowledge 70 | if *vFlag > 1 { 71 | fmt.Printf("Login %v. ", user) 72 | } 73 | SendMsg(conn, login_cmd) 74 | CentralControl(conn, user) 75 | } 76 | -------------------------------------------------------------------------------- /src/score/score_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package score 19 | 20 | import ( 21 | "testing" 22 | "time" 23 | ) 24 | 25 | const ( 26 | uid = 0 27 | initScore = 1 // Seed 28 | initScoreBalance = 2 // Seed 29 | ) 30 | 31 | func TestScoreInitial(t *testing.T) { 32 | // Close the process, as we do not want to update the SQL DB. 33 | if !Close() { 34 | t.FailNow() 35 | } 36 | sc, scb := Get(uid) 37 | if sc != 0 || scb != ConfigScoreBalanceZero { 38 | t.Errorf("Bad initial values score %v and scoreBalance %v\n", sc, scb) 39 | } 40 | 41 | now := time.Now() 42 | Initialize(uid, initScore, initScoreBalance, uint32(now.Unix()), "test", 1) 43 | sc, scb = Get(uid) 44 | // Some decay can be expected if time enough has passed. Score (sc) should decay downwards, 45 | // and ScoreBalance (scb) upwards. 46 | if !almost(sc, initScore) || !almost(initScoreBalance, scb) { 47 | t.Errorf("Failed to initialize correctly (score %v, scoreBalance %v)\n", sc, scb) 48 | } 49 | 50 | ts := getTerritoryScore(uid) 51 | delay := now.Add(time.Millisecond * 10) 52 | ts.decay(&delay) 53 | sc, scb = Get(uid) 54 | if !almost(sc, initScore) || !almost(initScoreBalance, scb) { 55 | t.Errorf("Failed to initialize correctly (score %v, scoreBalance %v)\n", sc, scb) 56 | } 57 | 58 | Add(uid, 1) 59 | sc, scb = Get(uid) 60 | if !almost(sc, initScore+1) || !almost(initScoreBalance+1, scb) { 61 | t.Errorf("Incorrect add, leading to %v, %v\n", sc, scb) 62 | t.FailNow() 63 | } 64 | 65 | // There is enough points for one payment 66 | if !Pay(uid, initScoreBalance+1) { 67 | t.Error("First pay failed") 68 | t.FailNow() 69 | } 70 | 71 | // But not for two 72 | sc, scb = Get(uid) 73 | if Pay(uid, 1) { 74 | t.Errorf("Second pay succeeded, was %v\n", scb) 75 | } 76 | } 77 | 78 | // Return true if 'a' is within 1% below 'b'. 79 | func almost(a, b float64) bool { 80 | diff := (b - a) / b 81 | return diff >= 0 && diff <= 0.1 82 | } 83 | -------------------------------------------------------------------------------- /src/cmd/server/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | // 21 | // Collection for functions that pack and unpack data from byte vectors. 22 | // 23 | 24 | import ( 25 | // "fmt" 26 | ) 27 | 28 | func ParseUint64(b []byte) (uint64, []byte, bool) { 29 | if len(b) < 8 { 30 | return 0, nil, false 31 | } 32 | var res uint64 33 | for i := 0; i < 8; i++ { 34 | res |= uint64(b[i]) << (uint(i) * 8) 35 | } 36 | return res, b[8:], true 37 | } 38 | 39 | func ParseInt32(b []byte) (int32, []byte, bool) { 40 | if len(b) < 4 { 41 | return 0, nil, false 42 | } 43 | var res uint32 44 | for i := 0; i < 4; i++ { 45 | res |= uint32(b[i]) << (uint(i) * 8) 46 | } 47 | return int32(res), b[4:], true 48 | } 49 | 50 | func ParseUint32(b []byte) (uint32, []byte, bool) { 51 | if len(b) < 4 { 52 | return 0, nil, false 53 | } 54 | var res uint32 55 | for i := 0; i < 4; i++ { 56 | res |= uint32(b[i]) << (uint(i) * 8) 57 | } 58 | return res, b[4:], true 59 | } 60 | 61 | func ParseUint16(b []byte) (uint16, []byte, bool) { 62 | if len(b) < 2 { 63 | return 0, nil, false 64 | } 65 | var res uint16 66 | for i := 0; i < 2; i++ { 67 | res |= uint16(b[i]) << (uint(i) * 8) 68 | } 69 | return res, b[2:], true 70 | } 71 | 72 | func EncodeUint16(n uint16, b []byte) { 73 | if cap(b) < 2 { 74 | panic("EncodeUint16 too small buffer") 75 | } 76 | b[0] = byte(n & 0xFF) 77 | b[1] = byte((n >> 8) & 0xFF) 78 | } 79 | 80 | func EncodeUint32(n uint32, b []byte) { 81 | if cap(b) < 4 { 82 | panic("EncodeUint32 too small buffer") 83 | } 84 | b[0] = byte(n & 0xFF) 85 | b[1] = byte((n >> 8) & 0xFF) 86 | b[2] = byte((n >> 16) & 0xFF) 87 | b[3] = byte((n >> 24) & 0xFF) 88 | } 89 | 90 | func EncodeUint64(n uint64, b []byte) { 91 | if cap(b) < 8 { 92 | panic("EncodeUint32 too small buffer") 93 | } 94 | b[0] = byte(n & 0xFF) 95 | b[1] = byte((n >> 8) & 0xFF) 96 | b[2] = byte((n >> 16) & 0xFF) 97 | b[3] = byte((n >> 24) & 0xFF) 98 | b[4] = byte((n >> 32) & 0xFF) 99 | b[5] = byte((n >> 40) & 0xFF) 100 | b[6] = byte((n >> 48) & 0xFF) 101 | b[7] = byte((n >> 56) & 0xFF) 102 | } 103 | -------------------------------------------------------------------------------- /src/timerstats/timerstats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package timerstats 19 | 20 | // 21 | // Keep tab of the list of timers, to make it easy to query for statistics. This will give a snapshot of 22 | // the last timer, not an average. 23 | // 24 | 25 | import ( 26 | "fmt" 27 | "io" 28 | "sort" 29 | "sync" 30 | "time" 31 | ) 32 | 33 | type TimerStatistic struct { 34 | timer *time.Duration 35 | sleepPeriod time.Duration 36 | descr string 37 | } 38 | 39 | type collection []TimerStatistic 40 | 41 | var timerStatSync sync.Mutex 42 | 43 | var allTimerStatistics collection 44 | 45 | func (ts TimerStatistic) fraction() float64 { 46 | if *ts.timer == 0 { 47 | // There was no reported timer value yet 48 | return 0 49 | } 50 | return float64(*ts.timer-ts.sleepPeriod) / float64(ts.sleepPeriod) 51 | } 52 | 53 | func Report(wr io.Writer) { 54 | timerStatSync.Lock() 55 | // fmt.Printf("Before sort: %#v\n", allTimerStatistics) 56 | sort.Sort(&allTimerStatistics) 57 | // fmt.Printf("After sort: %#v\n", allTimerStatistics) 58 | for _, stat := range allTimerStatistics { 59 | fmt.Fprintf(wr, "%8.4f (%5.2f per cent) %s\n", float64(*stat.timer)/1e9, stat.fraction()*100, stat.descr) 60 | } 61 | timerStatSync.Unlock() 62 | return 63 | } 64 | 65 | // Add a procedure. 66 | // descr: A description to be used in the report 67 | // sleepPeriod: The expected sleeping period of the procedure 68 | // timer: Pointer to actual sleeping period, managed by the caller. 69 | func Add(descr string, sleepPeriod time.Duration, timer *time.Duration) { 70 | timerStatSync.Lock() 71 | stat := TimerStatistic{timer, sleepPeriod, descr} 72 | allTimerStatistics = append(allTimerStatistics, stat) 73 | timerStatSync.Unlock() 74 | // fmt.Printf("AddTimerStatistic: %v\n", allTimerStatistics) 75 | } 76 | 77 | // Provide functions needed to fulfil the sorting interface 78 | func (timers *collection) Len() int { 79 | return len(*timers) 80 | } 81 | 82 | func (timers *collection) Less(i, j int) bool { 83 | return (*timers)[i].fraction() < (*timers)[j].fraction() 84 | } 85 | 86 | func (timers *collection) Swap(i, j int) { 87 | slice := *timers 88 | tmr := slice[i] 89 | slice[i] = slice[j] 90 | slice[j] = tmr 91 | } 92 | -------------------------------------------------------------------------------- /src/license/license.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package license 19 | 20 | import ( 21 | "crypto/md5" 22 | "crypto/rand" 23 | "encoding/hex" 24 | "flag" 25 | "io" 26 | ) 27 | 28 | var verboseFlag = flag.Int("license.v", 0, "Debug license management, Higher number gives more") 29 | 30 | const ( 31 | keyLength = 20 // Use 20 character for a license key 32 | UPPER_TIME_LIMIT = 0 // A load/save operation longer than this (in ns) will generate a log message 33 | ) 34 | 35 | // All this data is saved with the license (user DB). All names beginning with upper case will be saved. 36 | type License struct { 37 | Email string 38 | Password string // The password of the license 39 | License string // The license key for this person 40 | } 41 | 42 | // Compare the given password with the stored one. The stored password 43 | // is scrambled using md5, and so is never available as readable text. 44 | func VerifyPassword(passwclear, passwordcrypted string, salt string) bool { 45 | // fmt.Printf("license.VerifyPassword: '%s' - '%s'\n", hex.EncodeToString(hash.Sum()), lp.Password) 46 | if EncryptPassword(passwclear, salt) == passwordcrypted { 47 | return true 48 | } 49 | return false 50 | } 51 | 52 | // Update the password with a new one. The stored password 53 | // is scrambled using md5, and so is never available as readable text. 54 | func EncryptPassword(passw, salt string) string { 55 | hash := md5.New() 56 | _, err := io.WriteString(hash, passw) 57 | if err != nil { 58 | panic("license.NewPassword write to hash") 59 | } 60 | return hex.EncodeToString(hash.Sum(nil)) 61 | } 62 | 63 | func GenerateKey() string { 64 | // This list of characters is not very important. Some were excluded as it can be hard to read the difference between '1' and 'I', etc. 65 | const keyCharacters = "ABCDEFGHIJKLMNPQRSTUVXYZ23456789abcdefghijklmnpqrstuvxyz" 66 | var str [keyLength]byte 67 | var b [4]byte 68 | for i := 0; i < keyLength; i++ { 69 | var rnd uint 70 | rand.Read(b[:]) 71 | for j := 0; j < 4; j++ { 72 | rnd = (rnd << 8) + uint(b[j]) 73 | } 74 | str[i] = keyCharacters[rnd%uint(len(keyCharacters))] 75 | } 76 | return string(str[:]) 77 | } 78 | 79 | // Use a license key and a password 80 | func Make(password, salt string) (string, string) { 81 | licence := GenerateKey() 82 | EncryptPassword := EncryptPassword(password, salt) 83 | return licence, EncryptPassword 84 | } 85 | -------------------------------------------------------------------------------- /src/cmd/server/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "io/ioutil" 22 | "log" 23 | "math/rand" 24 | "os" 25 | "testing" 26 | ) 27 | 28 | func TestInventory(t *testing.T) { 29 | var up user 30 | up.conn = MakeDummyConn() 31 | inv := &up.pl.Inventory 32 | if inv.Len() != 0 { 33 | t.Error("Not empty", inv) 34 | } 35 | 36 | inv.Add(MakeHealthPotion(0)) 37 | if inv.Len() != 1 { 38 | t.Error("Failed to add", inv) 39 | } 40 | 41 | inv.Add(MakeHealthPotion(0)) 42 | if inv.Len() != 1 { 43 | t.Error("List should still have length 1", inv) 44 | } 45 | 46 | // Add another type of potion, which shall not increase the same counter 47 | inv.Add(MakeManaPotion(0)) 48 | if inv.Len() != 2 { 49 | t.Error("Shall be two entries", inv) 50 | } 51 | 52 | // Add mana potion again 53 | inv.Add(MakeManaPotion(0)) 54 | if inv.Len() != 2 { 55 | t.Error("Shall still be two entries", inv) 56 | } 57 | 58 | if up.pl.HitPoints != 0 || up.pl.Mana != 0 { 59 | t.Error("Initial condition for hp and mana wrong") 60 | t.FailNow() 61 | } 62 | 63 | inv.Use(ItemHealthPotionID, 0, &up)() 64 | if up.pl.HitPoints == 0 { 65 | t.Error("Failed to use healing pot", inv) 66 | } 67 | if inv.Len() != 2 { 68 | t.Error("Shall still be two entries", inv) 69 | } 70 | 71 | inv.Use(ItemHealthPotionID, 0, &up)() 72 | if inv.Len() != 1 { 73 | t.Error("Only mana potions", inv) 74 | } 75 | 76 | inv.Use(ItemManaPotionID, 0, &up)() 77 | if up.pl.Mana == 0 { 78 | t.Error("Failed to use mana potion") 79 | } 80 | if inv.Len() != 1 { 81 | t.Error("Only mana potions", inv) 82 | } 83 | 84 | inv.Use(ItemManaPotionID, 0, &up)() 85 | if inv.Len() != 0 { 86 | t.Error("Shall be empty inventory", inv) 87 | } 88 | } 89 | 90 | func BenchmarkFilesystem(b *testing.B) { 91 | b.StopTimer() 92 | dirs, err := ioutil.ReadDir("DB") 93 | if err != nil { 94 | b.Error(err) 95 | b.FailNow() 96 | return 97 | } 98 | num := float64(len(dirs)) 99 | log.Println("Number of entries:", num) 100 | b.StartTimer() 101 | for i := 0; i < b.N; i++ { 102 | n := int(rand.Float64() * num) 103 | name := CnfgChunkFolder + "/" + dirs[n].Name() 104 | f, err := os.Open(name) 105 | if err != nil { 106 | b.Error(name, err) 107 | b.FailNow() 108 | return 109 | } 110 | f.Close() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/cmd/clientsimulator/SimulatePlayer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "client_prot" 22 | "fmt" 23 | "math" 24 | "math/rand" 25 | "net" 26 | "os" 27 | "time" 28 | ) 29 | 30 | // 31 | // Simulate a player, setting up a process that will generate 32 | // commands now and then. 33 | func SimulatePlayer(ch chan msg_command, conn net.Conn) { 34 | moving := false 35 | request_coord(conn) 36 | for i := 0; ; i++ { 37 | time.Sleep(100000000) // Delay 100 ms 38 | rnd := rand.Uint32() 39 | switch rnd % 10 { 40 | case 0: 41 | request_chunk(conn) // Request a chunk 42 | case 1: 43 | moving = !moving 44 | if moving { 45 | SendMsg(conn, []byte{3, 0, client_prot.CMD_START_FWD}) 46 | } else { 47 | SendMsg(conn, []byte{3, 0, client_prot.CMD_STOP_FWD}) 48 | } 49 | case 2: 50 | dir := uint16(float32(rand.Uint32()%360) / 360 * 2 * math.Pi * 1000) 51 | h0 := uint8(dir & 0xff) 52 | h1 := uint8(dir >> 8) 53 | SendMsg(conn, []byte{7, 0, client_prot.CMD_SET_DIR, h0, h1, 0, 0}) 54 | } 55 | } 56 | } 57 | 58 | // Send a command to request the coordinates 59 | func request_coord(conn net.Conn) { 60 | const length = 3 61 | var ans [length]byte 62 | ans[0] = byte(length & 0xFF) 63 | ans[1] = byte(length >> 8) 64 | ans[2] = client_prot.CMD_GET_COORDINATE 65 | SendMsg(conn, ans[:]) 66 | } 67 | 68 | func request_chunk(conn net.Conn) { 69 | const length = 3 + 3*4 70 | var ans [length]byte 71 | ans[0] = byte(length & 0xFF) 72 | ans[1] = byte(length >> 8) 73 | ans[2] = client_prot.CMD_READ_CHUNK 74 | x := uint32(xpos / 3200) 75 | for i := uint(0); i < 4; i++ { 76 | ans[i+3] = byte((x >> (i * 8)) & 0xFF) 77 | } 78 | y := uint32(ypos / 3200) 79 | for i := uint(0); i < 4; i++ { 80 | ans[i+3+4] = byte((y >> (i * 8)) & 0xFF) 81 | } 82 | z := uint32(zpos / 3200) 83 | for i := uint(0); i < 4; i++ { 84 | ans[i+3+8] = byte((z >> (i * 8)) & 0xFF) 85 | } 86 | SendMsg(conn, ans[:]) 87 | } 88 | 89 | func SendMsg(conn net.Conn, b []byte) { 90 | if *vFlag > 1 { 91 | fmt.Printf("SendMsg: %v\n", b) 92 | } 93 | n, err := conn.Write(b) 94 | if err != nil { 95 | fmt.Printf("Failed to write %d bytes to connection: %s\n", len(b), err) 96 | os.Exit(1) 97 | } 98 | if n != len(b) { 99 | fmt.Printf("Could only write %d bytes out of %d\n", n, len(b)) 100 | os.Exit(1) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/cmd/shell/ListenForMessages.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "client_prot" 22 | "fmt" 23 | "net" 24 | "os" 25 | ) 26 | 27 | func ListenForServerMessages(conn net.Conn, user string) { 28 | var buff [10000]byte 29 | for { 30 | n, err := conn.Read(buff[0:2]) // Only the length 31 | if err != nil || n == 0 { 32 | if e2, ok := err.(*net.OpError); ok && e2.Temporary() { 33 | fmt.Println("ListenForServerMessages: temporary") 34 | continue 35 | } 36 | if e2, ok := err.(*net.OpError); ok && e2.Timeout() { 37 | fmt.Println("ListenForServerMessages: Timeout") 38 | continue 39 | } 40 | fmt.Printf("ListenForServerMessages: Failed to read: %v\n", err) 41 | os.Exit(1) // Major failure 42 | } 43 | if n == 1 { 44 | fmt.Printf("ListenForServerMessages: Got %d bytes reading from socket\n", n) 45 | n2, err := conn.Read(buff[1:2]) // Second byte of the length 46 | if err != nil || n2 != 1 { 47 | fmt.Printf("ListenForServerMessages: Failed to read: %v\n", err) 48 | os.Exit(1) // Major failure 49 | } 50 | n = 2 51 | } 52 | length := int(uint(buff[1])<<8 + uint(buff[0])) 53 | if length < 3 || length > cap(buff) { 54 | fmt.Printf("ListenForServerMessages: User %v Expecting %d bytes, which is bad\n", user, length) 55 | os.Exit(1) 56 | } 57 | n, err = conn.Read(buff[2:length]) // Read the rest.readin from socket 58 | if err != nil { 59 | fmt.Printf("ListenForServerMessages: Failed to read: %v\n", err) 60 | os.Exit(1) // Major failure 61 | } 62 | for tot := n + 2; tot < length; tot += n { 63 | n, err = conn.Read(buff[tot:length]) // Read the rest. 64 | if err != nil { 65 | fmt.Printf("ListenForServerMessages: Failed to read: %v\n", err) 66 | os.Exit(1) // Major failure 67 | } 68 | // fmt.Printf("Got trailing %d+%d bytes of %d\n", tot, n, length) 69 | } 70 | if *vFlag > 1 { 71 | fmt.Printf("ListenForServerMessages user %v Receive %v... (length %d)\n", user, buff[0:3], length) 72 | } 73 | // fmt.Printf("ListenForServerMessages (l %d) cmd %v\n", length, buff[:n]) 74 | switch buff[2] { 75 | case client_prot.CMD_OBJECT_LIST: 76 | if *vFlag >= 2 { 77 | fmt.Println("CMD_OBJECT_LIST") 78 | } 79 | case client_prot.CMD_MESSAGE: 80 | fmt.Printf("CMD_MESSAGE: %s\n", string(buff[3:length])) 81 | case client_prot.CMD_REPORT_COORDINATE: 82 | fmt.Println("CMD_REPORT_COORDINATE") 83 | fmt.Printf("ListenForServerMessages (l %d) cmd %v\n", length, buff[:n]) 84 | case client_prot.CMD_CHUNK_ANSWER: 85 | fmt.Printf("CMD_CHUNK_ANSWER: Got chunk buffwer length %d: %v\n", length, buff[0:length]) 86 | case client_prot.CMD_LOGIN_ACK: 87 | fmt.Println("User", user, "login ack") 88 | case client_prot.CMD_BLOCK_UPDATE: 89 | fmt.Println("CMD_BLOCK_UPDATE") 90 | case client_prot.CMD_REQ_PASSWORD: 91 | fmt.Println("CMD_REQ_PASSWORD") 92 | case client_prot.CMD_PROT_VERSION: 93 | fmt.Printf("Protocol version %d.%d\n", buff[5]+buff[6]<<8, buff[3]+buff[4]<<8) 94 | case client_prot.CMD_EQUIPMENT: // Ignore 95 | case client_prot.CMD_PLAYER_STATS: // Ignore 96 | case client_prot.CMD_RESP_AGGRO_FROM_MONSTER: 97 | default: 98 | fmt.Printf("Unknown command %v\n", buff[0:length]) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/DynamicBuffer/DynamicBuffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | // 19 | // A buffer with an initial capacity. Support incremental addition of data 20 | // and extend the capacity when needed. Optionally, the data can be compressed 21 | // 22 | package DynamicBuffer 23 | 24 | import ( 25 | "log" 26 | ) 27 | 28 | // This is a private structure, the factory function is needed to get a buffer 29 | // TODO: It is possible to use two different buffers depending using compression or not, 30 | // which would speed up when there is no need to have conditinal dependency om flags. 31 | // TODO: It may be more effective to re-use the same buffer every time, and return a copy 32 | // with the exact size. However, that would not be safe from multi processes. 33 | type buffer struct { 34 | buff []byte 35 | size int 36 | comp bool // True if the data is compressed 37 | } 38 | 39 | func MakeBuffer(capacity int) *buffer { 40 | return &buffer{make([]byte, capacity), 0, false} 41 | } 42 | 43 | func MakeCompressedBuffer(capacity int) *buffer { 44 | buff := buffer{make([]byte, capacity), 0, true} 45 | return &buff 46 | } 47 | 48 | func (b *buffer) Add(d byte) { 49 | if b.comp { 50 | if b.size == 0 { 51 | // First data. 52 | b.add(d) 53 | b.add(1) 54 | } else if b.buff[b.size-2] == d && b.buff[b.size-1] != 255 { 55 | // The same, simply increase the count 56 | b.buff[b.size-1]++ 57 | } else { 58 | // Either a new value, or the value did not fit (max count 255) 59 | b.add(d) 60 | b.add(1) 61 | } 62 | } else { 63 | // No compression, simply add the byte 64 | b.add(d) 65 | } 66 | } 67 | 68 | func (b *buffer) add(d byte) { 69 | if b.size == cap(b.buff) { 70 | // fmt.Printf("DynamicBuffer.Add: Resize from %d to %d\n", b.size, b.size*2) 71 | b2 := make([]byte, b.size*2) 72 | copy(b2, b.buff) 73 | b.buff = b2 74 | } 75 | b.buff[b.size] = d 76 | b.size++ 77 | } 78 | 79 | // Return the data as a slice 80 | func (b *buffer) Bytes() []byte { 81 | return b.buff[0:b.size] 82 | } 83 | 84 | // Compression is done according to PROT-001. 85 | 86 | // Need a helper struct to unpack compressed data 87 | type unpackBuffer struct { 88 | buff []byte // This is the buffer we will unpack 89 | index int // How far we have come in the unpacking 90 | count byte // How many that have been unpacked from that position 91 | } 92 | 93 | // Factory that creates the data needed for the state when doing uncompress 94 | func MakeUncompressBuffer(buff []byte) *unpackBuffer { 95 | b := unpackBuffer{buff, 0, 0} 96 | return &b 97 | } 98 | 99 | // Return true if all bytes have been read from this compressed buffer 100 | func (b *unpackBuffer) IsAtEOF() bool { 101 | return b.buff[b.index+1] == b.count && b.index+2 == len(b.buff) 102 | } 103 | 104 | // Get next unpacked byte, return a flag marking success or failure 105 | func (b *unpackBuffer) GetOne() (byte, bool) { 106 | if b.index+1 >= len(b.buff) { 107 | log.Printf("DynamicBuffer::GetOne buffer length %d, cap %d (%#v)\n", len(b.buff), cap(b.buff), *b) 108 | panic("") 109 | return 0, false 110 | } 111 | // If the last byte has been taken from the latest pair, go on to next pair 112 | if b.buff[b.index+1] == b.count { 113 | b.count = 0 114 | b.index += 2 115 | } 116 | b.count++ 117 | return b.buff[b.index], true 118 | } 119 | -------------------------------------------------------------------------------- /src/cmd/server/worldcache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | // 21 | // All chunks loaded in memory are managed by a hash table to make them quick and 22 | // easy to find. 23 | // TODO: A mechanism is needed to throw away chunks when there are too many 24 | // 25 | 26 | import ( 27 | // "fmt" 28 | "chunkdb" 29 | sync "github.com/larspensjo/Go-sync-evaluation/evalsync" 30 | ) 31 | 32 | const ( 33 | WORLD_CACHE_SIZE = 32767 // An even number, like 1000, doesn't work good with the random number 34 | ) 35 | 36 | var ( 37 | world_cache [WORLD_CACHE_SIZE]*chunk 38 | worldCacheLock sync.RWMutex 39 | cache_average_search_depth float32 // Keep track of how much searching is needed 40 | worldCacheNumChunks int 41 | ) 42 | 43 | const cache_depth_decay = 0.99 44 | 45 | func chunkFindHash(coord chunkdb.CC) uint { 46 | hash := uint(coord.X)*871 + uint(coord.Y)*988261 + uint(coord.Z)*79261 47 | return hash % WORLD_CACHE_SIZE 48 | } 49 | 50 | // Find the chunk, if it exists. Otherwise, return nil. There is already a lock in place for the cache. 51 | func chunkFindRO(coord chunkdb.CC) *chunk { 52 | ind := chunkFindHash(coord) 53 | var pc *chunk 54 | var n int 55 | for pc = world_cache[ind]; pc != nil; pc = pc.next { 56 | n++ // Number of searches 57 | if pc.Coord.X == coord.X && pc.Coord.Y == coord.Y && pc.Coord.Z == coord.Z { 58 | pc.touched = true 59 | break 60 | } 61 | } 62 | cache_average_search_depth = cache_depth_decay*cache_average_search_depth + (1-cache_depth_decay)*float32(n) 63 | return pc 64 | } 65 | 66 | // Find the chunk. If it doesn't exist, create it. 67 | // This is a speed critical function. 68 | func ChunkFind_WLwWLc(coord chunkdb.CC) *chunk { 69 | // Need a read lock on the cache, no changes will be done (yet). Assume the chunk is found, which is the normal case. 70 | worldCacheLock.RLock() 71 | pc := chunkFindRO(coord) 72 | worldCacheLock.RUnlock() 73 | if pc != nil { 74 | if pc.jellyBlocks != nil { 75 | // There may be jelly blocks that should be restored 76 | pc.Lock() 77 | // It may have changed between the test and the lock, but minimal chance 78 | pc.RestoreJellyBlocks(false) 79 | pc.Unlock() 80 | } 81 | return pc 82 | } 83 | 84 | // Now a write lock is needed to save the new chunk in the cache. 85 | worldCacheLock.Lock() 86 | // Meanwhile, before the lock was created, the chunk may have been created by another process. 87 | // It may look wasteful to call chunkFindRO again, but it only happens for new chunks. 88 | pc = chunkFindRO(coord) 89 | if pc != nil { 90 | worldCacheLock.Unlock() 91 | return pc 92 | } 93 | // Didn't find the chunk (again), get it from disk or create one. 94 | pc = dBFindChunkFromFS(coord) 95 | 96 | AddChunkToHashTable(pc) 97 | worldCacheLock.Unlock() 98 | 99 | return pc 100 | } 101 | 102 | func AddChunkToHashTable(pc *chunk) { 103 | ind := chunkFindHash(pc.Coord) 104 | pc.next = world_cache[ind] 105 | world_cache[ind] = pc // Insert the new pointer first 106 | worldCacheNumChunks++ 107 | } 108 | 109 | func RemoveChunkFromHashTable(pc *chunk) { 110 | ind := chunkFindHash(pc.Coord) 111 | for ppc := &world_cache[ind]; *ppc != nil; ppc = &(*ppc).next { 112 | if *ppc == pc { 113 | *ppc = pc.next 114 | pc.next = nil 115 | worldCacheNumChunks-- 116 | return 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/cmd/performance/perf.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "runtime" 23 | "sync" 24 | "sync/atomic" 25 | "time" 26 | ) 27 | 28 | // This command is used for testing performance of various constructs. 29 | 30 | func main() { 31 | // Empty loop 32 | const EL = 1e9 33 | start := time.Now() 34 | for i := int64(0); i < EL; i++ { 35 | } 36 | elapsed := time.Now().Sub(start) 37 | if elapsed < 1e8 { 38 | fmt.Println("Too quick for empty loop") 39 | } 40 | fmt.Printf("Empty loop: %.3f ns\n", float32(elapsed)/EL) 41 | 42 | // Read unit32 43 | const RUINT = 1e9 44 | var v uint32 45 | var x uint32 46 | start = time.Now() 47 | for i := int64(0); i < RUINT; i++ { 48 | x = v 49 | } 50 | elapsed = time.Now().Sub(start) 51 | if elapsed < 1e8 { 52 | fmt.Println("Too quick for read unit32") 53 | } 54 | fmt.Printf("Read uint32: %.3f ns\n", float32(elapsed)/RUINT) 55 | 56 | // Measure time using mutex 57 | const ML = 1e7 58 | var s sync.RWMutex 59 | start = time.Now() 60 | for i := int64(0); i < ML; i++ { 61 | s.Lock() 62 | s.Unlock() 63 | } 64 | elapsed = time.Now().Sub(start) 65 | if elapsed < 1e8 { 66 | fmt.Println("Too quick mutex lock/unlock") 67 | } 68 | fmt.Printf("Mutex lock/unlock: %.3f ns\n", float32(elapsed)/ML) 69 | 70 | // Measure time using read lock mutex 71 | const RL = 1e7 72 | start = time.Now() 73 | for i := int64(0); i < RL; i++ { 74 | s.RLock() 75 | s.RUnlock() 76 | } 77 | elapsed = time.Now().Sub(start) 78 | if elapsed < 1e8 { 79 | fmt.Println("Too quick for mutex rlock/runlock") 80 | } 81 | fmt.Printf("Mutex RLock/RUnlock: %.3f ns\n", float32(elapsed)/RL) 82 | 83 | // Measure time using LoadUint32 84 | const LUINT = 1e8 85 | start = time.Now() 86 | for i := int64(0); i < LUINT; i++ { 87 | x = atomic.LoadUint32(&v) 88 | } 89 | x = x + 0 // Have to use 'x'. 90 | elapsed = time.Now().Sub(start) 91 | if elapsed < 1e8 { 92 | fmt.Println("Too quick for atomc.LoadUint32") 93 | } 94 | fmt.Printf("atomc.LoadUint32: %.3f ns\n", float32(elapsed)/LUINT) 95 | 96 | // Measure time using channel 97 | const CT = 1e7 98 | ch := make(chan int, 1) 99 | start = time.Now() 100 | for i := int64(0); i < CT; i++ { 101 | ch <- 0 102 | <-ch 103 | } 104 | elapsed = time.Now().Sub(start) 105 | if elapsed < 1e8 { 106 | fmt.Println("Too quick for Write/read channel") 107 | } 108 | fmt.Printf("Write/read channel: %.3f ns\n", float32(elapsed)/CT) 109 | 110 | tst := uint64(0x1234567812345678) 111 | runtime.GOMAXPROCS(4) 112 | go Reader(ch, &tst) 113 | go Writer(ch, &tst) 114 | go Writer(ch, &tst) 115 | <-ch 116 | <-ch 117 | <-ch 118 | } 119 | 120 | func Writer(ch chan int, p *uint64) { 121 | fmt.Println("Writer starting") 122 | for i := int64(0); i < 5e9; i++ { 123 | *p = 0x1234567812345678 124 | *p = 0x8765432187654321 125 | *p = 0x1234567812345678 126 | *p = 0x8765432187654321 127 | *p = 0x1234567812345678 128 | *p = 0x8765432187654321 129 | *p = 0x1234567812345678 130 | *p = 0x8765432187654321 131 | *p = 0x1234567812345678 132 | *p = 0x8765432187654321 133 | } 134 | fmt.Println("Writer done") 135 | ch <- 0 136 | } 137 | 138 | func Reader(ch chan int, p *uint64) { 139 | fmt.Println("Reader starting") 140 | var v uint64 141 | for i := int64(0); i < 2e9; i++ { 142 | v = *p 143 | if v != 0x1234567812345678 && v != 0x8765432187654321 { 144 | fmt.Printf("Reader: got %x\n", v) 145 | } 146 | v = *p 147 | if v != 0x1234567812345678 && v != 0x8765432187654321 { 148 | fmt.Printf("Reader: got %x\n", v) 149 | } 150 | } 151 | fmt.Println("Reader done") 152 | ch <- 1 153 | } 154 | -------------------------------------------------------------------------------- /src/cmd/server/commtest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "client_prot" 23 | "fmt" 24 | "net" 25 | "runtime" 26 | "time" 27 | ) 28 | 29 | type dummyConn struct { 30 | // Use a channel to feed the readers for this connection. 31 | ch chan []byte // Send byte array slices 32 | slice []byte // Remainder of previous slice not read yet. 33 | cmdFlags [client_prot.CMD_Last]bool 34 | open bool 35 | buffer bytes.Buffer // Buffer written data until there is a complete message 36 | } 37 | 38 | type dummyAddr struct{} 39 | 40 | func (dc *dummyConn) Close() error { dc.open = false; return nil } 41 | func (*dummyConn) LocalAddr() net.Addr { return dummyAddr{} } 42 | func (*dummyConn) RemoteAddr() net.Addr { return dummyAddr{} } 43 | func (*dummyConn) SetTimeout(nsec int64) error { return nil } 44 | func (*dummyConn) SetReadTimeout(nsec int64) error { return nil } 45 | func (*dummyConn) SetWriteTimeout(nsec int64) error { return nil } 46 | 47 | func (dc *dummyConn) Read(b []byte) (n int, err error) { 48 | _, file, line, ok := runtime.Caller(1) 49 | if ok { 50 | fmt.Printf("dummyConn Read: called from %s:%d\n", file, line) 51 | } 52 | if dc.slice == nil { 53 | dc.slice = <-dc.ch 54 | } 55 | n = copy(b, dc.slice) 56 | if n == len(dc.slice) { 57 | dc.slice = nil 58 | } else { 59 | dc.slice = dc.slice[n:] 60 | } 61 | if ok { 62 | fmt.Printf("dummyConn Read: returning %v to %s:%d\n", b, file, line) 63 | } 64 | return 65 | } 66 | 67 | // Inject a byte stream, to be used by the process on Read(). Not currently 68 | // used, as it is tricky to test with parallel processes. 69 | func (dc *dummyConn) Inject(b []byte) { 70 | dc.ch <- b 71 | } 72 | 73 | // Don't use the channel, we want to look at the data that 74 | // was going to be written. 75 | func (dc *dummyConn) Write(b []byte) (n int, err error) { 76 | // Notice that the message may not be complete. 77 | dc.buffer.Write(b) 78 | buff := dc.buffer.Bytes() 79 | // fmt.Printf("dummyConn:Write write buffer is now %v\n", buff) 80 | length := int(buff[0]) + int(buff[1])<<8 81 | if length > len(buff) { 82 | // Waiting for more data 83 | return len(b), nil 84 | } 85 | dc.cmdFlags[buff[2]] = true // Remember that this command has been seen. 86 | switch buff[2] { 87 | case client_prot.CMD_MESSAGE: // Ignore 88 | case client_prot.CMD_LOGIN_ACK: // Ignore 89 | case client_prot.CMD_OBJECT_LIST: // Ignore 90 | case client_prot.CMD_REQ_PASSWORD: // Ignore 91 | case client_prot.CMD_REPORT_COORDINATE: // Ignore 92 | case client_prot.CMD_RESP_PLAYER_HIT_MONSTER: // Ignore 93 | case client_prot.CMD_EQUIPMENT: // Ignore 94 | default: 95 | fmt.Printf("dummyConn:Write unexpected %d: %v\n", len(buff), buff) 96 | } 97 | dc.buffer.Reset() 98 | return len(b), nil 99 | } 100 | 101 | // Test if a command has been seen, and clear the flag afterwards. 102 | func (dc *dummyConn) TestCommandSeen(i int) (ret bool) { 103 | ret = dc.cmdFlags[i] 104 | dc.cmdFlags[i] = false 105 | return 106 | } 107 | 108 | func (dc *dummyConn) TestOpen() bool { 109 | return dc.open 110 | } 111 | 112 | func (dc *dummyConn) SetReadDeadline(t time.Time) error { return nil } 113 | func (dc *dummyConn) SetDeadline(t time.Time) error { return nil } 114 | func (dc *dummyConn) SetWriteDeadline(t time.Time) error { return nil } 115 | 116 | func MakeDummyConn() *dummyConn { 117 | return &dummyConn{ch: make(chan []byte), open: true} 118 | } 119 | 120 | func (dummyAddr) Network() string { return "127.0.0.1:1234" } 121 | func (dummyAddr) String() string { return "127.0.0.1:1234" } 122 | -------------------------------------------------------------------------------- /dumpfile.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 5.5.24, for debian-linux-gnu (x86_64) 2 | 3 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 4 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 5 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 6 | /*!40101 SET NAMES utf8 */; 7 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 8 | /*!40103 SET TIME_ZONE='+00:00' */; 9 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 10 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 11 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 12 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 13 | 14 | -- 15 | -- Table structure for table `avatars` 16 | -- 17 | 18 | /*!40101 SET @saved_cs_client = @@character_set_client */; 19 | /*!40101 SET character_set_client = utf8 */; 20 | CREATE TABLE `avatars` ( 21 | `name` varchar(50) NOT NULL, 22 | `id` int(1) unsigned NOT NULL AUTO_INCREMENT, 23 | `owner` varchar(255) NOT NULL, 24 | `HeadType` smallint(5) unsigned NOT NULL DEFAULT '0', 25 | `BodyType` smallint(5) unsigned NOT NULL DEFAULT '0', 26 | `PositionX` double NOT NULL DEFAULT '0', 27 | `PositionY` double NOT NULL DEFAULT '0', 28 | `PositionZ` double NOT NULL DEFAULT '2', 29 | `isFlying` tinyint(1) NOT NULL DEFAULT '0', 30 | `isClimbing` tinyint(1) NOT NULL DEFAULT '0', 31 | `isDead` tinyint(1) NOT NULL DEFAULT '0', 32 | `DirHor` float NOT NULL DEFAULT '0', 33 | `DirVert` float NOT NULL DEFAULT '0', 34 | `AdminLevel` int(1) unsigned NOT NULL DEFAULT '0', 35 | `Level` int(1) unsigned NOT NULL DEFAULT '0', 36 | `Experience` double NOT NULL DEFAULT '0', 37 | `HitPoints` double NOT NULL DEFAULT '1', 38 | `Mana` double NOT NULL DEFAULT '0', 39 | `Kills` int(1) NOT NULL DEFAULT '0', 40 | `BlocksAdded` int(10) unsigned NOT NULL DEFAULT '0', 41 | `BlocksRemoved` int(10) unsigned NOT NULL DEFAULT '0', 42 | `HomeX` double NOT NULL DEFAULT '0', 43 | `HomeY` double NOT NULL DEFAULT '0', 44 | `HomeZ` double NOT NULL DEFAULT '0', 45 | `TargetX` double NOT NULL DEFAULT '0', 46 | `TargetY` double NOT NULL DEFAULT '0', 47 | `TargetZ` double NOT NULL DEFAULT '0', 48 | `ReviveX` double NOT NULL DEFAULT '0', 49 | `ReviveY` double NOT NULL DEFAULT '0', 50 | `ReviveZ` double NOT NULL DEFAULT '0', 51 | `maxchunks` int(1) NOT NULL DEFAULT '-1', 52 | `TimeOnline` int(10) unsigned NOT NULL DEFAULT '0', 53 | `jsonstring` varchar(1000) NOT NULL, 54 | `lastseen` date NOT NULL DEFAULT '0000-00-00', 55 | `Inventory` blob NOT NULL, 56 | `TScoreTotal` double NOT NULL DEFAULT '0', 57 | `TScoreBalance` double NOT NULL DEFAULT '0', 58 | `TScoreTime` int(11) NOT NULL DEFAULT '0', 59 | UNIQUE KEY `name` (`name`), 60 | UNIQUE KEY `id` (`id`) 61 | ) ENGINE=MyISAM AUTO_INCREMENT=55 DEFAULT CHARSET=latin1; 62 | /*!40101 SET character_set_client = @saved_cs_client */; 63 | 64 | -- 65 | -- Table structure for table `chunkdata` 66 | -- 67 | 68 | /*!40101 SET @saved_cs_client = @@character_set_client */; 69 | /*!40101 SET character_set_client = utf8 */; 70 | CREATE TABLE `chunkdata` ( 71 | `x` int(1) NOT NULL, 72 | `y` int(1) NOT NULL, 73 | `z` int(1) NOT NULL, 74 | `avatarID` int(1) unsigned NOT NULL DEFAULT '0', 75 | `json` varchar(100) NOT NULL DEFAULT '', 76 | `reqrelease` tinyint(1) NOT NULL DEFAULT '0', 77 | `support` tinyint(1) NOT NULL DEFAULT '0', 78 | PRIMARY KEY (`x`,`y`,`z`), 79 | KEY `avatarID` (`avatarID`) 80 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1; 81 | /*!40101 SET character_set_client = @saved_cs_client */; 82 | 83 | -- 84 | -- Table structure for table `friends` 85 | -- 86 | 87 | /*!40101 SET @saved_cs_client = @@character_set_client */; 88 | /*!40101 SET character_set_client = utf8 */; 89 | CREATE TABLE `friends` ( 90 | `avatar` int(1) NOT NULL, 91 | `friend` int(1) NOT NULL, 92 | KEY `avatar` (`avatar`) 93 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1; 94 | /*!40101 SET character_set_client = @saved_cs_client */; 95 | 96 | -- 97 | -- Table structure for table `users` 98 | -- 99 | 100 | /*!40101 SET @saved_cs_client = @@character_set_client */; 101 | /*!40101 SET character_set_client = utf8 */; 102 | CREATE TABLE `users` ( 103 | `email` varchar(255) NOT NULL, 104 | `password` char(32) NOT NULL DEFAULT '00000000000000000000000000000000', 105 | `lastseen` date NOT NULL, 106 | `lastaddr` varchar(20) NOT NULL DEFAULT '0.0.0.0', 107 | `licensekey` char(20) NOT NULL, 108 | UNIQUE KEY `email` (`email`) 109 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1; 110 | /*!40101 SET character_set_client = @saved_cs_client */; 111 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 112 | 113 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 114 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 115 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 116 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 117 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 118 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 119 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 120 | 121 | -- Dump completed on 2012-09-27 7:43:19 122 | -------------------------------------------------------------------------------- /src/cmd/server/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | // 21 | // This is a general configuration file for the game engine 22 | // Times are defined in nonoseconds. That means 1e8=0.1s, 1e9=1s, 1e10=10s, 1e11=100s, etc. 23 | // TODO: All parameters here should have precix Cnfg. 24 | // 25 | const ( 26 | // How many nanoseconds between update of player and monster positions 27 | ObjectsUpdatePeriod = 1e8 // 10 times per second update 28 | MonstersUpdateTargetPeriod = 5e9 // How frequently the monster looks reevaluate when to switch target (aggro) 29 | MonstersUpdateDirPeriod = 1e9 // How frequent the monster will evaluate what direction to move 30 | CnfgAttackPeriod = 1e9 // How frequently attacks are updated, or healing if no attack 31 | CnfgAutosavePeriod = 3e11 // Autosave players 32 | CnfgDefaultTriggerBlockTime = 10 // Default seconds until an activation block can be used again 33 | PURGE_MONSTER_PERIOD = 3e10 // 30s second between every purge 34 | SPAWN_MONSTER_PERIOD = 1e10 // 10s between tests 35 | CnfgHealingPeriod = 1.2e11 // Nanoseconds needed for healing 100% 36 | MonsterMovingProb = 0.6 // The probability that the monster is moving when no aggro 37 | MAX_PLAYERS = 2000 // Maximum number of logged in players 38 | RUNNING_SPEED = 4.0 // blocks per second of a running player. TODO: Should be a bigger number? 39 | GRAVITY = 5.0 // blocks per seconds squared 40 | QuadtreeInitSize = 64 // Measured in blocks. 41 | DefaultMonsterSpawnDistance = 25 // Number of blocks away that random monsters will spawn 42 | CheckMonsterDistForSpawn = 40 // Monsters inside this distance are counted when deciding whether to spawn more 43 | CnfgMonsterAggroDistance = 20 // How close you have to be to a monser to get aggro 44 | CnfgMonsterFieldOfView = 1.40 // The viewing angle for a monster, in radians 45 | CnfgMeleeDistLimit = 4 // Max block distance to be allowed to hit 46 | CnfgMaxChunkReqDist = 6 // Client can not request chunks further away than this (in one dimension). Max view distance is 160 blocks, which correspopnds to 5 chunks. 47 | MaxMonsterSpawnHeightDiff = 6 // The monster will not spawn outside of this diff to the player (in blocks) 48 | MonsterLimitForRespawn = 3 // If there is at least this many monsters near, no new will be spawned. 49 | LoginChallengeLength = 20 // The number of random bytes used in login, sent to the client. 50 | FlyingSpeedFactor = 3 // How much quicker you fly than walking 51 | PlayerHeight = 1.8 * 2 // Height of player 180 cm = 1.8m = 3.6 blocks 52 | PlayerJumpSpeed = 2 // Number of blocks per seoond intial upward speed. 53 | WORLD_SOIL_LEVEL = 9 // No soil above this level 54 | FLOATING_ISLANDS_LIM = 96 // No floating islands are created below this level 55 | FLOATING_ISLANDS_PROB = 0.85 // The density required for a floating island 56 | CnfgCaveWidth = 0.1 // A bigger number will make cave tunnels wider 57 | CnfgTestPlayerNamePrefix = "test" // When test players are allowed, whithout password, the name has to begine with this string. 58 | CnfgTestPlayersPerChunk = 1.0 // Defines the density of test players 59 | ClientChannelSize = 100 // Number of messages that can wait for being sent 60 | CnfgManaForHealing = 0.35 // Mana needed for the healing spell 61 | CnfgHealthAtHealingSpell = 0.3 // How much the player heals for a healing spell 62 | CnfgManaForCombAttack = 0.15 // Mana needed for combination attack 63 | CnfgWeaponDmgCombAttack = 1.5 // Damage for the extra attack 64 | CnfgMaxOwnChunk = 10 // The number of chunks a normal player can own. It can be overriden. 65 | CnfgJellyTimeout = 15 // Number of seconds a block will be in jelly state 66 | CnfgItemRewardNormalizer = 0.02 // How much experience to get for a dropped item of lowest grade at player level 67 | CnfgScoreMoveFact = 1.0 / 128 // This means that a player need to move 64 blocs in a chunk to award 1 point 68 | CnfgScoreDamageFact = 1.0 / 5 // Number of monsters that need to be killed for one point 69 | CnfgChunkFolder = "DB" // The folder where all chunks are stored 70 | CnfgSuperChunkFolder = "SDB" // The folder where all super chunks are stored 71 | ) 72 | -------------------------------------------------------------------------------- /src/cmd/server/globalUser.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | // This file contains global functions, not called from the client proc, that use the "user" struct. 21 | 22 | import ( 23 | "chunkdb" 24 | "client_prot" 25 | "fmt" 26 | "log" 27 | "math" 28 | "quadtree" 29 | . "twof" 30 | ) 31 | 32 | func SaveAllPlayers_RLa() { 33 | allPlayersSem.RLock() 34 | for i := 0; i < MAX_PLAYERS; i++ { 35 | up := allPlayers[i] 36 | if up != nil && up.Email != "" && up.connState == PlayerConnStateIn { 37 | up.forceSave = true 38 | } 39 | // up.Printf("Autosave"); 40 | } 41 | allPlayersSem.RUnlock() 42 | } 43 | 44 | // Send a text message to a player, which must not be locked. 45 | // If the message can't be sent, discard it. 46 | func (up *user) Printf(format string, a ...interface{}) { 47 | str := fmt.Sprintf(format, a...) 48 | length := len(str) + 3 49 | prefix := []byte{byte(length & 0xFF), byte(length >> 8), client_prot.CMD_MESSAGE} 50 | msg := string(prefix) + str 51 | up.writeNonBlocking([]byte(msg)) 52 | } 53 | 54 | // Send a message to a client, but it must not block. Because of that, send the message to the 55 | // local client process that can handle the blocking. 56 | // Condition: There is no guarantee to be any locks, which means there is no guarantee in what order 57 | // these messages arrive to the client. Ok, the order will stay the same, but there may be other 58 | // processes that manage to inject message in between others. 59 | func (up *user) writeNonBlocking(b []byte) { 60 | if length := int(b[0]) + int(b[1])<<8; length != len(b) { 61 | panic("Wrong length of message") 62 | } 63 | if *verboseFlag > 2 { 64 | log.Printf("Non blocking Send to %v '%v'\n", up.Name, b) 65 | } 66 | select { 67 | case up.channel <- b: 68 | default: 69 | } 70 | } 71 | 72 | // Send a non blocking command to the player. 73 | // The command can fail if the receiver is full. 74 | func (up *user) SendCommand(cmd ClientCommand) { 75 | // Use a select statement to make sure it never blocks. 76 | select { 77 | case up.commandChannel <- cmd: 78 | default: 79 | } 80 | } 81 | 82 | func (this *user) SomeoneMoved(o quadtree.Object) { 83 | this.objMoved = append(this.objMoved, o) 84 | } 85 | 86 | func (up *user) GetPreviousPos() *TwoF { 87 | return &TwoF{up.prevCoord.X, up.prevCoord.Y} 88 | } 89 | 90 | // Compose a message to the client to update block change. The player must not be locked. 91 | func (up *user) SendMessageBlockUpdate(cc chunkdb.CC, dx uint8, dy uint8, dz uint8, blType block) { 92 | const length = 19 93 | var ans [length]byte 94 | ans[0] = byte(length & 0xFF) 95 | ans[1] = byte(length >> 8) 96 | ans[2] = client_prot.CMD_BLOCK_UPDATE 97 | EncodeUint32(uint32(cc.X), ans[3:7]) 98 | EncodeUint32(uint32(cc.Y), ans[7:11]) 99 | EncodeUint32(uint32(cc.Z), ans[11:15]) 100 | ans[15], ans[16], ans[17] = dx, dy, dz 101 | ans[18] = uint8(blType) 102 | up.writeNonBlocking(ans[:]) 103 | } 104 | 105 | // Report the inventory for one item to a player. 106 | // The amount can be 0. The purpose of this function is to update the client for a specific 107 | // inventory item. 108 | func ReportOneInventoryItem_WluBl(up *user, code ObjectCode, lvl uint32) { 109 | const msgLen = 12 110 | b := make([]byte, msgLen) 111 | b[0] = byte(msgLen) 112 | // b[1] = 0 113 | b[2] = client_prot.CMD_UPD_INV 114 | for j := 0; j < 4; j++ { 115 | b[3+j] = code[j] 116 | } 117 | // b[7] = 0 // Default count is 0 118 | EncodeUint32(lvl, b[8:12]) 119 | up.RLock() 120 | inv := up.Inventory 121 | i := inv.Find(code, lvl) 122 | if i >= 0 { 123 | count := inv[i].Count 124 | if count > math.MaxUint8 { 125 | count = math.MaxUint8 // This is what can be shown to the client 126 | } 127 | b[7] = byte(count) 128 | } 129 | up.RUnlock() 130 | // Wait with the actual writing until after unlocking the player. 131 | up.writeNonBlocking(b) 132 | // log.Println(b) 133 | } 134 | 135 | // Add an object to the player inventory. It will automatically get a level that corresponds 136 | // to the current position. 137 | func AddOneObjectToUser_WLuBl(up *user, Type ObjectCode) { 138 | up.Lock() 139 | level := MonsterDifficulty(&up.Coord) // We want an object of a level corresponding to the monsters at this place. 140 | up.Inventory.AddOneObject(Type, level) 141 | up.Unlock() 142 | ReportOneInventoryItem_WluBl(up, Type, level) 143 | } 144 | 145 | // Find all players near 'up', including self, and report the equipment of 'up' 146 | func ReportEquipmentToNear_Bl(up *user) { 147 | nearPlayers := playerQuadtree.FindNearObjects_RLq(up.GetPreviousPos(), client_prot.NEAR_OBJECTS) 148 | log.Println("Near players", len(nearPlayers)) 149 | for _, o := range nearPlayers { 150 | if other, ok := o.(*user); ok { 151 | other.ReportEquipment_Bl(up) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/cmd/server/procs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | // 21 | // This is a scheduler for a number of processes. 22 | // 23 | // Currently, the scheduling principle will slow down the frequency 24 | // in case of heavy load, instead of always schedule relative to 25 | // the last invocation. 26 | // 27 | 28 | import ( 29 | "github.com/larspensjo/config" 30 | "log" 31 | "os" 32 | "os/signal" 33 | "time" 34 | "timerstats" 35 | ) 36 | 37 | func CatchSig() { 38 | ch := make(chan os.Signal) 39 | signal.Notify(ch, os.Interrupt, os.Kill) 40 | for { 41 | sig := <-ch 42 | log.Printf("Got signal %v %#v\n", sig, sig) 43 | if sig == os.Interrupt || sig == os.Kill { // 2: kill -HUP, Ctrl-C 44 | GraceFulShutdown() 45 | // Will not return 46 | } else { // kill with no arguments 47 | panic("Got signal\n") 48 | } 49 | } 50 | } 51 | 52 | func ProcPurgeOldChunks_WLw() { 53 | for i := 0; ; i++ { 54 | if i == WORLD_CACHE_SIZE { 55 | i = 0 56 | } 57 | worldCacheLock.Lock() 58 | for pc := world_cache[i]; pc != nil; { 59 | next := pc.next // Save the next pointer now, as the current 'pc' may get released 60 | if !pc.touched { 61 | // No one used this chunk since last checked, so it is released 62 | RemoveChunkFromHashTable(pc) 63 | // fmt.Printf("ProcPurgeOldChunks: free %v\n", pc.Coord) 64 | } else { 65 | // The chunk was touched. Give it another chance. 66 | pc.touched = false 67 | // fmt.Printf("ProcPurgeOldChunks: keep %v\n", pc.Coord) 68 | } 69 | pc = next 70 | } 71 | worldCacheLock.Unlock() 72 | time.Sleep(1e8) 73 | } 74 | } 75 | 76 | func ProcUpdateMonsterState() { 77 | var elapsed time.Duration 78 | timerstats.Add("ProcUpdateMonsterState", MonstersUpdateDirPeriod, &elapsed) 79 | lastTime := time.Now() 80 | for { 81 | start := time.Now() 82 | time.Sleep(MonstersUpdateDirPeriod) 83 | if start.Sub(lastTime) > 4e9 { 84 | lastTime = start 85 | } 86 | UpdateAllMonstersState() 87 | elapsed = time.Now().Sub(start) 88 | } 89 | } 90 | 91 | func ProcUpdateMonsterPos_RLmWLwWLqBlWLuWLc() { 92 | var elapsed time.Duration 93 | timerstats.Add("ProcUpdateMonsterPos", ObjectsUpdatePeriod, &elapsed) 94 | lastTime := time.Now() 95 | for { 96 | start := time.Now() 97 | time.Sleep(ObjectsUpdatePeriod) // TODO: Monsters could move at a lower frequency than players 98 | fullReport := false 99 | if start.Sub(lastTime) > 4e9 { 100 | // Request a full report, but not too often as it generates much traffic 101 | fullReport = true 102 | lastTime = start 103 | } 104 | UpdateAllMonsterPos_RLmWLwWLqWLuWLc(fullReport) 105 | elapsed = time.Now().Sub(start) 106 | } 107 | } 108 | 109 | func ProcUpdateMonstersTarget_RLmRLqBl() { 110 | var elapsed time.Duration 111 | timerstats.Add("ProcUpdateMonstersTarget", MonstersUpdateTargetPeriod, &elapsed) 112 | for { 113 | start := time.Now() 114 | time.Sleep(MonstersUpdateTargetPeriod) 115 | UpdateAllMonstersTarget_RLmRLq() 116 | elapsed = time.Now().Sub(start) 117 | } 118 | } 119 | 120 | func ProcPurgeMonsters_WLmWLqBl() { 121 | var elapsed time.Duration 122 | timerstats.Add("ProcPurgeMonsters", PURGE_MONSTER_PERIOD, &elapsed) 123 | for { 124 | start := time.Now() 125 | time.Sleep(PURGE_MONSTER_PERIOD) 126 | CmdPurgeMonsters_WLmWLq() 127 | elapsed = time.Now().Sub(start) 128 | } 129 | } 130 | 131 | func ProcSpawnMonsters_WLwWLuWLqWLmBlWLc() { 132 | var elapsed time.Duration 133 | timerstats.Add("ProcSpawnMonsters", SPAWN_MONSTER_PERIOD, &elapsed) 134 | for { 135 | start := time.Now() 136 | time.Sleep(SPAWN_MONSTER_PERIOD) 137 | CmdSpawnMonsters_WLwWLuWLqWLmWLc() 138 | elapsed = time.Now().Sub(start) 139 | } 140 | } 141 | 142 | // Control monsters attacking players 143 | func ProcMonsterMelee_RLmBl() { 144 | var elapsed time.Duration 145 | timerstats.Add("ProcMonsterMeelee", CnfgAttackPeriod, &elapsed) 146 | for { 147 | start := time.Now() 148 | time.Sleep(CnfgAttackPeriod) 149 | CmdMonsterMelee_RLm() 150 | elapsed = time.Now().Sub(start) 151 | } 152 | } 153 | 154 | var ClientCurrentMajorVersion, ClientCurrentMinorVersion int 155 | 156 | // Autosave of players 157 | func ProcAutosave_RLu() { 158 | var elapsed time.Duration 159 | timerstats.Add("ProcAutosave", CnfgAutosavePeriod, &elapsed) 160 | for { 161 | start := time.Now() 162 | oldMajor, oldMinor := ClientCurrentMajorVersion, ClientCurrentMinorVersion 163 | ClientCurrentMajorVersion, ClientCurrentMinorVersion = LoadClientVersionInformation() 164 | if (oldMajor != ClientCurrentMajorVersion || oldMinor != ClientCurrentMinorVersion) && *verboseFlag > 0 { 165 | log.Printf("Current client version %d.%d\n", ClientCurrentMajorVersion, ClientCurrentMinorVersion) 166 | } 167 | time.Sleep(CnfgAutosavePeriod) 168 | SaveAllPlayers_RLa() 169 | elapsed = time.Now().Sub(start) 170 | } 171 | } 172 | 173 | // Extract the version of the current client from the config file. This is done 174 | // repeatedly, which means the definition can be changed "live", while the server is running. 175 | func LoadClientVersionInformation() (int, int) { 176 | cnfg, err := config.ReadDefault(*configFileName) 177 | if err == nil && cnfg.HasSection("client") { 178 | major, err := cnfg.Int("client", "major") 179 | if err != nil { 180 | log.Println(*configFileName, "major:", err) 181 | return 0, 0 182 | } 183 | minor, err := cnfg.Int("client", "minor") 184 | if err != nil { 185 | log.Println(*configFileName, "minor:", err) 186 | return 0, 0 187 | } 188 | return major, minor 189 | } 190 | return 0, 0 191 | } 192 | -------------------------------------------------------------------------------- /src/client_prot/client_prot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2013 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package client_prot 19 | 20 | // 21 | // This defines the protocol used with the client. 22 | // The first two bytes is the length of the complete message, with LSB first. 23 | // The third byte always identifies the content of the message. 24 | // The iota functionality could have been used, but a command number can never change as it would 25 | // make client incompatible. 26 | // 27 | // For detailed description of the protocol, see the document in google docs. 28 | 29 | const ( 30 | CMD_LOGIN = 1 // The argument is a string with the login name 31 | CMD_SAVE = 2 // No argument 32 | CMD_QUIT = 3 // No argument 33 | CMD_MESSAGE = 4 // A text message to the client 34 | CMD_GET_COORDINATE = 5 // Used to request the player coordinates (no arguments) 35 | CMD_REPORT_COORDINATE = 6 // The answer sent from the server. Format is 3 x 8 bytes, with x, y, z, LSB first. 36 | CMD_READ_CHUNK = 7 // Read chunk at offset x,y,z from current chunk. Coded as 4 bytes, will be multiplied by chunk_size 37 | CMD_CHUNK_ANSWER = 8 // The requested chunk sent back to the client 38 | CMD_LOGIN_ACK = 9 // Acknowledge login to the client 39 | CMD_START_FWD = 10 // Player start moving forward 40 | CMD_STOP_FWD = 11 // Player stop moving forward 41 | CMD_START_BWD = 12 // Player start moving backward 42 | CMD_STOP_BWD = 13 // Player stop moving backward' 43 | CMD_START_LFT = 14 // Player start moving left 44 | CMD_STOP_LFT = 15 // Player stop moving left 45 | CMD_START_RGT = 16 // Player start moving right 46 | CMD_STOP_RGT = 17 // Player stop moving right 47 | CMD_JUMP = 18 // Player jump 48 | CMD_SET_DIR = 19 // For the client to set the looking direction. 49 | CMD_OBJECT_LIST = 20 // A list of players, mobs, and other moving objects positions 50 | CMD_HIT_BLOCK = 21 // The client registers a hit on an object 51 | CMD_BLOCK_UPDATE = 22 // One or more blocks have been updated in a chunk 52 | CMD_DEBUG = 23 // A string sent to the server, interpreted as a debug command 53 | CMD_REQ_PASSWORD = 24 // Request the password from the client, encrypt it with RC4 using argument. 54 | CMD_RESP_PASSWORD = 25 // An encrypted password from the client to the server 55 | CMD_PROT_VERSION = 26 // The version of the communication protocol 56 | CMD_VRFY_SUPERCHUNCK_CS = 29 // Request server to verify one or more super chunk checksums. If wrong, an update will be sent. 57 | CMD_SUPERCHUNK_ANSWER = 29 // (Same number) The full requested super chunk sent back to the client 58 | CMD_PLAYER_STATS = 30 // Information about the player that doesn't change very often. 59 | CMD_ATTACK_MONSTER = 31 // Initiate an attack on a monster 60 | CMD_PLAYER_ACTION = 32 // A generic action, see UserAction* below 61 | CMD_RESP_PLAYER_HIT_BY_MONSTER = 33 // The player was hit by one or more monsters 62 | CMD_RESP_PLAYER_HIT_MONSTER = 34 // The player hit one or more monsters 63 | CMD_RESP_AGGRO_FROM_MONSTER = 35 // The player got aggro from one or more monsters 64 | CMD_VRFY_CHUNCK_CS = 36 // Request server to verify checksum for one or more chunks. If wrong, the updated chunk will be sent. 65 | CMD_USE_ITEM = 37 // The player uses an item from the inventory. 66 | CMD_UPD_INV = 38 // Server updates the client about the amount of items. 67 | CMD_EQUIPMENT = 39 // Report equipment 68 | CMD_JELLY_BLOCKS = 40 // Turn blocks transparent and permeable 69 | CMD_PING = 41 // Used to measure communication delay. 70 | CMD_DROP_ITEM = 42 // Drop an item from the inventory 71 | CMD_LOGINFAILED = 43 // The login failed. 72 | CMD_REQ_PLAYER_INFO = 44 // Request player information 73 | CMD_RESP_PLAYER_NAME = 45 // A name of a player 74 | CMD_TELEPORT = 46 // Teleport player to a chunk coordinate. 75 | CMD_ERROR_REPORT = 47 // Send an error report to the server, in the form of a string. 76 | CMD_Last = 48 // ONE HIGHER THAN LAST COMMAND! Add no commands after this one. 77 | 78 | ProtVersionMajor = 5 79 | ProtVersionMinor = 2 80 | ) 81 | 82 | // 83 | // These are the object types. 84 | // 85 | const ( 86 | // Types used by OBJECT_LIST 87 | ObjTypePlayer = 0 88 | ObjTypeMonster = 1 89 | 90 | // States used by OBJECT_LIST 91 | ObjStateRemove = 0 92 | ObjStateInGame = 1 93 | 94 | NEAR_OBJECTS = 64 // All objects within this distance are considered to be near, and reported to clients 95 | BLOCK_COORD_RES = 100 // Fixed point resolution of coordinates 96 | 97 | // Action used as argument for the CMD_PLAYER_ACTION 98 | UserActionHeal = 0 99 | UserActionCombAttack = 1 100 | ) 101 | 102 | // These are flags used in the CMD_PLAYER_STATS command 103 | const ( 104 | UserFlagInFight = uint32(1 << 0) 105 | // UserFlagPlayerHit = uint32(1 << 1) // The player hit a monster, transient flag 106 | // UserFlagMonsterHit = uint32(1 << 2) // A monster hit the player, transient flag 107 | UserFlagHealed = uint32(1 << 3) // The player was healed (not the gradual healing) 108 | UserFlagJump = uint32(1 << 4) // The sound when the user hits the ground after jumping 109 | 110 | // Define a bit mask with all transient flags 111 | UserFlagTransientMask = (UserFlagHealed | UserFlagJump) 112 | ) 113 | -------------------------------------------------------------------------------- /src/cmd/server/createchunk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | // "fmt" 22 | "chunkdb" 23 | "github.com/larspensjo/Go-simplex-noise/simplexnoise" 24 | "math" 25 | "time" 26 | ) 27 | 28 | func dBdensity(xf, yf, zf float64) float64 { 29 | return simplexnoise.Noise3(xf*0.01, yf*0.01, zf*0.01)/2 + 0.5 // Now in range 0-1 30 | } 31 | 32 | var DBCreateStats struct { 33 | Num int 34 | TotTime time.Duration 35 | } 36 | 37 | // For a chunk at a coordinate, create it. 38 | // There is no need for a lock, as no one can access the chunk 39 | // TODO: Some algorithms depend on looking at the block above, which can only be done when inside the 40 | // chunk. If the test would require looking at another chunk, the tets skipped. This leads to some special 41 | // effects failure. 42 | func dBCreateChunk(c chunkdb.CC) *chunk { 43 | start := time.Now() 44 | ch := new(chunk) 45 | ch.rc = new(raw_chunk) 46 | ch.Coord = c 47 | z1 := int(c.Z * CHUNK_SIZE) 48 | 49 | for x := int32(0); x < CHUNK_SIZE; x++ { 50 | xf := float64(x + c.X*CHUNK_SIZE) 51 | for y := int32(0); y < CHUNK_SIZE; y++ { 52 | yf := float64(y + c.Y*CHUNK_SIZE) 53 | highFreq := 20 * simplexnoise.Noise2(xf*0.016, yf*0.016) // This will generate high frequency terrain 54 | f := simplexnoise.Noise2(xf*0.0025, yf*0.0025) // Factor to modulate the high frequency amplitude 55 | lowFreq := 15 * simplexnoise.Noise2(xf*0.0013, yf*0.0013) // Low frequency terrain 56 | stoneheight := math.Floor(2.5 + highFreq*f*f + lowFreq) 57 | 58 | // Given 'height', fill in the content of the current chunk. Iterate from high 'z' to low, 59 | // to enable tests that depends on the block above. 60 | for z := CHUNK_SIZE - 1; z >= 0; z-- { 61 | if *inhibitCreateChunks { 62 | ch.rc[x][y][z] = BT_Air 63 | continue 64 | } 65 | zf := float64(z + z1) 66 | if zf > FLOATING_ISLANDS_LIM { 67 | // Use a gradial transient, or all islands would have a hard cut off. 68 | f := (1-FLOATING_ISLANDS_PROB)/CHUNK_SIZE*(zf-FLOATING_ISLANDS_LIM) + FLOATING_ISLANDS_PROB 69 | if f > 1 { 70 | f = 1 71 | } 72 | density := f * dBdensity(xf/2, yf/2, zf) // Use a compressed layout in height 73 | if density > FLOATING_ISLANDS_PROB { 74 | if z != CHUNK_SIZE-1 && blockIsInvisible[ch.rc[x][y][z+1]] { 75 | ch.rc[x][y][z] = BT_Soil // Put grass on top 76 | } else { 77 | ch.rc[x][y][z] = BT_Stone 78 | } 79 | } else { 80 | ch.rc[x][y][z] = BT_Air 81 | } 82 | continue 83 | } 84 | density := dBdensity(xf/2, yf/2, zf) 85 | soildepth := math.Floor(2*simplexnoise.Noise2(xf*0.012, yf*0.012) + 2.8) 86 | if stoneheight > WORLD_SOIL_LEVEL { 87 | soildepth = 0 88 | } else if soildepth+stoneheight > WORLD_SOIL_LEVEL { 89 | soildepth = WORLD_SOIL_LEVEL - stoneheight 90 | } 91 | height := stoneheight + soildepth 92 | 93 | ch.rc[x][y][z] = BT_Air 94 | if zf <= stoneheight { 95 | // Initialize with stone, may be updated below 96 | if zf > 24 { 97 | ch.rc[x][y][z] = BT_Snow 98 | } else { 99 | ch.rc[x][y][z] = BT_Stone 100 | } 101 | } else if zf <= stoneheight+soildepth { 102 | // Initialize with soil, may be updated below 103 | ch.rc[x][y][z] = BT_Soil 104 | } 105 | 106 | // Excavate some holes in the terrain. Don't let the hole go too deep 107 | const HOLEDEPTH = 50 // Max depth of hole 108 | if zf > -HOLEDEPTH && zf < HOLEDEPTH { 109 | density := dBdensity(xf, yf, zf) // This costs a lot of CPU 110 | fadeoff := 1.0 111 | if zf >= -HOLEDEPTH && zf <= 0 { 112 | fadeoff = (HOLEDEPTH + zf) / HOLEDEPTH 113 | } else if zf < -HOLEDEPTH { 114 | fadeoff = 0 115 | } 116 | if density*fadeoff > 0.7 { 117 | ch.rc[x][y][z] = BT_Air 118 | } 119 | } 120 | 121 | // Some special cases if below water line 122 | if zf <= 0 { 123 | if ch.rc[x][y][z] == BT_Air { 124 | ch.rc[x][y][z] = BT_Water 125 | } else if ch.rc[x][y][z] == BT_Soil { 126 | ch.rc[x][y][z] = BT_Stone 127 | } 128 | if ch.rc[x][y][z] == BT_Stone && z+z1 == 0 && blockIsInvisible[ch.rc[x][y][1]] { 129 | // Replace stone with sand if it is at water level and air above. 130 | ch.rc[x][y][0] = BT_Sand 131 | } 132 | } 133 | 134 | a := density > 0.5-CnfgCaveWidth/2 && density < 0.5+CnfgCaveWidth/2 135 | if zf <= height && a { 136 | density2 := dBdensity(1000-xf/2, 1000-yf/2, 1000-zf) // Use a compressed layout in height 137 | b := density2 > 0.5-CnfgCaveWidth/2 && density2 < 0.5+CnfgCaveWidth/2 138 | if b && ch.rc[x][y][z] != BT_Water { 139 | ch.rc[x][y][z] = BT_Air 140 | } 141 | } 142 | 143 | // Add some scenery 144 | if ch.rc[x][y][z] == BT_Soil && z+1 < CHUNK_SIZE && blockIsInvisible[ch.rc[x][y][z+1]] { 145 | // This is a candidate for a tree override 146 | const ( 147 | t3 = 0.0005 // Very few big trees 148 | t2 = 0.005 149 | t1 = 0.010 150 | tflower = 0.012 // Less flowers than tuft of grass 151 | ttuft = 0.020 152 | ) 153 | rnd := math.Abs(simplexnoise.Noise2(xf*422.34, yf*234.123)) // Without scaling, there is a line where xf+yf==0 gives rnd=0 154 | if rnd > t1 { 155 | continue // Not needed for the algorithm but will save a call to Noise2. 156 | } 157 | // Use a low frequency function to make less trees for some areas. 158 | lowFreq := 1 - math.Abs(simplexnoise.Noise2(xf*0.002, yf*0.002)) 159 | // The lowFreq function takes away too many trees, ease it up a little 160 | lowFreq = 1 - lowFreq*lowFreq 161 | // fmt.Printf("%.5f ", lowFreq) 162 | switch { 163 | case rnd < t3*lowFreq: 164 | ch.rc[x][y][z+1] = BT_Tree3 165 | case rnd < t2*lowFreq: 166 | ch.rc[x][y][z+1] = BT_Tree2 167 | case rnd < t1*lowFreq: 168 | ch.rc[x][y][z+1] = BT_Tree1 169 | case rnd < tflower*lowFreq: 170 | ch.rc[x][y][z+1] = BT_Flowers 171 | case rnd < ttuft*lowFreq: 172 | ch.rc[x][y][z+1] = BT_Tuft 173 | } 174 | } 175 | } 176 | } 177 | } 178 | ch.compressAndChecksum() 179 | ch.touched = true 180 | delta := time.Now().Sub(start) 181 | DBCreateStats.Num++ 182 | DBCreateStats.TotTime += delta 183 | return ch 184 | } 185 | -------------------------------------------------------------------------------- /src/cmd/clientsimulator/ListenForMessages.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | // 19 | // There are three processes created: 20 | // 1. Simulating a player and generating messages that will be sent to the game server. 21 | // 2. Listening for messsages being sent back from the game server. 22 | // 3. The main control process, keeping track of the state. 23 | 24 | package main 25 | 26 | import ( 27 | "client_prot" 28 | "fmt" 29 | "net" 30 | "os" 31 | ) 32 | 33 | // All commands sent here has to match this interface. 34 | type msg_command interface { 35 | Execute() 36 | } 37 | 38 | var xpos, ypos, zpos int64 39 | 40 | func CentralControl(conn net.Conn, user string) { 41 | // Setup a channel callback messages. 42 | ch := make(chan msg_command) 43 | 44 | go SimulatePlayer(ch, conn) // Start the player simulator 45 | go ListenForServerMessages(ch, conn, user) 46 | 47 | for { 48 | cmd := <-ch 49 | cmd.Execute() 50 | } 51 | } 52 | 53 | func ListenForServerMessages(ch chan msg_command, conn net.Conn, user string) { 54 | var buff [15000]byte // TODO: Ugly naked constant 55 | for { 56 | // TODO: The mechanism for reading a message is ugly. Use something similar as the C++ client instead. 57 | n, err := conn.Read(buff[0:2]) // Only the length 58 | if err != nil || n == 0 { 59 | if e2, ok := err.(*net.OpError); ok && e2.Temporary() { 60 | fmt.Println("ListenForServerMessages: temporary") 61 | continue 62 | } 63 | if e2, ok := err.(*net.OpError); ok && e2.Timeout() { 64 | fmt.Println("ListenForServerMessages: timeout") 65 | continue 66 | } 67 | fmt.Printf("ListenForServerMessages: Failed to read: %v\n", err) 68 | os.Exit(1) // Major failure 69 | } 70 | if n == 1 { 71 | if *vFlag >= 1 { 72 | fmt.Printf("ListenForServerMessages: Got %d bytes reading from socket\n", n) 73 | } 74 | n2, err := conn.Read(buff[1:2]) // Second byte of the length 75 | if err != nil || n2 != 1 { 76 | fmt.Printf("ListenForServerMessages: Failed to read: %v\n", err) 77 | os.Exit(1) // Major failure 78 | } 79 | n = 2 80 | } 81 | length := int(uint(buff[1])<<8 + uint(buff[0])) 82 | if length < 3 || length > cap(buff) { 83 | fmt.Printf("ListenForServerMessages: User %v Expecting %d bytes, which is bad\n", user, length) 84 | conn.Read(buff[2:2]) 85 | fmt.Println("Command was", buff[2]) 86 | os.Exit(1) 87 | } 88 | n, err = conn.Read(buff[2:length]) // Read the rest.readin from socket 89 | if err != nil { 90 | fmt.Printf("ListenForServerMessages: Failed to read: %v\n", err) 91 | os.Exit(1) // Major failure 92 | } 93 | for tot := n + 2; tot < length; tot += n { 94 | n, err = conn.Read(buff[tot:length]) // Read the rest. 95 | if err != nil { 96 | fmt.Printf("ListenForServerMessages: Failed to read: %v\n", err) 97 | os.Exit(1) // Major failure 98 | } 99 | // fmt.Printf("Got trailing %d+%d bytes of %d\n", tot, n, length) 100 | } 101 | if *vFlag > 1 { 102 | fmt.Printf("ListenForServerMessages user %v Receive %v... (length %d)\n", user, buff[0:3], length) 103 | } 104 | // fmt.Printf("ListenForServerMessages (l %d) cmd %v\n", length, buff[:n]) 105 | switch buff[2] { 106 | case client_prot.CMD_RESP_PLAYER_HIT_BY_MONSTER: 107 | if *vFlag > 1 { 108 | fmt.Printf("Player %v hit with %.0f\n", user, float32(buff[7])/255*100) 109 | } 110 | case client_prot.CMD_OBJECT_LIST: 111 | // List of players or other things. For now, ignore this. 112 | case client_prot.CMD_MESSAGE: 113 | if *vFlag > 1 { 114 | fmt.Printf("%s\n", string(buff[3:length])) 115 | } 116 | case client_prot.CMD_REPORT_COORDINATE: 117 | ch <- ReportCoordinateCommand{buff[3:length]} 118 | case client_prot.CMD_CHUNK_ANSWER: 119 | // fmt.Printf("Got chunk buffer length %d: %v\n", length, buff[0:length]) 120 | ch <- ReportChunkCommand{buff[3:length]} 121 | case client_prot.CMD_LOGIN_ACK: 122 | if *vFlag > 0 { 123 | fmt.Println("User", user, "login ack") 124 | } 125 | waitForAck.Unlock() 126 | case client_prot.CMD_PLAYER_STATS: 127 | hp := buff[3] 128 | // exp := buff[4] 129 | // mana := buff[13] 130 | if hp == 0 { 131 | if *vFlag > 0 { 132 | fmt.Println(user, "dies.") 133 | } 134 | const length = 10 135 | var b [length]byte 136 | b[0] = length 137 | b[2] = client_prot.CMD_DEBUG 138 | msg := append(b[0:3], "/revive"...) 139 | SendMsg(conn, msg) 140 | } 141 | case client_prot.CMD_BLOCK_UPDATE: // For now, ignore this. 142 | case client_prot.CMD_REQ_PASSWORD: // Ignore 143 | case client_prot.CMD_EQUIPMENT: 144 | case client_prot.CMD_PROT_VERSION: 145 | case client_prot.CMD_RESP_PLAYER_HIT_MONSTER: 146 | case client_prot.CMD_JELLY_BLOCKS: 147 | case client_prot.CMD_RESP_AGGRO_FROM_MONSTER: 148 | monsterID := ParseUint32(buff[3:7]) 149 | if *vFlag > 0 { 150 | fmt.Println("Aggro by monster", monsterID) 151 | } 152 | const length = 7 153 | var b [length]byte 154 | b[0] = length 155 | b[2] = client_prot.CMD_ATTACK_MONSTER 156 | for i := uint(0); i < 4; i++ { 157 | b[i+3] = byte((monsterID >> (i * 8)) & 0xFF) 158 | } 159 | SendMsg(conn, b[:]) 160 | case client_prot.CMD_UPD_INV: 161 | fmt.Println(user, "got a drop") 162 | default: 163 | fmt.Printf("Unknown command %v\n", buff[0:length]) 164 | } 165 | } 166 | } 167 | 168 | type ReportCoordinateCommand struct { 169 | data []byte // 3 x 8 bytes for the coordinates 170 | } 171 | 172 | func (rcc ReportCoordinateCommand) Execute() { 173 | var x uint64 174 | for i := uint(0); i < 8; i++ { 175 | x += uint64(rcc.data[i]) << (i * 8) 176 | } 177 | var y uint64 178 | for i := uint(0); i < 8; i++ { 179 | y += uint64(rcc.data[i+8]) << (i * 8) 180 | } 181 | var z uint64 182 | for i := uint(0); i < 8; i++ { 183 | z += uint64(rcc.data[i+16]) << (i * 8) 184 | } 185 | // fmt.Printf("ReportCoordinate: %v,%v,%v\n", float(x)/100, float(y)/100, float(z)/100) 186 | // fmt.Printf("ReportCoordinate %v\n", rcc.data) 187 | xpos, ypos, zpos = int64(x), int64(y), int64(z) 188 | } 189 | 190 | type ReportChunkCommand struct { 191 | data []byte // variable 192 | } 193 | 194 | func (rcc ReportChunkCommand) Execute() { 195 | // fmt.Printf("Got chunk length %d, %v\n", len(rcc.data), rcc.data) 196 | // For now, the chunk isn't used. 197 | // fmt.Printf("Got chunk %v\n", rcc.data) 198 | } 199 | 200 | func ParseUint32(b []byte) uint32 { 201 | if len(b) < 4 { 202 | panic("Wrong size") 203 | } 204 | var res uint32 205 | for i := 0; i < 4; i++ { 206 | res |= uint32(b[i]) << (uint(i) * 8) 207 | } 208 | return res 209 | } 210 | -------------------------------------------------------------------------------- /src/cmd/server/player.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "chunkdb" 22 | "ephenationdb" 23 | "fmt" 24 | "keys" 25 | "labix.org/v2/mgo/bson" 26 | "log" 27 | "math" 28 | "math/rand" 29 | "score" 30 | "strconv" 31 | "strings" 32 | "time" 33 | ) 34 | 35 | // Functions in this file are only called from the client process. 36 | 37 | // All this data is saved with the player. Only fields with upper case prefix are saved. 38 | // There may be more data saved in the DB than is shown here. 39 | // TODO: Horisontal direction angle should be 0 radians to the east, as it is for monsters. 40 | type player struct { 41 | ZSpeed float64 // Upward movement speed 42 | Coord user_coord // The current player feet coordinates 43 | Flying bool // True if player is moving without any gravity. 44 | Climbing bool // True if player is on a ladder. It is similar to the flyng mode. 45 | Dead bool // True if the player is dead. 46 | WeaponGrade uint8 // 0 is no weapon, higher is better. 47 | ArmorGrade uint8 // 0 is no armor, higher is better. 48 | HelmetGrade uint8 // 0 is no helmet, higher is better. 49 | WeaponLvl uint32 // The level where the weapon was found. 50 | ArmorLvl uint32 // The level where the armor was found. 51 | HelmetLvl uint32 // The level where the helmet was found. 52 | DirHor float32 // The horistonal direction the player is looking at. 0 radians means looking to the north. This is controlled by the client. 53 | DirVert float32 // The vertical direction the player is looking at. 0 radians means horisontal This is controlled by the client. 54 | Level uint32 // The player level 55 | Exp float32 // Experience points, from 0-1. At 1.0, the next level is reached 56 | HitPoints float32 // Player hit points, 0-1. 1 is full hp, 0 is dead. 57 | Mana float32 // Player mana, 0-1. 58 | NumKill uint32 // Total number of monster kills. Just for fun. 59 | HomeSP user_coord // Your home spawn, if any. 60 | ReviveSP user_coord // This is where you come when you die. 61 | TargetCoor user_coord // Used for targeting mechanisms 62 | Territory []chunkdb.CC // The chunks allocated for this player. 63 | Listeners []uint32 // The list of people listening on this player logging in and out. 64 | Maxchunks int // Max number of chunks this player can own. 65 | BlockAdd uint32 // Blocks added by this player 66 | BlockRem uint32 // Blocks removed by this player 67 | TimeOnline uint32 // Total time online in seconds 68 | Head uint16 // Head type 69 | Body uint16 // Body type 70 | Keys keys.KeyRing // The list of keys that the player has 71 | Lastseen time.Time // When player weas last seen in the game 72 | Inventory PlayerInv 73 | } 74 | 75 | func (up *user) String() string { 76 | return fmt.Sprintf("[%s %v]", up.Name, up.Coord) 77 | } 78 | 79 | // Return true if ok 80 | func (up *user) Load_WLwBlWLc(email string) bool { 81 | // Connect to database 82 | db := ephenationdb.New() 83 | if db == nil { 84 | log.Println("No DB cpnnection", email) 85 | return false 86 | } 87 | err := db.C("avatars").Find(bson.M{"email": email}).One(&up.UserLoad) 88 | if err != nil { 89 | log.Println("Avatar for", email, err) 90 | return false 91 | } 92 | 93 | // Some post processing 94 | 95 | if up.Maxchunks == 0 { 96 | // This parameter was not initialized. 97 | up.Maxchunks = CnfgMaxOwnChunk 98 | } 99 | 100 | up.logonTimer = time.Now() 101 | 102 | if up.ReviveSP.X == 0 && up.ReviveSP.Y == 0 && up.ReviveSP.Z == 0 { 103 | // Check if there is any spawn point defined. 104 | up.ReviveSP = up.Coord 105 | up.HomeSP = up.Coord 106 | } 107 | 108 | score.Initialize(up.Id) 109 | return true 110 | } 111 | 112 | func Q(b bool) int { 113 | if b { 114 | return 1 115 | } 116 | return 0 117 | } 118 | 119 | // Return true if the save succeeded. 120 | func (up *user) Save_Bl() bool { 121 | start := time.Now() 122 | up.Lastseen = start // Update last seen online 123 | up.TimeOnline += uint32(start.Sub(up.logonTimer) / time.Second) // Update total time online, in seconds 124 | up.logonTimer = start 125 | db := ephenationdb.New() 126 | err := db.C("avatars").UpdateId(up.Id, bson.M{"$set": &up.player}) // Only update the fields found in 'pl'. 127 | 128 | if err != nil { 129 | log.Println("Save", up.Name, err) 130 | log.Printf("%#v\n", up) 131 | return false 132 | } 133 | 134 | if *verboseFlag > 1 { 135 | log.Printf("up.Save_Bl saved %v\n", up.Name) 136 | } 137 | elapsed := time.Now().Sub(start) 138 | if *verboseFlag > 0 { 139 | log.Printf("up.Save_Bl elapsed %d ms\n", elapsed/1e6) 140 | } 141 | return true 142 | } 143 | 144 | // This is mostly used for testing, and not part of the regular functionality. 145 | func (up *user) New_WLwWLc(name string) { 146 | // All players get a new struct, so there is no need to initialize 0 items. 147 | up.Name = name 148 | up.HitPoints = 1 149 | up.Mana = 1 150 | // Find ground for the player. Start from top of world and go down one block at a time 151 | // TODO: the dependencies on chunks must not be done in this process. 152 | // Players with the names "test" and a number are reserved names for testing. The number is 153 | // used to dsitribute the player. 154 | var x, y float64 = 0, 0 155 | 156 | if NameIsTestPlayer(name) { 157 | // Special handling if it is a test player. These are not saved, and the starting placed is randomized 158 | // but with a even distribution. 159 | num, _ := strconv.ParseFloat(name[len(CnfgTestPlayerNamePrefix):], 64) // ignore the error, not critical 160 | radie := math.Sqrt(num/CnfgTestPlayersPerChunk) * CHUNK_SIZE 161 | a, b := math.Sincos(rand.Float64() * math.Pi * 2) 162 | x = b * radie 163 | y = a * radie 164 | up.Id = OWNER_TEST - uint32(num) 165 | // fmt.Printf("Test prefix, substr: %v, x,y : (%d,%d)\n", name[len(TestPlayerNamePrefix):], x, y) 166 | } 167 | 168 | coord := user_coord{x, y, FLOATING_ISLANDS_LIM - 1} // Try this 169 | 170 | for ; coord.Z >= 0; coord.Z -= 1 { 171 | if !blockIsPermeable[DBGetBlockCached_WLwWLc(coord)] { 172 | break 173 | } 174 | } 175 | coord.Z += 1 176 | 177 | up.Coord = coord 178 | up.ReviveSP = coord 179 | // log.Printf("player.New %s at coord %v\n", pl.Name, pl.Coord) 180 | 181 | return 182 | } 183 | 184 | func NameIsTestPlayer(name string) bool { 185 | return strings.HasPrefix(name, CnfgTestPlayerNamePrefix) 186 | } 187 | -------------------------------------------------------------------------------- /src/cmd/server/pheenadv.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "chunkdb" 22 | "ephenationdb" 23 | "flag" 24 | "fmt" 25 | "github.com/larspensjo/config" 26 | "io/ioutil" 27 | "labix.org/v2/mgo" 28 | "labix.org/v2/mgo/bson" 29 | "license" 30 | "log" 31 | "math/rand" 32 | "os" 33 | "runtime" 34 | "runtime/pprof" 35 | "strconv" 36 | "strings" 37 | "superchunk" 38 | "time" 39 | "traffic" 40 | ) 41 | 42 | var ( 43 | tflag = flag.Bool("dotest", false, "Run the test suite and then terminate.") 44 | procFlag = flag.Int("p", 2, "Number of processes to use") 45 | ipPort = flag.String("i", ":57862", "IP port to listen on") 46 | logFileName = flag.String("log", "worldserver.log", "Log file name") 47 | allowTestUser = flag.Bool("testuser", false, "Allow connection of testusers without password named 'testX', where X is a number") 48 | verboseFlag = flag.Int("v", 0, "Verbose, Higher number gives more") 49 | cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") 50 | convertChunkFiles = flag.Bool("convertChunk", false, "Convert chunk files to new file format") 51 | welcomeMsgFile = flag.String("welcome", "welcome.txt", "The file that is displayed at login") 52 | logOnStdout = flag.Bool("s", false, "Send log file to standard otput") 53 | inhibitCreateChunks = flag.Bool("nocreate", false, "Only load modified chunks, and save no changes") 54 | configFileName = flag.String("configfile", "config.ini", "General configuration file") 55 | createuser = flag.String("createuser", "", "Create user from argument 'email,password,avatar'") 56 | bootDate = time.Now() 57 | 58 | trafficStatistics = traffic.New() 59 | superChunkManager = superchunk.New(CnfgSuperChunkFolder) 60 | encryptionSalt = "" 61 | ) 62 | 63 | func main() { 64 | flag.Parse() 65 | 66 | if !*logOnStdout { 67 | logFile, _ := os.OpenFile(*logFileName, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) 68 | log.SetOutput(logFile) 69 | } 70 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) 71 | 72 | cnfg, err := config.ReadDefault(*configFileName) 73 | if err != nil { 74 | log.Println("Fail to find", *configFileName, err) 75 | return 76 | } 77 | configSection := "db" 78 | if cnfg.HasSection(configSection) { 79 | f := func(key string) string { 80 | value, err := cnfg.String(configSection, key) 81 | if err != nil { 82 | log.Println("Config file", *configFileName, "Failt to find key", key, err) 83 | return "" 84 | } 85 | return value 86 | } 87 | err = ephenationdb.SetConnection(f) 88 | if err != nil { 89 | log.Println("main: open DB:", err) 90 | // Continue without DB. Only test users can connect. 91 | } 92 | } else { 93 | log.Println("Config file", *configFileName, "missing section", configSection) 94 | } 95 | if encryptionSalt, err = cnfg.String("login", "salt"); err != nil { 96 | encryptionSalt = "" // Effectively no salt 97 | } 98 | 99 | if *createuser != "" { 100 | CreateUser(*createuser) 101 | return 102 | } 103 | 104 | if *convertChunkFiles { 105 | ConvertFiles() 106 | return 107 | } 108 | if *cpuprofile != "" { 109 | f, err := os.Create(*cpuprofile) 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | pprof.StartCPUProfile(f) 114 | defer pprof.StopCPUProfile() // Also done from special command /shutdown 115 | } 116 | if *tflag { 117 | DoTest() 118 | return 119 | } 120 | log.Printf("Pheenadv world server\n") 121 | if *verboseFlag > 0 { 122 | log.Printf("Verbose flag set to %d\n", *verboseFlag) 123 | } 124 | if *inhibitCreateChunks { 125 | log.Println("No chunks will be created or saved") 126 | } 127 | runtime.GOMAXPROCS(*procFlag) 128 | rand.Seed(time.Now().UnixNano()) 129 | host, err := os.Hostname() 130 | if err != nil { 131 | panic(err) 132 | } 133 | log.Printf("Start world server on %s\n", host) 134 | if *allowTestUser { 135 | log.Printf("Testusers without password allowed\n") 136 | } 137 | err = SetupListenForClients_WLuBlWLqWLa(*ipPort) 138 | if err != nil { 139 | log.Printf("%v, server abort\n", err) 140 | os.Exit(1) 141 | } 142 | go ProcAutosave_RLu() 143 | go ProcPurgeOldChunks_WLw() 144 | go CatchSig() 145 | ManageMonsters_WLwWLuWLqWLmBlWLc() // Will not return 146 | } 147 | 148 | // Read all chunks, update them, and write them back again 149 | // This function can also be used for convert from one chunk file format to another, with some tweaking. 150 | func ConvertFiles() { 151 | dir, err := ioutil.ReadDir(CnfgChunkFolder) 152 | if err != nil { 153 | fmt.Printf("Failed to read . (%v)", err) 154 | return 155 | } 156 | var mod, unmod int 157 | for _, fi := range dir { 158 | fn := fi.Name() 159 | // fmt.Printf("%v ", fn) 160 | coords := strings.Split(fn, ",") 161 | if len(coords) != 3 { 162 | fmt.Printf("Skipping %v, bad file name for chunk\n", fn) 163 | continue 164 | } 165 | x, err := strconv.Atoi(coords[0]) 166 | if err != nil { 167 | fmt.Printf("Chunk %v bad file name\n", fn) 168 | continue 169 | } 170 | y, err := strconv.Atoi(coords[1]) 171 | if err != nil { 172 | fmt.Printf("Chunk %v bad file name\n", fn) 173 | continue 174 | } 175 | z, err := strconv.Atoi(coords[2]) 176 | if err != nil { 177 | fmt.Printf("Chunk %v bad file name\n", fn) 178 | continue 179 | } 180 | c := chunkdb.CC{X: int32(x), Y: int32(y), Z: int32(z)} 181 | ch := dBFindChunkFromFS(c) 182 | if ch.flag&CHF_MODIFIED != 0 { 183 | mod++ 184 | } else { 185 | unmod++ 186 | name := DBChunkFileName(c) 187 | err = os.Remove(name) 188 | if err != nil { 189 | fmt.Printf("Failed to remove unmodified file %v, err %v\n", fn, err) 190 | } 191 | } 192 | } 193 | fmt.Printf("%d Modified, %d non modified\n", mod, unmod) 194 | } 195 | 196 | // Helper function to create a user (license) and an avatar for that user 197 | func CreateUser(str string) { 198 | args := strings.Split(str, ",") 199 | if len(args) != 3 && len(args) != 4 { 200 | fmt.Println("Usage: server -createuser=email,password,avatar[,licensekey]") 201 | return 202 | } 203 | var up user 204 | up.New_WLwWLc(args[2]) 205 | up.Email = args[0] 206 | up.License, up.Password = license.Make(args[1], "") 207 | up.License = args[3] // Override 208 | c := ephenationdb.New().C("counters") 209 | var id struct { 210 | C uint32 211 | } 212 | change := mgo.Change{ 213 | Update: bson.M{"$inc": bson.M{"c": 1}}, 214 | } 215 | _, err := c.FindId("avatarId").Apply(change, &id) 216 | if err != nil { 217 | fmt.Println("Failed to update unique counter 'avatarId' in collection 'counter'", err) 218 | return 219 | } 220 | up.Id = id.C 221 | db := ephenationdb.New() 222 | err = db.C("avatars").Insert(&up) 223 | if err != nil { 224 | log.Println("Save", up.Name, err) 225 | return 226 | } 227 | fmt.Println("Created avatar number", up.Id, ":", up.Name) 228 | } 229 | -------------------------------------------------------------------------------- /src/superchunk/superchunk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package superchunk 19 | 20 | // This is a package that will maintain teleport locations. 21 | // The can be at most one TP for each chunk. 22 | // Data will be saved into the local file system. To optimize storage, information about 23 | // 10x10x10 chunks are stored in each file. 24 | 25 | import ( 26 | . "chunkdb" 27 | "fmt" 28 | sync "github.com/larspensjo/Go-sync-evaluation/evalsync" 29 | "io" 30 | "io/ioutil" 31 | "log" 32 | "os" 33 | ) 34 | 35 | const ( 36 | SCH_SIZE = 10 // This cannot be changed afterwards, if there are stored super chunks. 37 | ) 38 | 39 | // Definition of bits in the flag field 40 | const ( 41 | flagTPDefined uint8 = 0x01 42 | ) 43 | 44 | type superChunkManager struct { 45 | loaded map[CC]*superChunk // All superChunks loaded in memory 46 | lock sync.RWMutex // A RWMutex to protect against simultaneous access 47 | subFolder string 48 | } 49 | 50 | // Make sure the required sub folder exists 51 | func New(subFolder string) *superChunkManager { 52 | stat, err := os.Stat(subFolder) 53 | if err != nil { 54 | os.Mkdir(subFolder, 0777) 55 | } else if !stat.Mode().IsDir() { 56 | log.Panicln("Bad folder", subFolder) 57 | return nil 58 | } 59 | return &superChunkManager{loaded: make(map[CC]*superChunk), subFolder: subFolder} 60 | } 61 | 62 | // Information about one specific chunk 63 | type chunkData struct { 64 | flag uint8 65 | x, y, z uint8 66 | } 67 | 68 | // This type is what will be saved to file. The array is a 3D sub set of chunks. The chunk at 0,0,0 is always one with 69 | // the chunk coordinate rounded down to a near multiple of SCH_SIZE. 70 | type superChunk struct { 71 | checksum uint32 // A simplified value, which is really a counter that change everytime there is a change 72 | chunk [SCH_SIZE][SCH_SIZE][SCH_SIZE]chunkData 73 | } 74 | 75 | // The file name used for storing a superChunk. 76 | func (scm *superChunkManager) functionName(cc *CC) string { 77 | return fmt.Sprintf("%s/s%d,%d,%d", scm.subFolder, cc.X, cc.Y, cc.Z) 78 | } 79 | 80 | // The chunk coordinate is expected to be truncated to nearest super chunk address 81 | func (scm *superChunkManager) load(cc *CC) *superChunk { 82 | fn := scm.functionName(cc) 83 | d, err := ioutil.ReadFile(fn) 84 | if err != nil { 85 | // No file found 86 | return nil 87 | } 88 | if len(d) != 4+SCH_SIZE*SCH_SIZE*SCH_SIZE*4 { 89 | log.Println("Bad file size ", len(d)) 90 | return nil 91 | } 92 | var sc superChunk 93 | sc.checksum = uint32(d[0]) + uint32(d[1])<<8 + uint32(d[2])<<16 + uint32(d[3])<<24 94 | ind := 4 95 | for x := 0; x < SCH_SIZE; x++ { 96 | for y := 0; y < SCH_SIZE; y++ { 97 | for z := 0; z < SCH_SIZE; z++ { 98 | sc.chunk[x][y][z].flag = d[ind] 99 | ind++ 100 | sc.chunk[x][y][z].x = d[ind] 101 | ind++ 102 | sc.chunk[x][y][z].y = d[ind] 103 | ind++ 104 | sc.chunk[x][y][z].z = d[ind] 105 | ind++ 106 | } 107 | } 108 | } 109 | return &sc 110 | } 111 | 112 | // Do the actual writing of data 113 | func (sc *superChunk) write(writer io.Writer) { 114 | var d [4]byte 115 | d[0] = uint8(sc.checksum) 116 | d[1] = uint8(sc.checksum >> 8) 117 | d[2] = uint8(sc.checksum >> 16) 118 | d[3] = uint8(sc.checksum >> 24) 119 | writer.Write(d[0:4]) 120 | for x := 0; x < SCH_SIZE; x++ { 121 | for y := 0; y < SCH_SIZE; y++ { 122 | for z := 0; z < SCH_SIZE; z++ { 123 | d[0] = sc.chunk[x][y][z].flag 124 | d[1] = sc.chunk[x][y][z].x 125 | d[2] = sc.chunk[x][y][z].y 126 | d[3] = sc.chunk[x][y][z].z 127 | writer.Write(d[0:4]) 128 | } 129 | } 130 | } 131 | } 132 | 133 | // Save a super chunk 134 | func (scm *superChunkManager) save(cc *CC, sc *superChunk) { 135 | fn := scm.functionName(cc) 136 | file, err := os.Create(fn) 137 | if err != nil { 138 | log.Println("Failed to create", fn) 139 | return 140 | } 141 | sc.write(file) 142 | file.Close() 143 | } 144 | 145 | // Round a number down to nearest multiple of SCH_SIZE 146 | func trunc(a int32) int32 { 147 | if a < 0 { 148 | a -= SCH_SIZE - 1 149 | } 150 | return (a / SCH_SIZE) * SCH_SIZE 151 | } 152 | 153 | // Find the specified superchunk. The chunk coordinate must be a base coordinate. 154 | // It will always succeed, and create a new super chunk if it didn't exist. 155 | func (scm *superChunkManager) findSuperChunk(cc *CC) *superChunk { 156 | // Look for it in memory 157 | scm.lock.RLock() 158 | sc := scm.loaded[*cc] 159 | scm.lock.RUnlock() 160 | 161 | if sc != nil { 162 | // log.Println("Found", *cc) 163 | return sc 164 | } 165 | 166 | // It wasn't loaded into memory, try from file 167 | // There is a small chance this is done at the same time from several places. Worst case 168 | // result will be that the same file will be loaded twice. 169 | sc = scm.load(cc) 170 | 171 | // Not found, create one 172 | if sc == nil { 173 | log.Println("Creating new", cc) 174 | sc = new(superChunk) 175 | scm.lock.Lock() 176 | sc2 := scm.loaded[*cc] 177 | // Safety test, someone else may have created this superchunk before it was locked 178 | if sc2 != nil { 179 | sc = sc2 180 | } else { 181 | scm.loaded[*cc] = sc 182 | } 183 | scm.lock.Unlock() 184 | } 185 | 186 | // log.Println(*cc) 187 | scm.lock.Lock() 188 | scm.loaded[*cc] = sc 189 | scm.lock.Unlock() 190 | 191 | return sc 192 | } 193 | 194 | // Get the the teleport coordinate for the specified chunk coord. 195 | // 4:th argument is true iff there is one 196 | func (scm *superChunkManager) GetTeleport(cc *CC) (uint8, uint8, uint8, bool) { 197 | ccBase := CC{trunc(cc.X), trunc(cc.Y), trunc(cc.Z)} 198 | sc := scm.findSuperChunk(&ccBase) 199 | ch := &sc.chunk[cc.X-ccBase.X][cc.Y-ccBase.Y][cc.Z-ccBase.Z] 200 | return ch.x, ch.y, ch.z, ch.flag&flagTPDefined != 0 201 | } 202 | 203 | // For Chunk 'cc', set the teleport localtion to x,y,z. 204 | func (scm *superChunkManager) SetTeleport(cc *CC, x, y, z uint8) { 205 | ccBase := CC{trunc(cc.X), trunc(cc.Y), trunc(cc.Z)} 206 | sc := scm.findSuperChunk(&ccBase) 207 | ch := &sc.chunk[cc.X-ccBase.X][cc.Y-ccBase.Y][cc.Z-ccBase.Z] 208 | ch.flag |= flagTPDefined 209 | ch.x = x 210 | ch.y = y 211 | ch.z = z 212 | log.Println("SetTeleport", ccBase, ch) 213 | scm.lock.Lock() 214 | sc.checksum++ 215 | scm.save(&ccBase, sc) 216 | scm.lock.Unlock() 217 | } 218 | 219 | func (scm *superChunkManager) RemoveTeleport(cc *CC) { 220 | ccBase := CC{trunc(cc.X), trunc(cc.Y), trunc(cc.Z)} 221 | sc := scm.findSuperChunk(&ccBase) 222 | ch := &sc.chunk[cc.X-ccBase.X][cc.Y-ccBase.Y][cc.Z-ccBase.Z] 223 | ch.flag &= ^flagTPDefined 224 | ch.x = 0 225 | ch.y = 0 226 | ch.z = 0 227 | log.Println("RemoveTeleport", ccBase, cc) 228 | scm.lock.Lock() 229 | sc.checksum++ 230 | scm.save(&ccBase, sc) 231 | scm.lock.Unlock() 232 | } 233 | 234 | // Write the base Chunk Coord address, and then the super chunk 235 | func (scm *superChunkManager) Write(writer io.Writer, cc *CC) { 236 | ccBase := CC{trunc(cc.X), trunc(cc.Y), trunc(cc.Z)} 237 | sc := scm.findSuperChunk(&ccBase) 238 | // This is done without a lock. There is a risk for asynchronous changes, which are accepted. 239 | // log.Println("Sending", ccBase) 240 | var msg [3]byte 241 | msg[0] = byte(ccBase.X & 0xFF) 242 | msg[1] = byte(ccBase.Y & 0xFF) 243 | msg[2] = byte(ccBase.Z & 0xFF) 244 | writer.Write(msg[:]) 245 | sc.write(writer) 246 | } 247 | 248 | // Verify that the checksum is correct 249 | func (scm *superChunkManager) VerifyChecksum(cc *CC, checksum uint32) bool { 250 | ccBase := CC{trunc(cc.X), trunc(cc.Y), trunc(cc.Z)} 251 | sc := scm.findSuperChunk(&ccBase) 252 | // log.Println("VerifyChecksum for", cc, sc.checksum, checksum) 253 | return sc.checksum == checksum 254 | } 255 | 256 | func (scm *superChunkManager) Size() int { 257 | return len(scm.loaded) 258 | } 259 | -------------------------------------------------------------------------------- /src/cmd/server/combat.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | // 21 | // This file handles all combat algorithms 22 | // 23 | 24 | import ( 25 | "client_prot" 26 | "math" 27 | // "fmt" 28 | ) 29 | 30 | const ( 31 | // Two constants the determnine how much damage is done depending on level difference. The current 32 | // tuning is set so as to increase a 30% more damage when opponent is decreased with one level. 33 | COMB_NumberOfHitsToKill = 10 34 | COMB_KillAdjust = 0.262 // See https://docs.google.com/spreadsheet/ccc?key=0AnhOqCG9cmlvdHdlWmRDYkJRcklwOVZGNDh1alVIa3c 35 | // Three constants that determine the amount of experience points for a kill, depending on level difference 36 | COMB_ExpLevelOffset = -0.7 // Used for sigmoid 37 | COMB_ExpMultiplier = 1.5 // Multiplicative factor high lvl monster 38 | COMB_ExpForOneMonster = 0.0083 // Amount of exp for killing a monster at the same level 39 | COMB_MonsterLevelFreeZoneHor = 32 // All monsters spawn within this horisontal distance will have level 0 40 | COMB_MonsterLevelVertFactor = 3 // The vertical difficulty grows this times faster than the horisontal 41 | COMB_MonsterLevelGrowth = 32 // Number of blocks you have to walk to meet monsters at the next level. 42 | COMB_MonsterVsPlayerFact = 2 // Damage multiplier, used inverted in both directions 43 | COMB_EasyMonsterLevel = 10 // The monsters are easier below this level. 44 | COMB_AggresiveLevelStart = 5 // This is the level where monsters will start to get aggresive 45 | // If every armor item would give the same multiplier modifier as a weapon, then a list of top grade equipments 46 | // would totally dominate. A calibration constant is used to compensate for this. See computation at 47 | // https://docs.google.com/spreadsheet/ccc?key=0AnhOqCG9cmlvdHdlWmRDYkJRcklwOVZGNDh1alVIa3c&hl=en_US#gid=3 48 | COMB_ArmorModifierCal = 0.282 49 | ) 50 | 51 | var ( 52 | // Amount of experience for killing a monster at the same level as the player 53 | combatExperienceSameLevel = ExperienceForKill(0, 0) 54 | ) 55 | 56 | // The difficult of a monster shall depend on the distance to a starting line. 57 | func MonsterDifficulty(uc *user_coord) uint32 { 58 | dist := math.Abs(uc.Y) + math.Abs(uc.Z)*COMB_MonsterLevelVertFactor - COMB_MonsterLevelFreeZoneHor 59 | if dist <= 0 { 60 | return 0 61 | } 62 | return uint32(dist / COMB_MonsterLevelGrowth) 63 | } 64 | 65 | // The factor between monsters and players shall normally be COMB_MonsterVsPlayerFact, but more exaggerated for low level monsters. 66 | // The purpose is to make it easy for beginners. 67 | func MonsterVsPlayerFactor(level uint32) float32 { 68 | diff := COMB_EasyMonsterLevel - float32(level) 69 | if diff < 0 { 70 | diff = 0 71 | } 72 | return COMB_MonsterVsPlayerFact + float32(COMB_MonsterVsPlayerFact)/COMB_EasyMonsterLevel*diff 73 | } 74 | 75 | // The player is hit by a monster with the attributes as specified by the arguments 76 | func (up *user) Hit(monster uint32, level uint32, weaponDmg float32, weaponLvl uint32) { 77 | dmg := weaponDmg * 78 | PlayerLevelDiffMultiplier(up.Level, level) * 79 | WeaponLevelDiffMultiplier(level, weaponLvl, 1) / 80 | ArmorLevelDiffMultiplier(up.Level, up.ArmorLvl, up.ArmorGrade) / 81 | ArmorLevelDiffMultiplier(up.Level, up.HelmetLvl, up.HelmetGrade) / 82 | MonsterVsPlayerFactor(level) 83 | if dmg > 1 { 84 | dmg = 1 85 | } 86 | 87 | // This shouldn't be updated in the current process 88 | f := func(up *user) { 89 | up.Lock() 90 | up.HitPoints -= dmg 91 | up.updatedStats = true // This leads to a message being generated. 92 | if up.HitPoints <= 0 { 93 | up.HitPoints = 0 94 | up.Dead = true 95 | up.Unlock() // Must unlock before calling a function that can block 96 | } else { 97 | up.Unlock() 98 | } 99 | cp := ChunkFindCached_WLwWLc(up.Coord.GetChunkCoord()) 100 | owner := cp.owner 101 | if owner != up.Id && owner != OWNER_NONE && owner != OWNER_RESERVED && owner != OWNER_TEST { 102 | up.AddScore(owner, float64(dmg)*CnfgScoreDamageFact) 103 | } 104 | var b [8]byte 105 | b[0] = 8 106 | b[1] = 0 107 | b[2] = client_prot.CMD_RESP_PLAYER_HIT_BY_MONSTER 108 | EncodeUint32(monster, b[3:7]) 109 | b[7] = byte(dmg*255 + 0.5) 110 | up.writeBlocking_Bl(b[:]) 111 | } 112 | up.SendCommand(f) 113 | } 114 | 115 | // The monster is hit by a player with the attributes as specified by the arguments 116 | func (mp *monster) Hit_WLuBl(up *user, weaponDmg float32) { 117 | dmg := weaponDmg * PlayerLevelDiffMultiplier(mp.Level, up.Level) * WeaponLevelDiffMultiplier(up.Level, up.WeaponLvl, up.WeaponGrade) * MonsterVsPlayerFactor(mp.Level) 118 | if dmg > 1 { 119 | dmg = 1 120 | } 121 | mp.HitPoints -= dmg 122 | mp.updatedStats = true 123 | if mp.HitPoints <= 0 { 124 | pLevel := up.Level 125 | mp.HitPoints = 0 126 | mp.dead = true 127 | experience := ExperienceForKill(pLevel, mp.Level) 128 | up.Lock() 129 | up.flags &= ^client_prot.UserFlagInFight 130 | up.NumKill++ 131 | // Give more experience to low level players 132 | switch up.Level { 133 | case 0: 134 | experience *= 5 135 | case 1: 136 | experience *= 2.5 137 | case 2: 138 | experience *= 1.5 139 | } 140 | up.AddExperience(experience) // Must be locked 141 | up.Unlock() 142 | up.MonsterDropWLu(combatExperienceSameLevel / experience) // Adjust probability, relative 143 | // fmt.Printf("mp.Hit %#v\n", *mp) 144 | } 145 | var b [8]byte 146 | b[0] = 8 147 | b[1] = 0 148 | b[2] = client_prot.CMD_RESP_PLAYER_HIT_MONSTER 149 | EncodeUint32(mp.id, b[3:7]) 150 | b[7] = byte(dmg*255 + 0.5) 151 | up.writeBlocking_Bl(b[:]) 152 | } 153 | 154 | // Compare levels l1 and l2 of two fighters, and return a multiplier used in combat. 155 | // Return a multiplier between 0 and 1. 156 | func PlayerLevelDiffMultiplier(l1, l2 uint32) float32 { 157 | return float32(math.Exp(COMB_KillAdjust*(float64(l2)-float64(l1))) / COMB_NumberOfHitsToKill) 158 | } 159 | 160 | // Compensate for a fighter at level pLevel using a weapon at level wLevel. 161 | // The principle is that a weapon at wrong level will give a penalty. 162 | // Return a multiplier near 1. The same algorithm is used by the client in the inventory 163 | // screen. If it is changed here, make sure it is also changed in the client. 164 | // See https://docs.google.com/drawings/d/1cmObDuDgBvYhpsQagfjHPlckXjluU3CR9utVkTSh_rE/edit?hl=en_US 165 | func WeaponLevelDiffMultiplier(pLevel, wLevel uint32, wepType uint8) (ret float32) { 166 | var detract uint8 167 | if pLevel > wLevel+uint32(wepType) { 168 | detract = uint8(pLevel-wLevel) - wepType // This will be greater than zero 169 | } 170 | if pLevel+uint32(wepType) < wLevel { 171 | detract = uint8(wLevel-pLevel) - wepType // This will be greater than zero 172 | } 173 | if detract > wepType { 174 | detract = wepType 175 | } 176 | // fmt.Println("WeaponLevelDiffMultiplier: detract ", detract) 177 | wepType -= detract 178 | switch wepType { 179 | case 0: 180 | ret = 0.9 181 | case 1: 182 | ret = 1.0 183 | case 2: 184 | ret = 1.1 185 | case 3: 186 | ret = 1.2 187 | case 4: 188 | ret = 1.3 189 | default: 190 | ret = 1.0 // Shouldn't happen 191 | } 192 | return 193 | } 194 | 195 | // Compensate for a fighter at level 'pLevel' using an armor at level 'aLevel'. This is also used 196 | // for helmets. 197 | // The same principle (but not same value) is used as for a weapon, see above. The damage is divided by this factor. 198 | func ArmorLevelDiffMultiplier(pLevel, aLevel uint32, armType uint8) float32 { 199 | var detract uint8 200 | if pLevel > aLevel+uint32(armType) { 201 | detract = uint8(pLevel-aLevel) - armType // This will be greater than zero 202 | } 203 | if pLevel+uint32(armType) < aLevel { 204 | detract = uint8(aLevel-pLevel) - armType // This will be greater than zero 205 | } 206 | if detract > armType { 207 | detract = armType 208 | } 209 | // fmt.Println("WeaponLevelDiffMultiplier: detract ", detract) 210 | armType -= detract 211 | modifier := float32(1.0) // Default value 212 | switch armType { 213 | case 0: 214 | modifier = 0.9 215 | case 1: 216 | modifier = 1.0 217 | case 2: 218 | modifier = 1.1 219 | case 3: 220 | modifier = 1.2 221 | case 4: 222 | modifier = 1.3 223 | } 224 | return (modifier-1)*COMB_ArmorModifierCal + 1 225 | } 226 | 227 | // Calculate the experience for player 'pl' killing a monster at level 'level' 228 | func ExperienceForKill(pLevel, mLevel uint32) float32 { 229 | return COMB_ExpForOneMonster * float32(COMB_ExpMultiplier/(1+math.Exp(float64(pLevel)-float64(mLevel)+COMB_ExpLevelOffset))) 230 | } 231 | -------------------------------------------------------------------------------- /src/cmd/server/objects.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | // 19 | // Define objects and inventory 20 | // 21 | 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "io" 27 | "log" 28 | ) 29 | 30 | type ObjectCode string 31 | 32 | type Object struct { 33 | Type ObjectCode 34 | Level uint32 35 | Count uint32 36 | } 37 | 38 | type PlayerInv []Object // The slize of objects 39 | 40 | const ( 41 | ItemHealthPotionID ObjectCode = "POTH" 42 | ItemManaPotionID = "POTM" 43 | ItemWpnHandsID = "WEP0" 44 | ItemWeapon1ID = "WEP1" 45 | ItemWeapon2ID = "WEP2" 46 | ItemWeapon3ID = "WEP3" 47 | ItemWeapon4ID = "WEP4" 48 | ItemArmorNoneId = "ARM0" 49 | ItemArmor1ID = "ARM1" 50 | ItemArmor2ID = "ARM2" 51 | ItemArmor3ID = "ARM3" 52 | ItemArmor4ID = "ARM4" 53 | ItemHelmetNoneId = "HLM0" 54 | ItemHelmet1ID = "HLM1" 55 | ItemHelmet2ID = "HLM2" 56 | ItemHelmet3ID = "HLM3" 57 | ItemHelmet4ID = "HLM4" 58 | ItemScrollRessID = "S001" // A resurrection scroll 59 | ) 60 | 61 | var ( 62 | // A static list of all objects available. Every object is identified by a unique 4 character string. 63 | objectUseTable = map[ObjectCode]func(up *user, t ObjectCode, level uint32) (bool, bool){ 64 | ItemHealthPotionID: UsePotion_Wlu, 65 | "POTM": UsePotion_Wlu, 66 | "WEP0": UseWeapon_Wlu, 67 | "WEP1": UseWeapon_Wlu, 68 | "WEP2": UseWeapon_Wlu, 69 | "WEP3": UseWeapon_Wlu, 70 | "WEP4": UseWeapon_Wlu, 71 | "ARM0": UseArmor_Wlu, 72 | "ARM1": UseArmor_Wlu, 73 | "ARM2": UseArmor_Wlu, 74 | "ARM3": UseArmor_Wlu, 75 | "ARM4": UseArmor_Wlu, 76 | "HLM0": UseHelmet_Wlu, 77 | "HLM1": UseHelmet_Wlu, 78 | "HLM2": UseHelmet_Wlu, 79 | "HLM3": UseHelmet_Wlu, 80 | "HLM4": UseHelmet_Wlu, 81 | "S001": UseScroll_Wlu, // A resurrection scroll 82 | } 83 | ) 84 | 85 | // Get the index of an item 86 | func (inv *PlayerInv) Find(t ObjectCode, level uint32) int { 87 | for i, obj := range *inv { 88 | if obj.Type == t && obj.Level == level { 89 | return i 90 | } 91 | } 92 | log.Println("Object", t, "level", level, "not found in", inv) 93 | return -1 94 | } 95 | 96 | func (inv *PlayerInv) Clear() { 97 | *inv = nil 98 | } 99 | 100 | func (inv *PlayerInv) AddOneObject(t ObjectCode, level uint32) { 101 | for i, old := range *inv { 102 | if old.Type == t && old.Level == level { 103 | (*inv)[i].Count++ 104 | return 105 | } 106 | } 107 | *inv = append(*inv, Object{Type: t, Level: level, Count: 1}) 108 | } 109 | 110 | // Decrement the counter for one object, and remove it from the list when count is zero 111 | func (inv *PlayerInv) Remove(t ObjectCode, lvl uint32) { 112 | for i, obj := range *inv { 113 | if obj.Type != t || obj.Level != lvl { 114 | continue 115 | } 116 | (*inv)[i].Count-- 117 | if obj.Count == 0 { 118 | inv.RemoveIndex(i) 119 | } 120 | return 121 | } 122 | } 123 | 124 | // Remove inventory entry 'index' completely from the list 125 | func (inv *PlayerInv) RemoveIndex(index int) { 126 | if index > len(*inv) { 127 | log.Panicln("Illegal index", index, "for", inv) 128 | } 129 | last := len(*inv) - 1 130 | (*inv)[index] = (*inv)[last] 131 | *inv = (*inv)[0:last] // Reslice it, to drop last element 132 | return 133 | } 134 | 135 | // Use an object, return true if it was consumed 136 | func (inv *PlayerInv) Use_WluBl(up *user, t ObjectCode, lvl uint32) { 137 | i := inv.Find(t, lvl) 138 | if i == -1 { 139 | up.Printf_Bl("#FAIL") 140 | return // No such object in inventory 141 | } 142 | f := objectUseTable[t] 143 | consumed, broadcast := f(up, t, lvl) 144 | if broadcast { 145 | ReportEquipmentToNear_Bl(up) 146 | } 147 | if consumed { 148 | ReportOneInventoryItem_WluBl(up, t, lvl) 149 | } else { 150 | up.Printf_Bl("#FAIL") 151 | } 152 | } 153 | 154 | // Used for debugging 155 | func (this *PlayerInv) Report(wr io.Writer) { 156 | for _, o := range *this { 157 | fmt.Fprintf(wr, "!%#v", o) 158 | } 159 | return 160 | } 161 | 162 | // Compute the experience point value of dropping an item. The value is normalized, giving 1 for 163 | // an item of the same level as the player and of the lowest grade. 164 | // The type of the item has to be of the form "XXXN". 165 | func ItemValueAsDrop(playerLevel, itemLevel uint32, t ObjectCode) float32 { 166 | if len(t) != 4 || t[3] > '9' || t[3] < '0' { 167 | return 0 168 | } 169 | itemType := t[3] - '0' 170 | diff := 1 + (float32(itemLevel)-float32(playerLevel)+float32(itemType))/2 171 | if diff < 0 { 172 | diff = 0 173 | } else if diff > 3 { 174 | diff = 3 175 | } 176 | return diff 177 | } 178 | 179 | func ConvertWeaponTypeToID(weapongrade uint8) ObjectCode { 180 | code := []byte(ItemWpnHandsID) 181 | code[3] += weapongrade 182 | return ObjectCode(code) 183 | } 184 | 185 | func ConvertArmorTypeToID(armorgrade uint8) ObjectCode { 186 | code := []byte(ItemArmorNoneId) 187 | code[3] += armorgrade 188 | return ObjectCode(code) 189 | } 190 | 191 | func ConvertHelmetTypeToID(helmetgrade uint8) ObjectCode { 192 | code := []byte(ItemHelmetNoneId) 193 | code[3] += helmetgrade 194 | return ObjectCode(code) 195 | } 196 | 197 | // Use a item of type 't' and level 'lvl'. The type can be counted on being 4 characters. 198 | // Return first flag for being consumed, and teh second to broadcast the action to other players 199 | func UsePotion_Wlu(up *user, t ObjectCode, lvl uint32) (consumed, broadcast bool) { 200 | pl := &up.player 201 | if pl.Dead { 202 | return false, false 203 | } 204 | switch t { 205 | case ItemHealthPotionID: 206 | up.Lock() 207 | // TODO: The amount should depend on the level 208 | if up.Heal(0.3, 0) { 209 | pl.Inventory.Remove(ItemHealthPotionID, lvl) 210 | consumed = true 211 | } 212 | up.Unlock() 213 | case ItemManaPotionID: 214 | up.Lock() 215 | // TODO: The amount should depend on the level 216 | if up.AddMana(0.3) { 217 | pl.Inventory.Remove(ItemManaPotionID, lvl) 218 | consumed = true 219 | } 220 | up.Unlock() 221 | } 222 | return 223 | } 224 | 225 | // Use a item of type 't' and level 'lvl'. The type can be counted on being 4 characters. 226 | // Return first flag for being consumed, and teh second to broadcast the action to other players 227 | func UseWeapon_Wlu(up *user, t ObjectCode, lvl uint32) (bool, bool) { 228 | replaced := false 229 | grade := t[3] - '0' 230 | pl := &up.player 231 | up.Lock() 232 | if pl.WeaponLvl+uint32(pl.WeaponGrade) < lvl+uint32(grade) { 233 | // Move the old item back to the inventory 234 | pl.Inventory.AddOneObject(ConvertWeaponTypeToID(pl.WeaponGrade), pl.WeaponLvl) 235 | // Update current item type 236 | pl.WeaponGrade = grade 237 | pl.WeaponLvl = lvl 238 | pl.Inventory.Remove(t, lvl) 239 | replaced = true 240 | } 241 | up.Unlock() 242 | return replaced, replaced 243 | } 244 | 245 | // Use a item of type 't' and level 'lvl'. The type can be counted on being 4 characters. 246 | // Return first flag for being consumed, and teh second to broadcast the action to other players 247 | func UseArmor_Wlu(up *user, t ObjectCode, lvl uint32) (bool, bool) { 248 | replaced := false 249 | grade := t[3] - '0' 250 | pl := &up.player 251 | up.Lock() 252 | if pl.ArmorLvl+uint32(pl.ArmorGrade) < lvl+uint32(grade) { 253 | // Move the old item back to the inventory 254 | pl.Inventory.AddOneObject(ConvertArmorTypeToID(pl.ArmorGrade), pl.ArmorLvl) 255 | // Update current item type 256 | pl.ArmorGrade = grade 257 | pl.ArmorLvl = lvl 258 | pl.Inventory.Remove(t, lvl) 259 | replaced = true 260 | } 261 | up.Unlock() 262 | return replaced, replaced 263 | } 264 | 265 | // Use a item of type 't' and level 'lvl'. The type can be counted on being 4 characters. 266 | // Return first flag for being consumed, and teh second to broadcast the action to other players 267 | func UseScroll_Wlu(up *user, t ObjectCode, lvl uint32) (consumed, broadcast bool) { 268 | pl := &up.player 269 | if pl.Dead { 270 | return 271 | } 272 | switch t { 273 | case ItemScrollRessID: 274 | up.Lock() 275 | pl.ReviveSP = pl.Coord 276 | pl.Inventory.Remove(t, lvl) 277 | up.Unlock() 278 | consumed = true 279 | } 280 | return 281 | } 282 | 283 | // Use a item of type 't' and level 'lvl'. The type can be counted on being 4 characters. 284 | // Return first flag for being consumed, and teh second to broadcast the action to other players 285 | func UseHelmet_Wlu(up *user, t ObjectCode, lvl uint32) (bool, bool) { 286 | replaced := false 287 | grade := t[3] - '0' 288 | pl := &up.player 289 | up.Lock() 290 | if pl.HelmetLvl+uint32(pl.HelmetGrade) < lvl+uint32(grade) { 291 | // Move the old item back to the inventory 292 | pl.Inventory.AddOneObject(ConvertHelmetTypeToID(pl.HelmetGrade), pl.HelmetLvl) 293 | // Update current item type 294 | pl.HelmetGrade = grade 295 | pl.HelmetLvl = lvl 296 | pl.Inventory.Remove(t, lvl) 297 | replaced = true 298 | } 299 | up.Unlock() 300 | return replaced, replaced 301 | } 302 | -------------------------------------------------------------------------------- /src/score/score.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | // 19 | // This package will keep the score of territories. 20 | // 21 | // There are two parallel tables of scores, pointing to the same data. One 22 | // map is used for efficiently mapping from uid to a pointer to the score data, 23 | // and one linear table is used to traverse the complete list for updates and saves. 24 | // The reason for this is that there will be a lot of updates of scores, which has to be 25 | // very efficient. Every such access have to lock the map. Whenever the whole list is 26 | // traversed periodically, the map must not be locked as it takes some time. 27 | // 28 | // Note that the score entry itself is not locked. That means that there is a small chance 29 | // that data can be corrupted. The worst case is a failed update, which is acceptable. 30 | // 31 | // The "BalanceScore" is similar to the total score, but it will decay towards a value greater than 0. 32 | // 33 | package score 34 | 35 | import ( 36 | "chunkdb" 37 | "ephenationdb" 38 | "flag" 39 | "fmt" 40 | "io" 41 | "labix.org/v2/mgo" 42 | "labix.org/v2/mgo/bson" 43 | "launchpad.net/tomb" 44 | "log" 45 | "math" 46 | "sync" 47 | "time" 48 | "timerstats" 49 | ) 50 | 51 | const ( 52 | ConfigScoreUpdatePeriod = 1e11 // The perdiod between updates of the database 53 | ConfigScoreHalfLife = time.Hour * 24 * 30 // The half life off the score decay 54 | ConfigScoreBalHalfLife = time.Hour * 24 * 5 // The half life off the scoreBalance decay 55 | ConfigScoreBalanceZero = 10 // The value that ScoreBalance will decay to, which is not 0 56 | ConfigHandicapLimit = 10 // Any more chunks than this will impose a handicap on the score 57 | ) 58 | 59 | func init() { 60 | go maintainScore() 61 | } 62 | 63 | type territoryScore struct { 64 | Score float64 // Number of seconds players spent in this territory 65 | ScoreBalance float64 // The total score with the payment subtracted 66 | handicap float64 // How much to scale Score with. This is 1 for most players. 67 | TimeStamp time.Time // The score will decay depending on this time 68 | uid uint32 // Owner 69 | modified bool // Has the value changed since last it was saved? 70 | name string // Not really needed, but nice for info 71 | } 72 | 73 | var ( 74 | scores = make(map[uint32]*territoryScore) // This map must always be locked by a mutex. 75 | procStatus tomb.Tomb // Used to monitor the state of the process 76 | mutex sync.RWMutex // Used to protect the map 77 | chScoreList = make(chan *territoryScore, 100) 78 | disableSave = flag.Bool("score.DisableSave", false, "Disable all updates with the database") 79 | ) 80 | 81 | // Helper function. Get a pointer to the score for the specified 'uid'. 82 | func getTerritoryScore(uid uint32) *territoryScore { 83 | mutex.RLock() 84 | ts := scores[uid] // Need a read lock on the map 85 | mutex.RUnlock() 86 | if ts != nil { 87 | return ts 88 | } 89 | 90 | // There was no data yet for 'uid'. 91 | ts = new(territoryScore) 92 | loadFromSQL(ts, uid) 93 | mutex.Lock() 94 | scores[uid] = ts // Use a write lock 95 | mutex.Unlock() 96 | // Also send a copy to the maintenance process 97 | chScoreList <- ts 98 | return ts 99 | } 100 | 101 | // Add points to the Score and BalanceScore. 102 | func Add(uid uint32, points float64) { 103 | ts := getTerritoryScore(uid) 104 | ts.Score += points * ts.handicap 105 | ts.ScoreBalance += points 106 | ts.modified = true 107 | // log.Println("score.Add", uid, points, fact, ts) 108 | } 109 | 110 | // Given the number of chunks for a player, compute a factor used to decrease the player score with 111 | func computeFactor(numChunks int) float64 { 112 | terr := float64(numChunks) 113 | // Any number less than ConfigHandicapLimit shall count as ConfigHandicapLimit. That means that it will only be an effective decrease for 114 | // players with more than ConfigHandicapLimit chunks. 115 | if terr < ConfigHandicapLimit { 116 | terr = ConfigHandicapLimit 117 | } 118 | return ConfigHandicapLimit / terr 119 | } 120 | 121 | // Trig loading of a player 122 | func Initialize(uid uint32) { 123 | getTerritoryScore(uid) 124 | } 125 | 126 | // Pay 'cost' for a reward, and return true if the ScoreBalance was enough. 127 | func Pay(uid uint32, cost float64) bool { 128 | ts := getTerritoryScore(uid) 129 | // As there is no lock, there is a small chance that another call is executed at the same time. 130 | // If this happens, the worst case is that too much payment is accepted. This is unlikely and not fatal 131 | if ts.ScoreBalance < cost { 132 | return false 133 | } 134 | ts.ScoreBalance -= cost 135 | ts.modified = true 136 | return true 137 | } 138 | 139 | // Close the update process, and return the status 140 | func Close() (ret bool) { 141 | // log.Println("score.Close initiating") 142 | procStatus.Kill(nil) 143 | 144 | // Wait for completion 145 | ret = true 146 | if err := procStatus.Wait(); err != nil { 147 | log.Println(err) 148 | ret = false 149 | } 150 | // log.Println("score.Close done") 151 | return 152 | } 153 | 154 | // Debug function 155 | func Report(wr io.Writer) { 156 | now := time.Now() 157 | mutex.RLock() 158 | for uid, ts := range scores { 159 | age := float64(now.Sub(ts.TimeStamp)) / float64(time.Hour) 160 | fmt.Fprintf(wr, "%s (%d) Score %.1f Bal %.1f Hand %.1f (age %.2f hours)", ts.name, uid, ts.Score, ts.ScoreBalance, ts.handicap, age) 161 | if ts.Score > 0.1 { 162 | ts.decay(&now) // Trig a save if there is some score 163 | } 164 | } 165 | mutex.RUnlock() 166 | } 167 | 168 | // Given the time stamp, decay the score 169 | func (ts *territoryScore) decay(now *time.Time) { 170 | // The decay is based on an exponential half time 171 | deltaTime := float64(now.Sub(ts.TimeStamp)) 172 | ts.TimeStamp = *now 173 | ts.modified = true 174 | 175 | // Update decay of Score 176 | ts.Score *= math.Exp2(-deltaTime / float64(ConfigScoreHalfLife)) 177 | 178 | // Update the decay of ScoreBalance. Subtract the offset before doing the decay, and add it 179 | // back again afterwards. 180 | bal := ts.ScoreBalance - ConfigScoreBalanceZero 181 | ts.ScoreBalance = bal*math.Exp2(-deltaTime/float64(ConfigScoreBalHalfLife)) + ConfigScoreBalanceZero 182 | } 183 | 184 | // 185 | // A process that will manage the decay of all entries, and save to the database as needed. 186 | // 187 | func maintainScore() { 188 | var elapsed time.Duration // Used to measure performance of this process 189 | timerstats.Add("Score maintenance", ConfigScoreUpdatePeriod, &elapsed) 190 | var list []*territoryScore // Use a linear copy of all pointers so that the map doesn't have to be locked. 191 | defer procStatus.Done() 192 | // Stay in this loop until system shuts down (using the tomb control) 193 | for { 194 | start := time.Now() 195 | timer := time.NewTimer(ConfigScoreUpdatePeriod) // Used as the ticker to activate the process regularly 196 | again: 197 | select { 198 | case <-procStatus.Dying(): 199 | update(list) // Save any remaining data not updated before terminating 200 | return 201 | case ts := <-chScoreList: // This is the way new entries are received 202 | list = append(list, ts) 203 | goto again // Don't start a new timer 204 | case <-timer.C: 205 | update(list) 206 | elapsed = time.Now().Sub(start) // For statistics only 207 | } 208 | } 209 | } 210 | 211 | // Iterate over the copy of the list of all scores, and save to database where needed. 212 | // The map could be used here, in which case a lock would be required. But DB access take 213 | // a long time. 214 | func update(list []*territoryScore) { 215 | db := ephenationdb.New() 216 | now := time.Now() 217 | for _, ts := range list { 218 | if !ts.modified { 219 | continue 220 | } 221 | 222 | // The decay isn't executed unless there has been a change, to save from unnecessary DB updates. 223 | ts.decay(&now) 224 | saveToSQL(db, ts) 225 | } 226 | } 227 | 228 | // Load DB score data for territory owned by 'uid' into 'ts'. 229 | func loadFromSQL(ts *territoryScore, uid uint32) { 230 | var avatarScore struct { 231 | TScoreTotal, TScoreBalance float64 232 | TScoreTime uint32 233 | Name string 234 | Territory []chunkdb.CC // The chunks allocated for this player. 235 | } 236 | db := ephenationdb.New() 237 | query := db.C("avatars").FindId(uid) 238 | err := query.One(&avatarScore) 239 | if err != nil { 240 | log.Println(err) 241 | return 242 | } 243 | 244 | ts.uid = uid 245 | ts.handicap = computeFactor(len(avatarScore.Territory)) 246 | ts.Score = avatarScore.TScoreTotal 247 | ts.ScoreBalance = avatarScore.TScoreBalance 248 | ts.TimeStamp = time.Unix(int64(avatarScore.TScoreTime), 0) 249 | ts.name = avatarScore.Name 250 | ts.modified = true 251 | now := time.Now() 252 | ts.decay(&now) // Update the decay 253 | } 254 | 255 | func saveToSQL(db *mgo.Database, ts *territoryScore) { 256 | if *disableSave { 257 | return 258 | } 259 | var avatarScore struct { // This are the complete list of values that are saved 260 | TScoreTotal, TScoreBalance float64 261 | TScoreTime uint32 262 | } 263 | avatarScore.TScoreTotal = ts.Score 264 | avatarScore.TScoreBalance = ts.ScoreBalance 265 | avatarScore.TScoreTime = uint32(ts.TimeStamp.Unix()) 266 | c := db.C("avatars") 267 | err := c.UpdateId(ts.uid, bson.M{"$set": avatarScore}) 268 | ts.modified = false 269 | if err != nil { 270 | log.Println(err) 271 | return 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/quadtree/quadtree.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package quadtree 19 | 20 | // 21 | // This package is used for keeping track of what objects are close to each other. 22 | // The cost of checking all possible objects would grow with the square of the number of 23 | // objects, so this package will recursively divide a volume into 2 (in every dimension, giving 8 24 | // sub cubes) when the number of objects exceeds a certain limit. 25 | // 26 | 27 | import ( 28 | "fmt" 29 | "log" 30 | "sync" 31 | . "twof" 32 | ) 33 | 34 | // 35 | // Depth estimate: 36 | // 10000 players 37 | // Divided into 4^6 squares gives 2 players per square. 38 | // Allow for higher concentration at some places, and it should still be enough. 39 | 40 | const ( 41 | maxQuadtreeDepth = 6 // Do not make more levels below this 42 | minObjectsPerQuadtree = 5 // Lower limit of the number of objects before collapsing 43 | maxObjectsPerQuadtree = 10 // Upper limit of the number of objects before expanding 44 | expandFactor = 1.3 // How much the are is expanded when the volume is too small 45 | ) 46 | 47 | // The objects that are managed must fulfill this interface 48 | type Object interface { 49 | GetPreviousPos() *TwoF // The previous coordnate 50 | GetType() uint8 // Not needed by QuadTree 51 | GetZ() float64 // Not needed by QuadTree 52 | GetId() uint32 // Used by the client to identify objects. 53 | GetDir() float32 // Get looking direction in radians 54 | } 55 | 56 | // Use MakeQuadtree() to get one! 57 | type Quadtree struct { 58 | corner1 TwoF // Lower left corner 59 | corner2 TwoF // upper right corner 60 | center TwoF // middle of the square 61 | children [2][2]*Quadtree 62 | hasChildren bool 63 | depth int 64 | numObjects int // Sum of all objects in all children 65 | objects []Object 66 | sync.RWMutex 67 | } 68 | 69 | // Remember current Quadtree, for debugging purposes 70 | var debugCurrent *Quadtree 71 | 72 | // Check if the Quadtree is big enough to contain the given position. This is done by simply making 73 | // a bigger initial cube and moving all objects to the new one. Not a very cheap solution, but 74 | // it is expected to be done rarely. 75 | // Return the verified Quadtree. 76 | // TODO: This doesn't work with the current locking strategy. 77 | func (t *Quadtree) checkExpand(tf *TwoF) { 78 | changed := false 79 | newCorner1 := t.corner1 80 | newCorner2 := t.corner2 81 | for i := 0; i < 2; i++ { 82 | if tf[i] < t.corner1[i] { 83 | changed = true 84 | newCorner1[i] = t.corner2[i] - (t.corner2[i]-tf[i])*expandFactor 85 | } 86 | if tf[i] > t.corner2[i] { 87 | changed = true 88 | newCorner2[i] = t.corner1[i] + (tf[i]-t.corner1[i])*expandFactor 89 | } 90 | } 91 | if !changed { 92 | return 93 | } 94 | // log.Printf("Quadtree: Expanding (%.0f-%.0f) to (%.0f-%.0f)\n", t.corner1, t.corner2, newCorner1, newCorner2) 95 | t.destroyChildren() // This will move all objects to the root. 96 | t.corner1 = newCorner1 97 | t.corner2 = newCorner2 98 | // Next time an object is added, the tree will expand again. 99 | } 100 | 101 | // Return true if this Quadtree is empty. Used for debugging and testing. 102 | func (t *Quadtree) Empty() bool { 103 | // No need to lock for this operation. 104 | // fmt.Printf("Quadtree::Empty %d/%d objects\n", t.numObjects, len(t.objects)) 105 | return t.numObjects == 0 && !t.hasChildren && len(t.objects) == 0 106 | } 107 | 108 | func (t *Quadtree) Stats_RLq() string { 109 | t.RLock() 110 | defer t.RUnlock() 111 | return t.string2(false) 112 | } 113 | 114 | func (t *Quadtree) String_RLq() string { 115 | t.RLock() 116 | defer t.RUnlock() 117 | return t.string2(true) 118 | } 119 | 120 | func (t *Quadtree) string2(verbose bool) string { 121 | // Must not call the recursive implicit String() conversion, which would hand from read lock. 122 | var str string 123 | if verbose && t.objects != nil { 124 | str += fmt.Sprintf("%*sobjs:", t.depth-1, "") 125 | for _, o := range t.objects { 126 | str += fmt.Sprintf("\n%*s%v ", t.depth-1, "", o) 127 | } 128 | } 129 | if t.hasChildren { 130 | for x := 0; x < 2; x++ { 131 | for y := 0; y < 2; y++ { 132 | if verbose { 133 | str += fmt.Sprintf("%*sChild [%d,%d]:\n%v", t.depth-1, "", x, y, t.children[x][y].string2(verbose)) 134 | } else { 135 | str += t.children[x][y].string2(verbose) 136 | } 137 | } 138 | } 139 | } 140 | ret := "" 141 | if verbose || t.numObjects > 0 { 142 | ret = fmt.Sprintf("%*scorner1: ", t.depth-1, "") + t.corner1.String() + 143 | " corner2: " + t.corner2.String() + 144 | fmt.Sprintf("\n%*shasChildren: %v, depth: %d, numObjects: %d\n", 145 | t.depth-1, "", t.hasChildren, t.depth, t.numObjects) + str 146 | } 147 | return ret 148 | } 149 | 150 | func MakeQuadtree(c1, c2 TwoF, depth int) *Quadtree { 151 | var t Quadtree 152 | t.corner1 = c1 153 | t.corner2 = c2 154 | t.center = TwoF{(c1[0] + c2[0]) / 2, (c1[1] + c2[1]) / 2} 155 | t.depth = depth 156 | return &t 157 | } 158 | 159 | // Adds or removes an object from the children. The size of objects are considered to be 0, 160 | // which means an object can only be located in one child. 161 | func (t *Quadtree) fileObject(o Object, c *TwoF, add bool) { 162 | // Figure out in which child(ren) the object belongs 163 | for x := 0; x < 2; x++ { 164 | if x == 0 { 165 | if c[0] > t.center[0] { 166 | continue 167 | } 168 | } else if c[0] < t.center[0] { 169 | continue 170 | } 171 | 172 | for y := 0; y < 2; y++ { 173 | if y == 0 { 174 | if c[1] > t.center[1] { 175 | continue 176 | } 177 | } else if c[1] < t.center[1] { 178 | continue 179 | } 180 | 181 | //Add or remove the object 182 | if add { 183 | t.children[x][y].add(o, c) 184 | } else { 185 | t.children[x][y].remove(o, c) 186 | } 187 | return 188 | } 189 | } 190 | } 191 | 192 | // Take a leaf in the Quadtree, add children, and move all objects to the children. 193 | func (t *Quadtree) makeChildren() { 194 | for x := 0; x < 2; x++ { 195 | var minX, maxX float64 196 | if x == 0 { 197 | minX = t.corner1[0] 198 | maxX = t.center[0] 199 | } else { 200 | minX = t.center[0] 201 | maxX = t.corner2[0] 202 | } 203 | 204 | for y := 0; y < 2; y++ { 205 | var minY, maxY float64 206 | if y == 0 { 207 | minY = t.corner1[1] 208 | maxY = t.center[1] 209 | } else { 210 | minY = t.center[1] 211 | maxY = t.corner2[1] 212 | } 213 | 214 | t.children[x][y] = MakeQuadtree(TwoF{minX, minY}, 215 | TwoF{maxX, maxY}, 216 | t.depth+1) 217 | } 218 | } 219 | 220 | // Add all objects to the new children and remove them from "objects" 221 | for _, it := range t.objects { 222 | // fmt.Printf("%*smakeChildren move %v from (%v-%v)\n", t.depth-1, "", it, t.corner1, t.corner2) 223 | t.fileObject(it, it.GetPreviousPos(), true) // Use previous pos as the object may be moving asynchronously 224 | } 225 | t.objects = nil 226 | t.hasChildren = true 227 | } 228 | 229 | // Destroys the children of this, and moves all objects in its descendants 230 | // to the "objects" set 231 | func (t *Quadtree) destroyChildren() { 232 | // fmt.Printf("%*sDestroyChildren (%v-%v) numobjs %d\n", t.depth-1, "", t.corner1, t.corner2, t.numObjects) 233 | //Move all objects in descendants of this to the "objects" set 234 | t.collectObjects(&t.objects) 235 | 236 | for x := 0; x < 2; x++ { 237 | for y := 0; y < 2; y++ { 238 | t.children[x][y] = nil 239 | } 240 | } 241 | 242 | t.hasChildren = false 243 | } 244 | 245 | // Removes the specified object at the indicated position. 246 | func (t *Quadtree) remove(o Object, pos *TwoF) { 247 | t.numObjects-- 248 | if t.numObjects < 0 { 249 | log.Println(">>>>Quadtree:remove numobjects < 0") 250 | log.Printf(">>>>Pos %v, Current tree %p\n>>>>Object %#v\n>>>>Quadtree %s\n", pos, debugCurrent, o, t.string2(true)) 251 | log.Println(">>>>Actual position in tree:", debugCurrent.searchForObject(o).string2(true)) 252 | log.Panicln("Quadtree:remove numobjects < 0") 253 | } 254 | 255 | if t.hasChildren && t.numObjects < minObjectsPerQuadtree { 256 | t.destroyChildren() 257 | } 258 | 259 | if t.hasChildren { 260 | t.fileObject(o, pos, false) 261 | } else { 262 | // Find o in the local list 263 | // fmt.Printf("%*sremove: %v (%p) from %v\n", t.depth-1, "", o, o, t.objects) 264 | for i, o2 := range t.objects { 265 | if o2 == o { 266 | // Found it 267 | if last := len(t.objects) - 1; i == last { 268 | t.objects = t.objects[:last] 269 | } else { 270 | // Move the last element to this position 271 | t.objects[i] = t.objects[last] 272 | t.objects = t.objects[:last] 273 | } 274 | return 275 | } 276 | } 277 | log.Println(">>>>Quadtree:remove failed to find object") 278 | log.Printf(">>>>Pos %v, Current tree %p\n>>>>Object %#v\n>>>>Quadtree %s\n", pos, debugCurrent, o, t.string2(true)) 279 | log.Println(">>>>Actual position in tree:", debugCurrent.searchForObject(o).string2(true)) 280 | log.Panicln("Quadtree:remove failed to find object") 281 | } 282 | } 283 | 284 | //Removes the specified object at the indicated position. We can't ask 285 | func (t *Quadtree) Remove_WLq(o Object) { 286 | debugCurrent = t 287 | t.Lock() 288 | t.remove(o, o.GetPreviousPos()) 289 | t.Unlock() 290 | } 291 | 292 | // Add an object. 293 | func (t *Quadtree) Add_WLq(o Object, pos *TwoF) { 294 | // fmt.Printf("%*sAdd: %v to (%v-%v)\n", t.depth-1, "", o, t.corner1, t.corner2) 295 | t.Lock() 296 | t.checkExpand(pos) 297 | t.add(o, pos) 298 | t.Unlock() 299 | } 300 | 301 | // Add an object 302 | func (t *Quadtree) add(o Object, c *TwoF) { 303 | // fmt.Printf("%*sAdd: %v to (%v-%v)\n", t.depth-1, "", o, t.corner1, t.corner2) 304 | t.numObjects++ 305 | if !t.hasChildren && t.depth < maxQuadtreeDepth && t.numObjects > maxObjectsPerQuadtree { 306 | t.makeChildren() 307 | } 308 | 309 | if t.hasChildren { 310 | t.fileObject(o, c, true) // Use previous pos as the object may be moving asynchronously 311 | } else { 312 | t.objects = append(t.objects, o) 313 | } 314 | } 315 | 316 | // Test that an object, at the specified position, is in the quadtree where it should be. 317 | func (t *Quadtree) testPresent(o Object, pos *TwoF) bool { 318 | if !t.hasChildren { 319 | // There are no children to this tree, which means the object should be in the list of objects. 320 | for _, o2 := range t.objects { 321 | if o2 == o { 322 | // Found it 323 | return true 324 | } 325 | } 326 | return false 327 | } 328 | // Figure out in which child(ren) the object belongs 329 | for x := 0; x < 2; x++ { 330 | if x == 0 { 331 | if pos[0] > t.center[0] { 332 | continue 333 | } 334 | } else if pos[0] < t.center[0] { 335 | continue 336 | } 337 | 338 | for y := 0; y < 2; y++ { 339 | if y == 0 { 340 | if pos[1] > t.center[1] { 341 | continue 342 | } 343 | } else if pos[1] < t.center[1] { 344 | continue 345 | } 346 | 347 | return t.children[x][y].testPresent(o, pos) 348 | } 349 | } 350 | // This shall never happen! 351 | log.Panicln("Quadtree.testPresent failed", o, pos, t, debugCurrent) 352 | return false 353 | } 354 | 355 | // Changes the position of an object in this from oldPos to object.pos 356 | func (t *Quadtree) Move_WLq(o Object, to *TwoF) { 357 | debugCurrent = t 358 | from := o.GetPreviousPos() 359 | // Assume the obect was moved to another part of the quadtree 360 | changed := true 361 | // Usually, the object will not be moved from one part of the quadtree to another. Do a test if that is 362 | // the case, in which case only a read lock will be needed. This will add a constant cost, but will 363 | // allow many more parallel threads. 364 | t.RLock() 365 | if t.testPresent(o, to) { 366 | changed = false 367 | } 368 | t.RUnlock() 369 | if changed { 370 | t.Lock() 371 | t.remove(o, from) 372 | t.checkExpand(to) 373 | t.add(o, to) 374 | t.Unlock() 375 | } 376 | } 377 | 378 | // Adds all objects in this or its descendants to the specified set 379 | // TODO: The same objects can be added several times to the set 380 | func (t *Quadtree) collectObjects(os *[]Object) { 381 | if t.hasChildren { 382 | for x := 0; x < 2; x++ { 383 | for y := 0; y < 2; y++ { 384 | t.children[x][y].collectObjects(os) 385 | } 386 | } 387 | } else { 388 | // Add all "objects" into the provided list, if they are not already there 389 | for _, o := range t.objects { 390 | found := false 391 | for _, o2 := range *os { 392 | if o2 == o { 393 | found = true 394 | break 395 | } 396 | } 397 | if !found { 398 | *os = append(*os, o) 399 | } 400 | } 401 | } 402 | // fmt.Printf("%*scollectObjects (%v-%v) result: %v\n", t.depth-1, "", t.corner1, t.corner2, os) 403 | } 404 | 405 | // Find all objects within radius "dist" from "pos". 406 | func (t *Quadtree) findNearObjects(pos *TwoF, dist float64, objList *[]Object) { 407 | if !t.hasChildren { 408 | for _, o := range t.objects { 409 | // TODO: Use a squared distance instead, to save time for doing sqrt. 410 | if pos.Dist(o.GetPreviousPos()) > dist { 411 | continue // This object was too far away 412 | } 413 | *objList = append(*objList, o) 414 | } 415 | } else { 416 | // Traverse all sub squares that are inside the distance. More than one can match. 417 | for x := 0; x < 2; x++ { 418 | if x == 0 { 419 | if pos[0]-dist > t.center[0] { 420 | continue 421 | } 422 | } else if pos[0]+dist < t.center[0] { 423 | continue 424 | } 425 | for y := 0; y < 2; y++ { 426 | if y == 0 { 427 | if pos[1]-dist > t.center[1] { 428 | continue 429 | } 430 | } else if pos[1]+dist < t.center[1] { 431 | continue 432 | } 433 | t.children[x][y].findNearObjects(pos, dist, objList) 434 | } 435 | } 436 | } 437 | } 438 | 439 | // Find all objects within radius "dist" from "pos", excluding duplicates 440 | func (t *Quadtree) FindNearObjects_RLq(pos *TwoF, dist float64) []Object { 441 | var objList []Object 442 | t.RLock() 443 | t.findNearObjects(pos, dist, &objList) 444 | t.RUnlock() 445 | return objList 446 | } 447 | 448 | // Do full tree search for an object, not based on position. Used for debugging purposes. 449 | func (this *Quadtree) searchForObject(obj Object) *Quadtree { 450 | if this == nil { 451 | return nil 452 | } 453 | if !this.hasChildren { 454 | for _, o := range this.objects { 455 | if o == obj { 456 | return this 457 | } 458 | } 459 | } else { 460 | for x := 0; x < 2; x++ { 461 | for y := 0; y < 2; y++ { 462 | ret := this.children[x][y].searchForObject(obj) 463 | if ret != nil { 464 | return ret 465 | } 466 | } 467 | } 468 | } 469 | return nil 470 | } 471 | -------------------------------------------------------------------------------- /src/cmd/server/specialcommands.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | // 21 | // This file contains functions that decode text commands from the client. 22 | 23 | import ( 24 | "chunkdb" 25 | "client_prot" 26 | "fmt" 27 | "github.com/larspensjo/Go-sync-evaluation/evalsync" 28 | "log" 29 | "os" 30 | "runtime" 31 | "runtime/pprof" 32 | "score" 33 | "strconv" 34 | "strings" 35 | "time" 36 | "timerstats" 37 | ) 38 | 39 | // The player sent a string message 40 | func (up *user) playerStringMessage_RLuWLwRLqBlWLaWLc(buff []byte) { 41 | str := strings.TrimRight(string(buff), " ") // Remove trailing spaces, if any 42 | if *verboseFlag > 1 { 43 | log.Printf("User %v cmd: '%v'\n", up.Name, str) 44 | } 45 | message := strings.SplitN(str, " ", 2) 46 | switch message[0] { 47 | case "/keys": 48 | for _, key := range up.Keys { 49 | up.Printf_Bl("!%s(%d), uid %d", key.Descr, key.Kid, key.Uid) 50 | } 51 | case "/activator": 52 | if len(message) < 2 { 53 | return 54 | } 55 | up.ActivatorControl(message[1]) 56 | case "/home": 57 | if len(message) == 1 { 58 | up.Lock() 59 | up.Coord = up.HomeSP 60 | up.updatedStats = true 61 | up.Unlock() 62 | } 63 | case "/sethome": 64 | cc := up.Coord.GetChunkCoord() 65 | cp := ChunkFind_WLwWLc(cc) 66 | if cp.owner != up.Id { 67 | up.Printf_Bl("#FAIL Not your territory") 68 | break 69 | } 70 | if len(message) == 1 { 71 | up.Lock() 72 | up.HomeSP = up.Coord 73 | up.Unlock() 74 | up.Printf_Bl("Home spawn point updated!") 75 | } 76 | case "/territory": 77 | if len(message) < 2 { 78 | return 79 | } 80 | up.TerritoryCommand_WLwWLcBl(strings.Split(message[1], " ")) 81 | case "/revive": 82 | if len(message) == 1 && up.Dead { 83 | up.Lock() 84 | up.Dead = false 85 | up.HitPoints = 0.3 86 | up.updatedStats = true 87 | up.Coord = up.ReviveSP 88 | up.Unlock() 89 | } 90 | case "/level": 91 | if (up.AdminLevel >= 8 || *allowTestUser) && len(message) == 2 { 92 | lvl, err := strconv.ParseUint(message[1], 10, 0) 93 | if err != nil { 94 | up.Printf_Bl("%s", err) 95 | } else { 96 | up.Level = uint32(lvl) 97 | up.updatedStats = true 98 | } 99 | } 100 | case "/timers": 101 | if up.AdminLevel >= 2 || *allowTestUser { 102 | timerstats.Report(up) 103 | } 104 | case "/panic": 105 | if up.AdminLevel >= 8 || *allowTestUser { 106 | log.Panic("client_prot.DEBUG command 'panic'") 107 | } 108 | case "/status": 109 | var m runtime.MemStats 110 | runtime.ReadMemStats(&m) 111 | up.Printf_Bl("!!Status") 112 | up.Printf_Bl("!Chunks loaded: %d, super chunks %d", worldCacheNumChunks, superChunkManager.Size()) 113 | up.Printf_Bl("!Num players:%v, monsters %v, near monsters %d", numPlayers, len(monsterData.m), CountNearMonsters_RLq(up.GetPreviousPos())) 114 | up.Printf_Bl("!Mem in use %vMB, total alloc %vMB, num malloc %vM, num free %vM", 115 | m.Alloc/1e6, m.TotalAlloc/1e6, m.Mallocs/1e6, m.Frees/1e6) 116 | up.Printf_Bl("!Worst message write %.6f s, Worst chunk read %.6f s", float64(WorstWriteTime)/float64(time.Second), float64(DBStats.WorstRead)/float64(time.Second)) 117 | up.Printf_Bl("!Num chunks read: %d, average read time %.6f", DBStats.NumRead, float64(DBStats.TotRead)/float64(DBStats.NumRead)/float64(time.Second)) 118 | up.Printf_Bl("!Created chunks: %d, average time %.6f", DBCreateStats.Num, float64(DBCreateStats.TotTime)/float64(DBCreateStats.Num)/float64(time.Second)) 119 | up.Printf_Bl("!Server booted %v", bootDate) 120 | up.Printf_Bl("!%s", trafficStatistics) 121 | WorstWriteTime = 0 122 | DBStats.WorstRead = 0 123 | case "/players": 124 | up.ReportPlayers() 125 | case "/flying": 126 | up.Flying = !up.Flying 127 | up.Climbing = false // Always turn off climbing 128 | up.Printf_Bl("Flying: %v", up.Flying) 129 | case "/inv": 130 | fallthrough 131 | case "/inventory": 132 | if len(message) == 2 && up.AdminLevel > 8 { 133 | code := ObjectCode(message[1]) 134 | _, ok := objectUseTable[code] 135 | if message[1] == "clear" { 136 | up.Inventory.Clear() // There is no update message generated, so client won't know. 137 | up.WeaponGrade = 0 138 | up.ArmorGrade = 0 139 | up.HelmetGrade = 0 140 | up.WeaponLvl = 0 141 | up.ArmorLvl = 0 142 | up.HelmetLvl = 0 143 | } else if !ok { 144 | up.Printf_Bl("!Available objects:") 145 | for key, _ := range objectUseTable { 146 | up.Printf_Bl("!%v ", key) 147 | } 148 | } else { 149 | AddOneObjectToUser_WLuBl(up, code) 150 | } 151 | } else { 152 | up.Inventory.Report(up) 153 | up.Printf_Bl("!Equip modifiers: armor %.0f%%, helmet %.0f%%, weapon %.0f%%", 154 | (ArmorLevelDiffMultiplier(up.Level, up.ArmorLvl, up.ArmorGrade)-1)*100, 155 | (ArmorLevelDiffMultiplier(up.Level, up.HelmetLvl, up.HelmetGrade)-1)*100, 156 | (WeaponLevelDiffMultiplier(up.Level, up.WeaponLvl, up.WeaponGrade)-1)*100) 157 | } 158 | case "/GC": 159 | var m runtime.MemStats 160 | runtime.ReadMemStats(&m) 161 | up.Printf_Bl("GC next: %v, num: %v, paus total: %v", m.NextGC, m.NumGC, m.PauseTotalNs) 162 | // runtime.GC() 163 | case "/shutdown": 164 | if up.AdminLevel >= 8 { 165 | if *cpuprofile != "" { 166 | pprof.StopCPUProfile() 167 | } 168 | GraceFulShutdown() 169 | // Will not return 170 | } 171 | case "/evalsync": 172 | for _, str := range evalsync.Eval() { 173 | up.Printf_Bl("!%s", str) 174 | } 175 | case "/resetpos": 176 | up.Coord.X = 0 177 | up.Coord.Y = 0 178 | up.Coord.Z = FLOATING_ISLANDS_LIM - PlayerHeight // As high as possible 179 | up.Flying = false 180 | up.Climbing = false 181 | case "/prof": 182 | if up.AdminLevel >= 8 || *allowTestUser { 183 | const fn = "profdata.tmp" 184 | f, _ := os.OpenFile(fn, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) 185 | pprof.WriteHeapProfile(f) 186 | f.Close() 187 | up.Printf_Bl("pprof written to %s\n", fn) 188 | } 189 | case "/say": 190 | if len(message) < 2 { 191 | break 192 | } 193 | near := playerQuadtree.FindNearObjects_RLq(up.GetPreviousPos(), client_prot.NEAR_OBJECTS) 194 | n := 0 195 | for _, o := range near { 196 | other, ok := o.(*user) 197 | if !ok { 198 | continue // Only need to tell players, not monsters etc. 199 | } 200 | if other == up { 201 | continue // Found self 202 | } 203 | // Tell 'other' that we moved 204 | other.Printf("%s says: %s", up.Name, message[1]) 205 | n++ 206 | } 207 | if n == 0 { 208 | up.Printf_Bl("#FAIL No one is near") 209 | } else { 210 | up.Printf_Bl("You say: %s", message[1]) 211 | } 212 | case "/tell": 213 | if len(message) < 2 { 214 | break 215 | } 216 | up.TellOthers_RLaBl(message[1]) 217 | case "/friend": 218 | if len(message) < 2 { 219 | break 220 | } 221 | up.FriendCommand_RLaWLu(message[1]) 222 | case "/score": 223 | score.Report(up) 224 | case "/target": 225 | if len(message) < 2 { 226 | break 227 | } 228 | up.TargetCommand(message[1:]) 229 | } 230 | } 231 | 232 | func (up *user) TargetCommand(msg []string) { 233 | if msg == nil || len(msg) == 0 { 234 | return 235 | } 236 | switch msg[0] { 237 | case "set": 238 | up.TargetCoor = up.Coord 239 | return 240 | case "show": 241 | up.Printf_Bl("Current target: %v", up.TargetCoor) 242 | return 243 | case "reset": 244 | up.TargetCoor = user_coord{0, 0, 0} 245 | } 246 | } 247 | 248 | func (up *user) TerritoryCommand_WLwWLcBl(msg []string) { 249 | if msg == nil || len(msg) == 0 { 250 | return 251 | } 252 | switch msg[0] { 253 | case "show": 254 | up.Printf_Bl("Territory (%d of %d): %v", len(up.Territory), up.Maxchunks, up.Territory) 255 | if up.AdminLevel > 0 { 256 | cc := up.Coord.GetChunkCoord() 257 | cp := ChunkFind_WLwWLc(cc) 258 | up.Printf_Bl("adm: This place: %d", cp.owner) 259 | } 260 | case "claim": 261 | up.TerritoryClaim_WLwWLc(msg[1:]) 262 | case "grant": 263 | if up.AdminLevel < 5 || len(msg) != 2 { 264 | up.Printf_Bl("#FAIL") 265 | return 266 | } 267 | up.TerritoryGrant(msg[1]) 268 | case "revert": 269 | if up.AdminLevel < 10 { 270 | up.Printf_Bl("#FAIL") 271 | return 272 | } 273 | cc := up.Coord.GetChunkCoord() 274 | cp := ChunkFind_WLwWLc(cc) 275 | if cp.owner != OWNER_NONE && cp.owner != OWNER_RESERVED { 276 | up.Printf_Bl("#FAIL Can't revert when owner is %d", cp.owner) 277 | return 278 | } 279 | // Remove the old chunk and make a new one from scratch 280 | worldCacheLock.Lock() 281 | RemoveChunkFromHashTable(cp) 282 | cp = dBCreateAndSaveChunk(cc) 283 | AddChunkToHashTable(cp) 284 | worldCacheLock.Unlock() 285 | up.CmdReadChunk_WLwWLcBl(cc) // Use exisiting method to send chunk 286 | default: 287 | up.Printf_Bl("#FAIL Unknown territory command %v", msg[0]) 288 | } 289 | } 290 | 291 | func (up *user) TerritoryGrant(arg string) { 292 | cc := up.Coord.GetChunkCoord() 293 | cp := ChunkFind_WLwWLc(cc) 294 | newOwner, err := strconv.ParseInt(arg, 10, 64) 295 | if err != nil { 296 | up.Printf_Bl("%v", err) 297 | return 298 | } 299 | up.Printf_Bl("Changed owner from %d to %d", cp.owner, newOwner) 300 | cp.owner = uint32(newOwner) 301 | cp.Write() 302 | } 303 | 304 | func (up *user) TerritoryClaim_WLwWLc(arg []string) { 305 | const usage = "Usage: /territory claim [up/down]" 306 | if up.AdminLevel < 1 && len(up.Territory) >= up.Maxchunks { 307 | up.Printf_Bl("#FAIL !You are not allowed more chunks than %d", up.Maxchunks) 308 | return 309 | } 310 | if *allowTestUser && NameIsTestPlayer(up.Name) { 311 | up.Printf_Bl("#FAIL !Test players can't claim territory") 312 | return 313 | } 314 | if MonsterDifficulty(&up.Coord) > up.Level && up.AdminLevel == 0 { 315 | up.Printf_Bl("#FAIL !You are too low level for this area") 316 | return 317 | } 318 | if len(arg) > 1 { 319 | up.Printf_Bl(usage) 320 | return 321 | } 322 | cc := up.Coord.GetChunkCoord() 323 | if len(arg) > 0 { 324 | switch arg[0] { 325 | case "up": 326 | cc.Z++ 327 | case "down": 328 | cc.Z-- 329 | case "west": 330 | cc.X-- 331 | case "east": 332 | cc.X++ 333 | case "south": 334 | cc.Y-- 335 | case "north": 336 | cc.Y++ 337 | default: 338 | up.Printf_Bl(usage) 339 | return 340 | } 341 | } 342 | cp := ChunkFind_WLwWLc(cc) 343 | cp.Lock() 344 | if cp.owner != OWNER_NONE { 345 | cp.Unlock() 346 | up.Printf_Bl("#FAIL !Chunk %v is already allocated to ID %d", cc, cp.owner) 347 | return 348 | } 349 | 350 | // Make sure either it is the first chunk, or an adjacent chunk is already allocated, or the request will be denied. 351 | approved := len(up.Territory) == 0 || up.AdminLevel > 0 352 | adjacent := dBGetAdjacentChunks(&cc) 353 | for _, cp := range adjacent { 354 | if cp.owner == up.Id { 355 | approved = true 356 | break 357 | } 358 | } 359 | if !approved { 360 | up.Printf_Bl("#FAIL !You must allocate adjacent to another of your chunks") 361 | return 362 | } 363 | 364 | // All tests are approved, allocate the chunk 365 | ChunkFind_WLwWLc(chunkdb.CC{X: cc.X, Y: cc.Y, Z: cc.Z}) 366 | cp.owner = up.Id 367 | cp.flag |= CHF_MODIFIED 368 | cp.Write() 369 | cp.Unlock() 370 | up.Printf_Bl("!Congratulations, you now own chunk %v", cc) 371 | if up.Territory == nil { 372 | up.Territory = []chunkdb.CC{cc} 373 | } else { 374 | for _, chunk := range up.Territory { 375 | if chunk.X == cc.X && chunk.Y == cc.Y && chunk.Z == cc.Z { 376 | log.Printf("Chunk %v allocated to user %d (%s), but was already in DB list\n", cc, up.Id, up.Name) 377 | return 378 | } 379 | } 380 | up.Territory = append(up.Territory, cc) 381 | } 382 | up.Save_Bl() 383 | } 384 | 385 | func (up *user) TellOthers_RLaBl(arg string) { 386 | message := strings.SplitN(arg, " ", 2) 387 | if len(message) != 2 { 388 | return 389 | } 390 | 391 | name := message[0] 392 | allPlayersSem.RLock() 393 | other, ok := allPlayerNameMap[strings.ToLower(name)] 394 | allPlayersSem.RUnlock() 395 | 396 | if !ok { 397 | up.Printf_Bl("#FAIL No player %v logged in", name) 398 | } else { 399 | other.Printf("%v tells you: %s", up.Name, message[1]) 400 | up.Printf_Bl("You tell %v: %s", name, message[1]) 401 | } 402 | } 403 | 404 | // Handle all commands starting with /friend 405 | func (up *user) FriendCommand_RLaWLu(arg string) { 406 | if up.Id == OWNER_TEST { 407 | // Test players can't define friends 408 | return 409 | } 410 | cmd := strings.Split(arg, " ") 411 | switch cmd[0] { 412 | case "add": 413 | if len(cmd) != 2 { 414 | up.Printf_Bl("#FAIL !Usage: /friend add [name]") 415 | return 416 | } 417 | name := cmd[1] 418 | if up.Name == name { 419 | up.Printf_Bl("#FAIL !Can't add self") 420 | return 421 | } 422 | notFound, alreadyIn := up.AddToListener_RLaWLu(name) 423 | if notFound { 424 | up.Printf_Bl("#FAIL !%v must be logged in to add", name) 425 | } else if alreadyIn { 426 | up.Printf_Bl("#FAIL !%v is already on your friends list", name) 427 | } else { 428 | up.Printf_Bl("!%v added to your friends list", name) 429 | } 430 | case "remove": 431 | if len(cmd) != 2 { 432 | up.Printf_Bl("#FAIL !Usage: /friend remove [name]") 433 | return 434 | } 435 | name := cmd[1] 436 | notFound, notIn := up.RemoveFromListener_RLaWLu(name) 437 | if notFound { 438 | up.Printf_Bl("#FAIL !%v must be logged in to remove", name) 439 | } else if notIn { 440 | up.Printf_Bl("#FAIL !%v was not on your friends list", name) 441 | } else { 442 | up.Printf_Bl("!%v removed from your friends list", name) 443 | } 444 | } 445 | } 446 | 447 | func GraceFulShutdown() { 448 | log.Println("User requested shut down") 449 | score.Close() 450 | SaveAllPlayers_RLa() // This will only set the flag to save 451 | time.Sleep(1e9) // TODO: not a pretty way. Wait for players to be saved. 452 | log.Println("Goodbye!") 453 | os.Exit(0) 454 | } 455 | 456 | func (up *user) ActivatorControl(msg string) { 457 | cmd := strings.SplitN(string(msg), " ", 2) 458 | switch cmd[0] { 459 | case "show": 460 | cc := up.Coord.GetChunkCoord() 461 | cp := ChunkFind_WLwWLc(cc) 462 | for _, tr := range cp.blTriggers { 463 | up.Printf_Bl("!Trigger: %v", tr) 464 | } 465 | up.Printf_Bl("!Messages: %v", cp.triggerMsgs) 466 | case "clear": 467 | if len(cmd) < 2 { 468 | up.Printf_Bl("#FAIL !Usage: /activator clear chunkX chunkY chunkZ X Y Z") 469 | return 470 | } 471 | var x, y, z uint8 472 | var cc chunkdb.CC 473 | n, err := fmt.Sscan(cmd[1], &cc.X, &cc.Y, &cc.Z, &x, &y, &z) 474 | // up.Printf_Bl("scan result: %d,%d,%d, err %v, n %d", x, y, z, err, n) 475 | if err != nil || n != 6 { 476 | return 477 | } 478 | cp := ChunkFind_WLwWLc(cc) 479 | cp.Lock() 480 | msgp := cp.FindActivator(x, y, z) 481 | if msgp != nil { 482 | *msgp = nil 483 | } else { 484 | log.Println("Failed to find text message", x, y, z, cp.Coord) 485 | } 486 | cp.Write() 487 | cp.Unlock() 488 | case "add": 489 | if len(cmd) < 2 { 490 | up.Printf_Bl("#FAIL !Usage: /activator add chunkX chunkY chunkZ X Y Z MSG") 491 | return 492 | } 493 | var x, y, z uint8 494 | var cc chunkdb.CC 495 | n, err := fmt.Sscan(cmd[1], &cc.X, &cc.Y, &cc.Z, &x, &y, &z) 496 | // up.Printf_Bl("scan result: %d,%d,%d, err %v, n %d", x, y, z, err, n) 497 | if err != nil || n != 6 { 498 | return 499 | } 500 | cp := ChunkFind_WLwWLc(cc) 501 | tmp := strings.SplitN(cmd[1], " ", 7) 502 | if len(tmp) != 7 { 503 | up.Printf_Bl("#FAIL !Missing string at end") 504 | return 505 | } 506 | // up.Printf_Bl("!Adding activator to chunk %v at %d,%d,%d to '%s'", cc, x, y, z, tmp[6]) 507 | cp.Lock() 508 | msgp := cp.FindActivator(x, y, z) 509 | if msgp != nil { 510 | *msgp = append(*msgp, tmp[6]) 511 | } else { 512 | log.Println("Failed to find text message", x, y, z, cp.Coord) 513 | } 514 | cp.Write() 515 | cp.Unlock() 516 | } 517 | } 518 | 519 | func (up *user) ReportPlayers() { 520 | allPlayersSem.RLock() 521 | for _, p := range allPlayerIdMap { 522 | switch p.connState { 523 | case PlayerConnStateLogin: 524 | up.Printf_Bl("!%v state login", p.Name) 525 | case PlayerConnStatePass: 526 | up.Printf_Bl("!%v state password", p.Name) 527 | case PlayerConnStateIn: 528 | up.Printf_Bl("!%v level %d at chunk %v", p.Name, p.Level, p.Coord.GetChunkCoord()) 529 | default: 530 | up.Printf_Bl("!%v (unknown state)", p.Name) 531 | } 532 | } 533 | allPlayersSem.RUnlock() 534 | } 535 | -------------------------------------------------------------------------------- /src/cmd/server/listener.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Ephenation Authors 2 | // 3 | // This file is part of Ephenation. 4 | // 5 | // Ephenation is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, version 3. 8 | // 9 | // Ephenation is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with Ephenation. If not, see . 16 | // 17 | 18 | package main 19 | 20 | // 21 | // Listen for incoming messages from a client, decode the protocol and call the appropriate 22 | // function, usually in manage_clients.go. All data going back to the client also pass through 23 | // here. There is one goroutine spawned for every client. 24 | // 25 | 26 | import ( 27 | "net" 28 | // "fmt" 29 | "chunkdb" 30 | . "client_prot" 31 | "log" 32 | "os" 33 | "time" 34 | ) 35 | 36 | var ( 37 | // Just for nice info, keep track of who was last logged in 38 | lastUser string 39 | timeOfLogout time.Time 40 | ) 41 | 42 | const ( 43 | dummyLoginName = "" 44 | ) 45 | 46 | // This is a function that only listens for new connections. The listening is done forever in a goroutine of its own, 47 | // while this function returns the success status. 48 | func SetupListenForClients_WLuBlWLqWLa(addr string) error { 49 | listener, err := net.Listen("tcp", addr) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | go func() { 55 | // Errors are not expected from the call to accept. If they happen anyway, log a message and give it up. 56 | for failures := 0; failures < 100; { 57 | conn, err := listener.Accept() 58 | if err != nil { 59 | log.Print("Failed listening: ", err, "\n") 60 | failures++ 61 | } 62 | if ok, index := NewClientConnection_WLa(conn); ok { 63 | // A new connection is established. Spawn a new gorouting to handle that player 64 | go ManageOneClient_WLuBlWLqWLa(conn, index) 65 | } 66 | } 67 | log.Println("Too many listener.Accept() errors, giving up") 68 | os.Exit(1) 69 | }() 70 | return nil 71 | } 72 | 73 | // Send the protocol version to the client. 74 | func SendProtocolVersion_Bl(conn net.Conn) { 75 | b := []byte{11, 0, CMD_PROT_VERSION, 76 | ProtVersionMinor & 0xFF, 77 | ProtVersionMinor >> 8, 78 | ProtVersionMajor & 0xFF, 79 | ProtVersionMajor >> 8, 80 | byte(ClientCurrentMinorVersion & 0xFF), 81 | byte(ClientCurrentMinorVersion >> 8), 82 | byte(ClientCurrentMajorVersion & 0xFF), 83 | byte(ClientCurrentMajorVersion >> 8), 84 | } 85 | conn.Write(b) 86 | } 87 | 88 | // This is executed as one process for each client 89 | func ManageOneClient_WLuBlWLqWLa(conn net.Conn, i int) { 90 | // log.Print("RemoteAddr ", conn.RemoteAddr(), "\n") 91 | SendProtocolVersion_Bl(conn) 92 | ManageOneClient2_WLuWLqWLmBlWLcWLw(conn, i) 93 | if !NameIsTestPlayer(allPlayers[i].Name) && allPlayers[i].Name != dummyLoginName { 94 | CmdSavePlayerNow_RluBl(i) 95 | lastUser = allPlayers[i].Name 96 | timeOfLogout = time.Now() 97 | } 98 | CmdClose_BlWLqWLuWLa(i) 99 | } 100 | 101 | // This is executed as one process for each client. All messages sent because of this process must use blocking send. 102 | // The reason is that non-blocking send will be sent in a channel to this process, and we must not send messages 103 | // to our own channel. 104 | func ManageOneClient2_WLuWLqWLmBlWLcWLw(conn net.Conn, i int) { 105 | buff := make([]byte, 50) // Command buffer, also used for blocking messages. 106 | up := allPlayers[i] 107 | up.Name = dummyLoginName // To have something to print 108 | previous := time.Now() 109 | longPrevious := previous 110 | previousAttack := previous 111 | // Loop until player disconnects 112 | for { 113 | // Measure how much time has passed since last iteration 114 | now := time.Now() 115 | delta := now.Sub(previous) 116 | if up.connState == PlayerConnStateIn { 117 | // Ignore this unless the player is logged in. 118 | if delta > ObjectsUpdatePeriod { 119 | // These functions cost a lot, don't run them unless enough time has passed. 120 | previous = now 121 | // Check if near objects have moved and send messages to the client 122 | clientTellMovedObjects_Bl(up) 123 | // Update new position for player 124 | up.cmdUpdatePosition_WLuWLqWLmWLwWLc() 125 | fullReport := false 126 | if now.Sub(longPrevious) > 2*time.Second { 127 | fullReport = true 128 | longPrevious = now 129 | } 130 | // Tell everyone near if the player moved 131 | up.checkOnePlayerPosChanged_RLuWLqBl(fullReport) 132 | } 133 | delta = now.Sub(previousAttack) 134 | if delta > CnfgAttackPeriod { 135 | previousAttack = now 136 | up.ManageAttackPeriod_WLuBl(delta) // Manage general attacking tasks 137 | } 138 | } 139 | if up.updatedStats { 140 | // As the player isn't locked, the stats may be updated while this is done. Clear the flag 141 | // first, to ensure other updates are not lost. The transient flags are cleared later, and so it may happen 142 | // that a flag will be lost. But this is not vital information, so a loss can be accepted if it is unlikely. 143 | // log.Printf("State %v, flags 0x%x, hp %v, exp %v, level %v, mana %v\n", up.connState, up.flags, up.HitPoints, up.Exp, up.Level, up.Mana) 144 | up.updatedStats = false 145 | up.SendMsgUpdatedStats_Bl(buff) 146 | up.flags &= ^UserFlagTransientMask // Clear all transient flags, now that the client has been informed. 147 | } 148 | if up.forceSave { 149 | up.forceSave = false 150 | CmdSavePlayerNow_RluBl(i) 151 | } 152 | // Read out all waiting data, if any, from the incoming channels 153 | for moreData := true; moreData; { 154 | select { 155 | case clientMessage := <-up.channel: 156 | up.writeBlocking_Bl(clientMessage) 157 | case clientCommand := <-up.commandChannel: 158 | clientCommand(up) 159 | default: 160 | moreData = false 161 | } 162 | if up.connState == PlayerConnStateDisc { 163 | return 164 | } 165 | } 166 | // Set a new deadline. 167 | conn.SetReadDeadline(time.Now().Add(ObjectsUpdatePeriod)) 168 | n, err := conn.Read(buff[0:2]) // Read the length information. This will block for ObjectsUpdatePeriod ns 169 | if err != nil { 170 | if e2, ok := err.(*net.OpError); ok && (e2.Timeout() || e2.Temporary()) { 171 | // log.Printf("Read timeout %v", e2) // This will happen frequently 172 | continue 173 | } 174 | if *verboseFlag > 1 { 175 | // This is a normal case 176 | log.Printf("Disconnect %v because of '%v'\n", up.Name, err) 177 | } 178 | return 179 | } 180 | if n == 1 { 181 | if *verboseFlag > 1 { 182 | log.Printf("Got %d bytes reading from socket\n", n) 183 | } 184 | for n == 1 { 185 | n2, err := conn.Read(buff[1:2]) // Second byte of the length 186 | if err != nil || n2 != 1 { 187 | if e2, ok := err.(*net.OpError); ok && (e2.Timeout() || e2.Temporary()) { 188 | log.Printf("Read timeout %v", e2) 189 | continue 190 | } 191 | log.Printf("Failed again to read: %v\n", err) 192 | return 193 | } 194 | n = 2 195 | } 196 | } 197 | if n != 2 { 198 | log.Printf("Got %d bytes reading from socket\n", n) 199 | continue 200 | } 201 | length := int(uint(buff[1])<<8 + uint(buff[0])) 202 | if length > cap(buff) { 203 | buff2 := make([]byte, length) 204 | copy(buff2, buff[0:2]) 205 | buff = buff2 206 | if *verboseFlag > 2 { 207 | log.Printf("Expecting %d bytes, which is too much for buffer. Buffer was extended\n", length) 208 | } 209 | } 210 | // Read the rest of the bytes 211 | conn.SetReadDeadline(time.Now().Add(1e10)) // Give it up after a long time 212 | for n2 := n; n2 < length; { 213 | if *verboseFlag > 1 { 214 | log.Println("Got", n2, "out of", length, "bytes") 215 | } 216 | // If unlucky, we only get one byte at a time 217 | var e2 net.Error 218 | const maxDelay = 5 219 | for i := 0; i < maxDelay; i++ { 220 | // Normal case is one iteration in this loop 221 | n, err = conn.Read(buff[n2:length]) 222 | if err == nil { 223 | break // No error 224 | } 225 | e2, ok := err.(net.Error) 226 | if ok && !e2.Temporary() && !e2.Timeout() { // We don't really expect a timeout here 227 | break // Bad error, can't handle it. 228 | } 229 | if *verboseFlag > 1 { 230 | log.Println("Temporary, retry") 231 | } 232 | if *verboseFlag > 0 && i == maxDelay-1 { 233 | log.Println("Timeout, giving up") 234 | } 235 | } 236 | if err != nil { 237 | if *verboseFlag > 0 { 238 | log.Printf("Disconnect %v because of '%v'.\n", up.Name, err) 239 | if e2 != nil { 240 | log.Printf("Temporary: %v, Timeout: %v\n", e2.Temporary(), e2.Timeout()) 241 | } 242 | } 243 | return 244 | } 245 | n2 += n 246 | } 247 | trafficStatistics.AddReceived(length) 248 | // fmt.Printf("ManageOneClient: command (%d bytes) %v\n", n, buff[:length]) 249 | switch buff[2] { 250 | case CMD_PING: 251 | if buff[3] == 0 { 252 | // Request message 253 | buff[3] = 1 // Change it to response type 254 | allPlayers[i].writeBlocking_Bl(buff[0:4]) // And send it back. 255 | } 256 | case CMD_SAVE: 257 | CmdSavePlayerNow_RluBl(i) 258 | case CMD_LOGIN: 259 | if length <= 3 { 260 | // TODO. Too short command, no login name 261 | log.Print("LOGIN no name\n") 262 | return 263 | } 264 | if *verboseFlag > 2 { 265 | log.Printf("Logincmd %v\n", buff[3:length]) 266 | } 267 | up.CmdLogin_WLwWLuWLqBlWLc(string(buff[3:length])) 268 | case CMD_RESP_PASSWORD: 269 | // log.Printf("Logincmd %v\n", buff[3 : length]) 270 | if !up.CmdPassword_WLwWLuWLqBlWLc(buff[3:length]) { 271 | buff[0] = 3 272 | buff[1] = 0 273 | buff[2] = CMD_LOGINFAILED 274 | allPlayers[i].writeBlocking_Bl(buff[0:3]) // Tell client login failed. 275 | if *verboseFlag > 0 { 276 | log.Printf("Disconnect %v\n", up.Name) 277 | } 278 | return 279 | } 280 | up.FileMessage(*welcomeMsgFile) 281 | if len(allPlayerIdMap) > 1 { 282 | up.Printf_Bl("Current players:") 283 | up.ReportPlayers() 284 | } else if lastUser != "" { 285 | up.Printf_Bl("Last logout: %s in %s", lastUser, time.Now().Sub(timeOfLogout)) 286 | } 287 | if *verboseFlag > 0 { 288 | log.Println("Successful LOGIN for", up.Email, up.Name) 289 | } 290 | case CMD_QUIT: 291 | if *verboseFlag > 1 { 292 | log.Printf("Quit: %v\n", up.Name) 293 | } 294 | return 295 | case CMD_GET_COORDINATE: 296 | up.CmdReportCoordinate_RLuBl(false) 297 | case CMD_ATTACK_MONSTER: 298 | if length != 7 { 299 | log.Printf("CMD_ATTACK_MONSTER illegal length: %v\n", buff[0:length]) 300 | return 301 | } 302 | up.CmdAttackMonster_WLuRLm(buff[3:7]) 303 | case CMD_PLAYER_ACTION: 304 | if length != 4 { 305 | log.Printf("CMD_PLAYER_ACTION illegal length: %v\n", buff[0:length]) 306 | return 307 | } 308 | up.CmdPlayerAction_WLuBl(buff[3]) 309 | case CMD_READ_CHUNK: 310 | if length != 15 { 311 | log.Printf("CommandReadChunk illegal length: %v\n", buff[0:length]) 312 | return 313 | } else { 314 | MakeCommandReadChunk_WLwWLcBl(i, buff[3:length]) 315 | } 316 | case CMD_VRFY_CHUNCK_CS: 317 | if length < 10 || ((length-3)%7 != 0) { 318 | // fmt.Printf("Verify Checksum length error!") 319 | log.Printf("CMD_VRFY_CHUNCK_CS illegal length: %v\n", buff[0:length]) 320 | } else { 321 | // A list of chunk checksums can be recieved, these should be verified and if the 322 | // checksum is not correct, the updated block should be sent 323 | // fmt.Printf("CMD_VRFY_CHUNCK_CS - %v %v %v %v\n", buff[6], buff[7], buff[8], buff[9] ) 324 | CommandVerifyChunkCS_WLwWLcBl(i, buff[3:length]) 325 | } 326 | case CMD_HIT_BLOCK: 327 | if length != 18 { 328 | log.Printf("HitBlockCommand illegal length %d: %v\n", length, buff[0:length]) 329 | return 330 | } else { 331 | var b []byte 332 | var cc chunkdb.CC 333 | cc.X, b, _ = ParseInt32(buff[3:length]) 334 | cc.Y, b, _ = ParseInt32(b) 335 | cc.Z, b, _ = ParseInt32(b) 336 | up.HitBlock_WLwWLcRLq(cc, b[0], b[1], b[2]) 337 | // fmt.Printf("ManageOneClient %v\n", hbc) 338 | // fmt.Println("Buffer ", buff[0:18]) 339 | } 340 | case CMD_BLOCK_UPDATE: 341 | if length != 19 { 342 | // The block update command allows for many blocks to be updated at the same time, but 343 | // that is for the server->client, not for the client->server. 344 | log.Printf("AttachBlockCommand illegal length %d: %v\n", length, buff[0:length]) 345 | return 346 | } else { 347 | var b []byte 348 | var cc chunkdb.CC 349 | cc.X, b, _ = ParseInt32(buff[3:length]) 350 | cc.Y, b, _ = ParseInt32(b) 351 | cc.Z, b, _ = ParseInt32(b) 352 | bl := block(b[3]) 353 | if bl == BT_Teleport { 354 | cp := ChunkFind_WLwWLc(cc) 355 | cp.SetTeleport(cc, up, b[0], b[1], b[2]) 356 | } else { 357 | // log.Printf("Attach block %v at chunk %v\n", bl, cc) 358 | CmdAttachBlock_WLwWLcRLq(cc, b[0], b[1], b[2], bl, i) 359 | } 360 | } 361 | case CMD_JUMP: 362 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_JUMP) 363 | case CMD_START_FWD: 364 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_START_FWD) 365 | case CMD_STOP_FWD: 366 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_STOP_FWD) 367 | case CMD_START_BWD: 368 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_START_BWD) 369 | case CMD_STOP_BWD: 370 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_STOP_BWD) 371 | case CMD_START_LFT: 372 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_START_LFT) 373 | case CMD_STOP_LFT: 374 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_STOP_LFT) 375 | case CMD_START_RGT: 376 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_START_RGT) 377 | case CMD_STOP_RGT: 378 | up.CmdPlayerMove_WLuWLqWLmWLwWLc(CMD_STOP_RGT) 379 | case CMD_SET_DIR: 380 | if length != 7 { 381 | log.Printf("Illegal SET_DIR client command: %v\n", buff[0:length]) 382 | return 383 | } else { 384 | MakeDirectionsCommand(i, buff[3:length]) 385 | } 386 | case CMD_DEBUG: 387 | up.playerStringMessage_RLuWLwRLqBlWLaWLc(buff[3:length]) 388 | case CMD_USE_ITEM: 389 | code := ObjectCode(buff[3:7]) 390 | lvl := uint32(0) 391 | if length == 11 { 392 | // This is the new proper way, where a level of the item is also provided, 393 | // but some clients remains with the old format. TODO: Clean up. 394 | lvl, _, _ = ParseUint32(buff[7:11]) 395 | } 396 | up.Inventory.Use_WluBl(up, code, lvl) 397 | case CMD_DROP_ITEM: 398 | code := ObjectCode(buff[3:7]) 399 | lvl, _, _ := ParseUint32(buff[7:11]) 400 | up.Lock() 401 | // The Use function will not really do anything, only return a function. That way, only a read lock is needed. 402 | // The reason for this is that the Use function will do callbacks that will, in turn, lock what is needed. As this is 403 | // not known now, except that we know the user has to be read locked. 404 | val := ItemValueAsDrop(up.Level, lvl, code) * CnfgItemRewardNormalizer 405 | if val >= 0 { 406 | up.Inventory.Remove(code, lvl) 407 | up.AddExperience(val) 408 | } 409 | up.Unlock() 410 | // log.Println("CMD_DROP_ITEM", code, lvl, val) 411 | ReportOneInventoryItem_WluBl(up, code, lvl) 412 | case CMD_REQ_PLAYER_INFO: 413 | uid, _, _ := ParseUint32(buff[3:7]) 414 | allPlayersSem.RLock() 415 | up, ok := allPlayerIdMap[uid] 416 | allPlayersSem.RUnlock() 417 | if ok { 418 | l := len(up.Name) 419 | buff[0] = byte(8 + l) 420 | buff[1] = 0 421 | buff[2] = CMD_RESP_PLAYER_NAME 422 | EncodeUint32(uid, buff[3:7]) 423 | buff[7] = up.AdminLevel 424 | copy(buff[8:], up.Name) 425 | allPlayers[i].writeBlocking_Bl(buff[0 : 8+l]) 426 | allPlayers[i].ReportEquipment_Bl(up) 427 | } 428 | case CMD_VRFY_SUPERCHUNCK_CS: 429 | if length < 10 || ((length-3)%7 != 0) { 430 | // fmt.Printf("Verify Checksum length error!") 431 | log.Printf("CMD_VRFY_CHUNK_CS illegal length: %v\n", buff[0:length]) 432 | } else { 433 | // A list of chunk checksums can be recieved, these should be verified and if the 434 | // checksum is not correct, the updated block should be sent 435 | // fmt.Printf("CMD_VRFY_CHUNK_CS - %v %v %v %v\n", buff[6], buff[7], buff[8], buff[9] ) 436 | up.CommandVerifySuperchunkCS_Bl(buff[3:length]) 437 | } 438 | case CMD_TELEPORT: 439 | up.Teleport(buff[3:length]) 440 | case CMD_ERROR_REPORT: 441 | log.Printf("Error message for %v: %s\n", up.Name, string(buff[3:length])) 442 | default: 443 | log.Printf("Unknown command '%v'.\n", buff[0:length]) 444 | return 445 | } 446 | if length < n { 447 | // TODO: There are more commands still in the data, go to next 448 | panic("Too much") 449 | } 450 | } 451 | } 452 | 453 | func MakeCommandReadChunk_WLwWLcBl(i int, b []byte) { 454 | // fmt.Printf("MakeCommandReadChunk index %v argument %v\n", i, b) 455 | x, b, _ := ParseUint32(b) 456 | y, b, _ := ParseUint32(b) 457 | z, b, _ := ParseUint32(b) 458 | up := allPlayers[i] 459 | up.CmdReadChunk_WLwWLcBl(chunkdb.CC{X: int32(x), Y: int32(y), Z: int32(z)}) 460 | } 461 | 462 | func MakeDirectionsCommand(i int, b []byte) { 463 | hdir, b2, ok := ParseUint16(b) 464 | if !ok { 465 | log.Printf("MakeDirectionsCommand: Bad hor dir in %v\n", b) 466 | return 467 | } 468 | vdir, _, ok := ParseUint16(b2) 469 | if !ok { 470 | log.Printf("MakeDirectionsCommand: Bad vert dir in %v\n", b) 471 | return 472 | } 473 | dirHor := float32(hdir) / 100.0 474 | dirVert := float32(int16(vdir)) / 100.0 475 | CmdSetDirections(i, dirHor, dirVert) 476 | } 477 | 478 | func (up *user) ManageAttackPeriod_WLuBl(delta time.Duration) { 479 | mp := up.aggro 480 | dist2 := float64(0) // Distance to monster, squared 481 | if mp != nil { 482 | dx := up.Coord.X - mp.Coord.X 483 | dy := up.Coord.Y - mp.Coord.Y 484 | dz := up.Coord.Z - mp.Coord.Z 485 | dist2 = dx*dx + dy*dy + dz*dz 486 | if dist2 > CnfgMonsterAggroDistance*CnfgMonsterAggroDistance { 487 | mp = nil 488 | up.Printf_Bl("Too far away for combat") 489 | } 490 | if mp == nil || mp.invalid || mp.HitPoints <= 0 || mp.dead || up.Dead { 491 | up.Lock() 492 | up.aggro = nil 493 | up.flags &= ^UserFlagInFight 494 | up.Unlock() 495 | up.updatedStats = true 496 | } else { 497 | if dist2 <= CnfgMeleeDistLimit*CnfgMeleeDistLimit { 498 | // Close enough to hit 499 | mp.Hit_WLuBl(up, 1) 500 | } 501 | } 502 | } else if !up.Dead && up.aggro == nil && (up.HitPoints != 1 || up.Mana != 1) { 503 | // Restore health and mana for player if not being dead and not attacking 504 | up.HealthAndMana_WLu(delta) 505 | } 506 | } 507 | 508 | // Heal the player and restore mana, depending on how much time has passed. 509 | func (up *user) HealthAndMana_WLu(delta time.Duration) { 510 | up.Lock() 511 | newHP := up.HitPoints + float32(delta)/CnfgHealingPeriod 512 | if newHP > 1 { 513 | newHP = 1 514 | } 515 | if newHP > up.HitPoints { 516 | up.HitPoints = newHP 517 | up.updatedStats = true 518 | } 519 | up.Unlock() 520 | // The mana is only controlled by the ManageOneClient2() function (which called this function), and so there is no need for a lock. 521 | newMana := up.Mana + float32(delta)/CnfgHealingPeriod 522 | if newMana > 1 { 523 | newMana = 1 524 | } 525 | if newMana > up.Mana { 526 | up.Mana = newMana 527 | up.updatedStats = true 528 | } 529 | } 530 | --------------------------------------------------------------------------------