Split smart interact shortcut from single Alt+J send

bind: "interact" now resolves to game_binds.interact (Alt+J) only —
a single keystroke. The full ISBoxer-style chain (CTM on -> Alt+J ->
sleep walk_delay_ms -> CTM off) lives at bind: "smart_interact".

Fixes the doubling bug Grok flagged: bind: "interact" previously
expanded into the full chain unconditionally, so the loot_manual and
interact_hold example maps fired ctm_on/ctm_off twice and produced
unwanted sleep delays.

- engine.rs: rename shortcut trigger (was: "interact")
- examples/profile.yaml: loot now uses smart_interact; loot_manual and
  interact_hold keep "interact" as a single Alt+J send
- docs/MACROS.md: heading + shortcut comment
- docs/NOTES.md: trigger name
- Add new test: interact_simple_sends_only_alt_j (1-line single-send)
- Existing smart shortcut tests renamed to bind: smart_interact
- CHANGELOG.md: trigger split note

93/93 cargo test pass; clippy clean.
This commit is contained in:
en 2026-09-16 05:41:45 +02:00
parent 1ec8d78f71
commit 8eb5346d92
5 changed files with 52 additions and 15 deletions

View File

@ -48,3 +48,6 @@
clippy clean.
- Docs: `docs/MACROS.md` "Smart interact shortcut" section, `docs/NOTES.md`
callout, `examples/profile.yaml` updated with comments.
> Note: After this change, `bind: "interact"` is a single Alt+J send (game_binds.interact).
> The full chain trigger is now `bind: "smart_interact"`. Examples and tests updated.

View File

@ -78,11 +78,11 @@ Do not mash. A second interact while they are still pathing is how characters ru
If they stop short, raise `walk_delay_ms` (25004000), or use `interact.style: auto` (CTM stays on), or `interact.style: hold`.
## Smart interact shortcut (`bind: interact`)
## Smart interact shortcut (`bind: smart_interact`)
The example profile's `loot` map (Alt+G) and `interact` map (Alt+I) both use a **smart shortcut**: a single user keypress fires the full chain (CTM on → Interact with Target → sleep `walk_delay_ms` → CTM off). It is what ISBoxer did with a "Mapped Key" — you press one key, the multibox software sends the whole chain.
In enBoxer the shortcut is `bind: interact` in a step:
In enBoxer the shortcut is `bind: smart_interact` in a step:
```yaml
- name: loot
@ -90,7 +90,7 @@ In enBoxer the shortcut is `bind: interact` in a step:
steps:
- bind: assist
target: others
- bind: interact # <-- smart shortcut
- bind: smart_interact # <-- smart shortcut
target: others
```

View File

@ -40,7 +40,7 @@ Visible-pixel capture: `grim` (default). Overlay: `mpv --wayland-app-id=enboxer-
## Smart interact shortcut
`bind: interact` in any map step expands at compile time into the full chain
`bind: smart_interact` in any map step expands at compile time into the full chain
(CTM on → Interact with Target → walk delay → CTM off), driven by
`profile.interact` (`style` + `walk_delay_ms`). This is the ISBoxer Mapped
Key analog — one user keypress, four keystrokes dispatched to every captured

View File

@ -114,7 +114,7 @@ maps:
steps:
- bind: assist
target: others
- bind: interact
- bind: smart_interact # full chain (use "interact" for a single Alt+J send)
target: others
- name: loot_manual
# Same idea as `loot` but composed by hand. Pick this form if you want to
@ -127,7 +127,7 @@ maps:
target: others
- bind: ctm_on
target: others
- bind: interact
- bind: interact # single Alt+J send (NOT the smart shortcut)
target: others
- delay_ms: 5000
- bind: ctm_off
@ -140,7 +140,7 @@ maps:
steps:
- bind: ctm_on
target: others
- bind: interact
- bind: interact # single Alt+J send (NOT the smart shortcut)
target: others
release_steps:
- bind: ctm_off

View File

@ -205,11 +205,14 @@ impl Engine {
out.push(Action::Sleep(Duration::from_millis(ms)));
continue;
}
// ISBoxer-style Mapped Key shortcut: `bind: "interact"` fires the
// full CTM-toggle + Interact-with-Target + (optional) wait + CTM-off
// chain driven by profile.interact (style + walk_delay_ms).
// ISBoxer-style Mapped Key shortcut: `bind: "smart_interact"` fires
// the full CTM-toggle + Interact-with-Target + (optional) wait +
// CTM-off chain driven by profile.interact (style + walk_delay_ms).
// The user only pressed the map hotkey; the daemon composes the chain.
if step.bind.as_deref() == Some("interact") {
// For per-step control, use `bind: "interact"` to send a single
// Interact-with-Target keystroke (game_binds.interact = Alt+J) and
// compose ctm_on / ctm_off / walk_delay_ms yourself.
if step.bind.as_deref() == Some("smart_interact") {
let slots = self.resolve_targets(&step.target, &map_name)?;
let interact_key = self
.profile
@ -352,7 +355,7 @@ mod tests {
target: "others".into(),
},
Step {
bind: Some("interact".into()),
bind: Some("smart_interact".into()),
key: None,
delay_ms: None,
target: "others".into(),
@ -505,6 +508,37 @@ mod tests {
}
}
#[test]
fn interact_simple_sends_only_alt_j() {
// After the smart_interact split, `bind: "interact"` is a single
// Alt+J send (game_binds.interact = "g"). No CTM toggle, no sleep,
// no second send. Use this from manual chains or release_steps.
let mut e = sample();
let map = crate::profile::Map {
name: "interact_simple".into(),
hotkey: Hotkey("Alt+U".into()),
hold: false,
steps: vec![crate::profile::Step {
key: None,
bind: Some("interact".into()),
delay_ms: None,
target: "others".into(),
}],
release_steps: vec![],
};
e.profile.maps.push(map);
let acts = e.fire("Alt+U", Hold::Tap).unwrap();
assert_eq!(acts.len(), 1, "exactly one action: {acts:?}");
match &acts[0] {
Action::Send { key, slots, hold } => {
assert_eq!(key, "g");
assert_eq!(*hold, Hold::Tap);
assert_eq!(slots, &vec![2, 3]);
}
other => panic!("expected Send, got {other:?}"),
}
}
#[test]
fn interact_smart_shortcut_standard_emits_full_sequence() {
// bind: "interact" should fire CTM-on, Interact with Target,
@ -518,7 +552,7 @@ mod tests {
hold: false,
steps: vec![crate::profile::Step {
key: None,
bind: Some("interact".into()),
bind: Some("smart_interact".into()),
delay_ms: None,
target: "others".into(),
}],
@ -555,7 +589,7 @@ mod tests {
hold: false,
steps: vec![crate::profile::Step {
key: None,
bind: Some("interact".into()),
bind: Some("smart_interact".into()),
delay_ms: None,
target: "others".into(),
}],
@ -585,7 +619,7 @@ mod tests {
hold: true,
steps: vec![crate::profile::Step {
key: None,
bind: Some("interact".into()),
bind: Some("smart_interact".into()),
delay_ms: None,
target: "others".into(),
}],