This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
464 lines
26 KiB
Rust
464 lines
26 KiB
Rust
//! [`UiScript::extract`], the render list: the visible-tree [`crate::order::traversal`] zipped
|
||
//! with the resolved rects and region visuals, in the client's painter order.
|
||
|
||
use crate::layout::Rect;
|
||
use crate::order::{self, ZTarget};
|
||
use crate::widget::FrameHandle;
|
||
|
||
use super::clip::{effective_clip, scroll_clip_sources};
|
||
use super::{colorselect, slider, ExtractedQuad, FontObject, QuadContent, TexCoords, UiScript};
|
||
|
||
impl UiScript {
|
||
/// Every live draw target, visible or not: each frame's slot then its regions, in arena order.
|
||
/// Handles are generational, so a diff across a load names exactly what the load created.
|
||
pub fn live_targets(&self) -> Vec<ZTarget> {
|
||
let model = self.model_ref();
|
||
let mut out = Vec::new();
|
||
for (fh, frame) in model.arena.iter_frames() {
|
||
out.push(ZTarget::Frame(fh));
|
||
out.extend(frame.regions.iter().copied().map(ZTarget::Region));
|
||
}
|
||
out
|
||
}
|
||
|
||
/// The frame a draw target belongs to: itself for a frame slot, the owner for a region.
|
||
pub fn target_frame(&self, target: ZTarget) -> Option<FrameHandle> {
|
||
match target {
|
||
ZTarget::Frame(fh) => self.model_ref().arena.frame(fh).map(|_| fh),
|
||
ZTarget::Region(rh) => self.model_ref().arena.region(rh).map(|r| r.owner),
|
||
}
|
||
}
|
||
|
||
/// A frame's parent, or `None` for a top-level frame or a stale handle.
|
||
pub fn frame_parent(&self, frame: FrameHandle) -> Option<FrameHandle> {
|
||
self.model_ref().arena.frame(frame)?.parent
|
||
}
|
||
|
||
/// A frame's name, or `None` for an anonymous frame or a stale handle.
|
||
pub fn frame_name(&self, frame: FrameHandle) -> Option<String> {
|
||
self.model_ref().arena.frame(frame)?.name.clone()
|
||
}
|
||
|
||
/// The name of the nearest named frame at or above `target`.
|
||
pub fn target_owner_name(&self, target: ZTarget) -> Option<String> {
|
||
let model = self.model_ref();
|
||
let mut frame = match target {
|
||
ZTarget::Frame(fh) => Some(fh),
|
||
ZTarget::Region(rh) => Some(model.arena.region(rh)?.owner),
|
||
};
|
||
while let Some(fh) = frame {
|
||
let f = model.arena.frame(fh)?;
|
||
if let Some(name) = &f.name {
|
||
return Some(name.clone());
|
||
}
|
||
frame = f.parent;
|
||
}
|
||
None
|
||
}
|
||
|
||
/// The render list in painter order, ascending by `ZKey`; call [`UiScript::resolve`] first.
|
||
pub fn extract(&self) -> Vec<ExtractedQuad> {
|
||
let model = self.model_ref();
|
||
let list = order::traversal(&model.arena);
|
||
let mut out = Vec::with_capacity(list.len());
|
||
// Scroll child → its ScrollFrame's rect; a quad clips to every ancestor ScrollFrame's.
|
||
let scroll_sources = scroll_clip_sources(&model);
|
||
for &(target, zkey) in list.iter() {
|
||
let (rect, alpha, content, clip, scale) = match target {
|
||
ZTarget::Frame(fh) => {
|
||
let frame = model.arena.frame(fh);
|
||
let alpha = frame.map(|f| f.effective_alpha).unwrap_or(1.0);
|
||
let scale = frame.map(|f| f.effective_scale).unwrap_or(1.0);
|
||
let clip = effective_clip(&model, &scroll_sources, fh);
|
||
let content = match frame.map(|f| &f.kind_state) {
|
||
Some(crate::widget::KindState::Minimap(m)) => QuadContent::Minimap {
|
||
zoom: m.zoom,
|
||
inside_zoom: m.inside_zoom,
|
||
// Engine child 9 is `minimapPlayerModel`; its scale divided by the
|
||
// map's own leaves what the addon asked for, once.
|
||
arrow_scale: frame
|
||
.and_then(|f| {
|
||
f.children
|
||
.get(crate::widget::MINIMAP_ENGINE_CHILDREN - 1)
|
||
.and_then(|&c| model.arena.frame(c))
|
||
.map(|arrow| arrow.effective_scale / scale)
|
||
})
|
||
.filter(|s| s.is_finite() && *s > 0.0)
|
||
.unwrap_or(1.0),
|
||
},
|
||
// A `<Model>`/`<PlayerModel>` pane's content hole, named so the app can
|
||
// join it to that window's bake. Both kinds share `KindState::Model`, as
|
||
// the client's `CGCharacterModelBase` extends `CSimpleModel`.
|
||
Some(crate::widget::KindState::Model(m)) => QuadContent::ModelPane {
|
||
handle: fh,
|
||
name: frame.and_then(|f| f.name.clone()),
|
||
model: m.path.clone(),
|
||
skins: m.skins.clone(),
|
||
// Resolved here, where the token grammar lives
|
||
// ([`super::types::UnitPaneSubject`]); a token naming nothing right
|
||
// now leaves the pane with no subject, which empties its booth.
|
||
unit: m.unit.as_deref().and_then(|token| {
|
||
model.unit(token).map(|u| super::types::UnitPaneSubject {
|
||
guid: u.guid,
|
||
camera: m.unit_camera,
|
||
})
|
||
}),
|
||
facing: m.facing,
|
||
model_scale: m.scale,
|
||
position: m.position,
|
||
own_alpha: frame.map_or(1.0, |f| f.alpha),
|
||
icon: m.icon.clone(),
|
||
camera: m.camera,
|
||
light: m.light,
|
||
fog: m.armed_fog(),
|
||
},
|
||
_ => QuadContent::Frame,
|
||
};
|
||
(
|
||
model.resolved.get(&fh).copied(),
|
||
alpha,
|
||
content,
|
||
clip,
|
||
scale,
|
||
)
|
||
}
|
||
ZTarget::Region(rh) => {
|
||
let region = model.arena.region(rh);
|
||
let owner = region.map(|r| r.owner);
|
||
let owner_frame = owner.and_then(|o| model.arena.frame(o));
|
||
// A layer switched off by `Frame:DisableDrawLayer` hides its regions; skipped
|
||
// here so each region keeps its own shown state for when the layer returns.
|
||
if let (Some(r), Some(f)) = (region, owner_frame) {
|
||
if f.disabled_layers & (1 << r.draw_layer.index()) != 0 {
|
||
continue;
|
||
}
|
||
}
|
||
let clip = owner.and_then(|o| effective_clip(&model, &scroll_sources, o));
|
||
let mut rect = owner.and_then(|o| model.resolved.get(&o).copied());
|
||
// A StatusBar's fill has no anchors: `bar_fill_rect` places it.
|
||
let mut bar_fill: Option<&crate::widget::StatusBarState> = None;
|
||
if let Some(crate::widget::KindState::StatusBar(sb)) =
|
||
owner_frame.map(|f| &f.kind_state)
|
||
{
|
||
if sb.bar == Some(rh) {
|
||
rect = rect.map(|r| bar_fill_rect(r, sb));
|
||
bar_fill = Some(sb);
|
||
}
|
||
}
|
||
// A Slider's thumb likewise: at the value fraction along the track.
|
||
let mut thumb_fill = false;
|
||
if let Some(crate::widget::KindState::Slider(sl)) =
|
||
owner_frame.map(|f| &f.kind_state)
|
||
{
|
||
if sl.thumb == Some(rh) {
|
||
// The thumb's own `CSimpleTexture::GetWidth`/`GetHeight`, as the drag
|
||
// reads them (`slider::thumb_extent`), not its authored `<Size>`.
|
||
let tsize = super::region::virtual_span(&model, rh);
|
||
rect = rect
|
||
.map(|r| slider::thumb_rect(r, tsize, sl.vertical, sl.fraction()));
|
||
thumb_fill = true;
|
||
}
|
||
}
|
||
// The colour picker: the wheel and strip resolve like any region and the app
|
||
// paints them; the markers' rects derive from the HSV, inverting the pick.
|
||
let mut color_art: Option<QuadContent> = None;
|
||
let mut color_thumb = false;
|
||
if let Some(crate::widget::KindState::ColorSelect(cs)) =
|
||
owner_frame.map(|f| &f.kind_state)
|
||
{
|
||
let tsize = model.region_data.get(&rh).and_then(|d| d.size);
|
||
if cs.wheel_thumb == Some(rh) {
|
||
rect = cs
|
||
.wheel
|
||
.and_then(|w| model.region_resolved.get(&w).copied())
|
||
.map(|w| colorselect::wheel_thumb_rect(w, tsize, cs.hsv));
|
||
color_thumb = true;
|
||
} else if cs.value_thumb == Some(rh) {
|
||
// The strip anchors it and the wheel scales it: the client's unguarded
|
||
// `[this+0x318]` read.
|
||
let wheel = cs
|
||
.wheel
|
||
.and_then(|w| model.region_resolved.get(&w).copied());
|
||
rect = cs
|
||
.value_strip
|
||
.and_then(|v| model.region_resolved.get(&v).copied())
|
||
.map(|v| colorselect::value_thumb_rect(v, wheel, tsize, cs.hsv));
|
||
color_thumb = true;
|
||
} else if cs.wheel == Some(rh) {
|
||
color_art = Some(QuadContent::ColorWheel);
|
||
} else if cs.value_strip == Some(rh) {
|
||
color_art = Some(QuadContent::ColorValue {
|
||
hue: cs.hsv[0],
|
||
sat: cs.hsv[1],
|
||
});
|
||
}
|
||
}
|
||
// A Button's non-current state textures emit no quad, and its ButtonText takes
|
||
// the current state's font instance (disabled > highlighted > normal).
|
||
let mut state_font: Option<&FontObject> = None;
|
||
let mut state_color: Option<[f32; 4]> = None;
|
||
let mut state_justify: Option<super::JustifyH> = None;
|
||
let mut button_font: Option<&crate::widget::ButtonFont> = None;
|
||
if let Some(crate::widget::KindState::Button(bs)) =
|
||
owner_frame.map(|f| &f.kind_state)
|
||
{
|
||
let hovered = owner.is_some() && model.mouseover == owner;
|
||
// The press is not read here: the state texture latches on the transition
|
||
// (`ButtonState::set_state`). Only the Highlight, unlatched, reads hover.
|
||
if !bs.region_visible(rh, hovered) {
|
||
continue;
|
||
}
|
||
if bs.text == Some(rh) {
|
||
// The label takes one embedded font instance whole (normal `+0x33c`,
|
||
// highlight `+0x3b8`, disabled `+0x434`), so font and colour pair up;
|
||
// a state lacking a font object stays on normal (inferred, not traced).
|
||
// A locked highlight counts: the trade-skill list blanks the highlight
|
||
// texture and locks the selected row (`Blizzard_TradeSkillUI.lua:133`,
|
||
// `:144`), so this swap is its white text.
|
||
let highlighted = hovered || bs.locked_highlight;
|
||
let (name, color, justify) = if !bs.enabled()
|
||
&& bs.disabled_font.is_some()
|
||
{
|
||
(
|
||
bs.disabled_font.as_ref(),
|
||
bs.disabled_color,
|
||
bs.disabled_justify_h,
|
||
)
|
||
} else if bs.enabled() && highlighted && bs.highlight_font.is_some() {
|
||
(
|
||
bs.highlight_font.as_ref(),
|
||
bs.highlight_color,
|
||
bs.highlight_justify_h,
|
||
)
|
||
} else {
|
||
(
|
||
bs.normal_font.as_ref(),
|
||
bs.normal_color,
|
||
bs.normal_justify_h,
|
||
)
|
||
};
|
||
state_font = name.and_then(|n| model.font_object(n));
|
||
button_font = bs.font.as_ref();
|
||
state_color = color;
|
||
state_justify = justify;
|
||
}
|
||
}
|
||
// A title region never draws: `CreateTitleRegion 0x773910` builds a plain
|
||
// Region with no textures, a hit rectangle only.
|
||
if region.map(|r| r.kind) == Some(crate::widget::RegionKind::Title) {
|
||
continue;
|
||
}
|
||
// Hidden (the VisibleRegion bit): no quad; tested before the clone below.
|
||
let data_ref = model.region_data.get(&rh);
|
||
if data_ref.is_some_and(|d| d.hidden) {
|
||
continue;
|
||
}
|
||
// Deviation: the nameplate glow is shown but never drawn, because its additive
|
||
// rim reads as hard edge lines in our linear-blending pipeline; the lit plate
|
||
// brightens its bar instead. It stays a real shown ADD region, as
|
||
// `glow:IsShown()` is the mouseover signal 1.12 nameplate addons read.
|
||
if data_ref.is_some_and(super::nameplate::is_unpainted_glow) {
|
||
continue;
|
||
}
|
||
let mut data = data_ref.cloned().unwrap_or_default();
|
||
// One hop (region combine `0x772180`): the region's alpha times its owner's
|
||
// `effective_alpha`, which `SetAlpha 0x76a690` already carries down the tree.
|
||
let alpha = owner_frame.map(|f| f.effective_alpha).unwrap_or(1.0)
|
||
* data.alpha.unwrap_or(1.0);
|
||
if let Some(fo) = state_font {
|
||
// The font object's paint, behind the severance mask (`font_explicit`) on
|
||
// every axis as in `font::repaint`: an axis the label set itself stays.
|
||
if !data.font_explicit.face {
|
||
data.font_path = fo.font.clone().or(data.font_path);
|
||
}
|
||
if !data.font_explicit.height {
|
||
data.font_height = fo.height.or(data.font_height);
|
||
}
|
||
if !data.font_explicit.shadow {
|
||
data.font_shadow = fo.shadow.or(data.font_shadow);
|
||
}
|
||
if !data.font_explicit.outline {
|
||
data.outline = fo.outline;
|
||
}
|
||
// The mask, not `vertex_color.is_none()`: a label linked to its font object
|
||
// already carries the normal object's colour.
|
||
if !data.font_explicit.color {
|
||
data.vertex_color = fo.color.or(data.vertex_color);
|
||
}
|
||
// The instance's own `justifyH`, else the object's; a hover or disable
|
||
// swaps it here, as the client re-links the label (`0x779810`).
|
||
if !data.font_explicit.justify_h {
|
||
if let Some(j) = state_justify.or(fo.justify_h) {
|
||
data.justify.set_h(j);
|
||
}
|
||
}
|
||
if !data.font_explicit.justify_v {
|
||
if let Some(j) = fo.justify_v {
|
||
data.justify.set_v(j);
|
||
}
|
||
}
|
||
}
|
||
// `Button:SetFont` sits between: a local set on the embedded font outranks its
|
||
// object (clearing the `inheritMask` bit, `CSimpleFontString+0xD4`, for good)
|
||
// but loses to the label's own, so `font_explicit` gates it too.
|
||
if let Some(bf) = button_font {
|
||
if !data.font_explicit.face {
|
||
data.font_path = Some(bf.path.clone());
|
||
}
|
||
if !data.font_explicit.height {
|
||
data.font_height = Some(bf.height);
|
||
}
|
||
if !data.font_explicit.outline {
|
||
data.outline = super::Outline::flags(&bf.flags);
|
||
}
|
||
}
|
||
// The state colour repaints the label outright, even over its own colour.
|
||
if let Some(c) = state_color {
|
||
data.vertex_color = Some(c);
|
||
}
|
||
// A region draws at its resolved rect only: drawable regions carry anchors
|
||
// (authored, or `region::implicit_creation_anchor`) and a failed resolve
|
||
// latches unresolvable (`0x768d55`), so an unanchored Lua region draws nowhere,
|
||
// as in the reference. The bar fill and thumbs keep their fraction geometry.
|
||
if let Some(sb) = bar_fill {
|
||
data.tex_coords = Some(bar_fill_uv(data.tex_coords, sb));
|
||
}
|
||
if bar_fill.is_none() && !thumb_fill && !color_thumb {
|
||
rect = model.region_resolved.get(&rh).copied();
|
||
}
|
||
let is_text = matches!(
|
||
region.map(|r| r.kind),
|
||
Some(crate::widget::RegionKind::FontString)
|
||
);
|
||
// Generated art fills only an empty slot (`ColorPickerFrame.xml` sets none).
|
||
let content = if let Some(art) =
|
||
color_art.filter(|_| data.texture.is_none() && data.fill.is_none())
|
||
{
|
||
art
|
||
} else if is_text {
|
||
QuadContent::Text {
|
||
text: data.text,
|
||
color: data.vertex_color,
|
||
// The gx translator's answer, not the getter's: a cleared axis draws
|
||
// CENTER/MIDDLE (`0x44d420`), where `GetJustifyH` says "UNKNOWN".
|
||
justify_h: data.justify.paint_h(),
|
||
justify_v: data.justify.paint_v(),
|
||
font: data.font_path,
|
||
font_height: data.font_height,
|
||
text_height: data.text_height,
|
||
shadow: data.font_shadow,
|
||
outline: data.outline,
|
||
alpha_gradient: data.alpha_gradient,
|
||
// The owner's property: an addon's strings on a plate slide with it.
|
||
world_seat: owner
|
||
.is_some_and(|o| super::nameplate::is_world_seated(&model, o)),
|
||
}
|
||
} else {
|
||
// The draw gate is the texture slot, never the colour (`0x7706e0`: empty
|
||
// `+0xcc` emits nothing); a vertex colour alone is a tint. A gradient fills
|
||
// the texture slot, so it draws, at its midpoint since a quad has one tint.
|
||
let fill = data.fill.or_else(|| data.gradient.map(|g| g.midpoint()));
|
||
let has_path = data.texture.is_some();
|
||
let has_texture = has_path || fill.is_some();
|
||
QuadContent::Texture {
|
||
path: data.texture,
|
||
color: has_texture
|
||
.then(|| texture_color(fill, data.vertex_color))
|
||
.flatten(),
|
||
// The renderer acts on ADD only: the other four `alphaMode` values
|
||
// answer `GetBlendMode` but draw as straight alpha.
|
||
additive: data.blend == crate::script::BlendMode::Add,
|
||
tex_coords: data.tex_coords,
|
||
circular: data.circular,
|
||
portrait_unit: data.portrait_unit,
|
||
rotation: data.rotation,
|
||
// Real art only: a solid here is a tint on a white texel, where the
|
||
// reference's is a texel block it would grey. No stock caller
|
||
// desaturates a solid.
|
||
desaturated: data.desaturated && has_path,
|
||
}
|
||
};
|
||
// A region draws at its owner's scale: only frames carry one (`0x76ac90`).
|
||
let scale = owner_frame.map(|f| f.effective_scale).unwrap_or(1.0);
|
||
(rect, alpha, content, clip, scale)
|
||
}
|
||
};
|
||
// A `Model` frame's scene draws in its bucket's ARTWORK batch, last: `0x76fb00` drains
|
||
// quads, text, then render callbacks, and `0x76d160` registers the model's callback
|
||
// for layer 2 only (`0x76d17f cmp ebx,2`). So the world map's player arrow draws over
|
||
// the zone overlays at the same level, below its own OVERLAY and HIGHLIGHT regions.
|
||
let z = match &content {
|
||
QuadContent::ModelPane { .. } => zkey.callback(order::DrawLayer::Artwork).raw(),
|
||
_ => zkey.raw(),
|
||
};
|
||
out.push(ExtractedQuad {
|
||
target,
|
||
z,
|
||
rect,
|
||
alpha,
|
||
content,
|
||
clip,
|
||
scale,
|
||
});
|
||
// A ScrollingMessageFrame adds one Text quad per visible ring line, stacked bottom-up
|
||
// with each line's fade alpha; the ring lives in its kind state, not in FontStrings.
|
||
if let (ZTarget::Frame(fh), Some(fr)) = (target, rect) {
|
||
// The Backdrop draws at the frame slot; the lines take the ARTWORK content key,
|
||
// where the client's message strings live, over the BACKGROUND (hover box) art.
|
||
let paint = super::layout::FramePaint { alpha, scale };
|
||
Self::emit_backdrop(&model, fh, fr, zkey.raw(), paint, clip, &mut out);
|
||
Self::emit_message_lines(
|
||
&model,
|
||
fh,
|
||
fr,
|
||
zkey.content(order::DrawLayer::Artwork).raw(),
|
||
paint,
|
||
clip,
|
||
&mut out,
|
||
);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
}
|
||
|
||
/// The colour a Texture region draws with: texel × vertex colour per channel, alpha included (the
|
||
/// stage-0 combine preset at submit, `.data 0x85c250` index 1: `MODULATE(TEXTURE, DIFFUSE)`). A
|
||
/// desaturated region's pixel shader (`+0x128`) reads only the vertex alpha. `fill`, the region's
|
||
/// solid colour, is a real 8×8 texel block in the client, so where set the product is the drawn
|
||
/// colour; otherwise this is the tint the renderer modulates the art by, `None` for untinted.
|
||
fn texture_color(fill: Option<[f32; 4]>, vertex: Option<[f32; 4]>) -> Option<[f32; 4]> {
|
||
match (fill, vertex) {
|
||
(Some(f), Some(v)) => Some([f[0] * v[0], f[1] * v[1], f[2] * v[2], f[3] * v[3]]),
|
||
(Some(c), None) | (None, Some(c)) => Some(c),
|
||
(None, None) => None,
|
||
}
|
||
}
|
||
|
||
/// A StatusBar's fill rect: the frame rect scaled by the value fraction, rightward from the left
|
||
/// edge or upward from the bottom (1.12 has no reverse fill).
|
||
fn bar_fill_rect(r: Rect, sb: &crate::widget::StatusBarState) -> Rect {
|
||
let f = sb.fraction();
|
||
if sb.vertical {
|
||
Rect::new(r.bottom, r.left, r.bottom + r.height() * f, r.right)
|
||
} else {
|
||
Rect::new(r.bottom, r.left, r.top, r.left + r.width() * f)
|
||
}
|
||
}
|
||
|
||
/// A StatusBar's fill UVs: `base` (its `<TexCoords>`/`SetTexCoord`, or the full texture) cut to
|
||
/// the value fraction along the fill axis, `[left, right, top, bottom]` in 0..1 from top-left. The
|
||
/// client crops rather than scales: `SetValue` (`0x7cc450` → `0x7833c0`) drives `0x770410`, which
|
||
/// writes the 4-corner UV block (`+0x104..+0x120`) with `u1` set to the fill fraction. Vertical
|
||
/// mirrors it up `v`, pinning the art's bottom edge (inferred, not traced).
|
||
fn bar_fill_uv(base: Option<TexCoords>, sb: &crate::widget::StatusBarState) -> TexCoords {
|
||
// An affine base contributes its bounding edges (no live StatusBar uses one).
|
||
let [l, r, t, b] = base.map(|tc| tc.edges()).unwrap_or([0.0, 1.0, 0.0, 1.0]);
|
||
let f = sb.fraction();
|
||
TexCoords::Rect(if sb.vertical {
|
||
[l, r, b - (b - t) * f, b]
|
||
} else {
|
||
[l, l + (r - l) * f, t, b]
|
||
})
|
||
}
|