Browse Source

Server now has supervisor that enables restart on the fly. Starting with command actions.

Beoran 8 years ago
parent
commit
9a25d76146

+ 1 - 3
README

@@ -1,8 +1,6 @@
 WOE - Workers of Eruta 
 
-Woe is a MUD server implemented in ruby, used to implement the Workers of Eruta 
-MUD.
+Woe is a MUD server implemented in Go Language, used to implement the Workers of Eruta  MUD.
 
 Workers of Eruta is set in the land of Eruta, 60000 years in the future.
 
-

+ 1 - 0
data/var/account/B/Beoran/Beoran.account

@@ -1,4 +1,5 @@
 :name:Beoran
 :algo:plain
 :pass:hello
+:privilege:500
 --

+ 62 - 0
src/woe/server/action.go

@@ -0,0 +1,62 @@
+package server
+
+// import "github.com/beoran/woe/telnet"
+import "github.com/beoran/woe/world"
+import "github.com/beoran/woe/monolog"
+
+
+/* Actiondata are the params passed to an Actin, neatly wrapped in 
+ * a struct */
+type ActionData struct {
+    Client * Client
+    Server * Server
+    World  * world.World
+    Action * Action
+    Command  string
+    Argv   []string
+}
+
+/* A handler for an action. */
+type ActionHandler func(data * ActionData) (err error) 
+
+/* Actions that  a client can perform on a server or in 
+ * the server's world. */
+type Action struct {
+    Name        string
+    Privilege   world.Privilege
+    Handler     ActionHandler
+}
+
+
+var ActionMap map[string] Action
+
+func AddAction(name string, privilege world.Privilege, handler ActionHandler) {
+    monolog.Info("Adding new action %s with privilege %d", name, privilege)
+    action := Action{name, privilege, handler}
+    ActionMap[name] = action
+}
+
+func doShutdown(data * ActionData) (err error) {
+    return nil
+}
+
+func doRestart(data * ActionData) (err error) {
+    return nil
+}
+
+
+func doQuit(data * ActionData) (err error) {    
+    return nil
+}
+
+func ParseCommand(command string, data * ActionData) {
+    data.Command = command
+} 
+
+func init() {
+    ActionMap = make(map[string] Action)
+    AddAction("/shutdown"   , world.PRIVILEGE_LORD, doShutdown)
+    AddAction("/restart"    , world.PRIVILEGE_LORD, doRestart)
+    AddAction("/quit"       , world.PRIVILEGE_ZERO, doQuit)
+}
+

+ 31 - 4
src/woe/server/ask.go

@@ -9,7 +9,7 @@ import "github.com/beoran/woe/world"
 import "github.com/beoran/woe/monolog"
 import "bytes"
 import "regexp"
-import "fmt"
+// import "fmt"
 // import "strconv"
 
 
