- GetInventorySlotInfo folds case the way the client does, and gains the twelve bag-slot rows the shipped data really carries - ChatTypeGroup exists for addons that register their own chat types at file scope — one missing table was taking five addons down - GetChannelList answers in join order, shaped by the addons that consume it, and the harness's `--why` can now open any demand count - the survey seats a spellbook, and the shapeshift bar's shelf art exists hidden, both exposing paths an empty fixture could never reach Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
157 lines
8.2 KiB
Rust
157 lines
8.2 KiB
Rust
//! `GetChannelName` — the joined-channel lookup, in both directions.
|
|
//!
|
|
//! **The state this reads already existed and was built for this verb.** `ui_chat::edit`'s
|
|
//! `ChannelState` has held `joined: Vec<String>` in join order since the chat arc, and its own doc
|
|
//! comment names `GetChannelName(n)` as the law it implements. Only the registration was missing —
|
|
//! the same silent-gap shape the loader arc kept finding, one layer up: the capability was built,
|
|
//! nothing exposed it, and nothing complained.
|
|
//!
|
|
//! Corpus demand, counted by reading every line rather than the grep total (1207): **17 sites
|
|
//! across 6 addons**, every one an unguarded call, no library replicated —
|
|
//! `ChatLog` 7, `Recap` 4, `SmartRes` 2, `Enchantrix` 2, `FuBar_AssistFu` 1, `_LazyPig` 1. Both
|
|
//! lookup directions are live in the corpus: by index (`GetChannelName(i)`) and by name
|
|
//! (`GetChannelName("world")`, `GetChannelName("Trade - City")`).
|
|
//!
|
|
//! Signature verified against wow-5875-re `system/ui/scratch/zone-chat-channel-autojoin.md`
|
|
//! l.374-380 — `GetChannelName = 0x4a05e0`, **three** returns:
|
|
//!
|
|
//! | # | the client's | ours |
|
|
//! |---|---|---|
|
|
//! | 1 | `slot[+0x00]`, the client-local **1-based joined-slot index** (= `CHAT_MSG_*` arg8) | the position in `joined` |
|
|
//! | 2 | `slot[+0x04]`, the channel name | the `joined` entry |
|
|
//! | 3 | `slot[+0x98]`, FrameXML's `instanceID` (= arg10) | **0** — see below |
|
|
//!
|
|
//! **Return 3 is 0, not nil, and that is a recorded position rather than a shrug.** It is the split
|
|
//! index from `YOU_JOINED`'s second u32, which `ui_chat::event`'s doc already records as "0 on every
|
|
//! vanilla emulator" and deliberately does not store. A client that one day meets a server which
|
|
//! splits channels would need the field; nothing in the corpus reads it.
|
|
|
|
use mlua::{Lua, MultiValue, Value};
|
|
|
|
use super::Model;
|
|
|
|
/// The 1-based slot of `name` in join order, case-insensitively — `GetChannelName`'s first return.
|
|
fn slot_of(model: &Model, name: &str) -> Option<usize> {
|
|
model
|
|
.joined_channels
|
|
.iter()
|
|
.position(|c| c.eq_ignore_ascii_case(name))
|
|
.map(|i| i + 1)
|
|
}
|
|
|
|
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
|
let g = lua.globals();
|
|
|
|
// GetChannelName(indexOrName) → slot, name, instanceID.
|
|
//
|
|
// THE TRAP, and the whole risk in this verb: the first return is **always a NUMBER, never
|
|
// nil** — `0` when the channel is not joined. Verified from both sides. The reference's own
|
|
// callers compare it numerically and would raise on a nil: `ChatFrame.lua:2114`
|
|
// `if ( channelNum > 0 )` and `l.2232` `if ( channelNum <= 0 ) then return end`; so does the
|
|
// corpus, at `_LazyPig/LazyPig.lua:1996` `if id > 0 then`. Returning nil here would convert
|
|
// three working call sites into "attempt to compare nil with number".
|
|
//
|
|
// NOT a shared helper with `JoinChannelByName`, whose first return is a DIFFERENT number — the
|
|
// `ChatChannels.dbc` ChannelID, not this local slot index. wow-re calls that pair out
|
|
// explicitly (`zone-chat-channel-autojoin.md` l.379) and it is an easy, silent mistake.
|
|
g.set(
|
|
"GetChannelName",
|
|
lua.create_function(|lua, key: Value| {
|
|
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
|
let slot = match &key {
|
|
// The numeric form. The reference bounds-checks `1 <= n <= count` (`0x49bf30`) and
|
|
// answers only for a CONFIRMED-joined slot; `joined` holds exactly the confirmed
|
|
// ones (`ui_chat::feed` appends on the server's YOU_JOINED, not on the request), so
|
|
// the bound is the whole check here.
|
|
Value::Integer(_) | Value::Number(_) => {
|
|
let n = match &key {
|
|
Value::Integer(i) => *i,
|
|
Value::Number(n) => *n as i64,
|
|
_ => unreachable!(),
|
|
};
|
|
usize::try_from(n)
|
|
.ok()
|
|
.filter(|n| *n >= 1 && *n <= model.joined_channels.len())
|
|
}
|
|
// The name form. A numeric STRING arrives here and must still resolve as a number:
|
|
// `ChatFrame.lua:2113` passes the result of a `gsub` — `GetChannelName("1")` — and
|
|
// Lua's own coercion is what makes that work on the real client.
|
|
Value::String(s) => {
|
|
let name = s.to_str()?;
|
|
match name.trim().parse::<usize>() {
|
|
Ok(n) if n >= 1 && n <= model.joined_channels.len() => Some(n),
|
|
Ok(_) => None,
|
|
Err(_) => slot_of(&model, &name),
|
|
}
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
let Some(slot) = slot else {
|
|
// Not joined: the number 0, and nothing else. `channelName` is unread by every
|
|
// caller on this branch (all four sites bail on the number first).
|
|
return Ok(MultiValue::from_vec(vec![Value::Integer(0)]));
|
|
};
|
|
let name = model.joined_channels[slot - 1].clone();
|
|
Ok(MultiValue::from_vec(vec![
|
|
Value::Integer(slot as i64),
|
|
Value::String(lua.create_string(&name)?),
|
|
// instanceID — see the module doc: 0 on every vanilla emulator, and a number
|
|
// rather than nil so a caller can compare it like the client's.
|
|
Value::Integer(0),
|
|
]))
|
|
})?,
|
|
)?;
|
|
|
|
// GetChannelList() → slot1, name1, slot2, name2, … over every joined channel, in join order.
|
|
//
|
|
// **The shape is settled by two independent consumers, not by a recorded signature** — wow-re
|
|
// has the address (`0x4a02d0`, `scratch/bindings.md` l.152) and no contract:
|
|
//
|
|
// · the reference's own `FCFDropDown_LoadChannels(...)` walks `for i=1, arg.n, 2` and reads
|
|
// `arg[i+1]` as the NAME (FloatingChatFrame.lua l.445-455) — so the pair is (slot, name),
|
|
// in that order, and the caller steps by two;
|
|
// · `ChatLog.lua:424` packs it with `{ GetChannelList() }` and tests
|
|
// `type(value) == "number"` to spot an id — so it is a FLAT vararg, never a table.
|
|
//
|
|
// A third witness pins the flatness harder: `AceComm-2.0.lua:334` unpacks TEN pairs in one
|
|
// statement, `local _,a,_,b,…,j = GetChannelList()`.
|
|
//
|
|
// The slot numbering is [`slot_of`]'s — position in `joined_channels` + 1 — so this verb and
|
|
// `GetChannelName` can never disagree about which channel is 3.
|
|
//
|
|
// Zero joined channels is zero returns, not nil: `{ GetChannelList() }` is then an empty
|
|
// table, which is what every caller above already handles.
|
|
//
|
|
// Demand: 4 addons, and only ONE of them names it in its own source (ChatLog). The other
|
|
// three — FuBar_BGQueueNumber, FuBar_MageFu, FuBar_TankPointsFu — reach it through their
|
|
// embedded AceComm-2.0. That gap between "greps for the name" and "wants the name" is why the
|
|
// survey's own read-back exists (`--why`, d2fcef94) and why a hand grep is not the oracle here.
|
|
g.set(
|
|
"GetChannelList",
|
|
lua.create_function(|lua, ()| {
|
|
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
|
let mut out = Vec::with_capacity(model.joined_channels.len() * 2);
|
|
for (i, name) in model.joined_channels.iter().enumerate() {
|
|
out.push(Value::Integer(i as i64 + 1));
|
|
out.push(Value::String(lua.create_string(name)?));
|
|
}
|
|
Ok(MultiValue::from_vec(out))
|
|
})?,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
impl super::UiScript {
|
|
/// Mirror the app's confirmed-joined channel list, in join order, for [`install`]'s verb.
|
|
///
|
|
/// The `model.party` shape (`party.rs:172`), deliberately, and NOT the `open_chat_requests`
|
|
/// shape the chat-window work used: that one is a QUEUE the app drains (Lua → app), and this is
|
|
/// app state READ BY Lua, which is the opposite direction. `ui_chat::feed` owns both edges that
|
|
/// change it — the server's YOU_JOINED and YOU_LEFT notices — so it pushes here from one place.
|
|
pub fn set_joined_channels(&mut self, joined: Vec<String>) {
|
|
self.model_mut().joined_channels = joined;
|
|
}
|
|
}
|