Skip to content

Implement vanilla shield blocking#23

Open
HashimTheArab wants to merge 16 commits into
masterfrom
shields-vanilla-df-master
Open

Implement vanilla shield blocking#23
HashimTheArab wants to merge 16 commits into
masterfrom
shields-vanilla-df-master

Conversation

@HashimTheArab

@HashimTheArab HashimTheArab commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add the vanilla shield item with offhand, durability, repair, registration, creative/recipe availability through existing registries.
  • Track shield blocking from Bedrock sneaking/item-use input with the 5 tick warm-up, front-facing block checks, shield durability loss, shield block sound, melee recoil, axe shield-disable cooldown, and held/offhand state refreshes.
  • Add projectile deflection plus explosion/TNT origin and shield-blockability handling

Verification

  • go test ./...
  • make lint
  • git diff --check
  • GitHub Actions Build Update Discord link df-mc/dragonfly#138 passed for ac603a83d68be339d4cb8842260ca41d42470c2d
  • Confirmed no go.mod, go.sum, or gophertunnel paths changed

Sources


Note

Medium Risk
Touches core combat/damage flow (Player.Hurt/Explode) plus projectile collision behaviour and TNT/explosion attribution, so regressions could affect PvE/PvP balance and knockback/damage edge cases. Changes are well-covered with extensive new tests across player, entity, block, session, and world layers.

Overview
Adds a new minecraft:shield item (off-hand compatible, durability/repairable, registered) and implements vanilla-style shield blocking on Player, including a warm-up delay, front-facing source checks, durability loss, shield-disable cooldown on axe hits, recoil knockback, and shield block sound/metadata.

Updates the damage and input pipelines so shield blocking is driven by Bedrock auth-input sneaking/using-item signals, stays in sync when held/offhand slots change (including inventory transactions and stack requests), and can block projectile/explosion damage even in some immunity/zero-damage cases.

Extends explosions/TNT to carry an optional Source and UnblockableByShield flag, primes TNT with source+blockability via optional EntityRegistryConfig.TNTWithSource (with fallback), persists shield-unblockable TNT via NBT, and updates projectiles to mark shield blocks and deflect (skipping hit effects/callbacks) when blocked.

Reviewed by Cursor Bugbot for commit ac603a8. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Added Shield item with durability, off-hand use and repair rules.
    • Players can block melee, projectiles, and eligible explosions when facing the source; successful blocks apply reduced damage, attacker knockback, and shield durability loss.
    • Projectiles now deflect when blocked and are marked as blocked.
    • Explosions and TNT can carry a source and be flagged as shield-blockable or unblockable, affecting block behavior.
    • Shield block plays a dedicated sound and ties into held-item/use state.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds shield blocking end-to-end: new Shield item and sound, expanded explosion/damage schemas with source/blockability, TNT source propagation and blockability, projectile deflection and marking, player shield mechanics and state integration, session input/metadata wiring, inventory held-item updates, registry fallback, and many tests.

Changes

Shield Blocking Mechanics

