Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/api/Rs2LeaguesTransport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Rs2LeaguesTransport

**Source:** `runelite-client/.../microbot/util/leaguetransport/Rs2LeaguesTransport.java`
**Related:** `LeaguesRegion`, `LeagueTransportWidgets`

API driver for Leagues area teleports via UI chain:

`Activities -> Leagues -> View Areas -> Teleport to <region>`

## Primary call

- `LeaguesTeleportResult leaguesTeleport(LeaguesRegion region)`
- Runs context gates + unlocked-region scan in one client-thread pass
- Blocks caller thread only (no client-thread blocking)
- Performs full UI chain internally
- Returns rich result: success, failure reason enum, message, target, unlocked snapshot

## Gates

- **Seasonal world**: `WorldType.SEASONAL`
- **League save active**: `VarbitID.LEAGUE_ACCOUNT > 0`
- **Region unlocked**: `region.getAreaId()` present in `LEAGUE_AREA_SELECTION_0..5`

## Unlocked regions snapshot (future webwalker interrupt)

- `EnumSet<LeaguesRegion> unlockedRegions()`
- Returns empty set when not in leagues context
- Uses `LEAGUE_AREA_SELECTION_0..5` varbits, mapped to `LeaguesRegion` by `areaId`

## Advanced: non-blocking driver

- `Rs2LeaguesTransport.LeaguesTeleportDriver`
- Returned internally today; intended for advanced callers
- Call `tick()` from script loop until `!isActive()`

## Widget ids

Widget ids live in package-private `LeagueTransportWidgets`. Re-verify ids after game/client bumps if chain stops working.

76 changes: 75 additions & 1 deletion docs/entity-guides/items.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,78 @@ When you know the *intent* (consume, equip, drop) but not the verb the game uses

---

