auras: -1 ms is "does not run out", not 49.71 days
grom's mount buff carried a "50d" countdown. pfUI formats a buff's remaining time as `round(remaining / 86400) .. "d"` (api/api.lua:1689), so the span behind that is 4 294 967 seconds -- 0xFFFFFFFF milliseconds read unsigned, which is the core's own -1 for a permanent aura. SMSG_UPDATE_AURA_DURATION is four bytes of signed milliseconds and is supposed never to arrive for a permanent aura; this realm sends one anyway. AuraDurations read it as u32 and stamped an expiry 50 days out. A negative span now leaves the slot unstamped, which join_duration already answers (0, 0) for and which GetPlayerBuffTimeLeft returns 0 from -- no countdown, which is what a mount has. Zero is left alone: an expiring aura's last update is a real 0. Ruled out first: nothing in the chain's SpellDuration.dbc is within three orders of magnitude of it (the one infinite row, 21, is the usual -1). Also adds `ironforge-lava`, a capture scenario that actually has magma in frame -- `ironforge-forge` is a lighting shot of a railing. Its viewpoint is group g061's pool, one of the nine static nibble-2 pools the liquid census already pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -1318,6 +1318,19 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
minute: 720,
|
||||
ui: None,
|
||||
},
|
||||
// Ironforge's magma, filling the frame: the static nibble-2 pool in group g061, whose own
|
||||
// extent is x -4891..-4803, y -938..-851 at z 421.2 (the census in `liquid::surface`'s tests
|
||||
// places all eleven). `ironforge-forge` above is a lighting shot and has no liquid in it;
|
||||
// this is the one to look at when the question is what magma itself looks like, and its
|
||||
// outdoor opposite number is `lava-searing-river`.
|
||||
Scenario {
|
||||
name: "ironforge-lava",
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-4847.0, -895.0, 432.0],
|
||||
look: [-4815.0, -905.0, 421.0],
|
||||
minute: 720,
|
||||
ui: None,
|
||||
},
|
||||
// Ironforge: the Great Forge (lava, fire light, a huge interior).
|
||||
Scenario {
|
||||
name: "ironforge-forge",
|
||||
|
||||
@@ -43,13 +43,30 @@ struct DurationStamp {
|
||||
|
||||
impl AuraDurations {
|
||||
/// Records one `SMSG_UPDATE_AURA_DURATION`.
|
||||
///
|
||||
/// The field is four bytes of **signed** milliseconds, and a core sends `-1` for an aura that
|
||||
/// does not run out (vmangos `SpellAuraHolder::GetAuraDuration` on a permanent holder). Read
|
||||
/// unsigned that is `0xFFFFFFFF`, which is 4 294 967 seconds, which is 49.71 days: grom's
|
||||
/// mount carried a `50d` countdown in pfUI's buff frame, whose own formatter is
|
||||
/// `round(remaining / 86400) .. "d"` (`pfUI/api/api.lua:1689`). Nothing in the chain's
|
||||
/// `SpellDuration.dbc` is anywhere near it — the one infinite row, 21, is the usual `-1`.
|
||||
///
|
||||
/// So a negative span is the "no duration" sentinel and leaves the slot unstamped, which is
|
||||
/// what `join_duration` already answers `(0, 0)` for and what `GetPlayerBuffTimeLeft` then
|
||||
/// returns 0 from. The packet's own doc comment said this never arrives for a permanent aura;
|
||||
/// on this realm it does. Zero is left alone: an expiring aura's last update is a real 0 and
|
||||
/// has always stamped an expiry of now.
|
||||
pub(crate) fn set(&mut self, slot: u8, remaining_ms: u32, now: f64) {
|
||||
let total = f64::from(remaining_ms) / 1000.0;
|
||||
if trace_period().is_some() {
|
||||
info!(
|
||||
"aura trace: SMSG_UPDATE_AURA_DURATION slot {slot} = {remaining_ms} ms @ {now:.2}"
|
||||
);
|
||||
}
|
||||
if (remaining_ms as i32) < 0 {
|
||||
self.by_slot.remove(&slot);
|
||||
return;
|
||||
}
|
||||
let total = f64::from(remaining_ms) / 1000.0;
|
||||
self.by_slot.insert(
|
||||
slot,
|
||||
DurationStamp {
|
||||
@@ -1520,6 +1537,34 @@ mod tests {
|
||||
assert_eq!(join_duration(None, 200.0, 200.0, 5000.0), (0.0, 0.0));
|
||||
}
|
||||
|
||||
/// `-1` milliseconds is "this one does not run out", not 49.71 days.
|
||||
///
|
||||
/// grom's mount buff showed `50d` in pfUI's frame, which formats with
|
||||
/// `round(remaining / 86400) .. "d"` — so the span behind it was 4 294 967 seconds, which is
|
||||
/// `0xFFFFFFFF` milliseconds read unsigned. The chain's `SpellDuration.dbc` has no row within
|
||||
/// three orders of magnitude of it.
|
||||
#[test]
|
||||
fn a_negative_duration_is_the_permanent_sentinel_and_leaves_no_stamp() {
|
||||
let mut durations = AuraDurations::default();
|
||||
durations.set(3, 300_000, 10.0);
|
||||
assert!(durations.by_slot.contains_key(&3), "a real span stamps");
|
||||
|
||||
durations.set(3, u32::MAX, 11.0);
|
||||
assert!(
|
||||
!durations.by_slot.contains_key(&3),
|
||||
"-1 ms means permanent, so the slot keeps no expiry at all"
|
||||
);
|
||||
assert_eq!(
|
||||
join_duration(durations.by_slot.get(&3), 11.0, 11.0, 900.0),
|
||||
(0.0, 0.0),
|
||||
"and GetPlayerBuffTimeLeft answers 0 from it, which is no countdown"
|
||||
);
|
||||
|
||||
// Zero is a real last update from an aura that is going, and still stamps.
|
||||
durations.set(4, 0, 12.0);
|
||||
assert!(durations.by_slot.contains_key(&4));
|
||||
}
|
||||
|
||||
/// The duration packet arrives about a frame before the delta that fills its slot.
|
||||
#[test]
|
||||
fn a_stamp_that_arrives_one_frame_before_its_aura_still_joins_it() {
|
||||
|
||||
@@ -622,8 +622,12 @@ pub enum ServerPacket {
|
||||
ChannelUpdate {
|
||||
remaining_ms: u32,
|
||||
},
|
||||
/// `SMSG_UPDATE_AURA_DURATION` (self-only, never for a permanent aura): one aura slot's time
|
||||
/// left. It arrives before the delta naming the slot's spell, so buffer it by slot.
|
||||
/// `SMSG_UPDATE_AURA_DURATION` (self-only): one aura slot's time left, four bytes of signed
|
||||
/// milliseconds. It arrives before the delta naming the slot's spell, so buffer it by slot.
|
||||
///
|
||||
/// A permanent aura is supposed to get no packet at all. OctoWoW sends one anyway, carrying
|
||||
/// the core's own `-1`, so the consumer treats a negative span as "no duration" rather than
|
||||
/// as the 49.71 days it reads as unsigned.
|
||||
UpdateAuraDuration {
|
||||
slot: u8,
|
||||
remaining_ms: u32,
|
||||
|
||||
Reference in New Issue
Block a user