aboutsummaryrefslogtreecommitdiff
path: root/internal/world/ground.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 17:48:26 -0400
committerhistoria <[not public]>2026-09-18 17:48:26 -0400
commit8fe9e3c3fef0049df82281a62afe35dda516dfe2 (patch)
treedd269f2c0710f334e19f00a191260ebc46284a90 /internal/world/ground.go
parent194164a77eb4f2479b94c19e6f0e1278ab608781 (diff)
downloadthehouseoficarus-8fe9e3c3fef0049df82281a62afe35dda516dfe2.tar.gz
fix: close combat ghost, admin traversal, login throttle, journal double-pay, and quality/economy gaps
Diffstat (limited to 'internal/world/ground.go')
-rw-r--r--internal/world/ground.go87
1 files changed, 83 insertions, 4 deletions
diff --git a/internal/world/ground.go b/internal/world/ground.go
index 6915072..9cfc40c 100644
--- a/internal/world/ground.go
+++ b/internal/world/ground.go
@@ -10,6 +10,8 @@ const ReserveTicks = 100
type GroundItemInfo struct {
ItemID string
Quantity int
+ Quality int
+ MaxQuality int
ReservedFor string
ReserveTimer int
DespawnTimer int
@@ -19,6 +21,8 @@ type GroundItemInfo struct {
type groundEntry struct {
itemID string
quantity int
+ quality int
+ maxQuality int
isSpawn bool
respawnTimer int // >0 = counting down to respawn
respawnQty int
@@ -39,6 +43,8 @@ func (w *World) GroundItemsDetailed(roomID int) []GroundItemInfo {
info := GroundItemInfo{
ItemID: e.itemID,
Quantity: e.quantity,
+ Quality: e.quality,
+ MaxQuality: e.maxQuality,
DespawnTimer: e.despawnTimer,
IsSpawn: e.isSpawn,
}
@@ -64,11 +70,19 @@ func (w *World) GroundItems(roomID int) map[string]int {
}
func (w *World) AddReservedItem(roomID int, itemID string, qty int, owner string) {
+ w.AddReservedItemQ(roomID, itemID, qty, owner, 0, 0)
+}
+
+// AddReservedItemQ is AddReservedItem with the item's quality preserved (a
+// non-stackable max_quality item dropped on death/trigger keeps its charge).
+func (w *World) AddReservedItemQ(roomID int, itemID string, qty int, owner string, quality, maxQuality int) {
w.mu.Lock()
defer w.mu.Unlock()
e := &groundEntry{
itemID: itemID,
quantity: qty,
+ quality: quality,
+ maxQuality: maxQuality,
despawnTimer: DropDespawnTicks,
reservedFor: owner,
reserveTimer: ReserveTicks,
@@ -77,11 +91,19 @@ func (w *World) AddReservedItem(roomID int, itemID string, qty int, owner string
}
func (w *World) AddGroundItem(roomID int, itemID string, qty int) {
+ w.AddGroundItemQ(roomID, itemID, qty, 0, 0)
+}
+
+// AddGroundItemQ is AddGroundItem with the item's quality preserved. A
+// dropped partially-used max_quality item must not come back full on pickup.
+func (w *World) AddGroundItemQ(roomID int, itemID string, qty, quality, maxQuality int) {
w.mu.Lock()
defer w.mu.Unlock()
e := &groundEntry{
itemID: itemID,
quantity: qty,
+ quality: quality,
+ maxQuality: maxQuality,
despawnTimer: DropDespawnTicks,
}
w.groundItems[roomID] = append(w.groundItems[roomID], e)
@@ -94,6 +116,18 @@ func (w *World) AddGroundItem(roomID int, itemID string, qty int) {
// 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) {
+ removed, _, _, ok := w.removeReservedItem(roomID, itemID, qty, owner)
+ return removed, ok
+}
+
+// RemoveReservedItemQ is RemoveReservedItem that also reports the quality of
+// the entry the pickup actually came from, so a partially-used non-stackable
+// item keeps its charge through a drop/pickup round trip.
+func (w *World) RemoveReservedItemQ(roomID int, itemID string, qty int, owner string) (removed, quality, maxQuality int, ok bool) {
+ return w.removeReservedItem(roomID, itemID, qty, owner)
+}
+
+func (w *World) removeReservedItem(roomID int, itemID string, qty int, owner string) (removed, quality, maxQuality int, ok bool) {
w.mu.Lock()
defer w.mu.Unlock()
entries := w.groundItems[roomID]
@@ -115,7 +149,7 @@ func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner str
available += e.quantity
}
if available < qty {
- return 0, false
+ return 0, 0, 0, false
}
// Pass 2: take. Every entry reachable here is takeable, so no
@@ -123,8 +157,9 @@ func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner str
// 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
+ tookQuality, tookMaxQuality := 0, 0
+ tookFromEntry := false
for _, e := range entries {
if remaining <= 0 {
break
@@ -139,6 +174,10 @@ func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner str
if take > remaining {
take = remaining
}
+ if !tookFromEntry {
+ tookQuality, tookMaxQuality = e.quality, e.maxQuality
+ tookFromEntry = true
+ }
e.quantity -= take
removed += take
remaining -= take
@@ -146,7 +185,25 @@ func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner str
e.respawnTimer = e.respawnDelay
}
}
- return removed, true
+ return removed, tookQuality, tookMaxQuality, true
+}
+
+// AccessibleGroundQuality reports the quality of the first ground entry this
+// owner may take, so a pickup can stamp the item it is about to create. The
+// boolean is false when no accessible entry exists.
+func (w *World) AccessibleGroundQuality(roomID int, itemID, owner string) (quality, maxQuality int, ok bool) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ for _, e := range w.groundItems[roomID] {
+ if e.quantity <= 0 || !strings.EqualFold(e.itemID, itemID) {
+ continue
+ }
+ if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner {
+ continue
+ }
+ return e.quality, e.maxQuality, true
+ }
+ return 0, 0, false
}
// RemoveAvailableItem removes up to maxQty of itemID from the room's
@@ -221,6 +278,11 @@ func (w *World) RemoveGroundItem(roomID int, itemID string, qty int) int {
if e.quantity <= 0 {
continue
}
+ // This helper has no owner, so it must not consume another player's
+ // reserved entry (its siblings are reservation-aware).
+ if e.reserveTimer > 0 && e.reservedFor != "" {
+ continue
+ }
take := e.quantity
if take > remaining {
take = remaining
@@ -237,7 +299,7 @@ func (w *World) RemoveGroundItem(roomID int, itemID string, qty int) int {
}
func (w *World) tickGroundItems() {
- for _, entries := range w.groundItems {
+ for roomID, entries := range w.groundItems {
for _, e := range entries {
if e.respawnTimer > 0 {
e.respawnTimer--
@@ -259,12 +321,29 @@ func (w *World) tickGroundItems() {
}
}
}
+ // Prune dead entries: without this every drop/loot/firemaking action
+ // appends an entry that lives for the process lifetime and is scanned
+ // on every tick and pickup. A spawn entry awaiting respawn keeps its
+ // slot; everything else with no quantity is gone for good.
+ kept := entries[:0]
+ for _, e := range entries {
+ if e.quantity <= 0 && e.respawnTimer <= 0 {
+ continue
+ }
+ kept = append(kept, e)
+ }
+ if len(kept) == 0 {
+ delete(w.groundItems, roomID)
+ } else {
+ w.groundItems[roomID] = kept
+ }
}
}
func (w *World) mergeNearbyItems(entries []*groundEntry, target *groundEntry) {
for _, other := range entries {
if other != target && other.quantity > 0 && strings.EqualFold(other.itemID, target.itemID) &&
+ other.quality == target.quality && other.maxQuality == target.maxQuality &&
(other.reserveTimer <= 0 || other.reservedFor == "") &&
other.despawnTimer <= 0 {
target.quantity += other.quantity