Layer / File(s) Summary
Data Contracts & Types
server/block/explosion.go, server/entity/damage.go, server/item/shield.go, server/world/sound/item.go
ExplosionConfig adds UnblockableByShield and Source; ExplosionDamageSource stores origin, HasOrigin, BlockableByShield and Source; new Shield item and ShieldBlock sound type.
Entity Registry & TNT
server/world/entity.go, server/entity/tnt.go, server/entity/register.go
EntityRegistryConfig gains TNTWithSource with fallback; NewTNTWithSource and behaviour now carry optional source handle and shield-blockability; NBT encode/decode updated.
Block TNT Wiring
server/block/tnt.go
Block-level TNT logic now derives ignition/source handles (projectile owner, activator, ExplosionConfig.Source) and passes blockable flag into spawn helper calling TNTWithSource.
Projectile Behaviour
server/entity/projectile.go, server/entity/ent.go, server/entity/projectile_test.go
Projectiles track shield-blocked hits, deflect on shield block, suppress normal hit effects; Ent exposes ProjectileOwner() and MarkShieldBlocked(); tests validate deflection and suppression.
Player Shield Mechanics
server/player/shield.go, server/player/shield_test.go
Implements startup delay, cooldowns, facing checks, durability damage, knockback, axe-disable, item-use initiation, and public APIs ShieldBlocking()/StartShieldBlockingInput(). Extensive tests added.
Player State & Damage Pipeline
server/player/player.go
Player state tracks shield-block timing/input; Hurt delegates to internal hurt with pre/post shield attempts; Explode uses ExplosionDamageSource.BlockableByShield and applies reduced knockback when blocked; held-item and cooldown logic integrates shield state.
Session Input & Metadata
server/session/handler_player_auth_input.go, server/session/entity_metadata.go, server/session/world.go, server/session/entity_metadata_test.go
Input parsing derives shield-blocking from sneak/use flags and conditionally applies it; addSpecificMetadata sets shield-blocking metadata flag; playSound maps ShieldBlock to client packet; tests validate metadata.
Inventory / Held-item Wiring
server/session/handler_inventory_transaction.go, server/session/handler_item_stack_request.go, tests
Inventory transaction and item-stack handlers now trigger UpdateHeldItemState for held/offhand slot changes and rollback paths; transaction handler now accepts active *world.Tx; tests validate updates.
Registration
server/item/register.go, server/entity/register.go
Registers Shield{} and wires TNTWithSource: NewTNTWithSource in default registry.
Tests & Validation
many test files
Adds comprehensive tests across TNT, projectiles, player shield behaviour, item registration, metadata, inventory/session handlers, and NBT persistence.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🥕 A rabbit with a polished shield so bright,
Deflects a snowball and hears the block's light,
TNT keeps its source and explosions learn,
I hop through tests while tiny green leaves turn,
Durability ticks and cooldowns wait tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'Implement vanilla shield blocking' is concise and clearly summarizes the primary feature addition across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch shields-vanilla-df-master

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@HashimTheArab HashimTheArab force-pushed the shields-vanilla-df-master branch 5 times, most recently from 0e8dae4 to 4b9d95e Compare May 9, 2026 20:25
@HashimTheArab HashimTheArab force-pushed the shields-vanilla-df-master branch from 4b9d95e to e6e1954 Compare May 9, 2026 20:38
@HashimTheArab HashimTheArab marked this pull request as ready for review May 9, 2026 23:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
server/entity/projectile_test.go (2)

38-43: ⚡ Quick win

Use the new Ent.MarkShieldBlocked() helper for clarity.

Production code is expected to call shield blocking via the Ent.MarkShieldBlocked() helper added in server/entity/ent.go. The test currently bypasses that helper with a structural type-assertion chain. Switching to the helper makes the test mirror production usage and one less place to update if Behaviour() plumbing changes.

