Compare commits

...

10 Commits

Author SHA1 Message Date
nate smith 84cfc0f12d one way exits 2023-05-25 23:03:58 -07:00
nate smith 3bb94c490e implement Direction 2023-05-09 19:31:02 -07:00
nate smith 8fd8331a96 WIP Direction struct 2023-05-02 20:35:15 -07:00
nate smith 209833798b WIP: a notable unfucking 2023-05-02 20:23:12 -07:00
nate smith e86ac5875a towards exits, provides 2023-04-30 23:19:20 -07:00
nate smith dd5c377144 revisit exit thoughts after break 2023-04-28 23:52:58 -07:00
nate smith 31af07f7e7 hide serverAPI in witch 2023-04-03 23:41:05 -07:00
nate smith df68631aa3 go.mod 2023-04-03 23:17:27 -07:00
vilmibm 7289d38459 fix some stuff. go works now but only in one direction 2023-01-11 20:41:39 -08:00
vilmibm f4934e28f3 WIP on look handler 2023-01-10 19:05:43 -08:00
9 changed files with 500 additions and 134 deletions

11
go.mod
View File

@ -3,6 +3,7 @@ module github.com/vilmibm/hermeticum
go 1.18 go 1.18
require ( require (
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1
github.com/jackc/pgx/v4 v4.16.1 github.com/jackc/pgx/v4 v4.16.1
github.com/rivo/tview v0.0.0-20220703182358-a13d901d3386 github.com/rivo/tview v0.0.0-20220703182358-a13d901d3386
google.golang.org/grpc v1.47.0 google.golang.org/grpc v1.47.0
@ -11,9 +12,7 @@ require (
require ( require (
github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.12.1 // indirect github.com/jackc/pgconn v1.12.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
@ -24,9 +23,7 @@ require (
github.com/jackc/puddle v1.2.1 // indirect github.com/jackc/puddle v1.2.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/yuin/gopher-lua v0.0.0-20221210110428-332342483e3f // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
@ -34,3 +31,9 @@ require (
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
) )
require (
github.com/google/uuid v1.3.0
github.com/pkg/errors v0.9.1 // indirect
github.com/yuin/gopher-lua v0.0.0-20221210110428-332342483e3f
)

View File

@ -41,6 +41,63 @@ It's tempting to have the `exits` map because of its simplicity, but it actually
TODO draft something new TODO draft something new
Coming back to this after a long break, this new scheme with the exits table seems strictly worse than the tildemush approach. i don't like the amount of book keeping in the new approach--that's complexity that can lead to bugs. i think it's ultimately most elegant to just...let the exit exist in two rooms.
aside: i want to think through why exits shouldn't be on a room but it's a pretty quick answer. i don't want rooms that aren't world editable to be un-connectable to other things. if someone comes along and makes room A and then never comes back, it should be tunnel-able to from other rooms. i like the idea of people finding some cobwebbed room and then building a ladder up to it from somewhere.
so i'm going back to the tildemush approach. the next question is; is the exit maps a useful thing? couldn't the go handler just add a second, mirrored go handler? a handler that checks room directionality?
i've added a WITCH function, goes, which takes a direction and two rooms. this WITCH function adds two `go` verb handlers--one for the direction in the `goes` invocation and then one for the reverse. i like this more than the exit map, but it does mean that exits could compete each other. can't remember if that could happen in tildemush (was the exits map stored per exit? was that guarded?).
i think the competing is fine. i'm actually fine with it. i think that goes() could also add an invocation of `provides()` like this:
```lua
provides("use $this", function(args)
move_sender("target_room")
end)
```
A nice idea is the ability for an exit to add flavor to an entity transitioning through it; for this I can maybe add:
```lua
goesAnd(east, "ossuary", "gallery", function(args)
tellSender("the door squeals harshly as you move it but allows you passage")
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 ## server beta
- [x] grpc server - [x] grpc server

View File

@ -101,53 +101,19 @@ func newServer() (*gameWorldServer, error) {
} }
func (s *gameWorldServer) verbHandler(verb, rest string, sender, target db.Object) error { func (s *gameWorldServer) verbHandler(verb, rest string, sender, target db.Object) error {
log.Printf("VH %s %s %d %d", verb, rest, sender.ID, target.ID)
s.scriptsMutex.RLock() s.scriptsMutex.RLock()
sc, ok := s.scripts[target.ID] sc, ok := s.scripts[target.ID]
s.scriptsMutex.RUnlock() s.scriptsMutex.RUnlock()
var err error var err error
sid, _ := s.db.SessionIDForAvatar(target) getSend := func(sid string) func(*proto.ClientMessage) error {
serverAPI := witch.ServerAPI{ return s.msgRouter[sid]
Show: func(_ int, _ string) {},
Tell: func(_ int, _ string) {},
DB: func() db.DB {
return s.db
},
}
if sid != "" {
send := s.msgRouter[sid]
getSenderName := func(senderID int) *string {
senderName := "a mysterious stranger"
sender, err := s.db.GetObjectByID(senderID)
if err == nil {
senderName = sender.Data["name"]
} else {
log.Println(err.Error())
}
return &senderName
}
serverAPI.Show = func(senderID int, msg string) {
cm := proto.ClientMessage{
Type: proto.ClientMessage_EMOTE,
Text: msg,
Speaker: getSenderName(senderID),
}
send(&cm)
}
serverAPI.Tell = func(senderID int, msg string) {
cm := proto.ClientMessage{
Type: proto.ClientMessage_OVERHEARD,
Text: msg,
Speaker: getSenderName(senderID),
}
send(&cm)
}
} }
if !ok || sc == nil { if !ok || sc == nil {
if sc, err = witch.NewScriptContext(serverAPI); err != nil { if sc, err = witch.NewScriptContext(s.db, getSend); err != nil {
return err return err
} }
@ -239,6 +205,10 @@ func (s *gameWorldServer) Commands(stream proto.GameWorld_CommandsServer) error
return s.HandleError(send, err) return s.HandleError(send, err)
} }
for _, obj := range affected {
log.Printf("%s heard %s from %d", obj.Data["name"], cmd.Verb, avatar.ID)
}
for _, o = range affected { for _, o = range affected {
if err = s.verbHandler(cmd.Verb, cmd.Rest, *avatar, o); err != nil { if err = s.verbHandler(cmd.Verb, cmd.Rest, *avatar, o); err != nil {
log.Printf("error handling verb %s for object %d: %s", cmd.Verb, o.ID, err) log.Printf("error handling verb %s for object %d: %s", cmd.Verb, o.ID, err)

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v4/pgxpool" "github.com/jackc/pgx/v4/pgxpool"
@ -30,6 +31,7 @@ type DB interface {
// General // General
GetObject(owner, name string) (*Object, error) GetObject(owner, name string) (*Object, error)
GetObjectByID(ID int) (*Object, error) GetObjectByID(ID int) (*Object, error)
SearchObjectsByName(term string) ([]Object, error)
// Defaults // Defaults
Ensure() error Ensure() error
@ -37,10 +39,12 @@ type DB interface {
// Presence // Presence
SessionIDForAvatar(Object) (string, error) SessionIDForAvatar(Object) (string, error)
SessionIDForObjID(int) (string, error)
AvatarBySessionID(string) (*Object, error) AvatarBySessionID(string) (*Object, error)
BedroomBySessionID(string) (*Object, error) BedroomBySessionID(string) (*Object, error)
MoveInto(toMove Object, container 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 { type Account struct {
@ -113,6 +117,8 @@ func (db *pgDB) Ensure() error {
} }
} }
// TODO for some reason, when the seen() callback runs for foyer we're calling the stub Tell instead of the sid-closured Tell. figure out why.
roomScript := ` roomScript := `
seen(function() seen(function()
tellSender(my("description")) tellSender(my("description"))
@ -173,10 +179,10 @@ func (db *pgDB) Ensure() error {
oakDoor = &Object{ oakDoor = &Object{
Data: data, Data: data,
Script: ` Script: `
go("north", function() provides("get tetanus", function(args)
tellMe("the heavy door swings forward with ease. It creaks gently") tellSender("you now have tetanus")
moveSender("system", "pub")
end) end)
goes(north, "pub")
`, `,
} }
if err = db.CreateObject(sysAcc, oakDoor); err != nil { if err = db.CreateObject(sysAcc, oakDoor); err != nil {
@ -372,6 +378,16 @@ func (db *pgDB) SessionIDForAvatar(obj Object) (string, error) {
return *sid, nil return *sid, nil
} }
func (db *pgDB) SessionIDForObjID(id int) (string, error) {
obj, err := db.GetObjectByID(id)
if err != nil {
return "", err
}
return db.SessionIDForAvatar(*obj)
}
func (db *pgDB) AvatarBySessionID(sid string) (avatar *Object, err error) { func (db *pgDB) AvatarBySessionID(sid string) (avatar *Object, err error) {
avatar = &Object{} avatar = &Object{}
// TODO subquery // TODO subquery
@ -426,7 +442,8 @@ func (db *pgDB) Earshot(obj Object) ([]Object, error) {
WHERE id IN ( WHERE id IN (
SELECT contained FROM contains SELECT contained FROM contains
WHERE container = ( WHERE container = (
SELECT container FROM contains WHERE contained = $1 LIMIT 1))` SELECT container FROM contains WHERE contained = $1 LIMIT 1))
OR id = (SELECT container FROM contains WHERE contained = $1 LIMIT 1)`
rows, err := db.pool.Query(context.Background(), stmt, obj.ID) rows, err := db.pool.Query(context.Background(), stmt, obj.ID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -474,6 +491,54 @@ func (db *pgDB) GetObject(owner, name string) (obj *Object, err error) {
return 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) { func (db *pgDB) GetAccountAvatar(account Account) (*Object, error) {
ctx := context.Background() ctx := context.Background()
obj := &Object{ obj := &Object{

View File

@ -39,9 +39,3 @@ CREATE TABLE contains (
); );
CREATE TYPE heading AS ENUM ('north', 'south', 'east', 'west', 'above', 'below'); 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
);

View File

@ -0,0 +1,88 @@
package witch
import "fmt"
const (
dirEast = "_DIR_EAST"
dirWest = "_DIR_WEST"
dirNorth = "_DIR_NORTH"
dirSouth = "_DIR_SOUTH"
dirAbove = "_DIR_ABOVE"
dirBelow = "_DIR_BELOW"
)
type Direction struct {
raw string
}
func NewDirection(raw string) Direction {
return Direction{raw: raw}
}
func (d Direction) Reverse() Direction {
raw := ""
switch d.raw {
case dirAbove:
raw = dirBelow
case dirBelow:
raw = dirAbove
case dirEast:
raw = dirWest
case dirWest:
raw = dirEast
case dirNorth:
raw = dirSouth
case dirSouth:
raw = dirNorth
}
return NewDirection(raw)
}
// 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":
case "above":
raw = dirAbove
case "down":
case "below":
raw = dirBelow
case "east":
raw = dirEast
case "west":
raw = dirWest
case "north":
raw = dirNorth
case "south":
raw = dirSouth
default:
return Direction{}, fmt.Errorf("did not understand direction '%s'", humanDir)
}
return NewDirection(raw), nil
}
// Human returns a string form of this direction like "above" or "north"
func (d Direction) Human() (humanDir string) {
switch d.raw {
case dirAbove:
humanDir = "above"
case dirBelow:
humanDir = "below"
case dirEast:
humanDir = "east"
case dirWest:
humanDir = "west"
case dirNorth:
humanDir = "north"
case dirSouth:
humanDir = "south"
}
return humanDir
}
func (d Direction) Equals(o Direction) bool {
return d.raw == o.raw
}

View File

@ -1,62 +0,0 @@
package witch
import (
lua "github.com/yuin/gopher-lua"
)
func witchHas(l *lua.LState) int {
l.SetGlobal("_has", l.ToTable(1))
return 0
}
// TODO provides
func witchHears(l *lua.LState) int {
return addHandler(l, "say")
}
func witchSees(l *lua.LState) int {
return addHandler(l, "emote")
}
func witchGo(l *lua.LState) int {
// TODO get the handler map
// - check if handler map has a Go handler already, exit early if so
// TODO register this object as an exit in DB
return addHandler(l, "go")
}
func witchSeen(l *lua.LState) int {
return addHandler(l, "look")
}
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) int {
pattern := l.ToString(1)
cb := l.ToFunction(2)
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
}

View File

@ -1,10 +1,18 @@
package witch package witch
/*
This file is the interface between the game server and WITCH execution
*/
import ( import (
"fmt" "fmt"
"log" "log"
"regexp" "regexp"
"strings"
"github.com/vilmibm/hermeticum/proto"
"github.com/vilmibm/hermeticum/server/db" "github.com/vilmibm/hermeticum/server/db"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
@ -23,10 +31,76 @@ end)
` `
*/ */
type ServerAPI struct { type serverAPI struct {
Tell func(int, string) db db.DB
Show func(int, string) getSend func(string) func(*proto.ClientMessage) error
DB func() db.DB }
func (s *serverAPI) Tell(fromObjID, toObjID int, msg string) {
log.Printf("Tell: %d %d %s", fromObjID, toObjID, msg)
sid, err := s.db.SessionIDForObjID(toObjID)
if err != nil {
log.Println(err)
return
}
if sid == "" {
return
}
from, err := s.db.GetObjectByID(fromObjID)
if err != nil {
log.Println(err)
return
}
speakerName := "an ethereal presence"
if from.Data["name"] != "" {
speakerName = from.Data["name"]
}
log.Println(sid)
send := s.getSend(sid)
cm := proto.ClientMessage{
Type: proto.ClientMessage_OVERHEARD,
Text: msg,
Speaker: &speakerName,
}
send(&cm)
}
func (s *serverAPI) Show(fromObjID, toObjID int, action string) {
sid, err := s.db.SessionIDForObjID(toObjID)
if err != nil {
log.Println(err.Error())
return
}
from, err := s.db.GetObjectByID(fromObjID)
if err != nil {
log.Println(err)
return
}
speakerName := "an ethereal presence"
if from.Data["name"] != "" {
speakerName = from.Data["name"]
}
log.Println(sid)
send := s.getSend(sid)
cm := proto.ClientMessage{
Type: proto.ClientMessage_EMOTE,
Text: action,
Speaker: &speakerName,
}
send(&cm)
}
func (s *serverAPI) DB() db.DB {
return s.db
} }
type VerbContext struct { type VerbContext struct {
@ -37,14 +111,17 @@ type VerbContext struct {
} }
type ScriptContext struct { type ScriptContext struct {
db db.DB
getSend func(*proto.ClientMessage) error
script string script string
incoming chan VerbContext incoming chan VerbContext
serverAPI ServerAPI serverAPI serverAPI
} }
func NewScriptContext(sAPI ServerAPI) (*ScriptContext, error) { func NewScriptContext(db db.DB, getSend func(string) func(*proto.ClientMessage) error) (*ScriptContext, error) {
sc := &ScriptContext{ sc := &ScriptContext{
serverAPI: sAPI, serverAPI: serverAPI{db: db, getSend: getSend},
db: db,
} }
sc.incoming = make(chan VerbContext) sc.incoming = make(chan VerbContext)
@ -55,25 +132,54 @@ func NewScriptContext(sAPI ServerAPI) (*ScriptContext, error) {
for { for {
vc = <-sc.incoming vc = <-sc.incoming
if vc.Target.Script != sc.script { if vc.Target.Script != sc.script {
// TODO clear this object out of the exits table
sc.script = vc.Target.Script sc.script = vc.Target.Script
l = lua.NewState() l = lua.NewState()
l.SetGlobal("has", l.NewFunction(witchHas))
l.SetGlobal("hears", l.NewFunction(witchHears)) // direction constants
l.SetGlobal("sees", l.NewFunction(witchSees)) l.SetGlobal("east", lua.LString(dirEast))
l.SetGlobal("go", l.NewFunction(witchGo)) l.SetGlobal("west", lua.LString(dirWest))
l.SetGlobal("seen", l.NewFunction(witchSeen)) l.SetGlobal("north", lua.LString(dirNorth))
l.SetGlobal("my", l.NewFunction(witchMy)) l.SetGlobal("south", lua.LString(dirSouth))
l.SetGlobal("above", lua.LString(dirAbove))
l.SetGlobal("below", lua.LString(dirBelow))
l.SetGlobal("up", lua.LString(dirAbove))
l.SetGlobal("down", lua.LString(dirBelow))
// witch object behavior functions
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("_handlers", l.NewTable())
l.SetGlobal("_ID", lua.LNumber(vc.Target.ID))
if err := l.DoString(vc.Target.Script); err != nil { if err := l.DoString(vc.Target.Script); err != nil {
log.Printf("error parsing script %s: %s", vc.Target.Script, err.Error()) log.Printf("error parsing script %s: %s", vc.Target.Script, err.Error())
} }
} }
// witch action functions relative to calling context
l.SetGlobal("tellMe", l.NewFunction(func(l *lua.LState) int { l.SetGlobal("tellMe", l.NewFunction(func(l *lua.LState) int {
sender := l.GetGlobal("sender").(*lua.LTable) sender := l.GetGlobal("sender").(*lua.LTable)
senderID := int(lua.LVAsNumber(sender.RawGetString("ID"))) senderID := int(lua.LVAsNumber(sender.RawGetString("ID")))
sc.serverAPI.Tell(senderID, l.ToString(1))
log.Printf("tellMe: %d %s", senderID, l.ToString(1))
sc.serverAPI.Tell(senderID, vc.Target.ID, l.ToString(1))
return 0
}))
l.SetGlobal("tellSender", l.NewFunction(func(l *lua.LState) int {
sender := l.GetGlobal("sender").(*lua.LTable)
senderID := int(lua.LVAsNumber(sender.RawGetString("ID")))
log.Printf("tellMe: %d %s", senderID, l.ToString(1))
sc.serverAPI.Tell(vc.Target.ID, senderID, l.ToString(1))
return 0 return 0
})) }))
@ -83,7 +189,7 @@ func NewScriptContext(sAPI ServerAPI) (*ScriptContext, error) {
senderID := int(lua.LVAsNumber(sender.RawGetString("ID"))) senderID := int(lua.LVAsNumber(sender.RawGetString("ID")))
owner := l.ToString(1) owner := l.ToString(1)
name := l.ToString(2) name := l.ToString(2)
db := sc.serverAPI.DB() db := sc.db
senderObj, err := db.GetObjectByID(senderID) senderObj, err := db.GetObjectByID(senderID)
if err != nil { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
@ -104,26 +210,33 @@ func NewScriptContext(sAPI ServerAPI) (*ScriptContext, error) {
l.SetGlobal("showMe", l.NewFunction(func(l *lua.LState) int { l.SetGlobal("showMe", l.NewFunction(func(l *lua.LState) int {
sender := l.GetGlobal("sender").(*lua.LTable) sender := l.GetGlobal("sender").(*lua.LTable)
senderID := int(lua.LVAsNumber(sender.RawGetString("ID"))) senderID := int(lua.LVAsNumber(sender.RawGetString("ID")))
sc.serverAPI.Show(senderID, l.ToString(1))
log.Printf("showMe: %d %s", senderID, l.ToString(1))
sc.serverAPI.Show(senderID, vc.Target.ID, l.ToString(1))
return 0 return 0
})) }))
// TODO showSender?
// TODO check execute permission and bail out potentially // TODO check execute permission and bail out potentially
log.Printf("%#v", vc) //log.Printf("%#v", vc)
senderT := l.NewTable() senderT := l.NewTable()
senderT.RawSetString("name", lua.LString(vc.Sender.Data["name"])) senderT.RawSetString("name", lua.LString(vc.Sender.Data["name"]))
senderT.RawSetString("ID", lua.LNumber(vc.Sender.ID)) senderT.RawSetString("ID", lua.LNumber(vc.Sender.ID))
l.SetGlobal("sender", senderT) l.SetGlobal("sender", senderT)
l.SetGlobal("msg", lua.LString(vc.Rest)) l.SetGlobal("msg", lua.LString(vc.Rest))
l.SetGlobal("_SENDERID", lua.LNumber(vc.Sender.ID))
handlers := l.GetGlobal("_handlers").(*lua.LTable) handlers := l.GetGlobal("_handlers").(*lua.LTable)
handlers.ForEach(func(k, v lua.LValue) { handlers.ForEach(func(k, v lua.LValue) {
log.Println("checking handler verbs", k)
if k.String() != vc.Verb { if k.String() != vc.Verb {
return return
} }
v.(*lua.LTable).ForEach(func(kk, vv lua.LValue) { v.(*lua.LTable).ForEach(func(kk, vv lua.LValue) {
pattern := regexp.MustCompile(kk.String()) pattern := regexp.MustCompile(kk.String())
log.Println("checking handler", kk.String(), vv, pattern)
if pattern.MatchString(vc.Rest) { if pattern.MatchString(vc.Rest) {
// TODO TODO TODO TODO TODO // TODO TODO TODO TODO TODO
// this could be a remote code execution vuln; but by being here, I // this could be a remote code execution vuln; but by being here, I
@ -144,3 +257,126 @@ func NewScriptContext(sAPI ServerAPI) (*ScriptContext, error) {
func (sc *ScriptContext) Handle(vc VerbContext) { func (sc *ScriptContext) Handle(vc VerbContext) {
sc.incoming <- vc 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))
}

View File

@ -159,3 +159,18 @@ provides("give $this $money $unit", function(args)
say("i need more money") say("i need more money")
end end
end) end)
-- Example 3: a rusty door
has({
name = "rusty metal door"
description = "it's almost fully consumed by rust but still heavy and solid feeling"
})
allows({
read = "world",
write = "owner"
carry = "owner",
execute = "world",
})
goes("east", "gallery")