local respawnDistance = CreateConVar( "gc_respawndistance", "75", FCVAR_ARCHIVE, "Distance a map item may move from its spawn before it is returned" ) local respawnDelay = CreateConVar( "gc_itemrespawntime", "1", FCVAR_ARCHIVE, "Seconds before a collected or displaced map item respawns" ) local trackedItems = {} local suppressRespawns = false -- These use the item_ prefix but are map machinery or containers rather than -- touch pickups. Ammo crates are already infinite; the others may be removed -- deliberately by Hammer I/O and must not be recreated as loose supplies. local persistentItemClasses = { item_ammo_crate = true, item_dynamic_resupply = true, item_healthcharger = true, item_item_crate = true, item_suitcharger = true } local function IsRespawnable(ent) if not IsValid(ent) then return false end local className = ent:GetClass() if persistentItemClasses[className] then return false end return string.StartWith(className, "item_") or string.StartWith(className, "weapon_") end local function CaptureTemplate(ent) local bodygroups = {} for _, bodygroup in ipairs(ent:GetBodyGroups() or {}) do bodygroups[bodygroup.id] = ent:GetBodygroup(bodygroup.id) end return { className = ent:GetClass(), position = ent:GetPos(), angles = ent:GetAngles(), model = ent:GetModel(), skin = ent:GetSkin(), material = ent:GetMaterial(), color = ent:GetColor(), collisionGroup = ent:GetCollisionGroup(), name = ent:GetName(), keyValues = ent:GetKeyValues(), bodygroups = bodygroups } end local TrackItem local RespawnItem local function ScheduleRespawn(template) if suppressRespawns then return end timer.Simple(math.max(respawnDelay:GetFloat(), 0), function() if suppressRespawns then return end RespawnItem(template) end) end TrackItem = function(ent, template) if not IsValid(ent) or trackedItems[ent] then return end template = template or CaptureTemplate(ent) trackedItems[ent] = template ent:CallOnRemove("CustomCoopRespawnItem", function(removed) local savedTemplate = trackedItems[removed] trackedItems[removed] = nil if savedTemplate then ScheduleRespawn(savedTemplate) end end) end RespawnItem = function(template) local ent = ents.Create(template.className) if not IsValid(ent) then ErrorNoHalt("[custom_coop] Could not respawn " .. template.className .. "; retrying in 5 seconds\n") timer.Simple(5, function() if not suppressRespawns then RespawnItem(template) end end) return end -- Restore mapper-provided properties before Spawn. Position, angles and model -- are restored explicitly because GetKeyValues does not always include them. for key, value in pairs(template.keyValues or {}) do if key ~= "classname" and key ~= "origin" and key ~= "angles" and key ~= "hammerid" then ent:SetKeyValue(key, tostring(value)) end end ent:SetPos(template.position + Vector(0, 0, 3)) ent:SetAngles(template.angles) if template.model and template.model ~= "" then ent:SetModel(template.model) end if template.name and template.name ~= "" then ent:SetName(template.name) end ent:Spawn() ent:Activate() if not IsValid(ent) then ScheduleRespawn(template) return end ent:SetSkin(template.skin or 0) ent:SetMaterial(template.material or "") ent:SetColor(template.color or color_white) ent:SetCollisionGroup(template.collisionGroup or COLLISION_GROUP_NONE) for id, value in pairs(template.bodygroups or {}) do ent:SetBodygroup(id, value) end TrackItem(ent, template) ent:EmitSound("items/suitchargeok1.wav", 60, 100, 0.35) end hook.Add("OnEntityCreated", "CustomCoopTrackMapItems", function(ent) -- OnEntityCreated fires before keyvalues and map-created state are ready. timer.Simple(0, function() if IsRespawnable(ent) and ent:CreatedByMap() then TrackItem(ent) end end) end) hook.Add("WeaponEquip", "CustomCoopRespawnCollectedWeapon", function(weapon) local template = trackedItems[weapon] if not template then return end -- A newly acquired weapon becomes the player's inventory entity and is not -- removed. Detach it and immediately start a new respawn cycle for its slot. trackedItems[weapon] = nil weapon:RemoveCallOnRemove("CustomCoopRespawnItem") ScheduleRespawn(template) end) -- Prevent +use from dragging a map pickup away. Touch pickup still works, and -- duplicate weapons are left to the engine's EquipAmmo path so they correctly -- replenish reserve ammo and disappear only when ammo was actually accepted. hook.Add("AllowPlayerPickup", "CustomCoopProtectMapItems", function(_, ent) if trackedItems[ent] then return false end end) timer.Create("CustomCoopReturnDisplacedItems", 1, 0, function() local maxDistance = math.max(respawnDistance:GetFloat(), 0) local maxDistanceSquared = maxDistance * maxDistance for ent, template in pairs(trackedItems) do if not IsValid(ent) then trackedItems[ent] = nil elseif not IsValid(ent:GetParent()) and ent:GetPos():DistToSqr(template.position) > maxDistanceSquared then SafeRemoveEntityDelayed(ent, 0) end end end) hook.Add("PlayerSpawn", "CustomCoopKeepWeaponsOnDeath", function(ply) timer.Simple(0, function() if IsValid(ply) then ply:ShouldDropWeapon(false) end end) end) hook.Add("PreCleanupMap", "CustomCoopPauseItemRespawns", function() suppressRespawns = true trackedItems = {} end) hook.Add("PostCleanupMap", "CustomCoopResumeItemRespawns", function() suppressRespawns = false end) hook.Add("ShutDown", "CustomCoopStopItemRespawns", function() suppressRespawns = true end)