WIP very sleepy

trunk
vilmibm 2022-12-20 00:38:47 -08:00
parent 76382a2b36
commit 7413b12b4a
5 changed files with 190 additions and 9 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/vilmibm/hermeticum/proto" "github.com/vilmibm/hermeticum/proto"
"github.com/vilmibm/hermeticum/server/db" "github.com/vilmibm/hermeticum/server/db"
"github.com/vilmibm/hermeticum/server/witch"
"google.golang.org/grpc" "google.golang.org/grpc"
) )
@ -73,6 +74,7 @@ type gameWorldServer struct {
db db.DB db db.DB
mu sync.Mutex // for msgRouter mu sync.Mutex // for msgRouter
msgRouter map[string]func(*proto.ClientMessage) error msgRouter map[string]func(*proto.ClientMessage) error
Gateway *witch.Gateway
} }
func newServer() (*gameWorldServer, error) { func newServer() (*gameWorldServer, error) {
@ -89,6 +91,7 @@ func newServer() (*gameWorldServer, error) {
s := &gameWorldServer{ s := &gameWorldServer{
msgRouter: make(map[string]func(*proto.ClientMessage) error), msgRouter: make(map[string]func(*proto.ClientMessage) error),
db: db, db: db,
Gateway: witch.NewGateway(),
} }
return s, nil return s, nil
@ -251,20 +254,20 @@ func (s *gameWorldServer) Login(ctx context.Context, auth *proto.AuthInfo) (si *
return return
} }
func (s *gameWorldServer) HandleSay(avatar *db.Object, msg string) error { func (s *gameWorldServer) HandleSay(sender *db.Object, msg string) error {
name := avatar.Data["name"] name := sender.Data["name"]
if name == "" { if name == "" {
// TODO determine this based on a hash or something // TODO determine this based on a hash or something
name = "a mysterious figure" name = "a mysterious figure"
} }
heard, err := s.db.Earshot(*avatar) heard, err := s.db.Earshot(*sender)
if err != nil { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
return err 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() as, err := s.db.ActiveSessions()
if err != nil { if err != nil {
@ -274,6 +277,7 @@ func (s *gameWorldServer) HandleSay(avatar *db.Object, msg string) error {
sendErrs := []error{} sendErrs := []error{}
for _, h := range heard { for _, h := range heard {
s.Gateway.VerbHandler(msg, *sender, h)
// TODO once we have a script engine, deliver the HEARS event // TODO once we have a script engine, deliver the HEARS event
for _, sess := range as { for _, sess := range as {
if sess.AccountID == h.OwnerID { if sess.AccountID == h.OwnerID {

View File

@ -51,6 +51,7 @@ type Object struct {
Bedroom bool Bedroom bool
Data map[string]string Data map[string]string
OwnerID int OwnerID int
Script string
} }
type pgDB struct { type pgDB struct {
@ -203,11 +204,11 @@ func (db *pgDB) AvatarBySessionID(sid string) (avatar *Object, err error) {
// TODO subquery // TODO subquery
stmt := ` stmt := `
SELECT id, avatar, data, owner SELECT id, avatar, data, owner, script
FROM objects WHERE avatar = true AND 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)` 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( 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 return
} }
@ -249,7 +250,7 @@ func (db *pgDB) MoveInto(toMove Object, container Object) error {
func (db *pgDB) Earshot(obj Object) ([]Object, error) { func (db *pgDB) Earshot(obj Object) ([]Object, error) {
stmt := ` stmt := `
SELECT id, avatar, bedroom, data, owner FROM objects SELECT id, avatar, bedroom, data, owner, script FROM objects
WHERE id IN ( WHERE id IN (
SELECT contained FROM contains SELECT contained FROM contains
WHERE container = ( WHERE container = (
@ -263,7 +264,10 @@ func (db *pgDB) Earshot(obj Object) ([]Object, error) {
for rows.Next() { for rows.Next() {
heard := Object{} 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 return nil, err
} }
out = append(out, heard) out = append(out, heard)

View File

@ -17,7 +17,7 @@ CREATE TABLE objects (
avatar boolean NOT NULL DEFAULT FALSE, avatar boolean NOT NULL DEFAULT FALSE,
bedroom boolean NOT NULL DEFAULT FALSE, bedroom boolean NOT NULL DEFAULT FALSE,
data jsonb NOT NULL, data jsonb NOT NULL,
script text, script text NOT NULL,
owner integer REFERENCES accounts ON DELETE RESTRICT owner integer REFERENCES accounts ON DELETE RESTRICT
); );

View File

@ -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
`

View File

@ -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
}