Direction: Both (C→S "I'm attacking this target"; S→C "the attack resolved")
Numeric ID: 18
Server handler: ServerNet.bb:1548
Server attack engine: GameServer.bb:300 (ActorAttack)
Client handler: ClientNet.bb:1109
The combat packet. Carries melee + ranged-projectile attack initiation from client to server, and the resolved attack (hit / miss / observer-visible swing) from server to client. The server runs the entire damage formula, faction check, weapon-range check, and HP mutation; the client only animates and updates its HUD.
A fixed 2-byte payload, no sub-code. The client tags the target by RuntimeID; the server resolves to ActorInstance via RuntimeIDList.
| Offset | Width | Type | Field | Notes |
|---|---|---|---|---|
| 1 | 2 | Int | Target RuntimeID |
The actor the client wants to hit. Resolved server-side via RuntimeIDList. |
Total: 2 bytes. Validated by Len(M\MessageData$) = 2 (ServerNet.bb:1550) — packets of any other length are silently dropped.
| Sub-code | Recipient | Layout | Purpose |
|---|---|---|---|
"H" |
Attacker (A1\RNID) |
1B sub + 2B Victim\RuntimeID + 2B Damage+1 + 1B DamageType |
"I hit them" — for the attacker's HUD (damage numbers, attack animation). |
"Y" |
Victim (A2\RNID) |
1B sub + 2B Attacker\RuntimeID + 2B Damage+1 + 1B DamageType |
"They hit me" — for the victim's HUD (incoming damage, parry/hit animation). |
"O" |
All other players in the same area | 1B sub + 2B Attacker\RuntimeID + 2B Victim\RuntimeID (no damage payload) |
Observer swing animation. Observers re-sync HP via the next P_StatUpdate, so the damage isn't replicated on this channel. Subtle: the ClientNet "Else" branch doesn't decode a fresh Damage from the wire — in non-Strict UpdateNetwork(), Damage is an implicit function-scope variable that persists across Select Case iterations within one call, so it reads whatever the prior H/Y packet (or zero, on the first call) left there. Current behaviour is benign because P_StatUpdate re-syncs HP authoritatively. |
Damage+1 encoding: The 2-byte Damage field carries Damage + 1 so a value of 0 on the wire means "miss" (rendered as a parry animation). The wire field can be negative (signed 2-byte = -32768..32767), which lets the server signal a miss explicitly. Client subtracts 1 to recover the actual damage at ClientNet.bb:1117 / :1145.
Damage type bounds: The 1-byte DamageType is clamped client-side to [0, 19] before indexing into DamageTypes$ (ClientNet.bb:1121-1123). A malformed packet with a wild value falls back to type 0 instead of crashing the client.
C → S handler (ServerNet.bb:1548-1571)
Six gates, all required to fire:
- Sender validity:
AI <> Null(FindActorInstanceFromRNID resolves the sender). - Packet shape:
Len(M\MessageData$) = 2. - Combat delay:
MilliSecs() - AI\LastAttack >= CombatDelay. Prevents attack-spam cheating;AI\LastAttackis set on every successful attack inActorAttack. - Not riding a mount:
AI\Mount = Null. Mounted players can't attack (intentional gameplay constraint). - Same-area gate (added PR #276): both attacker and victim must be in the same
AreaInstance. Resolved viaObject.AreaInstance(AI\ServerArea)andObject.AreaInstance(A2\ServerArea); the dual lookup guards both sides against staleServerAreamid-portal. - PvP / NPC permission:
A2\RNID < 0 Or AInstance\Area\PvP = True. NPCs (RNID -1) are always attackable; players are only attackable in PvP-enabled areas.
ActorAttack damage engine (GameServer.bb:300-600+)
The damage engine itself runs additional checks:
- Already-dead target:
If A2\Attributes\Value[HealthStat] <= 0 Then Return False. Without this, two attackers landing in the same tick both saw HP > 0, both subtracted, both calledKillActoragainst freed memory (double-XP + use-after-free). - Both Aggressiveness ≠ 3: NPCs with
Aggressiveness = 3are non-combatant (typed mobs / vendors). - Faction rating:
A1\FactionRatings[A2\HomeFaction] > 150blocks the attack (friendly faction). - Range check: melee uses
7.0 + A1\Actor\Radius + A2\Actor\Radiussquared. Ranged projectile usesweapon.Range + A1.Radius + A2.Radiussquared.
CombatFormula |
Hit chance | Damage formula |
|---|---|---|
1 (Normal) |
90% | weapon.Damage ± strength-rolled bonus, critical 1/10 (×2). Armour subtracts GetArmourLevel + Resistances[DamageType] - 100 + ToughnessStat / 8. |
2 (No strength bonus) |
90% | weapon.Damage flat. Critical 1/10 (×2). Same armour formula. |
3 (Multiplied) |
90% | weapon.Damage × Strength. Critical 1/10. |
4 (Attack script) |
N/A | Delegates to a ThreadScript("Attack", "Main", attacker, victim) — content authors implement the formula in .rsl. No range/damage check server-side. |
CombatFormula is a global set at server boot from project config. The attack-script variant (4) is the modder hook for completely custom combat.
After damage is applied, the server emits three packets:
- To attacker (if
A1\RNID > 0):"H" + victim_rid + damage + dtype - To victim (if
A2\RNID > 0):"Y" + attacker_rid + damage + dtype - To all others in the same
AInstance\FirstInZonechain:"O" + attacker_rid + victim_rid(no damage)
The "O" loop (GameServer.bb:575-584) skips A1 and A2 (they already got their personalised packet) and skips Null-AreaInstance (stale-area mid-portal).
P_AttackActor is one of the most security-sensitive packets — a single attack hit/miss decision drives PvP outcomes. The validation requirements above cover the known attack surface; the recent same-area gate (#276) was the most-recently-added defence (specifically against cross-area packet injection that would have bypassed PvP rules).
The handler is NOT privilege-gated like the BVM clicker handlers — combat is the player's privilege; the gate is "are you allowed to fight this target?" not "are you allowed to call this function?".
| PR | Fixed |
|---|---|
| #276 | Same-area gate (cross-area injection prevention) |
| Two-attackers-same-tick fix (pre-PR) | The already-dead target guard at GameServer.bb:308 — prevents double KillActor + use-after-free |
| Defensive AInstance Null check | The broadcast loop at GameServer.bb:575 skips when AInstance is Null (mid-warp race) |
| #282 | FindActorInstanceFromRNID(M\FromID) -- O(1) sender resolution |
| #283 | Per-area FirstInZone chain walk in the observer broadcast loop |
| #287 | Pet-aggro broadcast (ActorAttack's pet recruitment) now walks the per-leader FirstSlave chain |
P_StandardUpdate— movement, including the speed-hack clamp that prevents teleport-into-range attacksP_StatUpdate— broadcasts HP changes; observers see victim's HP drop via this rather than the "O" P_AttackActorP_ActorDead— broadcast when victim's HP drops ≤ 0P_Projectile— projectile-launch broadcast (separate from this packet; ranged attacks emit both)P_ActorEffect— debuff / status effects that combat triggers
../encoding.md—RCE_StrFromInt$byte widths../handler-conventions.md— soft-fail discipline, bounds-check, same-area gate pattern../../modules/servernet.md— P_AttackActor's place in the dispatchGameServer.bb'sActorAttackfunction — the full damage engine