Skip to content

Feature/tick script system#4

Open
Posuceius wants to merge 3 commits intomainfrom
feature/tick-script-system
Open

Feature/tick script system#4
Posuceius wants to merge 3 commits intomainfrom
feature/tick-script-system

Conversation

@Posuceius
Copy link
Copy Markdown
Owner

@Posuceius Posuceius commented Apr 6, 2026


Summary by cubic

Introduce a tick-synchronized execution system that aligns script loops and waits with game ticks to cut latency and drift. Sleeps now wake on tick boundaries; new APIs let scripts run exactly once per tick with wall-clock timeouts and safe logout cleanup.

  • New Features

    • Added util/tick/TickDispatcher, TickListener, and TickWaiter for tick-based dispatch and waits with wall-clock timeouts and logout cleanup.
    • Global.sleepUntil now wakes on tick boundaries (with fallback); added sleepUntilNextTick() and sleepForTicks(int[, long]).
    • Script.scheduleOnGameTick(Runnable) to run logic once per game tick on a script thread.
    • Microbot now injects TickDispatcher for global access.
    • BlockingEventManager.isBlocking() exposes read-only blocking status (safe on client thread).
    • Updated CLAUDE.md with usage docs and examples.
  • Migration

    • No changes required: existing scripts benefit from faster sleepUntil.
    • Opt-in: switch loops to scheduleOnGameTick to run exactly per tick.

Written for commit 16746f4. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Tick-synchronized timing: waits and schedulers aligned to game tick boundaries for more precise, efficient timing
    • Tick-aligned scheduling utility to run tasks once per game tick
    • Runtime blocking-state query to check whether a blocking operation is active
  • Documentation

    • New guidance and examples for tick-aligned timing utilities and usage

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Introduces a tick-synchronized event system: new TickDispatcher, TickListener, and TickWaiter manage game-tick callbacks and waiters; Global gains tick-aligned sleep/wait utilities; Script adds a tick-scheduled helper; Microbot exposes the TickDispatcher; BlockingEventManager exposes a read-only isBlocking() method; documentation updated.

Changes

Cohort / File(s) Summary
Tick Infrastructure
runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java, runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickListener.java, runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java
New singleton dispatcher subscribing to game ticks, new listener interface and waiter primitive. TickDispatcher sequences onGameTick → onPostTick → evaluate waiters; TickWaiter handles per-wait condition/tick caps and signaling.
Global Utilities
runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java
Reworked sleepUntil(...) to use tick-boundary wakeups, added sleepUntilNextTick(...) and sleepForTicks(...) overloads and timeouts; enforces client-thread restrictions and returns boolean on timeout/failure.
Script / Scheduler
runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java
Added protected ScheduledFuture<?> scheduleOnGameTick(Runnable) to run a provided body once per game tick on a scheduled thread, with interruption and exception handling.
Integration
runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java
Added injected static TickDispatcher field exposed via a public static getter.
Blocking State
runelite-client/src/main/java/net/runelite/client/plugins/microbot/BlockingEventManager.java
Added public boolean isBlocking() to expose current execution state via existing atomic flag without dequeuing or triggering processing.
Docs
runelite-client/src/main/java/net/runelite/client/plugins/microbot/CLAUDE.md
Documentation section "Tick-Aligned Detection" added: describes tick-synchronized sleep APIs, scheduleOnGameTick, and related utilities and key files.

Sequence Diagram(s)

sequenceDiagram
    participant Script as Script Thread
    participant TickD as TickDispatcher
    participant TickW as TickWaiter
    participant Cond as Condition<br/>(Lock)
    participant Client as Client Thread<br/>(GameTick Event)

    Script->>TickD: registerWait(condition, maxTicks, timeout)
    TickD->>TickW: create & queue TickWaiter
    TickD-->>Script: return TickWaiter

    Script->>TickW: await()
    TickW->>Cond: acquire lock + await signal
    Note over TickW: Script thread blocked

    Client->>TickD: onGameTick(tickCount)
    TickD->>TickD: invoke onGameTick listeners
    TickD->>TickD: invoke onPostTick listeners
    TickD->>TickW: evaluateAndSignal(currentTick)
    TickW->>TickW: check condition / tick cap

    alt Condition Met or Cap/Timeout
        TickW->>Cond: signalAll()
    end

    Cond-->>TickW: lock reacquired
    TickW-->>Script: return conditionMet
