Skip to content

Fix: Add bounds check for slotTimers to prevent silent trade recording failure#75

Open
Reggie-Reuss wants to merge 2 commits intoFlipping-Utilities:masterfrom
Reggie-Reuss:fix/slot-timer-bounds-check
Open

Fix: Add bounds check for slotTimers to prevent silent trade recording failure#75
Reggie-Reuss wants to merge 2 commits intoFlipping-Utilities:masterfrom
Reggie-Reuss:fix/slot-timer-bounds-check

Conversation

@Reggie-Reuss
Copy link
Copy Markdown

@Reggie-Reuss Reggie-Reuss commented Mar 14, 2026

Summary

  • hydrateSlotTimers() only checks for null, but Gson deserializes an empty JSON array [] as a non-null empty ArrayList — bypassing initialization entirely
  • The three .get(slot) calls in screenOfferEvent() then throw IndexOutOfBoundsException on every GE offer event, silently dropping all real-time trades
  • GE History Tab imports (slot -1) are unaffected since they bypass screenOfferEvent(), which masks the failure — the plugin appears functional but records nothing
  • Added bounds checks in setWidgetsOnSlotTimers() for the same pattern

How I found this

I discovered this while investigating why only certain items were saving in my trade history. After my data recovery from a RuneLite crash that corrupted both my main JSON and backup JSON simultaneously, the recovered file had "slotTimers": []. This caused 16 days of silent trade recording failure before I traced it to IndexOutOfBoundsException errors in client.log.

Full root cause analysis: Phase 6: slotTimers Bug

Is this likely in normal use?

In a typical crash, probably not — the plugin falls back to the backup file which usually has valid 8-entry slotTimers. But it can happen in rare edge cases:

  • Both main + backup files corrupted simultaneously (my case — documented here)
  • File truncated at exactly the slotTimers boundary (theoretically possible for medium-sized account files in the 8-16KB range)
  • Manual JSON editing or third-party tools that produce "slotTimers": []
  • Schema changes between plugin versions

The fix is a one-line change to the root cause (null check → null || size() != 8) plus two defensive guards at the .get() call sites.

Changes

File Change
AccountData.hydrateSlotTimers Reinitialize if size() != 8, not just null
NewOfferEventPipelineHandler.screenOfferEvent Early return + log.warn if slotTimers undersized
FlippingPlugin.setWidgetsOnSlotTimers Skip loop if list undersized

Note

