From 434d514879f8981a5d9fd2ebbbc155d5e28daddb Mon Sep 17 00:00:00 2001 From: grom Date: Sun, 25 Oct 2026 16:11:54 -0600 Subject: [PATCH] =?UTF-8?q?superwow:=20the=20chat=20bubbles=20are=20widget?= =?UTF-8?q?s=20=E2=80=94=20CREATE=5FCHATBUBBLE=20and=20pfUI's=20skin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/chat_bubble.rs | 335 +++----- crates/benilla-app/src/game_plugins.rs | 2 + crates/benilla-app/src/ui_pass.rs | 17 +- .../benilla-app/src/ui_script/reference_ui.rs | 8 + crates/benilla-app/tests/ui_script_wall.rs | 9 +- crates/benilla-ui/src/script/chat_bubble.rs | 715 ++++++++++++++++++ .../script/compat/classic_api/chat_bubbles.rs | 52 +- crates/benilla-ui/src/script/extract.rs | 10 +- crates/benilla-ui/src/script/mod.rs | 2 + crates/benilla-ui/src/script/model.rs | 4 + docs/ADDON_COMPAT.md | 39 +- 11 files changed, 910 insertions(+), 283 deletions(-) create mode 100644 crates/benilla-ui/src/script/chat_bubble.rs diff --git a/crates/benilla-app/src/chat_bubble.rs b/crates/benilla-app/src/chat_bubble.rs index 1ccbf48a..9307bbfd 100644 --- a/crates/benilla-app/src/chat_bubble.rs +++ b/crates/benilla-app/src/chat_bubble.rs @@ -25,17 +25,13 @@ use std::collections::HashMap; use bevy::ecs::entity::EntityHashSet; use bevy::prelude::*; -use benilla_ui::layout::Rect as GxRect; -use benilla_ui::script::{inset_atlas_bleed, pieces, Backdrop, Insets}; -use benilla_ui::script::{JustifyH, JustifyV, Outline}; +use benilla_ui::script::{BubbleState, Outline, UiScript}; use crate::entities::StandBoxHeight; use crate::net::{Embodied, Guid, NetEntity, SelfGuid}; use crate::ui_chat::{default_color, ChatEventKind}; -use crate::ui_pass::{overlay_z, UiQuad, UiQuadAppend, UiQuads, UvRect}; -use crate::ui_text::{layout_text_quads, measure_text, FontSpec, Justify, TextSeat, UiFontAtlas}; +use crate::ui_text::{measure_text, FontSpec, UiFontAtlas}; use crate::vplates::{device_snap, gx_px, plate_basis, text_px, VPlateSet, VPlates}; -use benilla_assets::{AssetSet, WorldAssets}; use benilla_world::view::WorldCamera; /// The two bubble CVars (registrar `0x603280`), booting at the reference's defaults. @@ -90,10 +86,9 @@ impl Default for BubbleConfig { } } -/// The bubble art (`0x4b0940`). -const BG_TEXTURE: &str = "Interface\\Tooltips\\ChatBubble-Background"; -const EDGE_TEXTURE: &str = "Interface\\Tooltips\\ChatBubble-Backdrop"; -const TAIL_TEXTURE: &str = "Interface\\Tooltips\\ChatBubble-Tail"; +// The bubble art (`0x4b0940`) moved with the drawing: it now lives on the widgets +// (`benilla_ui::script::chat_bubble`), where the UI pass resolves and tiles it like any other +// frame's, half-texel bleed inset included. /// The 0.7 yd lift over the anchor height (`[0x7ffd7c]`). const LIFT: f32 = 0.7; @@ -110,20 +105,6 @@ const WRAP_W: f32 = 0.2; /// The border unit (edge size, insets, tail side): 16/1024 of the screen width. const BORDER_FRAC: f32 = 16.0 / 1024.0; -/// Paint order inside one bubble's level: the tail covers the bottom edge piece and the text draws -/// last, as the reference's batch drains textures before font strings (`0x76fb00`). -const Z_BG: u64 = 0; -const Z_EDGE: u64 = 1; -const Z_TAIL: u64 = 2; -const Z_TEXT: u64 = 3; - -/// The z base for the bubble at `rank`, farthest first: the reference's per-frame -/// `SetFrameLevel(2 + i)` walk (`0x4b12d5`-`0x4b1312`), one level per bubble, under the V-plates. -fn level_z(rank: usize) -> u64 { - let level = (rank as u64).min(overlay_z::BUBBLE_MAX_LEVEL); - overlay_z::BUBBLE + level * overlay_z::BUBBLE_STRIDE -} - /// The CVar gating this kind's bubble (`0x608b0d`); `None` for a kind that does not bubble. fn bubble_cvar(kind: ChatEventKind, cfg: &BubbleConfig) -> Option { use ChatEventKind as K; @@ -213,14 +194,6 @@ fn sanitize(text: &str) -> String { out } -/// The bleed inset for one border piece; zero texels (no patch chain) leaves the UVs alone. -fn pieces_inset(uvs: [[f32; 2]; 4], texels: Vec2) -> [[f32; 2]; 4] { - if texels.x <= 0.0 || texels.y <= 0.0 { - return uvs; - } - inset_atlas_bleed(uvs, texels.x, texels.y) -} - /// The frame's bottom-left origin, bottom-center on the seat. Deviation: snapped onto the device /// pixel grid ([`device_snap`]) so the border art blits 1:1; the reference seats it fractionally. fn seat_origin(seat: Vec2, w: f32, scale: f32) -> Vec2 { @@ -292,43 +265,6 @@ struct Bubbles(HashMap); #[derive(Resource, Default)] pub(crate) struct BubblesActive(pub(crate) EntityHashSet); -/// The bubble art, loaded at boot. The edge strip tiles UVs past 1 (repeat); bg and tail clamp. -#[derive(Resource)] -struct BubbleArt { - bg: Handle, - edge: Handle, - tail: Handle, - /// The edge atlas's size in texels, for the half-texel bleed inset. - edge_texels: Vec2, -} - -fn load_bubble_art( - mut commands: Commands, - assets: Option>, - mut images: ResMut>, -) { - let Some(mut assets) = assets else { - return; // no game data (a bare test app); `drive_bubbles` tolerates the missing art - }; - let bg = assets.sprite_texture(BG_TEXTURE, &mut images); - let edge = assets.sprite_texture_tiled(EDGE_TEXTURE, &mut images); - let tail = assets.sprite_texture(TAIL_TEXTURE, &mut images); - let (Some(bg), Some(edge), Some(tail)) = (bg, edge, tail) else { - warn!("chat_bubble: bubble art missing from the patch chain — bubbles will not draw"); - return; - }; - let edge_texels = images.get(&edge).map_or(Vec2::ZERO, |i| { - let s = i.texture_descriptor.size; - Vec2::new(s.width as f32, s.height as f32) - }); - commands.insert_resource(BubbleArt { - bg, - edge, - tail, - edge_texels, - }); -} - /// Every frame: drain the queue through the spawn gate (`0x608ac0`), ramp each bubble's fade /// against its lifetime and the 20 yd re-test (`0x4b0c30`), publish [`BubblesActive`], and draw. #[allow(clippy::type_complexity)] // one Bevy system's full input set @@ -342,8 +278,6 @@ fn drive_bubbles( self_q: Query<&Transform, With>, camera: Query<(&Camera, &Transform), With>, mut atlas: Option>, - mut quads: ResMut, - art: Option>, heights: Query<&StandBoxHeight>, time: Res