Files
benilla-octo/crates/benilla-ui/src/script/statusbar.rs
T
SamandClaude Fable 5 19461d1e0f fishing becomes whole: the bite, the line, the splash and the reel
- the whole loop plays: the line arcs rod tip to bobber, the bite splashes with its own sound, and clicking the bobber reels in
- the verdicts read like 1.12: yellow "your fish got away" lines, the reel-in sound, the fishing loot window
- "You feel rested." prints on the transition, rest states come from the client's own Exhaustion table, and level 60 trades the XP strip for the max-level rail
- the Skills pane lists what the real client lists: the hidden rows, the sort, gray numberless single-rank lines

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:03:16 +07:00

275 lines
11 KiB
Rust

//! The `StatusBar` method surface — the first *per-kind* widget behavior over the kind tag
//! (`CSimpleStatusBar`, factory `0x6eef20`).
//!
//! Grounded in wow-re's byte-verified LoadXML table (RF-28, `rf28-typed-widget-loadxml.md`):
//! a reversed `min > max` pair is swapped, `defaultValue` routes through SetValue, orientation is
//! the shared enum HORIZONTAL=0/VERTICAL=1 (`0x811b00`), the bar texture's layer defaults ARTWORK,
//! and the widget adds the `OnValueChanged` script slot (`+0x32c`). The **fill** mechanism is
//! byte-pinned too (`nameplate-vkey.md`): `SetValue` writes the bar region's 4-corner UV block with
//! `u1 = GetValue()` *and* recomputes `right = left + frac·width`, so the fill is a left-anchored
//! **crop** — the art is sliced, never squeezed. Applied at extract (`bar_fill_rect`/`bar_fill_uv`).
//!
//! The methods live in their own registry table, consulted by the frame `__index` dispatcher only
//! for StatusBar frames — so duck-typing addons (`if frame.SetValue then …`) see `nil` on every
//! other kind, exactly as against the client's per-class method sets.
use mlua::{Lua, Table, Value};
use super::object::{draw_layer_from_str, frame_handle_of};
use super::region::region_wrapper;
use super::{event, Model, RegionData};
use crate::order::DrawLayer;
use crate::widget::{KindState, RegionKind, StatusBarState};
/// Registry key of the StatusBar method table (the MAXCSTACK discipline: Lua-side root, named key).
pub(super) const REG_STATUSBAR_METHODS: &str = "__benilla_statusbar_methods";
/// Run `f` over a frame's StatusBar state under one short write borrow. Errors if `this` is not a
/// live StatusBar (unreachable through the kind dispatcher, but the method table is a plain Lua
/// value — a caller can fish it out and misapply it).
fn with_bar<T>(
lua: &Lua,
this: &Table,
f: impl FnOnce(&mut StatusBarState) -> T,
) -> mlua::Result<T> {
let h = frame_handle_of(lua, this)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let frame = model
.arena
.frame_mut(h)
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
match &mut frame.kind_state {
KindState::StatusBar(sb) => Ok(f(sb)),
_ => Err(mlua::Error::runtime("not a StatusBar")),
}
}
/// Store a (clamped) value; returns `Some(new_value)` if it actually changed — the caller fires
/// `OnValueChanged`. Degenerate range (`max <= min`) pins to `min` (zero-init members).
fn store_value(sb: &mut StatusBarState, v: f32) -> Option<f32> {
let clamped = v.clamp(sb.min, sb.max.max(sb.min));
(clamped != sb.value).then(|| {
sb.value = clamped;
clamped
})
}
/// Get-or-create the bar-fill texture region (`SetStatusBarTexture`/`<BarTexture>`); `layer`
/// re-layers an existing bar. Returns the region's id (for wrapper lookup).
fn ensure_bar(lua: &Lua, this: &Table, layer: Option<DrawLayer>) -> mlua::Result<u32> {
let h = frame_handle_of(lua, this)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let existing = match &model
.arena
.frame(h)
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?
.kind_state
{
KindState::StatusBar(sb) => sb.bar,
_ => return Err(mlua::Error::runtime("not a StatusBar")),
};
let rh = match existing {
Some(rh) => {
if let (Some(l), Some(region)) = (layer, model.arena.region_mut(rh)) {
region.draw_layer = l;
}
rh
}
None => {
let rh = model
.arena
.create_region(
h,
RegionKind::Texture,
layer.unwrap_or(DrawLayer::Artwork),
0,
)
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
model.region_data.insert(rh, RegionData::default());
model.touch_layout(); // a region entered the layout gate's read set (decision 0740)
if let Some(frame) = model.arena.frame_mut(h) {
if let KindState::StatusBar(sb) = &mut frame.kind_state {
sb.bar = Some(rh);
}
}
rh
}
};
Ok(model.region_id(rh))
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let m = lua.create_table()?;
m.set(
"SetMinMaxValues",
lua.create_function(|lua, (this, min, max): (Table, f32, f32)| {
// A reversed pair is swapped (RF-28's LoadXML does exactly this) — one behavior for the
// XML and API paths. The held value re-clamps into the new range; a move fires
// OnValueChanged, as a value change the caller didn't make explicitly is still a change.
let changed = with_bar(lua, &this, |sb| {
(sb.min, sb.max) = if min <= max { (min, max) } else { (max, min) };
store_value(sb, sb.value)
})?;
fire_value_changed(lua, &this, changed)
})?,
)?;
m.set(
"GetMinMaxValues",
lua.create_function(|lua, this: Table| with_bar(lua, &this, |sb| (sb.min, sb.max)))?,
)?;
m.set(
"SetValue",
lua.create_function(|lua, (this, v): (Table, f32)| {
let changed = with_bar(lua, &this, |sb| store_value(sb, v))?;
fire_value_changed(lua, &this, changed)
})?,
)?;
m.set(
"GetValue",
lua.create_function(|lua, this: Table| with_bar(lua, &this, |sb| sb.value))?,
)?;
m.set(
"SetOrientation",
lua.create_function(|lua, (this, o): (Table, String)| {
let vertical = match o.to_ascii_uppercase().as_str() {
"HORIZONTAL" => false,
"VERTICAL" => true,
_ => {
return Err(mlua::Error::runtime(format!(
"SetOrientation: unknown orientation '{o}'"
)))
}
};
with_bar(lua, &this, |sb| sb.vertical = vertical)
})?,
)?;
m.set(
"GetOrientation",
lua.create_function(|lua, this: Table| {
let v = with_bar(lua, &this, |sb| sb.vertical)?;
Ok(if v { "VERTICAL" } else { "HORIZONTAL" }.to_string())
})?,
)?;
// SetStatusBarTexture(path [, drawLayer]) | SetStatusBarTexture(r, g, b [, a]) — the same two
// forms as a region's SetTexture, targeting the bar region (created on first use).
m.set(
"SetStatusBarTexture",
lua.create_function(
|lua, (this, a1, a2, a3, a4): (Table, Value, Value, Value, Value)| {
let layer = match &a2 {
Value::String(s) => s.to_str().ok().and_then(|l| draw_layer_from_str(&l)),
_ => None,
};
let id = ensure_bar(lua, &this, layer)?;
let rh = {
let model = lua.app_data_ref::<Model>().expect("model app_data");
*model.id_to_region.get(&id).expect("bar region id")
};
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let data = model.region_data.entry(rh).or_default();
match &a1 {
Value::String(s) => {
data.texture = Some(s.to_str()?.to_string());
data.fill = None;
}
// A solid bar writes the same slot the path form does — each clears the other.
// `SetStatusBarColor` is the TINT (the fill region's vertex colour) and is a
// separate slot entirely: a bar can carry art AND a colour, and they multiply.
Value::Number(_) | Value::Integer(_) => {
data.texture = None;
data.fill = Some([
num_f32(&a1),
num_f32(&a2),
num_f32(&a3),
match &a4 {
Value::Nil => 1.0,
v => num_f32(v),
},
]);
}
_ => {}
}
Ok(())
},
)?,
)?;
m.set(
"GetStatusBarTexture",
lua.create_function(|lua, this: Table| {
let bar = with_bar(lua, &this, |sb| sb.bar)?;
let id = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
bar.map(|rh| model.region_id(rh))
};
match id {
Some(id) => Ok(Value::Table(region_wrapper(lua, id)?)),
None => Ok(Value::Nil),
}
})?,
)?;
m.set(
"SetStatusBarColor",
lua.create_function(
|lua, (this, r, g, b, a): (Table, f32, f32, f32, Option<f32>)| {
let id = ensure_bar(lua, &this, None)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let rh = *model.id_to_region.get(&id).expect("bar region id");
model.region_data.entry(rh).or_default().vertex_color =
Some([r, g, b, a.unwrap_or(1.0)]);
Ok(())
},
)?,
)?;
m.set(
"GetStatusBarColor",
lua.create_function(|lua, this: Table| {
let bar = with_bar(lua, &this, |sb| sb.bar)?;
let model = lua.app_data_ref::<Model>().expect("model app_data");
let c = bar
.and_then(|rh| model.region_data.get(&rh))
.and_then(|d| d.vertex_color)
.unwrap_or([1.0, 1.0, 1.0, 1.0]);
Ok((c[0], c[1], c[2], c[3]))
})?,
)?;
lua.set_named_registry_value(REG_STATUSBAR_METHODS, m)?;
Ok(())
}
/// Fire `OnValueChanged(self, value)` if `changed` carries the new value (RF-28: the StatusBar's own
/// script slot `+0x32c`). Fired outside any model borrow; errors go to [`Model::errors`].
fn fire_value_changed(lua: &Lua, this: &Table, changed: Option<f32>) -> mlua::Result<()> {
let Some(value) = changed else { return Ok(()) };
let id = {
let h = frame_handle_of(lua, this)?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.frame_id(h)
};
if let Err(e) = event::fire_widget_handler(
lua,
id,
"OnValueChanged",
vec![Value::Number(f64::from(value))],
) {
lua.app_data_mut::<Model>()
.expect("model app_data")
.errors
.push(e.to_string());
}
Ok(())
}
/// A Lua number-ish → f32 (nil/other → 0.0), for the color-form arguments.
fn num_f32(v: &Value) -> f32 {
match v {
Value::Number(n) => *n as f32,
Value::Integer(i) => *i as f32,
_ => 0.0,
}
}