package funmow import ( "fmt" "regexp" "strings" "time" ) type ExecutionContext struct { actor 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() { //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 // this isn't just an edge case, it happens quite frequently. // The alternative is to buffer inbound events, which is the safest, as it doesn't // impact the eventdistributor. 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 fmt.Println("close(inboundBuffer)") close(inboundBuffer) }() go func() { for { event, ok := <-inboundBuffer if !ok { break } c.HandleEvent(event) } // and finally we tell the event distributor to delete us. fmt.Println("Sending EventTypeTeardownComplete") c.outbound <- PlayerEvent{src: c.actor.ID, dst: c.actor.ID, messageType: EventTypeTeardownComplete} }() return c.inbound } func (c *ExecutionContext) HandleEvent(m PlayerEvent) { inside, _ := c.db.GetParent(c.actor.ID) switch m.messageType { case EventTypeEmit: fallthrough case EventTypeOEmit: if m.dst == inside { 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 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 == inside { speaker, found := c.db.Fetch(m.src) if !found { break } c.output("%s says \"%s\"", speaker.Name, m.message) } case EventTypePose: if m.dst == inside { 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 c.actor.Refresh() switch { 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, "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 message == "HOWLONG": time.Sleep(5 * time.Second) 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, "@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(at string) { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { c.output("True Limbo (#NaN)\nThere's nothing to see here. You are inside an object that doesn't exist.") return } if len(at) > 0 { object, matchType := room.MatchLinkNames(at, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: c.output("%s\n%s", object.DetailedName(), object.Description) case MatchNone: c.output("I don't see that here.") case MatchMany: c.output("I don't now which one you're trying to look at.") } } else { c.output("%s\n%s", room.DetailedName(), room.Description) lookLinks := func(linkType string, pretty string) { linknames := room.GetLinkNames(linkType, DBRefList{c.actor.ID}) if len(linknames) > 0 { c.output("%s\n %s", pretty, strings.Join(linknames, "\n ")) } } lookLinks("thing", "Things:") lookLinks("player", "Players:") exits := room.GetLinkNames("exit", nil) if len(exits) > 0 { c.output("Exits:") exitList := make([]string, 0) for _, e := range exits { aliases := strings.Split(e, ";") exitList = append(exitList, aliases[0]) } c.output(strings.Join(exitList, " ")) } } } 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(at string) { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } object, matchType := room.MatchLinkNames(at, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: objectParentID, _ := c.db.GetParent(object.ID) 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(objectParentID)) c.output("Next: %s", c.objectName(object.Next)) c.output("Owner: %s", c.objectName(object.Owner)) inventory := object.GetLinkNames("*", nil) if len(inventory) > 0 { c.output("Contents:\n %s", strings.Join(inventory, "\n ")) } case MatchNone: c.output("I don't see that here.") case MatchMany: c.output("I don't know which one you're trying to examine.") } } 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] roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } objectID, err := NewDBRefFromHashRef(objectName) wizard := false if err == nil { object, found := c.db.Fetch(objectID) if !found { c.output("I can't force what doesn't exist.") } if object.Type == "thing" || (object.Type == "player" && wizard) { c.outbound <- PlayerEvent{src: c.actor.ID, dst: object.ID, message: command, messageType: EventTypeForce} } else { c.output("Some things just can't be forced.") } } else { object, matchType := room.MatchLinkNames(objectName, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: if object.Type == "thing" || (object.Type == "player" && wizard) { c.outbound <- PlayerEvent{src: c.actor.ID, dst: object.ID, message: command, messageType: EventTypeForce} } else { c.output("Some things just can't be forced.") } case MatchNone: c.output("I don't see that here.") case MatchMany: c.output("I don't know which one you're trying to examine.") } } } func (c *ExecutionContext) sayCmd(message string) { inside, _ := c.db.GetParent(c.actor.ID) c.output("You say \"%s\"", message) c.outbound <- PlayerEvent{src: c.actor.ID, dst: inside, message: message, messageType: EventTypeSay} } func (c *ExecutionContext) poseCmd(message string) { inside, _ := c.db.GetParent(c.actor.ID) c.outbound <- PlayerEvent{src: c.actor.ID, dst: inside, message: message, messageType: EventTypePose} } func (c *ExecutionContext) inventoryCmd() { inventory := c.actor.GetLinkNames("*", nil) 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(message string) { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } object, matchType := room.MatchLinkNames(message, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: if object.ID == c.actor.ID { c.output("You can't pick yourself up.") 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(room.ID, "%s picks up %s.", c.actor.Name, object.Name) case MatchNone: c.output("I don't see that here.") case MatchMany: c.output("I don't know which one.") } } func (c *ExecutionContext) dropCmd(message string) { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } object, matchType := c.actor.MatchLinkNames(message, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: err := room.Contains(&object) if err != nil { return } inside, _ := c.db.GetParent(c.actor.ID) c.output("You drop %s.", object.Name) c.pemit(object.ID, "%s drops you.", c.actor.Name) c.oemit(inside, "%s drops %s.", c.actor.Name, object.Name) case MatchNone: c.output("You're not carrying that.") case MatchMany: c.output("I don't now which one.") } } func (c *ExecutionContext) enterCmd(message string) { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } object, matchType := room.MatchLinkNames(message, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: 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(room.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) case MatchNone: c.output("I don't see that here.") case MatchMany: c.output("I don't now which one.") } } func (c *ExecutionContext) leaveCmd() { inside, _ := c.db.GetParent(c.actor.ID) object, found := c.db.Fetch(inside) if !found { return } objectInside, _ := c.db.GetParent(inside) if objectInside == 0 { // probably trying to 'leave' a room c.output("You can't leave here.") return } room, found := c.db.Fetch(objectInside) if !found { return } err := room.Contains(&c.actor) if err != nil { return } //c.actor.Refresh() c.output("You climb out of %s.", object.Name) c.oemit(object.ID, "%s climbs out of %s.", c.actor.Name, object.Name) c.oemit(room.ID, "%s climbs out of %s.", c.actor.Name, object.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) { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } o := c.factory.NewThing() o.Name = strings.TrimSpace(message) o.Owner = c.actor.ID o.Commit() err := room.Contains(&o) if err != nil { return } c.oemit(room.ID, "A %s appears out of the ether.", o.Name) 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 } roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } toExit := c.factory.NewExit(inExit, targetRoom.ID, c.actor.ID) toExit.Commit() err := room.Contains(&toExit) if err != nil { return } c.output("%s Created.", toExit.DetailedName()) if len(outExit) > 0 { fromExit := c.factory.NewExit(outExit, room.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 { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } if len(inExit) > 0 { toExit := c.factory.NewExit(inExit, newRoom.ID, c.actor.ID) toExit.Commit() err := room.Contains(&toExit) if err != nil { return } c.output("%s Created.", toExit.DetailedName()) } if len(outExit) > 0 { fromExit := c.factory.NewExit(outExit, room.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 } objectName, name := params[1], params[2] roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } candidate, matchType := room.MatchLinkNames(objectName, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: candidate.Name = name candidate.Commit() c.output("Name set.") //c.actor.Refresh() case MatchNone: c.output("I don't see that here.") case MatchMany: c.output("I don't now which one.") } } 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] roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } var editObject *Object switch objectName { case "here": editObject = &room case "me": editObject = &c.actor default: candidate, matchType := room.MatchLinkNames(objectName, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: editObject = &candidate case MatchNone: c.output("I don't see that here.") return case MatchMany: c.output("I don't now which one.") return } } editObject.Description = description editObject.Commit() //c.actor.Refresh() c.output("Description set.") } func (c *ExecutionContext) telCmd(destStr string) { dest, err := NewDBRefFromHashRef(destStr) if err != nil { c.output("That doesn't look like a DBRef.") return } newRoom, found := c.db.Fetch(dest) if !found { c.output("That doesn't exist.") return } c.output("You feel an intense wooshing sensation.") err = newRoom.Contains(&c.actor) if err != nil { return } c.actor.Refresh() c.lookCmd("") } func (c *ExecutionContext) dumpCmd(refStr string) { ref, err := NewDBRefFromHashRef(refStr) if err != nil { c.output("That doesn't look like a DBRef.") return } obj, found := c.db.Fetch(ref) if !found { c.output("That doesn't exist.") return } c.output("%s", c.db.DumpObject(obj.ID)) } func (c *ExecutionContext) destroyCmd(message string) { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return } object, matchType := room.MatchLinkNames(message, c.actor.ID).ExactlyOne() switch matchType { case MatchOne: 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.actor.Refresh() c.output("%s vanishes into thin air.", name) } case MatchNone: c.output("I don't see that here.") case MatchMany: c.output("I don't know which one.") } } func (c *ExecutionContext) goCmd(dir string) bool { roomID, found := c.db.GetParent(c.actor.ID) room, found := c.db.Fetch(roomID) if !found { return false } exit, matchType := room.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(room.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) 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} }