<!-- Add new gotchas here as numbered entries (## 2, ## 3, ...). -->
## 2. After opening the bank, wait for a live `ItemContainerChanged(BANK)` before trusting `bankItems()` / `hasBankItem`

`Rs2Bank` mirrors the bank into `Rs2BankData` from `ItemContainerChanged` on the client thread. The interface can report open before the first container event is processed, so a script that calls `hasBankItem` / `withdraw*` in the same tick can see an empty or stale cache and conclude the item is missing.

**Why this matters:** Intermittent false "not in bank" after `openBank()` and rare races when the cache is one tick behind the widget.

**Pattern to follow:**

- Use `Rs2Bank.openBank()` (it waits for a new bank epoch after the UI opens). If you open the bank through a custom path, wait until `ItemContainerChanged` has run or delay one game tick before bulk lookups.
- `hasBankItem` retries 1-2 ticks when the bank is open and the first lookup saw quantity zero (insufficient quantity still fails immediately).

**Where this applies:** `Rs2Bank.openBank`, `updateLocalBank`, `hasBankItem`, `count`, `findBankItem` call sites.

**Defensive check:** Enable DEBUG and watch for `[Rs2Bank] hasBankItem miss after cache retry` or `no BANK ItemContainerChanged within` after opening.

---

## 3. Bank cache skips placeholder rows; automation only sees real stacks

`Rs2Bank.updateLocalBank` drops items whose `ItemComposition.getPlaceholderTemplateId() > 0`. The client may show a placeholder in the slot; the cached list has no entry for it.

**Why this matters:** Scripts that expect "any bank stack" for an item id may see false negatives when the account only has a placeholder until a real item is deposited.

**Pattern to follow:** Treat placeholder-only slots as "not withdrawable" unless you add a dedicated placeholder-aware path. Document user-facing behavior in script configs.

**Where this applies:** `Rs2Bank.updateLocalBank`, any helper using `bankItems()` / `findBankItem`.

---

## 4. Saved item id vs bank row (noted/unnoted and cache drift)

`hasBankItem(id)`, `count(id)`, `hasItem(id)`, and id-based `withdraw*` resolve the bank row in order: **exact id** → **linked noted/unnoted id** from `ItemComposition` → **fuzzy name** from composition (`getMembersName` / `getName`) against cached bank stacks.

**Why this matters:** Inventory setups and scripts often store `ItemID` constants; Jagex renumbers or the bank holds the noted variant while the preset uses the unnoted id (or vice versa). Without fallback you get false “not in bank”.

**Pattern to follow:** Prefer fuzzy or name-based setup rows when ids are unstable; enable DEBUG to see `[Rs2Bank] bank id drift` when a fallback row differs from the requested id.

**Where this applies:** `Rs2Bank.findBankStackRowForSavedId`, `resolveBankStackForSavedId`, id overloads of `hasBankItem` / `count` / `withdraw*`.

---

## 5. Optional inventory-setup validation (Tier A.3)

Set JVM flag `-Dmicrobot.bank.validateInventorySetup=true` so `Rs2InventorySetup.loadInventory` warns once per issue: invalid id, missing `ItemComposition`, id/name mismatch vs cache, and a **single inventory row** with quantity greater than 1 for a **non-stackable** item (same rule as withdraw grouping: use one row per unstacked item or fuzzy mode).

**Where this applies:** `Rs2InventorySetup.validateInventorySetupAgainstDefsIfEnabled`.

---

## 6. Inventory-setup load: keep-list uses ids + names, deposit only when needed

`Rs2InventorySetup.loadInventory()` (default) skips the bank when the inventory already matches the setup **and** there are no “foreign” stacks (items not in the setup’s keep list) **and** quantities do not exceed the setup’s grouped targets. Otherwise it opens the bank and calls `Rs2Bank.depositAllExcept(Set<Integer>, Map<String, Boolean>)`: non-fuzzy rows contribute exact ids (plus linked noted/unnoted ids); fuzzy rows contribute name keys (`true` = substring keep). The keep list includes inventory, equipment, additional filtered items, and rune pouch entries.

**Why this matters:** Name-only `depositAllExcept(Map)` missed noted/unnoted pairs and extra sections; unconditional deposit caused unnecessary UI churn.

**Pattern to follow:** Use `loadInventory(false)` when you must always open the bank (legacy behavior). For custom scripts, reuse `Rs2Bank.isInventoryItemRetainedForSetupDeposit` semantics when building keep predicates.

**Where this applies:** `Rs2InventorySetup.loadInventory`, `loadEquipment`, `Rs2Bank.depositAllExcept(Set, Map)`.

---

## 7. Release / regression — bank mirror (Tier C)

**Automated (CI):** `Rs2BankSetupDepositRetainTest` covers `isInventoryItemRetainedForSetupDeposit` (id + fuzzy + exact name).

**Manual smoke after a banking-affecting change:**

1. Open bank on a live profile; confirm inventory-setup load (or a script using `Rs2Bank.hasBankItem`) sees **coins** (`995`) and at least **one rune** you know is in the bank.
2. If setup load aborts with `Bank item mirror not ready after open`, capture DEBUG `Rs2Bank` logs and check `getBankLiveEpoch()` / `ItemContainerChanged(BANK)` delivery.

**Where this applies:** `Rs2Bank.getBankLiveEpoch`, `verifyBankMirrorAfterOpen`, `Rs2InventorySetup.loadInventory` / `loadEquipment`.

---

<!-- Add new gotchas here as numbered entries (## 8, ## 9, ...). -->
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ project.build.group=net.runelite
project.build.version=1.12.26.2

glslang.path=
microbot.version=2.5.8
microbot.version=2.5.9
microbot.commit.sha=nogit
microbot.repo.url=http://138.201.81.246:8081/repository/microbot-snapshot/
microbot.repo.username=
Expand Down
1 change: 1 addition & 0 deletions runelite-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ tasks.register<Test>("runUnitTests") {
exclude("**/Rs2WalkerIntegrationTest.class")
exclude("**/Rs2ReflectionGroundItemActionsIntegrationTest.class")
exclude("**/threadsafety/ClientThreadScannerTest.class")
exclude("**/ScreenshotHandlerTest.class")

useJUnit()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import net.runelite.client.plugins.microbot.util.overlay.GembagOverlay;
import net.runelite.client.plugins.microbot.util.player.Rs2Player;
import net.runelite.client.plugins.microbot.util.reflection.Rs2Reflection;
import net.runelite.client.plugins.microbot.util.walker.Rs2Walker;
import net.runelite.client.plugins.microbot.util.leaguetransport.Rs2LeaguesTransport;
import net.runelite.client.plugins.microbot.util.leaguetransport.SeasonalTransportHandlers;
import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache;
import net.runelite.client.plugins.microbot.util.shop.Rs2Shop;
import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab;
Expand All @@ -40,22 +43,25 @@
import net.runelite.client.ui.overlay.OverlayManager;
import net.runelite.client.ui.overlay.OverlayMenuEntry;
import net.runelite.client.util.ImageUtil;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.swing.*;
import java.awt.*;
import java.awt.AWTException;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.*;
import java.util.EnumSet;
import java.util.List;

import java.util.Objects;
import java.util.Optional;
@PluginDescriptor(
name = PluginDescriptor.Default + "Microbot",
description = "Microbot",
Expand All @@ -67,6 +73,14 @@
@Slf4j
public class MicrobotPlugin extends Plugin
{
/**
* Max age of {@code lastTransportAttempt} for attributing locked-region chat to a click.
* Canonical value is {@link Rs2LeaguesTransport#LEAGUES_LOCK_CHAT_MAX_ATTEMPT_AGE_MS}; kept here for script compatibility.
*
* @apiNote Treat as stable external API: renames or semantic changes break scripts — note in changelog when modifying.
*/
public static final long LEAGUES_LOCK_CHAT_MAX_ATTEMPT_AGE_MS = Rs2LeaguesTransport.LEAGUES_LOCK_CHAT_MAX_ATTEMPT_AGE_MS;
private EnumSet<WorldType> lastWorldTypeProfile = null;

@Inject
private Provider<MicrobotPluginListPanel> pluginListPanelProvider;
Expand Down Expand Up @@ -176,6 +190,8 @@ protected void startUp() throws AWTException

Microbot.getPouchScript().startUp();

Rs2Walker.setSeasonalTransportHandlers(SeasonalTransportHandlers.defaultHandlerList());

if (overlayManager != null)
{
overlayManager.add(microbotOverlay);
Expand Down Expand Up @@ -291,6 +307,12 @@ public void onGameStateChanged(GameStateChanged gameStateChanged)
// Region-based login detection logic
final Client client = Microbot.getClient();
if (client != null) {
EnumSet<WorldType> worldTypeProfile = normalizeWorldTypesForProfileComparison(client.getWorldType());
if (lastWorldTypeProfile != null && !lastWorldTypeProfile.equals(worldTypeProfile))
{
Rs2Bank.invalidateBankMirrorCache("world-type-profile-transition");
}
lastWorldTypeProfile = worldTypeProfile;
int[] currentRegions = client.getTopLevelWorldView().getMapRegions();
boolean wasLoggedIn = LoginManager.getLastKnownGameState() == GameState.LOGGED_IN;
if (!wasLoggedIn) {
Expand All @@ -309,11 +331,27 @@ public void onGameStateChanged(GameStateChanged gameStateChanged)
// and we also handle correct cache loading in onRuneScapeProfileChanged event
LoginManager.markLoggedOut();
Microbot.setLastKnownRegions(null);
Rs2LeaguesTransport.onLogout();
}
// update last known game state to track login/logout transitions
LoginManager.setLastKnownGameState(gameStateChanged.getGameState());
}

private static EnumSet<WorldType> normalizeWorldTypesForProfileComparison(EnumSet<WorldType> rawTypes)
{
EnumSet<WorldType> normalized = rawTypes == null
? EnumSet.noneOf(WorldType.class)
: rawTypes.clone();
// Profile compare should ignore normal-world and combat-variant flags.
normalized.remove(WorldType.MEMBERS);
normalized.remove(WorldType.PVP);
normalized.remove(WorldType.BOUNTY);
normalized.remove(WorldType.SKILL_TOTAL);
normalized.remove(WorldType.HIGH_RISK);
normalized.remove(WorldType.LAST_MAN_STANDING);
return normalized;
}

@Subscribe
public void onVarClientIntChanged(VarClientIntChanged event)
{
Expand Down Expand Up @@ -383,18 +421,54 @@ private void onMenuOptionClicked(MenuOptionClicked event)
@Subscribe
private void onChatMessage(ChatMessage event)
{
if (event.getType() == ChatMessageType.ENGINE && event.getMessage().equalsIgnoreCase("I can't reach that!"))
if (event.getType() == ChatMessageType.ENGINE)
{
Microbot.cantReachTarget = true;
String msg = event.getMessage();
if (msg != null && msg.equalsIgnoreCase("I can't reach that!"))
{
Microbot.cantReachTarget = true;
}
}
if (event.getType() == ChatMessageType.GAMEMESSAGE && event.getMessage().toLowerCase().contains("you can't log into a non-members"))
if (event.getType() == ChatMessageType.GAMEMESSAGE)
{
Microbot.cantHopWorld = true;
String msg = event.getMessage();
if (msg != null && containsIgnoreCase(msg, "you can't log into a non-members"))
{
Microbot.cantHopWorld = true;
}

// Leagues: "haven't unlocked access to X area" -> blacklist last transport dest.
if (msg != null)
{
Rs2LeaguesTransport.onLockedRegionGameMessage(msg);
}
}
Microbot.getPouchScript().onChatMessage(event);
Rs2Gembag.onChatMessage(event);
}

private static boolean containsIgnoreCase(String haystack, String needle)
{
if (haystack == null || needle == null || needle.isEmpty())
{
return false;
}
int hLen = haystack.length();
int nLen = needle.length();
if (nLen > hLen)
{
return false;
}
for (int i = 0; i <= hLen - nLen; i++)
{
if (haystack.regionMatches(true, i, needle, 0, nLen))
{
return true;
}
}
return false;
}

@Subscribe
public void onConfigChanged(ConfigChanged ev)
{
Expand Down Expand Up @@ -517,8 +591,8 @@ public void onOverlayMenuClicked(OverlayMenuClicked overlayMenuClicked)
@Subscribe
public void onGameTick(GameTick event)
{
// Cache loading is now handled properly during login/profile changes
// No need to call loadInitialCacheFromCurrentConfig on every tick
// Start Leagues teleport calibration ASAP after login (non-blocking; prompts for consent once).
Rs2LeaguesTransport.tickLeaguesCalibration();
}

@Subscribe(priority = 100)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ public void shutdown() {
Microbot.pauseAllScripts.set(false);
Rs2Walker.disableTeleports = false;
Microbot.getSpecialAttackConfigs().reset();
Rs2Walker.setTarget(null);
}
if (scheduledFuture != null && !scheduledFuture.isDone()) {
scheduledFuture.cancel(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ private void handleInitiatingBreakState() {
// Pause all scripts
Microbot.pauseAllScripts.compareAndSet(false, true);
PluginPauseEvent.setPaused(true);
Rs2Walker.setTarget(null);
Rs2Walker.clearWalkingRoute("break-handler:initiating-break");

// Remember the world we were in before the break
preBreakWorld = Microbot.getClientThread().runOnClientThreadOptional(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ public boolean run(QuestHelperConfig config, QuestHelperPlugin mQuestPlugin) {
}

if (Rs2Dialogue.isInDialogue() && dialogueStartedStep == questStep) {
Rs2Walker.setTarget(null);
Rs2Walker.clearWalkingRoute("quest-helper:dialogue-space-step");
Rs2Keyboard.keyPress(KeyEvent.VK_SPACE);
return;
} else {
Expand Down Expand Up @@ -1296,7 +1296,7 @@ public boolean applyNpcStep(NpcStep step) {

if (npc != null && npc.getLocalLocation() != null && Rs2Camera.isTileOnScreen(npc.getLocalLocation())
&& (Microbot.getClient().isInInstancedRegion() || Rs2Walker.canReach(npc.getWorldLocation()))) {
Rs2Walker.setTarget(null);
Rs2Walker.clearWalkingRoute("quest-helper:npc-step-visible-interact");

if (step.getText().stream().anyMatch(x -> x.toLowerCase().contains("kill"))) {
if (!Rs2Combat.inCombat()) {
Expand Down Expand Up @@ -1428,7 +1428,7 @@ public boolean applyObjectStep(ObjectStep step) {
}

if (hasLineOfSightToObject(object) || object != null && (Rs2Camera.isTileOnScreen(object.getLocalLocation()) || object.getCanvasLocation() != null)) {
Rs2Walker.setTarget(null);
Rs2Walker.clearWalkingRoute("quest-helper:object-step-interact");

if (itemId == -1)
object.click(chooseCorrectObjectOption(step, object));
Expand Down
Loading
Loading