Skip to content

fix: prevent spawner storage item duplication with stale GUI views - #341

Closed
libramine wants to merge 1 commit into
OpenVdra:mainfrom
libramine:fix/storage-dupe-atomic-debit
Closed

libramine wants to merge 1 commit into
OpenVdra:mainfrom
libramine:fix/storage-dupe-atomic-debit

Conversation

@libramine

Copy link
Copy Markdown

Summary

Fixes two item duplication exploits in the spawner storage GUI where two players viewing the same spawner (for example, two accounts synced with LAN Sync) can each take the same items.

Repro 1 — take-all: open the same Spawner Storage GUI on two accounts, turn LAN sync delay off, click take-all twice on each account, then turn sync delay back on. The items appear on both accounts.

Repro 2 — shift-click: same setup, shift-click the same stack out of the spawner storage on each account. The items appear on both accounts.

Root cause

SpawnerStorageAction moved items into the player's inventory before debiting the virtual inventory, and ignored the transactional result of the debit:

  • transferToPlayerInventory (left / right / shift click) computed the amount from the GUI slot, wrote it into the player inventory, then called removeItemsAndUpdateSellValue(...) and only used the result to decide whether to repaint.
  • transferItems (take-all) did the same and discarded the boolean result entirely.
  • handleDropPageItems removed from the virtual inventory but dropped the items regardless of the result.

The storage GUI is only a projection of VirtualInventory, so a second viewer's copy — or a repaint that has been scheduled but not yet run — can be stale. Both handlers therefore granted items that had already been debited by another viewer.

Fix

Debit first, grant second:

  • transferToPlayerInventory and transferItems now compute how much the player can receive, call removeItemsAndUpdateSellValue first, and only add to the player inventory the amount that was actually removed. A failed removal grants nothing and repaints the stale view.
  • handleDropPageItems now aborts the drop if the debit fails.
  • Added calculatePlayerCapacity / addToPlayerInventory helpers so the amount granted is always exactly the amount debited.
  • When take-all debits nothing (stale view or full inventory), the acting viewer is repainted to clear phantom slots.

This makes VirtualInventory.removeItems' existing transactional check the single gate for granting items, so stale or duplicate GUI views can no longer mint items.

Testing

  • ./gradlew build succeeds.
  • The repository has no unit tests; verified manually in-game with two accounts against the repro steps above (items are no longer duplicated).

Notes

  • Only SpawnerStorageAction.java changed.
  • No new config keys, commands, permissions or public API changes.

Storage items were granted to the player before being debited from the virtual inventory, and the transactional result of the debit was ignored. A stale GUI projection (a second viewer, or a repaint that had not run yet) could hand out the same items twice via take-all or shift-click.

Debit the virtual inventory first and only grant what was actually removed in transferToPlayerInventory, transferItems and handleDropPageItems. The count-map's existing transactional removeItems is now the gate, so stale views cannot mint items.
@n1ght3r

n1ght3r commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

@xthedev1 1.8.2 already covers the duplication issue. Further changes may potentially introduce performance issues or create other duplication exploits. For now, I think it's safer to keep the current implementation. Thanks

@libramine

Copy link
Copy Markdown
Author

@xthedev1 1.8.2 already covers the duplication issue. Further changes may potentially introduce performance issues or create other duplication exploits. For now, I think it's safer to keep the current implementation. Thanks

hm alright thanks!

@libramine libramine closed this Sep 19, 2026
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