diff --git a/server/cmd/main.go b/server/cmd/main.go index fecc888..b5bf986 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -12,6 +12,7 @@ import ( "github.com/vilmibm/hermeticum/proto" "github.com/vilmibm/hermeticum/server/db" + "github.com/vilmibm/hermeticum/server/witch" "google.golang.org/grpc" ) @@ -73,6 +74,7 @@ type gameWorldServer struct { db db.DB mu sync.Mutex // for msgRouter msgRouter map[string]func(*proto.ClientMessage) error + Gateway *witch.Gateway } func newServer() (*gameWorldServer, error) { @@ -89,6 +91,7 @@ func newServer() (*gameWorldServer, error) { s := &gameWorldServer{ msgRouter: make(map[string]func(*proto.ClientMessage) error), db: db, + Gateway: witch.NewGateway(), } return s, nil @@ -251,20 +254,20 @@ func (s *gameWorldServer) Login(ctx context.Context, auth *proto.AuthInfo) (si * return } -func (s *gameWorldServer) HandleSay(avatar *db.Object, msg string) error { - name := avatar.Data["name"] +func (s *gameWorldServer) HandleSay(sender *db.Object, msg string) error { + name := sender.Data["name"] if name == "" { // TODO determine this based on a hash or something name = "a mysterious figure" } - heard, err := s.db.Earshot(*avatar) + heard, err := s.db.Earshot(*sender) if err != nil { log.Println(err.Error()) return err } - log.Printf("found %#v in earshot of %#v\n", heard, avatar) + log.Printf("found %#v in earshot of %#v\n", heard, sender) as, err := s.db.ActiveSessions() if err != nil { @@ -274,6 +277,7 @@ func (s *gameWorldServer) HandleSay(avatar *db.Object, msg string) error { sendErrs := []error{} for _, h := range heard { + s.Gateway.VerbHandler(msg, *sender, h) // TODO once we have a script engine, deliver the HEARS event for _, sess := range as { if sess.AccountID == h.OwnerID { diff --git a/server/db/db.go b/server/db/db.go index f7f1bef..eafe2ff 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -51,6 +51,7 @@ type Object struct { Bedroom bool Data map[string]string OwnerID int + Script string } type pgDB struct { @@ -203,11 +204,11 @@ func (db *pgDB) AvatarBySessionID(sid string) (avatar *Object, err error) { // TODO subquery stmt := ` - SELECT id, avatar, data, owner + SELECT id, avatar, data, owner, script 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.OwnerID) + &avatar.ID, &avatar.Avatar, &avatar.Data, &avatar.OwnerID, &avatar.Script) return } @@ -249,7 +250,7 @@ func (db *pgDB) MoveInto(toMove Object, container Object) error { func (db *pgDB) Earshot(obj Object) ([]Object, error) { stmt := ` - SELECT id, avatar, bedroom, data, owner FROM objects + SELECT id, avatar, bedroom, data, owner, script FROM objects WHERE id IN ( SELECT contained FROM contains WHERE container = ( @@ -263,7 +264,10 @@ func (db *pgDB) Earshot(obj Object) ([]Object, error) { for rows.Next() { heard := Object{} - if err = rows.Scan(&heard.ID, &heard.Avatar, &heard.Bedroom, &heard.Data, &heard.OwnerID); err != nil { + if err = rows.Scan( + &heard.ID, &heard.Avatar, + &heard.Bedroom, &heard.Data, + &heard.OwnerID, &heard.Script); err != nil { return nil, err } out = append(out, heard) diff --git a/server/db/schema.sql b/server/db/schema.sql index 1bb6f37..2690c05 100644 --- a/server/db/schema.sql +++ b/server/db/schema.sql @@ -17,7 +17,7 @@ CREATE TABLE objects ( avatar boolean NOT NULL DEFAULT FALSE, bedroom boolean NOT NULL DEFAULT FALSE, data jsonb NOT NULL, - script text, + script text NOT NULL, owner integer REFERENCES accounts ON DELETE RESTRICT ); diff --git a/server/witch/header.go b/server/witch/header.go new file mode 100644 index 0000000..54818ca --- /dev/null +++ b/server/witch/header.go @@ -0,0 +1,42 @@ +package witch + +import ( + "log" + + "github.com/vilmibm/hermeticum/server/db" + lua "github.com/yuin/gopher-lua" +) + +func hasWrapper(obj db.Object) func(*lua.LState) int { + return func(ls *lua.LState) int { + lv := ls.ToTable(1) + log.Printf("%#v", lv) + return 0 + } +} + +func hearsWrapper(obj db.Object) func(*lua.LState) int { + return func(ls *lua.LState) int { + // TODO get handler from _handlers + // TODO call it + // TODO how to get message in here? + + return 0 + } +} + +func does(ls *lua.LState) int { + // TODO + return 0 +} + +const addHandler = ` +_addHandler = function(verb, pattern, cb) + _handlers[verb] = function(message) + f, l = string.find(message, pattern) + if f != nil + cb(message) + end + end +end +` diff --git a/server/witch/witch.go b/server/witch/witch.go new file mode 100644 index 0000000..8e91b83 --- /dev/null +++ b/server/witch/witch.go @@ -0,0 +1,131 @@ +package witch + +import ( + "sync" + + "github.com/vilmibm/hermeticum/server/db" + lua "github.com/yuin/gopher-lua" +) + +/* + + the purpose of this package is to provide abstractions for sending verbs to game objects. + + Game objects get pulled from the DB into memory and their scripts become Lua States. + +*/ + +type scriptContext struct { + script string + l *lua.LState + incoming chan string + // TODO whatever is needed to support calling a Go API +} + +func (sc *scriptContext) NeedsRefresh(obj db.Object) bool { + return sc.script != obj.Script +} + +// TODO using a dummy script for now + +const dummyScript = ` +has({ + name = "spaghetti", + description = "a plate of pasta covered in pomodoro sauce" +}) + +hears(".*eat.*", function(msg) + does("quivers nervously") +end) +` + +/* +allows({ + read = "world", + write = "owner" + carry = "owner", + execute = "world", +}) + +hears(".*eat.*", function(msg) + does("quivers nervously") +end) +` +*/ + +// TODO figure out channel stuff +// TODO figure out how to inject WITCH header +// - do i inject from Go or prepend some Lua code? +// TODO figure out how the Lua code can affect Go and thus the database + +func newScriptContext(obj db.Object) (*scriptContext, error) { + l := lua.NewState() + + l.SetGlobal("has", l.NewFunction(hasWrapper(obj))) + l.SetGlobal("_handlers", l.NewTable()) + + //if err := l.DoString(obj.Script); err != nil { + if err := l.DoString(dummyScript); err != nil { + return nil, err + } + + return &scriptContext{}, nil +} + +type Gateway struct { + // maps game object IDs to script contexts + m map[int]*scriptContext + mu sync.RWMutex +} + +func NewGateway() *Gateway { + return &Gateway{ + m: map[int]*scriptContext{}, + mu: sync.RWMutex{}, + } +} + +// RefreshObj ensures that the script context for the given game object is running the latest code for the object's script +func (g *Gateway) RefreshObject(obj db.Object) error { + g.mu.RLock() + var sc *scriptContext + + if sc, ok := g.m[obj.ID]; ok { + if !sc.NeedsRefresh(obj) { + g.mu.RUnlock() + return nil + } + } + g.mu.RUnlock() + + sc, err := newScriptContext(obj) + if err != nil { + return err + } + + g.mu.Lock() + g.m[obj.ID] = sc + g.mu.Unlock() + + return nil +} + +func (g *Gateway) VerbHandler(msg string, sender, target db.Object) error { + var sc *scriptContext + g.mu.RLock() + sc, ok := g.m[target.ID] + g.mu.RUnlock() + + if !ok || sc.NeedsRefresh(target) { + sc, err := newScriptContext(target) + if err != nil { + return err + } + + g.mu.Lock() + g.m[target.ID] = sc + g.mu.Unlock() + } + + return nil +}