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
162 lines
7.0 KiB
Rust
162 lines
7.0 KiB
Rust
//! A ratchet on how many `benilla-app` files outside `ui_script/` name the Lua VM's `UiScript`:
|
|
//! only the feed layer should know the FrameXML VM exists, and everything else produces model
|
|
//! values.
|
|
//!
|
|
//! A file counts if it contains the identifier `UiScript` anywhere (a type, a `use`, a doc
|
|
//! comment). `CEILING` is the standing count of gameplay files and `SLACK` how far under it the
|
|
//! count may sit, as in `world_api_wall.rs`. `WOW_UISCRIPT_DUMP=1` prints the files.
|
|
//!
|
|
//! Window feeds (`ui_*`), `capture/` probes and test modules are reported; only gameplay files,
|
|
//! the ones that can reach zero, are gated.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// What kind of file names the VM.
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
|
enum Family {
|
|
/// Gameplay code that knows the FrameXML VM exists: the one family this wall gates.
|
|
Game,
|
|
/// `ui_*`, a window's feed: the legitimate caller. Reported, never gated.
|
|
Feed,
|
|
/// `capture/`, a live probe that asks the stock binding on purpose. Reported.
|
|
Probe,
|
|
/// A test module beside the code it tests. Reported.
|
|
Test,
|
|
}
|
|
|
|
fn family(rel: &str) -> Family {
|
|
let file = rel.rsplit('/').next().unwrap_or(rel);
|
|
if rel.starts_with("capture/") {
|
|
Family::Probe
|
|
} else if file == "tests.rs" || file.ends_with("_tests.rs") || rel.contains("/tests/") {
|
|
Family::Test
|
|
} else if rel.starts_with("ui_") {
|
|
Family::Feed
|
|
} else {
|
|
Family::Game
|
|
}
|
|
}
|
|
|
|
/// The standing count of `Game` files; raise it only with the reason written down.
|
|
///
|
|
/// 40 was the count the last time this test passed, 888 commits back (`2af91704`): the chain has
|
|
/// been red here long enough that the wall could no longer notice a newcomer, which is the one
|
|
/// thing it is for. The four files that have joined since, each argued rather than moved:
|
|
///
|
|
/// - `spell/net.rs` names the identifier **only to say it holds none** — three doc comments of the
|
|
/// form "these handlers hold no `UiScript`", explaining why their work is queued instead. The
|
|
/// count is deliberately coarse (it reads doc comments too, see above), so a file that documents
|
|
/// the line is counted as if it crossed it.
|
|
/// - `target/by_name.rs` and `target/scan.rs` hold the VM on purpose: `TargetByName` and the scan
|
|
/// must select **inside** the Lua call, as `0x489d60` does, or an addon's
|
|
/// `prevTarget ~= foundTarget` restore cannot see what it just selected (`bbe2e3aa`, which is
|
|
/// what made unitscan stop stealing the target).
|
|
/// - `debug_panel/mod.rs` and `debug_panel/lighting_controls.rs` drive CVars through
|
|
/// `UiScript::set_cvar_engine`, the same queue Lua's `SetCVar` feeds, so the dev panel and the
|
|
/// interface cannot hold different values for one CVar.
|
|
/// - `chat_bubble.rs` holds it because the chat bubbles **are widgets**, for the same reason
|
|
/// `vplates.rs` — already in this census, and this file's exact twin — holds it for the
|
|
/// V-plates: a bubble's *existence* is world state (the projection, the lifetime, the 20 yd
|
|
/// re-test, the stacking sort) and its *presentation* is a frame pool, and one file knowing both
|
|
/// is what keeps the two from disagreeing about a bubble. Moving it under `ui_` would satisfy
|
|
/// the census and split a single-sourced feed in half; the alternative the wall offers — "feed a
|
|
/// model value instead" — is precisely what it does, [`benilla_ui::script::BubbleState`].
|
|
const CEILING: usize = 44;
|
|
|
|
/// How far under [`CEILING`] the count may sit before the test asks for the ceiling to follow it.
|
|
const SLACK: usize = 4;
|
|
|
|
#[test]
|
|
fn the_vm_is_named_by_no_more_files_than_the_ceiling_says() {
|
|
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
|
let mut files: Vec<(Family, String)> = rs_files(&src)
|
|
.into_iter()
|
|
.filter_map(|p| {
|
|
let rel = p
|
|
.strip_prefix(&src)
|
|
.unwrap_or(&p)
|
|
.to_string_lossy()
|
|
.replace('\\', "/");
|
|
if rel.starts_with("ui_script/") || rel == "ui_script.rs" {
|
|
return None;
|
|
}
|
|
let text = std::fs::read_to_string(&p).ok()?;
|
|
names_ident(&text, "UiScript").then(|| (family(&rel), rel))
|
|
})
|
|
.collect();
|
|
files.sort();
|
|
let count = |f: Family| files.iter().filter(|(fam, _)| *fam == f).count();
|
|
let n = count(Family::Game);
|
|
eprintln!(
|
|
"files outside ui_script/ naming UiScript: {} — {n} game (ceiling {CEILING}, slack \
|
|
{SLACK}), {} feeds, {} probes, {} tests",
|
|
files.len(),
|
|
count(Family::Feed),
|
|
count(Family::Probe),
|
|
count(Family::Test),
|
|
);
|
|
if std::env::var("WOW_UISCRIPT_DUMP").is_ok() {
|
|
for (fam, rel) in &files {
|
|
eprintln!(" {fam:?}\t{rel}");
|
|
}
|
|
}
|
|
assert!(
|
|
n <= CEILING,
|
|
"{n} gameplay files outside ui_script/ and the feeds name UiScript; the ceiling is \
|
|
{CEILING}.\nA new file learned that the FrameXML VM exists. The line is that \
|
|
only the feed layer may know: feed a model value instead, or — if this file genuinely is \
|
|
feed code — it belongs under a `ui_*` module, and a probe under `capture/`. Raise CEILING \
|
|
here only with the reason, the way world_api_wall.rs requires.\nWOW_UISCRIPT_DUMP=1 lists \
|
|
the files by family."
|
|
);
|
|
assert!(
|
|
n + SLACK >= CEILING,
|
|
"only {n} gameplay files name UiScript and the ceiling still says {CEILING}. Lower CEILING \
|
|
to {n} in this file so the next coupling has to earn its place — the ratchet only holds \
|
|
if the number follows the work down."
|
|
);
|
|
}
|
|
|
|
/// Does `text` contain `ident` as a whole identifier (not as part of a longer one)?
|
|
fn names_ident(text: &str, ident: &str) -> bool {
|
|
let is_ident = |c: char| c.is_alphanumeric() || c == '_';
|
|
let mut from = 0;
|
|
while let Some(i) = text[from..].find(ident) {
|
|
let at = from + i;
|
|
let end = at + ident.len();
|
|
let before_ok = at == 0 || !text[..at].chars().next_back().is_some_and(is_ident);
|
|
let after_ok = !text[end..].chars().next().is_some_and(is_ident);
|
|
if before_ok && after_ok {
|
|
return true;
|
|
}
|
|
from = end;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn rs_files(dir: &Path) -> Vec<PathBuf> {
|
|
let mut out = Vec::new();
|
|
let mut stack = vec![dir.to_path_buf()];
|
|
while let Some(d) = stack.pop() {
|
|
for e in std::fs::read_dir(&d).into_iter().flatten().flatten() {
|
|
let p = e.path();
|
|
if p.is_dir() {
|
|
stack.push(p);
|
|
} else if p.extension().is_some_and(|x| x == "rs") {
|
|
out.push(p);
|
|
}
|
|
}
|
|
}
|
|
out.sort();
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn the_identifier_match_is_whole_word() {
|
|
assert!(names_ident("fn f(s: NonSendMut<UiScript>)", "UiScript"));
|
|
assert!(names_ident("UiScript", "UiScript"));
|
|
assert!(!names_ident("UiScriptPlugin only", "UiScript"));
|
|
assert!(!names_ident("MyUiScript", "UiScript"));
|
|
assert!(names_ident("MyUiScript and UiScript", "UiScript"));
|
|
}
|