/target, /assist and /follow, and right click opens a clam
- /target and /assist find units by name like the original, and /follow walks you behind your target - right click opens clams and lockboxes, and the tooltip promises it - the freeze thaws clean, poses release instead of sticking, and you drop to neutral standing before it grips - unit frame art layers over the bars like the original Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,9 @@ pub(super) enum SlashIndex {
|
||||
LootFfa,
|
||||
LootRoundRobin,
|
||||
LootMaster,
|
||||
Target,
|
||||
Assist,
|
||||
Follow,
|
||||
}
|
||||
|
||||
impl SlashIndex {
|
||||
@@ -105,13 +108,16 @@ impl SlashIndex {
|
||||
Self::LootFfa => "LOOT_FFA",
|
||||
Self::LootRoundRobin => "LOOT_ROUNDROBIN",
|
||||
Self::LootMaster => "LOOT_MASTER",
|
||||
Self::Target => "TARGET",
|
||||
Self::Assist => "ASSIST",
|
||||
Self::Follow => "FOLLOW",
|
||||
}
|
||||
}
|
||||
|
||||
/// Every registered index — the registry proper. A reference command NOT in this list resolves
|
||||
/// nowhere and answers `HELP_TEXT_SIMPLE`, exactly as an unknown command does in the reference
|
||||
/// (better than a registered handler that silently does nothing).
|
||||
const ALL: [Self; 28] = [
|
||||
const ALL: [Self; 31] = [
|
||||
Self::Reply,
|
||||
Self::Join,
|
||||
Self::Leave,
|
||||
@@ -140,6 +146,9 @@ impl SlashIndex {
|
||||
Self::LootFfa,
|
||||
Self::LootRoundRobin,
|
||||
Self::LootMaster,
|
||||
Self::Target,
|
||||
Self::Assist,
|
||||
Self::Follow,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -122,15 +122,23 @@ pub(super) struct ChatProbes<'w, 's> {
|
||||
guids: Res<'w, crate::net::GuidIndex>,
|
||||
}
|
||||
|
||||
/// The two setters `DoEmote` drives besides the packet, bundled (the drain is at Bevy's 16-param
|
||||
/// ceiling): the **posture** (`EmoteSpecProc == 1` → `SetStandState`) and the **stow**
|
||||
/// (`SetSheatheState(0, SNAP)`, unconditional on every emote that passes the gates — wow-re
|
||||
/// `sheath-policy.md` §1, site `0x5ef630`). Both are queues into their subsystem's one setter,
|
||||
/// never applied here.
|
||||
/// Everything the drain **queues into another subsystem's one setter** rather than applying itself,
|
||||
/// bundled (the drain is at Bevy's 16-param ceiling).
|
||||
///
|
||||
/// - `stand`/`sheath` — the two setters `DoEmote` drives besides the packet: the **posture**
|
||||
/// (`EmoteSpecProc == 1` → `SetStandState`) and the **stow** (`SetSheatheState(0, SNAP)`,
|
||||
/// unconditional on every emote that passes the gates — wow-re `sheath-policy.md` §1, site
|
||||
/// `0x5ef630`).
|
||||
/// - `target`/`assist` — the by-name selection asks (decision 0886), answered by
|
||||
/// [`crate::target`]'s shared resolver so they commit through the same SetSelection path a click
|
||||
/// does. Chat never writes [`Selection`] itself.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(super) struct EmoteOut<'w> {
|
||||
pub(super) struct ChatOut<'w> {
|
||||
stand: MessageWriter<'w, crate::player::StandStateRequest>,
|
||||
sheath: MessageWriter<'w, crate::creature_anim::SheathRequest>,
|
||||
target: MessageWriter<'w, crate::target::TargetByNameRequest>,
|
||||
assist: MessageWriter<'w, crate::target::AssistRequest>,
|
||||
follow: MessageWriter<'w, crate::player::FollowRequest>,
|
||||
}
|
||||
|
||||
// One parameter per concern — the chat drain fans out to every command's consumer.
|
||||
@@ -156,7 +164,7 @@ pub(super) fn drain_chat_input(
|
||||
mut go_targets: MessageWriter<crate::creature_anim::SpellGoTargets>,
|
||||
// The command table (decision 0881) — the reference's own aliases, resolved at boot.
|
||||
table: Res<super::commands::SlashCommands>,
|
||||
mut emote_out: EmoteOut,
|
||||
mut chat_out: ChatOut,
|
||||
probes: ChatProbes,
|
||||
) {
|
||||
let ChatProbes {
|
||||
@@ -495,6 +503,36 @@ pub(super) fn drain_chat_input(
|
||||
ParsedChat::Forfeit => {
|
||||
script.queue_duel_request(benilla_ui::script::DuelRequest::Cancel);
|
||||
}
|
||||
// The by-name selection pair (decision 0886). Both hand the name to `crate::target`'s
|
||||
// shared resolver — the reference's own `0x493aa0`, parameterised per caller — so the
|
||||
// commit goes through the one SetSelection path a click, TAB and `TargetUnit` share.
|
||||
//
|
||||
// A bare `/target` reproduces `GetSlashCmdTarget`'s fallback (your current target's
|
||||
// name, iff it is a player) and is therefore a no-op re-select; a bare `/target` with a
|
||||
// creature selected resolves to nothing, exactly as the reference's `if
|
||||
// GetSlashCmdTarget(msg)` guard does.
|
||||
ParsedChat::Target { name } => {
|
||||
if let Some(name) =
|
||||
name.or_else(|| target_player_name(&selection, &mut names, &commands))
|
||||
{
|
||||
chat_out
|
||||
.target
|
||||
.write(crate::target::TargetByNameRequest { name });
|
||||
}
|
||||
}
|
||||
// A bare `/assist` is the ref's `AssistUnit("target")` — assist whoever is selected,
|
||||
// creature or player — so it passes `None` straight through rather than taking the
|
||||
// player-name fallback. The ref reaches the same unit by resolving its NAME first
|
||||
// (`AssistByName`), which can pick a different same-named player; going by the live
|
||||
// selection is the same intent without that ambiguity (recorded in 0886).
|
||||
ParsedChat::Assist { name } => {
|
||||
chat_out.assist.write(crate::target::AssistRequest { name });
|
||||
}
|
||||
// `/follow` (decision 0890) — the subject is resolved by `crate::target` and the motion
|
||||
// is `crate::player`'s; chat only carries the ask. Bare follows the current selection.
|
||||
ParsedChat::Follow { name } => {
|
||||
chat_out.follow.write(crate::player::FollowRequest { name });
|
||||
}
|
||||
// The ref's handler is `SlashCmdList["PVP"] = function() TogglePVP() end` — one line
|
||||
// over the same binding the popup row calls, so it enters the same intent queue
|
||||
// (decision 0646 §3; the `/duel` reasoning above, verbatim).
|
||||
@@ -565,7 +603,7 @@ pub(super) fn drain_chat_input(
|
||||
// sheathes in the reference too. The anim layer's one setter owns the idempotency
|
||||
// refusal, so an already-stowed body costs nothing.
|
||||
if let Ok((entity, _, _)) = self_player.single() {
|
||||
emote_out.sheath.write(crate::creature_anim::SheathRequest {
|
||||
chat_out.sheath.write(crate::creature_anim::SheathRequest {
|
||||
entity,
|
||||
state: 0,
|
||||
ceremony: false,
|
||||
@@ -577,7 +615,7 @@ pub(super) fn drain_chat_input(
|
||||
// the one setter in `crate::player`, which sends `CMSG_STANDSTATECHANGE`, holds the
|
||||
// local commit until the echo lands, and runs the sit-stow rider.
|
||||
if let Some(state) = posture {
|
||||
emote_out
|
||||
chat_out
|
||||
.stand
|
||||
.write(crate::player::StandStateRequest { state: state as u8 });
|
||||
}
|
||||
|
||||
@@ -159,16 +159,42 @@ pub(in crate::ui_chat) enum ParsedChat {
|
||||
/// kick, leave, loot settings, raid marks — apply to the local mirror, so the whole menu
|
||||
/// surface is exercisable serverless.
|
||||
PartyTest { arg: String },
|
||||
/// `/target [name]` — select by name (`TargetByName`, decision 0886). Resolves creatures AND
|
||||
/// players, case-insensitively, by whole name or longest common prefix, with no range, cone,
|
||||
/// liveness or hostility gate at all. `None` = bare, which the ref's `GetSlashCmdTarget` turns
|
||||
/// into your current target's name when that target is a player (a no-op re-select), and into
|
||||
/// nothing otherwise.
|
||||
Target { name: Option<String> },
|
||||
/// `/assist [name]` — select whatever the basis unit is targeting. A named basis resolves
|
||||
/// **players only** (typemask 0x10); bare assists your current target, creature or player.
|
||||
Assist { name: Option<String> },
|
||||
/// `/follow [name]` — auto-follow (decision 0890). A named subject resolves players only, and
|
||||
/// only living assistable ones (the ref's filter mode 2); bare follows the current selection,
|
||||
/// creature or player. Client-side movement — nothing goes on the wire.
|
||||
Follow { name: Option<String> },
|
||||
/// A slash line matching neither a chat command nor an `EmotesText` name — dropped.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// The name half of the ref's `GetSlashCmdTarget` (ChatFrame.lua:650-658): a non-empty argument
|
||||
/// is the name (first word — 1.12 names carry no spaces); empty defers to the dispatch's
|
||||
/// player-target fallback.
|
||||
/// The name half of the ref's `GetSlashCmdTarget` (ChatFrame.lua:650-658) — the argument grammar
|
||||
/// **thirteen** shipped commands share, `/target` and `/assist` among them.
|
||||
///
|
||||
/// Its first line is a trim, not a tokenizer: `gsub(msg, "(%s*)(.*[^%s]+)(%s*)", "%2", 1)` keeps
|
||||
/// **everything between the outer whitespace**, internal spaces included. This used to take the
|
||||
/// first word instead, on the reasoning that 1.12 player names carry no spaces — true, but
|
||||
/// `/target` resolves *creature* names too, and "Kobold Vermin" is the common case. `args` reaches
|
||||
/// us already trimmed ([`parse_line`]), which is exactly what that gsub computes.
|
||||
///
|
||||
/// Empty defers to the caller's fallback (a bare party command acts on the selected PLAYER —
|
||||
/// `target_player_name` in [`super`], the ref's `UnitIsPlayer("target")` branch).
|
||||
///
|
||||
/// **Not carried: the unit-token expansion.** The ref's tail expands `player`, `target`,
|
||||
/// `^party[1-4]` and `^raid[0-9]` through `UnitName` before returning, so `/assist party1` means
|
||||
/// "that member's name". benilla feeds only the `player` and `target` tokens to the VM's unit
|
||||
/// store, so the roster half has nothing to resolve against yet; the tokens would pass through as
|
||||
/// literal names. Named in decision 0886 as a gap rather than half-built here.
|
||||
fn slash_target_name(args: &str) -> Option<String> {
|
||||
let name = args.split_whitespace().next().unwrap_or("");
|
||||
(!name.is_empty()).then(|| name.to_string())
|
||||
(!args.is_empty()).then(|| args.to_string())
|
||||
}
|
||||
|
||||
/// Parse a trimmed slash line into an ACTION (`/join`, `/afk`, `/random`, `/wave`, the dev
|
||||
@@ -271,6 +297,17 @@ fn slash_command(index: SlashIndex, args: &str) -> ParsedChat {
|
||||
S::Duel => ParsedChat::Duel {
|
||||
name: slash_target_name(args),
|
||||
},
|
||||
// The by-name selection pair (decision 0886). Both take the whole trimmed argument —
|
||||
// `/target Kobold Vermin` is one name, not a name plus junk.
|
||||
S::Target => ParsedChat::Target {
|
||||
name: slash_target_name(args),
|
||||
},
|
||||
S::Assist => ParsedChat::Assist {
|
||||
name: slash_target_name(args),
|
||||
},
|
||||
S::Follow => ParsedChat::Follow {
|
||||
name: slash_target_name(args),
|
||||
},
|
||||
S::DuelCancel => ParsedChat::Forfeit,
|
||||
S::Pvp => ParsedChat::Pvp,
|
||||
// The social verbs (decision 0668). The argument is passed WHOLE: `/who`'s is a filter
|
||||
|
||||
@@ -602,20 +602,60 @@ fn real_alias_table_resolves_the_shipped_commands() {
|
||||
"{line} resolves to an emote"
|
||||
);
|
||||
}
|
||||
// …and the four names the reference has NO command for, which the DBC-name match used to
|
||||
// accept. `/follow` is the sharp one: it fired a text emote where the real client's
|
||||
// `SlashCmdList["FOLLOW"]` follows your target (benilla has no follow yet, so it is honestly
|
||||
// unknown rather than wrongly an emote).
|
||||
for line in ["/follow", "/joke", "/puzzle", "/attackmytarget"] {
|
||||
// …and the three names the reference has NO command for, which the DBC-name match used to
|
||||
// accept as emotes.
|
||||
for line in ["/joke", "/puzzle", "/attackmytarget"] {
|
||||
assert_eq!(parse_line(line), ParsedChat::Unknown, "{line}");
|
||||
}
|
||||
// `/follow` was the sharp one in that class: the DBC-name match fired a text emote where the
|
||||
// real client's `SlashCmdList["FOLLOW"]` follows your target. 0881 made it honestly unknown;
|
||||
// 0890 makes it the real command, over all three shipped aliases (SLASH_FOLLOW1-6 → `/f`,
|
||||
// `/follow`, `/fol`).
|
||||
for line in ["/follow", "/f", "/fol"] {
|
||||
assert_eq!(
|
||||
parse_line(line),
|
||||
ParsedChat::Follow { name: None },
|
||||
"{line}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
parse_line("/follow Probeone"),
|
||||
ParsedChat::Follow {
|
||||
name: Some("Probeone".into())
|
||||
}
|
||||
);
|
||||
// A command whose handler benilla does not register answers like any unknown command.
|
||||
assert_eq!(parse_line("/ginvite"), ParsedChat::Unknown);
|
||||
// The by-name selection pair (decision 0886) — every shipped alias, and the whole-argument
|
||||
// grammar that makes a multi-word creature name ONE name. `/tar` and `/a` are the short forms
|
||||
// the shipped strings carry (SLASH_TARGET2/4, SLASH_ASSIST1/3).
|
||||
assert_eq!(
|
||||
parse_line("/target Kobold Vermin"),
|
||||
ParsedChat::Target {
|
||||
name: Some("Kobold Vermin".into())
|
||||
},
|
||||
"the argument is trimmed WHOLE — `GetSlashCmdTarget`'s gsub, not a first-word split"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_line("/tar Hogger "),
|
||||
ParsedChat::Target {
|
||||
name: Some("Hogger".into())
|
||||
}
|
||||
);
|
||||
assert_eq!(parse_line("/target"), ParsedChat::Target { name: None });
|
||||
assert_eq!(
|
||||
parse_line("/a Bob"),
|
||||
ParsedChat::Assist {
|
||||
name: Some("Bob".into())
|
||||
}
|
||||
);
|
||||
assert_eq!(parse_line("/assist"), ParsedChat::Assist { name: None });
|
||||
// The whole shipped surface, so a table that half-loaded fails loudly: **225 distinct emote
|
||||
// commands** over the 169 `EmotesText` names (the strings repeat — `EMOTE87_CMD1` and `_CMD2`
|
||||
// are both "/sit" — and EMOTE27 "UNUSED" has no row, so it contributes none), and **55 distinct
|
||||
// aliases** across the 28 registered `SlashCmdList` indices.
|
||||
assert_eq!(table.counts(), (55, 225), "(slash, emote) aliases");
|
||||
// are both "/sit" — and EMOTE27 "UNUSED" has no row, so it contributes none), and **62 distinct
|
||||
// aliases** across the 31 registered `SlashCmdList` indices (0886 added TARGET's `/target`
|
||||
// `/tar` and ASSIST's `/assist` `/a` to 0881's 55; 0890 added FOLLOW's `/f` `/follow` `/fol`).
|
||||
assert_eq!(table.counts(), (62, 225), "(slash, emote) aliases");
|
||||
}
|
||||
|
||||
// ── The send-side posture-eligibility gate (`emote_send_eligible`) — the director-verified rows
|
||||
|
||||
Reference in New Issue
Block a user