Skip to content

fix(MageTrainingArena): guard buyReward against unloaded reward shop widget (1.1.9) - #640

Merged
chsami merged 1 commit into
developmentfrom
claude/err-mta-buyreward-null-reward-widget
Oct 15, 2026
Merged

chsami merged 1 commit into
developmentfrom
claude/err-mta-buyreward-null-reward-widget

Conversation

@chsami

@chsami chsami commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Problem

Client error telemetry, group 1271: 1 event from 1 reporter (Microbot 2.6.30, MageTrainingArenaPlugin 1.1.8, first and last seen 2026-10-12). It was thrown on thread MageTrainingArenaScript-6:

java.lang.NullPointerException: Cannot invoke "net.runelite.api.widgets.Widget.getDynamicChildren()" because ... is null
  at MageTrainingArenaScript.lambda$buyReward$33:587
  at Rs2Inventory.lambda$waitForInventoryChanges$72:2082
  at Global.sleepUntilTrue:231
  at Rs2Inventory.waitForInventoryChanges:2081
  at MageTrainingArenaScript.buyReward:586

Root cause

buyReward checks that the reward shop (197:0) is visible. Then, inside the waitForInventoryChanges action, it calls Rs2Widget.getWidget(197, 11).getDynamicChildren() without checking for null. If the interface closes, or child 11 is not built yet when the action runs, getWidget returns null and the call throws an NPE. When the reward item was not in the list, the old code also passed a null widget and index -1 to clickWidgetFast.

Change

  • Fetch the 197:11 container once, and return from the action if it is null.
  • Find the reward with a new package-private helper, findRewardIndex(Widget[], int). It returns -1 when the array is null or the item is missing, and it skips null entries. If the index is below 0, the action returns before clicking the item or the confirm button (197:9). waitForInventoryChanges keeps polling until its timeout, as before.
  • Bump MageTrainingArenaPlugin from 1.1.8 to 1.1.9.

Validation

  • New MageTrainingArenaScriptTest with 3 Mockito cases for findRewardIndex: a null array, a missing reward, and a match after a null entry.
  • ./gradlew test -PpluginList=MageTrainingArenaPlugin --tests '*MageTrainingArenaScriptTest' --tests '*PluginManifestVersionTest': BUILD SUCCESSFUL (3/3 and 1/1 passed).
  • ./gradlew build -PpluginList=MageTrainingArenaPlugin -x test: BUILD SUCCESSFUL.

Known gaps

  • No test exercises the null-container branch, the one that actually crashed, because Rs2Widget is static. That guard is a single line.
  • This is not new: bought++ still runs when waitForInventoryChanges times out without buying anything, and the new early return makes that path a little more common. It only affects a counter; the stop condition checks Rs2Inventory.contains.
  • MixologyScript has the same unguarded getWidget(...).getDynamicChildren() pattern on widget 882:2. That is out of scope here because it has no telemetry.
  • Not tested live in-game.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV

…widget (1.1.9)

Rs2Widget.getWidget(197, 11) returns null when the reward shop child is not
built yet or the shop has just closed, which threw an NPE inside
waitForInventoryChanges. Return early when the container is null and skip the
click and confirm when the reward item is not in the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV
@chsami
chsami merged commit f600bb2 into development Oct 15, 2026
1 check passed
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