Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,14 @@ public boolean shouldBlockAndProcess()

return true;
}

/**
* Returns whether a blocking event is currently executing.
* Unlike {@link #shouldBlockAndProcess()}, this method is read-only and does not
* dequeue or trigger any events. Safe to call from the client thread.
*/
public boolean isBlocking()
{
return isRunning.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,56 @@ sleepUntil(() -> Rs2Bank.isOpen(), 5000);

---

## Tick-Aligned Detection

The framework provides tick-aligned detection to reduce latency between game state changes and script reactions. Game state only changes on server ticks (~600ms), so checking between ticks is wasted work.

### Tick-Aligned sleepUntil

The standard `sleepUntil()` method wakes on game tick boundaries instead of polling every 100ms. This is automatic - all existing scripts benefit without code changes:

```java
// Same API, same thread, same behavior - just detects faster
sleepUntil(() -> Rs2Bank.isOpen(), 5000); // wakes when tick fires, not 100ms later
```

### scheduleOnGameTick (Opt-In)

Scripts can opt into tick-aligned loop execution instead of wall-clock timing:

```java
// BEFORE: runs every 600ms wall-clock (drifts relative to ticks)
mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(body, 0, 600, TimeUnit.MILLISECONDS);

// AFTER: runs exactly once per game tick
mainScheduledFuture = scheduleOnGameTick(body);
```

The loop body runs on a script thread where all Rs2 utilities and sleeps work normally. This is purely opt-in - existing scripts using `scheduleWithFixedDelay` continue to work unchanged.

### Additional Tick Utilities

```java
// Block until the next game tick fires
sleepUntilNextTick();

// Block for exactly N game ticks
sleepForTicks(3);

// With custom wall-clock timeout safety
sleepForTicks(3, 5000);
```

### Key Files

| File | Purpose |
|------|---------|
| `util/tick/TickDispatcher.java` | Singleton that signals script threads on each game tick |
| `util/tick/TickWaiter.java` | Thread synchronization primitive for tick-aligned sleep |
| `util/tick/TickListener.java` | Callback interface for tick notifications |

---

## Utility System (util/ folder)

The utility system provides static facade classes prefixed with `Rs2*` that abstract RuneLite API interactions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import net.runelite.client.plugins.microbot.api.playerstate.Rs2PlayerStateCache;
import net.runelite.client.plugins.microbot.api.tileitem.Rs2TileItemCache;
import net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache;
import net.runelite.client.plugins.microbot.util.tick.TickDispatcher;
import net.runelite.client.plugins.microbot.util.security.LoginManager;
import net.runelite.client.plugins.microbot.util.widget.Rs2Widget;
import net.runelite.client.ui.overlay.infobox.InfoBoxManager;
Expand Down Expand Up @@ -195,6 +196,10 @@ public class Microbot {
@Getter
private static Rs2PlayerStateCache rs2PlayerStateCache;

@Inject
@Getter
private static TickDispatcher tickDispatcher;

@Inject
@Getter
private static Rs2NpcCache rs2NpcCache;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,33 @@ public boolean isRunning() {
@Getter
protected static WorldPoint initialPlayerLocation;

/**
* Schedules the given body to execute once per game tick on a script thread.
* Unlike {@code scheduleWithFixedDelay(600ms)} which drifts on wall-clock time,
* this wakes exactly when each game tick fires via the {@link net.runelite.client.plugins.microbot.util.tick.TickDispatcher}.
* The body runs on a script thread where all Rs2 utilities and sleeps work normally.
*
* @param body the logic to execute each tick
* @return a ScheduledFuture that can be cancelled to stop execution
*/
protected ScheduledFuture<?> scheduleOnGameTick(Runnable body) {
return scheduledExecutorService.schedule(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Global.sleepUntilNextTick();
if (Thread.currentThread().isInterrupted()) break;
body.run();
} catch (Exception exception) {
if (exception instanceof InterruptedException) {
Thread.currentThread().interrupt();
break;
}
Microbot.logStackTrace("scheduleOnGameTick: ", exception);
}
}
}, 0, java.util.concurrent.TimeUnit.MILLISECONDS);
}

