diff --git a/roadmap.md b/roadmap.md index 46a684f..e2bbce7 100644 --- a/roadmap.md +++ b/roadmap.md @@ -67,6 +67,37 @@ end) The movement is still generated. + +so the problem with all of this is that the beahvior of the exit is +supposedly going to change based on how it is contained. however, we only +re-read the code when the code changes. we're trying to affect game state +as part of code compilation and that's uncool. + +a user would create a new exit, put all the finishing touches on it, then move +it from their person to the room the exit originates from. WITCH would not +recompile and no containership would update. + +i think the dream of goes(north, "pub") is donezo. i think the next best +thing is goes(north, "foyer", "pub"). on execution, the sender is checked; +if their container is the first room then they are moved to the second +room. both cases suffer from the double containment problem. that second +containment *cannot* be updated as part of WITCH compilation. + +so we're back to two options: +- one way exits +- special creation semantics that handle something like double containership + +in the interest of moving on--and of putting off special top level commands +that exist outside of WITCH--i want to do one way exits for now. this sucks +because all the flavor written for an exit has to be duplicated for its pair. + +some other ideas because i can't let go. what of a variation on the exits map +where each exit stores a key in its has data about where it goes. this is no +better than a dynamic handler (it's worse) and does not help the double +containership problem. + +give up and do one way exits. + ## server beta - [x] grpc server diff --git a/server/db/db.go b/server/db/db.go index b5f9656..80f63f2 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "math/rand" + "strings" "github.com/google/uuid" "github.com/jackc/pgx/v4/pgxpool" @@ -30,6 +31,7 @@ type DB interface { // General GetObject(owner, name string) (*Object, error) GetObjectByID(ID int) (*Object, error) + SearchObjectsByName(term string) ([]Object, error) // Defaults Ensure() error @@ -41,7 +43,8 @@ type DB interface { AvatarBySessionID(string) (*Object, error) BedroomBySessionID(string) (*Object, error) MoveInto(toMove Object, container Object) error - Earshot(Object) ([]Object, error) + Earshot(vantage Object) ([]Object, error) + Resolve(vantage Object, term string) ([]Object, error) } type Account struct { @@ -488,6 +491,54 @@ func (db *pgDB) GetObject(owner, name string) (obj *Object, err error) { return } +func (db *pgDB) SearchObjectsByName(term string) ([]Object, error) { + ctx := context.Background() + + stmt := ` + SELECT id, avatar, data, owner, script + FROM objects + WHERE data['name']::varchar LIKE $1 + ` + + rows, err := db.pool.Query(ctx, stmt, "%"+term+"%") + if err != nil { + return nil, err + } + + out := []Object{} + for rows.Next() { + o := Object{} + if err = rows.Scan( + &o.ID, + &o.Avatar, + &o.Data, + &o.OwnerID, + &o.Script); err != nil { + return nil, err + } + out = append(out, o) + } + + return out, nil +} + +func (db *pgDB) Resolve(vantage Object, term string) ([]Object, error) { + stuff, err := db.Earshot(vantage) + if err != nil { + return nil, err + } + + out := []Object{} + + for _, o := range stuff { + if strings.Contains(o.Data["name"], term) { + out = append(out, o) + } + } + + return out, nil +} + func (db *pgDB) GetAccountAvatar(account Account) (*Object, error) { ctx := context.Background() obj := &Object{ diff --git a/server/db/schema.sql b/server/db/schema.sql index ae72a3d..3c2c45d 100644 --- a/server/db/schema.sql +++ b/server/db/schema.sql @@ -39,9 +39,3 @@ CREATE TABLE contains ( ); CREATE TYPE heading AS ENUM ('north', 'south', 'east', 'west', 'above', 'below'); - -CREATE TABLE exits ( - startroom integer REFERENCES objects ON DELETE CASCADE, - endroom integer REFERENCES objects ON DELETE CASCADE, - direction heading NOT NULL -); diff --git a/server/witch/direction.go b/server/witch/direction.go index d65281e..0609b55 100644 --- a/server/witch/direction.go +++ b/server/witch/direction.go @@ -1,5 +1,7 @@ package witch +import "fmt" + const ( dirEast = "_DIR_EAST" dirWest = "_DIR_WEST" @@ -13,6 +15,10 @@ type Direction struct { raw string } +func NewDirection(raw string) Direction { + return Direction{raw: raw} +} + func (d Direction) Reverse() Direction { raw := "" switch d.raw { @@ -29,11 +35,11 @@ func (d Direction) Reverse() Direction { case dirSouth: raw = dirNorth } - return Direction{raw: raw} + return NewDirection(raw) } -// NormalizeHuman takes a direction someone might type like "up" or "north" and returns the correct Direction struct -func NormalizeHuman(humanDir string) Direction { +// NormalizeDirection takes a direction someone might type like "up" or "north" and returns the correct Direction struct +func NormalizeDirection(humanDir string) (Direction, error) { raw := "" switch humanDir { case "up": @@ -50,9 +56,11 @@ func NormalizeHuman(humanDir string) Direction { raw = dirNorth case "south": raw = dirSouth - + default: + return Direction{}, fmt.Errorf("did not understand direction '%s'", humanDir) } - return Direction{raw: raw} + + return NewDirection(raw), nil } // Human returns a string form of this direction like "above" or "north" @@ -74,3 +82,7 @@ func (d Direction) Human() (humanDir string) { return humanDir } + +func (d Direction) Equals(o Direction) bool { + return d.raw == o.raw +} diff --git a/server/witch/header.go b/server/witch/header.go deleted file mode 100644 index 80b9ffa..0000000 --- a/server/witch/header.go +++ /dev/null @@ -1,116 +0,0 @@ -package witch - -/* - -This file contains the definitions of functions that are injected into scope for WITCH scripts. See witch.go's ScriptContext to see how they are actually added to a LuaState. - -TODO: consider making this (or witch.go) a different package entirely. the `witch` prefix for the function names in this file is a little annoying. - -*/ - -import ( - "log" - "strings" - - lua "github.com/yuin/gopher-lua" -) - -func witchHas(l *lua.LState) int { - l.SetGlobal("_has", l.ToTable(1)) - return 0 -} - -func witchProvides(l *lua.LState) int { - // TODO test this manually - - verbAndPattern := l.ToString(1) - cb := l.ToFunction(2) - - split := strings.SplitN(verbAndPattern, " ", 2) - verb := split[0] - pattern := split[1] - - return addPatternHandler(l, verb, pattern, cb) -} - -func witchHears(l *lua.LState) int { - pattern := l.ToString(1) - cb := l.ToFunction(2) - return addPatternHandler(l, "say", pattern, cb) -} - -func witchSees(l *lua.LState) int { - pattern := l.ToString(1) - cb := l.ToFunction(2) - - return addPatternHandler(l, "emote", pattern, cb) -} - -func witchGoes(l *lua.LState) int { - // TODO validate direction - // TODO convert direction constant to english - - direction := l.ToString(1) - targetRoom := l.ToString(2) - - cb := func(l *lua.LState) int { - log.Printf("please move sender to target room '%s'", targetRoom) - return 0 - } - - // TODO call addPatternHandler again for the reverse direction (make a reverse helper) - return addPatternHandler(l, "go", direction, l.NewFunction(cb)) -} - -func witchSeen(l *lua.LState) int { - cb := l.ToFunction(1) - return addHandler(l, "look", cb) -} - -func witchMy(l *lua.LState) int { - hasT := l.GetGlobal("_has").(*lua.LTable) - val := hasT.RawGetString(l.ToString(1)) - l.Push(val) - return 1 -} - -func witchDoes(ls *lua.LState) int { - // TODO how to feed events back into the server? - // it needs to behave like an event showing up in Commands stream - // this handler needs a reference to the gateway which has a channel for sending events that the server will see? - return 0 -} - -func addHandler(l *lua.LState, verb string, cb *lua.LFunction) int { - pattern := ".*" - - log.Printf("adding handler: %s %s %#v", verb, pattern, cb) - - handlers := l.GetGlobal("_handlers").(*lua.LTable) - - verbHandlers, ok := handlers.RawGetString(verb).(*lua.LTable) - if !ok { - verbHandlers = l.NewTable() - handlers.RawSetString(verb, verbHandlers) - } - - verbHandlers.RawSetString(pattern, cb) - - return 0 -} - -func addPatternHandler(l *lua.LState, verb, pattern string, cb *lua.LFunction) int { - log.Printf("adding handler: %s %s %#v", verb, string(pattern), cb) - - handlers := l.GetGlobal("_handlers").(*lua.LTable) - - verbHandlers, ok := handlers.RawGetString(verb).(*lua.LTable) - if !ok { - verbHandlers = l.NewTable() - handlers.RawSetString(verb, verbHandlers) - } - - verbHandlers.RawSetString(pattern, cb) - - return 0 -} diff --git a/server/witch/witch.go b/server/witch/witch.go index a49a69d..460d283 100644 --- a/server/witch/witch.go +++ b/server/witch/witch.go @@ -10,6 +10,7 @@ import ( "fmt" "log" "regexp" + "strings" "github.com/vilmibm/hermeticum/proto" "github.com/vilmibm/hermeticum/server/db" @@ -120,6 +121,7 @@ type ScriptContext struct { func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) error) (*ScriptContext, error) { sc := &ScriptContext{ serverAPI: serverAPI{db: db, getSend: getSend}, + db: db, } sc.incoming = make(chan VerbContext) @@ -130,7 +132,6 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) for { vc = <-sc.incoming if vc.Target.Script != sc.script { - // TODO clear this object out of the exits table sc.script = vc.Target.Script l = lua.NewState() @@ -145,16 +146,17 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) l.SetGlobal("down", lua.LString(dirBelow)) // witch object behavior functions - l.SetGlobal("has", l.NewFunction(witchHas)) - l.SetGlobal("hears", l.NewFunction(witchHears)) - l.SetGlobal("sees", l.NewFunction(witchSees)) - l.SetGlobal("goes", l.NewFunction(witchGoes)) - l.SetGlobal("seen", l.NewFunction(witchSeen)) - l.SetGlobal("my", l.NewFunction(witchMy)) - l.SetGlobal("provides", l.NewFunction(witchProvides)) + l.SetGlobal("has", l.NewFunction(sc.wHas)) + l.SetGlobal("hears", l.NewFunction(sc.wHears)) + l.SetGlobal("sees", l.NewFunction(sc.wSees)) + l.SetGlobal("goes", l.NewFunction(sc.wGoes)) + l.SetGlobal("seen", l.NewFunction(sc.wSeen)) + l.SetGlobal("my", l.NewFunction(sc.wMy)) + l.SetGlobal("provides", l.NewFunction(sc.wProvides)) // witch helpers l.SetGlobal("_handlers", l.NewTable()) + l.SetGlobal("_ID", lua.LNumber(vc.Target.ID)) if err := l.DoString(vc.Target.Script); err != nil { log.Printf("error parsing script %s: %s", vc.Target.Script, err.Error()) @@ -187,7 +189,7 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) senderID := int(lua.LVAsNumber(sender.RawGetString("ID"))) owner := l.ToString(1) name := l.ToString(2) - db := sc.serverAPI.DB() + db := sc.db senderObj, err := db.GetObjectByID(senderID) if err != nil { log.Println(err.Error()) @@ -224,6 +226,7 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) senderT.RawSetString("ID", lua.LNumber(vc.Sender.ID)) l.SetGlobal("sender", senderT) l.SetGlobal("msg", lua.LString(vc.Rest)) + l.SetGlobal("_SENDERID", lua.LNumber(vc.Sender.ID)) handlers := l.GetGlobal("_handlers").(*lua.LTable) handlers.ForEach(func(k, v lua.LValue) { @@ -254,3 +257,126 @@ func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) func (sc *ScriptContext) Handle(vc VerbContext) { sc.incoming <- vc } + +func (sc *ScriptContext) addHandler(l *lua.LState, verb, pattern string, cb *lua.LFunction) { + log.Printf("adding handler: %s %s %#v", verb, string(pattern), cb) + + handlers := l.GetGlobal("_handlers").(*lua.LTable) + + verbHandlers, ok := handlers.RawGetString(verb).(*lua.LTable) + if !ok { + verbHandlers = l.NewTable() + handlers.RawSetString(verb, verbHandlers) + } + + verbHandlers.RawSetString(pattern, cb) +} + +func (sc *ScriptContext) wMy(l *lua.LState) int { + hasT := l.GetGlobal("_has").(*lua.LTable) + val := hasT.RawGetString(l.ToString(1)) + l.Push(val) + return 1 +} + +func (sc *ScriptContext) wHas(l *lua.LState) int { + l.SetGlobal("_has", l.ToTable(1)) + return 0 +} + +func (sc *ScriptContext) wHears(l *lua.LState) int { + pattern := l.ToString(1) + cb := l.ToFunction(2) + sc.addHandler(l, "say", pattern, cb) + return 0 +} + +func (sc *ScriptContext) wSees(l *lua.LState) int { + pattern := l.ToString(1) + cb := l.ToFunction(2) + + sc.addHandler(l, "emote", pattern, cb) + return 0 +} + +func (sc *ScriptContext) wSeen(l *lua.LState) int { + cb := l.ToFunction(1) + sc.addHandler(l, "look", ".*", cb) + return 0 +} + +func (sc *ScriptContext) wDoes(ls *lua.LState) int { + // TODO how to feed events back into the server? + // it needs to behave like an event showing up in Commands stream + // this handler needs a reference to the gateway which has a channel for sending events that the server will see? + return 0 +} + +func (sc *ScriptContext) wProvides(l *lua.LState) int { + // TODO test this manually + + verbAndPattern := l.ToString(1) + cb := l.ToFunction(2) + + split := strings.SplitN(verbAndPattern, " ", 2) + verb := split[0] + pattern := split[1] + + sc.addHandler(l, verb, pattern, cb) + return 0 +} + +func (sc *ScriptContext) wGoes(l *lua.LState) int { + direction := NewDirection(l.ToString(1)) + targetRoomTerm := l.ToString(2) + + log.Printf("GOT DIRECTION %v", direction) + + cb := func(l *lua.LState) (ret int) { + targetRoomList, err := sc.db.SearchObjectsByName(targetRoomTerm) + if err != nil { + log.Printf("failed to search for target room: %s", err.Error()) + return + } + switch len(targetRoomList) { + case 0: + log.Printf("failed to find any matching target room. tell player somehow") + return + case 1: + log.Printf("found the target room") + default: + log.Printf("found too many matching target rooms. tell player somehow") + return + } + + targetRoom := targetRoomList[0] + msg := l.GetGlobal("msg").String() + normalized, err := NormalizeDirection(msg) + if err != nil { + return + } + + sender, err := sc.getSenderFromState(l) + if err != nil { + log.Printf("failed to find sender %s", err.Error()) + return + } + + if normalized.Equals(direction) { + log.Printf("MOVING SENDER TO '%s'", targetRoom.Data["name"]) + // TODO error checking + sc.db.MoveInto(*sender, targetRoom) + sc.serverAPI.Tell(targetRoom.ID, sender.ID, fmt.Sprintf("you are now in %s", targetRoom.Data["name"])) + } + return + } + + sc.addHandler(l, "go", ".*", l.NewFunction(cb)) + return 0 +} + +func (sc *ScriptContext) getSenderFromState(l *lua.LState) (*db.Object, error) { + lsID := lua.LVAsNumber(l.GetGlobal("_SENDERID")) + + return sc.db.GetObjectByID(int(lsID)) +} diff --git a/witch_spike.lua b/witch_spike.lua index 203a92d..e0edf6e 100644 --- a/witch_spike.lua +++ b/witch_spike.lua @@ -173,21 +173,4 @@ allows({ execute = "world", }) --- option 1: fully manual - -provides("go east", function(args) - if sender.where = "gallery" then - move_sender("ossuary") - end -end) - -provides("go west", function(args) - if sender.where = "ossuary" then - move_sender("gallery") - end -end) - --- option 2: magical helper - --- automatically creates the two `go` handlers above -goes("east", "gallery", "ossuary") +goes("east", "gallery")