Table of Contents

Struct AbilityObjectHitBroadcast

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

Tells observers which body an ability object hit, so they can draw the impact the server resolved instead of guessing at one.

public struct AbilityObjectHitBroadcast : IBroadcast
Implements
IBroadcast
Inherited Members

Remarks

Why an observer is told rather than left to work it out. An ability object's trajectory is deterministic and every peer agrees on it; its HIT SET is not. The server resolves hits inside a rewind to the CASTER'S view, and a third-party observer holds every character interpolated against its own latency — so an observer running the same query answered a question nobody asked. It could miss a hit the server landed (corrected, by AbilityObjectDestroyedBroadcast) and it could resolve one the server did not, which nothing corrected: its copy ended early, played its impact effect where nothing had happened, and — with a fork — carried on down a heading the server never took.

What it costs, and why that is less than it looks. An observer's copy is deliberately run SpectatorInterpolationTicks behind the server's, so it stays consistent with the interpolated peers it is drawn against — see ComputeObserverFastForwardTicks. That is 66 ms at the shipped tick rate, and it is a head start this message already holds: for an observer inside roughly 133 ms round trip the authoritative answer arrives BEFORE the local guess would have fired. Only observers past that see the impact late, and by less than their one-way latency rather than by a round trip.

Sent to every observer of the caster, the owner included, exactly like AbilityObjectDestroyedBroadcast. The owner predicts its own hits and normally has this one already, in which case the receiver's per-object hit set makes the message a no-op — but an owner that MISPREDICTED A MISS had no correction at all before this, and its impact effect simply never played.

Reliable, and for the same reason its sibling is: there is no repeat behind it. A lost hit message is an impact nobody outside the server ever sees.

Fields

AbilityID

Ability the object belonged to.

public long AbilityID

Field Value

long

Blocked

True when the victim's raised shield ATE this object rather than being struck by it.

public bool Blocked

Field Value

bool

Remarks

