Skip to content

Commit e330aaf

Browse files
committed
feat: guard stale reward owner location
1 parent 7c98335 commit e330aaf

3 files changed

Lines changed: 32 additions & 2 deletions

File tree

internal/minimal/shared_world.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,7 @@ func (r *sharedWorldRegistry) registerGroundItem(ownerID uint64, ownerLogin stri
939939
defer r.mu.Unlock()
940940

941941
registeredOwner, ok := r.playerCharacter(ownerID)
942-
if !ok || characterAtBootstrapHPFloor(registeredOwner) || characterAtBootstrapHPFloor(character) {
942+
if !ok || characterAtBootstrapHPFloor(registeredOwner) || characterAtBootstrapHPFloor(character) || !sameGroundRewardOwnerLocation(registeredOwner, character) {
943943
return false
944944
}
945945
if _, exists := r.groundItemsByVID[vid]; exists {
@@ -968,6 +968,10 @@ func (r *sharedWorldRegistry) registerGroundItem(ownerID uint64, ownerLogin stri
968968
return true
969969
}
970970

971+
func sameGroundRewardOwnerLocation(registered loginticket.Character, supplied loginticket.Character) bool {
972+
return registered.MapIndex == supplied.MapIndex && registered.X == supplied.X && registered.Y == supplied.Y && registered.Z == supplied.Z
973+
}
974+
971975
func (r *sharedWorldRegistry) groundItemVisibleToCharacterLocked(ground sharedGroundItem, character loginticket.Character) bool {
972976
return r.topology.SharesVisibleWorld(character, loginticket.Character{MapIndex: ground.MapIndex, X: ground.X, Y: ground.Y})
973977
}

internal/minimal/shared_world_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,31 @@ func TestSharedWorldRegistryRegisterGroundRewardsRejectsStaleLiveOwnerSnapshotAf
459459
}
460460
}
461461

462+
func TestSharedWorldRegistryRegisterGroundRewardsRejectsStaleOwnerLocationSnapshotAfterOwnerMoves(t *testing.T) {
463+
registry := newSharedWorldRegistry()
464+
owner := peerVisibilityCharacter("StaleLocationRewardOwner", 0x01030198, 0x02040198, 1200, 2200, 0, 101, 201)
465+
ownerID, _ := registry.Join(owner, newPendingServerFrames(), nil)
466+
if ownerID == 0 {
467+
t.Fatal("expected reward owner join to allocate a shared-world entity id")
468+
}
469+
movedOwner := owner
470+
movedOwner.MapIndex = 42
471+
movedOwner.X = 5000
472+
movedOwner.Y = 6000
473+
movedOwner.Z = 7
474+
registry.UpdateCharacter(ownerID, movedOwner)
475+
476+
if registry.RegisterGroundItem(ownerID, "stale-location-reward-owner", owner, 0x07000019, inventory.ItemInstance{Vnum: 3001, Count: 1}) {
477+
t.Fatal("expected stale owner location ground-item registration to fail closed after registered owner moves")
478+
}
479+
if registry.RegisterGroundGold(ownerID, "stale-location-reward-owner", owner, 0x0700001A, 250) {
480+
t.Fatal("expected stale owner location ground-gold registration to fail closed after registered owner moves")
481+
}
482+
if registry.GroundItemExists(0x07000019) || registry.GroundItemExists(0x0700001A) {
483+
t.Fatal("expected rejected stale-location reward ground entries to stay absent")
484+
}
485+
}
486+
462487
func TestSharedWorldRegistryRegisterGroundGoldRejectsExistingVID(t *testing.T) {
463488
registry := newSharedWorldRegistry()
464489
owner := peerVisibilityCharacter("GoldDropOwner", 0x01030194, 0x02040194, 1200, 2200, 0, 101, 201)

spec/protocol/non-player-reward-bootstrap.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,10 @@ Transfer/rebootstrap destination visibility uses the same recipient gate for bot
119119
Ordinary session position updates that rebuild destination visibility use the same zero-HP gate, so a dead recipient cannot reacquire ground-entry visibility by arriving through a movement/sync-style shared-world update path instead of the structured transfer path.
120120
Fresh same-socket rebootstrap uses the same guard before building the selected character's trailing ground-entry burst, so a still-dead session cannot reacquire `ITEM_GROUND_ADD` / `ITEM_OWNERSHIP` visibility frames for pending ground rewards merely by re-entering the map while still at the bootstrap `0`-HP floor.
121121
Ground pickup also skips party-style delivery back into an owner that has since reached the bootstrap `0` HP floor: the ground handle keeps its owner metadata for display/ownership, but the live owner snapshot is withheld so a living collector either takes the item through the ordinary collector path or fails closed on collector capacity without mutating the dead owner's inventory/gold.
122+
Ground reward registration re-checks the registered owner snapshot before trusting the caller-supplied character copy, so a stale owner snapshot cannot create item-shaped or gold-shaped reward ground entries after the registered owner has already moved to a different runtime location.
122123
Lookup, pickup-resolution, and removal also re-check the registered collector snapshot before trusting the caller-supplied character copy, so a stale collector snapshot cannot see, resolve, or remove reward ground entries after that collector has already reached the bootstrap `0`-HP floor or moved out of the current ground-item visibility/reachability boundary.
123124
When that living and currently reachable collector succeeds, the collector receives the ordinary self pickup shape (`ITEM_GROUND_DEL`, inventory `ITEM_SET`, and normal `ITEM_GET` feedback) and the dead owner receives no queued party-style pickup feedback.
124-
This keeps death/restart cleanup from leaking new pickup surfaces, stale delete noise, debug/runtime-visible pickup affordances, stale collector pickup affordances, transfer-entry ground visibility, or late owner-delivery mutations for players that are already dead.
125+
This keeps death/restart cleanup and concurrent movement from leaking new pickup surfaces, stale delete noise, debug/runtime-visible pickup affordances, stale collector pickup affordances, stale owner-location ground registration, transfer-entry ground visibility, or late owner-delivery mutations for players that are already dead or no longer at the supplied reward location.
125126

126127
Current rules:
127128
- each configured drop spawns at the killer's current position

0 commit comments

Comments
 (0)