package westworldwithmessaging import ( "fmt" "sort" "sync" ) // SendMsgImmediately and NoAdditionalInfo are the Go versions of the // book's "SEND_MSG_IMMEDIATELY" and "NO_ADDITIONAL_INFO" - both // defined as 0 in the book's MessageDispatcher.h purely so call // sites read clearly instead of showing a bare 0/NULL. NoAdditionalInfo // is nil rather than 0, since Go's nil is what an empty "any" (our // ExtraInfo field's type) actually looks like. const SendMsgImmediately = 0.0 var NoAdditionalInfo any = nil // messageDispatcher is the Go version of the singleton class // MessageDispatcher. Same singleton pattern as everything else in // this package: unexported type, one shared instance, reached only // through the MessageDispatcher() function below. type messageDispatcher struct { // priorityQueue holds telegrams that haven't gone out yet because // they were sent with a delay, mirroring the book's // "std::set PriorityQ". A std::set keeps its contents // sorted (here, by DispatchTime) and automatically drops // duplicates. Go has no built-in sorted-set container, so we // reproduce both properties by hand: insertIntoPriorityQueue // (below) always keeps this slice sorted by DispatchTime, and // skips inserting anything that looks like a near-duplicate of // something already queued (see telegramsMatch in telegram.go). // index 0 is always the next telegram due to go out. priorityQueue []Telegram } var ( messageDispatcherInstance *messageDispatcher messageDispatcherOnce sync.Once ) // MessageDispatcher is the Go stand-in for // "MessageDispatcher::Instance()". func MessageDispatcher() *messageDispatcher { messageDispatcherOnce.Do(func() { messageDispatcherInstance = &messageDispatcher{} }) return messageDispatcherInstance } // Dispatch is a shorthand for MessageDispatcher(), matching the // book's "#define Dispatch MessageDispatcher::Instance()" macro - // same trick as EntityMgr earlier: Go has no preprocessor, so this // is a plain package-level variable instead, set up once when the // package loads. Dispatch.DispatchMessage(...) reads the same as the // book's Dispatch->DispatchMessage(...). var Dispatch = MessageDispatcher() // discharge is the Go version of the private C++ method Discharge. // It's what actually hands a telegram to its receiving entity - both // DispatchMessage (for immediate messages) and // DispatchDelayedMessages (once a delayed one comes due) call this // to do the real delivery. The book never pasted this method's body // directly, but we now have everything it needs: Entity.HandleMessage // (added this round), which is exactly "the message handling method // of the receiving entity" the book described back when DispatchMessage // was introduced. // // One thing we added that the book's description didn't mention: a // nil check. GetEntityFromID returns nil for an unregistered ID, and // calling a method on a nil Entity would panic - the book's C++ // equivalent (calling through a null BaseGameEntity*) would crash // the same way, so this guard is a real improvement, not just a // style choice. func (d *messageDispatcher) discharge(receiver Entity, msg Telegram) { if receiver == nil { return } receiver.HandleMessage(msg) } // insertIntoPriorityQueue is our Go stand-in for // "PriorityQ.insert(telegram)". A C++ std::set does two things on // insert: it keeps everything sorted, and it silently refuses to add // something that's already there (per whatever == means for the // contained type). We reproduce both by hand here: skip the insert // entirely if an equivalent telegram (telegramsMatch, see // telegram.go) is already queued, otherwise insert it at the correct // sorted position by DispatchTime. func (d *messageDispatcher) insertIntoPriorityQueue(t Telegram) { for _, existing := range d.priorityQueue { if telegramsMatch(existing, t) { return } } i := sort.Search(len(d.priorityQueue), func(i int) bool { return d.priorityQueue[i].DispatchTime >= t.DispatchTime }) d.priorityQueue = append(d.priorityQueue, Telegram{}) copy(d.priorityQueue[i+1:], d.priorityQueue[i:]) d.priorityQueue[i] = t } // DispatchMessage sends a message to another entity, either right // now (delay <= 0) or at some point in the future (delay > 0, added // to the current time to get the telegram's DispatchTime). func (d *messageDispatcher) DispatchMessage(delay float64, sender int, receiver int, msg int, extraInfo any) { // get a pointer to the receiver of the message pReceiver := EntityMgr.GetEntityFromID(receiver) // create the telegram telegram := Telegram{ Sender: sender, Receiver: receiver, Msg: msg, DispatchTime: 0, ExtraInfo: extraInfo, } // if there is no delay, route the telegram immediately if delay <= 0.0 { fmt.Printf("\nInstant telegram dispatched at time: %.5f by %s for %s. Msg is %s", Clock.GetCurrentTime(), GetNameOfEntity(sender), GetNameOfEntity(receiver), GetNameOfMessage(MessageType(msg))) // send the telegram to the recipient d.discharge(pReceiver, telegram) } else { // calculate the time when the telegram should be dispatched currentTime := Clock.GetCurrentTime() telegram.DispatchTime = currentTime + delay // and put it in the queue d.insertIntoPriorityQueue(telegram) fmt.Printf("\nDelayed telegram from %s recorded at time %.5f for %s. Msg is %s", GetNameOfEntity(sender), currentTime, GetNameOfEntity(receiver), GetNameOfMessage(MessageType(msg))) } } // DispatchDelayedMessages is meant to be called once per pass through // the main game loop: it checks the front of priorityQueue and // discharges any telegram whose DispatchTime has arrived. // // One difference from the book's version worth flagging: the C++ // code checks PriorityQ.begin()->DispatchTime without first checking // whether PriorityQ is empty - calling begin() on an empty std::set // and dereferencing it is undefined behavior in C++. Indexing an // empty Go slice would panic the same way, so we added the // len(d.priorityQueue) > 0 check ourselves to actually be safe, // rather than translate that bug faithfully. func (d *messageDispatcher) DispatchDelayedMessages() { // first get current time currentTime := Clock.GetCurrentTime() // now peek at the queue to see if any telegrams need dispatching. // remove all telegrams from the front of the queue that have gone // past their sell-by date for len(d.priorityQueue) > 0 && d.priorityQueue[0].DispatchTime < currentTime && d.priorityQueue[0].DispatchTime > 0 { // read the telegram from the front of the queue telegram := d.priorityQueue[0] fmt.Printf("\nQueued telegram ready for dispatch: Sent to %s. Msg is %s", GetNameOfEntity(telegram.Receiver), GetNameOfMessage(MessageType(telegram.Msg))) // find the recipient pReceiver := EntityMgr.GetEntityFromID(telegram.Receiver) // send the telegram to the recipient d.discharge(pReceiver, telegram) // and remove it from the queue d.priorityQueue = d.priorityQueue[1:] } }