From 2f34e75f27e5f99cfc1c5f8eaf6ab4ff2fddb1d9 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 16 Sep 2026 14:09:22 -0400 Subject: fix: reserved-loot pickup, credit pickup, payout recovery, and slot stake/bonus shutdown handling --- internal/world/ground.go | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) (limited to 'internal/world/ground.go') diff --git a/internal/world/ground.go b/internal/world/ground.go index 2d5ea7b..6915072 100644 --- a/internal/world/ground.go +++ b/internal/world/ground.go @@ -119,7 +119,10 @@ func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner str } // Pass 2: take. Every entry reachable here is takeable, so no - // mid-loop abort is possible. + // mid-loop abort is possible. The ownership check MUST match pass 1: + // without it a player could consume a stack reserved for someone else + // when a public stack and a reserved stack coexist (the reserved entry + // comes first in the slice, so the take loop would drain it first). removed := 0 remaining := qty for _, e := range entries { @@ -129,6 +132,9 @@ func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner str if e.quantity <= 0 || !strings.EqualFold(e.itemID, itemID) { continue } + if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner { + continue + } take := e.quantity if take > remaining { take = remaining @@ -143,6 +149,42 @@ func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner str return removed, true } +// RemoveAvailableItem removes up to maxQty of itemID from the room's +// ground, skipping entries reserved for another player, and returns the +// quantity actually removed. Unlike RemoveReservedItem it is not +// all-or-nothing: callers pass a generous upper bound (credit pickups use +// a sentinel far above any possible stock) and take whatever is accessible +// at the moment, so a request larger than the available stock still +// succeeds. +func (w *World) RemoveAvailableItem(roomID int, itemID string, maxQty int, owner string) int { + w.mu.Lock() + defer w.mu.Unlock() + removed := 0 + remaining := maxQty + for _, e := range w.groundItems[roomID] { + if remaining <= 0 { + break + } + if e.quantity <= 0 || !strings.EqualFold(e.itemID, itemID) { + continue + } + if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner { + continue + } + take := e.quantity + if take > remaining { + take = remaining + } + e.quantity -= take + removed += take + remaining -= take + if e.isSpawn && e.quantity <= 0 && e.respawnDelay > 0 { + e.respawnTimer = e.respawnDelay + } + } + return removed +} + // AccessibleGround returns the ground items in roomID this owner may take: // entries reserved for another player are excluded. Pickup flows use this // to size requests and match names against takeable stock only. -- cgit v1.2.3