hermeticum/server/witch/witch.go

383 lines
9.3 KiB
Go

package witch
/*
This file is the interface between the game server and WITCH execution
*/
import (
"fmt"
"log"
"regexp"
"strings"
"github.com/vilmibm/hermeticum/proto"
"github.com/vilmibm/hermeticum/server/db"
lua "github.com/yuin/gopher-lua"
)
/*
allows({
read = "world",
write = "owner"
carry = "owner",
execute = "world",
})
hears(".*eat.*", function()
does("quivers nervously")
end)
`
*/
type serverAPI struct {
db db.DB
getSend func(string) func(*proto.ClientMessage) error
}
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 {
Verb string
Rest string
Sender db.Object
Target db.Object
}
type ScriptContext struct {
db db.DB
getSend func(*proto.ClientMessage) error
script string
incoming chan VerbContext
serverAPI serverAPI
}
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)
go func() {
var l *lua.LState
var err error
var vc VerbContext
for {
vc = <-sc.incoming
if vc.Target.Script != sc.script {
sc.script = vc.Target.Script
l = lua.NewState()
// direction constants
l.SetGlobal("east", lua.LString(dirEast))
l.SetGlobal("west", lua.LString(dirWest))
l.SetGlobal("north", lua.LString(dirNorth))
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("_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())
}
}
// witch action functions relative to calling context
l.SetGlobal("tellMe", 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(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
}))
l.SetGlobal("moveSender", l.NewFunction(func(l *lua.LState) (ret int) {
ret = 0
sender := l.GetGlobal("sender").(*lua.LTable)
senderID := int(lua.LVAsNumber(sender.RawGetString("ID")))
owner := l.ToString(1)
name := l.ToString(2)
db := sc.db
senderObj, err := db.GetObjectByID(senderID)
if err != nil {
log.Println(err.Error())
return
}
container, err := db.GetObject(owner, name)
if err != nil {
log.Println(err.Error())
return
}
if err = db.MoveInto(*senderObj, *container); err != nil {
log.Println(err.Error())
}
return
}))
l.SetGlobal("showMe", l.NewFunction(func(l *lua.LState) int {
sender := l.GetGlobal("sender").(*lua.LTable)
senderID := int(lua.LVAsNumber(sender.RawGetString("ID")))
log.Printf("showMe: %d %s", senderID, l.ToString(1))
sc.serverAPI.Show(senderID, vc.Target.ID, l.ToString(1))
return 0
}))
// TODO showSender?
// TODO check execute permission and bail out potentially
//log.Printf("%#v", vc)
senderT := l.NewTable()
senderT.RawSetString("name", lua.LString(vc.Sender.Data["name"]))
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) {
log.Println("checking handler verbs", k)
if k.String() != vc.Verb {
return
}
v.(*lua.LTable).ForEach(func(kk, vv lua.LValue) {
pattern := regexp.MustCompile(kk.String())
log.Println("checking handler", kk.String(), vv, pattern)
if pattern.MatchString(vc.Rest) {
// TODO TODO TODO TODO TODO
// this could be a remote code execution vuln; but by being here, I
// believe vc.Verb has been effectively validated as "not a pile of
// lua code" since it matched a handler.
if err = l.DoString(fmt.Sprintf(`_handlers.%s["%s"]()`, vc.Verb, pattern)); err != nil {
log.Println(err.Error())
}
}
})
})
}
}()
return sc, nil
}
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))
}