Eventing #141
Replies: 1 comment
-
[May 2009 continued]
For the TimeSpan things, what you're probably looking for is code that creates an
Ah, attaching a "persisted" sensory message to adjacent rooms with "you hear a waterfall..." or whatnot, and have [ed 2021: Seems more evidence we may want some sensory events to have a reusable system to cascade some details across adjacent rooms with dampening, and maybe a similar reusable system for Effects to temporarily cascade room description details as well.]
Sure, if we want to limit the number of shallow classes, it might make sense to have a (Ideally the
[May 2009]
There's a few things I still don't quite understand about the intents here. At the very least we can dictionary the Event type to a list of subscribers of that specific event type, and thus eliminate most redundant searching. For a moment I thought we might want to special case BasicSensoryEvent to have a dictionary of each of it's subtypes, but I think those are generally the kinds of things that only broadcast to one (or a couple) rooms. IMO the vast majority of our events (CombatDamageEvents / PlayerEnteredRoomEvents / SkillEvents / SpellEvents / SocialEvents / etc) are going to be broadcast to just the current room. Next, would this be tying into the proposed "broadcast a request" paradigm? I think an example of intent there is that a player broadcasts a "RequestMoveRoom" event to go from one room to another, but the door in the way is subscribed and decides to mark the request as denied somehow since the door knows it is closed. Similarly, a "guard mob" could react to a player's attempted movement by marking the request as denied and saying "Nuh uh, you're not gettin' near the boss while I have anything to say about it!" Oh, what do you mean by suggesting we persist events subscriptions? If a mobile has a need of subscribing the event in the first place, they already need to do that whenever they spawn the first time, including when the MUD comes back online? Maybe you mean we should be able to attach a list of reaction scripts/behaviors to a mobile, which should be persisted the same way an item's Behaviors would be persisted. If a specific Behavior (like BlockPlayerMovement with a property that has a room ID, to implement the guard mob above) is attached by an in-game builder to a mobile they are designing, then that base mobile's Behaviors list gains a new Behavior which persists with it; it would be the construction of the Behavior that would subscribe to the necessary events, and the builder wouldn't have to do with such internals; the coder of the Behavior would put the appropriate Event subscription in the constructor, and rig it to a method. For the BlockPlayerMovement example, the coder would subscribe to RequestMoveRoom events, and upon their callback method getting called, would check the Event's intended target room (that the player is trying to go to) against the Behavior's target room (being the room the mobile is blocking entry to), and if they match, deny the request and maybe say something or attack the player, etc, according to other properties of that BlockPlayerMovement Behavior instance.
Yeah, we should discuss Effects and Behaviors themselves in another thread.
[ed 2021: Subscribing to the specific Player didn't really work well versus persistence. It could be made to work but would be awkward reconstruction edge cases that aren't worth it: In the end, the only "listeners" need to be "players" who are allowed to designate a list of "friends" and so a case like this really had a good balance of low complexity to minor perf impact to just broadcast a player login to all listeners of a global event - which would just be the current players. For a couple hundred players to check if that login player is on their friends list, is not worth trying to micro-optimize with risky complexity. It seems like anything we think of that might feel like "broadcast to the world" issues are actually similar - things like admin echoes to the world actually mean broadcasting echos to players via something like Player Manager instead of cascading an event down from the root world |
Beta Was this translation helpful? Give feedback.
-
Here's a collection of edited forums discussions which helped form and refine the modern Eventing system used in WheelMUD, now documented formally here. This thread could serve as a good place to resurrect and continue discussions or ask for Eventing clarifications.
This thread contains restored content from prior forums; see #134 for details.
[May 2009]
In response to plans to remove the now-defunct EventReaction system...
Yes, that seems like a good example of how to take advantage of the strengths of the Eventing system.
Although weak references will be useful for a number of things (like caching a target for a while, like for a "follow" command), the "where appropriate" are the keywords there. Most eventing subscriptions will be from Behavior to the Thing it is immediately attached to. For example, consider an ImmobilizationEffect on a player: It only needs to subscribe to movement events on the player Thing itself. Whether the player tried to move by typing "east" or an "enter portal" command or was the target of a summoning spell, etc... There will be a request passing through the player's OnMovementRequest for the ImmobilizationEffect to consider cancelling. If the player logs out or is otherwise destroyed, the ImmobilizationEffect will likewise be removed with the player (and should be restored when the player logs back in, as it is properly part of the player).
Possibly. Would need more info to think through the "right" safe approach for such a thing, if either side is able to be removed from game without the other doing so as well. Something with MultipleParentsBehavior (like exits) might make for another option.
[May 2010]
I'd been thinking more about eventing as I go through code, trying to get back to a compiling codebase with a less-tangled web of interdependencies. I realize we never really discussed the topic of Cancellable events here, which IMO will be critical to moving towards appropriate code locality. Let's look at the Open command, pretending like it's a non-core extension to the system introduced externally. Here's an example of what I see happening, to minimize alteration of the core code, and thus minimize core-integration complications:
OpenRequest
) is broadcast through the sender's parent's EventHandler, passing the cancellableEventArgs.OpenRequest
.Had nothing set the cancellableEventArgs.Cancel = true then the OpenedEvent would be sent after the 'open' state was set to 'true'. So as a pattern, all CancellableEvents are pre-events and are followed by post-events if and only if the pre-event was not cancelled.
There are of course other possible reactions to an
OpenRequest
too. Probably most likely/notable may be theLocksUnlocksBehavior
; upon receipt of theOpenRequest
, it would check to see if the event pertains to it's parent thing, and if so, might cancel the event should the 'locked' state currently be 'true'. (One may want automatic-unlocking code for convenience in their codebase, where, if the sender of the event has the key on hand, this could happen immediately and influence the choice of whether to cancel the event. In turn, the UnlockRequestEvent would be sent immediately. Should nothing cancel THAT then the UnlockedEvent post-event would be sent. TheOpenRequest
wouldn't become finished until all things it fired and caused, including this interruption event, were resolved, causing the UnlockedEvent to be sent too first.) TheLocksUnlocksBehavior
too becomes more self-contained, such that the locking-related code all goes into the behavior itself and DLL boundaries/reference chain becomes less important.In combination of both scenarios, there is a minor side-effect: depending on whether the unlock occurs first or the mobile goes aggro first may impact the success of the player seeing "You unlock the door with the iron key" before or after the mob goes aggro on them. The solution in this case would be to have
MobileBlocksDoorBehavior
also react toUnlockRequestEvent
too, if the coder desires the mob to prevent that too; it would end up cancelling both events in rapid succession. One might discover other side-effects with other complicated scenarios, but that would happen anyway, and the good news is that the 'fixes' for such problems are generally implemented local to the subject of the problem itself.On event GC: I've read that when using the 'event' keyword that the registrants aren't prevented from GC but I've also read otherwise. I don't have time to research that ATM, but if it is a problem, a simple solution may be to make base Things and Behaviors be IDisposable and cleanup after any destroyed Things/Behaviors subscribed events.
Ok so weak references are only used in eventing if one rolls their own eventing which explicitly uses weak references. So indeed, we should automatically unsubscribe any Thing (or Behavior) from any events they subscribed to at destruction/disposal times.
[June 2010]
Here's an example of where I'm at with making things more behavior driven and event driven. Things which are allowed to move have a
MovableBehavior
.MovableBehavior
has the following Move method:All in all, I bounced between having a single MoveEventArgs and the dual events and settled on the latter which inherit from a MoveEventArgs; if something wants to react to a thing leaving, or a thing arriving, independently, that should be easy to do IMO. (ed. 2021: yup, we do that now with
LeaveEvent
andArriveEvent
which are bothMovementEvent
.)So the cardinal movement commands ('move east' or 'east' etc) look for a
MovableBehavior
on thesender.Entity
for performing the Move itself. I haven't done it yet, but exits will just beThing
s with anExitBehavior
and get targeted by the 'move' command; 'east' would be its ID so the targeting mechanism would find it, would findExitBehavior
, and from that get its linked place (generally being a room). (ed. 2021: actually exits use Contextual Commands now.)The 'enter' command is similar. Find the
sender.Entity
MovableBehavior
first to see if it can move, finds the targetedThing
and itsExitBehavior
, then reuses the same Move method with its own sensory messaging like "You enter a portal" rather than "You leave to the east".As long as we don't try to support the idea of 'uncancelling' an event, and keep a fine enough grain of individual events, I think this can work well. We want to keep this simple; no 'reactor' to cancelled events, no fighting to uncancel a cancelled event, etc...
Perhaps a cancellable request should probably be sent to all registered receivers at time of broadcast, whether the request has had
Cancel=true
or not; it is up to the receivers to decide if they behave differently based on whether it has been cancelled or not. An example.Thing
which has both anExitBehavior
and anOpensClosesBehavior
(IE behaves like a door).AttacksMoverBehavior
with properties which specify the east exit as the only exit it is blocking.leaveEvent
above is being broadcast as a request via theOnMovementRequest(leaveEvent)
due to the player's movement command.AttacksMoverBehavior
receives and interprets the event, setsCancel=true
, and sends its ownStartCombatRequest
event. (After that resolves without cancellation, it will send its ownStartCombatEvent
too.)OpensClosesBehavior
receives and interprets the move event, and setsCancel=true
(it already was, that is ok) becauseIsOpen
isfalse
.So it doesn't matter whether
OpensClosesBehavior
knows if the event has already been cancelled, it just ensures the event does get cancelled while closed.But the
AttacksMoverBehavior
probably cares; if the order of 4 and 5 were reversed such that the movement was cancelled by the door instead of the mob, perhaps theAttacksMoverBehavior
shouldn't cause the combat. (The player tried the door handle, but didn't actually try to move through it?) Given the player intent, chances are the player sees "You can't go through it since it's closed right now" and then open the door and try to go there, THEN get attacked. So which order these behaviors got interpreted still doesn't necessarily particularly matter, even though sometimes the attack happens right away when the player clearly tries to move and sometimes it happens when the player would actually have an open pathway in front of them.Minor detail and IMO both would feel correct to the user here. Maybe there are better breaking examples where order matters more.
As long as the events aren't trying to each represent a lot of different things in one go, this sort of problem shouldn't be possible. (Requests either get cancelled or not, they should never get uncancelled. E.G. something that wants to "keep it open" should Cancel the Request to Close it.)
I think with correct event design, a thing shouldn't need to know what cancelled an event, just that it was cancelled.
Actually I can see cases where one might be tempted to have events modify the non-Cancel parameters of the Request (NOT the Event). However I think those should be the exception rather than the rule, if employed at all, and known to be dangerous when employed; One example would be like soaking some damage from a combat damage event with a
DamageReductionBehavior
or whatnot subscribing to CombatDamageEventArgs-based requests. This heads into dangerous ground though since if anything else modifies the damage, the (unpredictable) order becomes very important for math reasons (subtracts vs percentage multipliers etc); thus IMO such a thing should not occur through the requests/events, if it can be avoided, and such a CombatDamageEventArgs should only be broadcast with the final calculations.(ed 2021: Alternatively, if we want combat damage modifiers to occur in response to combat damage events, the
CombatDamageRequest
could provide a collection that listeners would "add" their modifiers to. Then the combat system would see theCombatDamageRequest
wasn't cancelled, but does have a list of multipliers and additions/subtraction effects to order as appropriate for the game system before applying them to the final result to be sent with the finalCombatDamageEvent
.)Relying on the order of processing would be error-prone. For example, take a mob and item which register to the same event. Let's say the mob is spawned, and soon after the item is spawned (and each registers upon creation of course). So the event is processed by the mob and then by the item. Then a player kills that mob (causing the mob to dispose, unregistering that mob instance from the event) and later a new version of the mob spawns, registering to the event. So now our order of processing is item then mob.
Order of registration is something we shouldn't have to manage carefully. Things ought to just work in a reasonable way, in either order. In most reasonable cases, I think this will come naturally.
You've lost me. Where are the dead references? What do you mean by a "replaced" item? What do you mean by re-entrant threading issues?
There should not be multiple places implementing the same code. For instance, if a
Thing
can be killed, it should have aMortalBehavior
orKillableBehavior
orHasHealthBehavior
or whatnot, which in turn has a method calledKill
; this method raises the kill Request (which can be cancelled, IE if the thing also has a temporaryImmortalityEffect
going on or whatnot), followed by the kill Event if it's not cancelled, after it applies the property changes which imply the base thing is dead. There should not be events such as "KillItNowWithFireEvent" or "CrazyExGirlfriendEvent" but if there is some effect or spell or whatnot which intends to kill the victim outright, the that code would check the target for MortalBehavior and call its Kill method.To expand on that a little, having only one place where Kill is actually implemented is a good thing for reasons you raise; it is up to the Kill method to make sure it doesn't "do bad things" if the Thing is already dead, for instance. Nice, isolated, testable little package of code. :)
Sort of. Sneak shouldn't "cancel the cancel" directly. Yes, I agree there can essentially be a stack of events which inherently resolve in a deterministic order; we need no special priority logic nor extra information in the events themselves. Simple events, here's how it could work:
ProcessCommandsThread
: Command processor processes the 'e' command for the player. It finds that there is a registered context command for 'e', being the ExitBehavior of an exit Thing in the room. Calls:ExitBehavior.ProcessCommand
: Raises the MoveEventArgs-based request. Among the event (request) handlers is:BlocksMoverBehavior.MoveEventHandler
:(on a mobile in the room) Sees that the request is indeed for a Thing in the same room as it, so it raises a new BlockMoverArgs-based request. Among the event (request) handlers is:
SneakingBehavior.BlocksMoverHandler
: (on the player who is in sneak mode) Sees that the BlocksMoverArgs-based request is indeed targeting the behavior's parent, so some sneak-detection logic is run here: Maybe the BlocksMover request's sender does indeed have a SensesBehavior so a sneak-detection-chance is rolled against the blocker's relevant senses or whatnot; the result indicates that the blocker did not perceive the sneaker, so the BlocksMoverArgs-based request is cancelled.Note that each piece is self-contained; since we resisted the 'quick fix' of adding sneaking logic into the BlocksMoverBehavior and instead ensured that it is also using proper eventing to indicate what it is trying to do, we've successfully added sneaking behavior exactly where it belong: only in the SneakingBehavior. As an added benefit, other things could now listen to the BlocksMover requests/events should they want/need to going forward.
With the above, we could also implement sneak as a command rather than (or in addition to) a toggle, IE 'sneak east' would add the
SneakingBehavior
, attempt to process the 'east' command, and then remove theSneakingBehavior
.[May 2009]
Here are some snippets from related direct conversations that were migrated to forums for future reference:
The sensory message is broadcast to the room, then each entity in the room either sees it or not.
Yes, sender's entity's location == room ... so
room.EventBroadcaster.Broadcast
Yes, broadcaster is clearly better.
GameEventBase by default has an associated sensory message, but... actually, lets make a YellEvent. This way, if we want to expand things to say, react to yelling, it is simple. Maybe a mobile has a reaction to yell events but not other sensory messages.
That's a cool idea. Would we want that to still be an 'event' at that point?
Or maybe another object that contains the event/sensory info? Don't know yet.
There is a world broadcaster too, probably an area broadcaster.
PlayerManager.world.EventBroadcaster.Broadcast
.Yes. Actually every
Thing
is now going to have an EventBroadcaster which relays to its SubThings and Behaviors....ok i just saw area broadcaster, that's there. so an area reset event or area-wide events or whatnot could send to the whole area.
Yea, the world broadcaster would be good for that, or a flag to yell the current area.
Yes, because it is more prepared for future OO uses.
Let's say we want a mob to attack someone who yells from their room.
They just check if (receivedEvent is YellEvent) attack player
etc... so ya, even if it gains no additional functionality i think it can make sense.
Well, that's what the SensoryMessage part is to abstract away the type of sense needed from the event?
So if we have a SocialEvent is a good example, the SensoryMessage part of it is highly sense-dependent, but the event is not.
I dont quite follow; CombatSoundEvent?
I'd say err on the side of too many shallow events, since they're more OO.
(By shallow here I mean classes that do little more than inherit.)
Ya, true, but at least they're likely to be seen by anyone making sweeping changes.
Ya but if we had SensoryEvent that was 'praising the mobile' why would we have him attack the player?
So if we added string parsing, the coder is still just as likely to scan for 'yell' formatting and forget 'say' formatting.
Plus things can then be spoofed; what if a clever player formats an emote or say that fools the AI into attacking a differnet player?
String parsing would be down-right ugly.
Are you thinking that maybe some Events should not have SensoryMessage part?
The base event is little more than a sensory component, plus some context.
I can see maybe a CommunicationEvent that yell and say would all either use or inherit from. Build a nice tree. Then the reactions could be to all communication events or all environment events or all combat events etc?
Im just thinking 'sensoryevent' is either fairly redundant with 'baseevent', else it is not explicit when to use it or when not to use it.
Ya that would sit well with me.
[ed 2021: I'm not sure how many events won't have any sensory component - perhaps if something like "game ticks" wanted to be a broadcast event... ATM all of our events, even the base GameEvent, has SensoryMessage as a constructor arg... Filed #142 to investigate.]
Beta Was this translation helpful? Give feedback.
All reactions