the bank is the reference's own window, and far sight freezes your body
- the bank window is the reference's own `BankFrame.xml` run off the player's chain — our 890-line copy is deleted whole; putting bags into the bank's bag slots works (the two verbs its buttons are wired to now exist), the purse is a real `SmallMoneyFrameTemplate` splitting 987654321 copper into three coin buttons instead of one string, `BANKFRAME_OPENED` loses the argument the real client never fires, and the bank stops auto-opening your bags while the vendor and mailbox keep doing it; the empty-slot click still plays the bag sound, because the reference's own line plays it on every click the cursor did not consume — named as faithful rather than quietly fixed - underneath it, the bank's inventory band is a VIEW over the container feed, not a second copy — inventory ids 40..69 had answered nil because the doll array stops at 24, so the stock file swapped in would have drawn an empty bank; the six bank bag slots become real inventory slots (64..=69) fed off the player descriptor, the three bag verbs are carved from the binary (`PutItemInBackpack` IS `PutItemInBag(0xFF)` through a ten-byte thunk, the occupied-by-an-ordinary-item leg auto-stores with no destination slot at all, and the return is the Lua number 1 or nil — the nil being the whole reason a plain left-click opens a bag), and the swap caught `BankButtonIDToInvSlotID`'s bag arm six slots wrong: it took a bag number 1..6 where the reference passes the button's own id, which is the container id - far sight freezes your own body while the view is out — the fifth conjunct of the movement predicates' shared precondition, joining the health term so the walk, the turn and the follow teardown go down together; both halves are load-bearing: the `IsActivePlayer` half is what keeps a Mind Control victim walking (possession rides the same PLAYER_FARSIGHT field), and the latch half is the RESOLVED engage rather than the raw field — and the camera is untouched, you still look around freely, which is the whole point of the spell - the reset offer no longer outlives the character that earned it — log out after walking out of the Deadmines and your next character was offered a reset the first one had earned — and the one lockout leg no probe can ever reach (vmangos writes a last-instance map only from raid binds, which the client's handler discards as not-a-party-dungeon) is now covered end to end through the real system, its module doc saying plainly: tested and never observed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,13 @@
|
||||
//! that answers term 1. (1) is [`track_instance_state`]'s `CurrentMap` flip; (2) is
|
||||
//! [`apply::update_last_instance`]'s queue, drained through the same helper so the two cannot
|
||||
//! drift — [`LatchWriter`] is which of them is speaking.
|
||||
//!
|
||||
//! **(2) is therefore unreachable on our server by construction, and no live probe can cover it**
|
||||
//! — not with any rigging, because `PermBindAllPlayers` (vmangos's only path to a permanent bind)
|
||||
//! runs under `if (IsRaid())` and no GM command creates a bind. Its cover is
|
||||
//! `both_latch_writers_through_the_real_system`, which drives both legs through the real system
|
||||
//! against the real `Map.dbc`. That is the whole of the evidence behind (2): treat it as tested,
|
||||
//! never as *observed*.
|
||||
|
||||
use benilla_protocol::messages::{InstanceResetFailure, RaidInstanceMessage, RaidInstanceWarning};
|
||||
use benilla_ui::script::UiScript;
|
||||
@@ -128,10 +135,10 @@ pub(crate) struct InstanceState {
|
||||
/// benilla's own half of the same term: we watched the player walk out of a party dungeon, so
|
||||
/// we know they are bound to one without being told (decision 1754).
|
||||
///
|
||||
/// Raised by [`LatchWriter::WorldEntry`], cleared by `SMSG_INSTANCE_RESET`, and deliberately
|
||||
/// **not** cleared by an `owns_saved = false` packet — that packet is the wrong answer this
|
||||
/// field exists to work around, and vmangos sends one on the very teleport that takes us out
|
||||
/// of the dungeon.
|
||||
/// Raised by [`LatchWriter::WorldEntry`], cleared by `SMSG_INSTANCE_RESET` and by a **logout**
|
||||
/// ([`clear_witness_on_logout`]), and deliberately **not** cleared by an `owns_saved = false`
|
||||
/// packet — that packet is the wrong answer this field exists to work around, and vmangos
|
||||
/// sends one on the very teleport that takes us out of the dungeon.
|
||||
saw_own_dungeon: bool,
|
||||
/// `0xb4e374` — the `Map.dbc` id of the dungeon we were last inside, or `None` for "none
|
||||
/// recorded". Only a **party dungeon** ever lands here (`0x495d33`'s `cmp [rec+8],1`).
|
||||
@@ -226,6 +233,12 @@ impl InstanceState {
|
||||
self.last_dungeon_at = 0;
|
||||
// A reset landed, so whatever we witnessed ourselves is gone with it. The reference has
|
||||
// no equivalent line because its term 1 is the server's to retract.
|
||||
self.forget_witness();
|
||||
}
|
||||
|
||||
/// Drop benilla's own half of term 1 — see [`clear_witness_on_logout`]. Kept a method so the
|
||||
/// one piece of state no server will correct has exactly one place that clears it.
|
||||
pub(crate) fn forget_witness(&mut self) {
|
||||
self.saw_own_dungeon = false;
|
||||
}
|
||||
|
||||
@@ -598,6 +611,24 @@ fn drain_instance(script: Option<NonSendMut<UiScript>>, commands: Res<NetCommand
|
||||
|
||||
/// The lockout family: the four chat lines, the bookkeeping behind the SELF menu's reset row, and
|
||||
/// that row's one send.
|
||||
/// Drop the witness at logout — the next login is somebody else's character (`poi_marker`'s own
|
||||
/// shape, and the same reason).
|
||||
///
|
||||
/// Only [`InstanceState::saw_own_dungeon`] is cleared, not the reference's three globals: the
|
||||
/// reference does not clear those either (`0x495d00`'s only caller is the `SMSG_INSTANCE_RESET`
|
||||
/// handler), and it does not need to — its term 1 is the server's, re-advertised on the next
|
||||
/// character's world entry, so a stale latch cannot offer that character a reset on its own. Ours
|
||||
/// is the half no server will correct, so it gets the lifetime the packet would have given it
|
||||
/// (decision 1754).
|
||||
fn clear_witness_on_logout(
|
||||
mut state: ResMut<InstanceState>,
|
||||
mut logged_out: MessageReader<crate::net::LoggedOutMessage>,
|
||||
) {
|
||||
if logged_out.read().next().is_some() {
|
||||
state.forget_witness();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct UiInstancePlugin;
|
||||
|
||||
impl Plugin for UiInstancePlugin {
|
||||
@@ -607,6 +638,7 @@ impl Plugin for UiInstancePlugin {
|
||||
(
|
||||
// The latch first: `feed_instance` publishes `CanShowResetInstances()` off it, and
|
||||
// a map change and its answer should not be a frame apart.
|
||||
clear_witness_on_logout.before(track_instance_state),
|
||||
track_instance_state.before(feed_instance),
|
||||
feed_instance.before(UiInput),
|
||||
drain_instance.after(UiInput),
|
||||
@@ -923,6 +955,106 @@ mod tests {
|
||||
assert!(!state.can_reset(Some(0), &dungeons, 100));
|
||||
}
|
||||
|
||||
/// The witness does not outlive the character that earned it (`clear_witness_on_logout`).
|
||||
///
|
||||
/// The reference can leave its stale latch lying about because term 1 is the server's and the
|
||||
/// next character's world entry re-answers it. Ours is the half nothing corrects, so a logout
|
||||
/// has to, or character B is offered a reset that character A walked out of.
|
||||
#[test]
|
||||
fn the_witness_does_not_survive_a_logout() {
|
||||
let dungeons = |m: u32| m == 36;
|
||||
let mut state = InstanceState::default();
|
||||
state.note_last_instance(36, Some(0), true, 100, LatchWriter::WorldEntry);
|
||||
assert!(state.can_reset(Some(0), &dungeons, 100));
|
||||
|
||||
state.forget_witness();
|
||||
assert!(!state.can_reset(Some(0), &dungeons, 100));
|
||||
// The reference's own three globals are untouched — we clear our addition, not its state.
|
||||
assert_eq!(state.last_dungeon, Some(36));
|
||||
assert_eq!(state.last_dungeon_at, 100);
|
||||
}
|
||||
|
||||
/// **Both latch writers, driven through the real system, against the real `Map.dbc`.**
|
||||
///
|
||||
/// This is the only cover writer 2 — `SMSG_UPDATE_LAST_INSTANCE` — can have. vmangos can
|
||||
/// never exercise it: `SendSavedInstances` names only `perm` binds, `DungeonMap::Add` takes a
|
||||
/// non-perm bind for a 5-man, and `PermBindAllPlayers` (the one path that makes a bind
|
||||
/// permanent) runs under `if (IsRaid())`, so the only map ids that packet can ever carry are
|
||||
/// raids — which `0x495d33`'s `cmp [rec+8],1` throws away. No GM command creates a bind
|
||||
/// either (`.instance` offers listbinds/unbind/groupunbind, nothing that binds). So the packet
|
||||
/// leg is unreachable on our server by construction, and a live probe cannot cover it however
|
||||
/// the character is rigged (decision 1754's open thread).
|
||||
///
|
||||
/// It runs both legs through `track_instance_state` itself rather than the methods, because
|
||||
/// the thing worth guarding is the wiring: that the queue drains against the map we ended up
|
||||
/// on rather than the one we left, and that the packet writer does **not** raise the witness
|
||||
/// (which would hand 1754's deviation to a server's say-so). Skips without client data.
|
||||
#[test]
|
||||
fn both_latch_writers_through_the_real_system() {
|
||||
let data = benilla_formats::wow_data_or_skip!();
|
||||
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
|
||||
let maps = benilla_formats::load_map_catalog(&mut chain).expect("Map.dbc");
|
||||
assert!(maps.is_party_dungeon(36), "Deadmines is InstanceType 1");
|
||||
assert!(
|
||||
!maps.is_party_dungeon(409),
|
||||
"Molten Core is a raid, not a dungeon"
|
||||
);
|
||||
|
||||
// The system carries the last observed map in a `Local`, so it has to be the SAME system
|
||||
// across ticks — `run_system_once` hands out a fresh one each call and would never see a
|
||||
// transition at all.
|
||||
let mut app = App::new();
|
||||
app.init_resource::<InstanceState>()
|
||||
.init_resource::<Time<Real>>()
|
||||
.insert_resource(benilla_assets::MapCatalogRes(maps))
|
||||
.insert_resource(benilla_world::world_map::CurrentMap(36))
|
||||
.add_systems(Update, track_instance_state);
|
||||
|
||||
let state = |app: &App| {
|
||||
let s = app.world().resource::<InstanceState>();
|
||||
(s.last_dungeon, s.saw_own_dungeon)
|
||||
};
|
||||
let queue = |app: &mut App, map: u32| {
|
||||
apply::update_last_instance(&mut app.world_mut().resource_mut::<InstanceState>(), map);
|
||||
};
|
||||
|
||||
// Writer 1 — we walk out of the Deadmines ourselves. The first tick only OBSERVES map 36
|
||||
// (there is no map we left at login), so nothing is recorded until the flip.
|
||||
app.update();
|
||||
assert_eq!(state(&app), (None, false));
|
||||
|
||||
app.insert_resource(benilla_world::world_map::CurrentMap(0));
|
||||
app.update();
|
||||
assert_eq!(
|
||||
state(&app),
|
||||
(Some(36), true),
|
||||
"the flip records the dungeon AND our own eyes (1754's term 1)"
|
||||
);
|
||||
|
||||
// Writer 2 — the packet, on a fresh state. A raid id is dropped (the only thing vmangos
|
||||
// could ever send); a party dungeon lands, and lands WITHOUT the witness.
|
||||
app.insert_resource(InstanceState::default());
|
||||
queue(&mut app, 409);
|
||||
app.update();
|
||||
assert_eq!(state(&app), (None, false), "a raid is not a party dungeon");
|
||||
|
||||
queue(&mut app, 36);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
state(&app),
|
||||
(Some(36), false),
|
||||
"the packet is the server's word, never our own eyes"
|
||||
);
|
||||
|
||||
// …and the packet's own early-out: the instance we are STANDING IN is not one we left.
|
||||
app.insert_resource(InstanceState::default());
|
||||
app.insert_resource(benilla_world::world_map::CurrentMap(36));
|
||||
app.update();
|
||||
queue(&mut app, 36);
|
||||
app.update();
|
||||
assert_eq!(state(&app), (None, false));
|
||||
}
|
||||
|
||||
/// `CanShowResetInstances()`'s four terms, one at a time.
|
||||
#[test]
|
||||
fn can_reset_needs_all_four_terms() {
|
||||
|
||||
Reference in New Issue
Block a user