@@ -101,6 +101,7 @@ func (me * Client) AskSomething(prompt string, re string, nomatch_prompt string,
     
     if noecho {
       me.NormalMode()
+      me.Printf("\n")
     }
     
     return something
@@ -132,9 +133,14 @@ func (me * Client) HandleCommand() {
     if bytes.HasPrefix(command, []byte("/quit")) {
       me.Printf("Byebye!\n")
       me.alive = false
+    } else if bytes.HasPrefix(command, []byte("/shutdown")) {
+      me.server.Broadcast("Shutting down server NOW!\n")
+      me.server.Shutdown();
+    } else if bytes.HasPrefix(command, []byte("/restart")) {
+      me.server.Broadcast("Restarting down server NOW!\n")
+      me.server.Restart();
     } else {
-      bro := fmt.Sprintf("Client %d said %s\r\n", me.id, command)  
-      me.server.Broadcast(bro)
+      me.server.Broadcast("Client %d said %s\r\n", me.id, command)  
     }
 }
  
@@ -196,7 +202,14 @@ func (me * Client) AccountDialog() bool {
     login  := me.AskLogin()
     if login == nil { return false }
     var err error
-    me.account, err = world.LoadAccount(me.server.DataPath(), string(login))    
+    
+    if me.server.World.GetAccount(string(login)) != nil {
+        me.Printf("Account already logged in!\n")
+        me.Printf("Disconnecting!\n")
+        return false 
+    }
+    
+    me.account, err = me.server.World.LoadAccount(me.server.DataPath(), string(login))    
     if err != nil {
         monolog.Warning("Could not load account %s: %v", login, err)  
     }
@@ -207,4 +220,18 @@ func (me * Client) AccountDialog() bool {
     }
 }
  
+func (me * Client) CharacterDialog() bool {
+    login  := me.AskLogin()
+    if login == nil { return false }
+    var err error
+    me.account, err = world.LoadAccount(me.server.DataPath(), string(login))    
+    if err != nil {
+        monolog.Warning("Could not load account %s: %v", login, err)  
+    }
+    if me.account != nil {
+      return me.ExistingAccountDialog()
+    } else {
+      return me.NewAccountDialog(string(login))
+    }
+}
 

+ 5 - 1
src/woe/server/client.go

@@ -56,6 +56,10 @@ func NewClient(server * Server, id int, conn net.Conn) * Client {
 func (me * Client) Close() {
     me.conn.Close()
     me.alive = false
+    if me.account != nil {     
+        me.server.World.RemoveAccount(me.account.Name)
+    }
+    me.account = nil
 }
 
 /** Goroutine that does the actual reading of input data, and sends it to the 
@@ -68,7 +72,7 @@ func (me * Client) ServeRead() {
             me.errchan <- err
             return
         }
-        // reply will be stored in me.telnet.Events
+        // reply will be stored in me.telnet.Events channel
         me.telnet.ProcessBytes(buffer[:read])
     }
 }

+ 76 - 21
src/woe/server/server.go

@@ -16,7 +16,11 @@ import (
 
 var MSSP map[string] string
 
-const MAX_CLIENTS = 1000
+const STATUS_OK             = 0
+const STATUS_CANNOT_LISTEN  = 1
+const STATUS_RESTART        = 0
+const STATUS_SHUTDOWN       = 4
+const MAX_CLIENTS           = 1000
 
 func init() {
      MSSP = map[string] string {
@@ -76,6 +80,7 @@ type Server struct {
     tickers map[string] * Ticker
     alive                 bool
     World               * world.World
+    exitstatus            int
 }
 
 
@@ -88,7 +93,7 @@ type Ticker struct {
 }
 
 
-const DEFAULT_MOTD =
+const DEFAULT_MOTD_OK =
 
 `
 ###############################
@@ -97,10 +102,21 @@ const DEFAULT_MOTD =
 
 `
 
+const DEFAULT_MOTD =
+`
+Welcome!
+`
+
 
-func (me * Server) SetupWorld() error {
-    /*
-    me.World, _ = world.LoadWorld(me.DataPath(), "WOE")
+
+func (me * Server) SetupWorld() (err error) {
+    me.World, err = world.LoadWorld(me.DataPath(), "WOE")
+    
+    if err != nil { 
+        monolog.Error("Could not load world WOE: %s", err)
+        return err
+    }
+    
     if me.World == nil {
         monolog.Info("Creating new default world...")
         me.World = world.NewWorld("WOE", DEFAULT_MOTD)
@@ -112,7 +128,6 @@ func (me * Server) SetupWorld() error {
             monolog.Info("Saved default world.")
         }
     }
-    */
     return nil
 }
 
@@ -120,7 +135,6 @@ func (me * Server) SetupWorld() error {
 func NewServer(address string) (server * Server, err error) {
     listener, err := net.Listen("tcp", address);
     if (err != nil) { 
-        io.Printf("")
         return nil, err
     }
     
@@ -133,16 +147,13 @@ func NewServer(address string) (server * Server, err error) {
     clients := make(map[int] * Client)
     tickers := make(map[string] * Ticker)
 
-    server = &Server{address, listener, logger, logfile, clients, tickers, true, nil}
+    server = &Server{address, listener, logger, logfile, clients, tickers, true, nil, STATUS_RESTART}
     err = server.SetupWorld()
     server.AddDefaultTickers()
     
     return server, err
 }
 
-func (me * Server) Close() {
-    me.logfile.Close();
-}
 
 
 func NewTicker(server * Server, name string, milliseconds int, callback func (me * Ticker, t time.Time) bool) (* Ticker) {
@@ -180,8 +191,6 @@ func (me * Server) StopTicker(name string) {
     ticker.Stop();
 }
 
-
-
 func (me * Server) AddTicker(name string, milliseconds int, callback func (me * Ticker, t time.Time) bool) (* Ticker) {
     _, have := me.tickers[name]
     
@@ -198,17 +207,17 @@ func (me * Server) AddTicker(name string, milliseconds int, callback func (me *
 
 
 func onWeatherTicker (me * Ticker, t time.Time) bool {
-    monolog.Info("Weather Ticker tick tock.")
+    me.Server.Broadcast("The weather is changing...\n")
     return true
 }
 
 
 func (me * Server) AddDefaultTickers() {
-    me.AddTicker("weather", 10000, onWeatherTicker)    
+    me.AddTicker("weather", 30000, onWeatherTicker)    
 }
 
 func (me * Server) handleDisconnectedClients() {
-    for { 
+    for me.alive { 
         time.Sleep(1)
         for id, client := range me.clients {
             if (!client.IsAlive()) {
@@ -243,24 +252,65 @@ func (me * Server) onConnect(conn net.Conn) (err error) {
     return client.Serve()
 }
 
-func (me * Server) Serve() (err error) { 
+func (me * Server) Shutdown() {
+    monolog.Info("Server is going to shut down.")
+    me.alive        = false
+    me.exitstatus   = STATUS_SHUTDOWN
+}
+
+func (me * Server) Restart() {
+    monolog.Info("Server is going to restart.")
+    me.alive        = false
+    me.exitstatus   = STATUS_RESTART
+}
+
+
+func (me * Server) Close() {
+    monolog.Info("Closing server, shutting down tickers.")
+    
+    for name, _ := range me.tickers {
+        me.RemoveTicker(name);
+    }
+
+    monolog.Info("Closing server, shutting down clients.")
+    for _, client := range me.clients {
+        if (client.IsAlive()) {
+            client.Close()
+        }
+    }
+    
+    me.handleDisconnectedClients()
+    monolog.Info("Closing server, closing logfile.")
+    me.logfile.Close();
+}
+
+func (me * Server) Serve() (status int, err error) { 
     // Setup random seed here, or whatever
     rand.Seed(time.Now().UTC().UnixNano())
     
     go me.handleDisconnectedClients()
     
     for (me.alive) {
+        if tcplistener, ok := me.listener.(*net.TCPListener) ; ok {
+          tcplistener.SetDeadline(time.Now().Add(5*time.Second))
+        }
         conn, err := me.listener.Accept()
         if err != nil {
-            return err
+            if noe, ok := err.(*net.OpError) ; ok && noe.Timeout() {
+                // it's a timeout. Do nothing, just listen again.
+                // this to allow the alive flag to do it's work.
+            } else {
+                return STATUS_CANNOT_LISTEN, err
+            }
+        } else {
+            go me.onConnect(conn)
         }
-        go me.onConnect(conn)
     }
-    return nil
+    return me.exitstatus, nil
 }
 
 
-func (me * Server) Broadcast(message string) {
+func (me * Server) BroadcastString(message string) {
     for _, client := range me.clients {
         if (client.IsAlive()) {
             client.WriteString(message)
@@ -268,6 +318,11 @@ func (me * Server) Broadcast(message string) {
     }       
 }
 
+func (me * Server) Broadcast(format string, args ...interface{}) {
+    msg := fmt.Sprintf(format, args...)
+    me.BroadcastString(msg)
+}
+
 
 // Returns the data path of the server
 func (me * Server) DataPath() string {

+ 2 - 135
src/woe/server/setuptelnet.go

@@ -217,7 +217,7 @@ func (me * Client) SetupTelnet() {
     me.SetupMXP()
     me.SetupMSP()
     me.SetupMSDP()
-    // color_test
+    // me.ColorTest()
 }
 
 
@@ -248,95 +248,8 @@ func (me * Client) SetupTelnet() {
     return nil
   end
   
-  def color_test
-    self.write("\e[1mBold\e[0m\r\n")
-    self.write("\e[3mItalic\e[0m\r\n")
-    self.write("\e[4mUnderline\e[0m\r\n")
-    30.upto(37) do | fg |
-      self.write("\e[#{fg}mForeground Color #{fg}\e[0m\r\n")
-      self.write("\e[1;#{fg}mBold Foreground Color #{fg}\e[0m\r\n")
-    end  
-    40.upto(47) do | bg |
-      self.write("\e[#{bg}mBackground Color #{bg}\e[0m\r\n")
-      self.write("\e[1;#{bg}mBold Background Color #{bg}\e[0m\r\n")
-    end    
   end
-  
-  def setup_telnet
-    loop do
-      tev = wait_for_input(0.5)
-      if tev
-        p "setup_telnet", tev
-      else
-        p "no telnet setup received..."
-        break
-      end
-    end
-    setup_mssp
-    setup_compress2
-    setup_naws
-    setup_ttype
-    setup_mxp
-    setup_msp
-    setup_msdp
-    # color_test
-    
-    
-    #p "mssp ev #{tev}"
-    # @telnet.telnet_send_negotiate(TELNET_WILL, TELNET_TELOPT_MSSP)        
-    # tev = wait_for_input(0.5)
-    # p "mssp ev #{tev}"
-    
-    # @telnet.telnet_ttype_send
-    
-    
-  end
- 
-  LOGIN_RE = /\A[A-Za-z][A-Za-z0-9]*\Z/
-  
-  def ask_something(prompt, re, nomatch_prompt, noecho=false)
-    something = nil
-    
-    if noecho
-      password_mode
-    end
 
-    while  something.nil? || something.empty? 
-      write("#{prompt}:")
-      something = wait_for_command
-      if something
-          something.chomp!
-        if re && something !~ re
-          write("\r\n#{nomatch_prompt}\r\n")
-          something = nil
-        end
-      end
-    end
-    
-    if noecho
-      normal_mode
-    end
-    
-    something.chomp!
-    return something
-  end
-  
-  
-  
-  def ask_login
-    return ask_something("Login", LOGIN_RE, "Login must consist of a letter followed by letters or numbers.")
-  end
-
-  EMAIL_RE = /@/
-
-  def ask_email
-    return ask_something("E-mail", EMAIL_RE, "Email must have at least an @ in there somewhere.")
-  end
-
-
-  def ask_password(prompt = "Password")
-    return ask_something(prompt, nil, "", true) 
-  end
   
   def handle_command
     order = wait_for_command
@@ -348,51 +261,5 @@ func (me * Client) SetupTelnet() {
       @server.broadcast("#{@account.id} said #{order}\r\n")
     end
   end
-  
-  def existing_account_dialog
-    pass  = ask_password
-    return false unless pass
-    unless @account.challenge?(pass)
-      printf("Password not correct!\n")
-      return false
-    end
-    return true
-  end
-  
-  def new_account_dialog(login)
-    while !@account 
-      printf("\nWelcome, %s! Creating new account...\n", login)
-      pass1  = ask_password
-      return false unless pass1
-      pass2 = ask_password("Repeat Password")
-      return false unless pass2
-      if pass1 != pass2
-        printf("\nPasswords do not match! Please try again!\n")
-        next
-      end
-      email = ask_email
-      return false unless email
-      @account = Woe::Account.new(:id => login, :email => email )
-      @account.password   = pass1
-      @account.woe_points = 7
-      unless @account.save_one
-        printf("\nFailed to save your account! Please contact a WOE administrator!\n")
-        return false
-      end
-      printf("\nSaved your account.\n")
-      return true
-    end
-  end
-  
-  def account_dialog
-    login  = ask_login
-    return false unless login
-    @account = Account.fetch(login)
-    if @account
-      return existing_account_dialog
-    else
-      return new_account_dialog(login)
-    end
-  end
- 
+   
 */

+ 8 - 0
src/woe/sitef/marshal.go

@@ -3,3 +3,11 @@ package sitef
 
 // Marshallling of structs from and to sitef format
 
+/*
+func Marshal(value interface{}) (byte [], error)
+
+st := reflect.TypeOf(s)
+field := st.Field(0)
+fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))
+
+*/

+ 58 - 0
src/woe/sitef/sitef.go

@@ -6,6 +6,7 @@ import "strings"
 import "fmt"
 import "bytes"
 import "bufio"
+import "strconv"
 
 // Sitef format for serialization
 // Sitef is a simple text format for serializing data to
@@ -36,6 +37,63 @@ import "bufio"
 
 type Record map[string]string
 
+func (me * Record) Put(key string, val string) {
+    (*me)[key] = val
+}
+
+func (me * Record) Putf(key string, format string, values ...interface{}) {
+    me.Put(key, fmt.Sprintf(format, values...)) 
+}
+
+func (me * Record) PutArray(key string, values []string) {
+    for i, value := range values {
+        realkey := fmt.Sprintf("%s[%d]", key, i)
+        me.Put(realkey, value)
+    }
+} 
+
+func (me * Record) PutInt(key string, val int) {
+    me.Putf(key, "%d", val)
+}
+
+func (me * Record) PutFloat64(key string, val float64) {
+    me.Putf(key, "%lf", val)
+}
+
+
+func (me Record) Get(key string) (result string) {
+    return me[key]
+}
+
+func (me Record) Getf(key string, format string, 
+    values ...interface{}) (amount int, ok bool) {
+    val := me.Get(key)
+    count, err := fmt.Sscanf(val, format, values...)
+    if err != nil {
+        return 0, false
+    }
+    return count, true
+}
+
+func (me Record) GetInt(key string) (val int, err error) {
+    i, err := strconv.ParseInt(me.Get(key), 0, 0)
+    return int(i), err
+}
+
+func (me Record) GetIntDefault(key string, def int) (val int) {
+    i, err := strconv.ParseInt(me.Get(key), 0, 0)
+    if err != nil {
+        return def;
+    }
+    return int(i);
+}
+
+
+func (me Record) GetFloat(key string) (val float64, error error) {
+    return strconv.ParseFloat(me.Get(key), 64)
+}
+
+
 type Error struct {
     error   string
     lineno  int

+ 61 - 5
src/woe/woe.go

@@ -3,20 +3,76 @@ package main
 // import "fmt"
 import "github.com/beoran/woe/server"
 import "github.com/beoran/woe/monolog"
+import "os"
+import "os/exec"
+import "flag"
+import "fmt"
 
+/* Command line flags. */
+var server_mode  = flag.Bool("s", false, "Run in server mode");
+var server_tcpip = flag.String("l", ":7000", "TCP/IP Address where the server will listen");
+ 
+/* Need to restart the server or not? */
+var server_restart = true
 
-
-func main() {
+func runServer() (status int) {
     monolog.Setup("woe.log", true, false)
     defer monolog.Close()
     monolog.Info("Starting WOE!")
-    monolog.Info("Server runs at port %d!", 7000)
-    woe, err := server.NewServer(":7000")
+    monolog.Info("Server runs at %s!", *server_tcpip)
+    woe, err := server.NewServer(*server_tcpip)
     if err != nil {
         monolog.Error(err.Error())
         panic(err)
     }
     defer woe.Close()
-    woe.Serve()
+    status, err = woe.Serve()
+    if err != nil {
+        monolog.Error(err.Error())
+        panic(err)
+    }
+    return status
+}
+
+
+
+func runSupervisor() (status int) {
+    monolog.Setup("woe.log", true, false)
+    defer monolog.Close()
+    monolog.Info("Starting WOE supervisor!")
+    for (server_restart) {
+        // wd  , _ := os.Getwd()
+        exe  := fmt.Sprintf("%s", os.Args[0]) 
+        argp := fmt.Sprintf("-l=%s", *server_tcpip)
+        cmd  := exec.Command(exe, "-s=true", argp)
+        monolog.Info("Starting server %s at %s!", exe, *server_tcpip)
+        cmd.Stderr = os.Stderr
+        cmd.Stdout = os.Stdout
+        err  := cmd.Run()
+        monolog.Info("Server at %s shut down!", *server_tcpip)
+        // monolog.Info("Server output: %s!", out);
+        if (err != nil ) { 
+            monolog.Error("Server shut down with error %s!", err)
+            server_restart = false;
+            return 1
+        }
+    }
+    return 0
+}
+
+
+
+/* Woe can be run in supervisor mode (the default) or server mode (-s).
+ * Server mode is the mode in which the real server is run. In supervisor mode, 
+ * woe runs a single woe server in server mode using os/exec. This is used to 
+ * be able to restart the server gracefully on recompile of the sources. 
+ */
+func main() {
+    flag.Parse()
+    if *server_mode {
+        os.Exit(runServer())
+    } else {
+        os.Exit(runSupervisor())
+    }
 }
 

+ 26 - 16
src/woe/world/account.go

@@ -7,8 +7,16 @@ import "github.com/beoran/woe/sitef"
 import "github.com/beoran/woe/monolog"
 import "fmt"
 import "errors"
-import "strconv"
 
+type Privilege int
+
+const (
+    PRIVILEGE_ZERO        = Privilege(iota * 100)
+    PRIVILEGE_NORMAL
+    PRIVILEGE_MASTER
+    PRIVILEGE_LORD
+    PRIVILEGE_IMPLEMENTOR
+)
 
 
 type Named struct {
@@ -22,6 +30,7 @@ type Account struct {
     Algo              string
     Email             string
     Points            int
+    Privilege         Privilege
     CharacterNames  []string
     characters      [] * Character
 }
@@ -37,7 +46,7 @@ func SavePathFor(dirname string, typename string, name string) string {
 
 
 func NewAccount(name string, pass string, email string, points int) (*Account) {
-    return &Account{name, pass, "plain", email, points, nil, nil}
+    return &Account{name, pass, "plain", email, points, PRIVILEGE_NORMAL, nil, nil}
 }
 
 // Password Challenge for an account.
@@ -69,15 +78,16 @@ func (me * Account) Save(dirname string) (err error) {
     path := SavePathFor(dirname, "account", me.Name)
     
     rec                := make(sitef.Record)
-    rec["name"]         = me.Name
-    rec["hash"]         = me.Hash
-    rec["algo"]         = me.Algo
-    rec["email"]        = me.Email
-    rec["points"]       = fmt.Sprintf("%d", me.Points)
-    rec["characters"]   = fmt.Sprintf("%d", len(me.characters))
+    rec.Put("name",         me.Name)
+    rec.Put("hash",         me.Hash)
+    rec.Put("algo",         me.Algo)
+    rec.Put("email",        me.Email)
+    rec.PutInt("points",    me.Points)
+    rec.PutInt("privilege", int(me.Privilege))
+    rec.PutInt("characters",len(me.characters))
     for i, chara   := range me.characters {
         key        := fmt.Sprintf("characters[%d]", i)
-        rec[key]    = chara.Name
+        rec.Put(key, chara.Name)
     }
     monolog.Debug("Saving Acccount record: %s %v", path, rec)
     return sitef.SaveRecord(path, rec)
@@ -101,20 +111,20 @@ func LoadAccount(dirname string, name string) (account *Account, err error) {
     monolog.Info("Loading Account record: %s %v", path, record)
     
     account = new(Account)
-    account.Name            = record["name"]
-    account.Hash            = record["hash"]
-    account.Algo            = record["algo"]
-    account.Email           = record["email"]
-    account.Points,  err    = strconv.Atoi(record["points"])    
+    account.Name            = record.Get("name")
+    account.Hash            = record.Get("hash")
+    account.Algo            = record.Get("algo")
+    account.Email           = record.Get("email")
+    account.Points,  err    = record.GetInt("points")
     if err != nil {
         account.Points = 0
     }
     var nchars int
-    nchars,  err            = strconv.Atoi(record["characters"])
+    nchars,  err            = record.GetInt("characters")
     if err != nil {
         nchars = 0
     } 
-    _ = nchars   
+    _ = nchars
     /* Todo: load characters here... */    
     monolog.Info("Loaded Account: %s %v", path, record)
     return account, nil

+ 1 - 0
src/woe/world/being.go

@@ -3,6 +3,7 @@ package world
 import (
     "fmt"
     "strings"
+    //"github.com/beoran/woe/sitef"
 )
 
 /* Aptitudes of a being, species or profession */

+ 70 - 0
src/woe/world/character.go

@@ -1,5 +1,12 @@
 package world
 
+import "fmt"
+// import "strconv"
+import "github.com/beoran/woe/monolog"
+import "github.com/beoran/woe/sitef"
+
+/* Characters may exist "outside" of any world, because, at login of a client,
+ * the Character must be loaded or created before it enters a World. */
 
 type Character struct {
     Being       
@@ -8,3 +15,66 @@ type Character struct {
 }
 
 
+func NewCharacter(being Being, accountname string, account * Account) (*Character) {
+    return &Character{being, accountname, account}
+}
+
+
+// Save a character as a sitef record.
+func (me * Character) SaveSitef(rec sitef.Record) (err error) {
+    rec["accountname"]  = me.AccountName
+    // TODO: saving: me.Being.SaveSitef(rec)
+    return nil
+}
+
+// Load a character from a sitef record.
+func (me * Character) LoadSitef(rec sitef.Record) (err error) {
+    me.AccountName = rec["accountname"] 
+    // TODO: load being. me.Being.SaveSitef(rec)
+    return nil
+}
+
+
+// Save a character as a sitef file.
+func (me * Character) Save(dirname string) (err error) {
+    path := SavePathFor(dirname, "character", me.Name)
+    
+    rec                := make(sitef.Record)
+    me.SaveSitef(rec)
+    monolog.Debug("Saving Character record: %s %v", path, rec)
+    return sitef.SaveRecord(path, rec)
+}
+
+// Load an character from a sitef file.
+func LoadCharacter(dirname string, name string) (character *Character, err error) {
+    
+    path := SavePathFor(dirname, "character", name)
+    
+    records, err := sitef.ParseFilename(path)
+    if err != nil {
+        return nil, err
+    }
+    
+    if len(records) < 1 {
+        return nil, fmt.Errorf("No sitef record found for %s!", name)
+    }
+    
+    record := records[0]
+    monolog.Info("Loading Account record: %s %v", path, record)
+    
+    character               = new(Character)
+    character.AccountName   = record["AccountName"]
+    account, err           := DefaultWorld.LoadAccount(dirname, character.AccountName);
+    if err != nil  {
+        return nil, err
+    } 
+    
+    if account == nil {
+        return nil, fmt.Errorf("Cound not load account %s for character %s", 
+            character.AccountName, character.Name)
+    }
+    
+    character.account = account
+    
+    return character, nil
+}

+ 61 - 27
src/woe/world/world.go

@@ -1,8 +1,8 @@
 package world
 
-import "os"
-import "encoding/xml"
 import "github.com/beoran/woe/monolog"
+import "github.com/beoran/woe/sitef"
+import "errors"
 
 /* Elements of the WOE game world.  
  * Only Zones, Rooms and their Exits, Items, 
@@ -29,6 +29,8 @@ type World struct {
     items                []   Item
     mobilemap       map[ID] * Mobile
     mobiles              []   Mobile
+    accounts             [] * Account
+    accountmap      map[string] * Account
 }
 
 
@@ -47,6 +49,7 @@ func NewWorld(name string, motd string) (*World) {
     world := new(World)
     world.Name = name
     world.MOTD = motd
+    world.accountmap = make(map[string] * Account)
     
     world.AddWoeDefaults()
     return world;
@@ -70,44 +73,75 @@ func (me * World) AddZone(zone * Zone) {
     me.AddEntity(&zone.Entity);
 }
 
+// Save an account as a sitef file.
 func (me * World) Save(dirname string) (err error) {
     path := SavePathFor(dirname, "world", me.Name)
     
-    file, err := os.Create(path)
-    if err != nil {
-        monolog.Error("Could not load %name: %v", err)
-        return err
-    }
-    enc := xml.NewEncoder(file)
-    enc.Indent(" ", "  ")
-    res := enc.Encode(me)
-    if (res != nil) {
-        monolog.Error("Could not save %s: %v", me.Name, err)
-    }
-    return res
-}
-
-func (me * World) onLoad() {
+    rec                := make(sitef.Record)
+    rec.Put("name",         me.Name)
+    rec.Put("motd",         me.MOTD)
+    monolog.Debug("Saving World record: %s %v", path, rec)
+    return sitef.SaveRecord(path, rec)
 }
 
-func LoadWorld(dirname string, name string) (result *World, err error) {
+// Load a world from a sitef file.
+func LoadWorld(dirname string, name string) (world * World, err error) {
+    
     path := SavePathFor(dirname, "world", name)
     
-    file, err := os.Open(path)
+    records, err := sitef.ParseFilename(path)
     if err != nil {
         return nil, err
     }
-    dec := xml.NewDecoder(file)    
-    result = new(World)
-    err = dec.Decode(result)
-    if err != nil {
-        monolog.Error("Could not load %s: %v", name, err)
-        panic(err)
+    
+    if len(records) < 1 {
+        return nil, errors.New("No record found!")
     }
     
-    result.onLoad()
-    return result, nil
+    record := records[0]
+    monolog.Info("Loading World record: %s %v", path, record)
+    
+    world = NewWorld(record.Get("name"), record.Get("motd"))
+    monolog.Info("Loaded World: %s %v", path, world)
+    return world, nil
+}
+
+
+// Returns an acccount that has already been loaded or nil if not found
+func (me * World) GetAccount(name string) (account * Account) {
+    account, ok := me.accountmap[name];
+    if !ok {
+        return nil
+    }
+    return account
+} 
+
+// Loads an account to be used with this world. Characters will be linked.
+// If the account was already loaded, returns that in stead.
+func (me * World) LoadAccount(dirname string, name string) (account *Account, err error) {
+    account = me.GetAccount(name)
+    if (account != nil) {
+        return account, nil
+    }
+    
+    account, err = LoadAccount(dirname, name);
+    if err != nil {
+        return account, err
+    }
+    me.accountmap[account.Name] = account
+    return account, nil
+}
+
+// Removes an account from this world by name.
+func (me * World) RemoveAccount(name string) {
+    _, have := me.accountmap[name]
+    if (!have) {
+        return
+    }    
+    delete(me.accountmap, name)
 }
 
+// Default world pointer
+var DefaultWorld * World