The third outcome of a collision, and until it travelled the receiver could not tell it from the first. A block is decided by the peer that resolves hits (AbilityObject.ResolvesHitsLocally), so the server ran NO OnHit chain for it — it destroyed the object and played its destroy events and nothing else. The message it sent was an ordinary impact, and every observer, the blocker's own client included, ran the ability's entire OnHit chain off the back of it: impact effects for a shot that never landed, AbilityForkHitAction redirecting a copy off a hit the server rejected, and RNG draws the server never made (AbilityObject's parity contract).

It is the block counterpart of Deflected, and the two are exclusive: a deflect gives the projectile BACK on a new heading, a block consumes it. So this carries no heading — the object ended — and rides the ordinary impact shape, because the point and the normal are the shield's and the destroy chain draws the shield impact from them.

CasterObjectID

NetworkObject id of the casting character.

public int CasterObjectID

Field Value

int

ContainerID

Deterministic container id the object lived in (identical on every peer).

public int ContainerID

Field Value

int

Deflected

True when the victim TURNED THIS OBJECT AWAY rather than being struck by it.

public bool Deflected

Field Value

bool

Remarks

A deflect is a rejected hit: the server ran no OnHit events, spent no hit count and dealt no damage, but the projectile changed direction — and a trajectory change is the one thing an observer cannot reproduce on its own, because the buff that caused it is the DEFENDER's and the observer never resolves hits. One bit is all this flag needs to be: it says a deflection happened, and nothing more. The new heading does NOT ride on it — re-deriving the reflection from Normal is not safe, because reflecting twice about the same normal returns the original vector and the caster's own client receives this message after predicting the deflection itself. The absolute heading therefore travels separately in PackedDeflectHeading, whose remarks carry the full argument. See DeflectBuffTemplate.ResolveDeflectedHeading.

It is also what lets a receiver act on a hit whose VICTIM it cannot resolve. A character outside this client's streaming budget is not spawned here, so the hit itself must be dropped — running the OnHit events target-less would fire impact effects for a body that is really there — but the redirect must not be, or the observer's copy flies on down a heading the server never took until the destroy message catches up with it.

DirectImpact

True when an ECA ACTION resolved this impact rather than the object's own sweep.

public bool DirectImpact

Field Value

bool

Remarks

A hitscan shot and an area blast run their own lag-compensated query and execute the OnHit chain directly, so they never touch the object's per-body hit set or its hit count. Their impacts were published by nobody at all until this flag existed, and a third party watching a gunfight saw beams appear and nothing be hit by them.

The flag is what keeps the two kinds apart on the receiver. A swept hit is applied through the hit set, which is what makes the message a free no-op for an owner that predicted it; an action impact is applied straight to the OnHit chain, because an area effect wired to OnTick pulses the same victims repeatedly and the hit set would silence every pulse after the first. These are sent to observers EXCEPT the owner for the same reason — with no hit set to absorb it, the owner would play each impact twice.

Ended

True when this hit ENDED the object on the server, so no separate destroy follows.

public bool Ended

Field Value

bool

Remarks

One collision used to produce two reliable messages describing it: this one, and an AbilityObjectDestroyedBroadcast naming the same caster, ability, container and object. Every shipped ability carries HitCount 1, so the second was in practice always implied by the first — ~15-20 bytes and a second reliable slot per collision-ended object per observer, for one bit of information.

Stated, never inferred. An observer must not spend HitCount itself — its copy is told about the hits the server resolved and never about the ones the server declined, so counting locally ends the copy early. That argument is against INFERRING the end, not against the server saying so inline, which is what this is.

Set only where the end is certain at the moment the message is WRITTEN, which is before the OnHit chain runs (see AbilityObject.ApplyHit: a destroyed object cannot publish, because destruction nulls its ability and caster). A chain that can PIERCE — one carrying an AbilityHitCountAction — may cancel the decrement this hit would otherwise apply, so for those the standalone destroy still travels. See AbilityObject.HitEndsObject.

The standalone AbilityObjectDestroyedBroadcast remains for every end that no hit message precedes: a shield sweeping nearby projectiles (ShieldInterceptAction) destroys with observer notification and publishes no hit at all.

Normal

Surface normal at Point. Not carried on a deflection.

public Vector3 Normal

Field Value

Vector3

Remarks

Travels through AimDirectionCompression — four bytes rather than twelve. It is a unit vector and the only thing a receiver does with it is orient an impact effect; the packer's 0.0055° of yaw and 0.0027° of pitch are three orders of magnitude finer than that needs. The one caller that reasoned FROM a normal rather than drawing with it — the deflection — does not read this field at all: the heading it produced is carried absolute in PackedDeflectHeading, resolved on the server before the message was built. See AbilityObjectObserverBroadcastSerializers.

ObjectID

Object id within the container (identical on every peer).

public int ObjectID

Field Value

int

PackedDeflectHeading

The heading the object left on after a deflection, packed by AimDirectionCompression. Meaningless unless Deflected is set.

public uint PackedDeflectHeading

Field Value

uint

Remarks

An ABSOLUTE heading, not an instruction to mirror. A receiver that recomputed the reflection from Normal would be correct exactly once: reflecting twice about the same normal returns the original vector, so the caster's own client — which predicts its deflections and then receives this message like everybody else — would turn the object straight back at the defender it had just been turned away from. Applying an absolute heading is idempotent, which is what makes the message safe to send to the peer that already worked it out.

Four bytes, on a message sent once per body per object, and it also removes the receiver's need to know anything about WHY the object turned — a future deflection that is not a simple mirror needs no wire change.

Point

World point of impact, measured on the server inside the rewind scope.

public Vector3 Point

Field Value

Vector3

VictimObjectID

NetworkObject id of the character that was hit, or 0 when the hit resolved to scenery.

public int VictimObjectID

Field Value

int

Remarks

Zero is a real outcome rather than a failure: a projectile is free to end on a wall, and the OnHit events still run for it with no target character — which is what an authored impact decal or sound needs.