/**
* Cancel scheduled tasks, clear shared state, and reset helpers.
* Safe to call multiple times; no-ops if already shut down.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import lombok.SneakyThrows;
import net.runelite.client.plugins.microbot.Microbot;
import net.runelite.client.plugins.microbot.util.math.Rs2Random;
import net.runelite.client.plugins.microbot.util.tick.TickDispatcher;
import net.runelite.client.plugins.microbot.util.tick.TickWaiter;

import java.util.concurrent.*;
import java.util.function.BooleanSupplier;
Expand Down Expand Up @@ -68,22 +70,29 @@ public static boolean sleepUntil(BooleanSupplier awaitedCondition) {
}

/**
* Polls until the supplied condition becomes true or the given duration elapses.
* No-op on the client thread to avoid blocking RuneLite.
* Polls until the supplied condition becomes true or timeout elapses.
* Wakes on game tick boundaries for precise detection instead of polling every 100ms.
* Must not be invoked on the client thread; callers should run on script/executor threads.
*/
public static boolean sleepUntil(BooleanSupplier awaitedCondition, int time) {
if (Microbot.getClient().isClientThread()) return false;
boolean done = false;
long startTime = System.currentTimeMillis();
long deadline = System.currentTimeMillis() + time;
try {
do {
done = awaitedCondition.getAsBoolean();
sleep(100);
} while (!done && System.currentTimeMillis() - startTime < time);
while (System.currentTimeMillis() < deadline) {
if (awaitedCondition.getAsBoolean()) return true;
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) break;
if (!sleepUntilNextTick(Math.min(remaining, 600))) {
// Tick-based wake not available (dispatcher null or no tick fired within timeout).
// Fall back to a short sleep to prevent busy-spinning.
sleep(100);
}
}
return awaitedCondition.getAsBoolean();
} catch (Exception e) {
Microbot.logStackTrace("Global Sleep: ", e);
}
return done;
return false;
}

public static boolean sleepUntil(BooleanSupplier awaitedCondition, Runnable action, long timeoutMillis, int sleepMillis) {
Expand Down Expand Up @@ -179,4 +188,56 @@ public boolean sleepUntilTick(int ticksToWait) {
int startTick = Microbot.getClient().getTickCount();
return Global.sleepUntil(() -> Microbot.getClient().getTickCount() >= startTick + ticksToWait, ticksToWait * 600 + 2000);
}

/**
* Blocks the calling thread until the next game tick fires.
* No-op if called on the client thread.
*/
public static void sleepUntilNextTick() {
sleepUntilNextTick(2000);
}

/**
* Blocks the calling thread until the next game tick fires, or the wall-clock timeout expires.
* No-op if called on the client thread.
*
* @param wallClockTimeoutMs maximum wall-clock time to wait in milliseconds
* @return true if a tick was received, false if timed out
*/
public static boolean sleepUntilNextTick(long wallClockTimeoutMs) {
if (Microbot.getClient().isClientThread()) return false;
TickDispatcher dispatcher = Microbot.getTickDispatcher();
if (dispatcher == null) return false;
TickWaiter waiter = dispatcher.registerWait(null, 1, wallClockTimeoutMs);
return waiter.await();
}

/**
* Blocks the calling thread for exactly the given number of game ticks.
* No-op if called on the client thread.
*
* @param ticks number of game ticks to wait
* @return true if all ticks elapsed, false if timed out
*/
public static boolean sleepForTicks(int ticks) {
return sleepForTicks(ticks, ticks * 800L + 2000);
}

/**
* Blocks the calling thread for exactly the given number of game ticks,
* or until the wall-clock timeout expires.
* No-op if called on the client thread.
*
* @param ticks number of game ticks to wait
* @param wallClockTimeoutMs maximum wall-clock time to wait in milliseconds
* @return true if all ticks elapsed, false if timed out
*/
public static boolean sleepForTicks(int ticks, long wallClockTimeoutMs) {
if (Microbot.getClient().isClientThread()) return false;
TickDispatcher dispatcher = Microbot.getTickDispatcher();
if (dispatcher == null) return false;
TickWaiter waiter = dispatcher.registerWait(null, ticks, wallClockTimeoutMs);
return waiter.await();
}

}
Loading