Browse Source

Working on data structures for world entities.

Beoran 9 năm trước cách đây
mục cha
commit
90af9c9dcc

+ 207 - 1
src/woe/server/ask.go

@@ -1,4 +1,210 @@
 package server
 package server
 
 
-/* This file contains dialog helpers fort the client. */
+/* This file contains dialog helpers for the client. */
+
+// import "github.com/beoran/woe/monolog"
+import t "github.com/beoran/woe/telnet"
+import "github.com/beoran/woe/telnet"
+import "github.com/beoran/woe/world"
+import "github.com/beoran/woe/monolog"
+import "bytes"
+import "regexp"
+import "fmt"
+// import "strconv"
+
+
+  
+// Switches to "password" mode.
+func (me * Client) PasswordMode() telnet.Event {
+// The server sends "IAC WILL ECHO", meaning "I, the server, will do any 
+// echoing from now on." The client should acknowledge this with an IAC DO 
+// ECHO, and then stop putting echoed text in the input buffer. 
+// It should also do whatever is appropriate for password entry to the input 
+// box thing - for example, it might * it out. Text entered in server-echoes 
+// mode should also not be placed any command history.
+// don't use the Q state machne for echos
+    me.telnet.TelnetSendBytes(t.TELNET_IAC, t.TELNET_WILL, t.TELNET_TELOPT_ECHO)
+    tev, _, _:= me.TryReadEvent(100)
+    if tev != nil && !telnet.IsEventType(tev, t.TELNET_DO_EVENT) { 
+        return tev
+    }
+    return nil
+}
+
+// Switches to "normal, or non-password mode.
+func (me * Client) NormalMode() telnet.Event {
+// When the server wants the client to start local echoing again, it s}s 
+// "IAC WONT ECHO" - the client must respond to this with "IAC DONT ECHO".
+// Again don't use Q state machine.   
+    me.telnet.TelnetSendBytes(t.TELNET_IAC, t.TELNET_WONT, t.TELNET_TELOPT_ECHO)
+    tev, _, _ := me.TryReadEvent(100)
+    if tev != nil && !telnet.IsEventType(tev, t.TELNET_DONT_EVENT) { 
+        return tev
+    }
+    return nil
+}
+
+func (me * Client) Printf(format string, args ...interface{}) {
+    me.telnet.TelnetPrintf(format, args...)
+}
+
+func (me * Client) ColorTest() {
+    me.Printf("\033[1mBold\033[0m\r\n")
+    me.Printf("\033[3mItalic\033[0m\r\n")
+    me.Printf("\033[4mUnderline\033[0m\r\n")
+    for fg := 30; fg < 38; fg++ {
+        me.Printf("\033[%dmForeground Color %d\033[0m\r\n", fg, fg)
+        me.Printf("\033[1;%dmBold Foreground Color %d\033[0m\r\n", fg, fg)
+    }
+    
+    for bg := 40; bg < 48; bg++ {
+        me.Printf("\033[%dmBackground Color %d\033[0m\r\n", bg, bg)
+        me.Printf("\033[1;%dmBold Background Color %d\033[0m\r\n", bg, bg)
+    }    
+}
+
+
+// Blockingly reads a single command from the client
+func (me * Client) ReadCommand() (something []byte) {
+    something = nil
+    for something == nil { 
+        something, _, _ = me.TryRead(-1)
+        if something != nil {
+            something = bytes.TrimRight(something, "\r\n")
+            return something
+        }
+    } 
+    return nil       
+}
+
+func (me * Client) AskSomething(prompt string, re string, nomatch_prompt string, noecho bool) (something []byte) {
+    something = nil
+    
+    if noecho {
+      me.PasswordMode()
+    }
+
+    for something == nil || len(something) == 0 { 
+        me.Printf("%s:", prompt)
+        something, _, _ = me.TryRead(-1)
+        if something != nil {
+            something = bytes.TrimRight(something, "\r\n")
+            if len(re) > 0 {
+                ok, _ := regexp.Match(re, something)
+                if !ok {
+                    me.Printf("\n%s\n", nomatch_prompt)
+                    something = nil
+                }
+            }
+        }
+    }
+    
+    if noecho {
+      me.NormalMode()
+    }
+    
+    return something
+  }
+  
+
+const LOGIN_RE = "^[A-Za-z][A-Za-z0-9]+$"
+
+func (me * Client) AskLogin() []byte {
+    return me.AskSomething("Login", LOGIN_RE, "Login must consist of a letter followed by letters or numbers.", false)
+}
+
+const EMAIL_RE = "@"
+
+func (me * Client) AskEmail() []byte {
+    return me.AskSomething("E-mail", EMAIL_RE, "Email must have at least an @ in there somewhere.", false)
+}
+
+func (me * Client) AskPassword() []byte {
+    return me.AskSomething("Password", "", "", true)
+}
+
+func (me * Client) AskRepeatPassword() []byte {
+    return me.AskSomething("Repeat Password", "", "", true)
+}
+
+func (me * Client) HandleCommand() {
+    command := me.ReadCommand()
+    if bytes.HasPrefix(command, []byte("/quit")) {
+      me.Printf("Byebye!\n")
+      me.alive = false
+    } else {
+      bro := fmt.Sprintf("Client %d said %s\r\n", me.id, command)  
+      me.server.Broadcast(bro)
+    }
+}
+ 
+func (me * Client) ExistingAccountDialog() bool {
+    pass  := me.AskPassword()
+    for pass == nil {
+        me.Printf("Password may not be empty!\n")        
+        pass  = me.AskPassword()
+    }
+    
+    if !me.account.Challenge(string(pass)) {
+        me.Printf("Password not correct!\n")
+        me.Printf("Disconnecting!\n")
+        return false
+    }    
+    return true
+}
+
+func (me * Client) NewAccountDialog(login string) bool {
+    for me.account == nil {    
+      me.Printf("\nWelcome, %s! Creating new account...\n", login)
+      pass1  := me.AskPassword()
+      
+      if pass1 == nil { 
+          return false
+      }
+      
+      pass2 := me.AskRepeatPassword()
+      
+      if pass1 == nil { 
+          return false
+      }
+      
+      if string(pass1) != string(pass2) {
+        me.Printf("\nPasswords do not match! Please try again!\n")
+        continue
+      }
+      
+      email := me.AskEmail()
+      if email == nil { return false  }
+      
+      me.account = world.NewAccount(login, string(pass1), string(email), 7)
+      err      := me.account.Save(me.server.DataPath())
+      
+      if err != nil {      
+        monolog.Error("Could not save account %s: %v", login, err)  
+        me.Printf("\nFailed to save your account!\nPlease contact a WOE administrator!\n")
+        return false
+      }
+      
+      monolog.Info("Created new account %s", login)  
+      me.Printf("\nSaved your account.\n")
+      return true
+    }
+    return false
+}
+  
+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 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))
+    }
+}
+ 
 
 

