Skip to content

docs(protocol): P_AttackActor detail page (combat math + damage broadcast)#288

Merged
CoreyRDean merged 2 commits into
developfrom
docs/p-attackactor-detail-page
May 26, 2026
Merged

docs(protocol): P_AttackActor detail page (combat math + damage broadcast)#288
CoreyRDean merged 2 commits into
developfrom
docs/p-attackactor-detail-page

Conversation

@CoreyRDean
Copy link
Copy Markdown
Collaborator

Summary

Third per-packet detail page after #285 (P_InventoryUpdate) and #286 (P_StandardUpdate). Documents the combat packet — one of the most security-sensitive surfaces in the protocol since every PvP outcome flows through its validation chain.

Non-technical

Combat is the highest-stakes wire surface for cheating prevention. This doc captures every gate the server applies between "client says I'm hitting X" and "X loses HP", plus the four formula variants and the three-channel broadcast pattern (attacker / victim / observers).

What's documented

Acceptance criteria

  • Both directions documented with per-byte field layouts
  • All 3 S→C sub-codes specified
  • All 6 C→S validation gates linked to source lines
  • ActorAttack engine gates documented
  • Damage formula variants enumerated
  • Catalog index regenerated; Detail column links to new page
  • No .bb source changes
  • compile.bat -t clean
  • test.bat — 26/26 pass

Test plan

  • gen_packet_index.sh --check idempotent
  • Spot-check field layouts against ServerNet.bb:1548, ClientNet.bb:1117/1145/1179, GameServer.bb:271-276/563-583
  • compile.bat -t + test.bat — pass

Trade-offs / deferred

  • 52 packet detail pages remain. Next recommended: P_SpellUpdate, P_ChatMessage, P_VerifyAccount, P_StatUpdate (P_AttackActor's observer re-sync channel).
  • The doc surfaces a subtle implementation detail: the ClientNet "Else" branch (observer "O" case) reads Damage as 0 because the wire payload doesn't carry damage for observers — it's an uninitialized local. Documented but not changed (current behavior is correct: animations play, HP re-syncs via P_StatUpdate).
  • Other unrelated deferred items unchanged.

🤖 Generated with Claude Code

…cast)

Third per-packet detail page (after P_InventoryUpdate #285 and
P_StandardUpdate #286). Documents the combat packet -- one of the
most security-sensitive surfaces because every PvP outcome flows
through its validation chain.

## What the doc covers

* **C->S field layout**: 2-byte payload (target RuntimeID), no
  sub-code. The packet shape is the second-shortest in the protocol
  (P_ActorGone is the shortest).
* **S->C three sub-codes** with per-recipient field layouts:
  - `"H"` to attacker -- `1B sub + 2B Victim\RuntimeID + 2B Damage+1 + 1B DamageType`
  - `"Y"` to victim -- `1B sub + 2B Attacker\RuntimeID + 2B Damage+1 + 1B DamageType`
  - `"O"` to observers -- `1B sub + 2B Attacker\RuntimeID + 2B Victim\RuntimeID` (NO damage payload)
* **The Damage+1 encoding trick**: 0 on wire means "miss" (parry
  animation); client subtracts 1 to recover the actual damage. Lets
  the engine use a signed 2-byte field to signal "miss" explicitly
  without burning a separate flag byte.
* **DamageType client-side bound** (0..19): protects against wild
  values out of the malformed-packet space.
* **Six C->S validation gates**: sender validity, packet length,
  CombatDelay anti-spam, mount-blocks-attack, same-area (PR #276),
  PvP-or-NPC permission.
* **ActorAttack damage engine** with the additional engine-level
  checks (already-dead target guard, Aggressiveness=3 block, faction
  rating, range check) and the four CombatFormula variants (1-3
  fixed formulas + #4 attack-script delegation).
* **Three-channel broadcast pattern**: attacker / victim / observers.
  Observer "O" broadcast does NOT carry damage (re-sync via
  P_StatUpdate) -- a design choice worth documenting because the
  current ClientNet "Else" branch reads damage as 0 (uninitialised
  local) which makes the no-damage decision implicit rather than
  explicit.

## Historical PRs surfaced

* #276 -- same-area gate
* #282 -- O(1) sender resolution
* #283 -- per-area FirstInZone chain in observer broadcast
* #287 -- per-leader FirstSlave chain in pet-aggro recruitment
* Two-attackers-same-tick fix (pre-PR-numbered) -- the already-dead
  guard
* AInstance Null defensive check in the broadcast loop

## Verification

* `compile.bat -t` clean.
* `test.bat` -- 26/26 pass.
* `gen_packet_index.sh` regenerated; Detail column links the new page.
* Spot-check 3 field layouts against the actual source: H/Y damage
  emission at GameServer.bb:563-568 + 271-276, observer "O"
  emission at GameServer.bb:573 + 328, client receive at
  ClientNet.bb:1117 / 1145.

## Deferred

52 packet detail pages remain. Next recommended candidates:
* P_SpellUpdate -- multi-sub-code spell lifecycle (cast / charge / free)
* P_ChatMessage -- multi-prefix dispatch (Chr$(252/253/254) channels)
* P_VerifyAccount -- the auth state machine (PRs #264-#268)
* P_StatUpdate -- HP/stat broadcast (P_AttackActor's observer
  re-sync channel)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@CoreyRDean CoreyRDean requested a review from a team as a code owner May 26, 2026 16:51
…cit-var framing

PR #288 reviewer optional suggestions, addressed before merge:

1. The C->S handler section's lead said "Five gates" but the
   numbered list had six items (the lead was written before
   adding the same-area gate from PR #276). Bumped to "Six".

2. The S->C "O" sub-code description claimed observers "read
   Damage as 0 (uninitialised local)". More precise: `Damage` is
   an *implicit function-scope* variable in non-Strict
   `UpdateNetwork()`. It zero-initialises on first entry, but
   persists across `Select Case` iterations within one call -- so
   the second P_AttackActor "O" in the same UpdateNetwork tick
   reads the PRIOR H/Y packet's Damage, not 0. Benign because
   P_StatUpdate re-syncs HP authoritatively, but the precise
   framing better captures the non-Strict dispatcher property.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23aa0e71af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

| `"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. |

**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](../../../src/Modules/ClientNet.bb#L1117) / [:1145](../../../src/Modules/ClientNet.bb#L1145).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Describe Damage+1 wire field as unsigned

This sentence documents the 2-byte damage payload as signed and says negatives are used to signal misses, but the actual codec path writes Damage + 1 with RCE_StrFromInt(..., 2) and reads it with RCE_IntFromStr after zeroing upper bytes, so the on-wire value is effectively 0..65535 and miss is encoded as 0 (client then subtracts 1). Keeping the signed-short claim here can mislead alternate client/server implementations into decoding values >= 32768 incorrectly and diverging from server combat outcomes.

Useful? React with 👍 / 👎.

| `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. |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct CombatFormula 4 range-check behavior

The table states that formula 4 has no server-side range check, but that is only true for the ranged-weapon scripted branch; melee scripted attacks still pass through the shared distance gate before the CombatFormula = 4 dispatch. This overgeneralization weakens the protocol doc’s accuracy for anti-cheat analysis by implying melee script combat bypasses a guard that is actually enforced.

Useful? React with 👍 / 👎.

@CoreyRDean CoreyRDean merged commit 0f0dab4 into develop May 26, 2026
1 check passed
@CoreyRDean CoreyRDean deleted the docs/p-attackactor-detail-page branch May 26, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant