superwow: the chat bubbles are widgets — CREATE_CHATBUBBLE and pfUI's skin
The last real SuperWoW feature, and one piece of work closed three things: `CREATE_CHATBUBBLE` (which could not honestly be fired, because its payload can only be the bubble), `C_ChatBubbles.GetAllChatBubbles` (which answered an empty list, truthfully, for the same reason) and pfUI's whole bubble skin, which walks that list. `benilla-ui`'s `script/chat_bubble.rs` is a pool of real frames — anonymous WorldFrame children with a backdrop (`0x4b0a35` IS `SetBackdrop`), a tail texture and a FontString, which is what the reference's own `CGChatBubbleFrame` is and how addons of the era found bubbles before SuperWoW published the event. The app keeps everything a bubble's existence depends on — the spawn gate, the lifetime, the fade, the range re-test, the stacking sort and the text measurement that sizes the frame — and hands it over as `BubbleState`: the same split `vplates.rs` already makes for the plates, so the engine decides what is on screen, the widget layer decides what Lua sees, and neither can disagree about a bubble because there is one feed. Three things improved by the move rather than being preserved by it: - The bubbles sit at frame levels 2, 3, 4… over the V-plates at level 1, which is the reference's own order (`0x4b12d5`-`0x4b1312`). The quad band had them UNDERNEATH — a deviation `overlay_z` carried in a comment. That band is gone and the lane is down to one tenant. - The art is loaded, tiled and bleed-inset by the UI pass like any other frame's, so this file drops its own texture resource and its own half-texel inset helper. - The system moved BEFORE the paint. It sat in `UiQuadAppend`, the window *after* the extract: right for an appender, a frame late for a feed. The test that matters transcribes pfUI's own `ScanBubbles` loop — it finds the FontString with the line and its chat-type colour, blanks the one Texture region and parents its own frame to the bubble — and then checks the EXTRACT for drawn quads: both backdrop halves, the tail, and the text at the em the app measured the box with. A test that only checked the widgets would have passed with nothing on screen. Two architectural gates caught this and both were answered rather than silenced. The one-shot gate: `drive_bubbles` now holds the VM and drains a queue, so it is EXEMPT with the argument — the queue's product is app-side state, the widget sync is idempotent and binds on the first frame there is a WorldFrame, and gating the system instead would pile lines up and spawn them all at once with fresh lifetimes. The VM wall: its ceiling rises by one with the reason, which is that this file is `vplates.rs`'s exact twin. `CREATE_CHATBUBBLE` fires per APPEARANCE, not per pool slot ever built — the opposite of `NAME_PLATE_CREATED`'s rule, because a recycled frame has been re-anchored, re-sized and re-texted since it was last seen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -1,25 +1,21 @@
|
||||
//! ClassicAPI's `C_ChatBubbles`: the speech bubbles an addon wants to restyle.
|
||||
//!
|
||||
//! **Answers an empty list, and that is the truthful answer here, not a stub.** The function's
|
||||
//! contract is "hand me the bubble *frames*", so an addon can walk their regions and repaint
|
||||
//! them — pfUI's does exactly that, replacing each bubble's textures with its own backdrop and
|
||||
//! re-fonting the `FontString` it finds inside (`modules/bubbles.lua:30-48`). This client has no
|
||||
//! such frames to hand over: bubbles are drawn natively by `benilla-app`'s `chat_bubble.rs` as a
|
||||
//! 2-D world overlay, the same way the reference's own `CGChatBubbleFrame` is drawn — they are
|
||||
//! not Lua frames and have no regions a script can reach.
|
||||
//! **Answers the live bubbles.** The function's contract is "hand me the bubble *frames*", so an
|
||||
//! addon can walk their regions and repaint them — pfUI's does exactly that, blanking each
|
||||
//! bubble's textures and re-fonting the `FontString` it finds inside
|
||||
//! (`modules/bubbles.lua:30-48`). This file used to answer an empty list, truthfully, because the
|
||||
//! bubbles were drawn straight into the overlay quad lane and were not Lua frames at all; they
|
||||
//! are now a widget pool (`script/chat_bubble.rs`), which is also what the reference has.
|
||||
//!
|
||||
//! So the honest report is zero, and the consequence is that bubbles render in this client's own
|
||||
//! style rather than pfUI's. The alternative was leaving the namespace absent, which is strictly
|
||||
//! worse: `ipairs(nil)` raises, and the call sits in `ScanBubbles`, which pfUI runs the frame
|
||||
//! after **every** `CHAT_MSG_SAY`/`YELL`/`MONSTER_SAY`/`MONSTER_YELL` — so the absence raised on
|
||||
//! any NPC or player speaking within earshot, repeatedly, for a cosmetic feature.
|
||||
//!
|
||||
//! Making the native bubbles addressable from Lua is the real fix and is not built: it would mean
|
||||
//! giving each live bubble a frame with a texture and a `FontString`, kept in step with the
|
||||
//! native overlay's own spawn, stacking and expiry rules. `docs/ADDON_COMPAT.md` carries the row.
|
||||
//! Pool order, live bubbles only, a fresh table per call. The order is stable for a bubble's whole
|
||||
//! life, which is what an addon keying a cache on the frame needs; it is **not** the stacking
|
||||
//! order, which changes every frame with the camera (`chat_bubble::BubbleState`).
|
||||
|
||||
use mlua::{Lua, Table};
|
||||
|
||||
use crate::script::object::frame_wrapper;
|
||||
use crate::script::Model;
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<Table> {
|
||||
let t = lua.create_table()?;
|
||||
|
||||
@@ -27,7 +23,21 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<Table> {
|
||||
// let a caller's own writes into it survive into the next caller's loop.
|
||||
t.set(
|
||||
"GetAllChatBubbles",
|
||||
lua.create_function(|lua, ()| lua.create_table())?,
|
||||
lua.create_function(|lua, ()| {
|
||||
let live = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
model.chat_bubbles.live()
|
||||
};
|
||||
let out = lua.create_table()?;
|
||||
for (i, frame) in (1..).zip(live) {
|
||||
let id = lua
|
||||
.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.frame_id(frame);
|
||||
out.set(i, frame_wrapper(lua, id)?)?;
|
||||
}
|
||||
Ok(out)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
Ok(t)
|
||||
@@ -37,9 +47,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<Table> {
|
||||
mod tests {
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// The shape the call site needs: `ipairs` over it must run zero times and not raise.
|
||||
/// Nobody talking is an **empty list, not nothing**: `ipairs(nil)` raises, and the call sits
|
||||
/// in pfUI's `ScanBubbles`, which it runs the frame after *every*
|
||||
/// `CHAT_MSG_SAY`/`YELL`/`MONSTER_SAY`/`MONSTER_YELL` — so an absent namespace raised on any
|
||||
/// NPC or player speaking within earshot, repeatedly, for a cosmetic feature. The live case,
|
||||
/// and pfUI's own loop over it, are in `script/chat_bubble.rs`'s tests.
|
||||
#[test]
|
||||
fn it_answers_an_empty_list_rather_than_nothing() {
|
||||
fn a_silent_world_answers_an_empty_list_rather_than_nothing() {
|
||||
let s = UiScript::new().unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(C_ChatBubbles.GetAllChatBubbles())")
|
||||
|
||||
Reference in New Issue
Block a user