mirror of
https://github.com/TemujinCalidius/SurvivorCore.git
synced 2026-08-14 09:02:29 +00:00
Merge pull request #73 from TemujinCalidius/fix/lootbag-streaming
fix: loot bag + beacon vanished on respawn (StreamingEnabled)
This commit is contained in:
@@ -5,6 +5,16 @@ 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 vanished on respawn (StreamingEnabled places)** — respawning away from the
|
||||
death spot let Roblox stream the bag out of the owner's client: the bag looked deleted and the
|
||||
local `Destroying` fired, killing the beacon. The bag is now a **stream-persistent Model for its
|
||||
owner** (`AddPersistentPlayer`), and the beacon is **position-driven plain data** — no Instance
|
||||
reference, so no replication race (the 0.5s delay is gone) and no streaming fragility. The beam
|
||||
clears on an explicit server signal when the bag is truly emptied/expired.
|
||||
|
||||
## 0.7.2 — 2026-07-06
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
--[[
|
||||
LootBagBeacon — client. Owner-only "your stuff is over there" juice (TCE port).
|
||||
|
||||
When the server drops YOUR loot bag it fires `LootBagDropped` at you; this raises a tall golden
|
||||
beam + a soft ground ring + a point light over the bag — visible only to you (built locally).
|
||||
Everything cleans itself up when the bag despawns or empties. Booted by startClient().
|
||||
The server fires `LootBagDropped` at the owner with a POSITION + lifetime (plain data — immune
|
||||
to instance-replication races and streaming eviction); this raises a tall golden beam + ground
|
||||
ring + light there, visible only to this player. It clears when the server signals the bag is
|
||||
truly gone (a nil fire: looted empty or expired) or when the lifetime lapses — NOT when the
|
||||
bag's replica streams out, which is what used to kill the beacon on respawn. Booted by
|
||||
startClient().
|
||||
]]
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
@@ -19,18 +22,20 @@ local started = false
|
||||
|
||||
local GOLD = Color3.fromRGB(255, 220, 80)
|
||||
|
||||
local function bagHost(bag: Instance): BasePart?
|
||||
if bag:IsA("BasePart") then
|
||||
return bag
|
||||
local currentBeam: BasePart? = nil
|
||||
local currentToken = 0 -- invalidates stale lifetime timers when a new bag drops
|
||||
|
||||
local function clearBeacon()
|
||||
currentToken += 1
|
||||
if currentBeam and currentBeam.Parent then
|
||||
currentBeam:Destroy()
|
||||
end
|
||||
return bag:FindFirstChildWhichIsA("BasePart")
|
||||
currentBeam = nil
|
||||
end
|
||||
|
||||
local function buildBeacon(bag: Instance)
|
||||
local host = bagHost(bag)
|
||||
if not host then
|
||||
return
|
||||
end
|
||||
local function buildBeacon(position: Vector3, lifetime: number)
|
||||
clearBeacon()
|
||||
local token = currentToken
|
||||
|
||||
local beam = Instance.new("Part")
|
||||
beam.Name = "_LootBeacon"
|
||||
@@ -41,7 +46,7 @@ local function buildBeacon(bag: Instance)
|
||||
beam.Color = GOLD
|
||||
beam.Transparency = 0.35
|
||||
beam.Size = Vector3.new(0.8, 120, 0.8)
|
||||
beam.CFrame = CFrame.new(host.Position + Vector3.new(0, 60, 0))
|
||||
beam.CFrame = CFrame.new(position + Vector3.new(0, 60, 0))
|
||||
|
||||
local ring = Instance.new("Part")
|
||||
ring.Name = "_LootBeaconRing"
|
||||
@@ -53,21 +58,22 @@ local function buildBeacon(bag: Instance)
|
||||
ring.Color = GOLD
|
||||
ring.Transparency = 0.55
|
||||
ring.Size = Vector3.new(0.2, 5, 5)
|
||||
ring.CFrame = CFrame.new(host.Position + Vector3.new(0, 0.2, 0)) * CFrame.Angles(0, 0, math.rad(90))
|
||||
ring.CFrame = CFrame.new(position + Vector3.new(0, 0.2, 0)) * CFrame.Angles(0, 0, math.rad(90))
|
||||
ring.Parent = beam
|
||||
|
||||
local light = Instance.new("PointLight")
|
||||
light.Color = GOLD
|
||||
light.Brightness = 2.5
|
||||
light.Range = 18
|
||||
light.Parent = host
|
||||
light.Parent = beam
|
||||
|
||||
beam.Parent = workspace
|
||||
currentBeam = beam
|
||||
|
||||
-- Clean up when the bag goes away (looted empty or despawned).
|
||||
bag.Destroying:Connect(function()
|
||||
if beam.Parent then
|
||||
beam:Destroy()
|
||||
-- Fallback cleanup at lifetime (the server's nil-fire normally clears it first).
|
||||
task.delay(math.max(1, lifetime), function()
|
||||
if token == currentToken and currentBeam == beam then
|
||||
clearBeacon()
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -78,9 +84,11 @@ function LootBagBeacon.start(_options: { [string]: any }?)
|
||||
end
|
||||
started = true
|
||||
|
||||
Remotes.event("LootBagDropped").OnClientEvent:Connect(function(bag, _lifetime)
|
||||
if typeof(bag) == "Instance" then
|
||||
buildBeacon(bag)
|
||||
Remotes.event("LootBagDropped").OnClientEvent:Connect(function(position, lifetime)
|
||||
if typeof(position) == "Vector3" then
|
||||
buildBeacon(position, tonumber(lifetime) or 300)
|
||||
else
|
||||
clearBeacon() -- nil = the bag is truly gone (looted empty or expired)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -198,9 +198,27 @@ local function dropBag(player: Player, position: Vector3)
|
||||
return -- died empty-handed; no bag
|
||||
end
|
||||
|
||||
-- The bag is a MODEL so it can be made stream-persistent for its owner: with the default
|
||||
-- StreamingEnabled place, respawning far from the death spot would otherwise evict the bag
|
||||
-- from the owner's client — it looked deleted (and killed the beacon) even though the server
|
||||
-- still had it.
|
||||
local tmpl = bagTemplate()
|
||||
local bag: Instance = if tmpl then tmpl:Clone() else buildPlaceholderBag()
|
||||
local inner: Instance = if tmpl then tmpl:Clone() else buildPlaceholderBag()
|
||||
local bag: Model
|
||||
if inner:IsA("Model") then
|
||||
bag = inner
|
||||
else
|
||||
bag = Instance.new("Model")
|
||||
inner.Parent = bag
|
||||
if inner:IsA("BasePart") then
|
||||
bag.PrimaryPart = inner
|
||||
end
|
||||
end
|
||||
bag.Name = "LootBag"
|
||||
pcall(function()
|
||||
bag.ModelStreamingMode = Enum.ModelStreamingMode.PersistentPerPlayer
|
||||
bag:AddPersistentPlayer(player)
|
||||
end)
|
||||
|
||||
-- Aggregate contents as IntValue children (item id → qty), equipment included.
|
||||
local counts: { [string]: number } = {}
|
||||
@@ -268,13 +286,18 @@ 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. 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).
|
||||
-- Owner-only client juice: beacon + "your things dropped" toast. The beacon payload is PLAIN
|
||||
-- DATA (position + lifetime) — no Instance reference, so there is no replication race and no
|
||||
-- streaming fragility; the client builds the beam at the position and keeps it until the bag
|
||||
-- is truly gone (the nil-fire below) or the lifetime lapses.
|
||||
local host2 = bagHost(bag)
|
||||
local beaconPos = if host2 then host2.Position else position
|
||||
if cfg.OwnerBeacon ~= false then
|
||||
task.delay(0.5, function()
|
||||
if bag.Parent and player.Parent then
|
||||
Remotes.event("LootBagDropped"):FireClient(player, bag, lifetime)
|
||||
Remotes.event("LootBagDropped"):FireClient(player, beaconPos, lifetime)
|
||||
bag.Destroying:Connect(function()
|
||||
-- Truly gone server-side (emptied or expired) → clear the owner's beacon early.
|
||||
if player.Parent then
|
||||
Remotes.event("LootBagDropped"):FireClient(player, nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user