aboutsummaryrefslogtreecommitdiff
path: root/internal/world/ground.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-16 02:56:56 -0400
committerhistoria <[not public]>2026-09-16 02:56:56 -0400
commitf2d44f10f52d3fe68fdb19b518a330f7af9e1ed2 (patch)
tree9db54e8d4fd2f9bd30c651700f7cf246eef1b017 /internal/world/ground.go
parentad60843fbf577eda80105e3916822d663aaefcb3 (diff)
downloadthehouseoficarus-f2d44f10f52d3fe68fdb19b518a330f7af9e1ed2.tar.gz
fix: disconnect race, rename ownership, reserved pickup, economy recovery retry/refund semantics
Diffstat (limited to 'internal/world/ground.go')
-rw-r--r--internal/world/ground.go59
1 files changed, 51 insertions, 8 deletions
diff --git a/internal/world/ground.go b/internal/world/ground.go
index 879cc71..2d5ea7b 100644
--- a/internal/world/ground.go
+++ b/internal/world/ground.go
@@ -87,23 +87,47 @@ func (w *World) AddGroundItem(roomID int, itemID string, qty int) {
w.groundItems[roomID] = append(w.groundItems[roomID], e)
}
+// RemoveReservedItem removes up to qty of itemID from the room's ground,
+// refusing entries reserved for another player. It is ALL-OR-NOTHING:
+// when any part of the quantity is unobtainable (a reserved-for-others
+// entry, or too little accessible stock) nothing is taken and (0, false)
+// is returned. Callers must never lose already-removed items to a failed
+// pickup, so the removal is verified before any state is touched.
func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner string) (int, bool) {
w.mu.Lock()
defer w.mu.Unlock()
- removed := 0
- remaining := qty
- for _, e := range w.groundItems[roomID] {
- if remaining <= 0 {
+ entries := w.groundItems[roomID]
+
+ // Pass 1: verify the full quantity is takable by this owner without
+ // mutating anything, so a blocked pickup can never leave the ground
+ // half-emptied with nothing delivered.
+ available := 0
+ for _, e := range entries {
+ if available >= qty {
break
}
- if !strings.EqualFold(e.itemID, itemID) {
+ if e.quantity <= 0 || !strings.EqualFold(e.itemID, itemID) {
continue
}
- if e.quantity <= 0 {
+ if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner {
continue
}
- if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner {
- return removed, false
+ available += e.quantity
+ }
+ if available < qty {
+ return 0, false
+ }
+
+ // Pass 2: take. Every entry reachable here is takeable, so no
+ // mid-loop abort is possible.
+ removed := 0
+ remaining := qty
+ for _, e := range entries {
+ if remaining <= 0 {
+ break
+ }
+ if e.quantity <= 0 || !strings.EqualFold(e.itemID, itemID) {
+ continue
}
take := e.quantity
if take > remaining {
@@ -119,6 +143,25 @@ func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner str
return removed, true
}
+// 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.
+func (w *World) AccessibleGround(roomID int, owner string) map[string]int {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ out := make(map[string]int)
+ for _, e := range w.groundItems[roomID] {
+ if e.quantity <= 0 {
+ continue
+ }
+ if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner {
+ continue
+ }
+ out[e.itemID] += e.quantity
+ }
+ return out
+}
+
func (w *World) RemoveGroundItem(roomID int, itemID string, qty int) int {
w.mu.Lock()
defer w.mu.Unlock()