diff --git a/client/cmd/main.go b/client/cmd/main.go index c7860d1..e333d2e 100644 --- a/client/cmd/main.go +++ b/client/cmd/main.go @@ -178,6 +178,8 @@ func _main() error { commandInput := tview.NewInputField().SetLabel("> ") handleInput := func(_ tcell.Key) { input := commandInput.GetText() + // TODO command history + commandInput.SetText("") // TODO do i need to clear the input's text? go cs.HandleInput(input) } diff --git a/server/cmd/main.go b/server/cmd/main.go index 1ecbbdc..75fb0cd 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "io" @@ -73,6 +74,10 @@ func newServer() (*gameWorldServer, error) { return nil, err } + if err = db.ClearSessions(); err != nil { + return nil, fmt.Errorf("could not clear sessions: %w", err) + } + s := &gameWorldServer{ msgRouter: make(map[string]func(*proto.ClientMessage) error), db: db, @@ -105,23 +110,45 @@ func (s *gameWorldServer) Commands(stream proto.GameWorld_CommandsServer) error } send := s.msgRouter[sid] - msg := &proto.ClientMessage{ - Type: proto.ClientMessage_OVERHEARD, - Text: fmt.Sprintf("%s sent command %s with args %s", - sid, cmd.Verb, cmd.Rest), - } + // TODO what is the implication of returning an error from this function? - speaker := "ECHO" - msg.Speaker = &speaker - - err = send(msg) + avatar, err := s.db.AvatarBySessionID(sid) if err != nil { - log.Printf("failed to send %v to %s: %s", msg, sid, err) + return s.HandleError(send, err) + } + log.Printf("found avatar %#v", avatar) + + switch cmd.Verb { + case "say": + if err = s.HandleSay(avatar, cmd.Rest); err != nil { + s.HandleError(func(_ *proto.ClientMessage) error { return nil }, err) + } + default: + msg := &proto.ClientMessage{ + Type: proto.ClientMessage_WHISPER, + Text: fmt.Sprintf("unknown verb: %s", cmd.Verb), + } + if err = send(msg); err != nil { + s.HandleError(send, err) + } } - // TODO find the user who ran action via SessionInfo - // TODO get area of effect, which should include the sender - // TODO dispatch the command to each affected object + /* + + msg := &proto.ClientMessage{ + Type: proto.ClientMessage_OVERHEARD, + Text: fmt.Sprintf("%s sent command %s with args %s", + sid, cmd.Verb, cmd.Rest), + } + + speaker := "ECHO" + msg.Speaker = &speaker + + err = send(msg) + if err != nil { + log.Printf("failed to send %v to %s: %s", msg, sid, err) + } + */ } } @@ -194,11 +221,94 @@ func (s *gameWorldServer) Login(ctx context.Context, auth *proto.AuthInfo) (si * return } + av, err := s.db.AvatarBySessionID(sessionID) + if err != nil { + return nil, fmt.Errorf("failed to find avatar for %s: %w", sessionID, err) + } + + bedroom, err := s.db.BedroomBySessionID(sessionID) + if err != nil { + return nil, fmt.Errorf("failed to find bedroom for %s: %w", sessionID, err) + } + + err = s.db.MoveInto(*av, *bedroom) + if err != nil { + return nil, fmt.Errorf("failed to move %d into %d: %w", av.ID, bedroom.ID, err) + } + si = &proto.SessionInfo{SessionID: sessionID} + // TODO actually put them in world + return } +func (s *gameWorldServer) HandleSay(avatar *db.Object, msg string) error { + name := avatar.Data["name"] + if name == "" { + // TODO determine this based on a hash or something + name = "a mysterious figure" + } + + heard, err := s.db.Earshot(*avatar) + if err != nil { + log.Println(err.Error()) + return err + } + + log.Printf("found %#v in earshot of %#v\n", heard, avatar) + + as, err := s.db.ActiveSessions() + if err != nil { + return err + } + + sendErrs := []error{} + + for _, h := range heard { + // TODO once we have a script engine, deliver the HEARS event + for _, sess := range as { + if sess.AccountID == h.OwnerID { + cm := proto.ClientMessage{ + Type: proto.ClientMessage_OVERHEARD, + Text: msg, + Speaker: &name, + } + err = s.msgRouter[sess.ID](&cm) + if err != nil { + sendErrs = append(sendErrs, err) + } + } + } + } + + if len(sendErrs) > 0 { + errMsg := "send errors: " + for i, err := range sendErrs { + errMsg += err.Error() + if i < len(sendErrs)-1 { + errMsg += ", " + } + } + return errors.New(errMsg) + } + + return nil +} + +func (s *gameWorldServer) HandleError(send func(*proto.ClientMessage) error, err error) error { + log.Printf("error: %s", err.Error()) + msg := &proto.ClientMessage{ + Type: proto.ClientMessage_WHISPER, + Text: "server error :(", + } + err = send(msg) + if err != nil { + log.Printf("error sending to client: %s", err.Error()) + } + return err +} + // TODO other server functions func main() { diff --git a/server/db/db.go b/server/db/db.go index e4c8bda..f7f1bef 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -24,11 +24,14 @@ type DB interface { GetAccount(string) (*Account, error) StartSession(Account) (string, error) EndSession(string) error + ClearSessions() error // Presence + ActiveSessions() ([]Session, error) AvatarBySessionID(string) (*Object, error) BedroomBySessionID(string) (*Object, error) MoveInto(toMove Object, container Object) error + Earshot(Object) ([]Object, error) } type Account struct { @@ -37,11 +40,17 @@ type Account struct { Pwhash string } +type Session struct { + ID string + AccountID int +} + type Object struct { ID int Avatar bool Bedroom bool Data map[string]string + OwnerID int } type pgDB struct { @@ -194,11 +203,11 @@ func (db *pgDB) AvatarBySessionID(sid string) (avatar *Object, err error) { // TODO subquery stmt := ` - SELECT id, avatar, data + SELECT id, avatar, data, owner FROM objects WHERE avatar = true AND owner = ( SELECT a.id FROM sessions s JOIN accounts a ON s.account = a.id WHERE s.id = $1)` err = db.pool.QueryRow(context.Background(), stmt, sid).Scan( - &avatar.ID, &avatar.Avatar, &avatar.Data) + &avatar.ID, &avatar.Avatar, &avatar.Data, &avatar.OwnerID) return } @@ -238,6 +247,54 @@ func (db *pgDB) MoveInto(toMove Object, container Object) error { return tx.Commit(ctx) } +func (db *pgDB) Earshot(obj Object) ([]Object, error) { + stmt := ` + SELECT id, avatar, bedroom, data, owner FROM objects + WHERE id IN ( + SELECT contained FROM contains + WHERE container = ( + SELECT container FROM contains WHERE contained = $1 LIMIT 1))` + rows, err := db.pool.Query(context.Background(), stmt, obj.ID) + if err != nil { + return nil, err + } + + out := []Object{} + + for rows.Next() { + heard := Object{} + if err = rows.Scan(&heard.ID, &heard.Avatar, &heard.Bedroom, &heard.Data, &heard.OwnerID); err != nil { + return nil, err + } + out = append(out, heard) + } + + return out, nil +} + +func (db *pgDB) ActiveSessions() (out []Session, err error) { + stmt := `SELECT id, account FROM sessions` + rows, err := db.pool.Query(context.Background(), stmt) + if err != nil { + return + } + + for rows.Next() { + s := Session{} + if err = rows.Scan(&s.ID, &s.AccountID); err != nil { + return + } + out = append(out, s) + } + + return +} + +func (db *pgDB) ClearSessions() (err error) { + _, err = db.pool.Exec(context.Background(), "DELETE FROM sessions") + return +} + func randSmell() string { // TODO seeding smells := []string{