♻️ Suggested simplification
 func (t *projectileShieldTarget) Hurt(_ float64, src world.DamageSource) (float64, bool) {
 	if s, ok := src.(ProjectileDamageSource); ok && t.blocked {
-		s.Projectile.(*Ent).Behaviour().(interface{ MarkShieldBlocked() }).MarkShieldBlocked()
+		s.Projectile.(*Ent).MarkShieldBlocked()
 	}
 	return 0, t.vulnerable
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/entity/projectile_test.go` around lines 38 - 43, The test's Hurt
method uses a structural type assertion to call the shield-blocking behavior;
replace that chain with the new helper by calling Ent.MarkShieldBlocked() on the
projectile instance: inside projectileShieldTarget.Hurt, after confirming src is
a ProjectileDamageSource and t.blocked, cast s.Projectile to *Ent and invoke its
MarkShieldBlocked() method (instead of
s.Projectile.(*Ent).Behaviour().(interface{ MarkShieldBlocked()
}).MarkShieldBlocked()), so the test mirrors production usage and centralizes
the behavior.

83-88: 💤 Low value

Manually-built Ent skips Open wiring.

newProjectileShieldTestEnt constructs an Ent directly rather than going through Open(tx, handle, data). This works for the two tests that only invoke behaviour.hitEntity (which never touches e.tx), but it would silently break if those tests evolve to call any method that dereferences e.tx (e.g., Close, SetNameTag, SetOnFire with a state change). Worth a comment to flag that this builder intentionally returns a "tx-less" Ent for direct unit testing of hitEntity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/entity/projectile_test.go` around lines 83 - 88,
newProjectileShieldTestEnt constructs an Ent directly without calling Open(tx,
handle, data), so the returned Ent has no transaction (e.tx nil) which is fine
for tests that only call behaviour.hitEntity but will silently break if tests
later call methods that dereference e.tx (e.g., Close, SetNameTag, SetOnFire).
Update newProjectileShieldTestEnt (or add an inline comment above it) to
explicitly document that it intentionally returns a "tx-less" Ent for direct
unit testing of behaviour.hitEntity, or alternatively change the helper to call
Ent.Open(tx, handle, data) so the built Ent has a proper tx; reference the
newProjectileShieldTestEnt function, Ent type, Open(tx,...), and e.tx when
making the change.
server/entity/damage.go (1)

49-58: 💤 Low value

Optional: consider *mgl64.Vec3 over the Origin/HasOrigin pair.

The HasOrigin flag exists only to disambiguate "no origin set" from "origin at world zero". A *mgl64.Vec3 would encode the same nullability without an extra field, but since the rest of the entity API consistently passes mgl64.Vec3 by value, the current pattern is also reasonable. Feel free to keep as-is.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/entity/damage.go` around lines 49 - 58, The Origin/HasOrigin pair in
ExplosionDamageSource should be replaced with a nullable pointer to avoid the
extra boolean: change the struct field(s) so Origin is of type *mgl64.Vec3
(remove HasOrigin), update all call sites and consumers of ExplosionDamageSource
(constructors, uses in functions/methods, and any comparisons) to treat Origin
== nil as “no origin” and otherwise dereference the pointer for the Vec3 value,
and ensure any code that previously read HasOrigin now checks for Origin != nil;
reference ExplosionDamageSource, Origin and HasOrigin to locate and update the
affected code paths.
server/entity/projectile.go (2)

286-289: ⚖️ Poor tradeoff

Fragile return-channel via lt.shieldBlocked.

This pattern resets lt.shieldBlocked to false, calls l.Hurt(...), and then inspects lt.shieldBlocked to detect a deflection, relying on Hurt's callees synchronously calling MarkShieldBlocked() on this very ProjectileBehaviour. Issues:

  • Couples ProjectileBehaviour mutable state to a transient per-call signal, which is hard to reason about and easy to break if Hurt ever becomes asynchronous, calls the projectile multiple times, or re-enters.
  • MarkShieldBlocked becomes part of the public API only as a side-channel.
  • Concurrent ticks on different threads with shared behaviour pointers (unlikely today, but not guaranteed forever) would race.

A more robust approach is to surface the shield-block decision through the damage source / Hurt return, e.g., a Shielded boolean as part of the result, or a dedicated source field that the player code sets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/entity/projectile.go` around lines 286 - 289, The code is using
lt.shieldBlocked as a fragile side-channel set by l.Hurt; change the API so
l.Hurt (or the damage result it returns) includes an explicit shielded/blocked
boolean indicating whether the hit was shield-blocked, then stop using
lt.shieldBlocked and MarkShieldBlocked as a cross-call mutable flag. Update the
call site in ProjectileBehaviour (where lt.shieldBlocked is reset and
l.Hurt(dmg, src) is invoked) to capture the new return value (e.g.,
result.Shielded or a bool) and call lt.deflect(e, vel) only when that explicit
flag is true; remove/reset any MarkShieldBlocked usage and any reliance on
shared mutable state so the decision is local to the Hurt call return.

316-323: 💤 Low value

Edge case: zero-magnitude reflected vector won't move out of hitbox.

The early return on vel.Len() == 0 is good, but for non-zero but very small velocities the 0.05 unit offset may still leave the projectile inside the blocker's bounding box. Since the velocity is then reversed, on the next tick the projectile moves away from the blocker, so re-collision should not occur in practice. Worth a comment to make the intent of the 0.05 nudge explicit (separation, not full ejection):

📝 Suggested clarifying comment
 func (lt *ProjectileBehaviour) deflect(e *Ent, vel mgl64.Vec3) {
 	if vel.Len() == 0 {
 		return
 	}
 	reflected := vel.Mul(-1)
 	e.SetVelocity(reflected)
+	// Nudge slightly along the reflected direction to avoid re-resolving the same
+	// collision on the next sub-tick before the reversed velocity carries the
+	// projectile clear of the blocker's bounding box.
 	e.data.Pos = e.Position().Add(reflected.Normalize().Mul(0.05))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/entity/projectile.go` around lines 316 - 323, The deflect logic in
ProjectileBehaviour.deflect returns early for zero velocity but uses a fixed
0.05 position nudge which is intended as a separation hack rather than a
guaranteed full ejection; update the function to add a concise clarifying
comment above the reflected/nudge code explaining that reflected := vel.Mul(-1),
e.SetVelocity(reflected) and e.data.Pos =
Position().Add(reflected.Normalize().Mul(0.05)) use a small separation offset to
avoid stuck-in-hitbox re-collisions and that zero-length velocity is already
handled by the early return (optionally note why the value 0.05 was chosen and
that tiny non-zero velocities will be moved next tick).
server/world/entity_test.go (1)

19-68: 💤 Low value

Consider using table-driven tests for better maintainability.

The three test functions have nearly identical structure and differ only in the parameters passed to TNTWithSource. Consider consolidating them into a single table-driven test to reduce duplication and improve maintainability.

♻️ Example table-driven test approach
+func TestEntityRegistryConfigTNTWithSourceFallback(t *testing.T) {
+	tests := []struct {
+		name              string
+		source            *EntityHandle
+		allowUnblockable  bool
+	}{
+		{"DefaultTNT", nil, true},
+		{"SourceAwareTNT", EntitySpawnOpts{}.New(entityRegistryTestType{}, entityRegistryTestType{}), true},
+		{"ShieldUnblockableTNT", nil, false},
+	}
+	
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			called := false
+			reg := EntityRegistryConfig{
+				TNT: func(opts EntitySpawnOpts, fuse time.Duration) *EntityHandle {
+					called = true
+					return opts.New(entityRegistryTestType{}, entityRegistryTestType{})
+				},
+			}.New([]EntityType{entityRegistryTestType{}})
+			
+			if h := reg.Config().TNTWithSource(EntitySpawnOpts{}, time.Second, tt.source, tt.allowUnblockable); h == nil {
+				t.Fatal("expected fallback TNTWithSource to create TNT through TNT")
+			}
+			if !called {
+				t.Fatal("expected fallback TNTWithSource to call TNT")
+			}
+		})
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/world/entity_test.go` around lines 19 - 68, Consolidate the three
nearly identical tests into one table-driven test that iterates over cases
varying the TNTWithSource arguments (source nil vs non-nil and shieldUnblockable
true/false) and expected behavior; use a single test function (e.g.,
TestEntityRegistryConfig_TNTWithSource_Fallback_Table) that constructs the same
EntityRegistryConfig with the TNT stub (referencing TNT, EntityRegistryConfig,
New, EntitySpawnOpts, and TNTWithSource) and for each case calls
reg.Config().TNTWithSource(...), asserting non-nil result and that the TNT stub
was invoked; keep the existing assertions but drive inputs and expectations from
the table to remove duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/entity/projectile.go`:
- Around line 192-198: When handling a deflection in tickMovement, capture the
tick-start position (e.g. startPos := e.Position()) before calling tickMovement
and use that startPos to compute the per-tick displacement for the movement
packet; replace the current m.dpos = m.pos.Sub(result.Position()) with m.dpos =
e.Position().Sub(startPos) so Movement.Send() sees the full tick delta, and keep
m.pos/m.vel and m.dvel assignments as they are (use the saved startPos variable
to compute dpos after the deflection branch).

In `@server/entity/tnt.go`:
- Around line 104-105: The code converts fuse := from
tntFuseAndBlockability(data.Data) into uint8 ticks which will wrap for values
>255; update the handling around fuse (from tntFuseAndBlockability and where m
:= map[string]any{"Fuse": uint8(...)} is created) to detect oversized fuse tick
counts (fuse.Milliseconds()/50 > 255) and either clamp the tick value to 255 or
return/reject with an error before NBT encoding; ensure the check uses the same
milliseconds/50 calculation and apply the sanitized uint8 value into the map so
no silent wrapping occurs.

In `@server/player/player.go`:
- Around line 1424-1426: The code directly clears the flag shieldBlockingInput
which bypasses the cleanup in SetShieldBlockingInput and leaves
shieldBlockingUseHandled stale; change the path that currently sets
shieldBlockingInput = false to call SetShieldBlockingInput(false) so the
function resets shieldBlockingUseHandled and performs any other teardown
consistently (look for symbols shieldBlockingInput, SetShieldBlockingInput,
shieldBlockingUseHandled, and HandleItemUse to verify the fix).

In `@server/session/entity_metadata.go`:
- Around line 84-86: Replace the non-existent constant
protocol.EntityDataFlagBlocking with protocol.EntityDataFlagBlockedUsingShield
in the shield-blocking metadata path: in the block where you check shieldBlocker
and call m.SetFlag (the code using protocol.EntityDataKeyFlagsTwo and
m.SetFlag), update the constant to EntityDataFlagBlockedUsingShield; also update
the assertions in the tests (entity_metadata_test.go) that reference the old
constant to expect protocol.EntityDataFlagBlockedUsingShield instead.

In `@server/session/handler_player_auth_input.go`:
- Around line 169-179: The shieldBlockingInput function is missing handling for
InputFlagStartSneaking; update the initial condition in shieldBlockingInput to
include flags.Load(packet.InputFlagStartSneaking) so that a StartSneaking-only
packet is treated like a sneak input and triggers the shield warm-up transition.
Locate shieldBlockingInput and add packet.InputFlagStartSneaking to the OR group
with InputFlagSneaking/InputFlagSneakDown/InputFlagSneakCurrentRaw (keeping the
existing wasSneaking/sneaking checks and return values unchanged).

---

Nitpick comments:
In `@server/entity/damage.go`:
- Around line 49-58: The Origin/HasOrigin pair in ExplosionDamageSource should
be replaced with a nullable pointer to avoid the extra boolean: change the
struct field(s) so Origin is of type *mgl64.Vec3 (remove HasOrigin), update all
call sites and consumers of ExplosionDamageSource (constructors, uses in
functions/methods, and any comparisons) to treat Origin == nil as “no origin”
and otherwise dereference the pointer for the Vec3 value, and ensure any code
that previously read HasOrigin now checks for Origin != nil; reference
ExplosionDamageSource, Origin and HasOrigin to locate and update the affected
code paths.

In `@server/entity/projectile_test.go`:
- Around line 38-43: The test's Hurt method uses a structural type assertion to
call the shield-blocking behavior; replace that chain with the new helper by
calling Ent.MarkShieldBlocked() on the projectile instance: inside
projectileShieldTarget.Hurt, after confirming src is a ProjectileDamageSource
and t.blocked, cast s.Projectile to *Ent and invoke its MarkShieldBlocked()
method (instead of s.Projectile.(*Ent).Behaviour().(interface{
MarkShieldBlocked() }).MarkShieldBlocked()), so the test mirrors production
usage and centralizes the behavior.
- Around line 83-88: newProjectileShieldTestEnt constructs an Ent directly
without calling Open(tx, handle, data), so the returned Ent has no transaction
(e.tx nil) which is fine for tests that only call behaviour.hitEntity but will
silently break if tests later call methods that dereference e.tx (e.g., Close,
SetNameTag, SetOnFire). Update newProjectileShieldTestEnt (or add an inline
comment above it) to explicitly document that it intentionally returns a
"tx-less" Ent for direct unit testing of behaviour.hitEntity, or alternatively
change the helper to call Ent.Open(tx, handle, data) so the built Ent has a
proper tx; reference the newProjectileShieldTestEnt function, Ent type,
Open(tx,...), and e.tx when making the change.

In `@server/entity/projectile.go`:
- Around line 286-289: The code is using lt.shieldBlocked as a fragile
side-channel set by l.Hurt; change the API so l.Hurt (or the damage result it
returns) includes an explicit shielded/blocked boolean indicating whether the
hit was shield-blocked, then stop using lt.shieldBlocked and MarkShieldBlocked
as a cross-call mutable flag. Update the call site in ProjectileBehaviour (where
lt.shieldBlocked is reset and l.Hurt(dmg, src) is invoked) to capture the new
return value (e.g., result.Shielded or a bool) and call lt.deflect(e, vel) only
when that explicit flag is true; remove/reset any MarkShieldBlocked usage and
any reliance on shared mutable state so the decision is local to the Hurt call
return.
- Around line 316-323: The deflect logic in ProjectileBehaviour.deflect returns
early for zero velocity but uses a fixed 0.05 position nudge which is intended
as a separation hack rather than a guaranteed full ejection; update the function
to add a concise clarifying comment above the reflected/nudge code explaining
that reflected := vel.Mul(-1), e.SetVelocity(reflected) and e.data.Pos =
Position().Add(reflected.Normalize().Mul(0.05)) use a small separation offset to
avoid stuck-in-hitbox re-collisions and that zero-length velocity is already
handled by the early return (optionally note why the value 0.05 was chosen and
that tiny non-zero velocities will be moved next tick).

In `@server/world/entity_test.go`:
- Around line 19-68: Consolidate the three nearly identical tests into one
table-driven test that iterates over cases varying the TNTWithSource arguments
(source nil vs non-nil and shieldUnblockable true/false) and expected behavior;
use a single test function (e.g.,
TestEntityRegistryConfig_TNTWithSource_Fallback_Table) that constructs the same
EntityRegistryConfig with the TNT stub (referencing TNT, EntityRegistryConfig,
New, EntitySpawnOpts, and TNTWithSource) and for each case calls
reg.Config().TNTWithSource(...), asserting non-nil result and that the TNT stub
was invoked; keep the existing assertions but drive inputs and expectations from
the table to remove duplication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 652953a8-a857-41ed-b725-1db9f91de395

📥 Commits

Reviewing files that changed from the base of the PR and between a2f6175 and 421970c.

📒 Files selected for processing (28)
  • server/block/explosion.go
  • server/block/tnt.go
  • server/block/tnt_test.go
  • server/entity/damage.go
  • server/entity/ent.go
  • server/entity/projectile.go
  • server/entity/projectile_test.go
  • server/entity/register.go
  • server/entity/tnt.go
  • server/entity/tnt_test.go
  • server/item/register.go
  • server/item/shield.go
  • server/item/shield_test.go
  • server/player/player.go
  • server/player/shield.go
  • server/player/shield_test.go
  • server/session/entity_metadata.go
  • server/session/entity_metadata_test.go
  • server/session/handler_inventory_transaction.go
  • server/session/handler_inventory_transaction_test.go
  • server/session/handler_item_stack_request.go
  • server/session/handler_item_stack_request_test.go
  • server/session/handler_player_auth_input.go
  • server/session/handler_player_auth_input_test.go
  • server/session/world.go
  • server/world/entity.go
  • server/world/entity_test.go
  • server/world/sound/item.go

Comment thread server/entity/projectile.go
Comment thread server/entity/tnt.go Outdated
Comment thread server/player/player.go
Comment thread server/session/entity_metadata.go
Comment thread server/session/handler_player_auth_input.go
Comment thread server/block/tnt.go Outdated
Comment thread server/player/player.go Outdated
Comment thread server/player/shield.go

Copy link
Copy Markdown
Owner Author

CodeRabbit follow-up:

  • Fixed the valid TNT NBT fuse wrapping issue by clamping encoded fuse ticks to the uint8 range and added regression coverage.
  • Fixed the stale shieldBlockingUseHandled latch when held-item updates cancel shield input and added regression coverage.
  • Fixed InputFlagStartSneaking-only shield input handling and added regression coverage.
  • Skipped the projectile m.dpos suggestion: Movement.Send() uses dpos only as a non-zero gate before sending the absolute position, and shield deflection always nudges by 0.05, which is above the 0.001 epsilon, so viewers still receive the movement update.
  • Skipped the metadata constant suggestion: the checked module is github.com/sandertv/gophertunnel v1.56.2, and it defines protocol.EntityDataFlagBlocking; local go test ./... and make lint pass with the current constant.

Comment thread server/player/player.go
Comment thread server/entity/tnt.go
Comment thread server/player/player.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
server/player/shield_test.go (2)

831-844: 💤 Low value

Use t.Run subtests for table-driven tests.

TestShieldDurabilityDamage, TestShouldAttemptShieldBlockBeforeHurtHandler (lines 860-907), and TestShouldAttemptShieldBlockAfterHurtHandlerWithHandlerMutatedDamage (lines 909-953) all loop over cases and use t.Fatalf, which aborts on the first failure and hides remaining cases. Promoting each case to a subtest via t.Run(test.name, ...) (or a generated name for TestShieldDurabilityDamage) gives per-case names in go test -v/CI output and lets -run target a single case.

♻️ Example for TestShouldAttemptShieldBlockBeforeHurtHandler
 	} {
-		if got := shouldAttemptShieldBlockBeforeHurtHandler(test.rawDamage, test.src); got != test.want {
-			t.Fatalf("%v: expected shouldAttemptShieldBlockBeforeHurtHandler to return %v, got %v", test.name, test.want, got)
-		}
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			if got := shouldAttemptShieldBlockBeforeHurtHandler(test.rawDamage, test.src); got != test.want {
+				t.Fatalf("expected shouldAttemptShieldBlockBeforeHurtHandler to return %v, got %v", test.want, got)
+			}
+		})
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/player/shield_test.go` around lines 831 - 844, Convert the
table-driven loops in TestShieldDurabilityDamage,
TestShouldAttemptShieldBlockBeforeHurtHandler, and
TestShouldAttemptShieldBlockAfterHurtHandlerWithHandlerMutatedDamage to t.Run
subtests: give each case a stable name (either a provided "name" field in the
test struct or generated via fmt.Sprintf like "%v" or "case_%d"), call
t.Run(name, func(t *testing.T) { ... }) for each iteration, and replace t.Fatalf
with t.Fatalf/t.Errorf inside the subtest so failures report per-case without
aborting other cases; update the test case structs in those functions if needed
to include a name field and ensure shieldDurabilityDamage and the two handler
tests use the new subtest structure.

813-829: 💤 Low value

Tighten the projectile-source assertion in TestShieldKnocksBackMeleeAttacker.

After the successful melee call at lines 817-818, attacker.src, attacker.force, and attacker.height are populated. The follow-up check at lines 826-828 only inspects the return value, so a regression that returns false while still calling KnockBack for a projectile source would silently pass. Snapshotting the fields before the second call and asserting they are unchanged would catch that.

♻️ Suggested tightening
+	prevSrc, prevForce, prevHeight := attacker.src, attacker.force, attacker.height
 	if p.knockBackShieldAttacker(entity.ProjectileDamageSource{Projectile: attacker}) {
 		t.Fatal("expected projectile source not to knock back the attacker")
 	}
+	if attacker.src != prevSrc || attacker.force != prevForce || attacker.height != prevHeight {
+		t.Fatalf("expected projectile source not to mutate attacker knockback state, got %v/%v/%v", attacker.src, attacker.force, attacker.height)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/player/shield_test.go` around lines 813 - 829, The test
TestShieldKnocksBackMeleeAttacker currently checks only the boolean result for
the projectile case, so a regression that still calls KnockBack would be missed;
before calling
p.knockBackShieldAttacker(entity.ProjectileDamageSource{Projectile: attacker})
capture attacker.src, attacker.force and attacker.height into local variables
and then assert after the call that those three fields remain equal to the
captured values (and still equal to the expected melee values), while also
asserting the call returns false via p.knockBackShieldAttacker.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/player/shield_test.go`:
- Around line 831-844: Convert the table-driven loops in
TestShieldDurabilityDamage, TestShouldAttemptShieldBlockBeforeHurtHandler, and
TestShouldAttemptShieldBlockAfterHurtHandlerWithHandlerMutatedDamage to t.Run
subtests: give each case a stable name (either a provided "name" field in the
test struct or generated via fmt.Sprintf like "%v" or "case_%d"), call
t.Run(name, func(t *testing.T) { ... }) for each iteration, and replace t.Fatalf
with t.Fatalf/t.Errorf inside the subtest so failures report per-case without
aborting other cases; update the test case structs in those functions if needed
to include a name field and ensure shieldDurabilityDamage and the two handler
tests use the new subtest structure.
- Around line 813-829: The test TestShieldKnocksBackMeleeAttacker currently
checks only the boolean result for the projectile case, so a regression that
still calls KnockBack would be missed; before calling
p.knockBackShieldAttacker(entity.ProjectileDamageSource{Projectile: attacker})
capture attacker.src, attacker.force and attacker.height into local variables
and then assert after the call that those three fields remain equal to the
captured values (and still equal to the expected melee values), while also
asserting the call returns false via p.knockBackShieldAttacker.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c0556ac0-eacc-4e9f-b6e7-cf92a842e92c

📥 Commits

Reviewing files that changed from the base of the PR and between a2622c9 and ac603a8.

📒 Files selected for processing (4)
  • server/entity/tnt.go
  • server/player/player.go
  • server/player/shield.go
  • server/player/shield_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • server/player/shield.go
  • server/entity/tnt.go
  • server/player/player.go

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ac603a8. Configure here.

}
if b, ok := e.(shieldBlocker); ok && b.ShieldBlocking() {
m.SetFlag(protocol.EntityDataKeyFlagsTwo, protocol.EntityDataFlagBlocking&63)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shield blocking flag uses wrong metadata key or mask

Medium Severity

The shield blocking metadata flag is set using protocol.EntityDataFlagBlocking&63 on EntityDataKeyFlagsTwo. The &63 mask extracts the bit position within the second 64-bit flags field. However, the EntityDataFlagBlocking constant in the Bedrock protocol is flag index 71, and 71 & 63 = 7. If the gophertunnel library defines EntityDataFlagBlocking as the raw bit index (e.g., 7) rather than the absolute index (71), this mask operation would still yield 7 but the flag would need to be set on EntityDataKeyFlags (first field), not EntityDataKeyFlagsTwo. The crawling pattern EntityDataFlagCrawling&63 works because crawling is indeed in the second flags field. Without verifying the actual constant value, there's a risk the blocking flag is set in the wrong flags field, causing the client to not display the shield blocking animation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ac603a8. Configure here.

Comment thread server/player/shield.go
return false
}
return isZeroDamageProjectile(rawDamage, src)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shield durability uses raw damage ignoring armor-reduced zero

Medium Severity

shouldAttemptShieldBlockAfterHurtHandler skips shield blocking when damageBeforeHandler (after armor/resistance reduction) is positive but damageLeft is zero (handler zeroed it). However, it also skips when armor fully reduces totalDamage to 0 and rawDamage > 0 (since damageBeforeHandler=0 and isZeroDamageProjectile fails for non-projectiles). This means melee attacks fully reduced by armor never trigger shield blocking—the shield won't play the block sound, recoil the attacker, or take durability damage. In vanilla Minecraft, shields block melee attacks regardless of armor, taking priority over armor reduction.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ac603a8. Configure here.

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