package funmow import ( "fmt" "regexp" "strings" "unicode" ) type ExecutionContext struct { actor Object context Object inbound chan PlayerEvent outbound chan PlayerEvent eventDistributor *EventDistributor db *DB factory *ObjectFactory forceContext bool } func NewForceContext(e *EventDistributor, w *DB, outbound chan PlayerEvent) *ExecutionContext { c := new(ExecutionContext) c.outbound = outbound c.eventDistributor = e c.db = w c.factory = NewObjectFactory(w) c.forceContext = true return c } func NewExecutionContext(actorID DBRef, e *EventDistributor, w *DB, outbound chan PlayerEvent) *ExecutionContext { c := new(ExecutionContext) actor, found := w.Fetch(actorID) if !found { return nil } c.actor = actor c.outbound = outbound c.eventDistributor = e c.db = w c.factory = NewObjectFactory(w) return c } func (c *ExecutionContext) StartInboundChannel() chan PlayerEvent { c.inbound = make(chan PlayerEvent) inboundBuffer := make(chan PlayerEvent) c.outbound = c.outbound go func() { // An inbound event buffer to protect the event distributor from long // running tasks. The alternative is to run each inbound event in a // separate goroutine but that has potential problems (ie, two emits // arrive in order; the first one requires an extra DB lookup to sort // out context. Now the second one ends up sending its output event // before the first, and the client sees things backwards. queue := make([]*PlayerEvent, 0) running := true for running { if len(queue) == 0 { select { case newEvent, ok := <-c.inbound: if !ok { running = false break } queue = append(queue, &newEvent) } } else { event := queue[0] select { case newEvent, ok := <-c.inbound: if !ok { running = false break } queue = append(queue, &newEvent) case inboundBuffer <- *event: queue = queue[1:] } } } // closure of c.inbound is the signal from the event distributor that we need to die. // we then close inboundBuffer to tell the event goroutine to die close(inboundBuffer) }() go func() { for { event, ok := <-inboundBuffer if !ok { break } c.HandleEvent(event) } // mark player as offline if c.actor.Type == "player" { c.actor.SetFlag("online", false) c.actor.Commit() } // and finally we tell the event distributor to delete us. c.outbound <- PlayerEvent{src: c.actor.ID, dst: c.actor.ID, messageType: EventTypeTeardownComplete} }() return c.inbound } func (c *ExecutionContext) HandleEvent(m PlayerEvent) { c.actor.Refresh() contextID, found := c.db.GetParent(c.actor.ID) if found { c.context, _ = c.db.Fetch(contextID) } switch m.messageType { case EventTypeLogin: if c.actor.Type == "player" { c.actor.SetFlag("online", true) c.actor.Commit() c.system("Yay! %s has connected! Oh boy!", c.actor.Name) c.oemit(c.context.ID, "You hear a rustling as %s awakens from a slumber.", c.actor.Name) } case EventTypeEmit: fallthrough case EventTypeOEmit: if m.dst == c.context.ID { c.output("%s", m.message) } case EventTypePEmit: c.output("%s", m.message) case EventTypeWall: speaker, found := c.db.Fetch(m.src) if !found { break } c.output("In the distance, you hear %s bellow out: \"%s\"", speaker.Name, m.message) case EventTypeSystem: c.output(m.message) case EventTypePage: speaker, found := c.db.Fetch(m.src) if !found { break } c.output("%s pages you: \"%s\"", speaker.Name, m.message) case EventTypeSay: if m.dst == c.context.ID { speaker, found := c.db.Fetch(m.src) if !found { break } c.output("%s says \"%s\"", speaker.Name, m.message) } case EventTypePose: if m.dst == c.context.ID { if m.src == c.actor.ID { c.output("%s %s", c.actor.Name, m.message) } else { speaker, found := c.db.Fetch(m.src) if !found { break } c.output("%s %s", speaker.Name, m.message) } } case EventTypeForce: actor, found := c.db.Fetch(m.dst) if !found { break } c.actor = actor c.evaluateCommand(m) case EventTypeCommand: c.evaluateCommand(m) } } func (c *ExecutionContext) evaluateCommand(m PlayerEvent) { message := m.message switch { case message == "whoami": c.output(c.actor.Name) case message == "l": c.lookCmd("") case message == "look": c.lookCmd("") case strings.HasPrefix(message, "l "): // look at c.lookCmd(strings.TrimPrefix(message, "l ")) case strings.HasPrefix(message, "look "): // look at c.lookCmd(strings.TrimPrefix(message, "look ")) case strings.HasPrefix(message, "ex "): c.examineCmd(strings.TrimPrefix(message, "ex ")) case strings.HasPrefix(message, "examine "): c.examineCmd(strings.TrimPrefix(message, "examine ")) case strings.HasPrefix(message, "\""): c.sayCmd(strings.TrimPrefix(message, "\"")) case strings.HasPrefix(message, "say "): c.sayCmd(strings.TrimPrefix(message, "say ")) case strings.HasPrefix(message, ":"): c.poseCmd(strings.TrimPrefix(message, ":")) case strings.HasPrefix(message, "pose "): c.poseCmd(strings.TrimPrefix(message, "pose ")) case message == "i": c.inventoryCmd() case message == "inventory": c.inventoryCmd() case strings.HasPrefix(message, "get "): c.getCmd(strings.TrimPrefix(message, "get ")) case strings.HasPrefix(message, "drop "): c.dropCmd(strings.TrimPrefix(message, "drop ")) case strings.HasPrefix(message, "give "): c.giveCmd(message) case strings.HasPrefix(message, "put "): c.putCmd(message) case strings.HasPrefix(message, "enter "): c.enterCmd(strings.TrimPrefix(message, "enter ")) case message == "leave": c.leaveCmd() case message == "quit": c.quitCmd(m.connectionID) case message == "WHO": c.whoCmd() case strings.HasPrefix(message, "@create "): c.createCmd(strings.TrimPrefix(message, "@create ")) case strings.HasPrefix(message, "@dig "): c.digCmd(message) case strings.HasPrefix(message, "@open "): c.openCmd(message) case strings.HasPrefix(message, "@name "): c.nameCmd(message) case strings.HasPrefix(message, "@desc "): c.descCmd(message) case strings.HasPrefix(message, "@set "): c.setCmd(message) case strings.HasPrefix(message, "@tel "): c.telCmd(strings.TrimPrefix(message, "@tel ")) case strings.HasPrefix(message, "@dump "): c.dumpCmd(strings.TrimPrefix(message, "@dump ")) case strings.HasPrefix(message, "@destroy "): c.destroyCmd(strings.TrimPrefix(message, "@destroy ")) case strings.HasPrefix(message, "@force "): c.forceCmd(message) default: if !c.goCmd(message) { c.output("What?\n") } } } func (c *ExecutionContext) lookCmd(target string) { if len(target) > 0 { object, found := c.MatchFirst(c.context.ID, target, false, false) if !found { c.output("I don't see that here.") return } c.output("%s\n%s", object.DetailedName(), object.Description) } else { c.output("%s\n%s", c.context.DetailedName(), c.context.Description) contents := c.context.GetContents(c.actor.ID) if len(contents) > 0 { c.output("Contents:\n" + strings.Join(contents, "\n")) } exits := c.context.GetExits() if len(exits) > 0 { c.output("Obvious Exits:\n" + strings.Join(exits, " ")) } } } func (c *ExecutionContext) objectName(id DBRef) string { o, found := c.db.Fetch(id) if found { return o.DetailedName() } else { return fmt.Sprintf("MISSING OBJECT <#%d>", id) } } func (c *ExecutionContext) examineCmd(objectName string) { object, found := c.MatchFirst(c.context.ID, objectName, true, true) if !found { c.output("I don't see that here.") return } c.output("%s", object.DetailedName()) c.output("ID: %d", object.ID) c.output("Type: %s", object.Type) c.output("@name: %s", object.Name) c.output("@desc: %s", object.Description) c.output("Inside: %s", c.objectName(c.context.ID)) c.output("Next: %s", c.objectName(object.Next)) c.output("Owner: %s", c.objectName(object.Owner)) contents := object.GetContents(0) if len(contents) > 0 { c.output("Contents:\n %s", strings.Join(contents, "\n ")) } exits := object.GetExits() if len(exits) > 0 { c.output("Exits:\n %s", strings.Join(exits, "\n ")) } flags := make([]string, 0, len(object.Flags)) for k, v := range object.Flags { if v { flags = append(flags, k) } } c.output("Flags:\n%s", strings.Join(flags, ", ")) properties := make([]string, 0, len(object.Properties)) for k, v := range object.Properties { properties = append(properties, fmt.Sprintf("%s: \"%s\"", k, v)) } c.output("Properties:\n%s", strings.Join(properties, ", ")) } func (c *ExecutionContext) forceCmd(input string) { r, _ := regexp.Compile(`^@force\pZ+([^=]*[^=\pZ]{1})\pZ*=\pZ*(.*)\pZ*$`) params := r.FindStringSubmatch(input) if params == nil { return } objectName, command := params[1], params[2] wizard := c.actor.GetFlag("wizard") object, found := c.MatchFirst(c.context.ID, objectName, true, false) if !found { c.output("I don't see that here.") return } if object.Type != "thing" && object.Type != "player" { c.output("Some things just can't be forced.") return } if object.Type == "player" && !wizard { c.output("It's not nice to force others to do your bidding.") return } if object.Owner != c.actor.ID && !wizard { c.output("It's not nice to touch other people's things.") return } c.outbound <- PlayerEvent{src: c.actor.ID, dst: object.ID, message: command, messageType: EventTypeForce} } func (c *ExecutionContext) sayCmd(message string) { c.output("You say \"%s\"", message) c.outbound <- PlayerEvent{src: c.actor.ID, dst: c.context.ID, message: message, messageType: EventTypeSay} } func (c *ExecutionContext) poseCmd(message string) { c.outbound <- PlayerEvent{src: c.actor.ID, dst: c.context.ID, message: message, messageType: EventTypePose} } func (c *ExecutionContext) inventoryCmd() { inventory := c.actor.GetContents(0) if len(inventory) > 0 { c.output("Inventory:\n %s", strings.Join(inventory, "\n ")) } else { c.output("You're not carrying anything.") } } func (c *ExecutionContext) getCmd(objectName string) { object, found := c.MatchFirst(c.context.ID, objectName, false, false) if !found { c.output("I don't see that here.") return } if object.ID == c.actor.ID { c.output("You can't pick yourself up.") return } if object.Type == "room" { c.output("You can't carry a whole room, silly!") return } err := c.actor.Contains(&object) if err != nil { return } //c.actor.Refresh() c.output("You pick up %s.", object.Name) c.pemit(object.ID, "%s picked you up.", c.actor.Name) c.oemit(c.context.ID, "%s picks up %s.", c.actor.Name, object.Name) } func (c *ExecutionContext) putCmd(input string) { c.xferCmd(input, "put", "into") } func (c *ExecutionContext) giveCmd(input string) { c.xferCmd(input, "give", "to") } func (c *ExecutionContext) xferCmd(input string, verb string, preposition string) { r, _ := regexp.Compile(`^` + verb + `\pZ+([^=]*[^=\pZ]{1})\pZ*` + preposition + `\pZ*(.*)\pZ*$`) params := r.FindStringSubmatch(input) if params == nil { return } objectName, receiverName := params[1], params[2] object, found := c.MatchFirst(c.actor.ID, objectName, false, false) if !found { c.output("You can't " + verb + " what you don't have.") return } receiver, found := c.MatchFirst(c.context.ID, receiverName, false, false) if !found { c.output("I cant find who or what you want to " + verb + " this " + preposition + ".") return } err := receiver.Contains(&object) if err != nil { return } c.output("You "+verb+" %s "+preposition+" %s.", object.Name, receiver.Name) c.pemit(object.ID, "%s "+verb+"s you "+preposition+" %s.", c.actor.Name, receiver.Name) if verb == "give" { c.pemit(receiver.ID, "%s gives you %s.", c.actor.Name, object.Name) } else { c.pemit(receiver.ID, "%s puts %s into you.", c.actor.Name, object.Name) } } func (c *ExecutionContext) MatchFirst(context DBRef, matchName string, globalDBRefMatch bool, playerNameMatch bool) (Object, bool) { if matchName == "here" { return c.db.Fetch(context) } if matchName == "me" { return c.actor, true } if globalDBRefMatch { ref, valid := NewDBRefFromHashRef(matchName) if valid { o, found := c.db.Fetch(ref) if found { return o, true } } } if playerNameMatch { playerMeta, foundMeta := c.db.GetPlayer(matchName) if foundMeta { o, found := c.db.Fetch(playerMeta.ID) if found { return o, true } } } for childID, _ := range c.db.GetChildren(context) { o, found := c.db.Fetch(childID) if found { _, refValid := NewDBRefFromHashRef(matchName) lowerObjectName := strings.ToLower(o.Name) lowerMatchName := strings.ToLower(matchName) if strings.HasPrefix(lowerObjectName, lowerMatchName) || refValid { return o, true } } } return Object{}, false } func (c *ExecutionContext) dropCmd(objectName string) { object, found := c.MatchFirst(c.actor.ID, objectName, false, false) if !found { c.output("You're not carrying that.") return } err := c.context.Contains(&object) if err != nil { return } c.output("You drop %s.", object.Name) c.pemit(object.ID, "%s drops you.", c.actor.Name) c.oemit(c.context.ID, "%s drops %s.", c.actor.Name, object.Name) } func (c *ExecutionContext) enterCmd(objectName string) { object, found := c.MatchFirst(c.context.ID, objectName, false, false) if !found { c.output("I don't see that here.") return } if object.Type == "player" { c.output("Maybe you should seek consent prior to entering another player.") return } if object.Type == "exit" { c.output("This s not how exits are meant to be used.") return } if object.ID == c.actor.ID { c.output("Entering yourself would be a bad idea.") return } err := object.Contains(&c.actor) if err != nil { return } c.output("You climb into %s.", object.Name) c.oemit(c.context.ID, "%s climbs into %s.", c.actor.Name, object.Name) c.oemit(object.ID, "%s squeezes into %s with you.", c.actor.Name, object.Name) } func (c *ExecutionContext) leaveCmd() { outerObjectID, _ := c.db.GetParent(c.context.ID) if outerObjectID == 0 { // probably trying to 'leave' a room c.output("You can't leave here.") return } container, found := c.db.Fetch(outerObjectID) if !found { return } err := container.Contains(&c.actor) if err != nil { return } c.output("You climb out of %s.", c.context.Name) c.oemit(c.context.ID, "%s climbs out of %s.", c.actor.Name, c.context.Name) c.oemit(container.ID, "%s climbs out of %s.", c.actor.Name, c.context.Name) } func (c *ExecutionContext) quitCmd(connectionID int) { c.output("So long, it's been good to know yah.") c.outbound <- PlayerEvent{src: c.actor.ID, dst: c.actor.ID, messageType: EventTypeQuit, connectionID: connectionID} } func (c *ExecutionContext) whoCmd() { // onlinePlayers := c.eventDistributor.OnlinePlayers() c.output("Currently Online:\n") //for _, ref := range onlinePlayers { // c.output("%s\n", c.db.GetName(ref)) //} } func (c *ExecutionContext) createCmd(message string) { o := c.factory.NewThing() o.Name = strings.TrimSpace(message) o.Owner = c.actor.ID o.Commit() err := c.actor.Contains(&o) if err != nil { return } c.output("%s Created.", o.DetailedName()) } func (c *ExecutionContext) openCmd(input string) { // @open =#, r, _ := regexp.Compile(`^@open\pZ+([^=]*[^=\pZ]+)\pZ*=#([0-9]+)(?:\pZ*,\pZ*([^,]*[^,\pZ]+)\pZ*)?`) params := r.FindStringSubmatch(input) if params == nil { return } inExit, roomIDStr, outExit := params[1], params[2], params[3] if len(inExit) == 0 || len(roomIDStr) == 0 { c.output("Bad command or file name.") return } targetID, _ := NewDBRefFromString(roomIDStr) // this will never fail, the regexp guarantees that targetRoom, found := c.db.Fetch(targetID) if !found { c.output("Target not found.") return } toExit := c.factory.NewExit(inExit, targetRoom.ID, c.actor.ID) toExit.Commit() err := c.context.Contains(&toExit) if err != nil { return } c.output("%s Created.", toExit.DetailedName()) if len(outExit) > 0 { fromExit := c.factory.NewExit(outExit, c.context.ID, c.actor.ID) fromExit.Commit() err = targetRoom.Contains(&fromExit) if err != nil { return } c.output("%s Created.", fromExit.DetailedName()) } } func (c *ExecutionContext) digCmd(input string) { // @dig =, //@dig foo bar = oo;foo;f,ack;back;b r, _ := regexp.Compile(`^@dig\pZ+([^=]*[^=\pZ]+)(\pZ*=\pZ*(?:([^,]*[^,\pZ]+)\pZ*)?(?:\pZ*,\pZ*([^,]*[^,\pZ]+)\pZ*)?)?`) params := r.FindStringSubmatch(input) if params == nil { return } roomName, inExit, outExit := params[1], params[2], params[3] if len(roomName) == 0 { c.output("Rooms can't not have names.") return } newRoom := c.factory.NewRoom() newRoom.Name = roomName newRoom.Owner = c.actor.ID newRoom.Commit() c.output("%s Created.", newRoom.DetailedName()) if len(inExit) > 0 || len(outExit) > 0 { if len(inExit) > 0 { toExit := c.factory.NewExit(inExit, newRoom.ID, c.actor.ID) toExit.Commit() err := c.context.Contains(&toExit) if err != nil { return } c.output("%s Created.", toExit.DetailedName()) } if len(outExit) > 0 { fromExit := c.factory.NewExit(outExit, c.context.ID, c.actor.ID) fromExit.Commit() err := newRoom.Contains(&fromExit) if err != nil { return } c.output("%s Created.", fromExit.DetailedName()) } } } func (c *ExecutionContext) nameCmd(input string) { r, _ := regexp.Compile(`^@name\pZ+([^=]*[^=\pZ]{1})\pZ*=\pZ*(.*)\pZ*$`) params := r.FindStringSubmatch(input) if params == nil { return } searchName, newName := params[1], params[2] candidate, found := c.MatchFirst(c.context.ID, searchName, true, false) if !found { c.output("I don't see that here.") return } if candidate.Type == "player" { i := strings.IndexFunc(newName, func(c rune) bool { return unicode.IsSpace(c) }) if i != -1 { c.output("Player names can't have spaces.") return } if c.actor.ID != candidate.ID && !c.actor.GetFlag("wizard") { c.output("Only wizards can rename other players.") return } err := c.db.RenamePlayer(candidate.Name, newName) if err != nil { c.output("I can't do that. Something has gone wrong.") return } } candidate.Name = newName candidate.Commit() c.output("Name set.") } func (c *ExecutionContext) setCmd(input string) { r, _ := regexp.Compile(`^@set\pZ+([^=]*[^=\pZ]{1})\pZ*=\pZ*(!)?(.*)\pZ*$`) params := r.FindStringSubmatch(input) if params == nil { return } objectName, value, flag := params[1], params[2] != "!", params[3] object, found := c.MatchFirst(c.context.ID, objectName, true, true) if !found { c.output("I don't know what that is") return } object.SetFlag(flag, value) object.Commit() c.output("It is so.") } func (c *ExecutionContext) descCmd(input string) { r, _ := regexp.Compile(`^@desc\pZ+([^=]*[^=\pZ]{1})\pZ*=\pZ*(.*)\pZ*$`) params := r.FindStringSubmatch(input) if params == nil { return } objectName, description := params[1], params[2] object, found := c.MatchFirst(c.actor.ID, objectName, false, false) if !found { c.output("I don't see that here.") return } object.Description = description object.Commit() c.output("Description set.") } func (c *ExecutionContext) telCmd(destStr string) { var dest Object var found bool dest, found = c.MatchFirst(c.context.ID, destStr, true, false) if !found { c.output("Invalid destination.") return } if dest.Type == "exit" { c.output("As fun is it sounds, you're not going to teleport into an exit.") return } if dest.Type == "player" { c.output("Teleporting into players is very impolite.") return } if dest.Owner != c.actor.ID && !dest.GetFlag("jump_ok") { c.output("You're not allowed to do that.") } c.output("You feel an intense wooshing sensation.") c.oemit(c.context.ID, "%s teleports out of the room.", c.actor.Name) c.oemit(dest.ID, "%s teleports in to the room.", c.actor.Name) err := dest.Contains(&c.actor) if err != nil { return } c.actor.Refresh() c.lookCmd("") } func (c *ExecutionContext) dumpCmd(refStr string) { object, found := c.MatchFirst(c.context.ID, refStr, true, true) if !found { c.output("I can't find that.") return } c.output("%s", c.db.DumpObject(object.ID)) } func (c *ExecutionContext) destroyCmd(target string) { object, found := c.MatchFirst(c.actor.ID, target, true, false) if !found { c.output("I don't see that here.") return } if object.ID == c.actor.ID { c.output("There are alternatives to suicide.") return } if object.Type == "player" { c.output("I didn't think homicide was your thing.") return } name := object.DetailedName() err := object.Delete() if err == nil { c.output("%s vanishes into thin air.", name) } } func (c *ExecutionContext) goCmd(dir string) bool { exit, matchType := c.context.MatchExitNames(dir).ExactlyOne() switch matchType { case MatchOne: if exit.Next.Valid() { newRoom, found := c.db.Fetch(exit.Next) if !found { c.output("That exit appears to be broken!") return true } err := newRoom.Contains(&c.actor) if err != nil { return false } //c.actor.Refresh() c.output("You head towards %s.", newRoom.Name) c.oemit(c.context.ID, "%s leaves the room.", c.actor.Name) c.oemit(newRoom.ID, "%s enters the room.", c.actor.Name) return true } case MatchNone: return false case MatchMany: c.output("Ambiguous exit names are ambiguous.") return true } return false } func (c *ExecutionContext) wall(format string, a ...interface{}) { message := fmt.Sprintf(format, a...) c.outbound <- PlayerEvent{src: c.actor.ID, message: message, messageType: EventTypeWall} } func (c *ExecutionContext) system(format string, a ...interface{}) { message := fmt.Sprintf(format, a...) c.outbound <- PlayerEvent{src: c.actor.ID, message: message, messageType: EventTypeSystem} } func (c *ExecutionContext) oemit(audience DBRef, format string, a ...interface{}) { message := fmt.Sprintf(format, a...) c.outbound <- PlayerEvent{src: c.actor.ID, dst: audience, message: message, messageType: EventTypeOEmit} } func (c *ExecutionContext) output(format string, a ...interface{}) { message := fmt.Sprintf(format, a...) if !c.forceContext { c.outbound <- PlayerEvent{src: c.actor.ID, dst: c.actor.ID, message: message, messageType: EventTypeOutput} } } func (c *ExecutionContext) pemit(target DBRef, format string, a ...interface{}) { message := fmt.Sprintf(format, a...) c.outbound <- PlayerEvent{src: c.actor.ID, dst: target, message: message, messageType: EventTypePEmit} } func (c *ExecutionContext) emit(audience DBRef, format string, a ...interface{}) { message := fmt.Sprintf(format, a...) c.outbound <- PlayerEvent{src: c.actor.ID, dst: audience, message: message, messageType: EventTypeEmit} }