Loading
sequenceDiagram
    participant Script as Script Thread
    participant Global as Global.sleepUntil()
    participant TickD as TickDispatcher
    participant TickW as TickWaiter
    participant Client as Client Thread

    Script->>Global: sleepUntil(condition, timeoutMs)
    Global->>Global: compute deadline & immediate check

    alt Condition True
        Global-->>Script: return true
    else
        Global->>TickD: registerWait(condition, 0, timeoutMs)
        TickD->>TickW: create & queue waiter
        Global->>TickW: await()
        Note over TickW: Script blocked until tick or timeout

        Client->>TickD: onGameTick(tick)
        TickD->>TickW: evaluateAndSignal(tick)
        TickW->>TickW: signal completion

        TickW-->>Global: return boolean
        Global-->>Script: return result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A twitchy rabbit taps the clock,
Hop-sync’d to every little knock,
Ticks align, the waiters cheer,
Threads wake up — the path is clear! 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Feature/tick script system' is vague and does not clearly convey the main changes; it lacks specificity about what the feature does or its primary purpose. Consider a more descriptive title such as 'Add tick-synchronized execution system for scripts' or 'Implement TickDispatcher for game tick alignment' to better communicate the main change.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tick-script-system

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java (1)

188-209: Inconsistent return type between overloads.

sleepUntilNextTick() returns void while sleepUntilNextTick(long) returns boolean. This minor inconsistency could confuse callers who might expect to check timeout status from the no-arg version.

