aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-16 14:09:22 -0400
committerhistoria <[not public]>2026-09-16 14:09:22 -0400
commit2f34e75f27e5f99cfc1c5f8eaf6ab4ff2fddb1d9 (patch)
treec50388fe639286bda1b1999ca6c2c972d03eb3a5 /internal/world
parentc054b27f628f23fecc505b9e09e78721738f3c4e (diff)
downloadthehouseoficarus-2f34e75f27e5f99cfc1c5f8eaf6ab4ff2fddb1d9.tar.gz
fix: reserved-loot pickup, credit pickup, payout recovery, and slot stake/bonus shutdown handling
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/ground.go44
-rw-r--r--internal/world/ground_test.go50
2 files changed, 93 insertions, 1 deletions
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.
diff --git a/internal/world/ground_test.go b/internal/world/ground_test.go
new file mode 100644
index 0000000..ef77766
--- /dev/null
+++ b/internal/world/ground_test.go
@@ -0,0 +1,50 @@
+package world
+
+import "testing"
+
+// TestRemoveReservedItemSkipsOtherPlayersReservations: the take loop must
+// apply the same ownership check as the availability pass. With a stack
+// reserved for Bob stored before a public stack, Alice's pickup must take
+// the public stack only — consuming Bob's reservation instead silently
+// destroys his loot while telling Alice the ground still holds it.
+func TestRemoveReservedItemSkipsOtherPlayersReservations(t *testing.T) {
+ w := New(t.TempDir())
+ w.AddReservedItem(1, "ore", 10, "bob")
+ w.AddGroundItem(1, "ore", 10)
+
+ taken, ok := w.RemoveReservedItem(1, "ore", 10, "alice")
+ if !ok || taken != 10 {
+ t.Fatalf("alice's pickup of the public stack = (%d, %v), want (10, true)", taken, ok)
+ }
+ if got := w.AccessibleGround(1, "alice")["ore"]; got != 0 {
+ t.Errorf("alice still sees %d ore after taking the public stack; removal consumed reserved stock", got)
+ }
+ // Bob's reserved stack must be untouched and still his to take.
+ if got := w.AccessibleGround(1, "bob")["ore"]; got != 10 {
+ t.Errorf("bob's reserved stack = %d, want 10", got)
+ }
+ bobTaken, ok := w.RemoveReservedItem(1, "ore", 10, "bob")
+ if !ok || bobTaken != 10 {
+ t.Errorf("bob's pickup of his own reservation = (%d, %v), want (10, true)", bobTaken, ok)
+ }
+}
+
+// TestRemoveAvailableItemTakesAccessibleStock: a request far larger than
+// the ground stock succeeds and takes exactly what is accessible, skipping
+// entries reserved for other players.
+func TestRemoveAvailableItemTakesAccessibleStock(t *testing.T) {
+ w := New(t.TempDir())
+ w.AddReservedItem(1, "credits", 10, "bob")
+ w.AddGroundItem(1, "credits", 5)
+
+ taken := w.RemoveAvailableItem(1, "credits", 999999, "alice")
+ if taken != 5 {
+ t.Fatalf("remove-available took %d, want the 5 accessible credits", taken)
+ }
+ if got := w.AccessibleGround(1, "alice")["credits"]; got != 0 {
+ t.Errorf("alice still sees %d accessible credits", got)
+ }
+ if got := w.AccessibleGround(1, "bob")["credits"]; got != 10 {
+ t.Errorf("bob's reserved credits = %d, want 10", got)
+ }
+}