From 5d8267e31654de2ba341ba7947fc95960df90269 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:57:10 +0300 Subject: [PATCH] Refactor damage detector --- lua/entities/gmod_wire_damage_detector.lua | 422 +++++++++------------ 1 file changed, 186 insertions(+), 236 deletions(-) diff --git a/lua/entities/gmod_wire_damage_detector.lua b/lua/entities/gmod_wire_damage_detector.lua index e5baba3665..fe6aa082d5 100644 --- a/lua/entities/gmod_wire_damage_detector.lua +++ b/lua/entities/gmod_wire_damage_detector.lua @@ -1,234 +1,131 @@ AddCSLuaFile() -DEFINE_BASECLASS( "base_wire_entity" ) -ENT.PrintName = "Wire Damage Detector" -ENT.Author = "Jimlad" -ENT.WireDebugName = "Damage Detector" -if CLIENT then return end -- No more client +DEFINE_BASECLASS("base_wire_entity") +ENT.PrintName = "Wire Damage Detector" +ENT.WireDebugName = "Damage Detector" -local newE2Table = WireLib.E2Table.New +if CLIENT then return end --- Global table to keep track of all detectors local Wire_Damage_Detectors = {} --- Damage detection function -local function CheckWireDamageDetectors( ent, inflictor, attacker, amount, dmginfo ) - if amount > 0 then - for k,_ in pairs(Wire_Damage_Detectors) do - local detector = k - if IsValid(detector) then - if detector.on then - if not detector.updated then - detector:UpdateLinkedEnts() - detector.updated = true - detector:NextThink(CurTime()) -- Update link info once per tick per detector at most - end - if detector.key_ents[ent] then - detector:UpdateDamage( dmginfo, ent ) - end - end - else - Wire_Damage_Detectors[k] = nil +hook.Add("PostEntityTakeDamage", "CheckWireDamageDetectors", function(ent, dmg) + local curtime = CurTime() + + for _, detector in ipairs(Wire_Damage_Detectors) do + local tab = detector:GetTable() + + if tab.on then + if not tab.updated then + tab.updated = true + tab.UpdateLinkedEnts(detector, tab) + detector:NextThink(curtime) + end + + if tab.key_ents[ent] then + tab.UpdateDamage(detector, tab, dmg, ent) end end end -end -hook.Add("EntityTakeDamage", "CheckWireDamageDetectors", function( ent, dmginfo ) - if not next(Wire_Damage_Detectors) then return end - CheckWireDamageDetectors( ent, dmginfo:GetInflictor(), dmginfo:GetAttacker(), dmginfo:GetDamage(), dmginfo ) end) - function ENT:Initialize() - self:PhysicsInit( SOLID_VPHYSICS ) - self:SetMoveType( MOVETYPE_VPHYSICS ) - self:SetSolid( SOLID_VPHYSICS ) - - self.Outputs = WireLib.CreateSpecialOutputs(self, { "Clk", "Damage", "Attacker", "Victim", "Victims", "Position", "Force", "Type" } , { "NORMAL", "NORMAL", "ENTITY", "ENTITY", "TABLE", "VECTOR", "VECTOR", "STRING" } ) - self.Inputs = WireLib.CreateInputs(self, { - "On", - "Entity (This entity will be added whenever this input changes to a valid entity) [ENTITY]", - "Entities (These entities will be added whenever this input changes.\nCan be changed at most once per second.) [ARRAY]", - "Reset" - }) - - self.on = false - self.updated = false -- Tracks whether constraints were updated that tick - self.hit = false -- Tracks whether detector registered any damage that tick + table.insert(Wire_Damage_Detectors, self) + self:PhysicsInit(SOLID_VPHYSICS) - self.firsthit_dmginfo = {} -- Stores damage info representing damage during an interval + WireLib.CreateOutputs(self, { "Clk", "Damage", "Attacker [ENTITY]", "Victim [ENTITY]", "Victims [TABLE]", "Position [VECTOR]", "Force [VECTOR]", "Type [STRING]" }) + WireLib.CreateInputs(self, { "On", "Entity (This entity will be added whenever this input changes to a valid entity) [ENTITY]", "Entities (These entities will be added whenever this input changes.\nCan be changed at most once per second.) [ARRAY]", "Reset" }) - self.linked_entities = {} -- numerical array - self.linked_entities_lookup = {} -- lookup table indexed by entities - - self:LinkEnt( self ) + self.on = false + self.updated = false + self.hit = false + self.firsthit_dmginfo = {} + self.linked_entities = {} + self.linked_entities_lookup = {} + self:LinkEnt(self) + self.damage = 0 self.count = 0 self.key_ents = {} -- Store output damage info - self.victims = newE2Table() - WireLib.TriggerOutput( self, "Victims", self.victims ) - self.damage = 0 - - Wire_Damage_Detectors[self] = true -end - ---[[***************************** - How entities are stored: - self.linked_entities_lookup: Table: Lookup table used for linking and unlinking - self.linked_entities Array: Entities the detector is directly linked to. - self.key_ents KeyTable: Entities to check damage on (including constrained ents), only updated when the damage hook is called,.Contains entities as table keys -*****************************]] - -function ENT:OnRemove() - Wire_Damage_Detectors[self] = nil - Wire_Remove(self) - self:ClearEntities() -end - --- Update overlay -function ENT:ShowOutput() - local num = #self.linked_entities - self:SetOverlayText(string.format("(%s)\nLinked to %s entities%s", - (self.includeconstrained == 0) and "Individual Props" or "Constrained Props", - num, - (num == 1 and self.linked_entities[1] == self) and "\nLinked only to self" or "" - )) + self.victims = {} + WireLib.TriggerOutput(self, "Victims", self.victims) end -function ENT:Setup( includeconstrained ) +function ENT:Setup(includeconstrained) self.includeconstrained = includeconstrained - self:ShowOutput() -end - -function ENT:LinkEnt( ent, dontupdateoutput ) - if not ent or not ent:IsValid() then return end - - if self.linked_entities_lookup[ent] then return false end - - self.linked_entities_lookup[ent] = true - self.linked_entities[#self.linked_entities+1] = ent - ent:CallOnRemove( "DDetector.Unlink", function( ent ) - if IsValid( self ) then - self:UnlinkEnt( ent ) - end - end ) - - if not dontupdateoutput then self:ShowOutput() end - WireLib.SendMarks( self, self.linked_entities ) - return true -end - -function ENT:UnlinkEnt( ent ) - if not self.linked_entities_lookup[ent] then return false end - - self.linked_entities_lookup[ent] = nil - - for i=1,#self.linked_entities do - if self.linked_entities[i] == ent then - table.remove( self.linked_entities, i ) - break - end - end - - ent:RemoveCallOnRemove( "DDetector.Unlink" ) - - self:ShowOutput() - WireLib.SendMarks( self, self.linked_entities ) - return true -end - -function ENT:ClearEntities() - for i=1, #self.linked_entities do - if IsValid( self.linked_entities[i] ) then -- generally, all entities should be kept valid automatically by the CallOnRemove functions, but CallOnRemove isn't called for players apparently - self.linked_entities[i]:RemoveCallOnRemove( "DDetector.Unlink" ) - end - end - - self.linked_entities = {} - self.linked_entities_lookup = {} - - self:ShowOutput() - WireLib.SendMarks( self, self.linked_entities ) - return true end -function ENT:TriggerInput( iname, value ) - if iname == "On" then +function ENT:TriggerInput(name, value) + if name == "On" then self.on = value ~= 0 - elseif iname == "Entities" then -- Populate linked_entities from "Array" + elseif name == "Entities" then if value then - if self.arrayInputNextChange and self.arrayInputNextChange > CurTime() then return end self:ClearEntities() - for _, v in pairs( value ) do - if IsValid( v ) then - self:LinkEnt( v, true ) + for _, ent in pairs(value) do + if IsValid(ent) then + self:LinkEnt(ent, true) end end - self.arrayInputNextChange = CurTime() + 1 - self:ShowOutput() end - elseif iname == "Entity" then - if IsValid( value )then - self:LinkEnt( value ) + elseif name == "Entity" then + if IsValid(value) then + self:LinkEnt(value) end - elseif iname == "Reset" then + elseif name == "Reset" then if value then - self.count = 0 - self.firsthit_dmginfo = {} - self.victims = newE2Table() - self.damage = 0 - self:TriggerOutput() + local tab = self:GetTable() + tab.count = 0 + tab.firsthit_dmginfo = {} + tab.victims = {} + tab.damage = 0 + tab.TriggerOutput(self, tab) end end end -function ENT:TriggerOutput() -- Entity outputs won't trigger again until they change - local attacker = self.firsthit_dmginfo[1] - WireLib.TriggerOutput( self, "Attacker", IsValid(attacker) and attacker or NULL) +function ENT:TriggerOutput(tab) + local attacker = tab.firsthit_dmginfo[1] + WireLib.TriggerOutput(self, "Attacker", IsValid(attacker) or NULL) - local victim = self.firsthit_dmginfo[2] - WireLib.TriggerOutput( self, "Victim", IsValid(victim) and victim or NULL) + local victim = tab.firsthit_dmginfo[2] + WireLib.TriggerOutput(self, "Victim", IsValid(victim) or NULL) - self.victims.size = table.Count(self.victims.s) - WireLib.TriggerOutput( self, "Victims", self.victims or newE2Table() ) - WireLib.TriggerOutput( self, "Position", self.firsthit_dmginfo[3] or Vector(0,0,0) ) - WireLib.TriggerOutput( self, "Force", self.firsthit_dmginfo[4] or Vector(0,0,0) ) - WireLib.TriggerOutput( self, "Type", self.firsthit_dmginfo[5] or "" ) - WireLib.TriggerOutput( self, "Damage", self.damage or 0 ) - - WireLib.TriggerOutput( self, "Clk", self.count ) + WireLib.TriggerOutput(self, "Victims", tab.victims) + WireLib.TriggerOutput(self, "Position", tab.firsthit_dmginfo[3] or Vector(0, 0, 0)) + WireLib.TriggerOutput(self, "Force", tab.firsthit_dmginfo[4] or Vector(0, 0, 0)) + WireLib.TriggerOutput(self, "Type", tab.firsthit_dmginfo[5] or "") + WireLib.TriggerOutput(self, "Damage", tab.damage) + WireLib.TriggerOutput(self, "Clk", tab.count) end -function ENT:UpdateLinkedEnts() -- Check to see if prop is registered by the detector - if #self.linked_entities == 0 then return end - - self.key_ents = {} +function ENT:UpdateLinkedEnts(tab) + local linked = tab.linked_entities + if #linked == 0 then return end - for i=1, #self.linked_entities do -- include linked_entities - local ent = self.linked_entities[i] - if IsValid( ent ) then - if self.includeconstrained == 1 then -- Don't update constrained entities unless we have to - self:UpdateConstrainedEnts( ent ) - end + local key_ents = {} + local include_constrained = tab.includeconstrained == 1 + tab.key_ents = key_ents - self.key_ents[ent] = true - else - self.linked_entities[ent] = nil + for _, ent in ipairs(linked) do + if include_constrained then + tab.UpdateConstrainedEnts(self, tab, ent) end + + key_ents[ent] = true end end -function ENT:UpdateConstrainedEnts( ent ) -- Finds all entities constrained to 'ent' - local ents = constraint.GetAllConstrainedEntities( ent ) +function ENT:UpdateConstrainedEnts(tab, ent) + local key_ents = tab.key_ents - for _,v in pairs( ents ) do - self.key_ents[v] = true + for ent in pairs(constraint.GetAllConstrainedEntities(ent)) do + key_ents[ent] = true end end -local damageTypes = { + +local dmg_types = { [DMG_GENERIC] = "Generic", [DMG_CRUSH] = "Crush", [DMG_BULLET] = "Bullet", @@ -242,7 +139,7 @@ local damageTypes = { [DMG_SONIC] = "Sonic", [DMG_ENERGYBEAM] = "Laser", [DMG_DROWN] = "Drown", - [DMG_PARALYZE] = "Poison", + [DMG_PARALYZE] = "Paralyze", [DMG_POISON] = "Poison", [DMG_NERVEGAS] = "Neurotoxin", [DMG_RADIATION] = "Radiation", @@ -253,73 +150,119 @@ local damageTypes = { [DMG_ENERGYBEAM] = "Laser", [DMG_DIRECT] = "Direct" } -function ENT:UpdateDamage( dmginfo, ent ) -- Update damage table - local damage = dmginfo:GetDamage() - - local victims = self.victims - if not self.hit then -- Only register the first target's damage info - self.firsthit_dmginfo = { - dmginfo:GetAttacker(), - ent, - dmginfo:GetDamagePosition(), - dmginfo:GetDamageForce() - } +function ENT:UpdateDamage(tab, dmg, ent) + local damage = dmg:GetDamage() + local victims = tab.victims - -- Damage type (handle almost all types) - self.dmgtype = damageTypes[dmginfo:GetDamageType()] or "Other" + if not tab.hit then + tab.dmgtype = dmg_types[dmg:GetDamageType()] or "Other" + tab.firsthit_dmginfo = { dmg:GetAttacker(), ent, dmg:GetDamagePosition(), dmg:GetDamageForce(), tab.dmgtype } - - victims = newE2Table() - self.victims = victims - self.firsthit_dmginfo[5] = self.dmgtype - - self.hit = true + victims = {} + tab.victims = victims + tab.hit = true end - if self.dmgtype == "Explosive" then -- Explosives will output the entity that receives the most damage - if self.damage < damage then - self.damage = damage - self.firsthit_dmginfo[2] = ent + if tab.dmgtype == "Explosive" then + if tab.damage < damage then + tab.damage = damage + tab.firsthit_dmginfo[2] = ent end else - self.damage = self.damage + damage + tab.damage = tab.damage + damage end - -- Update victims table (ent, damage) - local entID = tostring(ent:EntIndex()) - local old_dmg = select(2, victims:Get(entID)) or 0 - victims:Set(entID, old_dmg + damage) + local id = tostring(ent:EntIndex()) + victims[id] = damage + (victims[id] or 0) - self.count = self.count + 1 - if self.count == math.huge then self.count = 0 end -- This shouldn't ever happen... unless you're really REALLY bored + tab.count = tab.count + 1 end function ENT:Think() - self.updated = false - self.hit = false - if self.damage > 0 then - self:TriggerOutput() - self.damage = 0 + local tab = self:GetTable() + tab.updated = false + tab.hit = false + + if tab.damage > 0 then + tab.TriggerOutput(self, tab) + tab.damage = 0 end + return true end -duplicator.RegisterEntityClass("gmod_wire_damage_detector", WireLib.MakeWireEnt, "Data", "includeconstrained") +function ENT:UpdateOutputs() + WireLib.SendMarks(self, self.linked_entities) +end + +function ENT:CheckEnt(checkent) + for index, ent in ipairs(self.linked_entities) do + if checkent == ent then + return true, index + end + end + + return false, 0 +end + +function ENT:LinkEnt(ent) + if self:CheckEnt(ent) then return false end + + table.insert(self.linked_entities, ent) + + ent:CallOnRemove("DamageDetector.Unlink" .. self:EntIndex(), function(ent) + self:UnlinkEnt(ent) + end) + + self:UpdateOutputs() + + return true +end + +function ENT:UnlinkEnt(ent) + local bool, index = self:CheckEnt(ent) + + if bool then + table.remove(self.linked_entities, index) + ent:RemoveCallOnRemove("DamageDetector.Unlink" .. self:EntIndex()) + self:UpdateOutputs() + end + + return bool +end + +function ENT:ClearEntities() + for index, ent in ipairs(self.linked_entities) do + ent:RemoveCallOnRemove("DamageDetector.Unlink" .. self:EntIndex()) + end + + self.linked_entities = {} + self:UpdateOutputs() +end + +function ENT:OnRemove() + table.RemoveByValue(Wire_Damage_Detectors, self) + self:ClearEntities() + BaseClass.OnRemove(self) +end + +function ENT:PrepareOverlayData() + local num = #self.linked_entities + self:SetOverlayText(string.format("(%s)\nLinked to %s entities%s", (self.includeconstrained == 1) and "Constrained Props" or "Individual Props", num, (num == 1 and self.linked_entities[1] == self) and "\nLinked only to self" or "")) +end function ENT:BuildDupeInfo() - local info = BaseClass.BuildDupeInfo(self) or {} + local info = BaseClass.BuildDupeInfo(self) if #self.linked_entities > 0 then - info.linked_entities = {} + local tab = {} - for i=1,#self.linked_entities do - if IsValid( self.linked_entities[i] ) then - info.linked_entities[i] = self.linked_entities[i]:EntIndex() - else - self.linked_entities[i] = nil - end + for index, ent in ipairs(self.linked_entities) do + tab[index] = ent:EntIndex() end + + info.linked_entities = tab end return info @@ -329,18 +272,25 @@ function ENT:ApplyDupeInfo(ply, ent, info, GetEntByID) BaseClass.ApplyDupeInfo(self, ply, ent, info, GetEntByID) if info.linked_entities then - self:ClearEntities() + -- Backwards compat + if isnumber(info.linked_entities) then + info.linked_entities = { info.linked_entities } + end - if type( info.linked_entities ) == "number" then -- old dupe compatibility - self:LinkEnt( GetEntByID( info.linked_entities ) ) - else - for i=1,#info.linked_entities do - self:LinkEnt( GetEntByID( info.linked_entities[i] ) ) + for index, entid in ipairs(info.linked_entities) do + local ent = GetEntByID(entid) + + if IsValid(ent) then + table.insert(self.linked_entities, ent) + + ent:CallOnRemove("DamageDetector.Unlink" .. self:EntIndex(), function(ent) + self:UnlinkEnt(ent) + end) end end - end - self:ShowOutput() - -- wait a while after dupe before sending marks, because the entity doesn't exist clientside yet - timer.Simple( 0.1, function() if IsValid( self ) then WireLib.SendMarks( self, self.linked_entities ) end end ) + self:UpdateOutputs() + end end + +duplicator.RegisterEntityClass("gmod_wire_damage_detector", WireLib.MakeWireEnt, "Data", "includeconstrained")