From be808eb7e6f5541a5315ab4c34fbf73406e67b9a Mon Sep 17 00:00:00 2001 From: chairmanpow Date: Tue, 6 Oct 2026 17:05:56 +0100 Subject: [PATCH] fix: battleground objectives use the plain-use path and skip the client gate --- crates/benilla-app/src/target/click.rs | 7 +++ crates/benilla-app/src/target/cursor_mode.rs | 63 +++++++++++--------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/crates/benilla-app/src/target/click.rs b/crates/benilla-app/src/target/click.rs index 53942be4..f217cc0e 100644 --- a/crates/benilla-app/src/target/click.rs +++ b/crates/benilla-app/src/target/click.rs @@ -645,6 +645,13 @@ pub(crate) fn resolve_go_action( me_store: Option<&ObjectStore>, net: &NetCommands, ) -> GoAction { + // A battleground objective is a plain use: its template lock is the server's capture + // bookkeeping, not a chest lock the client should resolve, and the server validates the use + // itself. Without this the objective's lock chain can refuse (`Refuse`/`OpenLock`) a click the + // reference sends as `CMSG_GAMEOBJ_USE`. + if inputs.battleground.active() { + return GoAction::Use; + } let Some(tmpl) = inputs.templates.get(guid) else { return GoAction::Use; }; diff --git a/crates/benilla-app/src/target/cursor_mode.rs b/crates/benilla-app/src/target/cursor_mode.rs index 43012781..f9861ed0 100644 --- a/crates/benilla-app/src/target/cursor_mode.rs +++ b/crates/benilla-app/src/target/cursor_mode.rs @@ -306,20 +306,24 @@ fn highlightable_flags( if type_id == GO_TYPE_MEETINGSTONE { return !overrides.meeting_stone_queued; } + // Inside a battleground the whole shared gate is skipped for the objective types the type gate + // above let through: the objective's `GAMEOBJECT_FACTION` is a team marker the client cannot + // resolve against the player's race faction on a cross-faction realm, and the occupied banners' + // `GAMEOBJECT_FLAGS`/activate bit are the server's per-player bookkeeping. The server validates + // every battleground use (`CanUseBattleGroundObject`), so the client offer is safe. + if overrides.battleground { + return true; + } // The faction term (`0x5f3026`): the GameObject's reaction toward us must beat hostile. An - // unresolved reaction passes, so a data gap never blanks the world. Skipped inside a - // battleground: the objective's faction is a team marker the client cannot resolve against the - // player's race faction on a cross-faction realm, and the server gates the use itself. - if !overrides.battleground { - let ordinary_faction_ok = |r: Option| r.is_none_or(|r| r > 1); - if type_id == GO_TYPE_TRAP { - // TRAP inverts it (`0x5f2fc6`): highlightable only to whoever it is hostile to. - if !reaction.is_none_or(|r| r == 1) { - return false; - } - } else if !ordinary_faction_ok(reaction) { + // unresolved reaction passes, so a data gap never blanks the world. + let ordinary_faction_ok = |r: Option| r.is_none_or(|r| r > 1); + if type_id == GO_TYPE_TRAP { + // TRAP inverts it (`0x5f2fc6`): highlightable only to whoever it is hostile to. + if !reaction.is_none_or(|r| r == 1) { return false; } + } else if !ordinary_faction_ok(reaction) { + return false; } if flags & GO_FLAG_IN_USE_OR_NO_INTERACT != 0 { return false; @@ -608,23 +612,26 @@ pub(super) fn classify_cursor( // arm is not applied. The lock arm needs `GO_FLAG_LOCKED`, so an herb the player cannot // gather stays lit and refuses on the click, while a padlocked door grays. let facts = super::lock::go_facts(Some((store, crate::go_anim::go_state(anim, store)))); - let lock_unmet = go_inputs - .locks - .as_deref() - .and_then(|l| l.0.slots(lock_id).filter(|_| lock_id != 0)) - .is_some_and(|slots| { - super::lock::resolve_lock( - slots, - &player_actions.spells, - go_inputs.spells.as_deref(), - go_inputs.skill_lines.as_ref().map(|s| &s.catalog), - Some(self_store), - &go_inputs.objects, - facts, - &mut None, - ) - .blocks_usable(facts.flag_locked) - }); + // A battleground objective's lock is capture bookkeeping, not a chest lock: the plain-use + // path skips it, so the gray arm must too. + let lock_unmet = !go_inputs.battleground.active() + && go_inputs + .locks + .as_deref() + .and_then(|l| l.0.slots(lock_id).filter(|_| lock_id != 0)) + .is_some_and(|slots| { + super::lock::resolve_lock( + slots, + &player_actions.spells, + go_inputs.spells.as_deref(), + go_inputs.skill_lines.as_ref().map(|s| &s.catalog), + Some(self_store), + &go_inputs.objects, + facts, + &mut None, + ) + .blocks_usable(facts.flag_locked) + }); let unable = kind != CursorKind::PickLock && (lock_unmet || dist_sq > go_interact_range_sq(store.0.gameobject_type_id())); Some((kind, unable))