179 lines
6.9 KiB
Go
179 lines
6.9 KiB
Go
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<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 {
|
|
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:]
|
|
}
|
|
}
|