Linux and Windows get their sound back, and emotes refuse while moving
- every build off macOS has been silent since the 09-02 snapshot (B356): the non-macOS device layer was a stub that opened nothing, and nothing had ever compiled the client for another target; that layer is now the same output stream over cpal — one seam, not two stacks: the ring, the render thread and the rebuild-on-device-change loop are one code on every platform, only the device file differs; the audio thread never allocates, a lost or changed default device is noticed at kira's own cadence, and realtime is `SCHED_FIFO` on unix and MMCSS "Pro Audio" on Windows, either refusable, the ring turning a refusal into a degradation; and the two instruments this bug says were missing exist — a Linux and Windows cross-compile under `clippy -D warnings`, and a live output test that opens the real device and asserts the ring never ran dry, because compiling is not sounding (1920) - an emote that requires standing still now refuses on the move: `CheckEmoteEligible`'s `0x4000` arm writes an out-param that `DoEmote` turns into a red "You can't do that while moving!" and aborts; 20 emotes of 78 can raise the line, read off the shipped `Emotes.dbc`; SWIMMING is not in the mask, so a swimmer holding still emotes fine, and `IsSelfControlled` is TRUE for an ordinary player, so a feared or confused player emotes away happily (1904); and the cast path gains its equipped-item rung — the button greyed and pressing it still sent: the reference short-circuits to "fits" on four gates, not one, the miss being a zero `EquippedItemSubClassMask`, which means no item required at all, and the rung sits above the crowd-control validator, so a player both stunned and missing the weapon is told about the weapon; that ladder has six arms, not three — CHARMED, FLEEING and CONFUSED join (1925) - the quest reward's money is the reference's own widget: the tail of the shared `QuestFrameItems_Update` drives `questState.."MoneyFrame"` through `MoneyFrame_Update`, and the two hand-built coin trios become one `MoneyFrameTemplate` instance each at the reference's anchors, which our coins already used; the progress panel stays on coins, being the required-money flow, not the reward one (1928); and the flight master's `!` joins the questgiver sweep, because the green `!` and the gold `!` are the SAME slot, `unit+0xb2c`, torn down once before both queries re-issue; the mouseover re-ask is refuted — the only taxi-status sender has one live caller — and the two queries are not symmetric: the questgiver's goes out on every sweep, the taxi one only when the unit's key moves or on a full sweep, as the wire shows at Dungar Longdrink: 68 questgiver status queries against 1 taxi-node one (1918) - `GetSpellTabInfo` out of range answers `nil, nil, 0, 0` — the reference returns four values on every path that returns (`0x4b3ce0`), and ours answered a single nil copied from another binding's shape; it matters because stock `SpellBookFrame_OnLoad` calls it unconditionally and then does `id > (offset + numSpells)`, which a nil raises on and (0+0) makes true, hiding every button on the page — the behaviour to match; the argument raises the reference's usage string when absent, and the index truncates toward zero THEN decrements (1931); the perf probe gains a `WOW_PROBE_UNCAP=vsync` arm, because the uncapped present mode is a request, not a schedule — measured, it taxes the number it was meant to pin (11.80 `cpu_ms` uncapped against 14.65 under it), so the default stays uncapped; and the last three test temp paths are keyed on the pid, after concurrent runs raced on one fixed shared file Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
/// The dressing-room pane's bake yaw in radians (the ref's `Model:SetRotation`), read by the
|
||||
/// app onto the `"dressup"` booth slot each frame — the twin of [`Self::paperdoll_yaw`].
|
||||
/// app onto the `"dressup"` booth slot each frame — the twin of `Self::paperdoll_yaw`.
|
||||
pub fn dressup_yaw(&self) -> f32 {
|
||||
self.model_ref().dressup_yaw
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//!
|
||||
//! With the answer 0, that dropdown sized itself to 40px and seven character names drew outside
|
||||
//! their own frame. The reference's own `SmallMoneyFrame` does the same thing (`MoneyFrame.lua`
|
||||
//! l.202), which is why [`UiScript::set_digit_advances`](super::UiScript::set_digit_advances)
|
||||
//! l.202), which is why `UiScript::set_digit_advances`
|
||||
//! existed: a digits-only stand-in for exactly this, built because the general answer was missing.
|
||||
//! This is the general answer, and it retires that special case.
|
||||
//!
|
||||
@@ -106,7 +106,7 @@ impl super::UiScript {
|
||||
///
|
||||
/// Call again to replace it — which the host must do whenever its **raster environment**
|
||||
/// changes (window resize, `uiScale`), for the same reason
|
||||
/// [`UiScript::forget_text_metrics`](super::UiScript::forget_text_metrics) exists: glyph
|
||||
/// `UiScript::forget_text_metrics` exists: glyph
|
||||
/// advances step to whole *physical* pixels, so a measurer built under one seam does not
|
||||
/// answer for another.
|
||||
pub fn set_text_measurer(&mut self, measurer: Box<dyn TextMeasure>) {
|
||||
|
||||
@@ -427,7 +427,7 @@ pub(crate) struct Model {
|
||||
pub(crate) target_nearest_friend_requests: Vec<bool>,
|
||||
/// `(name, exactMatch)` pairs `TargetByName` queued since the app's last
|
||||
/// [`UiScript::take_target_by_name_requests`] drain — the by-NAME twin of
|
||||
/// [`Self::target_requests`], which takes unit tokens. The app runs the shared by-name
|
||||
/// `Self::target_requests`, which takes unit tokens. The app runs the shared by-name
|
||||
/// resolver (`crate::target::by_name`, decision 0886) and commits the selection ([`unit`]).
|
||||
pub(crate) target_by_name_requests: Vec<(String, bool)>,
|
||||
/// Set when `ClearTarget()` fired with a live target — the ESC chain's LAST leg
|
||||
|
||||
@@ -444,14 +444,52 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetSpellTabInfo(i) -> name, texture, offset, numSpells (the Era flat tuple); 1-based `i`,
|
||||
// out of range -> a single nil (GetMerchantItemInfo's own out-of-range shape).
|
||||
// GetSpellTabInfo(i) -> name, texture, offset, numSpells. **FOUR values on every path that
|
||||
// returns** — `0x4b3ce0` has two live `ret`s and both `mov eax,4`; the `eax=0` one at `0x4b3d0d`
|
||||
// is dead code after `luaL_error` longjmps. Byte-carved by a wow-re cross-check (decision 1931,
|
||||
// `system/ui/scratch/spelltabinfo-return-contract.md`).
|
||||
//
|
||||
// **OUT OF RANGE IS `nil, nil, 0, 0` — LITERAL zeros, not a single nil.** This comment used to say
|
||||
// "out of range -> a single nil (GetMerchantItemInfo's own out-of-range shape)", i.e. one
|
||||
// binding's shape copied onto another on the assumption that they generalise. They do not
|
||||
// (1919 proved it for GetSkillLineInfo; this is the same bug one verb over). Leg `0x4b3e12`
|
||||
// pushes nil, nil, then `push 0; push 0; lua_pushnumber` twice — `0x6f3810` copies its two
|
||||
// stack dwords VERBATIM, so that is exactly +0.0, not a stale offset and nothing off the array
|
||||
// base. Index 0, a negative, past-the-count and the empty/no-player registry all reach it via
|
||||
// ONE unsigned branch (`0x4b3d26 dec ebx / cmp ebx,eax / jae`).
|
||||
//
|
||||
// Why the zeros are load-bearing rather than cosmetic: `SpellBookFrame_OnLoad` runs
|
||||
// `SpellBookSkillLineTab_OnClick(1)` -> `GetSpellTabInfo(1)` UNCONDITIONALLY at load, and
|
||||
// `SpellBookFrame_Update` can pass `SpellBookFrame:GetID()`, which is 0. The stock file then
|
||||
// does `id > (offset + numSpells)` unguarded (l.295) — a nil there raises, and `(0+0)` makes it
|
||||
// true, so the reference HIDES EVERY BUTTON ON THE PAGE. That is the behaviour to match.
|
||||
//
|
||||
// **The argument RAISES when absent or not number-coercible** — `luaL_error(L, "Usage:
|
||||
// GetSpellTabInfo(index)")`, a numeric string accepted. [`number_arg`] is that exact shape
|
||||
// (coerce, truncate toward zero, raise the usage string), and the decrement happens AFTER it.
|
||||
// Do NOT share `GetSpellName`'s marshaller: `0x4b3ec0` does `fsub 1.0` BEFORE truncating, and
|
||||
// the two disagree below 1 (`0.5` is out-of-range here and index 0 there).
|
||||
//
|
||||
// Slot 1 is also nil on an IN-RANGE tab whose skillLineId has no DBC row (`0x4b3dbd`), and
|
||||
// `(string?, nil, num, num)` is live in shipped data (SkillLine.dbc 733/753/754 have
|
||||
// spellIconID 0) — both already the shape the in-range arm below produces.
|
||||
g.set(
|
||||
"GetSpellTabInfo",
|
||||
lua.create_function(|lua, i: usize| {
|
||||
lua.create_function(|lua, i: Value| {
|
||||
let i = super::binding_abi::number_arg(lua, i, "Usage: GetSpellTabInfo(index)")?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let Some(tab) = i.checked_sub(1).and_then(|n| model.spellbook.tabs.get(n)) else {
|
||||
return Ok(MultiValue::from_vec(vec![Value::Nil]));
|
||||
// Truncate (done), THEN decrement, then the unsigned compare: a negative or zero
|
||||
// index lands below 0 and `usize::try_from` refuses it — the `jae` leg.
|
||||
let tab = usize::try_from(i64::from(i) - 1)
|
||||
.ok()
|
||||
.and_then(|n| model.spellbook.tabs.get(n));
|
||||
let Some(tab) = tab else {
|
||||
return Ok(MultiValue::from_vec(vec![
|
||||
Value::Nil,
|
||||
Value::Nil,
|
||||
Value::Integer(0),
|
||||
Value::Integer(0),
|
||||
]));
|
||||
};
|
||||
let texture = match &tab.texture {
|
||||
Some(t) => Value::String(lua.create_string(t)?),
|
||||
@@ -929,7 +967,11 @@ mod tests {
|
||||
fn tab_info_shapes_and_book_id_offsets() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
assert_eq!(s.eval::<i64>("return GetNumSpellTabs()").unwrap(), 0);
|
||||
assert!(s.eval::<bool>("return GetSpellTabInfo(1) == nil").unwrap());
|
||||
// `== nil` reads true either way — Lua compares only the first value. The ARITY is the
|
||||
// load-bearing half and lives in `out_of_range_answers_four_values` below (1931).
|
||||
assert!(s
|
||||
.eval::<bool>("return (GetSpellTabInfo(1)) == nil")
|
||||
.unwrap());
|
||||
|
||||
s.set_spellbook(book());
|
||||
assert_eq!(s.eval::<i64>("return GetNumSpellTabs()").unwrap(), 2);
|
||||
@@ -1485,4 +1527,59 @@ mod tests {
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/// **Out of range answers `nil, nil, 0, 0` — four values, the last two NUMBERS** (1931). The
|
||||
/// count is the assertion: `GetSpellTabInfo(0) == nil` is true either way, which is exactly how
|
||||
/// the old one-nil shape passed. Stock `SpellBookFrame.lua:295` does `id > (offset + numSpells)`
|
||||
/// unguarded straight off `SpellBookFrame_OnLoad`, so slots 3+4 nil is a raise on load, and
|
||||
/// `(0+0)` is what makes the reference hide every button — the behaviour this pins.
|
||||
#[test]
|
||||
fn out_of_range_answers_four_values() {
|
||||
let s = UiScript::new().unwrap();
|
||||
for idx in ["0", "1", "99", "-1", "0.5"] {
|
||||
assert_eq!(
|
||||
s.eval::<i64>(&format!("return select('#', GetSpellTabInfo({idx}))"))
|
||||
.unwrap(),
|
||||
4,
|
||||
"GetSpellTabInfo({idx}) must answer four values"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>(&format!(
|
||||
"local _,_,o,n = GetSpellTabInfo({idx}) return o + n"
|
||||
))
|
||||
.unwrap(),
|
||||
0,
|
||||
"GetSpellTabInfo({idx}) slots 3+4 must be NUMBERS summing to 0"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>(&format!("return (GetSpellTabInfo({idx})) == nil"))
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
/// The argument is shape A (1931): absent or non-numeric RAISES the reference's own usage
|
||||
/// string; a numeric STRING is coerced; a negative is truncated and falls to the fallback
|
||||
/// rather than raising. `-1` and `0.5` are covered by the test above.
|
||||
#[test]
|
||||
fn tab_index_raises_when_absent_and_coerces_a_numeric_string() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
for bad in [
|
||||
"GetSpellTabInfo()",
|
||||
"GetSpellTabInfo(nil)",
|
||||
"GetSpellTabInfo({})",
|
||||
"GetSpellTabInfo('abc')",
|
||||
] {
|
||||
let err = s.run(bad).expect_err(bad);
|
||||
assert!(
|
||||
format!("{err}").contains("Usage: GetSpellTabInfo(index)"),
|
||||
"{bad}: got {err}"
|
||||
);
|
||||
}
|
||||
s.set_spellbook(book());
|
||||
// "1" coerces exactly as 1 does — same name comes back.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return (GetSpellTabInfo('1'))").unwrap(),
|
||||
s.eval::<String>("return (GetSpellTabInfo(1))").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ impl super::UiScript {
|
||||
/// one GOLD line (the reference's engine SetText gold; a cross-interior dot renders FAINT
|
||||
/// gold — the byte law's `|cffb0b0b0` wrap modulating the gold base, director-matched),
|
||||
/// seated centred ABOVE the cursor: the tooltip's BOTTOM at the given UI-space point. The
|
||||
/// plate FOLLOWS the pointer — [`Self::minimap_tooltip_move`] re-seats it as the cursor
|
||||
/// plate FOLLOWS the pointer — `Self::minimap_tooltip_move` re-seats it as the cursor
|
||||
/// drifts within one blip. Same world-owned fade lifecycle as the mouseover tooltip:
|
||||
/// hover loss arms [`Self::world_tooltip_fade`].
|
||||
pub fn minimap_tooltip(&mut self, text: &str, ui_x: f32, ui_y: f32, grey: bool) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user