From ef7e88c5c05c045001a62e3f29a5ef7566212449 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 17:22:56 -0400 Subject: [PATCH 01/12] feat(PluginConstants): add PERT prefix for LeftClickCastPlugin Yellow [P] prefix in the #FFFF00 color family, matching the existing HTML-wrapped prefix convention. --- .../net/runelite/client/plugins/microbot/PluginConstants.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java b/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java index 3c28f7f994..0bc43bbe8f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java +++ b/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java @@ -34,6 +34,7 @@ private PluginConstants() public static final String NATE = "[N] "; public static final String SYN = "[Syn] "; public static final String BIGL = "[BL] "; + public static final String PERT = "[P] "; public static final boolean DEFAULT_ENABLED = false; public static final boolean IS_EXTERNAL = true; //test From cb8ae96549a6206e4d2a845608e434204b5c4edd Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 17:23:01 -0400 Subject: [PATCH 02/12] feat(leftclickcast): add PertTargetSpell enum Wraps every target-castable MagicAction (autocast Strike->Surge, ancient Rush/Burst/Blitz/Barrage, non-autocast combat, Arceuus offensives, utility target spells) behind a minimal display-name shape for the plugin config dropdown. --- .../leftclickcast/PertTargetSpell.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java new file mode 100644 index 0000000000..521072613b --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java @@ -0,0 +1,96 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import lombok.Getter; +import net.runelite.client.plugins.skillcalculator.skills.MagicAction; + +@Getter +public enum PertTargetSpell +{ + // Modern autocastable combat lines (Strike -> Surge) + WIND_STRIKE("Wind Strike", MagicAction.WIND_STRIKE), + WATER_STRIKE("Water Strike", MagicAction.WATER_STRIKE), + EARTH_STRIKE("Earth Strike", MagicAction.EARTH_STRIKE), + FIRE_STRIKE("Fire Strike", MagicAction.FIRE_STRIKE), + WIND_BOLT("Wind Bolt", MagicAction.WIND_BOLT), + WATER_BOLT("Water Bolt", MagicAction.WATER_BOLT), + EARTH_BOLT("Earth Bolt", MagicAction.EARTH_BOLT), + FIRE_BOLT("Fire Bolt", MagicAction.FIRE_BOLT), + WIND_BLAST("Wind Blast", MagicAction.WIND_BLAST), + WATER_BLAST("Water Blast", MagicAction.WATER_BLAST), + EARTH_BLAST("Earth Blast", MagicAction.EARTH_BLAST), + FIRE_BLAST("Fire Blast", MagicAction.FIRE_BLAST), + WIND_WAVE("Wind Wave", MagicAction.WIND_WAVE), + WATER_WAVE("Water Wave", MagicAction.WATER_WAVE), + EARTH_WAVE("Earth Wave", MagicAction.EARTH_WAVE), + FIRE_WAVE("Fire Wave", MagicAction.FIRE_WAVE), + WIND_SURGE("Wind Surge", MagicAction.WIND_SURGE), + WATER_SURGE("Water Surge", MagicAction.WATER_SURGE), + EARTH_SURGE("Earth Surge", MagicAction.EARTH_SURGE), + FIRE_SURGE("Fire Surge", MagicAction.FIRE_SURGE), + + // Ancient autocastable combat lines (Rush/Burst/Blitz/Barrage for each element) + SMOKE_RUSH("Smoke Rush", MagicAction.SMOKE_RUSH), + SHADOW_RUSH("Shadow Rush", MagicAction.SHADOW_RUSH), + BLOOD_RUSH("Blood Rush", MagicAction.BLOOD_RUSH), + ICE_RUSH("Ice Rush", MagicAction.ICE_RUSH), + SMOKE_BURST("Smoke Burst", MagicAction.SMOKE_BURST), + SHADOW_BURST("Shadow Burst", MagicAction.SHADOW_BURST), + BLOOD_BURST("Blood Burst", MagicAction.BLOOD_BURST), + ICE_BURST("Ice Burst", MagicAction.ICE_BURST), + SMOKE_BLITZ("Smoke Blitz", MagicAction.SMOKE_BLITZ), + SHADOW_BLITZ("Shadow Blitz", MagicAction.SHADOW_BLITZ), + BLOOD_BLITZ("Blood Blitz", MagicAction.BLOOD_BLITZ), + ICE_BLITZ("Ice Blitz", MagicAction.ICE_BLITZ), + SMOKE_BARRAGE("Smoke Barrage", MagicAction.SMOKE_BARRAGE), + SHADOW_BARRAGE("Shadow Barrage", MagicAction.SHADOW_BARRAGE), + BLOOD_BARRAGE("Blood Barrage", MagicAction.BLOOD_BARRAGE), + ICE_BARRAGE("Ice Barrage", MagicAction.ICE_BARRAGE), + + // Non-autocastable combat spells + CRUMBLE_UNDEAD("Crumble Undead", MagicAction.CRUMBLE_UNDEAD), + IBAN_BLAST("Iban Blast", MagicAction.IBAN_BLAST), + MAGIC_DART("Magic Dart", MagicAction.MAGIC_DART), + SARADOMIN_STRIKE("Saradomin Strike", MagicAction.SARADOMIN_STRIKE), + CLAWS_OF_GUTHIX("Claws of Guthix", MagicAction.CLAWS_OF_GUTHIX), + FLAMES_OF_ZAMORAK("Flames of Zamorak", MagicAction.FLAMES_OF_ZAMORAK), + + // Arceuus offensive target spells + GHOSTLY_GRASP("Ghostly Grasp", MagicAction.GHOSTLY_GRASP), + SKELETAL_GRASP("Skeletal Grasp", MagicAction.SKELETAL_GRASP), + UNDEAD_GRASP("Undead Grasp", MagicAction.UNDEAD_GRASP), + INFERIOR_DEMONBANE("Inferior Demonbane", MagicAction.INFERIOR_DEMONBANE), + SUPERIOR_DEMONBANE("Superior Demonbane", MagicAction.SUPERIOR_DEMONBANE), + DARK_DEMONBANE("Dark Demonbane", MagicAction.DARK_DEMONBANE), + LESSER_CORRUPTION("Lesser Corruption", MagicAction.LESSER_CORRUPTION), + GREATER_CORRUPTION("Greater Corruption", MagicAction.GREATER_CORRUPTION), + + // Utility target spells + CONFUSE("Confuse", MagicAction.CONFUSE), + WEAKEN("Weaken", MagicAction.WEAKEN), + CURSE("Curse", MagicAction.CURSE), + BIND("Bind", MagicAction.BIND), + SNARE("Snare", MagicAction.SNARE), + ENTANGLE("Entangle", MagicAction.ENTANGLE), + VULNERABILITY("Vulnerability", MagicAction.VULNERABILITY), + ENFEEBLE("Enfeeble", MagicAction.ENFEEBLE), + STUN("Stun", MagicAction.STUN), + TELE_BLOCK("Tele Block", MagicAction.TELE_BLOCK), + TELEOTHER_LUMBRIDGE("Tele Other Lumbridge", MagicAction.TELEOTHER_LUMBRIDGE), + TELEOTHER_FALADOR("Tele Other Falador", MagicAction.TELEOTHER_FALADOR), + TELEOTHER_CAMELOT("Tele Other Camelot", MagicAction.TELEOTHER_CAMELOT); + + private final String displayName; + private final MagicAction magicAction; + + PertTargetSpell(String displayName, MagicAction magicAction) + { + this.displayName = displayName; + this.magicAction = magicAction; + } + + @Override + public String toString() + { + return displayName; + } +} From 6fc3d47c5cc8243586848136291b27cf5f6ed6d6 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 17:23:07 -0400 Subject: [PATCH 03/12] feat(leftclickcast): add LeftClickCastConfig Three items: enabled (master switch), spell (PertTargetSpell dropdown, default Fire Strike), requireMagicWeapon (staff gate). --- .../leftclickcast/LeftClickCastConfig.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java new file mode 100644 index 0000000000..04df181d05 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java @@ -0,0 +1,42 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("leftclickcast") +public interface LeftClickCastConfig extends Config +{ + @ConfigItem( + keyName = "enabled", + name = "Enabled", + description = "Replace the left-click Attack option on NPCs with Cast Spell", + position = 0 + ) + default boolean enabled() + { + return true; + } + + @ConfigItem( + keyName = "spell", + name = "Spell", + description = "The spell cast when left-clicking an attackable NPC", + position = 1 + ) + default PertTargetSpell spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "requireMagicWeapon", + name = "Require magic weapon", + description = "When enabled, the Cast entry is only inserted while a staff, bladed staff, powered staff, or powered wand is equipped. Disable to cast regardless of equipped weapon.", + position = 2 + ) + default boolean requireMagicWeapon() + { + return true; + } +} From d766bd38d94886f8bbae2f6f2382f25e52f5603e Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 17:23:13 -0400 Subject: [PATCH 04/12] feat(leftclickcast): add LeftClickCastPlugin onMenuEntryAdded inserts a Cast entry above the Attack entry for attackable NPCs, gated by the enabled flag, a non-null configured spell, and (optionally) a magic-weapon equip check via varbit 357. Cast is dispatched through Rs2Magic.castOn off-thread via CompletableFuture because Global.sleepUntil is a no-op on the client thread and would otherwise silently drop the cast. --- .../leftclickcast/LeftClickCastPlugin.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java new file mode 100644 index 0000000000..d2244c69c1 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -0,0 +1,97 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Provides; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.MenuEntry; +import net.runelite.api.NPC; +import net.runelite.api.Varbits; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.microbot.PluginConstants; +import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; + +@PluginDescriptor( + name = PluginConstants.PERT + "Left-Click Cast", + description = "Replaces left-click Attack on NPCs with a preconfigured Cast Spell action.", + tags = {"magic", "combat", "spell", "left-click", "cast", "pvm", "pvp"}, + authors = {"Pert"}, + version = LeftClickCastPlugin.version, + minClientVersion = "2.0.13", + enabledByDefault = PluginConstants.DEFAULT_ENABLED, + isExternal = PluginConstants.IS_EXTERNAL +) +@Slf4j +public class LeftClickCastPlugin extends Plugin +{ + static final String version = "1.0.0"; + + // Magic WeaponType ordinals against varbit 357 (EQUIPPED_WEAPON_TYPE). + // Sourced from the RuneLite core WeaponType enum: STAFF, BLADED_STAFF, POWERED_STAFF, POWERED_WAND. + private static final Set MAGIC_WEAPON_TYPES = ImmutableSet.of(22, 23, 26, 27); + + @Inject + private Client client; + + @Inject + private LeftClickCastConfig config; + + @Provides + LeftClickCastConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(LeftClickCastConfig.class); + } + + @Subscribe + public void onMenuEntryAdded(MenuEntryAdded event) + { + if (!config.enabled()) + { + return; + } + + PertTargetSpell spell = config.spell(); + if (spell == null) + { + return; + } + + if (!"Attack".equals(event.getOption())) + { + return; + } + + MenuEntry entry = event.getMenuEntry(); + NPC npc = entry.getNpc(); + if (npc == null) + { + return; + } + + if (config.requireMagicWeapon() && !isMagicWeaponEquipped()) + { + return; + } + + client.getMenu().createMenuEntry(-1) + .setOption("Cast") + .setTarget("" + spell.getDisplayName() + " " + event.getTarget()) + .setType(net.runelite.api.MenuAction.RUNELITE) + // Rs2Magic.castOn uses sleepUntil, which is a no-op on the client thread — dispatch off-thread. + .onClick(e -> CompletableFuture.runAsync( + () -> Rs2Magic.castOn(spell.getMagicAction(), new Rs2NpcModel(npc)))); + } + + private boolean isMagicWeaponEquipped() + { + return MAGIC_WEAPON_TYPES.contains(client.getVarbitValue(Varbits.EQUIPPED_WEAPON_TYPE)); + } +} From f76537a203fc5d0c8e3351d7ae4f6de4666223b6 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 17:23:17 -0400 Subject: [PATCH 05/12] docs(leftclickcast): add plugin README Covers purpose, config reference, limitations (no rune or spellbook auto-management, staff-only default, PvP out of scope), and the full supported-spells list. --- .../microbot/leftclickcast/docs/README.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md diff --git a/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md new file mode 100644 index 0000000000..bcd101f3da --- /dev/null +++ b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md @@ -0,0 +1,38 @@ +# Left-Click Cast + +Replaces the left-click **Attack** option on attackable NPCs with a preconfigured **Cast Spell** action. The plugin stays invisible when you swap to a melee or ranged weapon, so leaving it enabled is safe. + +## How it works + +When an "Attack" menu entry is added for an NPC, the plugin inserts a new menu entry for the selected spell and places it above Attack, making the spell the left-click action. + +All casting is dispatched through the Microbot client's existing `Rs2Magic.castOn(MagicAction, Actor)` — the plugin does not re-implement rune checks, spellbook switching, or targeting. + +## Configuration + +| Option | Default | Description | +| --- | --- | --- | +| **Enabled** | `true` | Master switch. When off, no menu entries are inserted. | +| **Spell** | `Fire Strike` | The spell cast on left-click. Only spells in the dropdown are supported. | +| **Require magic weapon** | `true` | When on, the Cast entry only shows while a staff, bladed staff, powered staff, or powered wand is equipped (detected via varbit `EQUIPPED_WEAPON_TYPE`). Disable to cast regardless of weapon. | + +## Limitations + +- **No rune auto-management.** If you run out of runes, the cast fails cleanly and you see the normal "You do not have enough ..." chat message. +- **No auto-spellbook switching.** If the selected spell is not on your current spellbook, the cast fails silently. Switch spellbooks manually. +- **The dropdown is the source of truth.** Spells not listed in the dropdown are not supported by this plugin. +- **Staff-only default.** With `Require magic weapon` enabled (default), non-magic weapon types produce normal Attack behavior. Disable the toggle if you want to cast from melee/ranged weapons as well. +- **PvP out of scope.** Left-click cast on players is not implemented; OSRS's native autocast already handles staves in PvP. +- **Cooperative menu composition.** If another plugin inserts menu entries after this one on the same tick, its entry becomes the top entry instead — a known limitation of RuneLite's menu model. + +## Supported spells + +**Modern combat (Strike → Surge):** Wind Strike, Water Strike, Earth Strike, Fire Strike, Wind Bolt, Water Bolt, Earth Bolt, Fire Bolt, Wind Blast, Water Blast, Earth Blast, Fire Blast, Wind Wave, Water Wave, Earth Wave, Fire Wave, Wind Surge, Water Surge, Earth Surge, Fire Surge. + +**Ancient combat:** Smoke / Shadow / Blood / Ice — Rush, Burst, Blitz, Barrage. + +**Non-autocastable combat:** Crumble Undead, Iban Blast, Magic Dart, Saradomin Strike, Claws of Guthix, Flames of Zamorak. + +**Arceuus offensive:** Ghostly Grasp, Skeletal Grasp, Undead Grasp, Inferior Demonbane, Superior Demonbane, Dark Demonbane, Lesser Corruption, Greater Corruption. + +**Utility target spells:** Confuse, Weaken, Curse, Bind, Snare, Entangle, Vulnerability, Enfeeble, Stun, Tele Block, Tele Other (Lumbridge / Falador / Camelot). From a8d95b687af2842cb73d93a308c7bbe7c08d693a Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 17:23:19 -0400 Subject: [PATCH 06/12] chore(debug): register LeftClickCastPlugin in debugPlugins --- src/test/java/net/runelite/client/Microbot.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/net/runelite/client/Microbot.java b/src/test/java/net/runelite/client/Microbot.java index 0bbce8830b..8933287b08 100644 --- a/src/test/java/net/runelite/client/Microbot.java +++ b/src/test/java/net/runelite/client/Microbot.java @@ -10,6 +10,7 @@ import net.runelite.client.plugins.microbot.astralrc.AstralRunesPlugin; import net.runelite.client.plugins.microbot.autofishing.AutoFishingPlugin; import net.runelite.client.plugins.microbot.example.ExamplePlugin; +import net.runelite.client.plugins.microbot.leftclickcast.LeftClickCastPlugin; import net.runelite.client.plugins.microbot.sailing.MSailingPlugin; import net.runelite.client.plugins.microbot.thieving.ThievingPlugin; import net.runelite.client.plugins.microbot.woodcutting.AutoWoodcuttingPlugin; @@ -20,7 +21,8 @@ public class Microbot private static final Class[] debugPlugins = { AIOFighterPlugin.class, - AgentServerPlugin.class + AgentServerPlugin.class, + LeftClickCastPlugin.class }; public static void main(String[] args) throws Exception From 2332d5e0300752be41a0a0f59ec4aa6a4d7da2ef Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 18:14:51 -0400 Subject: [PATCH 07/12] fix(leftclickcast): use VarbitID.COMBAT_WEAPON_CATEGORY net.runelite.api.Varbits is @Deprecated in favor of gameval identifier classes. Same varbit (357), no behavior change. --- .../plugins/microbot/leftclickcast/LeftClickCastPlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java index d2244c69c1..62c1b3f9e7 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -9,8 +9,8 @@ import net.runelite.api.Client; import net.runelite.api.MenuEntry; import net.runelite.api.NPC; -import net.runelite.api.Varbits; import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.gameval.VarbitID; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.plugins.Plugin; @@ -92,6 +92,6 @@ public void onMenuEntryAdded(MenuEntryAdded event) private boolean isMagicWeaponEquipped() { - return MAGIC_WEAPON_TYPES.contains(client.getVarbitValue(Varbits.EQUIPPED_WEAPON_TYPE)); + return MAGIC_WEAPON_TYPES.contains(client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY)); } } From a9c811e192c65ac1b9bcfe842a648bfa428fa605 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 18:41:43 -0400 Subject: [PATCH 08/12] fix(leftclickcast): move to PostMenuSort + runtime magic-weapon check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs found during manual verification: 1. The static weapon-type set (22/23/26/27) from the design doc was wrong — real STAFF value (at least on this client) is 18. Replaced with a runtime check that reads the weapon's attack-style struct via EnumID.WEAPON_STYLES + ParamID.ATTACK_STYLE_NAME and flags the weapon as magic if any style is Casting or Defensive Casting. Mirrors core AttackStylesPlugin and auto-covers future weapons. 2. Mutating the Attack entry inside onMenuEntryAdded did not survive the game's menu sort pass — entries of type RUNELITE get sorted below NPC_* entries, so left-click kept firing Attack/Walk-Here. Moved the mutation to onPostMenuSort (fires after the sort) and swap the entry to the tail of the array — tail slot is the left-click action in RuneLite's menu model. --- .../leftclickcast/LeftClickCastPlugin.java | 99 ++++++++++++++----- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java index 62c1b3f9e7..89b66fd742 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -1,15 +1,18 @@ package net.runelite.client.plugins.microbot.leftclickcast; -import com.google.common.collect.ImmutableSet; import com.google.inject.Provides; -import java.util.Set; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; -import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; +import net.runelite.api.EnumComposition; +import net.runelite.api.EnumID; +import net.runelite.api.MenuAction; import net.runelite.api.MenuEntry; +import net.runelite.api.Menu; import net.runelite.api.NPC; -import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.ParamID; +import net.runelite.api.StructComposition; +import net.runelite.api.events.PostMenuSort; import net.runelite.api.gameval.VarbitID; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; @@ -29,15 +32,10 @@ enabledByDefault = PluginConstants.DEFAULT_ENABLED, isExternal = PluginConstants.IS_EXTERNAL ) -@Slf4j public class LeftClickCastPlugin extends Plugin { static final String version = "1.0.0"; - // Magic WeaponType ordinals against varbit 357 (EQUIPPED_WEAPON_TYPE). - // Sourced from the RuneLite core WeaponType enum: STAFF, BLADED_STAFF, POWERED_STAFF, POWERED_WAND. - private static final Set MAGIC_WEAPON_TYPES = ImmutableSet.of(22, 23, 26, 27); - @Inject private Client client; @@ -51,47 +49,94 @@ LeftClickCastConfig provideConfig(ConfigManager configManager) } @Subscribe - public void onMenuEntryAdded(MenuEntryAdded event) + public void onPostMenuSort(PostMenuSort event) { + // Don't mutate while the right-click menu is open — entries are frozen at open-time. + if (client.isMenuOpen()) + { + return; + } if (!config.enabled()) { return; } - PertTargetSpell spell = config.spell(); if (spell == null) { return; } - - if (!"Attack".equals(event.getOption())) + if (config.requireMagicWeapon() && !isMagicWeaponEquipped()) { return; } - MenuEntry entry = event.getMenuEntry(); - NPC npc = entry.getNpc(); - if (npc == null) + Menu menu = client.getMenu(); + MenuEntry[] entries = menu.getMenuEntries(); + + // Find the top-most NPC Attack entry (the game's already-sorted left-click candidate). + int attackIdx = -1; + NPC npc = null; + for (int i = entries.length - 1; i >= 0; i--) { - return; + MenuEntry e = entries[i]; + if ("Attack".equals(e.getOption()) && e.getNpc() != null) + { + attackIdx = i; + npc = e.getNpc(); + break; + } } - - if (config.requireMagicWeapon() && !isMagicWeaponEquipped()) + if (attackIdx < 0) { return; } - client.getMenu().createMenuEntry(-1) - .setOption("Cast") - .setTarget("" + spell.getDisplayName() + " " + event.getTarget()) - .setType(net.runelite.api.MenuAction.RUNELITE) - // Rs2Magic.castOn uses sleepUntil, which is a no-op on the client thread — dispatch off-thread. - .onClick(e -> CompletableFuture.runAsync( - () -> Rs2Magic.castOn(spell.getMagicAction(), new Rs2NpcModel(npc)))); + MenuEntry attack = entries[attackIdx]; + final NPC target = npc; + attack.setOption("Cast " + spell.getDisplayName()); + attack.setType(MenuAction.RUNELITE); + // Rs2Magic.castOn uses sleepUntil, which is a no-op on the client thread — dispatch off-thread. + attack.onClick(e -> CompletableFuture.runAsync( + () -> Rs2Magic.castOn(spell.getMagicAction(), new Rs2NpcModel(target)))); + + // Move to the tail of the array — that slot is the left-click action in RuneLite's menu model. + if (attackIdx != entries.length - 1) + { + entries[attackIdx] = entries[entries.length - 1]; + entries[entries.length - 1] = attack; + menu.setMenuEntries(entries); + } } + // A weapon counts as "magic" when its style struct exposes Casting or Defensive Casting. + // Mirrors the core AttackStylesPlugin logic (EnumID.WEAPON_STYLES + ParamID.ATTACK_STYLE_NAME). private boolean isMagicWeaponEquipped() { - return MAGIC_WEAPON_TYPES.contains(client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY)); + int weaponType = client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY); + EnumComposition weaponStyles = client.getEnum(EnumID.WEAPON_STYLES); + if (weaponStyles == null) + { + return false; + } + int styleEnumId = weaponStyles.getIntValue(weaponType); + if (styleEnumId == -1) + { + return false; + } + int[] styleStructs = client.getEnum(styleEnumId).getIntVals(); + for (int structId : styleStructs) + { + StructComposition sc = client.getStructComposition(structId); + if (sc == null) + { + continue; + } + String name = sc.getStringValue(ParamID.ATTACK_STYLE_NAME); + if ("Casting".equalsIgnoreCase(name) || "Defensive Casting".equalsIgnoreCase(name)) + { + return true; + } + } + return false; } } From 941ef8f688390c9400031f62ea1f9d91c54ce89a Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 18:55:22 -0400 Subject: [PATCH 09/12] feat(leftclickcast): support Players (wilderness / PvP) Scan loop now accepts Attack entries on either NPCs or Players. Dispatch wraps NPCs in Rs2NpcModel; raw Player is passed through, which Rs2Magic.castOn handles via its instanceof Player branch. --- .../leftclickcast/LeftClickCastPlugin.java | 27 ++++++++++++++----- .../microbot/leftclickcast/docs/README.md | 3 +-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java index 89b66fd742..41b91c43b2 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -3,6 +3,7 @@ import com.google.inject.Provides; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; +import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.EnumComposition; import net.runelite.api.EnumID; @@ -11,6 +12,7 @@ import net.runelite.api.Menu; import net.runelite.api.NPC; import net.runelite.api.ParamID; +import net.runelite.api.Player; import net.runelite.api.StructComposition; import net.runelite.api.events.PostMenuSort; import net.runelite.api.gameval.VarbitID; @@ -73,16 +75,26 @@ public void onPostMenuSort(PostMenuSort event) Menu menu = client.getMenu(); MenuEntry[] entries = menu.getMenuEntries(); - // Find the top-most NPC Attack entry (the game's already-sorted left-click candidate). + // Find the top-most NPC or Player Attack entry (the game's already-sorted left-click candidate). int attackIdx = -1; - NPC npc = null; + Actor targetActor = null; for (int i = entries.length - 1; i >= 0; i--) { MenuEntry e = entries[i]; - if ("Attack".equals(e.getOption()) && e.getNpc() != null) + if (!"Attack".equals(e.getOption())) + { + continue; + } + if (e.getNpc() != null) + { + attackIdx = i; + targetActor = e.getNpc(); + break; + } + if (e.getPlayer() != null) { attackIdx = i; - npc = e.getNpc(); + targetActor = e.getPlayer(); break; } } @@ -92,12 +104,15 @@ public void onPostMenuSort(PostMenuSort event) } MenuEntry attack = entries[attackIdx]; - final NPC target = npc; + // Rs2Magic.castOn requires Rs2NpcModel for NPCs but accepts raw Player (Rs2PlayerModel implements Player). + final Actor dispatchTarget = targetActor instanceof NPC + ? new Rs2NpcModel((NPC) targetActor) + : (Player) targetActor; attack.setOption("Cast " + spell.getDisplayName()); attack.setType(MenuAction.RUNELITE); // Rs2Magic.castOn uses sleepUntil, which is a no-op on the client thread — dispatch off-thread. attack.onClick(e -> CompletableFuture.runAsync( - () -> Rs2Magic.castOn(spell.getMagicAction(), new Rs2NpcModel(target)))); + () -> Rs2Magic.castOn(spell.getMagicAction(), dispatchTarget))); // Move to the tail of the array — that slot is the left-click action in RuneLite's menu model. if (attackIdx != entries.length - 1) diff --git a/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md index bcd101f3da..ea3ebd3caa 100644 --- a/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md +++ b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md @@ -1,6 +1,6 @@ # Left-Click Cast -Replaces the left-click **Attack** option on attackable NPCs with a preconfigured **Cast Spell** action. The plugin stays invisible when you swap to a melee or ranged weapon, so leaving it enabled is safe. +Replaces the left-click **Attack** option on attackable NPCs and on players (wilderness / PvP) with a preconfigured **Cast Spell** action. The plugin stays invisible when you swap to a melee or ranged weapon, so leaving it enabled is safe. ## How it works @@ -22,7 +22,6 @@ All casting is dispatched through the Microbot client's existing `Rs2Magic.castO - **No auto-spellbook switching.** If the selected spell is not on your current spellbook, the cast fails silently. Switch spellbooks manually. - **The dropdown is the source of truth.** Spells not listed in the dropdown are not supported by this plugin. - **Staff-only default.** With `Require magic weapon` enabled (default), non-magic weapon types produce normal Attack behavior. Disable the toggle if you want to cast from melee/ranged weapons as well. -- **PvP out of scope.** Left-click cast on players is not implemented; OSRS's native autocast already handles staves in PvP. - **Cooperative menu composition.** If another plugin inserts menu entries after this one on the same tick, its entry becomes the top entry instead — a known limitation of RuneLite's menu model. ## Supported spells From bf2df2084c7a46a452cc4d85f72e66f5b230ce3c Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 19:07:55 -0400 Subject: [PATCH 10/12] feat(leftclickcast): five spell slots with hotkey-driven active slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds five `@ConfigItem` spell slots, each with a corresponding Keybind, grouped under "Spell Slots" and "Hotkeys" config sections. A `HotkeyListener` per slot is registered on startUp and unregistered on shutDown; pressing a bound hotkey sets the in-memory active slot and (optionally) posts a chat message with the new spell's display name. The menu-sort hot path now reads the active slot's spell instead of the legacy single `spell` key. The legacy `spell` key remains defined so existing configs aren't invalidated, and is migrated into `slot1Spell` on startUp when slot 1 is still at its default — existing users keep their choice without manual action. Active slot is session-local and resets to slot 1 on every startUp. Bumps plugin version to 1.1.0. --- .../leftclickcast/LeftClickCastConfig.java | 151 +++++++++++++++++- .../leftclickcast/LeftClickCastPlugin.java | 141 +++++++++++++++- .../microbot/leftclickcast/docs/README.md | 19 ++- 3 files changed, 305 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java index 04df181d05..59cf06320a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java @@ -3,6 +3,8 @@ import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Keybind; @ConfigGroup("leftclickcast") public interface LeftClickCastConfig extends Config @@ -18,10 +20,11 @@ default boolean enabled() return true; } + // Retained so existing stored config is not invalidated. Read once at startUp for migration into slot1Spell. @ConfigItem( keyName = "spell", name = "Spell", - description = "The spell cast when left-clicking an attackable NPC", + description = "Legacy single-spell setting — migrated into Slot 1 on startup.", position = 1 ) default PertTargetSpell spell() @@ -39,4 +42,150 @@ default boolean requireMagicWeapon() { return true; } + + @ConfigSection( + name = "Spell Slots", + description = "Up to five spells that can be bound to hotkeys for mid-fight swapping.", + position = 10 + ) + String spellSlotsSection = "spellSlots"; + + @ConfigSection( + name = "Hotkeys", + description = "Hotkey bindings that switch the active spell slot.", + position = 11 + ) + String hotkeysSection = "hotkeys"; + + @ConfigItem( + keyName = "slot1Spell", + name = "Slot 1 Spell", + description = "Spell for slot 1 (the startup-active slot).", + section = spellSlotsSection, + position = 0 + ) + default PertTargetSpell slot1Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot2Spell", + name = "Slot 2 Spell", + description = "Spell for slot 2.", + section = spellSlotsSection, + position = 1 + ) + default PertTargetSpell slot2Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot3Spell", + name = "Slot 3 Spell", + description = "Spell for slot 3.", + section = spellSlotsSection, + position = 2 + ) + default PertTargetSpell slot3Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot4Spell", + name = "Slot 4 Spell", + description = "Spell for slot 4.", + section = spellSlotsSection, + position = 3 + ) + default PertTargetSpell slot4Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot5Spell", + name = "Slot 5 Spell", + description = "Spell for slot 5.", + section = spellSlotsSection, + position = 4 + ) + default PertTargetSpell slot5Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot1Hotkey", + name = "Slot 1 Hotkey", + description = "Hotkey that activates slot 1.", + section = hotkeysSection, + position = 0 + ) + default Keybind slot1Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot2Hotkey", + name = "Slot 2 Hotkey", + description = "Hotkey that activates slot 2.", + section = hotkeysSection, + position = 1 + ) + default Keybind slot2Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot3Hotkey", + name = "Slot 3 Hotkey", + description = "Hotkey that activates slot 3.", + section = hotkeysSection, + position = 2 + ) + default Keybind slot3Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot4Hotkey", + name = "Slot 4 Hotkey", + description = "Hotkey that activates slot 4.", + section = hotkeysSection, + position = 3 + ) + default Keybind slot4Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot5Hotkey", + name = "Slot 5 Hotkey", + description = "Hotkey that activates slot 5.", + section = hotkeysSection, + position = 4 + ) + default Keybind slot5Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "activeSlotChatMessage", + name = "Chat feedback on slot change", + description = "Post a game chat message when a hotkey switches the active slot.", + section = hotkeysSection, + position = 5 + ) + default boolean activeSlotChatMessage() + { + return true; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java index 41b91c43b2..2fde871a39 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -4,6 +4,7 @@ import java.util.concurrent.CompletableFuture; import javax.inject.Inject; import net.runelite.api.Actor; +import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.EnumComposition; import net.runelite.api.EnumID; @@ -16,13 +17,18 @@ import net.runelite.api.StructComposition; import net.runelite.api.events.PostMenuSort; import net.runelite.api.gameval.VarbitID; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.Keybind; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.input.KeyManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.plugins.microbot.PluginConstants; import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.util.HotkeyListener; @PluginDescriptor( name = PluginConstants.PERT + "Left-Click Cast", @@ -36,7 +42,9 @@ ) public class LeftClickCastPlugin extends Plugin { - static final String version = "1.0.0"; + static final String version = "1.1.0"; + + private static final int SLOT_COUNT = 5; @Inject private Client client; @@ -44,12 +52,60 @@ public class LeftClickCastPlugin extends Plugin @Inject private LeftClickCastConfig config; + @Inject + private KeyManager keyManager; + + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private ConfigManager configManager; + + private volatile int activeSlot = 0; + + private final HotkeyListener[] hotkeyListeners = new HotkeyListener[SLOT_COUNT]; + @Provides LeftClickCastConfig provideConfig(ConfigManager configManager) { return configManager.getConfig(LeftClickCastConfig.class); } + @Override + protected void startUp() + { + activeSlot = 0; + for (int i = 0; i < SLOT_COUNT; i++) + { + final int slotIndex = i; + HotkeyListener listener = new HotkeyListener(() -> slotHotkeyFor(slotIndex)) + { + @Override + public void hotkeyPressed() + { + onSlotHotkey(slotIndex); + } + }; + hotkeyListeners[i] = listener; + keyManager.registerKeyListener(listener); + } + migrateLegacySpellKey(); + } + + @Override + protected void shutDown() + { + for (int i = 0; i < hotkeyListeners.length; i++) + { + HotkeyListener listener = hotkeyListeners[i]; + if (listener != null) + { + keyManager.unregisterKeyListener(listener); + hotkeyListeners[i] = null; + } + } + } + @Subscribe public void onPostMenuSort(PostMenuSort event) { @@ -62,7 +118,7 @@ public void onPostMenuSort(PostMenuSort event) { return; } - PertTargetSpell spell = config.spell(); + PertTargetSpell spell = slotSpellFor(activeSlot); if (spell == null) { return; @@ -108,11 +164,12 @@ public void onPostMenuSort(PostMenuSort event) final Actor dispatchTarget = targetActor instanceof NPC ? new Rs2NpcModel((NPC) targetActor) : (Player) targetActor; - attack.setOption("Cast " + spell.getDisplayName()); + final PertTargetSpell dispatchSpell = spell; + attack.setOption("Cast " + dispatchSpell.getDisplayName()); attack.setType(MenuAction.RUNELITE); // Rs2Magic.castOn uses sleepUntil, which is a no-op on the client thread — dispatch off-thread. attack.onClick(e -> CompletableFuture.runAsync( - () -> Rs2Magic.castOn(spell.getMagicAction(), dispatchTarget))); + () -> Rs2Magic.castOn(dispatchSpell.getMagicAction(), dispatchTarget))); // Move to the tail of the array — that slot is the left-click action in RuneLite's menu model. if (attackIdx != entries.length - 1) @@ -123,6 +180,82 @@ public void onPostMenuSort(PostMenuSort event) } } + private Keybind slotHotkeyFor(int index) + { + switch (index) + { + case 0: + return config.slot1Hotkey(); + case 1: + return config.slot2Hotkey(); + case 2: + return config.slot3Hotkey(); + case 3: + return config.slot4Hotkey(); + case 4: + return config.slot5Hotkey(); + default: + return Keybind.NOT_SET; + } + } + + private PertTargetSpell slotSpellFor(int index) + { + switch (index) + { + case 0: + return config.slot1Spell(); + case 1: + return config.slot2Spell(); + case 2: + return config.slot3Spell(); + case 3: + return config.slot4Spell(); + case 4: + return config.slot5Spell(); + default: + return config.slot1Spell(); + } + } + + private void onSlotHotkey(int index) + { + activeSlot = index; + if (config.activeSlotChatMessage()) + { + PertTargetSpell spell = slotSpellFor(index); + String display = spell != null ? spell.getDisplayName() : "(no spell)"; + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.GAMEMESSAGE) + .value("Left-Click Cast: now casting " + display) + .build()); + } + } + + // Best-effort: if the user had previously set the legacy `spell` key to a non-default value and + // slot1Spell is still at its default, copy the legacy value into slot1Spell so existing configs keep working. + private void migrateLegacySpellKey() + { + try + { + PertTargetSpell legacy = configManager.getConfiguration( + "leftclickcast", "spell", PertTargetSpell.class); + if (legacy == null || legacy == PertTargetSpell.FIRE_STRIKE) + { + return; + } + if (config.slot1Spell() != PertTargetSpell.FIRE_STRIKE) + { + return; + } + configManager.setConfiguration("leftclickcast", "slot1Spell", legacy); + } + catch (Exception ignored) + { + // Migration is best-effort; ignore any deserialization or storage errors. + } + } + // A weapon counts as "magic" when its style struct exposes Casting or Defensive Casting. // Mirrors the core AttackStylesPlugin logic (EnumID.WEAPON_STYLES + ParamID.ATTACK_STYLE_NAME). private boolean isMagicWeaponEquipped() diff --git a/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md index ea3ebd3caa..be60ed19ae 100644 --- a/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md +++ b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md @@ -13,9 +13,26 @@ All casting is dispatched through the Microbot client's existing `Rs2Magic.castO | Option | Default | Description | | --- | --- | --- | | **Enabled** | `true` | Master switch. When off, no menu entries are inserted. | -| **Spell** | `Fire Strike` | The spell cast on left-click. Only spells in the dropdown are supported. | +| **Spell** | `Fire Strike` | Legacy single-spell setting. On startup the plugin migrates this into **Slot 1 Spell** if Slot 1 is still at its default — keeps existing configs working without any manual action. | | **Require magic weapon** | `true` | When on, the Cast entry only shows while a staff, bladed staff, powered staff, or powered wand is equipped (detected via varbit `EQUIPPED_WEAPON_TYPE`). Disable to cast regardless of weapon. | +### Spell slots and hotkeys + +The plugin exposes five independently configurable spell slots, grouped under two sections in the config panel: + +| Section | Options | +| --- | --- | +| **Spell Slots** | `Slot 1 Spell` … `Slot 5 Spell` — each picks any spell from the full supported-spells dropdown. All five default to `Fire Strike`. | +| **Hotkeys** | `Slot 1 Hotkey` … `Slot 5 Hotkey` — RuneLite-standard hotkey pickers, all unbound by default. `Chat feedback on slot change` — toggles the chat message posted when a hotkey switches slots (default on). | + +**How slot switching works:** + +- **Slot 1 is always active at startup.** Enabling the plugin (or restarting the client) resets the active slot to Slot 1. The active slot is runtime-only — it is never written to config. +- **Press a bound slot hotkey** (while the game window is focused and no text field is active) to make that slot the active slot. The next menu-sort uses the new slot's spell. +- **Unbound hotkeys are inert.** A slot whose hotkey is `Not set` cannot be activated by keypress. RuneLite's hotkey plumbing suppresses hotkeys while you're typing in a chat or search widget, so hotkey letters won't accidentally swap slots during text entry. +- **Slot 1 needs no hotkey.** Because it's the startup default, leave its hotkey unbound unless you want to explicitly return to it from another slot. +- **Chat feedback** (when enabled) prints `Left-Click Cast: now casting ` on every slot change. Toggle it off if it's noisy during combat rotations. + ## Limitations - **No rune auto-management.** If you run out of runes, the cast fails cleanly and you see the normal "You do not have enough ..." chat message. From 54f48d83f0d987faceb17bebca4c9da657a701a9 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 14 Apr 2026 21:33:54 -0400 Subject: [PATCH 11/12] feat(leftclickcast): fast-path cast dispatch via dual menuAction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the async Rs2Magic.castOn pipeline (tab switch + 150-300ms sleep + sleepUntil(isWidgetSelected) + NPC interact) with two synchronous client.menuAction calls fired back-to-back: one WIDGET_TARGET to select the spell client-side, one WIDGET_TARGET_ON_NPC / WIDGET_TARGET_ON_PLAYER to dispatch it on the hovered target. Both packets queue on the same event-loop tick, so the server processes selection and cast on the same game tick — indistinguishable from "I pre-selected the spell and clicked the target". Falls back to Rs2Magic.castOn when the spellbook widget (group 218) isn't loaded yet (first cast after login without opening Magic tab), nudging Rs2Tab.switchTo(InterfaceTab.MAGIC) so subsequent clicks take the fast path. Also falls back when the selected spell isn't on the active spellbook (modern vs. ancients mismatch). Bumps plugin version to 1.2.0. --- .../leftclickcast/LeftClickCastPlugin.java | 63 ++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java index 2fde871a39..a51c4c92b7 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -25,9 +25,13 @@ import net.runelite.client.input.KeyManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.api.widgets.Widget; import net.runelite.client.plugins.microbot.PluginConstants; import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; +import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; +import net.runelite.client.plugins.skillcalculator.skills.MagicAction; import net.runelite.client.util.HotkeyListener; @PluginDescriptor( @@ -42,7 +46,7 @@ ) public class LeftClickCastPlugin extends Plugin { - static final String version = "1.1.0"; + static final String version = "1.2.0"; private static final int SLOT_COUNT = 5; @@ -160,16 +164,11 @@ public void onPostMenuSort(PostMenuSort event) } MenuEntry attack = entries[attackIdx]; - // Rs2Magic.castOn requires Rs2NpcModel for NPCs but accepts raw Player (Rs2PlayerModel implements Player). - final Actor dispatchTarget = targetActor instanceof NPC - ? new Rs2NpcModel((NPC) targetActor) - : (Player) targetActor; + final Actor dispatchTarget = targetActor; final PertTargetSpell dispatchSpell = spell; attack.setOption("Cast " + dispatchSpell.getDisplayName()); attack.setType(MenuAction.RUNELITE); - // Rs2Magic.castOn uses sleepUntil, which is a no-op on the client thread — dispatch off-thread. - attack.onClick(e -> CompletableFuture.runAsync( - () -> Rs2Magic.castOn(dispatchSpell.getMagicAction(), dispatchTarget))); + attack.onClick(e -> castOnTargetFast(dispatchSpell, dispatchTarget)); // Move to the tail of the array — that slot is the left-click action in RuneLite's menu model. if (attackIdx != entries.length - 1) @@ -232,6 +231,54 @@ private void onSlotHotkey(int index) } } + // Fast-path cast: fire two synchronous client.menuAction packets back-to-back so the server processes + // the spell selection and the spell-on-target dispatch on the same game tick. Falls back to + // Rs2Magic.castOn (tab switch + sleeps + clicks) if the spellbook widget isn't loaded yet or the + // spell isn't on the current spellbook. + private void castOnTargetFast(PertTargetSpell spell, Actor target) + { + if (target == null) + { + return; + } + MagicAction magic = spell.getMagicAction(); + Widget magicRoot = client.getWidget(218, 0); + boolean widgetReady = magicRoot != null && magicRoot.getStaticChildren() != null; + if (widgetReady) + { + try + { + int spellWidgetId = magic.getWidgetId(); + // Packet 1: select the spell client-side (WIDGET_TARGET on the spell widget). + client.menuAction(-1, spellWidgetId, MenuAction.WIDGET_TARGET, 1, -1, "Cast", magic.getName()); + // Packet 2: dispatch the selected spell on the target, same tick. + if (target instanceof NPC) + { + NPC npc = (NPC) target; + client.menuAction(0, 0, MenuAction.WIDGET_TARGET_ON_NPC, npc.getIndex(), -1, "Use", npc.getName()); + } + else if (target instanceof Player) + { + Player p = (Player) target; + client.menuAction(0, 0, MenuAction.WIDGET_TARGET_ON_PLAYER, p.getId(), -1, "Use", p.getName()); + } + return; + } + catch (Exception ignored) + { + // Spell not on the active spellbook (e.g., modern while on ancients) — fall through. + } + } + else + { + // Spellbook widget not yet loaded this session; nudge it open so the next click is fast. + Rs2Tab.switchTo(InterfaceTab.MAGIC); + } + // Slow fallback path. Rs2Magic.castOn uses sleepUntil which is a no-op on the client thread, so dispatch async. + final Actor dispatch = target instanceof NPC ? new Rs2NpcModel((NPC) target) : target; + CompletableFuture.runAsync(() -> Rs2Magic.castOn(magic, dispatch)); + } + // Best-effort: if the user had previously set the legacy `spell` key to a non-default value and // slot1Spell is still at its default, copy the legacy value into slot1Spell so existing configs keep working. private void migrateLegacySpellKey() From ca055bc9c28ee4a7186b3d0085a1553bfc273491 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Wed, 15 Apr 2026 07:44:46 -0400 Subject: [PATCH 12/12] feat(leftclickcast): enable-toggle hotkey + unified chat feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an `Enable/Disable Hotkey` config item at the top of the Hotkeys section. Pressing the hotkey flips `config.enabled()` and posts an `ExternalPluginsChanged` event so the open MicrobotConfigPanel rebuilds and the inner Enabled checkbox visually flips in sync — the panel otherwise doesn't subscribe to ConfigChanged for individual checkbox refresh. Renames `activeSlotChatMessage` → `chatFeedback` and uses it as a single gate for all plugin chat output. Both checkbox clicks and the toggle hotkey route through a shared `@Subscribe onConfigChanged` handler, so they emit the same chat message via one code path. Materializes `@ConfigItem` defaults to storage on `startUp` via `configManager.setDefaultConfiguration(config, false)`. Without this, MicrobotConfigPanel's checkbox lookup returns null for newly added keys (`parseBoolean(null) → false`) while the proxy returns the @ConfigItem default, leaving the UI and runtime out of sync. Bumps plugin version to 1.3.0. --- .../leftclickcast/LeftClickCastConfig.java | 32 ++++++---- .../leftclickcast/LeftClickCastPlugin.java | 58 ++++++++++++++++++- 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java index 59cf06320a..f387be7e69 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java @@ -117,12 +117,24 @@ default PertTargetSpell slot5Spell() return PertTargetSpell.FIRE_STRIKE; } + @ConfigItem( + keyName = "enabledToggleHotkey", + name = "Enable/Disable Hotkey", + description = "Hotkey that toggles the plugin on and off.", + section = hotkeysSection, + position = 0 + ) + default Keybind enabledToggleHotkey() + { + return Keybind.NOT_SET; + } + @ConfigItem( keyName = "slot1Hotkey", name = "Slot 1 Hotkey", description = "Hotkey that activates slot 1.", section = hotkeysSection, - position = 0 + position = 1 ) default Keybind slot1Hotkey() { @@ -134,7 +146,7 @@ default Keybind slot1Hotkey() name = "Slot 2 Hotkey", description = "Hotkey that activates slot 2.", section = hotkeysSection, - position = 1 + position = 2 ) default Keybind slot2Hotkey() { @@ -146,7 +158,7 @@ default Keybind slot2Hotkey() name = "Slot 3 Hotkey", description = "Hotkey that activates slot 3.", section = hotkeysSection, - position = 2 + position = 3 ) default Keybind slot3Hotkey() { @@ -158,7 +170,7 @@ default Keybind slot3Hotkey() name = "Slot 4 Hotkey", description = "Hotkey that activates slot 4.", section = hotkeysSection, - position = 3 + position = 4 ) default Keybind slot4Hotkey() { @@ -170,7 +182,7 @@ default Keybind slot4Hotkey() name = "Slot 5 Hotkey", description = "Hotkey that activates slot 5.", section = hotkeysSection, - position = 4 + position = 5 ) default Keybind slot5Hotkey() { @@ -178,13 +190,13 @@ default Keybind slot5Hotkey() } @ConfigItem( - keyName = "activeSlotChatMessage", - name = "Chat feedback on slot change", - description = "Post a game chat message when a hotkey switches the active slot.", + keyName = "chatFeedback", + name = "Chat feedback", + description = "Post a game chat message on plugin events (active slot change, enable/disable toggle).", section = hotkeysSection, - position = 5 + position = 6 ) - default boolean activeSlotChatMessage() + default boolean chatFeedback() { return true; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java index a51c4c92b7..0a0ed5f989 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -21,7 +21,10 @@ import net.runelite.client.chat.QueuedMessage; import net.runelite.client.config.ConfigManager; import net.runelite.client.config.Keybind; +import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.events.ExternalPluginsChanged; import net.runelite.client.input.KeyManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; @@ -46,7 +49,7 @@ ) public class LeftClickCastPlugin extends Plugin { - static final String version = "1.2.0"; + static final String version = "1.3.0"; private static final int SLOT_COUNT = 5; @@ -65,10 +68,15 @@ public class LeftClickCastPlugin extends Plugin @Inject private ConfigManager configManager; + @Inject + private EventBus eventBus; + private volatile int activeSlot = 0; private final HotkeyListener[] hotkeyListeners = new HotkeyListener[SLOT_COUNT]; + private HotkeyListener enabledToggleListener; + @Provides LeftClickCastConfig provideConfig(ConfigManager configManager) { @@ -78,6 +86,9 @@ LeftClickCastConfig provideConfig(ConfigManager configManager) @Override protected void startUp() { + // MicrobotConfigPanel renders boolean checkboxes from raw stored values; missing keys read as false + // even when the @ConfigItem default is true. Materialize defaults so the UI and the proxy agree. + configManager.setDefaultConfiguration(config, false); activeSlot = 0; for (int i = 0; i < SLOT_COUNT; i++) { @@ -93,6 +104,15 @@ public void hotkeyPressed() hotkeyListeners[i] = listener; keyManager.registerKeyListener(listener); } + enabledToggleListener = new HotkeyListener(() -> config.enabledToggleHotkey()) + { + @Override + public void hotkeyPressed() + { + onEnabledToggleHotkey(); + } + }; + keyManager.registerKeyListener(enabledToggleListener); migrateLegacySpellKey(); } @@ -108,6 +128,11 @@ protected void shutDown() hotkeyListeners[i] = null; } } + if (enabledToggleListener != null) + { + keyManager.unregisterKeyListener(enabledToggleListener); + enabledToggleListener = null; + } } @Subscribe @@ -220,7 +245,7 @@ private PertTargetSpell slotSpellFor(int index) private void onSlotHotkey(int index) { activeSlot = index; - if (config.activeSlotChatMessage()) + if (config.chatFeedback()) { PertTargetSpell spell = slotSpellFor(index); String display = spell != null ? spell.getDisplayName() : "(no spell)"; @@ -231,6 +256,35 @@ private void onSlotHotkey(int index) } } + private void onEnabledToggleHotkey() + { + boolean newValue = !config.enabled(); + configManager.setConfiguration("leftclickcast", "enabled", newValue); + // MicrobotConfigPanel doesn't subscribe to ConfigChanged for individual checkbox refresh, but it does + // rebuild on ExternalPluginsChanged. Posting that here makes the open config panel re-read this and + // every other config item, so the "Enabled" checkbox visually flips to match the keybind toggle. + eventBus.post(new ExternalPluginsChanged()); + // Chat feedback is emitted by onConfigChanged so checkbox clicks and hotkey presses share one path. + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!"leftclickcast".equals(event.getGroup()) || !"enabled".equals(event.getKey())) + { + return; + } + if (!config.chatFeedback()) + { + return; + } + boolean enabled = "true".equals(event.getNewValue()); + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.GAMEMESSAGE) + .value("Left-Click Cast: " + (enabled ? "enabled" : "disabled")) + .build()); + } + // Fast-path cast: fire two synchronous client.menuAction packets back-to-back so the server processes // the spell selection and the spell-on-target dispatch on the same game tick. Falls back to // Rs2Magic.castOn (tab switch + sleeps + clicks) if the spellbook widget isn't loaded yet or the