Try-ons, slash targets, RestartGx and the video verbs apply in the call (#2790)
Several verbs did their work in a later drain: try-ons, the chat input's /target, /assist and /follow, RestartGx, and the engine CVar verbs (SetGamma, the video pairs, SetMultisampleFormat, the minimap zoom, the glue writes). A script's next line read the old state. LoadBindings also switched the current set, so a Character Specific tick then Cancel left the character set live. Reference: a try-on reads the cache and lands or refuses in its call (`0x504540`, `0x504b2d`). RestartGx runs gxRestart synchronously (`0x48dab0` → `0x63ce00`), and SetMultisampleFormat restarts in its call (`0x48c79b`). LoadBindings never writes the current set (`0x4b8249`-`0x4b8252`). Changed: those verbs apply in the call, and the slash commands call the stock verbs. The drains left at the frame each name why; SaveBindings only marks its set until shutdown (`0x4b6310`, `0x4b65e0`). Tests: per verb, a same-chunk read of its effect. Part of #2202. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -367,7 +367,10 @@ fn read_diff(path: &Option<std::path::PathBuf>) -> Option<Vec<(String, Vec<Strin
|
||||
}
|
||||
|
||||
/// Drain the VM's binding requests: `SaveBindings` writes the set's diff; saving the account set
|
||||
/// deletes the character file.
|
||||
/// deletes the character file. Left at the frame, not in the call: the call's own work, the copy
|
||||
/// and the dirty mark (`0x4b8260` → `0x4b6310`), is the VM's and signals nothing (only a copy into
|
||||
/// the live set does, `0x4b6498`), and the reference writes the files at the interface's shutdown
|
||||
/// (`0x490bd0` → `0x4b65e0`), which no script reads.
|
||||
fn drain_binding_requests(
|
||||
mut hosted_vm: crate::script_calls::HostedVm,
|
||||
mut files: ResMut<BindingFiles>,
|
||||
|
||||
@@ -3587,4 +3587,87 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// The engine verbs that write a CVar run its callback inside the call, as `SetCVar` does:
|
||||
/// `SetGamma` (`0x489267`), `SetWorldDetail` (`0x488e51`), a video pair's setter (`0x488f30`)
|
||||
/// and the minimap's `SetZoom` (`0x6da974`) each call `CVar::Set` (`0x63df50`), and
|
||||
/// `RestartGx`'s handler commits the latched rows synchronously (`0x48dab0` → `0x639f60`).
|
||||
/// So each knob has moved, and `GetCVar` reads the committed `gxWindow`, before the chunk's
|
||||
/// next line, with no frame run.
|
||||
#[test]
|
||||
fn the_engine_cvar_verbs_run_their_callbacks_inside_the_call() {
|
||||
let mut app = crate::game_plugins::schedule_tests::headless_client();
|
||||
app.insert_resource(bevy::ecs::error::DefaultErrorHandler(
|
||||
bevy::ecs::error::ignore,
|
||||
));
|
||||
let (tx, _rx) = crossbeam_channel::unbounded();
|
||||
app.insert_resource(crate::net::NetCommands(tx));
|
||||
app.update();
|
||||
app.insert_non_send_resource(UiScript::new().expect("a VM"));
|
||||
// The registry seeds the new VM's mirror.
|
||||
app.update();
|
||||
let window: String = crate::script_calls::with_hosted_vm(app.world_mut(), |s| {
|
||||
s.expect("a VM").eval(
|
||||
r#"
|
||||
CreateFrame("Minimap", "ProbeMinimap", UIParent):SetZoom(2)
|
||||
SetGamma(0.25)
|
||||
SetWorldDetail(0)
|
||||
SetFarclip(500)
|
||||
SetCVar("gxWindow", "1")
|
||||
RestartGx()
|
||||
return GetCVar("gxWindow")
|
||||
"#,
|
||||
)
|
||||
})
|
||||
.expect("the chunk runs");
|
||||
assert_eq!(
|
||||
window, "1",
|
||||
"the restart committed the stage inside its call"
|
||||
);
|
||||
let cvars = res::<Cvars>(&app);
|
||||
assert_eq!(cvars.get("gamma"), Some("0.750000"));
|
||||
assert_eq!(cvars.get("frillDensity"), Some("16"));
|
||||
assert_eq!(cvars.get("farclip"), Some("500.000000"));
|
||||
assert_eq!(cvars.get("minimapZoom"), Some("2"));
|
||||
assert_eq!(cvars.get("gxWindow"), Some("1"));
|
||||
assert_eq!(res::<crate::ui_gamma::DisplayGamma>(&app).0, 0.75);
|
||||
assert_eq!(res::<crate::minimap::MinimapZoom>(&app).outdoor, 2);
|
||||
}
|
||||
|
||||
/// The Video options' Okay sets the resolution, then the multisample format
|
||||
/// (`OptionsFrame.lua:239-240`), with no `RestartGx` when no other row asked one. Each verb
|
||||
/// runs `gxRestart` inside its call (`0x48c0bc`, `0x48c79b`), so both latched changes commit.
|
||||
#[test]
|
||||
fn an_okay_with_a_new_resolution_and_multisample_commits_both() {
|
||||
let mut app = crate::game_plugins::schedule_tests::headless_client();
|
||||
app.insert_resource(bevy::ecs::error::DefaultErrorHandler(
|
||||
bevy::ecs::error::ignore,
|
||||
));
|
||||
let (tx, _rx) = crossbeam_channel::unbounded();
|
||||
app.insert_resource(crate::net::NetCommands(tx));
|
||||
app.insert_resource(benilla_world::view::MsaaFormats {
|
||||
formats: vec![(24, 24, 1), (24, 24, 4)],
|
||||
});
|
||||
app.update();
|
||||
let mut script = UiScript::new().expect("a VM");
|
||||
let size = |width, height| benilla_ui::script::ScreenResolution { width, height };
|
||||
script.set_screen_resolutions(vec![size(800, 600), size(1024, 768)], Some(size(800, 600)));
|
||||
app.insert_non_send_resource(script);
|
||||
app.update();
|
||||
let (resolution, samples): (String, String) =
|
||||
crate::script_calls::with_hosted_vm(app.world_mut(), |s| {
|
||||
s.expect("a VM").eval(
|
||||
r#"
|
||||
SetScreenResolution(2)
|
||||
SetMultisampleFormat(2)
|
||||
return GetCVar("gxResolution"), GetCVar("gxMultisample")
|
||||
"#,
|
||||
)
|
||||
})
|
||||
.expect("the chunk runs");
|
||||
assert_eq!((resolution.as_str(), samples.as_str()), ("1024x768", "4"));
|
||||
let cvars = res::<Cvars>(&app);
|
||||
assert_eq!(cvars.get("gxResolution"), Some("1024x768"));
|
||||
assert_eq!(cvars.get("gxMultisample"), Some("4"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@ impl FollowState {
|
||||
}
|
||||
}
|
||||
|
||||
/// A start-following request: the chat input's `/follow`, or the UI's `FollowUnit`/`FollowByName`.
|
||||
/// A name finds players only; the start gate refuses a creature either way.
|
||||
#[derive(bevy::ecs::message::Message, Clone, Debug)]
|
||||
/// A start-following request: `FollowUnit` or `FollowByName`. A name finds players only; the start
|
||||
/// gate refuses a creature either way.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum FollowRequest {
|
||||
/// `FollowUnit(unit)`; a bare `/follow` is `FollowUnit("target")`.
|
||||
Unit(String),
|
||||
@@ -301,10 +301,9 @@ fn follow_trace_on() -> bool {
|
||||
*ON.get_or_init(|| std::env::var_os("WOW_FOLLOW_TRACE").is_some())
|
||||
}
|
||||
|
||||
/// Register the follow state and its request message.
|
||||
/// Register the follow state.
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.init_resource::<FollowState>()
|
||||
.add_message::<FollowRequest>();
|
||||
app.init_resource::<FollowState>();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -554,6 +554,8 @@ struct Harness {
|
||||
impl Harness {
|
||||
fn new(lua: &str) -> Self {
|
||||
let mut script = UiScript::new().unwrap();
|
||||
// A dressing-room pane's `SetUnit` is a call no host takes here.
|
||||
script.keep_calls();
|
||||
script.set_screen_size(1024.0, 768.0);
|
||||
script.set_unit_guids(&benilla_ui::script::UnitGuids {
|
||||
player: ME,
|
||||
|
||||
@@ -95,7 +95,8 @@ impl ScreenshotState {
|
||||
|
||||
/// Captures each `Screenshot()` one frame after the call. Spawn last frame's asks before draining
|
||||
/// this frame's: that order is the one-frame hold (module doc). While a movie plays the frame pass
|
||||
/// skips its present block, the capture in it (`0x44267c`), so the asks wait for its end.
|
||||
/// skips its present block, the capture in it (`0x44267c`), so the asks wait for its end. Not a
|
||||
/// call's: the verb only arms the request (`0x4893d0`, `0x442a10`), which the frame pass takes.
|
||||
fn ask_for_captures(
|
||||
mut commands: Commands,
|
||||
mut hosted_vm: crate::script_calls::HostedVm,
|
||||
|
||||
@@ -145,6 +145,10 @@ disjoint::disjoint_set! {
|
||||
cvars: crate::cvars::CvarSteps<'static>,
|
||||
/// The camera's five views, which the view verbs seat and save.
|
||||
views: crate::player::camera_view::ViewSteps<'static, 'static>,
|
||||
/// The display restart's commit of the latched rows.
|
||||
gx_restart: crate::video::GxRestartSteps<'static>,
|
||||
/// The dressing room the `DressUpModel` verbs edit.
|
||||
dressup: crate::ui_dressup::DressUpSteps<'static>,
|
||||
asks: engine::RecordAsks<'static, 'static>,
|
||||
/// What the getters read as they run ([`live::LiveReads`]).
|
||||
live: live::LiveReads<'static, 'static>,
|
||||
@@ -926,6 +930,8 @@ impl CallHost for ScriptCallHost<'_, '_> {
|
||||
let writes = appliers.views().apply(request);
|
||||
appliers.cvars().write(script, &writes);
|
||||
}
|
||||
ScriptCall::RestartGx => appliers.gx_restart().restart(script, 1),
|
||||
ScriptCall::DressUp(intent) => appliers.dressup().apply(script, intent),
|
||||
ScriptCall::RecordAsk(benilla_ui::script::RecordAsk::AuctionName { guid, list }) => {
|
||||
appliers.auction().wait_for_name(guid, list);
|
||||
}
|
||||
@@ -2223,6 +2229,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A probe's `/target <name>` runs the stock handler's `TargetByName` (`ChatFrame.lua:748-751`,
|
||||
/// `0x489d60`), which selects inside its call, so the next line the drain takes reads the new
|
||||
/// target. Main wrote the ask for a reader later in the frame, and the next line read the old.
|
||||
#[test]
|
||||
fn a_chat_target_line_selects_before_the_next_line() {
|
||||
let mut f = frame(true);
|
||||
spawn_mob(&mut f, &[]);
|
||||
let world = f.app.world_mut();
|
||||
let mut names = world.resource_mut::<crate::names::NameCache>();
|
||||
names.insert_player(OLD, "Old Mob".into(), None);
|
||||
names.insert_player(MOB, "Red Affinity".into(), None);
|
||||
as_creature(world, MOB);
|
||||
probe_line_world(world);
|
||||
world.insert_resource(crate::ui_chat::commands::SlashCommands::build(
|
||||
|name| match name {
|
||||
"SLASH_TARGET1" => Some("/target".to_string()),
|
||||
"SLASH_SCRIPT1" => Some("/script".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|_| None,
|
||||
));
|
||||
{
|
||||
let mut script = world.non_send_resource_mut::<UiScript>();
|
||||
script.push_chat_input("/target Red Affinity".into());
|
||||
script.push_chat_input("/script NAMED = UnitName(\"target\")".into());
|
||||
}
|
||||
crate::ui_chat::drain_chat_input_once(world);
|
||||
assert_eq!(eval::<String>(&mut f, "return NAMED"), "Red Affinity");
|
||||
assert_eq!(selected(&f), Some(MOB));
|
||||
}
|
||||
|
||||
/// `TargetByName` (`0x489d60`) on a miss leaves the target alone and shows message `0x127`
|
||||
/// (`ERR_UNIT_NOT_FOUND`) for a name given (`0x489dbd`, tested on the raw string's first byte,
|
||||
/// so blanks count) or `0xb8` (`ERR_GENERIC_NO_TARGET`) for an empty one (`0x489dcf`), inside
|
||||
|
||||
@@ -24,7 +24,7 @@ use super::{SoundConfig, SoundOutput};
|
||||
use crate::query_cache::Ask;
|
||||
|
||||
/// Drain the VM's queued `PlaySound` intents into the kit player; drained even with no catalog,
|
||||
/// so the queue cannot grow unbounded.
|
||||
/// so the queue cannot grow unbounded. Left at the frame: no script reads what a play does.
|
||||
fn drain_ui_sounds(
|
||||
// With the volumes, a CVar knob the call host's callbacks write; the drain runs no Lua.
|
||||
mut vm: crate::script_calls::VmWith<Res<'static, SoundConfig>>,
|
||||
|
||||
@@ -974,7 +974,8 @@ fn fade_out_ambience(zone: &mut ZoneAudio, fade_ms: u64) {
|
||||
}
|
||||
|
||||
/// The Lua music slot: this frame's queued verbs, the loop, the slider and the watch.
|
||||
/// Unconditional: the stream is the caller's, not the world soundscape's.
|
||||
/// Unconditional: the stream is the caller's, not the world soundscape's. Left at the frame: no
|
||||
/// script reads what `PlayMusic` or `StopMusic` does.
|
||||
fn lua_music(
|
||||
// With the volumes, a CVar knob the call host's callbacks write; the slot runs no Lua.
|
||||
mut vm: crate::script_calls::VmWith<Res<'static, SoundConfig>>,
|
||||
|
||||
@@ -80,19 +80,6 @@ impl NameSearch {
|
||||
}
|
||||
}
|
||||
|
||||
/// `/target <name>`: resolve and select (`TargetByName`).
|
||||
#[derive(bevy::ecs::message::Message, Clone, Debug)]
|
||||
pub(crate) struct TargetByNameRequest {
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
/// `/assist [name]`: select the basis unit's target (`AssistByName`, or bare
|
||||
/// `AssistUnit("target")`, whose basis is the current selection).
|
||||
#[derive(bevy::ecs::message::Message, Clone, Debug)]
|
||||
pub(crate) struct AssistRequest {
|
||||
pub(crate) name: Option<String>,
|
||||
}
|
||||
|
||||
/// The follow start gate's refusals with lines (`0x60fed0`'s mode-3 chain, after its silent arms)
|
||||
/// as a pure function, its order pinned by test; "we" are the active mover:
|
||||
///
|
||||
@@ -425,7 +412,7 @@ impl PlayerLookup<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `/target <name>` and the Lua `TargetByName(name, exactMatch)` through [`scan::commit_owing`], the
|
||||
/// `TargetByName(name, exactMatch)`, the stock `/target`'s, through [`scan::commit_owing`], the
|
||||
/// stop, select, re-swing path a click takes. `exact` is the Lua second argument, which `0x489d8e`
|
||||
/// reads with default 0 as the resolver's exact-only flag (consumed at `0x493cab`). The pick goes
|
||||
/// to `SetSelection` as it is (`0x489daa`), so a party member out of range is selected by guid. A
|
||||
@@ -467,7 +454,7 @@ fn missing_line(name: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// `AssistByName(name)` (`0x489c40`) and `/assist [name]`: the basis is the player a nonempty name
|
||||
/// `AssistByName(name)` (`0x489c40`), the stock `/assist`'s: the basis is the player a nonempty name
|
||||
/// resolves to (typemask 0x10, mode 0, `0x489c82`), else the selection (`0x489c89`), looked up as a
|
||||
/// unit (`0x489c9b`, typemask 8); [`SelectCommit::assist`] then selects its target. A basis the
|
||||
/// object manager does not hold, a party member out of range included, shows
|
||||
@@ -499,59 +486,6 @@ pub(super) fn assist_named(
|
||||
commit.assist(basis, how);
|
||||
}
|
||||
|
||||
/// What the chat layer's `/target` and `/assist` asks read and write.
|
||||
type ByNameAsk = (
|
||||
ByNameScan<'static, 'static>,
|
||||
SelectCommit<'static, 'static>,
|
||||
Res<'static, crate::ui_action::UiErrorKeys>,
|
||||
);
|
||||
|
||||
/// One by-name ask committed through the one SetSelection path, its signals in place with the
|
||||
/// host lent, as the binding's `SetSelection` signals inside the call (`0x49389c`), then what it
|
||||
/// owes past them.
|
||||
fn ask_by_name(
|
||||
vm: &mut crate::script_calls::VmWith<ByNameAsk>,
|
||||
step: impl FnOnce(&ByNameScan, &mut SelectCommit, &crate::ui_action::UiErrorKeys),
|
||||
) {
|
||||
let select = {
|
||||
let (scan_params, mut commit, errors) = vm.own();
|
||||
let before = super::click::Standing::of(&commit.selection, &commit.seam);
|
||||
let owed = commit.owing(|commit| step(&scan_params, commit, &errors));
|
||||
before.owing_debt(owed)
|
||||
};
|
||||
vm.engine_select(select, |(_, mut commit, _), owed| commit.pay_now(owed));
|
||||
}
|
||||
|
||||
/// Drain the chat layer's `/target <name>` asks.
|
||||
pub(super) fn target_by_name_requests(
|
||||
mut requests: MessageReader<TargetByNameRequest>,
|
||||
mut vm: crate::script_calls::VmWith<ByNameAsk>,
|
||||
) {
|
||||
for request in requests.read() {
|
||||
ask_by_name(&mut vm, |scan_params, commit, errors| {
|
||||
target_named(scan_params, commit, errors, &request.name, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain the chat layer's `/assist [name]` asks.
|
||||
pub(super) fn assist_requests(
|
||||
mut requests: MessageReader<AssistRequest>,
|
||||
mut vm: crate::script_calls::VmWith<ByNameAsk>,
|
||||
) {
|
||||
for request in requests.read() {
|
||||
ask_by_name(&mut vm, |scan_params, commit, errors| {
|
||||
assist_named(
|
||||
scan_params,
|
||||
commit,
|
||||
errors,
|
||||
request.name.as_deref().unwrap_or_default(),
|
||||
"/assist",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// What a follow request comes to at the start gate (`0x60fed0`).
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum FollowStart {
|
||||
@@ -563,8 +497,8 @@ pub(crate) enum FollowStart {
|
||||
Started { name: String },
|
||||
}
|
||||
|
||||
/// The follow's start, from the chat layer's `/follow` and from `FollowUnit` and `FollowByName`
|
||||
/// inside their calls ([`crate::script_calls::ScriptCallHost`]); follow sends nothing on the wire.
|
||||
/// The follow's start, from `FollowUnit` and `FollowByName`, the stock `/follow`'s, inside their
|
||||
/// calls ([`crate::script_calls::ScriptCallHost`]); follow sends nothing on the wire.
|
||||
#[derive(SystemParam)]
|
||||
pub(crate) struct FollowSteps<'w, 's> {
|
||||
scan_params: ByNameScan<'w, 's>,
|
||||
@@ -655,20 +589,6 @@ impl FollowSteps<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain the chat layer's `/follow [name]` into [`crate::player`]: a refusal's line through the
|
||||
/// action feed, the signals from [`crate::ui_follow`]'s transitions.
|
||||
pub(super) fn follow_requests(
|
||||
mut requests: MessageReader<crate::player::FollowRequest>,
|
||||
mut steps: FollowSteps,
|
||||
errors: Res<crate::ui_action::UiErrorKeys>,
|
||||
) {
|
||||
for request in requests.read() {
|
||||
if let FollowStart::Refused(key) = steps.start(request) {
|
||||
errors.push(crate::ui_action::UiError::key(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The cue `SetSelection` plays for a guid whose object it does not find (`0x843328`).
|
||||
const UNHELD_SELECT_SOUND: &str = "igCharacterSelect";
|
||||
|
||||
@@ -840,7 +760,8 @@ impl SelectCommit<'_, '_> {
|
||||
SelectDebt::default()
|
||||
}
|
||||
|
||||
/// Pay all of `debt` now, for a writer whose signals have no listeners to run first.
|
||||
/// Pay all of `debt` now, for a test's writer whose signals have no listeners to run first.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn pay_now(&mut self, mut debt: SelectDebt) {
|
||||
while debt != SelectDebt::default() {
|
||||
debt = self.pay(debt);
|
||||
@@ -1260,7 +1181,6 @@ mod tests {
|
||||
world.init_resource::<crate::ui_action::UiErrorKeys>();
|
||||
world.init_resource::<crate::player::FollowState>();
|
||||
world.init_resource::<crate::player::Approach>();
|
||||
world.init_resource::<Messages<crate::player::FollowRequest>>();
|
||||
let mut names = NameCache::default();
|
||||
names.insert_player(ME, "Me".into(), None);
|
||||
names.insert_player(them.guid, "Them".into(), None);
|
||||
@@ -1320,21 +1240,20 @@ mod tests {
|
||||
selection.guid = target.map(|(_, g)| g);
|
||||
}
|
||||
|
||||
/// One request through the follow drain: the error lines it queued, and whom we follow after.
|
||||
/// One request through the follow's start, as `FollowUnit`'s or `FollowByName`'s call makes
|
||||
/// it: the line its refusal shows, and whom we follow after.
|
||||
fn drain(
|
||||
world: &mut World,
|
||||
request: crate::player::FollowRequest,
|
||||
) -> (Vec<&'static str>, Option<u64>) {
|
||||
world.write_message(request);
|
||||
world
|
||||
.run_system_cached(follow_requests)
|
||||
.expect("the drain runs");
|
||||
let keys = world
|
||||
.resource_mut::<crate::ui_action::UiErrorKeys>()
|
||||
.take()
|
||||
.into_iter()
|
||||
.map(|e| e.key)
|
||||
.collect();
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
let start = world
|
||||
.run_system_once(move |mut steps: FollowSteps| steps.start(&request))
|
||||
.expect("the start runs");
|
||||
let keys = match start {
|
||||
FollowStart::Refused(key) => vec![key],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
(keys, world.resource::<crate::player::FollowState>().guid)
|
||||
}
|
||||
|
||||
|
||||
@@ -938,9 +938,9 @@ fn an_acquired_attacker_signals_its_select_in_place() {
|
||||
assert_eq!(wire_order(&f.rx), ["RandomRoll"]);
|
||||
}
|
||||
|
||||
/// The chat layer's `/target` and `/assist` asks commit through `SetSelection` (`0x493540`),
|
||||
/// which signals inside it (`0x49389c`): the listener's roll goes out behind the select's send,
|
||||
/// in the ask's own step, with no lane to wait for.
|
||||
/// A bare `/assist`'s `AssistByName("")` commits through `SetSelection` (`0x493540`), which
|
||||
/// signals inside it (`0x49389c`): the listener's roll goes out behind the select's send, inside
|
||||
/// the call, with no lane to wait for.
|
||||
#[test]
|
||||
fn a_chat_assist_signals_its_select_in_place() {
|
||||
let mut f = fix(false);
|
||||
@@ -948,13 +948,10 @@ fn a_chat_assist_signals_its_select_in_place() {
|
||||
f.make_unit(mob);
|
||||
f.make_unit_with(old, &[(16, MOB as u32), (17, 0)]);
|
||||
rolling_listener(&mut f);
|
||||
let world = f.app.world_mut();
|
||||
world
|
||||
.resource_mut::<Messages<super::by_name::AssistRequest>>()
|
||||
.write(super::by_name::AssistRequest { name: None });
|
||||
world
|
||||
.run_system_once(super::by_name::assist_requests)
|
||||
.expect("the /assist reader runs");
|
||||
crate::script_calls::with_hosted_vm(f.app.world_mut(), |s| {
|
||||
s.expect("a VM").run(r#"AssistByName("")"#)
|
||||
})
|
||||
.expect("the assist runs");
|
||||
assert_eq!(f.selected(), Some(MOB));
|
||||
assert_eq!(wire_order(&f.rx), ["SetSelection 0x31", "RandomRoll"]);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@ use bevy::ecs::system::RunSystemOnce;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::front_tests::{fix, Fix, ME, MOB, OLD};
|
||||
use super::{by_name, click, cursor_mode, Hovered, PickOcclusion, PressPick};
|
||||
use super::{click, cursor_mode, Hovered, PickOcclusion, PressPick};
|
||||
use crate::net::{ClientCommand, GuidIndex, ObjectStore};
|
||||
use crate::target::TargetByNameRequest;
|
||||
use crate::ui_action::UiErrorKeys;
|
||||
use crate::vplates::PlateClicks;
|
||||
|
||||
@@ -639,45 +638,6 @@ fn a_right_press_with_a_payload_is_consumed_before_the_world_sees_it() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `TargetByName` (`0x489d60`) leaves the target alone on a miss and shows message `0x127`
|
||||
/// (`ERR_UNIT_NOT_FOUND`) for a name given (`0x489dbd`, tested on the raw string's first byte, so
|
||||
/// blanks count) or `0xb8` (`ERR_GENERIC_NO_TARGET`) for an empty one (`0x489dcf`); the chat
|
||||
/// layer's `/target` request is the binding's body. A hit shows nothing. The Lua binding's miss is
|
||||
/// pinned in `script_calls`, whose call host fires the lines inside the call.
|
||||
#[test]
|
||||
fn a_target_name_that_finds_nobody_shows_the_references_line() {
|
||||
let request = |f: &mut Fix, name: &str| {
|
||||
f.app
|
||||
.world_mut()
|
||||
.resource_mut::<Messages<TargetByNameRequest>>()
|
||||
.write(TargetByNameRequest { name: name.into() });
|
||||
f.app
|
||||
.world_mut()
|
||||
.run_system_once(by_name::target_by_name_requests)
|
||||
.expect("the drain runs");
|
||||
};
|
||||
for (name, line) in [
|
||||
("Nobody", "ERR_UNIT_NOT_FOUND"),
|
||||
(" ", "ERR_UNIT_NOT_FOUND"),
|
||||
("", "ERR_GENERIC_NO_TARGET"),
|
||||
] {
|
||||
let mut f = fix(false);
|
||||
request(&mut f, name);
|
||||
assert_eq!(f.errors(), [line], "{name:?}");
|
||||
assert_eq!(f.selected(), Some(OLD), "{name:?}: the target stays");
|
||||
assert!(f.sent().is_empty(), "{name:?}");
|
||||
}
|
||||
|
||||
let mut f = fix(false);
|
||||
f.app
|
||||
.world_mut()
|
||||
.resource_mut::<crate::names::NameCache>()
|
||||
.insert_player(MOB, "Mob".into(), None);
|
||||
request(&mut f, "Mob");
|
||||
assert!(f.errors().is_empty(), "{:?}", f.errors());
|
||||
assert_eq!(f.selected(), Some(MOB));
|
||||
}
|
||||
|
||||
/// Talking to an NPC in reach whose dialog status is 1, unavailable, triggers Unavailable Quest
|
||||
/// Givers (`0x5f0268`-`0x5f0278`) ahead of its service ladder; another status triggers nothing.
|
||||
/// The click's select lit the NPC, which the selection's tutorial ladder then judges
|
||||
|
||||
@@ -93,11 +93,8 @@ pub(crate) use scan::{fire_owed_selects_lane, take_owed_deselects, LastEnemy};
|
||||
pub(crate) use scan::{
|
||||
Acquire, AttackNearestRequest, AttackPick, CreatureTypes, Owed, OwedSelect, OwedSelects,
|
||||
};
|
||||
// The chat layer's by-name asks (`/target`, `/assist`), and the typemask `TargetByName`'s
|
||||
// candidate feed shares.
|
||||
pub(crate) use by_name::{
|
||||
assistable_alive, AssistRequest, Candidacy, NameSearch, PlayerLookup, TargetByNameRequest,
|
||||
};
|
||||
// The typemask `TargetByName`'s candidate feed shares.
|
||||
pub(crate) use by_name::{assistable_alive, Candidacy, NameSearch, PlayerLookup};
|
||||
// The one SetSelection path, for the loot response's select (`0x48f3a0`), and what a selecting
|
||||
// call owes after its signals.
|
||||
pub(crate) use by_name::{SelectCommit, SelectDebt};
|
||||
@@ -328,8 +325,6 @@ impl Plugin for TargetPlugin {
|
||||
.init_resource::<scan::TabHistory>()
|
||||
.init_resource::<scan::LastEnemy>()
|
||||
.add_message::<AttackNearestRequest>()
|
||||
.add_message::<TargetByNameRequest>()
|
||||
.add_message::<AssistRequest>()
|
||||
.add_message::<click::PlateRightClick>()
|
||||
.add_message::<Interact>()
|
||||
.add_systems(
|
||||
@@ -400,14 +395,6 @@ impl Plugin for TargetPlugin {
|
||||
crate::ui_action::drain_arrival_casts,
|
||||
)
|
||||
.chain(),
|
||||
// The chat layer's by-name asks: `/target` and `/assist` commit through
|
||||
// `scan::commit`; `/follow` hands its subject to `crate::player`.
|
||||
(
|
||||
by_name::target_by_name_requests,
|
||||
by_name::assist_requests,
|
||||
by_name::follow_requests,
|
||||
)
|
||||
.chain(),
|
||||
// The melee probe's press, the player's Attack; the TAB keys and ATTACKTARGET
|
||||
// run their bodies' `TargetNearestEnemy` and `AttackTarget` as script calls.
|
||||
crate::ui_action::attack_nearest_probe,
|
||||
|
||||
@@ -9,7 +9,7 @@ use benilla_protocol::messages::{member_status, GroupMemberEntry, PartyMemberSta
|
||||
use benilla_protocol::SessionEvent;
|
||||
|
||||
use super::front_tests::{fix, Fix, ME, OLD};
|
||||
use super::{by_name, scan, Selection};
|
||||
use super::{scan, Selection};
|
||||
use crate::net::{ClientCommand, GuidIndex, ObjectStore};
|
||||
use crate::ui_action::{UiError, UiErrorKeys};
|
||||
use crate::ui_party::{GroupState, GROUPTYPE_RAID};
|
||||
@@ -242,7 +242,7 @@ fn assist_selects_the_bases_target_out_of_range_by_guid() {
|
||||
/// holds (`0x489c0e`); `AssistByName` shows `ERR_UNIT_NOT_FOUND` for a name whose pick is no held
|
||||
/// unit (`0x489d17`), a party member out of range included (`0x489c9b`), and
|
||||
/// `ERR_GENERIC_NO_TARGET` for an empty name with nothing selected (`0x489d2b`). A call's line is
|
||||
/// signalled inside it; the stock `/assist`, which no call carries, queues the same lines.
|
||||
/// signalled inside it, the stock `/assist`'s included.
|
||||
#[test]
|
||||
fn an_assist_that_finds_no_unit_says_so() {
|
||||
let mut f = grouped();
|
||||
@@ -288,33 +288,6 @@ fn an_assist_that_finds_no_unit_says_so() {
|
||||
"an empty name assists the selection"
|
||||
);
|
||||
|
||||
let assist = |f: &mut Fix, name: Option<&str>| {
|
||||
let world = f.app.world_mut();
|
||||
world
|
||||
.resource_mut::<Messages<by_name::AssistRequest>>()
|
||||
.write(by_name::AssistRequest {
|
||||
name: name.map(str::to_string),
|
||||
});
|
||||
world
|
||||
.run_system_once(by_name::assist_requests)
|
||||
.expect("the /assist reader runs");
|
||||
// A one-shot reader starts at the oldest message, as the frame's reader does not.
|
||||
world
|
||||
.resource_mut::<Messages<by_name::AssistRequest>>()
|
||||
.clear();
|
||||
};
|
||||
assist(&mut f, None);
|
||||
assert_eq!(
|
||||
errors(&f),
|
||||
vec![UiError::key("ERR_GENERIC_NO_TARGET")],
|
||||
"a bare /assist"
|
||||
);
|
||||
assist(&mut f, Some("Nobody"));
|
||||
assert_eq!(
|
||||
errors(&f),
|
||||
vec![UiError::key("ERR_UNIT_NOT_FOUND")],
|
||||
"/assist Nobody"
|
||||
);
|
||||
assert_eq!(selection(&f), (None, None), "no miss moves the selection");
|
||||
}
|
||||
|
||||
|
||||
@@ -95,12 +95,9 @@ pub(super) struct ChatProbes<'w, 's> {
|
||||
}
|
||||
|
||||
/// What the drain hands to another subsystem's setter, bundled for the same ceiling. Chat never
|
||||
/// writes [`Selection`]: [`crate::target`]'s resolver commits through the click's path.
|
||||
/// writes [`Selection`]: a line's selection runs the stock verb, whose call commits it.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(super) struct ChatOut<'w> {
|
||||
target: MessageWriter<'w, crate::target::TargetByNameRequest>,
|
||||
assist: MessageWriter<'w, crate::target::AssistRequest>,
|
||||
follow: MessageWriter<'w, crate::player::FollowRequest>,
|
||||
/// `/partytest ping` stores and signals a group member's ping as the wire's handler does.
|
||||
ping: ResMut<'w, crate::minimap::MinimapPing>,
|
||||
/// The red error line by GlobalStrings key. The system's only `UiErrorKeys` access: a second
|
||||
@@ -580,32 +577,38 @@ pub(super) fn drain_chat_input(
|
||||
ParsedChat::Forfeit => {
|
||||
script.duel_call(benilla_ui::script::DuelRequest::Cancel);
|
||||
}
|
||||
// `crate::target`'s resolver (the reference's `0x493aa0`) commits through the one
|
||||
// SetSelection path. A bare `/target` takes `GetSlashCmdTarget`'s fallback: a no-op
|
||||
// re-select of a player target, nothing for a creature.
|
||||
// The stock handler's verb (`ChatFrame.lua:748-751`), `TargetByName` (`0x489d60`),
|
||||
// which selects inside its call, before the next line is taken. A bare `/target`
|
||||
// takes `GetSlashCmdTarget`'s fallback: a no-op re-select of a player target,
|
||||
// nothing for a creature.
|
||||
ParsedChat::Target { name } => {
|
||||
if let Some(name) =
|
||||
name.or_else(|| target_player_name(&selection, &names, &commands))
|
||||
{
|
||||
chat_out
|
||||
.target
|
||||
.write(crate::target::TargetByNameRequest { name });
|
||||
return Some(LineWork::Lua(LineLua::Handler(format!(
|
||||
"TargetByName({})",
|
||||
parse::lua_quoted_string(&name)
|
||||
))));
|
||||
}
|
||||
}
|
||||
// Deviation: a probe's bare `/assist` assists the live selection, where the stock
|
||||
// handler (`ChatFrame.lua:740`) resolves a selected player's name through
|
||||
// `AssistByName`, because a name resolve can land on a different player.
|
||||
// Deviation: a probe's bare `/assist` assists the live selection, `AssistByName("")`
|
||||
// (`0x489c89`), where the stock handler (`ChatFrame.lua:740`) resolves a selected
|
||||
// player's name through `AssistByName`, because a name resolve can land on a
|
||||
// different player.
|
||||
ParsedChat::Assist { name } => {
|
||||
chat_out.assist.write(crate::target::AssistRequest { name });
|
||||
return Some(LineWork::Lua(LineLua::Handler(format!(
|
||||
"AssistByName({})",
|
||||
parse::lua_quoted_string(name.as_deref().unwrap_or_default())
|
||||
))));
|
||||
}
|
||||
// Bare is `FollowUnit("target")`, where the stock handler (`ChatFrame.lua:754`)
|
||||
// first tries a selected player's name; named is `FollowByName(name)` with no exact
|
||||
// flag, so prefix matching.
|
||||
// flag, so prefix matching. Each starts the follow inside its call (`0x60fed0`).
|
||||
ParsedChat::Follow { name } => {
|
||||
chat_out.follow.write(match name {
|
||||
Some(name) => crate::player::FollowRequest::Name { name, exact: false },
|
||||
None => crate::player::FollowRequest::Unit("target".into()),
|
||||
});
|
||||
return Some(LineWork::Lua(LineLua::Handler(match name {
|
||||
Some(name) => format!("FollowByName({})", parse::lua_quoted_string(&name)),
|
||||
None => "FollowUnit(\"target\")".to_string(),
|
||||
})));
|
||||
}
|
||||
// `SlashCmdList["PVP"]` is `TogglePVP()`, the same call.
|
||||
ParsedChat::Pvp => script.toggle_pvp(),
|
||||
|
||||
@@ -638,7 +638,8 @@ fn flush(
|
||||
*file.dirty.get(script) = false;
|
||||
}
|
||||
|
||||
/// The debounced save, and the `AppExit` flush.
|
||||
/// The debounced save, and the `AppExit` flush, at the frame: no script reads the file before the
|
||||
/// next load.
|
||||
fn save_chat_looks(
|
||||
// The slots beside the VM: the save runs no Lua.
|
||||
mut vm: crate::script_calls::VmWith<Res<'static, super::edit::ChannelState>>,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! The dressing-room drain: applies the stock window's `DressUpModel` verbs ([`DressUpIntent`]) in
|
||||
//! call order, since `DressUpItem` can reset and try on in one call, and composes the booth's look
|
||||
//! ([`DressUpPreview`]): the player's own gear with each substitution in its [`equip_slot`], the
|
||||
//! held triple through [`held_lanes`]. A try-on whose template ([`Items::template`]) is cached
|
||||
//! lands in its place in that order; one that is not lands when the record arrives.
|
||||
//! There is no fit check: the reference previews whatever it is handed (`DressUpFrame.lua:2-16`).
|
||||
//! The dressing room: applies the stock window's `DressUpModel` verbs ([`DressUpIntent`]) inside
|
||||
//! their calls ([`DressUpSteps`]), in call order, since `DressUpItem` can reset and try on in one
|
||||
//! call, and composes the booth's look each frame ([`DressUpPreview`]): the player's own gear with
|
||||
//! each substitution in its [`equip_slot`], the held triple through [`held_lanes`]. A try-on whose
|
||||
//! template ([`Items::template`]) is cached at the call lands there, or refuses there; one that is
|
||||
//! not lands when the record arrives. There is no fit check: the reference previews whatever it
|
||||
//! is handed (`DressUpFrame.lua:2-16`).
|
||||
|
||||
use benilla_protocol::CharEnumItem;
|
||||
use benilla_ui::script::DressUpIntent;
|
||||
use benilla_ui::script::{DressUpIntent, UiScript};
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::entities::equip_slot;
|
||||
@@ -107,7 +108,7 @@ fn lanes_coexist(main: u8, off: u8, dual_wield: bool) -> bool {
|
||||
/// The room's substitutions over the player's own gear, resolved and pending.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct DressUpRoom {
|
||||
/// False after `Close`: the booth is empty.
|
||||
/// False after [`Self::close`]: the booth is empty.
|
||||
open: bool,
|
||||
/// `Undress()` has taken off the player's own pieces until the next `Dress`: `0x504490` clears
|
||||
/// component slots `0..0xb` (`0x5044b0`) and detaches and zeroes both held lanes
|
||||
@@ -151,20 +152,24 @@ impl DressUpRoom {
|
||||
None => self.pending.push(item),
|
||||
}
|
||||
}
|
||||
DressUpIntent::Close => {
|
||||
self.open = false;
|
||||
self.bare = false;
|
||||
self.worn = Default::default();
|
||||
self.held.clear();
|
||||
self.pending.clear();
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// One frame: the records that landed for the try-ons still waiting, whose callbacks
|
||||
/// (`0x504ba0`) ran as each arrived, ahead of the verbs called since; then those verbs in call
|
||||
/// order. Returns the `ERR_NOT_EQUIPPABLE` count to show.
|
||||
/// Both panes hidden, so the booth empties. The reference's widget keeps its state while
|
||||
/// hidden, but `DressUpItem` re-issues `SetUnit("player")` on a hidden frame, so the kept
|
||||
/// state is never seen.
|
||||
fn close(&mut self) {
|
||||
self.open = false;
|
||||
self.bare = false;
|
||||
self.worn = Default::default();
|
||||
self.held.clear();
|
||||
self.pending.clear();
|
||||
}
|
||||
|
||||
/// The records that landed for the try-ons still waiting, whose callbacks (`0x504ba0`) ran as
|
||||
/// each arrived, ahead of the verbs called since; then those verbs in call order. Returns the
|
||||
/// `ERR_NOT_EQUIPPABLE` count to show.
|
||||
fn step(
|
||||
&mut self,
|
||||
intents: impl IntoIterator<Item = DressUpIntent>,
|
||||
@@ -227,27 +232,98 @@ fn not_equippable(inventory_type: u32) -> bool {
|
||||
matches!(inventory_type, 0 | 18 | 24 | 27) || inventory_type > 28
|
||||
}
|
||||
|
||||
/// Whether a dressing-room pane is shown: the dressing room's or the auction house's
|
||||
/// `AuctionDressUpFrame`, read off the frames (the stock files carry no hook of ours). A hidden
|
||||
/// pane has no model, and a verb on it does nothing (`0x504596`).
|
||||
fn pane_shown(script: &UiScript) -> bool {
|
||||
script.frame_visible("DressUpFrame") || script.frame_visible("AuctionDressUpFrame")
|
||||
}
|
||||
|
||||
/// Show `refused` `ERR_NOT_EQUIPPABLE` lines (`0x504b2d`, `push 0x13; call 0x496720`), each as
|
||||
/// `DisplayError` shows it, under the call's input events.
|
||||
fn show_refusals(script: &mut UiScript, sink: &mut crate::ui_action::MessageSink, refused: u32) {
|
||||
let input = script.input_events();
|
||||
for _ in 0..refused {
|
||||
if let Some(line) = crate::ui_action::keyed_line(script, "ERR_NOT_EQUIPPABLE") {
|
||||
crate::ui_action::show_now(script, sink, "ui_dressup", line.under(input));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The dressing-room verbs inside their calls ([`benilla_ui::script::ScriptCall::DressUp`]),
|
||||
/// and the frame's turn for the records that landed since.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(crate) struct DressUpSteps<'w> {
|
||||
/// Absent in a harness without the room.
|
||||
room: Option<ResMut<'w, DressUpRoom>>,
|
||||
items: Option<Res<'w, Items>>,
|
||||
commands: Option<Res<'w, NetCommands>>,
|
||||
sink: crate::ui_action::MessageSink<'w>,
|
||||
}
|
||||
|
||||
impl DressUpSteps<'_> {
|
||||
/// One verb inside its call: a try-on looks its record up in the cache as it stands at the
|
||||
/// call (`0x504579`), and a cached one goes on, or refuses, before the verb returns
|
||||
/// (`0x5045dd`-`0x5045ee`, `0x504b2d`). The callbacks of the records that landed since the
|
||||
/// last verb run first, as each ran at its arrival.
|
||||
pub(crate) fn apply(&mut self, script: &mut UiScript, intent: DressUpIntent) {
|
||||
let (Some(room), Some(items), Some(commands)) = (
|
||||
self.room.as_deref_mut(),
|
||||
self.items.as_deref(),
|
||||
self.commands.as_deref(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !pane_shown(script) {
|
||||
return;
|
||||
}
|
||||
let refused = room.step([intent], items, commands);
|
||||
show_refusals(script, &mut self.sink, refused);
|
||||
}
|
||||
|
||||
/// The frame's turn: the room emptied when no pane is shown, then the records that landed for
|
||||
/// the try-ons still waiting, each refusal shown from its callback (`0x504ba0`).
|
||||
fn frame(&mut self, script: &mut UiScript) {
|
||||
let (Some(room), Some(items)) = (self.room.as_deref_mut(), self.items.as_deref()) else {
|
||||
return;
|
||||
};
|
||||
if !pane_shown(script) {
|
||||
room.close();
|
||||
}
|
||||
let refused = room.resolve_pending(items);
|
||||
show_refusals(script, &mut self.sink, refused);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DressUpUiPlugin;
|
||||
|
||||
impl Plugin for DressUpUiPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// A drain, the frame its verbs are called: a try-on's cache lookup reads the cache as it
|
||||
// stood at the call, before the next frame's packets land.
|
||||
app.init_resource::<DressUpRoom>()
|
||||
.add_systems(Update, drain_dressup.in_set(UiDrain));
|
||||
.add_systems(Update, compose_dressup.in_set(UiDrain));
|
||||
}
|
||||
}
|
||||
|
||||
/// The booth's look, each frame: the room's turn ([`DressUpSteps::frame`]), then the player's own
|
||||
/// look with the room's substitutions written in, pushed to the pane when it moved.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn drain_dressup(
|
||||
// The VM, whose call host writes the worn pins, and the player the pane clones, pins read.
|
||||
mut vm_and_me: ParamSet<(
|
||||
crate::script_calls::HostedVm,
|
||||
Query<(&ObjectStore, &NetEntity, Option<&crate::entities::WornPins>), With<SelfPlayer>>,
|
||||
fn compose_dressup(
|
||||
// The room, beside the host that applies the verbs to it, and the player the pane clones,
|
||||
// whose pins the host writes.
|
||||
mut vm: crate::script_calls::VmWith<(
|
||||
DressUpSteps<'static>,
|
||||
Query<
|
||||
'static,
|
||||
'static,
|
||||
(
|
||||
&'static ObjectStore,
|
||||
&'static NetEntity,
|
||||
Option<&'static crate::entities::WornPins>,
|
||||
),
|
||||
With<SelfPlayer>,
|
||||
>,
|
||||
)>,
|
||||
mut room: ResMut<DressUpRoom>,
|
||||
mut preview: ResMut<DressUpPreview>,
|
||||
items: Res<Items>,
|
||||
commands: Res<NetCommands>,
|
||||
// A miss sends the `CMSG_GUILD_QUERY` whose answer paints the tabard.
|
||||
guilds: Res<crate::ui_guild::GuildState>,
|
||||
@@ -256,28 +332,12 @@ fn drain_dressup(
|
||||
Res<crate::ui_action::PlayerActions>,
|
||||
Option<Res<crate::ui_action::Spells>>,
|
||||
),
|
||||
mut sink: crate::ui_action::MessageSink,
|
||||
) {
|
||||
let ran = vm_and_me.p0().run(|script| {
|
||||
let ran = vm.turn(|script, (mut steps, _)| {
|
||||
let Some(script) = script else {
|
||||
return false;
|
||||
};
|
||||
let mut intents = script.take_dressup_intents();
|
||||
// The room empties when neither the dressing room nor the auction house's
|
||||
// `AuctionDressUpFrame` is shown, read off the frames (the stock files carry no hook of
|
||||
// ours), and a verb on a hidden pane does nothing (`0x504596`). The reference's widget
|
||||
// keeps its state while hidden, but `DressUpItem` re-issues `SetUnit("player")` on a
|
||||
// hidden frame, so the kept state is never seen.
|
||||
if !script.frame_visible("DressUpFrame") && !script.frame_visible("AuctionDressUpFrame") {
|
||||
intents = vec![DressUpIntent::Close];
|
||||
}
|
||||
// A refusal shows as its record lands: in the call when cached, else from the cache's
|
||||
// callback (`0x504ba0`).
|
||||
let refused = room.step(intents, &items, &commands);
|
||||
let lines = (0..refused)
|
||||
.filter_map(|_| crate::ui_action::keyed_line(script, "ERR_NOT_EQUIPPABLE"))
|
||||
.collect::<Vec<_>>();
|
||||
crate::ui_action::show_messages(script, &mut sink, "ui_dressup", lines);
|
||||
steps.frame(script);
|
||||
true
|
||||
});
|
||||
if !ran {
|
||||
@@ -286,14 +346,16 @@ fn drain_dressup(
|
||||
// The crest comes off the guild cache this system holds, not the outfit `player_look`
|
||||
// builds.
|
||||
let dual_wield = known.0.knows_dual_wield(known.1.as_deref());
|
||||
let me = vm_and_me.p1();
|
||||
let (steps, me) = vm.own();
|
||||
let (Some(room), Some(items)) = (steps.room.as_deref(), steps.items.as_deref()) else {
|
||||
return;
|
||||
};
|
||||
let look = match (room.open, me.single().ok()) {
|
||||
(true, Some((store, net, pins))) => {
|
||||
player_look(store, net, pins, &room, &items, dual_wield).map(|l| DressUpLook {
|
||||
(true, Some((store, net, pins))) => player_look(store, net, pins, room, items, dual_wield)
|
||||
.map(|l| DressUpLook {
|
||||
emblem: crate::ui_guild::unit_guild_emblem(&store.0, &guilds, Ask::New(&commands)),
|
||||
..l
|
||||
})
|
||||
}
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
if preview.look != look {
|
||||
@@ -491,7 +553,7 @@ mod tests {
|
||||
"Reset puts the player's own chest back on"
|
||||
);
|
||||
|
||||
room.apply(DressUpIntent::Close, &items, &cmds);
|
||||
room.close();
|
||||
assert!(!room.open, "closing empties the room (the booth goes dark)");
|
||||
}
|
||||
|
||||
@@ -888,6 +950,55 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A try-on applies inside its call (`0x504540`): a cached bag refuses there, so the chunk's
|
||||
/// next line reads the `UI_ERROR_MESSAGE` its `DisplayError` signalled (`0x504b2d`), and a
|
||||
/// cached chest is on the booth before the verb returns.
|
||||
#[test]
|
||||
fn a_try_on_refuses_and_lands_inside_its_call() {
|
||||
let mut app = crate::game_plugins::schedule_tests::headless_client();
|
||||
app.insert_resource(bevy::ecs::error::DefaultErrorHandler(
|
||||
bevy::ecs::error::ignore,
|
||||
));
|
||||
let (tx, _rx) = crossbeam_channel::unbounded();
|
||||
app.insert_resource(NetCommands(tx));
|
||||
app.update();
|
||||
{
|
||||
let mut items = app.world_mut().resource_mut::<Items>();
|
||||
items.insert_template(4500, Some(worn("Small Bag", 9100, 18)));
|
||||
items.insert_template(2000, Some(worn("Shiny Chest", 7000, 5)));
|
||||
}
|
||||
let mut s = UiScript::new().expect("a VM");
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
app.insert_non_send_resource(s);
|
||||
let seen: Option<String> = crate::script_calls::with_hosted_vm(app.world_mut(), |s| {
|
||||
let s = s.expect("a VM");
|
||||
s.run(
|
||||
r#"
|
||||
ERR_NOT_EQUIPPABLE = "That item can't be equipped."
|
||||
local room = CreateFrame("Frame", "DressUpFrame", UIParent)
|
||||
dm = CreateFrame("DressUpModel", "DressUpModel", room)
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UI_ERROR_MESSAGE")
|
||||
f:SetScript("OnEvent", function() seen = arg1 end)
|
||||
"#,
|
||||
)
|
||||
.expect("the room");
|
||||
let seen = s
|
||||
.eval::<Option<String>>("dm:SetUnit('player') dm:TryOn(4500) return seen")
|
||||
.expect("the chunk runs");
|
||||
s.run("dm:TryOn(2000)").expect("the try-on");
|
||||
seen
|
||||
});
|
||||
// No frame has run since: the room is as the call left it.
|
||||
let worn_chest = app.world().resource::<DressUpRoom>().worn[4].is_some();
|
||||
assert_eq!(
|
||||
seen.as_deref(),
|
||||
Some("That item can't be equipped."),
|
||||
"the refusal signals inside the call"
|
||||
);
|
||||
assert!(worn_chest, "the cached chest went on inside its call");
|
||||
}
|
||||
|
||||
/// A bow takes the off lane and clears the main; a crossbow takes the main and clears the off.
|
||||
#[test]
|
||||
fn a_tried_on_ranged_weapon_takes_a_hand_and_clears_the_melee_pair() {
|
||||
|
||||
@@ -288,8 +288,8 @@ fn watch_layout(mut hosted_vm: crate::script_calls::HostedVm, mut file: ResMut<L
|
||||
})
|
||||
}
|
||||
|
||||
/// The autosave: dirty and one quiet second rewrite the file atomically. Every orderly end is
|
||||
/// [`save_now`]'s.
|
||||
/// The autosave: dirty and one quiet second rewrite the file atomically, at the frame, as no
|
||||
/// script reads the file before the next load. Every orderly end is [`save_now`]'s.
|
||||
fn save_layout(mut hosted_vm: crate::script_calls::HostedVm, mut file: ResMut<LayoutFile>) {
|
||||
hosted_vm.run(|script| {
|
||||
let Some(script) = script else { return };
|
||||
|
||||
@@ -240,7 +240,8 @@ fn migrate_character_bar(
|
||||
}
|
||||
|
||||
/// Persist on the engine's dirty edge. The verbs signal `UPDATE_MACROS` themselves, inside their
|
||||
/// calls.
|
||||
/// calls. Left at the frame: the file is read only at the next load, so no script sees when it
|
||||
/// is written.
|
||||
fn save_dirty_macros(
|
||||
mut hosted_vm: crate::script_calls::HostedVm,
|
||||
mut files: ResMut<MacroFiles>,
|
||||
|
||||
@@ -792,7 +792,8 @@ fn forget_dead_vm_tiles(
|
||||
}
|
||||
|
||||
/// The per-frame pass: feed the engine the facts it asked for, keep one tile per visible pane,
|
||||
/// pack the atlas, place every tile at its cell and its play head, and aim the camera.
|
||||
/// pack the atlas, place every tile at its cell and its play head, and aim the camera. Not a
|
||||
/// call's: a tile is the pane's draw, which no getter reads.
|
||||
#[allow(clippy::type_complexity)] // a Bevy system's full input set
|
||||
fn sync_tiles(
|
||||
mut vm: crate::script_calls::VmWith<(
|
||||
|
||||
@@ -74,6 +74,7 @@ fn load_room_with_bags(s: &UiScript) {
|
||||
/// (`PaperDollFrame.lua:100-104`).
|
||||
fn shown_paper_doll() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.keep_calls();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in crate::ui_script::test_ui::production_order(&[
|
||||
CHARACTER_UI,
|
||||
@@ -146,6 +147,7 @@ fn backpack_with_jerky(s: &mut UiScript) -> String {
|
||||
fn ctrl_click_on_a_bag_item_opens_the_room_wearing_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.keep_calls();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_room_with_bags(&s);
|
||||
let btn = backpack_with_jerky(&mut s);
|
||||
@@ -178,6 +180,7 @@ fn ctrl_click_on_a_bag_item_opens_the_room_wearing_it() {
|
||||
fn shift_click_posts_the_link_with_chat_open_and_splits_with_it_closed() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.keep_calls();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_room_with_bags(&s);
|
||||
let btn = backpack_with_jerky(&mut s);
|
||||
@@ -218,6 +221,7 @@ fn shift_click_posts_the_link_with_chat_open_and_splits_with_it_closed() {
|
||||
fn ctrl_clicking_a_chat_link_previews_it() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.keep_calls();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_room(&s);
|
||||
|
||||
@@ -339,6 +343,7 @@ fn shift_clicking_an_unresolved_slot_posts_nothing_and_never_raises() {
|
||||
fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.keep_calls();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_room(&s);
|
||||
s.run("ShowUIPanel(DressUpFrame)").unwrap();
|
||||
@@ -391,6 +396,7 @@ fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() {
|
||||
fn the_room_binds_the_player() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.keep_calls();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_room(&s);
|
||||
s.set_unit_guids(&benilla_ui::script::UnitGuids {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
//! "vsync on" is `AutoNoVsync` paced to the monitor's refresh rate ([`present_choice`]), which
|
||||
//! keeps the synced look without the crash. `$WOW_PRESENT=fifo` restores plain `Fifo`.
|
||||
|
||||
use benilla_ui::script::ScreenResolution;
|
||||
use benilla_ui::script::{ScreenResolution, UiScript};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::renderer::RenderAdapterInfo;
|
||||
use bevy::window::{
|
||||
@@ -505,32 +505,50 @@ impl Plugin for VideoPlugin {
|
||||
#[derive(Resource, Default, Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(crate) struct GxRestarts(u32);
|
||||
|
||||
/// Carry `RestartGx()` calls into [`GxRestarts`] and commit the latched `gx*` rows (flags `3`),
|
||||
/// where the reference's restart calls `CVar::Update` (`0x63e060`) on each; `GetCVar` answers the
|
||||
/// applied value until then.
|
||||
fn drain_restart_gx(
|
||||
mut vm: crate::script_calls::VmWith<crate::cvars::CvarSteps<'static>>,
|
||||
mut restarts: ResMut<GxRestarts>,
|
||||
) {
|
||||
vm.turn(|script, mut steps| {
|
||||
let Some(script) = script else {
|
||||
/// `gxRestart`'s handler (`0x639f60`) inside the call that ran it
|
||||
/// ([`benilla_ui::script::ScriptCall::RestartGx`]): the latched `gx*` rows (flags `3`) committed,
|
||||
/// where the reference's restart calls `CVar::Update` (`0x63e060`) on each (`0x639ec0`), their
|
||||
/// callbacks run and the VM's mirror synced, so `GetCVar` reads the committed value on the
|
||||
/// verb's next line. The device switch it asks, a [`GxRestarts`] bump, is the window appliers' in
|
||||
/// the frame's drain: no script reads the window but through these rows.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(crate) struct GxRestartSteps<'w> {
|
||||
cvars: crate::cvars::CvarSteps<'w>,
|
||||
/// Absent in a harness without the video plugin.
|
||||
restarts: Option<ResMut<'w, GxRestarts>>,
|
||||
}
|
||||
|
||||
impl GxRestartSteps<'_> {
|
||||
/// `runs` restarts at once.
|
||||
pub(crate) fn restart(&mut self, script: &mut UiScript, runs: u32) {
|
||||
// The VM's writes no call took first, so a stage made before the restart is committed.
|
||||
self.cvars.sync(Some(script));
|
||||
let Some(cvars) = self.cvars.cvars() else {
|
||||
return;
|
||||
};
|
||||
let asks = script.take_restart_gx_asks();
|
||||
if asks == 0 {
|
||||
// A `ResMut` deref-mut is a change signal, so a quiet frame touches nothing.
|
||||
return;
|
||||
}
|
||||
let Some(cvars) = steps.cvars() else {
|
||||
return;
|
||||
};
|
||||
restarts.0 = restarts.0.wrapping_add(asks);
|
||||
let committed = cvars.commit_latched();
|
||||
steps.sync(Some(script));
|
||||
if let Some(restarts) = self.restarts.as_deref_mut() {
|
||||
restarts.0 = restarts.0.wrapping_add(runs);
|
||||
}
|
||||
self.cvars.sync(Some(script));
|
||||
info!(
|
||||
"video: RestartGx — {committed} staged setting(s) committed; re-asserting the \
|
||||
display mode and present mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The restarts no call took, a VM's with no host lent ([`GxRestartSteps`]).
|
||||
fn drain_restart_gx(mut vm: crate::script_calls::VmWith<GxRestartSteps<'static>>) {
|
||||
vm.turn(|script, mut steps| {
|
||||
let Some(script) = script else {
|
||||
return;
|
||||
};
|
||||
let runs = script.take_restart_gx_asks();
|
||||
// A `ResMut` deref-mut is a change signal, so a quiet frame touches nothing.
|
||||
if runs > 0 {
|
||||
steps.restart(script, runs);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +277,15 @@ pub enum ScriptCall {
|
||||
/// `NextView`, `PrevView` or `FlipCameraYaw`, seated on the camera inside the call, its CVar
|
||||
/// writes with it (`0x512f17`, `0x50fa09`).
|
||||
CameraView(super::CameraViewRequest),
|
||||
/// `gxRestart`'s handler (`0x639f60`), run inside `RestartGx`, `SetScreenResolution` and
|
||||
/// `SetMultisampleFormat` (through `0x63ce00`, which calls it synchronously) and
|
||||
/// `RestoreVideoDefaults` (directly, `0x639a51`): the host commits the latched display rows
|
||||
/// (`0x639ec0`) and runs their callbacks before the verb goes on.
|
||||
RestartGx,
|
||||
/// A dressing-room verb ([`super::DressUpIntent`]): `Dress`, `Undress`, `TryOn`, or `SetUnit`
|
||||
/// and `RefreshUnit` on a `DressUpModel`, applied to the room inside the call; a cached
|
||||
/// record's try-on goes on, or refuses with `ERR_NOT_EQUIPPABLE`, there (`0x504540`).
|
||||
DressUp(super::DressUpIntent),
|
||||
/// `FollowUnit` or `FollowByName` ([`super::FollowRequest`]): the start gate, its refusal's
|
||||
/// line or the follow armed and `AUTOFOLLOW_BEGIN` signalled, inside the call (`0x60fed0`).
|
||||
Follow(super::FollowRequest),
|
||||
|
||||
@@ -236,6 +236,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
impl super::UiScript {
|
||||
/// The `RequestTimePlayed()` sends a harness's VM kept ([`Self::keep_calls`]): a count, as the
|
||||
/// request body is empty.
|
||||
#[cfg(test)]
|
||||
pub fn take_played_time_asks(&mut self) -> u32 {
|
||||
self.take_calls_where(|call| {
|
||||
matches!(call, super::ScriptCall::RequestTimePlayed).then_some(())
|
||||
|
||||
@@ -424,8 +424,8 @@ impl super::UiScript {
|
||||
self.model_mut().video_caps = caps;
|
||||
}
|
||||
|
||||
/// Drain the `RestartGx()` calls queued since the last call; the host answers each by
|
||||
/// re-asserting the staged video settings against the window.
|
||||
/// Drain the restarts no call took ([`restart_gx`]); the host answers each by committing the
|
||||
/// staged video settings and re-asserting them against the window.
|
||||
pub fn take_restart_gx_asks(&mut self) -> u32 {
|
||||
std::mem::take(&mut self.model_mut().restart_gx_asks)
|
||||
}
|
||||
@@ -790,10 +790,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
set_from_engine(&mut model, "gxColorBits", f.color_bits.to_string());
|
||||
set_from_engine(&mut model, "gxDepthBits", f.depth_bits.to_string());
|
||||
set_from_engine(&mut model, "gxMultisample", f.samples.to_string());
|
||||
// Deviation: the reference then runs the console command `gxRestart` (`0x842978`
|
||||
// through `0x63ce00`); none is queued here, because `gxMultisample` applies at next
|
||||
// launch anyway: the camera reads its sample count once, at spawn.
|
||||
Ok(())
|
||||
drop(model);
|
||||
// Each `CVar::Set` (`0x48c72a`, `0x48c75b`, `0x48c78c`) runs its row's callback
|
||||
// inside the call.
|
||||
written(lua)?;
|
||||
// Then `gxRestart`, always (`0x48c79b`).
|
||||
restart_gx(lua)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -848,6 +850,44 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
super::video_pairs::install(lua)
|
||||
}
|
||||
|
||||
/// `gxRestart`'s handler (`0x639f60`) inside the call that runs it: `RestartGx` (`0x48dab0`),
|
||||
/// `SetScreenResolution` (`0x48c0bc`) and `SetMultisampleFormat` (`0x48c79b`) through
|
||||
/// `ConsoleCommandExecute` (`0x63ce00`), which calls the handler synchronously, and
|
||||
/// `RestoreVideoDefaults`, which calls it directly (`0x639a51`). The host commits the latched rows
|
||||
/// (`0x639ec0`) before the verb goes on. With no host open (a bare VM) it is
|
||||
/// counted for the host's frame drain.
|
||||
fn restart_gx(lua: &Lua) -> mlua::Result<()> {
|
||||
if super::host::open(lua) {
|
||||
return super::calls::submit(lua, super::ScriptCall::RestartGx);
|
||||
}
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.restart_gx_asks += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `names` each set to its registered default, through the host's callbacks inside the call.
|
||||
fn restore_defaults(lua: &Lua, names: &[&str]) -> mlua::Result<()> {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
for name in names {
|
||||
let Some(default) = model
|
||||
.cvars
|
||||
.get(&name.to_ascii_lowercase())
|
||||
.map(|slot| slot.default.clone())
|
||||
else {
|
||||
// Unregistered in a bare VM: nothing to restore, nothing to warn about.
|
||||
continue;
|
||||
};
|
||||
set_from_engine(&mut model, name, default);
|
||||
}
|
||||
drop(model);
|
||||
written(lua)
|
||||
}
|
||||
|
||||
/// How many of [`VIDEO_DEFAULT_CVARS`] lead it as the device record's `gx*` writes, ahead of
|
||||
/// `RestoreVideoDefaults`' restart.
|
||||
const VIDEO_DEFAULT_GX: usize = 6;
|
||||
|
||||
/// What `RestoreVideoDefaults` puts back: the CVars among the reference verb's writes that benilla
|
||||
/// registers, each at its registered default. The verb writes no `uiScale`, `useUiScale` or
|
||||
/// `anisotropic`.
|
||||
@@ -957,8 +997,11 @@ fn install_video_verbs(lua: &Lua) -> mlua::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
set_from_engine(&mut model, "gxResolution", want);
|
||||
model.restart_gx_asks += 1;
|
||||
Ok(())
|
||||
drop(model);
|
||||
// `CVar::Set` (`0x48c0ad`) runs the row's callback, then `gxRestart` runs
|
||||
// (`0x48c0bc`), both inside the call.
|
||||
written(lua)?;
|
||||
restart_gx(lua)
|
||||
})?,
|
||||
)?;
|
||||
// ── GetRefreshRates ─────────────────────────────────────────────────────────────────────
|
||||
@@ -1003,40 +1046,24 @@ fn install_video_verbs(lua: &Lua) -> mlua::Result<()> {
|
||||
// ── RestartGx ────────────────────────────────────────────────────────────────────────────
|
||||
// "Apply the staged video settings now": `0x48dab0` runs the console command `gxRestart`
|
||||
// (`0x63ce00`), whose handler `0x639f60` applies the staged device format and commits the
|
||||
// fourteen latched CVars. Here it queues a counted request, as `Screenshot()` does; the host
|
||||
// commits its gx stages (`Cvars::commit_latched`) and re-asserts them against the window.
|
||||
// The reference runs each gx change callback at `SetCVar`, where it can veto the write; the
|
||||
// host runs it at the commit. No restart applies `gxMultisample`: the camera reads it once.
|
||||
lua.globals().set(
|
||||
"RestartGx",
|
||||
lua.create_function(|lua, ()| {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.restart_gx_asks += 1;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// fourteen latched CVars, synchronously ([`restart_gx`]). The reference runs each gx change
|
||||
// callback at `SetCVar`, where it can veto the write; the host runs it at the commit. No
|
||||
// restart applies `gxMultisample`: the camera reads it once.
|
||||
lua.globals()
|
||||
.set("RestartGx", lua.create_function(|lua, ()| restart_gx(lua))?)?;
|
||||
// ── RestoreVideoDefaults ─────────────────────────────────────────────────────────────────
|
||||
// The Defaults button's whole effect: `OptionsFrame_SetDefaults` only sets the widgets, which
|
||||
// the button's `OnClick` then hides without `_Save` (`OptionsFrame.xml:688-690`). Ends with a
|
||||
// restart, as the reference calls the `gxRestart` handler between its two passes (`0x639a51`).
|
||||
// the button's `OnClick` then hides without `_Save` (`OptionsFrame.xml:688-690`).
|
||||
lua.globals().set(
|
||||
"RestoreVideoDefaults",
|
||||
lua.create_function(|lua, ()| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
for name in VIDEO_DEFAULT_CVARS {
|
||||
let Some(default) = model
|
||||
.cvars
|
||||
.get(&name.to_ascii_lowercase())
|
||||
.map(|slot| slot.default.clone())
|
||||
else {
|
||||
// Unregistered in a bare VM: nothing to restore, nothing to warn about.
|
||||
continue;
|
||||
};
|
||||
set_from_engine(&mut model, name, default);
|
||||
}
|
||||
model.restart_gx_asks += 1;
|
||||
Ok(())
|
||||
// The device record's `gx*` (`0x639c40`), the restart (`0x639a51`), then the quality
|
||||
// rows (`0x639a60`), each write a `CVar::Set` that runs its row's callback inside the
|
||||
// call.
|
||||
let (gx, quality) = VIDEO_DEFAULT_CVARS.split_at(VIDEO_DEFAULT_GX);
|
||||
restore_defaults(lua, gx)?;
|
||||
restart_gx(lua)?;
|
||||
restore_defaults(lua, quality)
|
||||
})?,
|
||||
)?;
|
||||
// ── GetGamma / SetGamma ──────────────────────────────────────────────────────────────────
|
||||
@@ -1067,8 +1094,13 @@ fn install_video_verbs(lua: &Lua) -> mlua::Result<()> {
|
||||
// `lua_isnumber` (`0x4891fe`): a number or a numeric string; anything else raises
|
||||
// (`0x6f4940`).
|
||||
let v = super::binding_abi::float_arg(lua, value, USAGE_SET_GAMMA)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
write_cvar(&mut model, CVAR_GAMMA, format_f(1.0 - v));
|
||||
write_cvar(
|
||||
&mut lua.app_data_mut::<Model>().expect("model app_data"),
|
||||
CVAR_GAMMA,
|
||||
format_f(1.0 - v),
|
||||
);
|
||||
// `CVar::Set` (`0x489267`) runs the row's callback inside the call.
|
||||
written(lua)?;
|
||||
// Zero return values, not nil (`eax = 0` at every `ret`).
|
||||
Ok(mlua::MultiValue::new())
|
||||
})?,
|
||||
@@ -1140,6 +1172,9 @@ fn install_world_detail_verbs(lua: &Lua) -> mlua::Result<()> {
|
||||
);
|
||||
write_cvar(&mut model, CVAR_SMALL_CULL, small_cull_text(stop));
|
||||
write_cvar(&mut model, CVAR_WORLD_DETAIL, stop.to_string());
|
||||
drop(model);
|
||||
// Each `CVar::Set` (`0x488e51`, `0x488e8e`) runs its row's callback inside the call.
|
||||
written(lua)?;
|
||||
// Zero return values, not nil (`eax = 0` at every `ret`).
|
||||
Ok(mlua::MultiValue::new())
|
||||
})?,
|
||||
@@ -1560,4 +1595,13 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(trimmed, "Archimonde");
|
||||
}
|
||||
|
||||
/// `RestoreVideoDefaults` restarts between the device record's writes and the quality rows'
|
||||
/// (`0x639a51`), so the split falls after the last `gx*` row.
|
||||
#[test]
|
||||
fn the_video_defaults_split_after_the_device_records_rows() {
|
||||
let (gx, quality) = super::VIDEO_DEFAULT_CVARS.split_at(super::VIDEO_DEFAULT_GX);
|
||||
assert!(gx.iter().all(|n| n.starts_with("gx")), "{gx:?}");
|
||||
assert!(!quality.iter().any(|n| n.starts_with("gx")), "{quality:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The dressing room: the `DressUpModel` verbs (table `0x84f190`) and the intent queue behind them
|
||||
//! and behind `SetUnit`/`RefreshUnit` on a dressing-room pane. The reference edits a clone of the
|
||||
//! unit's model (`0x5059a0`, `0x504350`); here the app composes the look from the intents in
|
||||
//! order, which matters: on a closed window `DressUpItem` resets, then tries on
|
||||
//! (`DressUpFrame.lua:3-7`).
|
||||
//! The dressing room: the `DressUpModel` verbs (table `0x84f190`) and `SetUnit`/`RefreshUnit` on a
|
||||
//! dressing-room pane, each handed to the host, which applies it to the room inside the call
|
||||
//! ([`super::ScriptCall::DressUp`]). The reference edits a clone of the unit's model (`0x5059a0`,
|
||||
//! `0x504350`); here the app composes the look from the verbs in call order, which matters: on a
|
||||
//! closed window `DressUpItem` resets, then tries on (`DressUpFrame.lua:3-7`).
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::widget::FrameKind;
|
||||
/// Registry key of the `DressUpModel` method table, its own three; the rest come by the chain.
|
||||
pub(super) const REG_DRESSUPMODEL_METHODS: &str = "__benilla_dressupmodel_methods";
|
||||
|
||||
/// One queued dressing-room intent, applied in order.
|
||||
/// One dressing-room verb, applied inside its call.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum DressUpIntent {
|
||||
/// `SetUnit`, `RefreshUnit` and `Dress` (the Reset button): the reference's rebuild from the
|
||||
@@ -26,26 +26,24 @@ pub enum DressUpIntent {
|
||||
/// `TryOn(item)`: the item in its `InventoryType`'s slot, a held one in the lane `0x5048e8`
|
||||
/// picks, with no class, level or proficiency check (`DressUpFrame.lua:2-16`).
|
||||
TryOn(u32),
|
||||
/// The window was hidden, so the booth empties. The reference keeps its state while hidden,
|
||||
/// but the next `DressUpItem` re-issues `SetUnit("player")`, so that state is never seen.
|
||||
Close,
|
||||
}
|
||||
|
||||
impl super::UiScript {
|
||||
/// Drain the dressing room's intents, oldest first; the app applies them in order.
|
||||
/// The dressing-room verbs a harness's VM kept ([`Self::keep_calls`]), oldest first.
|
||||
pub fn take_dressup_intents(&mut self) -> Vec<DressUpIntent> {
|
||||
std::mem::take(&mut self.model_mut().dressup_intents)
|
||||
self.take_calls_where(|call| match call {
|
||||
super::ScriptCall::DressUp(intent) => Some(*intent),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn queue(lua: &Lua, intent: DressUpIntent) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.dressup_intents
|
||||
.push(intent);
|
||||
/// Hand `intent` to the host, which applies it before this returns.
|
||||
fn submit(lua: &Lua, intent: DressUpIntent) -> mlua::Result<()> {
|
||||
super::calls::submit(lua, super::ScriptCall::DressUp(intent))
|
||||
}
|
||||
|
||||
/// Queue the rebuild when `PlayerModel`'s `SetUnit`/`RefreshUnit` lands on a `DressUpModel`.
|
||||
/// The rebuild when `PlayerModel`'s `SetUnit`/`RefreshUnit` lands on a `DressUpModel`.
|
||||
pub(super) fn redress_if_dressup(lua: &Lua, this: &Table) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let is_dressup = lua
|
||||
@@ -55,7 +53,7 @@ pub(super) fn redress_if_dressup(lua: &Lua, this: &Table) -> mlua::Result<()> {
|
||||
.frame(h)
|
||||
.is_some_and(|f| f.kind == FrameKind::DressUpModel);
|
||||
if is_dressup {
|
||||
queue(lua, DressUpIntent::Dress);
|
||||
submit(lua, DressUpIntent::Dress)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -77,8 +75,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"Undress",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
frame_handle_of(lua, &this)?;
|
||||
queue(lua, DressUpIntent::Undress);
|
||||
Ok(())
|
||||
submit(lua, DressUpIntent::Undress)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -87,8 +84,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"Dress",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
frame_handle_of(lua, &this)?;
|
||||
queue(lua, DressUpIntent::Dress);
|
||||
Ok(())
|
||||
submit(lua, DressUpIntent::Dress)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -100,7 +96,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
frame_handle_of(lua, &this)?;
|
||||
if let Ok(id) = u32::try_from(item_arg(lua, &item)) {
|
||||
if id != 0 {
|
||||
queue(lua, DressUpIntent::TryOn(id));
|
||||
submit(lua, DressUpIntent::TryOn(id))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -117,6 +113,7 @@ mod tests {
|
||||
|
||||
fn room() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.keep_calls();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(r#"dm = CreateFrame("DressUpModel", "DM", UIParent)"#)
|
||||
.unwrap();
|
||||
@@ -167,7 +164,7 @@ mod tests {
|
||||
|
||||
/// The stock `DressUpItemLink` passes the link's digits: a digit string is an id, a link is 0.
|
||||
#[test]
|
||||
fn the_verbs_queue_intents_in_order_and_try_on_reads_its_argument_like_the_client() {
|
||||
fn the_verbs_hand_their_calls_over_in_order_and_try_on_reads_its_argument_like_the_client() {
|
||||
let mut s = room();
|
||||
s.run(
|
||||
r#"
|
||||
@@ -195,7 +192,7 @@ mod tests {
|
||||
DressUpIntent::Dress,
|
||||
]
|
||||
);
|
||||
// The same two PlayerModel verbs on a paper-doll pane queue nothing.
|
||||
// The same two PlayerModel verbs on a paper-doll pane make no call.
|
||||
s.run(r#"pm = CreateFrame("PlayerModel", "PMOnly", UIParent) pm:SetUnit("player") pm:RefreshUnit()"#)
|
||||
.unwrap();
|
||||
assert!(s.take_dressup_intents().is_empty());
|
||||
|
||||
@@ -331,7 +331,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
CVAR_VERSION_CHECK,
|
||||
if on { "1" } else { "0" }.to_string(),
|
||||
);
|
||||
Ok(())
|
||||
super::cvars::written(lua)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -358,7 +358,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
CVAR_SCRIPT_MEMORY,
|
||||
(mb << 10).to_string(),
|
||||
);
|
||||
Ok(())
|
||||
super::cvars::written(lua)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -893,9 +893,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"SetSavedAccountName",
|
||||
lua.create_function(|lua, name: Value| {
|
||||
let name = string_arg(lua, name, "Usage: SetSavedAccountName(\"accountName\")")?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
super::cvars::write_cvar(&mut model, "accountName", name);
|
||||
Ok(())
|
||||
super::cvars::write_cvar(
|
||||
&mut lua.app_data_mut::<Model>().expect("model app_data"),
|
||||
"accountName",
|
||||
name,
|
||||
);
|
||||
super::cvars::written(lua)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -951,7 +954,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
},
|
||||
);
|
||||
super::cvars::set_from_engine(&mut model, notice.cvar(), "1".to_string());
|
||||
Ok(())
|
||||
drop(model);
|
||||
super::cvars::written(lua)
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -621,6 +621,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}
|
||||
name
|
||||
};
|
||||
// `ConnectToRealm`'s `CVar::Set` (`0x5ab7bf`) runs the row's callback inside the call.
|
||||
super::cvars::written(lua)?;
|
||||
if let Some(name) = name {
|
||||
call(lua, GlueCall::ChangeRealm(name));
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
/// The `Stuck()` calls a harness's VM kept, each one cast of spell 7355.
|
||||
#[cfg(test)]
|
||||
pub fn take_stuck_casts(&mut self) -> u32 {
|
||||
self.take_calls_like(&super::ScriptCall::GmTicket(GmTicketCall::Stuck)) as u32
|
||||
}
|
||||
|
||||
@@ -458,7 +458,7 @@ impl super::UiScript {
|
||||
|
||||
/// The guids of the creators and givers the item builds a harness's VM kept the calls of
|
||||
/// ([`Self::keep_calls`]) missed in the name cache, in ask order.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[cfg(test)]
|
||||
pub fn take_item_name_asks(&mut self) -> Vec<u64> {
|
||||
self.take_calls_where(|call| match call {
|
||||
super::ScriptCall::RecordAsk(super::RecordAsk::ItemBuildName(guid)) => Some(*guid),
|
||||
@@ -468,7 +468,7 @@ impl super::UiScript {
|
||||
|
||||
/// The charter item guids whose petition record or owner the item builds a harness's VM kept
|
||||
/// the calls of missed, in ask order.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[cfg(test)]
|
||||
pub fn take_petition_asks(&mut self) -> Vec<u64> {
|
||||
self.take_calls_where(|call| match call {
|
||||
super::ScriptCall::RecordAsk(super::RecordAsk::Petition(item)) => Some(*item),
|
||||
@@ -488,16 +488,6 @@ impl super::UiScript {
|
||||
self.model_mut().item_sets.insert(set_id, view);
|
||||
}
|
||||
|
||||
/// The member entries the set blocks a harness's VM kept the calls of
|
||||
/// ([`Self::keep_calls`]) asked the item-name cache for, in ask order.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn take_item_name_record_asks(&mut self) -> Vec<u32> {
|
||||
self.take_calls_where(|call| match call {
|
||||
super::ScriptCall::RecordAsk(super::RecordAsk::ItemName(entry)) => Some(*entry),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Pushes the whole `ItemRandomProperties` table once, at load: a clicked tooltip cannot
|
||||
/// repaint on a late answer.
|
||||
pub fn set_random_properties(
|
||||
|
||||
@@ -285,8 +285,9 @@ impl KeybindState {
|
||||
}
|
||||
|
||||
/// `LoadBindings(set)`: 0 defaults, 1 account, 2 character, which falls back to the account
|
||||
/// set (the reference's starts as a copy), each falling back to the defaults. Loading defaults
|
||||
/// keeps the current set, as the window's Reset To Default does.
|
||||
/// set (the reference's starts as a copy), each falling back to the defaults. The current set
|
||||
/// stays: the verb only copies the set into the live one (`0x4b8249`-`0x4b8252`), so the
|
||||
/// window's Cancel (`LoadBindings(GetCurrentBindingSet())`) reloads the set saved last.
|
||||
fn load(&mut self, set: u32) {
|
||||
let list = match set {
|
||||
0 => &self.defaults,
|
||||
@@ -298,9 +299,6 @@ impl KeybindState {
|
||||
_ => return,
|
||||
};
|
||||
self.live = list.clone();
|
||||
if set != 0 {
|
||||
self.current_set = set;
|
||||
}
|
||||
self.generation += 1;
|
||||
}
|
||||
|
||||
@@ -404,9 +402,13 @@ impl super::UiScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// Host-side `LoadBindings` (world entry: character set if it exists, else account).
|
||||
/// Host-side `LoadBindings` (world entry: character set if it exists, else account), which
|
||||
/// makes `set` current first, as the sets' read does (`0x4b64cb`).
|
||||
pub fn load_binding_set(&mut self, set: u32) {
|
||||
let mut model = self.model_mut();
|
||||
if set != 0 {
|
||||
model.keybinds.current_set = set;
|
||||
}
|
||||
model.keybinds.load(set);
|
||||
model.keybinds.signal_owed = true;
|
||||
}
|
||||
@@ -999,6 +1001,34 @@ mod tests {
|
||||
assert_eq!(s.take_keybind_requests(), vec![KeybindRequest::Save(1)]);
|
||||
}
|
||||
|
||||
/// Ticking Character Specific loads set 2 (`KeyBindingFrame_ChangeBindingProfile`), and
|
||||
/// Cancel's `LoadBindings(GetCurrentBindingSet())` puts the account set back: `LoadBindings`
|
||||
/// never writes the current set (`0x4b8249`-`0x4b8252`), so it is still 1.
|
||||
#[test]
|
||||
fn a_cancelled_character_tick_leaves_the_account_set_live() {
|
||||
let mut s = script();
|
||||
s.run(r#"SetBinding("F", "JUMP") SaveBindings(1)"#).unwrap();
|
||||
s.run(r#"SetBinding("F", "CAMERAZOOMIN") SaveBindings(2)"#)
|
||||
.unwrap();
|
||||
// World entry on the account set.
|
||||
s.load_binding_set(1);
|
||||
assert_eq!(s.current_binding_set(), 1);
|
||||
s.run("LoadBindings(2)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("F")"#).unwrap(),
|
||||
"CAMERAZOOMIN",
|
||||
"the tick shows the character set"
|
||||
);
|
||||
assert_eq!(s.eval::<i64>("return GetCurrentBindingSet()").unwrap(), 1);
|
||||
s.run("LoadBindings(GetCurrentBindingSet())").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetBindingAction("F")"#).unwrap(),
|
||||
"JUMP",
|
||||
"Cancel puts the account set back"
|
||||
);
|
||||
assert_eq!(s.current_binding_set(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_addons_bindings_xml_registers_as_ordinary_rows() {
|
||||
let s = script();
|
||||
|
||||
@@ -393,7 +393,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let text = model.minimap.active_zoom().to_string();
|
||||
super::cvars::set_from_engine(&mut model, cvar, text);
|
||||
}
|
||||
Ok(())
|
||||
drop(model);
|
||||
// `set_zoom`'s `CVar::Set` (`0x6da974`) runs the row's callback inside the call.
|
||||
super::cvars::written(lua)
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
|
||||
@@ -559,7 +559,7 @@ pub(crate) struct Model {
|
||||
pub(crate) current_resolution: Option<usize>,
|
||||
/// What this device and presentation path offer, behind `GetVideoCaps`.
|
||||
pub(crate) video_caps: super::cvars::VideoCaps,
|
||||
/// `RestartGx()` calls, the video window's apply button.
|
||||
/// The `gxRestart` runs no call took, a bare VM's, for the host's frame drain.
|
||||
pub(crate) restart_gx_asks: u32,
|
||||
|
||||
/// Globals `RegisterForSave` declared, in order: written at logout and read back at load.
|
||||
@@ -1051,8 +1051,6 @@ pub(crate) struct Model {
|
||||
/// slots 1..=19 as the doll numbers them: the slot getters' other-unit leg (`0x4c8420` →
|
||||
/// `0x5f0d60`).
|
||||
pub(crate) visible_equipment: HashMap<u64, char_stats::InventorySlots>,
|
||||
/// `DressUpModel` `SetUnit`, `Dress`, `Undress` and `TryOn` calls, applied in call order.
|
||||
pub(crate) dressup_intents: Vec<super::dressup::DressUpIntent>,
|
||||
/// Tabard designs per `TabardModel`, the app's preview five, the host facts and queued calls.
|
||||
pub(crate) tabard_designs: HashMap<crate::widget::FrameHandle, [i32; 5]>,
|
||||
pub(crate) tabard_preview: Option<[i32; 5]>,
|
||||
@@ -1707,7 +1705,6 @@ impl Model {
|
||||
inventory_alerts: [0; 12],
|
||||
weapon_enchants: [None; 2],
|
||||
visible_equipment: HashMap::new(),
|
||||
dressup_intents: Vec::new(),
|
||||
tabard_designs: HashMap::new(),
|
||||
tabard_preview: None,
|
||||
tabard_host: Default::default(),
|
||||
|
||||
@@ -122,6 +122,7 @@ pub enum PvpCall {
|
||||
|
||||
impl super::UiScript {
|
||||
/// The `TogglePVP` calls a harness's VM kept ([`Self::keep_calls`]).
|
||||
#[cfg(test)]
|
||||
pub fn take_pvp_toggles(&mut self) -> u32 {
|
||||
self.take_calls_like(&super::ScriptCall::Pvp(PvpCall::Toggle)) as u32
|
||||
}
|
||||
|
||||
@@ -333,6 +333,7 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
/// The `CloseTradeSkill` calls a harness's VM kept ([`Self::keep_calls`]).
|
||||
#[cfg(test)]
|
||||
pub fn take_trade_skill_closes(&mut self) -> usize {
|
||||
self.take_calls_where(|call| match call {
|
||||
super::ScriptCall::TradeSkill(TradeSkillCall::Close) => Some(()),
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::borrow::Cow;
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use super::{Model, NearestMode, ScriptCall};
|
||||
use super::{Model, ScriptCall};
|
||||
|
||||
/// A selection ask from Lua, queued in call order for the app to resolve and commit. The reference
|
||||
/// routes `TargetUnit`, `AssistUnit` and `TargetLastEnemy` through one helper (`0x489a40`: commit,
|
||||
@@ -361,6 +361,7 @@ impl super::UiScript {
|
||||
/// The guids `UnitName` read off a unit with no object and found no name for, which the
|
||||
/// reference's lookup asks the server for (`0x5171eb`), as a harness's VM kept them
|
||||
/// ([`Self::keep_calls`]), in call order.
|
||||
#[cfg(test)]
|
||||
pub fn take_unit_name_asks(&mut self) -> Vec<u64> {
|
||||
self.take_calls_where(|call| match call {
|
||||
super::ScriptCall::RecordAsk(super::RecordAsk::PlayerName(guid)) => Some(*guid),
|
||||
@@ -536,7 +537,8 @@ impl super::UiScript {
|
||||
/// Take the `TargetNearest*([reverse])` calls out of the call stream, `true` for reverse.
|
||||
/// They name no unit: the reference runs the TAB cycler (`0x493f60`) straight into
|
||||
/// `SetSelection`.
|
||||
pub fn take_target_nearest_requests(&mut self) -> Vec<(NearestMode, bool)> {
|
||||
#[cfg(test)]
|
||||
pub fn take_target_nearest_requests(&mut self) -> Vec<(super::NearestMode, bool)> {
|
||||
self.take_calls_where(|c| match c {
|
||||
ScriptCall::TargetNearest { mode, reverse, .. } => Some((*mode, *reverse)),
|
||||
_ => None,
|
||||
@@ -544,6 +546,7 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
/// Take the `TargetByName(name, exactMatch)` calls out of the call stream.
|
||||
#[cfg(test)]
|
||||
pub fn take_target_by_name_requests(&mut self) -> Vec<(String, bool)> {
|
||||
self.take_calls_where(|c| match c {
|
||||
ScriptCall::TargetByName { name, exact, .. } => Some((name.clone(), *exact)),
|
||||
|
||||
@@ -178,8 +178,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}
|
||||
Write::TruncOneMinus(name) => (name, ftol_low(1.0 - v).to_string()),
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
write_cvar(&mut model, name, text);
|
||||
write_cvar(
|
||||
&mut lua.app_data_mut::<Model>().expect("model app_data"),
|
||||
name,
|
||||
text,
|
||||
);
|
||||
// `CVar::Set` (`0x488f30` and its siblings) runs the row's callback inside the
|
||||
// call.
|
||||
super::cvars::written(lua)?;
|
||||
// Zero return values, not nil (`xor eax,eax` at every `ret`).
|
||||
Ok(MultiValue::new())
|
||||
})?,
|
||||
|
||||
+20
-21
@@ -36,7 +36,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- [benilla-lua](#benilla-lua): 11
|
||||
- [benilla-protocol](#benilla-protocol): 29
|
||||
- [benilla-srp](#benilla-srp): 2
|
||||
- [benilla-ui](#benilla-ui): 135
|
||||
- [benilla-ui](#benilla-ui): 134
|
||||
- [benilla-world](#benilla-world): 49
|
||||
|
||||
### benilla-app
|
||||
@@ -75,8 +75,8 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
#### bindings
|
||||
|
||||
- A press with Super (Cmd) held runs no binding, so a system chord such as Cmd+Tab or Cmd+Q never acts in game. ([`src/bindings.rs:19`](../crates/benilla-app/src/bindings.rs#L19))
|
||||
- A trackpad's `Pixel` deltas are carried until they make a line, and step at most once per event (`crate::ui_script::Record::Wheel`). ([`src/bindings.rs:522`](../crates/benilla-app/src/bindings.rs#L522))
|
||||
- Latches die with the VM that made them, because a latch names a command of that VM's table and releasing it against a new one would run another load's up-half. ([`src/bindings.rs:598`](../crates/benilla-app/src/bindings.rs#L598))
|
||||
- A trackpad's `Pixel` deltas are carried until they make a line, and step at most once per event (`crate::ui_script::Record::Wheel`). ([`src/bindings.rs:525`](../crates/benilla-app/src/bindings.rs#L525))
|
||||
- Latches die with the VM that made them, because a latch names a command of that VM's table and releasing it against a new one would run another load's up-half. ([`src/bindings.rs:601`](../crates/benilla-app/src/bindings.rs#L601))
|
||||
- The Windows key table stops at `VK_F12` and drops later F-keys; they are named here so a stored `F13`-`F24` can be pressed. ([`src/bindings/chord.rs:165`](../crates/benilla-app/src/bindings/chord.rs#L165))
|
||||
- On the Mac and Linux a key its platform reports no character for keeps its US name too, where the Mac reference drops it (`0x8a4aa`), and a Linux dead key not yet pressed bare stays bindable. ([`src/bindings/layout.rs:116`](../crates/benilla-app/src/bindings/layout.rs#L116))
|
||||
- A character above U+00FF names the key by itself. The reference's `MapVirtualKeyA` returns an ANSI code-page byte, which `0x42da4b` drops past `0xff` and the namer's UTF-8 encoder (`0x41abb0`) otherwise reads as Latin-1, so such a key is mislabelled or dropped; named by what it types, it stays bindable and readable. ([`src/bindings/layout.rs:216`](../crates/benilla-app/src/bindings/layout.rs#L216))
|
||||
@@ -322,7 +322,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- A missing global prints nothing, where the reference prints an empty line (`0x703c02`), because benilla's other `DisplayError` routes do the same. ([`src/ui_chat/broadcast.rs:167`](../crates/benilla-app/src/ui_chat/broadcast.rs#L167))
|
||||
- In a capital with a blank city word the walk composes `"Trade - "` (it tests the row pointer, not the string, `0x49a2e4`-`0x49a2ed`); we decline, because a wrong channel name is joined silently and for good. ([`src/ui_chat/channels.rs:141`](../crates/benilla-app/src/ui_chat/channels.rs#L141))
|
||||
- The reference holds every line that waits on its `$`-macro subject's name in the name cache's callback list (`0x49daea` → `0x55f080`) until the name answers, with no bound; here at most `MAX_HELD_SUBJECT_LINES` wait, the oldest dropped past that, as a subject no player owns never answers. ([`src/ui_chat/feed.rs:391`](../crates/benilla-app/src/ui_chat/feed.rs#L391))
|
||||
- A probe's bare `/assist` assists the live selection, where the stock handler (`ChatFrame.lua:740`) resolves a selected player's name through `AssistByName`, because a name resolve can land on a different player. ([`src/ui_chat/input/mod.rs:595`](../crates/benilla-app/src/ui_chat/input/mod.rs#L595))
|
||||
- A probe's bare `/assist` assists the live selection, `AssistByName("")` (`0x489c89`), where the stock handler (`ChatFrame.lua:740`) resolves a selected player's name through `AssistByName`, because a name resolve can land on a different player. ([`src/ui_chat/input/mod.rs:594`](../crates/benilla-app/src/ui_chat/input/mod.rs#L594))
|
||||
- A line of 256 bytes or more, which reaches here only through `chat_send`'s deviation for `0x49f604`, is slurred whole instead of cut to that buffer, so it reaches the server whole for the refusal that deviation leaves to it. ([`src/ui_chat/input/send.rs:232`](../crates/benilla-app/src/ui_chat/input/send.rs#L232))
|
||||
- The files live in `benilla-config/Logs/`, not the install's `Logs`, because the install is read-only. ([`src/ui_chat/logging.rs:4`](../crates/benilla-app/src/ui_chat/logging.rs#L4))
|
||||
- No re-run when a remote player streams in, because the reference's re-run only repeats the join, which vmangos swallows, or the leave, whose "Not on channel" notice nothing prints. ([`src/ui_chat/recruitment.rs:7`](../crates/benilla-app/src/ui_chat/recruitment.rs#L7))
|
||||
@@ -331,7 +331,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
|
||||
#### ui_dressup
|
||||
|
||||
- The base is the player's melee pair, held drawn, because the booth shares the select screen's assembly; the reference clones the live model with its attachments, drawn or sheathed (`0x5059a0`, `0x70ea00`). ([`src/ui_dressup.rs:43`](../crates/benilla-app/src/ui_dressup.rs#L43))
|
||||
- The base is the player's melee pair, held drawn, because the booth shares the select screen's assembly; the reference clones the live model with its attachments, drawn or sheathed (`0x5059a0`, `0x70ea00`). ([`src/ui_dressup.rs:44`](../crates/benilla-app/src/ui_dressup.rs#L44))
|
||||
|
||||
#### ui_duel
|
||||
|
||||
@@ -416,7 +416,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
|
||||
#### ui_models
|
||||
|
||||
- The reference's paint (`0x76d240`) reads the eye before it rebuilds the root, so a facing change draws one frame late; not reproduced, because it is an ordering quirk. ([`src/ui_models/mod.rs:1467`](../crates/benilla-app/src/ui_models/mod.rs#L1467))
|
||||
- The reference's paint (`0x76d240`) reads the eye before it rebuilds the root, so a facing change draws one frame late; not reproduced, because it is an ordering quirk. ([`src/ui_models/mod.rs:1468`](../crates/benilla-app/src/ui_models/mod.rs#L1468))
|
||||
|
||||
#### ui_pet
|
||||
|
||||
@@ -502,7 +502,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
|
||||
- `gxWindow "0"` is borderless fullscreen, not 1.12's exclusive mode-set at `gxResolution`, because Wayland has no client mode-setting (winit ignores `Fullscreen::Exclusive` there), and X11's XRandR changes the desktop's mode and a crash leaves it changed. ([`src/video.rs:5`](../crates/benilla-app/src/video.rs#L5))
|
||||
- On Windows, NVIDIA's Vulkan driver (610.88 seen) crashes inside `vkQueuePresentKHR` on the first present after any swapchain resize while the present mode is `Fifo`, `FifoRelaxed` or `AutoVsync` (a maximize is a resize). ([`src/video.rs:17`](../crates/benilla-app/src/video.rs#L17))
|
||||
- Sizes are logical px, because the windowed `gxResolution` is a logical inner size here. ([`src/video.rs:606`](../crates/benilla-app/src/video.rs#L606))
|
||||
- Sizes are logical px, because the windowed `gxResolution` is a logical inner size here. ([`src/video.rs:624`](../crates/benilla-app/src/video.rs#L624))
|
||||
|
||||
#### vplates
|
||||
|
||||
@@ -742,7 +742,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- `pow(10, n)` is the platform's, where the reference's (`0x73f940`, `fyl2x`) rounds its extended-precision steps at 53 bits, so a power or a fraction term can differ in the last place. ([`src/script/binding_abi.rs:281`](../crates/benilla-ui/src/script/binding_abi.rs#L281))
|
||||
- The disabled-checked leg (`0x785380`) tests and reloads the checked slot (`[+0x4e0]`, `0x785388`/`0x7853a5`), a copy of the checked leg's (`0x785270`), so in 1.12.1 a check box with a checked texture repaints that one and never gets its disabled-checked texture from a path. benilla tests and reloads the slot the method names. ([`src/script/button.rs:621`](../crates/benilla-ui/src/script/button.rs#L621))
|
||||
- The slot is read before the handler, where the reference reads it after (`[esi+0x4dc]`), because reading first keeps the borrow simple and no stock handler re-slots its row. ([`src/script/button.rs:1209`](../crates/benilla-ui/src/script/button.rs#L1209))
|
||||
- The reference's only bound is `luaD_call`'s count of 200 nested calls (`0x6f65ae`), past which it raises `C stack overflow` (`0x6f65bf`); eight here, as each level keeps an apply's frame on the Rust stack. ([`src/script/calls.rs:698`](../crates/benilla-ui/src/script/calls.rs#L698))
|
||||
- The reference's only bound is `luaD_call`'s count of 200 nested calls (`0x6f65ae`), past which it raises `C stack overflow` (`0x6f65bf`); eight here, as each level keeps an apply's frame on the Rust stack. ([`src/script/calls.rs:707`](../crates/benilla-ui/src/script/calls.rs#L707))
|
||||
- The reference composes the Shortcut with `GetRealZoneText` alone (`0x49efe8`), so in a capital `/leave Trade` sends `Trade - Stormwind City` and leaves nothing; ours uses the join's city-aware name, because the reference's form cannot leave the channel it joined. ([`src/script/channel.rs:516`](../crates/benilla-ui/src/script/channel.rs#L516))
|
||||
- The frame's bottom-left origin is snapped onto the device grid so the border art blits 1:1; the reference ceils the anchor point itself onto it (`0x7666a0`, `0x766750`). ([`src/script/chat_bubble.rs:482`](../crates/benilla-ui/src/script/chat_bubble.rs#L482))
|
||||
- No truncation at 255 characters (`0x49f604`), because vmangos refuses a longer line itself (`Chat.cpp:2177`): it is dropped where the reference sends its first 255. ([`src/script/chat_send.rs:26`](../crates/benilla-ui/src/script/chat_send.rs#L26))
|
||||
@@ -765,13 +765,12 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- The list is the window sizes this client can present, not the device's display modes, because benilla makes no exclusive mode-set. `"WxH"` is a contract: `OptionsFrame.lua:281-284` splits on the `x`, raising on a string without one, and `CT_Viewport.lua:107` matches `"(%d+)x(%d+)"`, failing silently. ([`src/script/cvars.rs:81`](../crates/benilla-ui/src/script/cvars.rs#L81))
|
||||
- The tie-break is ours, because area alone with the reference's adjacent-only dedupe can leave a duplicate `"WxH"` when two modes share an area (1280x960, 1600x768). ([`src/script/cvars.rs:94`](../crates/benilla-ui/src/script/cvars.rs#L94))
|
||||
- An id out of range is ignored, because the reference reads one entry past the last (`0x48c67c`) and a clamp would apply a format the player did not pick. ([`src/script/cvars.rs:779`](../crates/benilla-ui/src/script/cvars.rs#L779))
|
||||
- The reference then runs the console command `gxRestart` (`0x842978` through `0x63ce00`); none is queued here, because `gxMultisample` applies at next launch anyway: the camera reads its sample count once, at spawn. ([`src/script/cvars.rs:793`](../crates/benilla-ui/src/script/cvars.rs#L793))
|
||||
- The reference (`0x48dad0` into `0x639a20`) writes hardware-recommended values from its matched `VideoHardware.dbc` row and the `CGxFormat` preset it names (`[0xc4e6ac]`): twelve `gx*` CVars, a synchronous `RestartGx`, then sixteen more. benilla has no such matching, so its registered defaults are its recommended configuration. ([`src/script/cvars.rs:855`](../crates/benilla-ui/src/script/cvars.rs#L855))
|
||||
- The index is of the live window size, not of the `gxResolution` CVar the reference parses (`0x48bf20`), because the window here can be dragged to any size, where the reference's backbuffer stays at `gxResolution`'s, and `CT_Viewport` scales by the live one. ([`src/script/cvars.rs:908`](../crates/benilla-ui/src/script/cvars.rs#L908))
|
||||
- 0, a negative or an index past the count clamps to the nearer end, because the reference reads one element past the last there (`0x48c00d`), uninitialised memory. ([`src/script/cvars.rs:926`](../crates/benilla-ui/src/script/cvars.rs#L926))
|
||||
- Always empty, because a refresh rate is only selectable through an exclusive mode-set and benilla has none. ([`src/script/cvars.rs:967`](../crates/benilla-ui/src/script/cvars.rs#L967))
|
||||
- 1.12 has no such CVar; it exists because our Graphics page needs somewhere to keep the stop, and takes the API's name. ([`src/script/cvars.rs:1086`](../crates/benilla-ui/src/script/cvars.rs#L1086))
|
||||
- The getter reads `CVAR_WORLD_DETAIL`, where the reference's reads only `smallCull` (`0x488d77`), because benilla's stop follows the knob that draws: a console `frillDensity` write moves the ground clutter and `WorldDetail` with it, and no renderer reads `smallCull` (its callback's `[0x868620]` has no reader). ([`src/script/cvars.rs:1114`](../crates/benilla-ui/src/script/cvars.rs#L1114))
|
||||
- The reference (`0x48dad0` into `0x639a20`) writes hardware-recommended values from its matched `VideoHardware.dbc` row and the `CGxFormat` preset it names (`[0xc4e6ac]`): twelve `gx*` CVars, a synchronous `RestartGx`, then sixteen more. benilla has no such matching, so its registered defaults are its recommended configuration. ([`src/script/cvars.rs:895`](../crates/benilla-ui/src/script/cvars.rs#L895))
|
||||
- The index is of the live window size, not of the `gxResolution` CVar the reference parses (`0x48bf20`), because the window here can be dragged to any size, where the reference's backbuffer stays at `gxResolution`'s, and `CT_Viewport` scales by the live one. ([`src/script/cvars.rs:948`](../crates/benilla-ui/src/script/cvars.rs#L948))
|
||||
- 0, a negative or an index past the count clamps to the nearer end, because the reference reads one element past the last there (`0x48c00d`), uninitialised memory. ([`src/script/cvars.rs:966`](../crates/benilla-ui/src/script/cvars.rs#L966))
|
||||
- Always empty, because a refresh rate is only selectable through an exclusive mode-set and benilla has none. ([`src/script/cvars.rs:1010`](../crates/benilla-ui/src/script/cvars.rs#L1010))
|
||||
- 1.12 has no such CVar; it exists because our Graphics page needs somewhere to keep the stop, and takes the API's name. ([`src/script/cvars.rs:1118`](../crates/benilla-ui/src/script/cvars.rs#L1118))
|
||||
- The getter reads `CVAR_WORLD_DETAIL`, where the reference's reads only `smallCull` (`0x488d77`), because benilla's stop follows the knob that draws: a console `frillDensity` write moves the ground clutter and `WorldDetail` with it, and no renderer reads `smallCull` (its callback's `[0x868620]` has no reader). ([`src/script/cvars.rs:1146`](../crates/benilla-ui/src/script/cvars.rs#L1146))
|
||||
- The promotion's IME legs (`0x77d417`–`0x77d431`) and the IME writer's `or 6` (`0x77acde`, in `0x77ac60`) are not modelled: benilla has no IME composition. ([`src/script/editbox/flush.rs:28`](../crates/benilla-ui/src/script/editbox/flush.rs#L28))
|
||||
- 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. ([`src/script/extract.rs:223`](../crates/benilla-ui/src/script/extract.rs#L223))
|
||||
- A host of 0x80 bytes or more is refused. The reference copies the host into a 0x80-byte buffer (`0x5abc7c`-`0x5abc9c`) and writes its terminator one byte past the end, and the cut host could end in a whitelisted suffix the real one does not; this takes the safe side. ([`src/script/glue_core.rs:424`](../crates/benilla-ui/src/script/glue_core.rs#L424))
|
||||
@@ -782,10 +781,10 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- The backpack, the bank and the keyring are each bounded by their own slots, where `0x4f990f` bounds them only by the player list's count (0x71) from their bases (the bank at `+0x27`, `0x4f98ae`; the keyring at `+0x51`, `0x4f98c0`), a reference bug that lets their index read the next container's slots. ([`src/script/held.rs:74`](../crates/benilla-ui/src/script/held.rs#L74))
|
||||
- A span count that no longer matches the live set always rebuilds, so a state this model does not flag (a ring wiped by `SetMaxLines`, a hidden frame, a frame not yet laid out, which the reference's rebuild skips at `0x78af7b`) leaves no stale button. ([`src/script/hyperlink.rs:167`](../crates/benilla-ui/src/script/hyperlink.rs#L167))
|
||||
- `IsInInstance()` then answers `nil, "none"`, because the reference returns two results without pushing either (`0x48a7a9`), a stack bug. ([`src/script/instance.rs:24`](../crates/benilla-ui/src/script/instance.rs#L24))
|
||||
- Bounds-checked where the reference indexes unguarded, so none reads past it. ([`src/script/item_stats.rs:588`](../crates/benilla-ui/src/script/item_stats.rs#L588))
|
||||
- Nil for a display row with no icon, where the reference answers the unloadable `Interface\Icons\`, on which `SetTexture` keeps the previous image (`0x7702ac`), so a reused button would show its old icon. ([`src/script/item_stats.rs:740`](../crates/benilla-ui/src/script/item_stats.rs#L740))
|
||||
- Bounds-checked where the reference indexes unguarded, so none reads past it. ([`src/script/item_stats.rs:578`](../crates/benilla-ui/src/script/item_stats.rs#L578))
|
||||
- Nil for a display row with no icon, where the reference answers the unloadable `Interface\Icons\`, on which `SetTexture` keeps the previous image (`0x7702ac`), so a reused button would show its old icon. ([`src/script/item_stats.rs:730`](../crates/benilla-ui/src/script/item_stats.rs#L730))
|
||||
- A key rebound to the command it already has keeps a duplicate index in 1.12.1 (`0x4b7786` lowers the other keys' indices before `0x4b77ac` returns on the same name), so both getters there answer one key too few; that client fault is not reproduced. ([`src/script/keybind.rs:223`](../crates/benilla-ui/src/script/keybind.rs#L223))
|
||||
- An addon file whose first binding has no header opens a `HEADER_<addon>` row, where the reference files those rows under the previous file's last header, because under a foreign header the player could not find them. ([`src/script/keybind.rs:326`](../crates/benilla-ui/src/script/keybind.rs#L326))
|
||||
- An addon file whose first binding has no header opens a `HEADER_<addon>` row, where the reference files those rows under the previous file's last header, because under a foreign header the player could not find them. ([`src/script/keybind.rs:324`](../crates/benilla-ui/src/script/keybind.rs#L324))
|
||||
- The handle's value is the address of a `CGxTex` (`SMemAlloc(0x54)`, `0x594a96`), which benilla cannot reproduce; each texture ranks instead by a creation stamp, one global sequence number taken when its cache key is first created and taken anew once its last region has let go of it (`super::texel::Texels`). ([`src/script/layer_batch.rs:9`](../crates/benilla-ui/src/script/layer_batch.rs#L9))
|
||||
- The reference's byte copy can end inside a character and keep the stray bytes; a `String` must stay UTF-8, so the cut stops at the character boundary. ([`src/script/macros.rs:135`](../crates/benilla-ui/src/script/macros.rs#L135))
|
||||
- A listing that finds no icon stays empty, to be listed again on the next ask, where the reference's dedup then reads below its null list (`0x4f0134`-`0x4f0145`) and faults. ([`src/script/macros.rs:555`](../crates/benilla-ui/src/script/macros.rs#L555))
|
||||
@@ -798,9 +797,9 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- The reference adds this rim over the bar; we brighten the bar (`LIT_BOOST`), because the rim reads as hard edge lines in our linear-blending pipeline. ([`src/script/nameplate.rs:991`](../crates/benilla-ui/src/script/nameplate.rs#L991))
|
||||
- A wrapper that decodes to no live widget takes `default_parent`, because the reference, with no `IsA` check, reads a wild pointer. ([`src/script/object/anchor_args.rs:218`](../crates/benilla-ui/src/script/object/anchor_args.rs#L218))
|
||||
- An `OnSizeChanged` that resolve produces fires at the next `UiScript::resolve`, because a Lua handler must not run in the middle of a level compaction. ([`src/script/object/toplevel.rs:77`](../crates/benilla-ui/src/script/object/toplevel.rs#L77))
|
||||
- A third `%s` gets an empty string, because the reference pushes two varargs (`0x6093f2 add esp,0x14`) and would read uninitialised stack. ([`src/script/pvp/mod.rs:318`](../crates/benilla-ui/src/script/pvp/mod.rs#L318))
|
||||
- A token resolving to guid 0 answers nil too. The reference jumps from the guid test (`0x5172f6`) to `return 1` (`0x517343`) with nothing pushed, so the one value is its top argument, the token it read (`0x5172e6`); that is a stack quirk, not an answer. ([`src/script/pvp/mod.rs:567`](../crates/benilla-ui/src/script/pvp/mod.rs#L567))
|
||||
- With `UNIT_PVP_NAME` or the title missing from `_G` (no install) this answers the plain name, where the reference would format an empty string, because the decoration is install data. ([`src/script/pvp/mod.rs:571`](../crates/benilla-ui/src/script/pvp/mod.rs#L571))
|
||||
- A third `%s` gets an empty string, because the reference pushes two varargs (`0x6093f2 add esp,0x14`) and would read uninitialised stack. ([`src/script/pvp/mod.rs:319`](../crates/benilla-ui/src/script/pvp/mod.rs#L319))
|
||||
- A token resolving to guid 0 answers nil too. The reference jumps from the guid test (`0x5172f6`) to `return 1` (`0x517343`) with nothing pushed, so the one value is its top argument, the token it read (`0x5172e6`); that is a stack quirk, not an answer. ([`src/script/pvp/mod.rs:568`](../crates/benilla-ui/src/script/pvp/mod.rs#L568))
|
||||
- With `UNIT_PVP_NAME` or the title missing from `_G` (no install) this answers the plain name, where the reference would format an empty string, because the decoration is install data. ([`src/script/pvp/mod.rs:572`](../crates/benilla-ui/src/script/pvp/mod.rs#L572))
|
||||
- An in-flight name reads `...`, where the reference's reads `" "` (`0x82ee00`), because a blank looks like a nameless objective. ([`src/script/quest_log.rs:54`](../crates/benilla-ui/src/script/quest_log.rs#L54))
|
||||
- One signal per missed record, where the reference runs every callback of every miss as its record lands (items at `0x55bea8` and `0x55bf39` in `0x55bdb0`, from `0x555140`; `0x4e083a`), 2^k - 1 updates for a walking handler over k first-seen records. ([`src/script/quest_log.rs:1212`](../crates/benilla-ui/src/script/quest_log.rs#L1212))
|
||||
- A local clock step mid-session (an NTP correction) jumps the reference's countdown but not ours, because the step changes no time the quest has left. ([`src/script/quest_log.rs:1282`](../crates/benilla-ui/src/script/quest_log.rs#L1282))
|
||||
|
||||
Reference in New Issue
Block a user