diff --git a/gamemode/weaponsystem.lua b/gamemode/weaponsystem.lua index c412b34..09fc764 100644 --- a/gamemode/weaponsystem.lua +++ b/gamemode/weaponsystem.lua @@ -15,6 +15,30 @@ local respawnDelay = CreateConVar( local trackedItems = {} local suppressRespawns = false +-- Sandbox defaults this to 9999, overriding every Source ammo type's intended +-- carry limit. Coop uses the original per-ammo limits instead. +local maxAmmoOverride = GetConVar("gmod_maxammo") +if maxAmmoOverride and maxAmmoOverride:GetInt() > 0 then + RunConsoleCommand("gmod_maxammo", "0") +end + +local function ClampPlayerAmmo(ply) + for ammoType = 1, 255 do + local maximum = game.GetAmmoMax(ammoType) + if maximum and maximum >= 0 and ply:GetAmmoCount(ammoType) > maximum then + ply:SetAmmo(maximum, ammoType) + end + end +end + +-- Also repairs reserves accumulated while the sandbox-wide 9999 override was +-- active. Unknown and explicitly infinite ammo types are left unchanged. +timer.Simple(0, function() + for _, ply in ipairs(player.GetAll()) do + if IsValid(ply) then ClampPlayerAmmo(ply) end + end +end) + -- 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. @@ -42,16 +66,18 @@ local itemAmmoTypes = { item_rpg_round = "RPG_Round" } +-- These weapons store their ammunition directly on the player rather than in +-- a clip, so Clip1() cannot tell us how much a duplicate pickup is worth. +local cliplessWeaponAmmo = { + weapon_frag = 1, + weapon_rpg = 1, + weapon_slam = 1 +} + local function CanReceiveAmmo(ply, ammoType) if ammoType == nil or ammoType < 0 then return false end - local maximum - local maxAmmoOverride = GetConVar("gmod_maxammo") - if maxAmmoOverride and maxAmmoOverride:GetInt() > 0 then - maximum = maxAmmoOverride:GetInt() - else - maximum = game.GetAmmoMax(ammoType) - end + local maximum = game.GetAmmoMax(ammoType) -- Unknown/custom ammo types are left to their own pickup implementation. if maximum == nil or maximum < 0 then return true end @@ -74,6 +100,8 @@ local function CaptureTemplate(ent) bodygroups[bodygroup.id] = ent:GetBodygroup(bodygroup.id) end + local isWeapon = ent:IsWeapon() + return { className = ent:GetClass(), position = ent:GetPos(), @@ -85,7 +113,9 @@ local function CaptureTemplate(ent) collisionGroup = ent:GetCollisionGroup(), name = ent:GetName(), keyValues = ent:GetKeyValues(), - bodygroups = bodygroups + bodygroups = bodygroups, + clip1 = isWeapon and ent:Clip1() or nil, + clip2 = isWeapon and ent:Clip2() or nil } end @@ -104,8 +134,11 @@ end TrackItem = function(ent, template) if not IsValid(ent) or trackedItems[ent] then return end - template = template or CaptureTemplate(ent) + template = template or ent.CustomCoopRespawnTemplate or CaptureTemplate(ent) trackedItems[ent] = template + -- Lua entity fields survive script auto-refresh. Keeping the template on + -- the entity lets a fresh copy of this file recover its original spawn. + ent.CustomCoopRespawnTemplate = template ent:CallOnRemove("CustomCoopRespawnItem", function(removed) local savedTemplate = trackedItems[removed] @@ -147,6 +180,11 @@ RespawnItem = function(template) return end + -- Spawn normally fills these from the weapon script. Restore the captured + -- values as well so mapper-specified pickup quantities survive recreation. + if template.clip1 ~= nil then ent:SetClip1(template.clip1) end + if template.clip2 ~= nil then ent:SetClip2(template.clip2) end + ent:SetSkin(template.skin or 0) ent:SetMaterial(template.material or "") ent:SetColor(template.color or color_white) @@ -168,6 +206,19 @@ hook.Add("OnEntityCreated", "CustomCoopTrackMapItems", function(ent) end) end) +local function TrackExistingItems() + for _, ent in ipairs(ents.GetAll()) do + if IsRespawnable(ent) and (ent:CreatedByMap() or ent.CustomCoopRespawnTemplate) then + TrackItem(ent) + end + end +end + +-- InitPostEntity handles a normal map load; the zero-delay scan handles GMod's +-- script auto-refresh, where existing entities do not fire OnEntityCreated. +hook.Add("InitPostEntity", "CustomCoopTrackExistingMapItems", TrackExistingItems) +timer.Simple(0, TrackExistingItems) + hook.Add("WeaponEquip", "CustomCoopRespawnCollectedWeapon", function(weapon) local template = trackedItems[weapon] if not template then return end @@ -179,17 +230,46 @@ hook.Add("WeaponEquip", "CustomCoopRespawnCollectedWeapon", function(weapon) ScheduleRespawn(template) end) --- A duplicate weapon is removed through WEAPON:EquipAmmo even when the player --- cannot accept more ammo. Refusing only the useless attempt leaves the pickup --- available and silent until this player spends ammunition (or someone else --- who needs it touches it). +-- Duplicate HL2 weapons do not transfer ammo consistently in GMod. Handle +-- tracked map weapons here so useful ammo is transferred exactly once, while +-- a full player leaves the pickup available and silent for someone else. hook.Add("PlayerCanPickupWeapon", "CustomCoopIgnoreFullDuplicateWeapons", function(ply, weapon) - if not trackedItems[weapon] or not ply:HasWeapon(weapon:GetClass()) then return end + if not ply:HasWeapon(weapon:GetClass()) then return end - if not CanReceiveAmmo(ply, weapon:GetPrimaryAmmoType()) - and not CanReceiveAmmo(ply, weapon:GetSecondaryAmmoType()) then - return false + local primaryAmmoType = weapon:GetPrimaryAmmoType() + local secondaryAmmoType = weapon:GetSecondaryAmmoType() + + -- Some map logic creates weapons dynamically, so they are not part of the + -- map-entity tracker. A second copy of an ammo-less weapon is still always + -- useless and should never produce another pickup event. + if primaryAmmoType < 0 and secondaryAmmoType < 0 then return false end + + if not trackedItems[weapon] then return end + + local primaryAmount = math.max(weapon:Clip1(), 0) + if primaryAmount == 0 then + primaryAmount = cliplessWeaponAmmo[weapon:GetClass()] or 0 end + + local given = 0 + if primaryAmmoType >= 0 and primaryAmount > 0 then + given = given + (ply:GiveAmmo(primaryAmount, primaryAmmoType) or 0) + end + + local secondaryAmount = math.max(weapon:Clip2(), 0) + if secondaryAmmoType >= 0 and secondaryAmount > 0 then + given = given + (ply:GiveAmmo(secondaryAmount, secondaryAmmoType) or 0) + end + + -- Handle the duplicate ourselves. The native path is inconsistent for + -- HL2 weapons under GMod: it may drain or hide the entity without giving + -- usable reserve ammo. Removing only after GiveAmmo succeeds also makes a + -- full pickup silent and leaves it available to another player. + if given > 0 then + SafeRemoveEntityDelayed(weapon, 0) + end + + return false end) -- Stock ammo items normally reject a full player themselves. Checking here as