//! The selection calls of the script call stream ([`crate::script_calls`]), each committed through //! the one SetSelection path when its turn comes, so a later call in the same pass reads it. use bevy::prelude::*; use benilla_ui::script::SelectionRequest; use super::by_name::{self, ByNameScan, SelectCommit}; use super::scan; /// Everything the selection calls read and write. #[derive(bevy::ecs::system::SystemParam)] pub(crate) struct ScriptSelect<'w, 's> { /// The one unit-token resolver, shared with the reach feed so `TargetUnit("target")` and /// `CheckInteractDistance("target", …)` name the same unit. tokens: crate::ui_unit::UnitTokens<'w, 's>, /// `TargetLastEnemy`'s memory (`[0xb4e2e8]`), stamped by `scan::remember_last_enemy`. last_enemy: Res<'w, scan::LastEnemy>, /// The SetSelection tail shared with the world click, `/target` and `/assist`, classification /// included. commit: SelectCommit<'w, 's>, by_name: ByNameScan<'w, 's>, /// The TAB cycler's inputs, for the `TargetNearest*` calls. scan: scan::TargetScan<'w, 's>, history: ResMut<'w, scan::TabHistory>, time: Res<'w, Time>, } impl ScriptSelect<'_, '_> { /// `TargetUnit` (`0x4899d0`), `AssistUnit` (`0x489b80`) and `TargetLastEnemy` share the /// reference's helper `0x489a40`: a resolved unit or a roster member is committed, anything /// else is a no-op, never a deselect. Here a token must name a streamed unit, so an /// out-of-range roster member is a no-op (the guid-only selection is not built). pub(crate) fn select(&mut self, request: SelectionRequest) { match request { SelectionRequest::Unit(token) => { if let Some((entity, guid)) = self.tokens.resolve(&token, &self.commit.selection) { self.commit.commit(entity, guid); } } // `0x489ba9 call 0x515940(token)`, then the shared tail. An unresolvable token is // silent here; the reference shows message `0xb8` `ERR_GENERIC_NO_TARGET` (`0x489c0e`). SelectionRequest::Assist(token) => { match self.tokens.resolve(&token, &self.commit.selection) { Some((basis, _)) => self.commit.assist(basis, "AssistUnit"), None => info!("assist (AssistUnit): \"{token}\" names nothing; silent no-op"), } } // `0x489c40`, `/assist `: a player name, prefix-matched by the app's name scan. SelectionRequest::AssistByName(name) => { by_name::assist_named(&self.by_name, &mut self.commit, Some(&name), "AssistByName"); } // `0x489b45` → `0x489a40`. Empty memory is a no-op (the shim `0x489b40` tests nothing), // unlike `TargetLastTarget 0x489b00`, which reaches `0x493540(0,0)` and deselects. SelectionRequest::LastEnemy => { let Some(guid) = self.last_enemy.0 else { info!("TargetLastEnemy: nothing hostile has been targeted yet; no-op"); return; }; match self.tokens.held(guid) { Some((entity, guid)) => self.commit.commit(entity, guid), // Stale: a no-op, and the memory stays, as the reference never clears it. None => info!( "TargetLastEnemy: {guid:#x} is no longer streamed; target left untouched" ), } } } } /// `TargetByName(name, exactMatch)` (`0x489d60`), the stock `/target`. pub(crate) fn target_by_name(&mut self, name: &str, exact: bool) { by_name::target_named(&self.by_name, &mut self.commit, name, exact); } /// `TargetNearest*([reverse])` (`0x489a80`/`0x489aa0`/`0x489ac0`/`0x489ae0` → /// `0x493f60(reverse, mode)`), one cycle of the cycler the TAB key runs. pub(crate) fn target_nearest(&mut self, mode: benilla_ui::script::NearestMode, reverse: bool) { let engaged = self.commit.engaged(); scan::cycle( scan::ScanSide::of(mode), reverse, self.time.elapsed_secs_f64(), &self.scan, &mut self.history, &mut self.commit.selection, &mut self.commit.seam, engaged, ); } /// `TargetLastTarget()` (`0x489b00`): a held last-target pair goes to `0x489a40` as a stack /// copy, so a streamed unit is committed (which stamps the outgoing target, swapping the two) /// and an unstreamed one is a no-op (the roster fallback is not built, as for /// `TargetLastEnemy`); an empty pair deselects through `0x493540(0,0)` (`0x489b2d`). pub(crate) fn target_last_target(&mut self) { match self.commit.selection.last { Some(guid) => match self.tokens.held(guid) { Some((entity, guid)) => self.commit.commit(entity, guid), None => info!("TargetLastTarget: {guid:#x} is no longer streamed; no-op"), }, None => self.commit.clear(), } } /// `ClearTarget()` (`0x489ff0`): deselect whatever the calls before it left selected. pub(crate) fn clear_target(&mut self) { self.commit.clear(); } } #[cfg(test)] mod tests { use super::*; /// The selection calls from Lua, through the binding bodies of ASSISTTARGET (`AssistUnit`) and /// TARGETLASTHOSTILE (`TargetLastEnemy`): an empty basis, a garbage token and a stale memory /// are each a no-op (`0x489a40`'s bare `ret`), never a deselect or a panic. #[test] fn the_selection_calls_run_assist_and_last_enemy_without_ever_deselecting() { use crate::net::{Guid, NetCommands, ObjectStore, SelfPlayer}; use benilla_ui::script::UiScript; use bevy::ecs::system::RunSystemOnce; const ME: u64 = 1; const BASIS: u64 = 0xB0A2; const VICTIM: u64 = 0xC0DE; const GONE: u64 = 0x6017; // `UNIT_FIELD_TARGET` is a 2-field guid at index 16; HEALTH/MAXHEALTH keep the units live. let store = |pairs: &[(u16, u32)]| ObjectStore(benilla_protocol::ObjectFields::from_pairs(pairs)); let (tx, _rx) = crossbeam_channel::unbounded(); let mut world = World::new(); world.insert_resource(NetCommands(tx)); world.init_resource::(); world.init_resource::(); world.init_resource::(); world.init_resource::(); world.init_resource::>(); world.init_resource::(); world.init_resource::(); world.init_resource::(); world.init_resource::(); world.init_resource::(); world.init_resource::(); world.init_resource::(); world.init_resource::(); world.init_resource::