+ 25 - 4
src/woe/server/client.go

@@ -9,6 +9,7 @@ import (
     // "io"
     // "io"
     "github.com/beoran/woe/monolog"
     "github.com/beoran/woe/monolog"
     "github.com/beoran/woe/telnet"
     "github.com/beoran/woe/telnet"
+    "github.com/beoran/woe/world"
 )
 )
 
 
 /* Specific properties of a client. */
 /* Specific properties of a client. */
@@ -39,6 +40,7 @@ type Client struct {
     timechan chan time.Time 
     timechan chan time.Time 
     telnet * telnet.Telnet
     telnet * telnet.Telnet
     info     ClientInfo
     info     ClientInfo
+    account* world.Account
 }
 }
 
 
 
 
@@ -48,7 +50,7 @@ func NewClient(server * Server, id int, conn net.Conn) * Client {
     timechan := make (chan time.Time, 32)
     timechan := make (chan time.Time, 32)
     telnet   := telnet.New()
     telnet   := telnet.New()
     info     := ClientInfo{-1, -1, 0, false, false, false, false, false, false, false, false, nil, "none"}
     info     := ClientInfo{-1, -1, 0, false, false, false, false, false, false, false, false, nil, "none"}
-    return &Client{server, id, conn, true, -1, datachan, errchan, timechan, telnet, info}
+    return &Client{server, id, conn, true, -1, datachan, errchan, timechan, telnet, info, nil}
 }
 }
 
 
 func (me * Client) Close() {
 func (me * Client) Close() {
@@ -86,6 +88,16 @@ func (me * Client) ServeRead() {
 
 
 
 
 func (me * Client) TryReadEvent(millis int) (event telnet.Event, timeout bool, close bool) {
 func (me * Client) TryReadEvent(millis int) (event telnet.Event, timeout bool, close bool) {
+    var timerchan <-chan(time.Time)
+    
+    if millis >= 0 {
+        timerchan = time.Tick(time.Millisecond * time.Duration(millis))
+    } else {
+        /* If tiome is negative, block by using a fake time channel that never gets sent anyting */
+        timerchan = make(<-chan(time.Time))
+    }
+    
+    
     select {
     select {
         case event := <- me.telnet.Events:
         case event := <- me.telnet.Events:
             return event, false, false
             return event, false, false
@@ -95,7 +107,7 @@ func (me * Client) TryReadEvent(millis int) (event telnet.Event, timeout bool, c
             me.Close()
             me.Close()
             return nil, false, true
             return nil, false, true
             
             
-        case _ = <- time.Tick(time.Millisecond * time.Duration(millis)):
+        case _ = <- timerchan:
             return nil, true, false
             return nil, true, false
     }
     }
 }
 }
@@ -125,10 +137,18 @@ func (me * Client) Serve() (err error) {
     go me.ServeWrite()
     go me.ServeWrite()
     go me.ServeRead()
     go me.ServeRead()
     me.SetupTelnet()
     me.SetupTelnet()
+    if (!me.AccountDialog()) {
+        time.Sleep(3); 
+        // sleep so output gets flushed, hopefully. Also slow down brute force attacks.
+        me.Close()
+        return nil
+    }
+    
+    me.Printf("Welcome, %s\n", me.account.Name)
     
     
     for (me.alive) {
     for (me.alive) {
-        
-        
+        me.HandleCommand()
+        /*
         data, _, _ := me.TryRead(3000)
         data, _, _ := me.TryRead(3000)
         
         
         if data == nil {
         if data == nil {
@@ -136,6 +156,7 @@ func (me * Client) Serve() (err error) {
         } else {
         } else {
             me.server.Broadcast(string(data))
             me.server.Broadcast(string(data))
         }
         }
+        */ 
         
         
     }
     }
     return nil
     return nil

+ 35 - 3
src/woe/server/server.go

@@ -6,9 +6,11 @@ import (
     "net"
     "net"
   //  "errors"
   //  "errors"
     "os"
     "os"
-   "time"
-   "fmt"
-   "github.com/beoran/woe/monolog"
+    "math/rand"
+    "time"
+    "fmt"
+    "path/filepath"
+    "github.com/beoran/woe/monolog"
 )
 )
 
 
 var MSSP map[string] string
 var MSSP map[string] string
@@ -210,6 +212,9 @@ func (me * Server) onConnect(conn net.Conn) (err error) {
 }
 }
 
 
 func (me * Server) Serve() (err error) { 
 func (me * Server) Serve() (err error) { 
+    // Setup random seed here, or whatever
+    rand.Seed(time.Now().UTC().UnixNano())
+    
     go me.handleDisconnectedClients()
     go me.handleDisconnectedClients()
     
     
     for (me.alive) {
     for (me.alive) {
@@ -230,3 +235,30 @@ func (me * Server) Broadcast(message string) {
         }
         }
     }       
     }       
 }
 }
+
+
+// Returns the data path of the server
+func (me * Server) DataPath() string {
+    // 
+    cwd, err := os.Getwd();
+    if  err != nil {
+        cwd = "."
+    }
+    
+    return filepath.Join(cwd, "data", "var")
+}
+
+// Returns the script path of the server
+func (me * Server) ScriptPath() string {
+    // 
+    cwd, err := os.Getwd();
+    if err != nil {
+        cwd = "."
+    }
+    
+    return filepath.Join(cwd, "data", "script")
+}
+
+
+
+

+ 68 - 0
src/woe/world/account.go

@@ -0,0 +1,68 @@
+package world
+
+import "path/filepath"
+import "os"
+import "encoding/xml"
+
+
+type Named struct {
+    Name string
+}
+
+
+type Account struct {
+    Name              string
+    PasswordHash      string
+    PasswordAlgo      string
+    Email             string
+    WoePoints         int
+    CharacterNames  []string
+}
+
+func SavePathFor(dirname string, typename string, name string) string {
+    return filepath.Join(dirname, typename, name + ".xml")
+} 
+
+func NewAccount(name string, pass string, email string, points int) (*Account) {
+    return &Account{name, pass, "plain", email, points, nil}
+}
+
+// Password Challenge for an account.
+func (me * Account) Challenge(challenge string) bool {
+    if me.PasswordAlgo == "plain" {
+        return me.PasswordHash == challenge
+    }
+    // XXX implement encryption later
+    return false
+}
+
+func (me * Account) Save(dirname string) (err error) {
+    path := SavePathFor(dirname, "account", me.Name)
+    
+    file, err := os.Create(path)
+    if err != nil {
+        return err
+    }
+    enc := xml.NewEncoder(file)
+    enc.Indent(" ", "  ")
+    return enc.Encode(me)
+}
+
+func LoadAccount(dirname string, name string) (account *Account, err error) {
+    path := SavePathFor(dirname, "account", name)
+    
+    file, err := os.Open(path)
+    if err != nil {
+        return nil, err
+    }
+    dec := xml.NewDecoder(file)    
+    account = new(Account)
+    err = dec.Decode(account)
+    return account, nil
+}
+
+
+
+
+
+

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

@@ -0,0 +1,229 @@
+package world
+
+import (
+    "fmt"
+    "strings"
+)
+
+type BeingKind int
+
+const (
+    BEING_KIND_NONE BeingKind = iota
+    BEING_KIND_CHARACTER
+    BEING_KIND_MANTUH
+    BEING_KIND_NEOMAN
+    BEING_KIND_HUMAN    
+    BEING_KIND_CYBORG
+    BEING_KIND_ANDROID    
+    BEING_KIND_NONCHARACTER
+    BEING_KIND_MAVERICK
+    BEING_KIND_ROBOT
+    BEING_KIND_BEAST
+    BEING_KIND_SLIME
+    BEING_KIND_BIRD
+    BEING_KIND_REPTILE
+    BEING_KIND_FISH
+    BEING_KIND_CORRUPT    
+    BEING_KIND_NONHUMAN
+)
+
+func (me BeingKind) ToString() string {
+    switch me {
+        case BEING_KIND_NONE:       return "None"
+        case BEING_KIND_CHARACTER:  return "Character"
+        case BEING_KIND_MANTUH:     return "Mantuh"
+        case BEING_KIND_NEOMAN:     return "Neoman"
+        case BEING_KIND_HUMAN:      return "Human"    
+        case BEING_KIND_CYBORG:     return "Cyborg"
+        case BEING_KIND_ANDROID:    return "Android"
+        case BEING_KIND_NONCHARACTER: return "Non Character"
+        case BEING_KIND_MAVERICK:   return "Maverick"
+        case BEING_KIND_ROBOT:      return "Robot"
+        case BEING_KIND_BEAST:      return "Beast"
+        case BEING_KIND_SLIME:      return "Slime"
+        case BEING_KIND_BIRD:       return "Bird"
+        case BEING_KIND_REPTILE:    return "Reptile"
+        case BEING_KIND_FISH:       return "Fish"
+        case BEING_KIND_CORRUPT:    return "Corrupted"
+        default: return ""
+    }
+    return ""
+}
+
+
+type BeingProfession int
+
+const (
+    BEING_PROFESSION_NONE       BeingProfession = iota
+    BEING_PROFESSION_OFFICER
+    BEING_PROFESSION_WORKER
+    BEING_PROFESSION_ENGINEER
+    BEING_PROFESSION_HUNTER
+    BEING_PROFESSION_SCHOLAR
+    BEING_PROFESSION_MEDIC
+    BEING_PROFESSION_CLERIC  
+    BEING_PROFESSION_ROGUE
+)
+
+func (me BeingProfession) ToString() string {
+    return ""
+}
+
+
+/* Vital statistic of a Being. */
+type Vital struct {
+    Now int
+    Max int
+}
+
+/* Report a vital statistic as a Now/Max string */
+func (me * Vital) ToNowMax() string {
+    return fmt.Sprintf("%d/%d", me.Now , me.Max)
+}
+
+// alias of the above, since I'm lazy at times
+func (me * Vital) TNM() string {
+    return me.ToNowMax()
+}
+
+
+/* Report a vital statistic as a rounded percentage */
+func (me * Vital) ToPercentage() string {
+    percentage := (me.Now * 100) / me.Max 
+    return fmt.Sprintf("%d", percentage)
+}
+
+/* Report a vital statistic as a bar of characters */
+func (me * Vital) ToBar(full string, empty string, length int) string {
+    numfull := (me.Now * length) / me.Max
+    numempty := length - numfull
+    return strings.Repeat(empty, numempty) + strings.Repeat(full, numfull)  
+}
+
+
+
+type Being struct {
+    Entity
+    
+    // Essentials
+    Kind            BeingKind
+    Profession      BeingProfession
+    Level           int
+    
+    
+    // Talents
+    Strength        int
+    Toughness       int
+    Agility         int
+    Dexterity       int
+    Intelligence    int
+    Wisdom          int
+    Charisma        int
+    Essence         int
+        
+    // Vitals
+    HP              Vital
+    MP              Vital
+    JP              Vital
+    LP              Vital
+        
+    // Equipment values
+    Offense         int
+    Protection      int
+    Block           int
+    Rapidity        int
+    Yield           int
+
+    // Skills array
+    // Skills       []Skill
+       
+    // Arts array
+    // Arts         []Art
+    // Affects array
+    // Affects      []Affect
+       
+    // Equipment
+    // Equipment
+    
+    // Inventory
+    // Inventory 
+}
+
+// Derived stats 
+func (me *Being) Force() int {
+    return (me.Strength * 2 + me.Wisdom) / 3
+}
+    
+func (me *Being) Vitality() int {
+    return (me.Toughness * 2 + me.Charisma) / 3
+}
+
+func (me *Being) Quickness() int {
+    return (me.Agility * 2 + me.Intelligence) / 3
+}
+
+func (me * Being) Knack() int {
+    return (me.Dexterity * 2 + me.Essence) / 3
+}
+    
+    
+func (me * Being) Understanding() int {
+    return (me.Intelligence * 2 + me.Toughness) / 3
+}
+
+func (me * Being) Grace() int { 
+    return (me.Charisma * 2 + me.Agility) / 3
+}
+    
+func (me * Being) Zeal() int {
+    return (me.Wisdom * 2 + me.Strength) / 3
+}
+
+
+func (me * Being) Numen() int {
+      return (me.Essence * 2 + me.Dexterity) / 3
+}
+
+// Generates a prompt for use with the being/character
+func (me * Being) ToPrompt() string {
+    if me.Essence > 0 {
+        return fmt.Sprintf("HP:%s MP:%s JP:%s LP:%s", me.HP.TNM(), me.MP.TNM(), me.JP.TNM, me.LP.TNM())
+    } else {
+        return fmt.Sprintf("HP:%s MP:%s LP:%s", me.HP.TNM(), me.MP.TNM(), me.LP.TNM())
+    }
+}
+
+
+// Generates an overview of the essentials of the being as a string.
+func (me * Being) ToEssentials() string {
+    return fmt.Sprintf("%s %d %s %s", me.Name, me.Level, me.Kind, me.Profession)
+}
+
+// Generates an overview of the physical talents of the being as a string.
+func (me * Being) ToBodyTalents() string {
+    return fmt.Sprintf("STR: %3d    TOU: %3d    AGI: %3d    DEX: %3d", me.Strength, me.Toughness, me.Agility, me.Dexterity)
+}
+
+// Generates an overview of the mental talents of the being as a string.
+func (me * Being) ToMindTalents() string {
+    return fmt.Sprintf("INT: %3d    WIS: %3d    CHA: %3d    ESS: %3d", me.Intelligence, me.Wisdom, me.Charisma, me.Essence)
+}
+
+// Generates an overview of the equipment values of the being as a string.
+func (me * Being) ToEquipmentValues() string {
+    return fmt.Sprintf("OFF: %3d    PRO: %3d    BLO: %3d    RAP: %3d    YIE: %3d", me.Offense, me.Protection, me.Block, me.Rapidity, me.Yield)
+}
+
+// Generates an overview of the status of the being as a string.
+func (me * Being) ToStatus() string {
+    status := me.ToEssentials()
+    status += "\n" + me.ToBodyTalents();
+    status += "\n" + me.ToMindTalents();
+    status += "\n" + me.ToEquipmentValues();
+    status += "\n" + me.ToPrompt();
+    status += "\n"
+      return status
+}
+
+
+

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

@@ -0,0 +1,9 @@
+package world
+
+
+type Character struct {
+    Being       
+    AccountName string
+}
+
+

+ 12 - 0
src/woe/world/entity.go

@@ -0,0 +1,12 @@
+package world
+
+
+// An entity is anything that can exist in a World
+type Entity struct {
+    ID                  string
+    Name                string
+    ShortDescription    string
+    Description         string
+    Aliases           []string
+}
+

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

@@ -0,0 +1 @@
+package world

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

@@ -0,0 +1 @@
+package world

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

@@ -0,0 +1 @@
+package world

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

@@ -0,0 +1 @@
+package world

+ 8 - 0
src/woe/world/world.go

@@ -0,0 +1,8 @@
+package world
+
+
+type World struct {
+    
+}
+
+

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

@@ -0,0 +1 @@
+package world