♻️ Consider returning boolean for consistency
-public static void sleepUntilNextTick() {
-    sleepUntilNextTick(2000);
-}
+public static boolean sleepUntilNextTick() {
+    return sleepUntilNextTick(2000);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java`
around lines 188 - 209, The no-arg overload sleepUntilNextTick() currently
returns void while sleepUntilNextTick(long) returns boolean; change
sleepUntilNextTick() to return boolean and propagate the result from
sleepUntilNextTick(2000) so callers can observe timeout status, update its
Javadoc to document the boolean return, and adjust any call sites of
sleepUntilNextTick() that assumed void to handle/ignore the boolean as
appropriate; refer to the methods sleepUntilNextTick() and
sleepUntilNextTick(long) and the TickWaiter usage for locating the code to
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java`:
- Around line 197-204: The registerWait method registers a TickWaiter with
startTick taken from the cached currentTick which may be zero before the first
GameTick and cause immediate timeouts; modify registerWait in TickDispatcher to
use the live client tick (client.getTickCount()) as the startTick (or fall back
to client.getTickCount() only when currentTick == 0) when constructing the
TickWaiter so that TickWaiter startTick reflects the real client tick counter
(keep references to TickWaiter and its constructor signature intact and only
change the value passed as startTick).

In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java`:
- Around line 91-96: The unconditional-wait branch in TickWaiter (where
condition == null) currently calls signalCompletion(true) immediately; change it
to count ticks and only resolve when the elapsed ticks reach maxTicks: record
the start tick (or use an existing startTick field), compute elapsed =
currentTick - startTick (or increment an internal tick counter each tick), and
call signalCompletion(true) only when elapsed >= maxTicks; otherwise just return
false so the waiter keeps waiting; update the logic around currentTick, maxTicks
and signalCompletion in the TickWaiter tick handler to mirror the
conditional-wait path.

---

Nitpick comments:
In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java`:
- Around line 188-209: The no-arg overload sleepUntilNextTick() currently
returns void while sleepUntilNextTick(long) returns boolean; change
sleepUntilNextTick() to return boolean and propagate the result from
sleepUntilNextTick(2000) so callers can observe timeout status, update its
Javadoc to document the boolean return, and adjust any call sites of
sleepUntilNextTick() that assumed void to handle/ignore the boolean as
appropriate; refer to the methods sleepUntilNextTick() and
sleepUntilNextTick(long) and the TickWaiter usage for locating the code to
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77befb3c-de0d-4c7c-a011-b56d7b2cc4e2

📥 Commits

Reviewing files that changed from the base of the PR and between 587d51a and d8067b5.

📒 Files selected for processing (8)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/BlockingEventManager.java
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/CLAUDE.md
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickListener.java
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java">

<violation number="1" location="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java:143">
P2: Orphaned waiters are cleared without being signaled on logout, leaving script threads blocked until their wall-clock timeout expires. Iterate over the waiters and signal their completion before clearing the queue. This requires adding a package-private `cancel()` method (or similar) to `TickWaiter` that calls `signalCompletion(false)`, then calling it here for each waiter before `clear()`.</violation>

<violation number="2" location="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java:199">
P2: Edge case: if `registerWait` is called before the first `GameTick` event fires, `currentTick` is still 0 while the real tick count (from `client.getTickCount()`) may already be large. When `evaluateAndSignal` runs with the real tick count, `(currentTick - startTick) >= maxTicks` will be true immediately, causing the waiter to timeout on its very first evaluation. Use `client.getTickCount()` instead of `currentTick` here to capture the accurate tick count at registration time.</violation>
</file>

<file name="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java">

<violation number="1" location="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java:84">
P1: Tick timeout check fires before condition evaluation, so on the boundary tick a waiter that should succeed is instead reported as timed-out. Move the timeout check after the condition evaluation (or the unconditional-wait check) so the condition gets one last chance on the final allowed tick.</violation>

<violation number="2" location="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java:91">
P1: Bug: unconditional waits (`condition == null`) resolve immediately on the first tick, ignoring `maxTicks`. For `sleepForTicks(3)`, which calls `registerWait(null, 3, timeout)`: on the first tick evaluation, the tick-limit check `(currentTick - startTick) >= 3` is false (only 1 tick elapsed), so it falls through to the `condition == null` branch which immediately signals completion. This means `sleepForTicks(N)` always waits only 1 tick regardless of N.

The unconditional path should return `false` to keep waiting, letting only the tick-limit check above resolve it when enough ticks have elapsed.</violation>

<violation number="3" location="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java:135">
P2: When `await()` returns due to wall-clock timeout, `completed` is not set to `true`, so the dispatcher continues holding and evaluating this waiter every tick. If `maxTicks` is 0, the orphaned waiter stays in the queue indefinitely. Set `completed = true` before returning so that a follow-up `evaluateAndSignal` check can detect and remove it, or signal completion directly.</violation>
</file>

<file name="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java">

<violation number="1" location="runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java:85">
P1: Busy-loop when `TickDispatcher` is null. If `sleepUntilNextTick` returns immediately (dispatcher not yet initialized), this loop spins with zero delay, pegging the CPU at 100% for the entire timeout. Add a fallback `sleep` when tick-based waiting is unavailable.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java (1)

41-43: Consider adding a shutdown method for completeness.

The subscriber handles are stored but never used for unregistration. While this is fine for an application-lifetime singleton, adding a shutdown() method that calls eventBus.unregister(gameTickSubscriber) etc. would make the class more testable and complete.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java`
around lines 41 - 43, Add a shutdown method to TickDispatcher that unregisters
the retained subscribers: implement public void shutdown() {
eventBus.unregister(gameTickSubscriber);
eventBus.unregister(gameStateChangedSubscriber); } so tests and shutdown logic
can call TickDispatcher.shutdown() to cleanly remove the subscribers stored in
gameTickSubscriber and gameStateChangedSubscriber; ensure the method is public
and idempotent (safe to call multiple times).
runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java (1)

222-224: Consider documenting the timeout formula rationale.

The default timeout ticks * 800L + 2000 assumes ~800ms worst-case per tick plus a 2-second buffer. This is reasonable for handling server lag, but a brief inline comment would help future maintainers understand the magic numbers.

📝 Suggested documentation
 public static boolean sleepForTicks(int ticks) {
+    // 800ms accounts for worst-case tick timing under server lag; +2000ms buffer for safety
     return sleepForTicks(ticks, ticks * 800L + 2000);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java`
around lines 222 - 224, Add a short inline comment to the sleepForTicks(int)
method explaining the timeout formula used in the default argument (ticks * 800L
+ 2000), e.g., note that 800L represents a conservative ~800ms worst‑case per
game tick and the +2000 is a 2s safety buffer to handle server lag; place the
comment adjacent to the return statement or method signature in the
sleepForTicks method to make the "magic numbers" self‑explaining.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java`:
- Around line 222-224: Add a short inline comment to the sleepForTicks(int)
method explaining the timeout formula used in the default argument (ticks * 800L
+ 2000), e.g., note that 800L represents a conservative ~800ms worst‑case per
game tick and the +2000 is a 2s safety buffer to handle server lag; place the
comment adjacent to the return statement or method signature in the
sleepForTicks method to make the "magic numbers" self‑explaining.

In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java`:
- Around line 41-43: Add a shutdown method to TickDispatcher that unregisters
the retained subscribers: implement public void shutdown() {
eventBus.unregister(gameTickSubscriber);
eventBus.unregister(gameStateChangedSubscriber); } so tests and shutdown logic
can call TickDispatcher.shutdown() to cleanly remove the subscribers stored in
gameTickSubscriber and gameStateChangedSubscriber; ensure the method is public
and idempotent (safe to call multiple times).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b129eab-43b0-41b4-83fa-0161c4fb68c9

📥 Commits

Reviewing files that changed from the base of the PR and between d8067b5 and 16746f4.

📒 Files selected for processing (3)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickDispatcher.java
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tick/TickWaiter.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant