- follow a player and the screen names who you're following, the right-click menu grows its Follow row, and your own movement is what cancels it — creatures are refused like the real client - a locked lockbox click asks the server instead of refusing silently, a quest-starter chest offers its quest before opening, and an item on cooldown hides its green "open" line - a parried swing plays its clang like the real client keys it - the README's What works / Not built yet caught up on a week of shipping Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
92 lines
4.5 KiB
Rust
92 lines
4.5 KiB
Rust
//! The auto-follow **Era API surface** — two globals, no state.
|
|
//!
|
|
//! [`super::duel`]'s shape exactly: everything the UI *reads* about a follow arrives as an event
|
|
//! argument (`AUTOFOLLOW_BEGIN`'s name), so there is no snapshot to push — only the outbound half.
|
|
//! Each call queues a [`FollowRequest`] the app drains ([`super::UiScript::take_follow_requests`])
|
|
//! and turns into its own follow start, keeping the engine free of ECS reach (decision 0068 §3).
|
|
//!
|
|
//! The two are the reference's own follow bindings, and between them they cover every shipped call
|
|
//! site in the 1.12 FrameXML:
|
|
//!
|
|
//! | binding | called by | subject |
|
|
//! |---|---|---|
|
|
//! | `FollowUnit(unit)` | `SlashCmdList["FOLLOW"]` with no argument | a unit token |
|
|
//! | `FollowByName(name, exactMatch)` | `SlashCmdList["FOLLOW"]` with one; `UnitPopup_OnClick`'s FOLLOW row | a name |
|
|
//!
|
|
//! `FollowByName` is `0x489ec0`, one more caller of the shared by-name resolver `0x493aa0` —
|
|
//! typemask `0x10` (players only) and filter mode 2 (`CanAssist` + alive), with **the second Lua
|
|
//! argument steering exact-only matching** (decision 0886's table). That argument is what makes the
|
|
//! popup row different from the slash command: `UnitPopup.lua` passes `FollowByName(name, 1)`, so
|
|
//! clicking *Follow* on a menu that already names the unit exactly must not prefix-match its way
|
|
//! onto somebody else standing nearby, while `/follow rag` still may.
|
|
//!
|
|
//! benilla parses slash lines in Rust rather than in Lua (decision 0881), so the chat half of that
|
|
//! table enters the app's follow funnel directly; these globals are the *other* half — the shipped
|
|
//! UI's, and any addon's.
|
|
|
|
use mlua::Lua;
|
|
|
|
use super::Model;
|
|
|
|
/// Outbound follow intents queued by the Era API calls, drained by the app
|
|
/// ([`super::UiScript::take_follow_requests`]). Plain data — [`super::duel::DuelRequest`]'s twin.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum FollowRequest {
|
|
/// `FollowUnit(unit)` — follow whoever a unit token points at. The app resolves the token;
|
|
/// `"target"` is the bare `/follow`, and takes the selection **whatever it is** (the reference
|
|
/// applies no typemask on this path — decision 0890).
|
|
ByUnit(String),
|
|
/// `FollowByName(name, exactMatch)` — follow a player found by name. `exact` is the second Lua
|
|
/// argument: set, it skips the resolver's longest-common-prefix tier and admits only a
|
|
/// whole-string (case-insensitive) hit.
|
|
ByName { name: String, exact: bool },
|
|
}
|
|
|
|
impl super::UiScript {
|
|
/// Drain the follow intents queued since the last call.
|
|
pub fn take_follow_requests(&mut self) -> Vec<FollowRequest> {
|
|
std::mem::take(&mut self.model_mut().follow_requests)
|
|
}
|
|
}
|
|
|
|
/// Register the follow globals (the same style/place [`super::duel`] registers its own).
|
|
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
|
let g = lua.globals();
|
|
|
|
// FollowUnit(unit) — `SlashCmdList["FOLLOW"]`'s no-argument arm, `FollowUnit("target")`.
|
|
g.set(
|
|
"FollowUnit",
|
|
lua.create_function(|lua, unit: String| {
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
model.follow_requests.push(FollowRequest::ByUnit(unit));
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
|
|
// FollowByName(name, exactMatch) — the unit popup's Follow row passes `1`; the slash command
|
|
// passes nothing, which is the prefix-matching case (`nil` is false here, as in the ref, where
|
|
// the argument reaches `0x493aa0` as a plain zero).
|
|
g.set(
|
|
"FollowByName",
|
|
lua.create_function(|lua, (name, exact): (String, Option<mlua::Value>)| {
|
|
// Absent is the slash command's case; the popup row passes `1`. A numeric zero counts
|
|
// as false as well — no shipped caller passes one, and treating it that way is the
|
|
// reading both plausible arg-2 fetches on the C side (`lua_toboolean` vs a number
|
|
// compare) agree on for every value that can actually arrive here.
|
|
let exact = match exact {
|
|
None | Some(mlua::Value::Nil) | Some(mlua::Value::Boolean(false)) => false,
|
|
Some(mlua::Value::Integer(n)) => n != 0,
|
|
Some(mlua::Value::Number(n)) => n != 0.0,
|
|
Some(_) => true,
|
|
};
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
model
|
|
.follow_requests
|
|
.push(FollowRequest::ByName { name, exact });
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|