Compare commits
10 Commits
9b7e92c118
...
84cfc0f12d
Author | SHA1 | Date |
---|---|---|
nate smith | 84cfc0f12d | |
nate smith | 3bb94c490e | |
nate smith | 8fd8331a96 | |
nate smith | 209833798b | |
nate smith | e86ac5875a | |
nate smith | dd5c377144 | |
nate smith | 31af07f7e7 | |
nate smith | df68631aa3 | |
vilmibm | 7289d38459 | |
vilmibm | f4934e28f3 |
11
go.mod
11
go.mod
|
@ -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
|
||||||
|
)
|
||||||
|
|
57
roadmap.md
57
roadmap.md
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue