Files
Programming-Game-AI-by-Exam…/chapter-2-state-driven-agent-design/westworldwithmessaging/message_dispatcher.go
T

159 lines
5.9 KiB
Go

package westworldwithmessaging
import (
"sort"
"sync"
)
// 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<Telegram> 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 {
// 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)
}
}
// 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]
// 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:]
}
}