Java is not my primary language, so this would benefit from review by someone more experienced with the codebase — similar to my other PR (#74). Happy to adjust based on feedback.

Summary by CodeRabbit

  • Bug Fixes
    • Added validation to timer data initialization to re-create timers when missing or the count is incorrect, ensuring consistent timer state.
    • Guarded periodic timer updates to skip ticks when timer data is invalid, preventing runtime errors and improving stability.
    • Added warnings and early exits when timer lists are null or undersized to avoid unsafe operations.

If slotTimers is deserialized as an empty list rather than null (e.g.
from a recovered or manually constructed JSON file), hydrateSlotTimers()
skips initialization because it only checks for null. The three
unguarded .get(slot) calls in screenOfferEvent() then throw
IndexOutOfBoundsException on every GE offer event, silently dropping
all real-time trades with no user-visible error.

- AccountData.hydrateSlotTimers: reinitialize if size != 8, not just null
- NewOfferEventPipelineHandler.screenOfferEvent: early return with log
  warning if slotActivityTimers is missing or undersized
- FlippingPlugin.setWidgetsOnSlotTimers: skip loop if list undersized
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9099183b-4af3-400e-a729-825ffbcff41b

📥 Commits

Reviewing files that changed from the base of the PR and between 67b9ea7 and 9327b9f.

📒 Files selected for processing (1)
  • src/main/java/com/flippingutilities/controller/FlippingPlugin.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/flippingutilities/controller/FlippingPlugin.java

📝 Walkthrough

Walkthrough

Added defensive checks and stricter initialization for slot timers: methods now verify the presence and exact count (8) of slot timers before operating, logging and early-returning or reinitializing when the condition is not met.

Changes

Cohort / File(s) Summary
Plugin timer runtime
src/main/java/com/flippingutilities/controller/FlippingPlugin.java
Added early-return when accountData.getSlotTimers().size() < 8 in UI binding (setWidgetsOnSlotTimers) and refactored startSlotTimers() to capture, validate, log, and skip ticks for invalid timer lists before scheduling client-thread updates.
Offer pipeline guard
src/main/java/com/flippingutilities/controller/NewOfferEventPipelineHandler.java
screenOfferEvent now checks slotActivityTimers for null or size < 8, logs a warning (including null vs undersized), and returns Optional.empty() to avoid out-of-bounds assumptions.
AccountData initialization
src/main/java/com/flippingutilities/model/AccountData.java
hydrateSlotTimers(FlippingPlugin) now reinitializes slotTimers when its size is not exactly 8; preserves and refreshes client/plugin references when size == 8.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through code at dawn's first light,
Eight tiny timers held up tight,
I checked for gaps and filled each space,
Logged a warning, saved the race,
Now offers screen with steadier sight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding bounds checks to prevent silent trade recording failures due to undersized slotTimers.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@AntonyGarand
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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 (1)
src/main/java/com/flippingutilities/controller/FlippingPlugin.java (1)

1004-1015: Consider adding a similar guard for consistency.

The startSlotTimers method accesses getSlotTimers() without validating size. While forEach won't throw on an undersized list, this could lead to silent partial updates if timers are missing. For consistency with the other guards, consider adding a null/size check, or at minimum a log warning.

♻️ Optional: Add defensive check
 private ScheduledFuture startSlotTimers() {
-    return executor.scheduleAtFixedRate(() ->
-            dataHandler.viewAccountData(currentlyLoggedInAccount).getSlotTimers().forEach(slotWidgetTimer ->
+    return executor.scheduleAtFixedRate(() -> {
+            List<SlotActivityTimer> slotTimers = dataHandler.viewAccountData(currentlyLoggedInAccount).getSlotTimers();
+            if (slotTimers == null || slotTimers.size() < 8) {
+                log.warn("slotTimers missing or undersized in startSlotTimers, skipping update");
+                return;
+            }
+            slotTimers.forEach(slotWidgetTimer ->
                     clientThread.invokeLater(() -> {
                         try {
                             slotsPanel.updateTimerDisplays(slotWidgetTimer.getSlotIndex(), slotWidgetTimer.createFormattedTimeString());
                             slotWidgetTimer.updateTimerDisplay();
                         } catch (Exception e) {
                             log.error("exception when trying to update timer", e);
                         }
-                    })), 1000, 1000, TimeUnit.MILLISECONDS);
+                    }));
+        }, 1000, 1000, TimeUnit.MILLISECONDS);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/flippingutilities/controller/FlippingPlugin.java` around
lines 1004 - 1015, The startSlotTimers lambda calls
dataHandler.viewAccountData(currentlyLoggedInAccount).getSlotTimers() and then
blindly iterates/update slotsPanel.updateTimerDisplays and
slotWidgetTimer.updateTimerDisplay; add a defensive check to validate the
returned slot timers collection (null check and a size/emptiness check or
expected-size check) before the forEach, and log a warning (including
currentlyLoggedInAccount) and skip the update if validation fails so you avoid
silent partial updates and keep behavior consistent with other guards.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/com/flippingutilities/controller/FlippingPlugin.java`:
- Around line 1004-1015: The startSlotTimers lambda calls
dataHandler.viewAccountData(currentlyLoggedInAccount).getSlotTimers() and then
blindly iterates/update slotsPanel.updateTimerDisplays and
slotWidgetTimer.updateTimerDisplay; add a defensive check to validate the
returned slot timers collection (null check and a size/emptiness check or
expected-size check) before the forEach, and log a warning (including
currentlyLoggedInAccount) and skip the update if validation fails so you avoid
silent partial updates and keep behavior consistent with other guards.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c2fe29d7-611d-4bfc-9dd9-5ca5ea70217c

📥 Commits

Reviewing files that changed from the base of the PR and between ac7d1a3 and 67b9ea7.

📒 Files selected for processing (3)
  • src/main/java/com/flippingutilities/controller/FlippingPlugin.java
  • src/main/java/com/flippingutilities/controller/NewOfferEventPipelineHandler.java
  • src/main/java/com/flippingutilities/model/AccountData.java

forEach on an empty list will not throw, but if slotTimers is
undersized the timer updates will silently do nothing. Guard added
for consistency with the other three call sites and to surface the
condition via log warning rather than silent no-op.
@Reggie-Reuss
Copy link
Copy Markdown
Author

Reggie-Reuss commented Mar 26, 2026

Good catch from CodeRabbit on startSlotTimers — added the same null/size guard there for consistency. forEach on an empty list won't crash, but it would silently do nothing with no indication of why, so the warning log is useful to have. Pushed in a follow-up commit 9327b9f.

Also acknowledged the docstring coverage warning — that appears to be a pre-existing repo-wide condition rather than something introduced by this PR.

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.

2 participants