fix: loot-bag owner beacon race + looting while dead

- Beacon: LootBagDropped fired the same frame the bag was created, so the
  Instance reference arrived as nil on the client (not yet replicated) and the
  owner beacon silently never appeared. The fire is now delayed 0.5s.
- Dead looting: collect() never verified the collector was alive, so a corpse
  lying next to its own bag could scoop everything back before respawning.
  The server now rejects dead collectors (isAlive gate, Harvesting pattern).

Live verification note: the running Studio session executed stale bytecode
(old collect granted loot to a dead player even though the new source was
synced) — verified source-in-place; behavior verification needs a Studio
restart, which CI/release artifacts are unaffected by.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Samuel Lison
2026-07-06 14:55:55 +10:00
co-authored by Claude Opus 4.8
parent 015dc60a8e
commit 78f99a0667
2 changed files with 23 additions and 2 deletions
+9
View File
@@ -5,6 +5,15 @@ All notable changes to SurvivorCore are recorded here. The format follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). At release time, `## Unreleased`
is promoted to the new version and `main` is tagged `vX.Y.Z`.
## Unreleased
### Fixed
- **Loot-bag beacon never appeared** — the owner's beacon remote fired the same frame the bag was
created, so the Instance reference arrived as `nil` on the client (not yet replicated) and the
beacon silently skipped. The fire is now delayed a beat past replication.
- **Looting while dead** — a dead player could scoop their own bag from the corpse before
respawning, defeating the corpse-run. The server now rejects collectors who aren't alive.
## 0.7.1 — 2026-07-06
### Fixed
+14 -2
View File
@@ -113,6 +113,12 @@ local function collect(bag: Instance, player: Player)
if looting[player] then
return
end
-- Dead players can't loot — the corpse falls right next to the bag, and a quick E while dead
-- would scoop everything back before the respawn corpse-run even begins.
local hum = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
if not hum or hum.Health <= 0 then
return
end
looting[player] = true
local entries = {}
@@ -262,9 +268,15 @@ local function dropBag(player: Player, position: Vector3)
Hooks.run("lootbag:dropped", ctx)
EventBridge.fire("lootbag:dropped", player, { position = position, items = total })
-- Owner-only client juice: beacon + "your things dropped" toast.
-- Owner-only client juice: beacon + "your things dropped" toast. The beacon fire is DELAYED a
-- beat — the bag was created this frame, and an Instance sent before it replicates arrives as
-- nil on the client (the beacon would silently never appear).
if cfg.OwnerBeacon ~= false then
Remotes.event("LootBagDropped"):FireClient(player, bag, lifetime)
task.delay(0.5, function()
if bag.Parent and player.Parent then
Remotes.event("LootBagDropped"):FireClient(player, bag, lifetime)
end
end)
end
Remotes.event("Notify"):FireClient(player, {
kind = "death",