From 93027d3258ceb9e9ccd6895d19aa139bfe5ae109 Mon Sep 17 00:00:00 2001 From: Grocel Date: Thu, 5 Jun 2025 16:56:10 +0200 Subject: [PATCH 01/22] Major Update to Wire Map Interface - Removed built-in Lua Run functions. When used an error message will be shown in game and the code will not be ran. - Improved compatibility to GMods lua_run entity. You can copy-paste affected code key values to a lua_run entity with no further changes required. - Improved security: Blocked AddOutput and RunPassedCode from user input. It prints an error message instead. - Added additional support for hammer searchers (targetnames beginning with "!") - Added additional Lua globals for lua_run entity context. - Added duplicator and GMod save support with basic anti-tampering. - Added Convars: sv_wire_mapinterface_min_trigger_time, sv_wire_mapinterface_max_sub_entities - Improved stability and reliability. - Improved performance. - Fixed an unnamed number of bugs. --- lua/autorun/wire_load.lua | 1 + .../info_wiremapinterface/convert.lua | 380 ++++--- .../info_wiremapinterface/entitycontrol.lua | 404 ++++---- .../info_wiremapinterface/entityoverride.lua | 865 +++++++++++++++- .../info_wiremapinterface/gmodoutputs.lua | 75 ++ lua/entities/info_wiremapinterface/init.lua | 824 +++++++++------ lua/entities/info_wiremapinterface/io.lua | 936 ++++++++++++++++++ .../info_wiremapinterface/networking.lua | 89 ++ .../info_wiremapinterface/savestate.lua | 303 ++++++ .../info_wiremapinterface_savestate/init.lua | 207 ++++ lua/wire/client/cl_wire_map_interface.lua | 254 +++-- lua/wire/server/wire_map_interface.lua | 166 ++++ lua/wire/server/wirelib.lua | 168 ++-- wiremod.fgd | 37 +- 14 files changed, 3903 insertions(+), 806 deletions(-) create mode 100644 lua/entities/info_wiremapinterface/gmodoutputs.lua create mode 100644 lua/entities/info_wiremapinterface/io.lua create mode 100644 lua/entities/info_wiremapinterface/networking.lua create mode 100644 lua/entities/info_wiremapinterface/savestate.lua create mode 100644 lua/entities/info_wiremapinterface_savestate/init.lua create mode 100644 lua/wire/server/wire_map_interface.lua diff --git a/lua/autorun/wire_load.lua b/lua/autorun/wire_load.lua index 0f070912b0..ca6ea3ff0c 100644 --- a/lua/autorun/wire_load.lua +++ b/lua/autorun/wire_load.lua @@ -87,6 +87,7 @@ if SERVER then include("wire/server/modelplug.lua") include("wire/server/debuggerlib.lua") include("wire/server/sents_registry.lua") + include("wire/server/wire_map_interface.lua") if CreateConVar("wire_force_workshop", "1", FCVAR_ARCHIVE, "Should Wire force all clients to download the Workshop edition of Wire, for models? (requires restart to disable)"):GetBool() then if select(2, WireLib.GetVersion()):find("Workshop", 1, true) then diff --git a/lua/entities/info_wiremapinterface/convert.lua b/lua/entities/info_wiremapinterface/convert.lua index 0180b4dabf..216a81a0bb 100644 --- a/lua/entities/info_wiremapinterface/convert.lua +++ b/lua/entities/info_wiremapinterface/convert.lua @@ -2,142 +2,288 @@ -- Per type converting functions for -- converting from map inputs to wire outputs. (String to Value) -local MapToWireTypes = { - [0] = {"NORMAL", function(str) -- Number, default - return tonumber(str) or 0 - end}, - - [1] = {"NORMAL", function(self, ent, I) -- switches between 0 and 1 each call, useful for toggling. - if (!IsValid(self) or !IsValid(ent) or !I) then return 0 end - - self.WireOutputToggle = self.WireOutputToggle or {} - self.WireOutputToggle[ent] = self.WireOutputToggle[ent] or {} - self.WireOutputToggle[ent][I] = !self.WireOutputToggle[ent][I] - - return self.WireOutputToggle[ent][I] and 1 or 0 - end, true}, - - [2] = {"STRING", function(str) -- String - return str or "" - end}, - - [3] = {"VECTOR2", function(str) -- 2D Vector - local x, y = unpack(string.Explode(" ", str or "")) - x = tonumber(x) or 0 - y = tonumber(y) or 0 - - return {x, y} - end}, - - [4] = {"VECTOR", function(str) -- 3D Vector - local x, y, z = unpack(string.Explode(" ", str or "")) - x = tonumber(x) or 0 - y = tonumber(y) or 0 - z = tonumber(z) or 0 - - return Vector(x, y, z) - end}, - - [5] = {"VECTOR4", function(str) -- 4D Vector - local x, y, z, w = unpack(string.Explode(" ", str or "")) - x = tonumber(x) or 0 - y = tonumber(y) or 0 - z = tonumber(z) or 0 - w = tonumber(w) or 0 - - return {x, y, z, w} - end}, - - [6] = {"ANGLE", function(str) -- Angle - local p, y, r = unpack(string.Explode(" ", str or "")) - p = tonumber(p) or 0 - y = tonumber(y) or 0 - r = tonumber(r) or 0 - - return Angle(p, y, r) - end}, - - [7] = {"ENTITY", function(val) -- Entity - return Entity(tonumber(val) or 0) or NULL - end}, - - [8] = {"ARRAY", function(str) -- Array/Table - return string.Explode(" ", str or "") - end}, -} --- Per type converting functions for --- converting from wire inputs to map outputs. (Value to String) -local WireToMapTypes = { - [0] = {"NORMAL", function(val) -- Number, default - return tostring(val or 0) - end}, +local function isEqualList(_, listA, listB) + if listA == listB then + return true + end + + if listA == nil then + return false + end + + if listB == nil then + return false + end + + if #listA ~= #listB then + return false + end + + for i, va in ipairs(listA) do + local vb = listB[i] + + if va ~= vb then + return false + end + end + + return true +end + +local function isEqualGeneric(_, varA, varB) + return varA == varB +end + +local function isEqualEntity(_, entA, entB) + if not IsValid(entA) then + return false + end + + if not IsValid(entB) then + return false + end + + return entA == entB +end + +local g_supportedTypesById = { + [0] = { -- Number, default + wireType = "NORMAL", + + toHammer = function(_, wireValue) + wireValue = tonumber(wireValue or 0) or 0 + return string.format("%.20g", wireValue) + end, + + toWire = function(_, hammerValue) + return tonumber(hammerValue or 0) or 0 + end, + + wireIsEqual = function(_, wireValueA, wireValueB) + wireValueA = tonumber(wireValueA) + wireValueB = tonumber(wireValueB) + + if not wireValueA then + return false + end + + if not wireValueB then + return false + end + + return wireValueA == wireValueB + end + }, + + [1] = { -- Number, toggle + wireType = "NORMAL", + + -- Wire Input: Trigger Hammer output only if true. Does not pass the input value from Wire to Hammer. + -- Wire Output: Toggle the Wire Output when triggered by Hammer. Does not pass the input value from Hammer or Wire. + isToggle = true, + + toHammer = function(_, wireValue) -- Return a boolean, 0 = false, 1 = true, useful for toggling. It triggers Hammer output only if true. + return tobool(wireValue) + end, + + toWire = function(_, hammerValue) -- Is being switched between 0 and 1 each call from Hammer input. + return tobool(hammerValue) and 1 or 0 + end, + + wireIsEqual = isEqualGeneric, + }, + + [2] = { -- String + wireType = "STRING", + + toHammer = function(_, wireValue) + return tostring(wireValue or "") + end, + + toWire = function(_, hammerValue) + return hammerValue or "" + end, + + wireIsEqual = isEqualGeneric, + }, + + [3] = { -- 2D Vector + wireType = "VECTOR2", + + toHammer = function(_, wireValue) + wireValue = wireValue or {0, 0} - [1] = {"NORMAL", function(val) -- Return a boolean, 0 = false, 1 = true, useful for toggling. - return (tonumber(val) or 0) > 0 - end, true}, + local x = tonumber(wireValue[1] or 0) or 0 + local y = tonumber(wireValue[2] or 0) or 0 - [2] = {"STRING", function(val) -- String - return val or "" - end}, + return string.format("%.20g %.20g", x, y) + end, - [3] = {"VECTOR2", function(val) -- 2D Vector - val = val or {0, 0} + toWire = function(_, hammerValue) + local x, y = unpack(string.Explode(" ", hammerValue or "")) - local x = math.Round(val[1] or 0) - local y = math.Round(val[2] or 0) + x = tonumber(x or 0) or 0 + y = tonumber(y or 0) or 0 - return x.." "..y - end}, + return {x, y} + end, - [4] = {"VECTOR", function(val) -- 3D Vector - val = val or Vector(0, 0, 0) + wireIsEqual = isEqualList, + }, - local x = math.Round(val.x or 0) - local y = math.Round(val.y or 0) - local z = math.Round(val.z or 0) + [4] = { -- 3D Vector + wireType = "VECTOR", - return x.." "..y.." "..z - end}, + toHammer = function(_, wireValue) + wireValue = wireValue or Vector(0, 0, 0) - [5] = {"VECTOR4", function(val) --4D Vector - val = val or {0, 0, 0, 0} + local x = tonumber(wireValue.x or 0) or 0 + local y = tonumber(wireValue.y or 0) or 0 + local z = tonumber(wireValue.z or 0) or 0 - local x = math.Round(val[1] or 0) - local y = math.Round(val[2] or 0) - local z = math.Round(val[3] or 0) - local w = math.Round(val[4] or 0) + return string.format("%.20g %.20g %.20g", x, y, z) + end, - return x.." "..y.." "..z.." "..w - end}, + toWire = function(_, hammerValue) + local x, y, z = unpack(string.Explode(" ", hammerValue or "")) - [6] = {"ANGLE", function(val) -- Angle - val = val or Angle(0, 0, 0) + x = tonumber(x or 0) or 0 + y = tonumber(y or 0) or 0 + z = tonumber(z or 0) or 0 - local p = math.Round(val.p or 0) - local y = math.Round(val.y or 0) - local r = math.Round(val.r or 0) + return Vector(x, y, z) + end, - return p.." "..y.." "..r - end}, + wireIsEqual = isEqualGeneric, + }, - [7] = {"ENTITY", function(val) -- Entity - if (!IsValid(val)) then return "0" end + [5] = { -- 4D Vector + wireType = "VECTOR4", - return tostring(val:EntIndex()) - end}, + toHammer = function(_, wireValue) + val = val or {0, 0, 0, 0} - [8] = {"ARRAY", function(val) -- Array/Table - return table.concat(val or {}, " ") - end}, + local x = tonumber(val[1] or 0) or 0 + local y = tonumber(val[2] or 0) or 0 + local z = tonumber(val[3] or 0) or 0 + local w = tonumber(val[4] or 0) or 0 + + return string.format("%.20g %.20g %.20g %.20g", x, y, z, w) + end, + + toWire = function(_, hammerValue) + local x, y, z, w = unpack(string.Explode(" ", hammerValue or "")) + + x = tonumber(x or 0) or 0 + y = tonumber(y or 0) or 0 + z = tonumber(z or 0) or 0 + w = tonumber(w or 0) or 0 + + return {x, y, z, w} + end, + + wireIsEqual = isEqualList, + }, + + [6] = { -- Angle + wireType = "ANGLE", + + toHammer = function(_, wireValue) + wireValue = wireValue or Angle(0, 0, 0) + + local p = tonumber(wireValue.p or 0) or 0 + local y = tonumber(wireValue.y or 0) or 0 + local r = tonumber(wireValue.r or 0) or 0 + + return string.format("%.20g %.20g %.20g", p, y, r) + end, + + toWire = function(_, hammerValue) + local p, y, r = unpack(string.Explode(" ", hammerValue or "")) + p = tonumber(p or 0) or 0 + y = tonumber(y or 0) or 0 + r = tonumber(r or 0) or 0 + + return Angle(p, y, r) + end, + + wireIsEqual = isEqualGeneric, + }, + + [7] = { -- Entity + wireType = "ENTITY", + + toHammer = function(_, wireValue) + if not IsValid(wireValue) then return "0" end + return tostring(wireValue:EntIndex()) + end, + + toWire = function(self, hammerValue) + local id = tonumber(hammerValue or 0) + + if id ~= nil then + if id == 0 then + return game.GetWorld() + end + + return ents.GetByIndex(id) + end + + return self:GetFirstEntityByTargetnameOrClass(hammerValue) or NULL + end, + + wireIsEqual = isEqualEntity, + }, + + [8] = { -- Array/Table + wireType = "ARRAY", + + toHammer = function(_, wireValue) + return table.concat(wireValue or {}, " ") + end, + + toWire = function(_, hammerValue) + return string.Explode(" ", hammerValue or "") + end, + + wireIsEqual = isEqualList, + }, } --- Converting functions -function ENT:Convert_MapToWire(n) - local typetab = MapToWireTypes[n or 0] or MapToWireTypes[0] - return typetab[1], typetab[2], typetab[3] +function ENT:GetMapToWireConverter(typeId) + local typeData = g_supportedTypesById[typeId or 0] or g_supportedTypesById[0] + if not typeData then + return + end + + return typeData.toWire, typeData.isToggle or false +end + +function ENT:GetWireToMapConverter(typeId) + local typeData = g_supportedTypesById[typeId or 0] or g_supportedTypesById[0] + if not typeData then + return + end + + return typeData.toHammer, typeData.isToggle or false +end + +function ENT:IsEqualWireValue(typeId, wireValueA, wireValueB) + local typeData = g_supportedTypesById[typeId or 0] or g_supportedTypesById[0] + if not typeData then + return false + end + + return typeData.wireIsEqual(self, wireValueA, wireValueB) end -function ENT:Convert_WireToMap(n) - local typetab = WireToMapTypes[n or 0] or WireToMapTypes[0] - return typetab[1], typetab[2], typetab[3] + +function ENT:GetWireTypenameByTypeId(typeId) + local typeData = g_supportedTypesById[typeId or 0] or g_supportedTypesById[0] + if not typeData then + return + end + + return typeData.wireType end + diff --git a/lua/entities/info_wiremapinterface/entitycontrol.lua b/lua/entities/info_wiremapinterface/entitycontrol.lua index 9e9bf31286..bf4d271abf 100644 --- a/lua/entities/info_wiremapinterface/entitycontrol.lua +++ b/lua/entities/info_wiremapinterface/entitycontrol.lua @@ -1,254 +1,302 @@ -- This part of the wire map interface entity controls -- the adding and removing of its in-/outputs entities. +function ENT:HandleWireEntNameUpdated() + if not self.WireEntNameUpdated then + return + end --- The removing function --- Its for removing all the wiremod stuff from unused entities. -local function RemoveWire(Entity, SendToCL) - if (not IsValid(Entity)) then return end - - Wire_Remove(Entity, not SendToCL) + self:AddEntitiesByName(self.WireEntName) + self.WireEntNameUpdated = nil +end - local self = Entity._WireMapInterfaceEnt - if (IsValid(self)) then - self.WireEntsCount = math.max(self.WireEntsCount - 1, 0) - if (self.WireEnts) then - self.WireEnts[Entity] = nil - end - if (self.WireOutputToggle) then - self.WireOutputToggle[Entity] = nil - end - if (self.Wired) then - self.Wired[Entity] = nil - end +function ENT:HandleWireEntsUpdated() + if not self.WireEntsUpdated then + return end - if (not SendToCL) then return end + local ready = false - Entity:_RemoveOverrides() - WireLib._RemoveWire(Entity:EntIndex(), true) -- Remove entity from the list, so it doesn't count as a wire able entity anymore. + if self.WireEntsRemoved then + self:TriggerHammerOutputSafe("OnWireEntsRemoved", self) + self.WireEntsRemoved = nil + ready = true + end - umsg.Start("WireMapInterfaceEnt") - umsg.Entity(Entity) - umsg.Char(-1) - umsg.End() + if self.WireEntsAdded then + if self:GetWiredEntityCount() > 0 then + -- Avoid triggering "created" event if the list if entity is actually empty. + self:TriggerHammerOutputSafe("OnWireEntsCreated", self) + ready = true + end - Entity:RemoveCallOnRemove("WireMapInterface_OnRemove") - if (table.IsEmpty(Entity.OnDieFunctions)) then - Entity.OnDieFunctions = nil + self.WireEntsAdded = nil end -end -function ENT:Timedpairs(name, tab, steps, cb, endcb, ...) - if (table.IsEmpty(tab)) then return end + if ready then + -- Only trigger "ready" if one of the other both had been triggered. + self:TriggerHammerOutputSafe("OnWireEntsReady", self) + end - local name = self:EntIndex().."_"..tostring(name) - self.TimedpairsTable = self.TimedpairsTable or {} + self.NextNetworkTime = math.max(self.NextNetworkTime or 0, CurTime() + self.MIN_THINK_TIME * 2) + self.WireEntsUpdated = nil +end - WireLib.Timedpairs(name, tab, steps, function(...) +-- Entity add functions +function ENT:AddEntitiesByName(name) + local entities = self:GetEntitiesByTargetnameOrClass(name) + self:AddEntitiesByTable(entities) +end - if (IsValid(self)) then - self.TimedpairsTable[name] = true - end - return cb(...) +function ENT:AddEntitiesByTable(entitiesToAdd) + if not entitiesToAdd then return end - end, function(...) + local tmp = {} - if (IsValid(self)) then - self.TimedpairsTable[name] = nil + for key, value in pairs(entitiesToAdd) do + if isentity(key) and IsValid(key) then + tmp[key] = key end - if (endcb) then - return endcb(...) + + if isentity(value) and IsValid(value) then + tmp[value] = value end + end - end, ...) + for _, wireEnt in pairs(tmp) do + self:AddSingleEntity(wireEnt) + end end -local function CallOnEnd(self, AddedEnts) - if (not IsValid(self)) then return end +function ENT:AddSingleEntity(wireEnt) + if not self:IsWireableEntity(wireEnt) then + return + end - self.WirePortsChanged = true - self:GiveWireInterfeceClient(nil, AddedEnts) - self:TriggerOutput("onwireentscreated", self) - self:TriggerOutput("onwireentsready", self) -end + local hardLimit = self.WireEntsHardLimit or 0 -function ENT:GiveWireInterfece(EntsToAdd) - if (not EntsToAdd) then return end - if (table.IsEmpty(EntsToAdd) or not self.WirePortsChanged) then return end - local AddedEnts = {} - self:UpdateData() + if hardLimit >= self:GetMaxSubEntities() * 3 then + -- Stop adding more things until cleaned up. + return + end - self:Timedpairs("WireMapInterface_Adding", EntsToAdd, 1, function(obj1, obj2, self) - if (not IsValid(self)) then return false end -- Stop loop when the entity gets removed. - if (self.WirePortsChanged) then - self:TriggerOutput("onwireentsstartchanging", self) - end - self.WirePortsChanged = nil + local id = wireEnt:EntIndex() + local wireEnts = self.WireEntsRegister - local Entity = (IsEntity(obj1) and obj1) or (IsEntity(obj2) and obj2) + local item = wireEnts[id] + local oldWireEnt = item and item.ent - local Ent, Func = self:AddSingleEntity(Entity, CallOnEnd, AddedEnts) - if (Ent == "limid_exceeded") then return false end -- Stop loop when maximum got exceeded - if (not IsValid(Ent) or not Func) then return end + local isInList = IsValid(oldWireEnt) and oldWireEnt == wireEnt - AddedEnts[Ent] = Func - end, function(k, v, self) CallOnEnd(self, AddedEnts) end, self) -end + if isInList and wireEnt._WireMapInterfaceEnt_HasPorts then + return + end -function ENT:GiveWireInterfeceClient(ply, EntsToAdd) - if (not self.WireEnts) then return end + if not isInList then + if not self.WireEntsUpdated then + self:TriggerHammerOutputSafe("OnWireEntsStartChanging", self) + end - self:Timedpairs((IsValid(ply) and (ply:EntIndex().."") or "").."WireMapInterface_Adding_CL", EntsToAdd or self.WireEnts, 1, function(Entity, Func, self) - if (not IsValid(self)) then return false end -- Stop loop when the entity gets removed. - if (not IsValid(Entity)) then return end - if (not self:IsWireableEntity(Entity)) then return end + self:OverrideEnt(wireEnt) + end - Func(self, Entity, ply, not IsValid(ply)) - end, nil, self) -end + if wireEnt._WMI_AddPorts then + wireEnt:_WMI_AddPorts(self.WireInputRegister, self.WireOutputRegister) + end + wireEnts[id] = { + ent = wireEnt, + cid = wireEnt:GetCreationID(), + } --- Entity add functions -function ENT:AddEntitiesByName(Name) - Name = tostring(Name or "") - if (Name == "") then return end + if not isInList then + self.WireEntsHardLimit = hardLimit + 1 + + self.WireEntsUpdated = true + self.WireEntsAdded = true + + self.ShouldNetworkEntities = true + self.WireEntsSorted = nil + self.WireEntsCount = nil + end - self:AddEntitiesByTable(ents.FindByName(Name)) + self:ApplyWireOutputBufferSingle(wireEnt) + + -- Sometimes CurTime() may get lag compensated and "time travels" when the entity is being added by duplicator/gun trigger. + -- So we delay the think call past the predicted event time to avoid any race conditions. + self:NextThink(CurTime() + self.MIN_THINK_TIME) + return wireEnt end -function ENT:AddEntitiesByTable(Table) - if (not Table) then return end +-- Entity remove functions +function ENT:RemoveAllEntities() + local wireEnts = self.WireEntsRegister + + for id, item in pairs(wireEnts) do + self:RemoveSingleEntity(item.ent) + wireEnts[id] = nil + end - self:GiveWireInterfece(Table) + self.WireEntsSorted = nil + self.WireEntsCount = nil + self.WireEntsHardLimit = nil end -local function AddSingleEntityCL(self, Entity, ply, SendToAll) - if (not IsValid(Entity)) then return end - if (not IsValid(self)) then return end - if (not self.WireEnts[Entity]) then return end - if (not SendToAll and not IsValid(ply)) then return end - - if (SendToAll) then - umsg.Start("WireMapInterfaceEnt") - else - umsg.Start("WireMapInterfaceEnt", ply) - end - umsg.Entity(Entity) - umsg.Char(self.flags % 64) - -- Allow valid spawnflags only. - umsg.End() +function ENT:RemoveEntitiesByName(name) + local entities = self:GetEntitiesByTargetnameOrClass(name) + self:RemoveEntitiesByTable(entities) end -function ENT:AddSingleEntity(Entity, callOnEnd, AddedEnts) - if (not IsValid(Entity)) then return end - if (not self:IsWireableEntity(Entity)) then return end - if (not self:CheckEntLimid(callOnEnd, AddedEnts)) then return "limid_exceeded" end +function ENT:RemoveEntitiesByTable(entitiesToRemove) + if not entitiesToRemove then return end - if (IsValid(Entity._WireMapInterfaceEnt)) then - RemoveWire(Entity, true) - end + local tmp = {} - self:OverrideEnt(Entity) - Entity:CallOnRemove("WireMapInterface_OnRemove", RemoveWire) + for key, value in pairs(entitiesToRemove) do + if isentity(key) and IsValid(key) then + tmp[key] = key + end - if (self.Inames) then - Entity.Inputs = WireLib.CreateSpecialInputs(Entity, self.Inames, self.Itypes, self.Idescs) + if isentity(value) and IsValid(value) then + tmp[value] = value + end end - if (self.Onames) then - Entity.Outputs = WireLib.CreateSpecialOutputs(Entity, self.Onames, self.Otypes, self.Odescs) + + for _, wireEnt in pairs(tmp) do + self:RemoveSingleEntity(wireEnt) end +end - self.WireEnts = self.WireEnts or {} - self.WireEnts[Entity] = AddSingleEntityCL +function ENT:RemoveSingleEntity(wireEnt) + if not IsValid(wireEnt) then return end + if not IsValid(wireEnt._WireMapInterfaceEnt) then return end + if wireEnt._WireMapInterfaceEnt ~= self then return end - return Entity, AddSingleEntityCL + local id = wireEnt:EntIndex() + local wireEnts = self.WireEntsRegister + + if not wireEnts[id] then + return + end + + if wireEnt._WMI_RemoveOverrides then + wireEnt:_WMI_RemoveOverrides(self) + end + + wireEnts[id] = nil + self.WireEntsSorted = nil + self.WireEntsCount = nil end +function ENT:UnregisterWireEntityInternal(wireEnt) + if not IsValid(wireEnt) then + return + end + + local wireEnts = self.WireEntsRegister + local id = wireEnt:EntIndex() --- Entity remove functions -function ENT:RemoveAllEntities(callback) - for name, _ in pairs(self.TimedpairsTable or {}) do - WireLib.TimedpairsStop(name) + if not wireEnts[id] then + return end - self.WirePortsChanged = true + if not self.WireEntsUpdated then + self:TriggerHammerOutputSafe("OnWireEntsStartChanging", self) + end - self:RemoveEntitiesByTable(self.WireEnts, callback) - self.WireEntsCount = 0 -end + wireEnts[id] = nil + wireEnt._WireMapInterfaceEnt = nil + local hardLimit = self.WireEntsHardLimit or 0 + self.WireEntsHardLimit = math.max(hardLimit - 1, 0) -function ENT:RemoveEntitiesByName(Name, callback) - Name = tostring(Name or "") - if (Name == "") then return end + self.WireEntsUpdated = true + self.WireEntsRemoved = true - self:RemoveEntitiesByTable(ents.FindByName(Name), callback) + self.ShouldNetworkEntities = true + self.WireEntsSorted = nil + self.WireEntsCount = nil + + -- Sometimes CurTime() may get lag compensated and "time travels" when the entity is being destoryed by gun fire. + -- So we delay the think past the predicted event time to avoid any race conditions. + self:NextThink(CurTime() + self.MIN_THINK_TIME) end -function ENT:RemoveEntitiesByTable(Table, callback) - if (not Table) then return end - if (table.IsEmpty(Table) or not self.WirePortsChanged) then return end +function ENT:SanitizeAndSortWiredEntities() + local wireEnts = self.WireEntsRegister - local Removed = nil - self:Timedpairs("WireMapInterface_Removing", Table, 1, function(obj1, obj2, self) - local Entity = (IsEntity(obj1) and obj1) or (IsEntity(obj2) and obj2) + if not wireEnts or table.IsEmpty(wireEnts) then + self.WireEntsSorted = {} + self.WireEntsCount = 0 + self.WireEntsHardLimit = nil - if (not IsValid(Entity)) then return end - if (not IsValid(Entity._WireMapInterfaceEnt)) then return end - if (Entity._WireMapInterfaceEnt ~= self) then return end - if (self and self.WirePortsChanged) then - self:TriggerOutput("onwireentsstartchanging", self) - self.WirePortsChanged = nil - end + return + end + + for id, item in pairs(wireEnts) do + local wireEnt = item.ent + + if not self:IsWireableEntity(wireEnt) or + not wireEnt._IsWireMapInterfaceSubEntity or + not wireEnt._WireMapInterfaceEnt_Data or + not wireEnt._WMI_GetSpawnId + then + -- Remove invalid/broken wire entities. - RemoveWire(Entity, true) - Removed = true - end, - function(k, v, self, callback) - if (not IsValid(self)) then return end - self.WirePortsChanged = true + if IsValid(wireEnt) then + if wireEnt._WMI_RemoveOverrides then + wireEnt:_WMI_RemoveOverrides(self) + end + end - if (callback) then - callback(self, Removed) + wireEnts[id] = nil end + end - if (not Removed) then return end - self:TriggerOutput("onwireentsremoved", self) - self:TriggerOutput("onwireentsready", self) - end, self, callback) + local count = 0 + local wireEntsSorted = {} -end + for id, item in SortedPairsByMemberValue(wireEnts, "cid", false) do + local wireEnt = item.ent -function ENT:RemoveSingleEntity(Entity) - if (not IsValid(Entity)) then return end - if (not IsValid(Entity._WireMapInterfaceEnt)) then return end - if (Entity._WireMapInterfaceEnt ~= self) then return end + if self:CheckEntLimit(count, wireEnt) then + count = count + 1 + wireEntsSorted[count] = wireEnt + else + -- Remove newest wire entities first if limit is exhausted. + if wireEnt._WMI_RemoveOverrides then + wireEnt:_WMI_RemoveOverrides(self) + end - RemoveWire(Entity, true) - self:TriggerOutput("onwireentsremoved", self) - self:TriggerOutput("onwireentsready", self) + wireEnts[id] = nil + end + end + + self.WireEntsSorted = wireEntsSorted + self.WireEntsCount = count + self.WireEntsHardLimit = nil end +function ENT:GetWiredEntities() + if not self.WireEntsSorted then + self:SanitizeAndSortWiredEntities() + end + return self.WireEntsSorted +end -function ENT:GetWiredEntities() - return table.Copy(self.WireEnts or {}) +function ENT:GetWiredEntityCount() + if not self.WireEntsCount then + self:SanitizeAndSortWiredEntities() + end + + return self.WireEntsCount end -function ENT:SetWiredEntities(Table) - if (not Table) then return end +function ENT:SetWiredEntities(entities) + if not entities then return end - local Ents = {} - local Count = 0 - for obj1, obj2 in pairs(Table) do -- Filter invalid stuff out! - local Entity = (IsEntity(obj1) and obj1) or (IsEntity(obj2) and obj2) - if (IsValid(Entity)) then - Count = Count + 1 - Ents[Count] = Entity - end - end - self:RemoveAllEntities(function(self) - self:AddEntitiesByTable(Ents) - end) + self:RemoveAllEntities() + self:AddEntitiesByTable(entities) end + diff --git a/lua/entities/info_wiremapinterface/entityoverride.lua b/lua/entities/info_wiremapinterface/entityoverride.lua index 7962bf7379..df3b2f593e 100644 --- a/lua/entities/info_wiremapinterface/entityoverride.lua +++ b/lua/entities/info_wiremapinterface/entityoverride.lua @@ -1,71 +1,858 @@ -- Stuff that the entity gets for its wire stuff. +local WireLib = WireLib + local WIREENT = {} + +local g_wirelinkName = "wirelink" +local g_wireTools = { + "wire", + "wire_adv", + "wire_debugger", + "wire_wirelink", + "multi_wire", +} + +local g_memberBlacklist = { + _IsWireMapInterfaceSubEntity = true, + _WireMapInterfaceEnt_TmpPorts = true, + _WireMapInterfaceEnt_HasPorts = true, + _WireMapInterfaceEnt_SpawnId = true, + _WireMapInterfaceEnt_MapId = true, + _WireMapInterfaceEnt_Data = true, + _WireMapInterfaceEnt = true, + _WMI_OverrideEnt = true, + _WMI_RemoveOverrides = true, + PhysgunDisabled = true, + m_tblToolsAllowed = true, + Inputs = true, + Outputs = true, + IsWire = true, + WireDebugName = true, +} + +function WIREENT:_WMI_GetConnectedWireInputSource(name) + local wireinputs = self.Inputs + if not wireinputs then return nil end + + local wireinput = wireinputs[name] + if not wireinput then return nil end + if not IsValid(wireinput.Src) then return nil end + + return wireinput.Src +end + +function WIREENT:_WMI_GetConnectedWireWirelinkSource() + local wireoutputs = self.Outputs + if not wireoutputs then return nil end + + local wireoutput = wireoutputs[g_wirelinkName] + if not wireoutput then return nil end + if not wireoutput.Connected then return nil end + + for key, connectedItem in ipairs(wireoutput.Connected) do + if IsValid(connectedItem.Entity) then + return connectedItem.Entity + end + end + + return nil +end + +function WIREENT:_WMI_IsConnectedWireInput(name) + if not self:_WMI_GetConnectedWireInputSource(name) then return false end + return true +end + +function WIREENT:_WMI_IsConnectedWireOutput(name) + local wireoutputs = self.Outputs + if not wireoutputs then return false end + + local wireoutput = wireoutputs[name] + if not wireoutput then return false end + if not wireoutput.Connected then return false end + + for key, connectedItem in ipairs(wireoutput.Connected) do + if IsValid(connectedItem.Entity) then + return true + end + end + + return false +end + +function WIREENT:_WMI_IsConnectedWirelink() + if not self.extended then + -- wirelink had not been created yet + return false + end + + if self:_WMI_IsConnectedWireOutput(g_wirelinkName) then + -- wirelink had been connected via Wire Tool + return true + end + + return false +end + -- Trigger wire input -function WIREENT:TriggerInput(name, value, ...) - if (not name or (name == "") or not value) then return end +function WIREENT:TriggerInput(name, value, ext, ...) + if not name then return end + if name == "" then return end + if not value then return end - local Entity = self._WireMapInterfaceEnt - if (not IsValid(Entity)) then return end - if (not Entity.TriggerWireInput) then return end + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then return end - local Input = self.Inputs[name] or {} - Entity:TriggerWireInput(name, value, IsValid(Input.Src) == true, self, ...) + local interfaceEnt = self._WireMapInterfaceEnt + if not IsValid(interfaceEnt) then return end + if not interfaceEnt.TriggerWireInput then return end + + -- Ensure the Wirelink is actually physically connected. Otherwise it is not possible to detect disconnections. + -- It is also needed for the custom ownership management this entity has. This entity may change Wiremod owner depeding on the ownership other connected entities. + -- So using E2 with E:wirelink() does not work for this, as it would bypass prop protection or other protection the map/server has for this map entity. + local isWirelink = self:_WMI_IsConnectedWirelink() and ext and ext.wirelink + local wired = self:_WMI_IsConnectedWireInput(name) or isWirelink + + if not isWirelink then + local realValueBuffer = wmidata.realValueBuffer or {} + wmidata.realValueBuffer = realValueBuffer + + realValueBuffer[name] = value + end + + local device = self:_WMI_GetInputDevice(isWirelink and g_wirelinkName or name) + + if not isWirelink then + wmidata.lastDirectInputDevice = device + else + wmidata.lastDirectInputDevice = nil + end + + interfaceEnt:TriggerWireInput(name, value, wired, self) +end + +function WIREENT:_WMI_GetDirectLinkedInputValue(name) + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return nil + end + + local realValueBuffer = wmidata.realValueBuffer + if not realValueBuffer then + return nil + end + + return realValueBuffer[name] +end + +function WIREENT:_WMI_GetInputDevice(name) + if not name then + local device = self:_WMI_GetConnectedWireWirelinkSource() + + if not IsValid(device) then + device = self:_WMI_GetLastDirectInputDevice() + end + + return device + end + + if name == g_wirelinkName then + return self:_WMI_GetConnectedWireWirelinkSource() + end + + return self:_WMI_GetConnectedWireInputSource(name) +end + +function WIREENT:_WMI_GetLastDirectInputDevice() + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return nil + end + + local device = wmidata.lastDirectInputDevice + if not IsValid(device) then + return nil + end + + return device +end + +function WIREENT:_WMI_FindWireMapInterfaceEnt(interface) + if not interface then + return nil + end + + local mapId = interface.mapId + local spawnId = interface.spawnId + + if not spawnId then + return nil + end + + mapId = tonumber(mapId) + + if not WireLib.WireMapInterfaceValidateId(mapId) then + return nil + end + + local interfaceEnt = ents.GetMapCreatedEntity(mapId) + if not IsValid(interfaceEnt) then + return nil + end + + if not interfaceEnt.IsWireMapInterface then + return nil + end + + if not interfaceEnt:ValidateDupedMapId(mapId, spawnId) then + return nil + end + + return interfaceEnt +end + +local g_functionCallNesting_GetPlayer = 0 + +function WIREENT:GetPlayer(...) + -- Fake the owner of the map owned entity if we don't have one. It is needed for E2 wirelink to work. + -- It uses the owner of the contected entity e.g. the E2 chip. + + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return nil + end + + if wmidata.oldMethods.GetPlayer then + local ply = wmidata.oldMethods.GetPlayer(self, ...) + + if IsValid(ply) then + return ply + end + end + + local dupe = wmidata.dupe + if dupe and IsValid(dupe.owner) then + return dupe.owner + end + + if not self:_WMI_IsConnectedWirelink() then + -- Owner fake is exclusive to wirelink + return nil + end + + local device = self:_WMI_GetInputDevice() + if not IsValid(device) or device == self then + return nil + end + + if g_functionCallNesting_GetPlayer > 5 then + -- prevent recursive loop + return nil + end + + g_functionCallNesting_GetPlayer = g_functionCallNesting_GetPlayer + 1 + + local fakeOwner = WireLib.GetOwner(device) + + g_functionCallNesting_GetPlayer = math.max(g_functionCallNesting_GetPlayer - 1, 0) + + return fakeOwner +end + +function WIREENT:OnEntityCopyTableFinish(dupedata, ...) + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return + end + + if wmidata.oldMethods.OnEntityCopyTableFinish then + wmidata.oldMethods.OnEntityCopyTableFinish(self, dupedata, ...) + end + + -- Prevent weird stuff from happening on garry dupe/save + dupedata.Inputs = nil + dupedata.Outputs = nil + dupedata.IsWire = nil + + local oldMembers = wmidata.oldMembers or {} + for k, v in pairs(oldMembers) do + dupedata[k] = nil + end + + local oldSettings = wmidata.oldSettings or {} + for k, v in pairs(oldSettings) do + dupedata[k] = nil + end + + for k, v in pairs(g_memberBlacklist) do + dupedata[k] = nil + end +end + +function WIREENT:PreEntityCopy(...) + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return + end + + if wmidata.oldMethods.PreEntityCopy then + wmidata.oldMethods.PreEntityCopy(self, ...) + end + + duplicator.ClearEntityModifier(self, "WireDupeInfo") + duplicator.ClearEntityModifier(self, "WireMapInterfaceEntDupeInfo") + + local dupeData = self:_WMI_BuildDupeData() + + if dupeData.wireDupeInfo then + duplicator.StoreEntityModifier(self, "WireDupeInfo", dupeData.wireDupeInfo) + end + + if dupeData.wireMapInterfaceEntDupeInfo then + duplicator.StoreEntityModifier(self, "WireMapInterfaceEntDupeInfo", dupeData.wireMapInterfaceEntDupeInfo) + end +end + +function WIREENT:PostEntityPaste(ply, ent, createdEntities, ...) + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return + end + + if wmidata.oldMethods.PostEntityPaste then + wmidata.oldMethods.PostEntityPaste(self, ply, ent, createdEntities, ...) + end + + if not IsValid(ent) then + return + end + + local entityMods = ent.EntityMods + if not entityMods then + return + end + + local dupeData = { + wireDupeInfo = entityMods.WireDupeInfo, + wireMapInterfaceEntDupeInfo = entityMods.WireMapInterfaceEntDupeInfo, + } + + ent:_WMI_ApplyDupeData(ply, dupeData, createdEntities) +end + +function WIREENT:_WMI_BuildDupeData(interfaceEnt) + local data = {} + local wmiDupeInfo = {} + + data.wireDupeInfo = WireLib.BuildDupeInfo(self) + data.wireMapInterfaceEntDupeInfo = wmiDupeInfo + + if not IsValid(interfaceEnt) then + interfaceEnt = self._WireMapInterfaceEnt + + if not IsValid(interfaceEnt) then + return data + end + end + + local interfaceMapId = nil + if interfaceEnt:CreatedByMap() then + interfaceMapId = interfaceEnt:MapCreationID() + end + + local wmidata = self._WireMapInterfaceEnt_Data or {} + + wmiDupeInfo.entIdx = self:EntIndex() + wmiDupeInfo.mapId = self:_WMI_MapCreationIdDuped() + wmiDupeInfo.spawnId = self:_WMI_GetSpawnId(interfaceEnt) + + wmiDupeInfo.interface = { + mapId = interfaceMapId, + spawnId = interfaceEnt:GetSpawnId(), + } + + wmiDupeInfo.tmpPorts = { + inputs = wmidata.inputs, + outputs = wmidata.outputs, + } + + return data +end + +function WIREENT:_WMI_ApplyDupeData(ply, dupeData, createdEntities, interfaceEnt) + if not dupeData then + return + end + + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return + end + + local wireMapInterfaceEntDupeInfo = dupeData.wireMapInterfaceEntDupeInfo or {} + local wireDupeInfo = dupeData.wireDupeInfo or {} + + local interface = wireMapInterfaceEntDupeInfo.interface + local tmpPorts = wireMapInterfaceEntDupeInfo.tmpPorts + local spawnId = wireMapInterfaceEntDupeInfo.spawnId + local entIdx = wireMapInterfaceEntDupeInfo.entIdx + local mapId = wireMapInterfaceEntDupeInfo.mapId + + local dupe = wmidata.dupe or {} + wmidata.dupe = dupe + dupe.owner = ply + + dupe.mapId = mapId + dupe.entIdx = entIdx + dupe.spawnId = spawnId + + local dupeInterfaceEnt = dupe.interfaceEnt + dupe.interfaceEnt = nil + + if not IsValid(interfaceEnt) then + interfaceEnt = dupeInterfaceEnt + + if not IsValid(interfaceEnt) then + interfaceEnt = self:_WMI_FindWireMapInterfaceEnt(interface) + + if not IsValid(interfaceEnt) then + self:_WMI_RemoveOverrides() + return + end + end + end + + if not interfaceEnt:IsWireableEntity(self) then + self:_WMI_RemoveOverrides(interfaceEnt) + return + end + + dupe.interfaceEnt = interfaceEnt + + if interfaceEnt:HashSubEntityMapId(entIdx, mapId) ~= spawnId then + self:_WMI_RemoveOverrides(interfaceEnt) + return + end + + self:_WMI_ApplyWireDupeData(ply, wireDupeInfo, createdEntities, tmpPorts) + interfaceEnt:AddSingleEntity(self) +end + +local function GetEntityLookupFunction(createdEntities) + return function(idx, default) + if idx == nil then + return default + end + + if idx == 0 then + return game.GetWorld() + end + + local ent = createdEntities[idx] + if IsValid(ent) then + return ent + end + + return default + end +end + +function WIREENT:_WMI_ApplyWireDupeData(ply, wireDupeInfo, createdEntities, tmpPorts) + self:_WMI_AddTmpPorts(tmpPorts) + + local lookupFunc = GetEntityLookupFunction(createdEntities) + WireLib.ApplyDupeInfo(ply, self, wireDupeInfo, lookupFunc) +end + +function WIREENT:_WMI_MapCreationIdDuped() + local wmidata = self._WireMapInterfaceEnt_Data + + if wmidata then + local dupe = wmidata.dupe + + if dupe and dupe.mapId then + return dupe.mapId + end + end + + local mapId = self._WireMapInterfaceEnt_MapId + if mapId then + return mapId + end + + return self:MapCreationID() +end + +function WIREENT:_WMI_GetSpawnIdDuped(interfaceEnt) + local wmidata = self._WireMapInterfaceEnt_Data + + if wmidata then + local dupe = wmidata.dupe + + if dupe and dupe.spawnId then + return dupe.spawnId + end + end + + return WIREENT._WMI_GetSpawnId(self, interfaceEnt) +end + +function WIREENT:_WMI_GetSpawnId(interfaceEnt) + if self._WireMapInterfaceEnt_SpawnId then + return self._WireMapInterfaceEnt_SpawnId + end + + self._WireMapInterfaceEnt_SpawnId = nil + + if not IsValid(interfaceEnt) then + local interfaceEnt = self._WireMapInterfaceEnt + + if not IsValid(interfaceEnt) then + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return nil + end + + local dupe = wmidata.dupe + if not dupe then + return nil + end + + interfaceEnt = dupe.interfaceEnt + if not IsValid(interfaceEnt) then + return nil + end + end + end + + local id = interfaceEnt:HashSubEntityMapId( + self:EntIndex(), + WIREENT._WMI_MapCreationIdDuped(self) + ) + + self._WireMapInterfaceEnt_SpawnId = id + + return id end -- Remove its overrides -function WIREENT:_RemoveOverrides() - for k, v in pairs(self._Overrides_WireMapInterfaceEnt or {}) do - self[k] = v +function WIREENT:_WMI_RemoveOverrides(interfaceEnt) + local wmidata = self._WireMapInterfaceEnt_Data + + if not IsValid(interfaceEnt) then + interfaceEnt = self._WireMapInterfaceEnt + end + + self:RemoveCallOnRemove("WireMapInterface_OnRemove") + duplicator.ClearEntityModifier(self, "WireDupeInfo") + duplicator.ClearEntityModifier(self, "WireMapInterfaceEntDupeInfo") + + local spawnId = WIREENT._WMI_GetSpawnId(self, interfaceEnt) + local spawnIdDuped = WIREENT._WMI_GetSpawnIdDuped(self, interfaceEnt) + local mapId = WIREENT._WMI_MapCreationIdDuped(self) + + WireLib.UnregisterWireMapInterfaceSpawnId(spawnId, spawnIdDuped, mapId, self) + WireLib.Remove(self) + + if IsValid(interfaceEnt) then + interfaceEnt:UnregisterWireEntityInternal(self) end - self._Overrides_WireMapInterfaceEnt = nil - for k, _ in pairs(self._Added_WireMapInterfaceEnt or {}) do + for k, v in pairs(g_memberBlacklist) do self[k] = nil end - self._Added_WireMapInterfaceEnt = nil - for key, value in pairs(self._Settings_WireMapInterfaceEnt or {}) do - if (not value or (value == 0) or (value == "")) then - self[key] = nil + -- Once we have a mapId, keep it forever, in case this entity is re-added to WMI again. + self._WireMapInterfaceEnt_MapId = mapId + + if not wmidata then + return + end + + local oldMembers = wmidata.oldMembers + if oldMembers then + for memberName, oldMember in pairs(oldMembers) do + if not g_memberBlacklist[memberName] then + self[memberName] = oldMember + end + end + end + + local oldSettings = wmidata.oldSettings + if oldSettings then + if not oldSettings.m_tblToolsAllowed then + self.m_tblToolsAllowed = false + else + self.m_tblToolsAllowed = oldSettings.m_tblToolsAllowed + end + + if not oldSettings.PhysgunDisabled then + self.PhysgunDisabled = nil else - self[key] = value + self.PhysgunDisabled = oldSettings.PhysgunDisabled end end - self._Settings_WireMapInterfaceEnt = nil - self._WireMapInterfaceEnt = nil - self._RemoveOverride = nil + table.Empty(wmidata) end -- Adds its overrides -function ENT:OverrideEnt(Entity) - Entity._Overrides_WireMapInterfaceEnt = Entity._Overrides_WireMapInterfaceEnt or {} - Entity._Added_WireMapInterfaceEnt = Entity._Added_WireMapInterfaceEnt or {} - - for k, v in pairs(WIREENT) do - if ((Entity[k] == nil) or (k == "_Overrides_WireMapInterfaceEnt") or (k == "_Added_WireMapInterfaceEnt") or (k == "_Settings_WireMapInterfaceEnt")) then - Entity._Overrides_WireMapInterfaceEnt[k] = nil - Entity._Added_WireMapInterfaceEnt[k] = true +function WIREENT:_WMI_OverrideEnt(interfaceEnt) + self._WMI_RemoveOverrides = WIREENT._WMI_RemoveOverrides + + local wmidata = self._WireMapInterfaceEnt_Data or {} + self._WireMapInterfaceEnt_Data = wmidata + + self._IsWireMapInterfaceSubEntity = true + + self.WireDebugName = string.format( + "Wire Map Interface [%s]", + self:GetClass() + ) + + local spawnId = WIREENT._WMI_GetSpawnId(self, interfaceEnt) + local spawnIdDuped = WIREENT._WMI_GetSpawnIdDuped(self, interfaceEnt) + local mapId = WIREENT._WMI_MapCreationIdDuped(self) + + self._WireMapInterfaceEnt_MapId = mapId + + WireLib.RegisterWireMapInterfaceSpawnId(spawnId, spawnIdDuped, mapId, self) + + local oldMembers = wmidata.oldMembers or {} + wmidata.oldMembers = oldMembers + + local oldMethods = wmidata.oldMethods or {} + wmidata.oldMethods = oldMethods + + for memberName, newMember in pairs(WIREENT) do + local oldMember = self[memberName] + + if not g_memberBlacklist[memberName] and oldMember ~= newMember then + oldMembers[memberName] = oldMember + + if not oldMember or isfunction(oldMember) then + oldMethods[memberName] = oldMember + end + + self[memberName] = newMember + end + end + + self:CallOnRemove("WireMapInterface_OnRemove", function(this) + if this._WMI_RemoveOverrides then + this:_WMI_RemoveOverrides() + end + end) +end + +function WIREENT:_WMI_AddPorts(wireInputRegister, wireOutputRegister) + if not IsValid(self._WireMapInterfaceEnt) then return end + + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return + end + + if wireInputRegister and wireInputRegister:hasPorts() then + local split = wireInputRegister.wire + + wmidata.inputs = table.Copy(split) + wmidata.inputs.descs = nil + + if not self.Inputs then + self.Inputs = WireLib.CreateSpecialInputs(self, split.names, split.types, split.descs) + else + self.Inputs = WireLib.AdjustSpecialInputs(self, split.names, split.types, split.descs) + end + + self._WireMapInterfaceEnt_TmpPorts = nil + self._WireMapInterfaceEnt_HasPorts = true + end + + if wireOutputRegister and wireOutputRegister:hasPorts() then + local split = wireOutputRegister.wire + + wmidata.outputs = table.Copy(split) + wmidata.outputs.descs = nil + + if not self.Outputs then + self.Outputs = WireLib.CreateSpecialOutputs(self, split.names, split.types, split.descs) else - Entity._Overrides_WireMapInterfaceEnt[k] = v - Entity._Added_WireMapInterfaceEnt[k] = nil + self.Outputs = WireLib.AdjustSpecialOutputs(self, split.names, split.types, split.descs) end - Entity[k] = v + + self._WireMapInterfaceEnt_TmpPorts = nil + self._WireMapInterfaceEnt_HasPorts = true + end +end + +function WIREENT:_WMI_AddTmpPorts(tmpPorts) + -- Add Ports from dupe as a dummy, so connections will not get lost when pasted too early. + if not tmpPorts then + return + end + + if self._WireMapInterfaceEnt_HasPorts then + return + end + + local inputs = tmpPorts.inputs + local outputs = tmpPorts.outputs + + if inputs then + if not self.Inputs then + self.Inputs = WireLib.CreateSpecialInputs(self, inputs.names, inputs.types) + else + self.Inputs = WireLib.AdjustSpecialInputs(self, inputs.names, inputs.types) + end + + self._WireMapInterfaceEnt_TmpPorts = true + end + + if outputs then + if not self.Outputs then + self.Outputs = WireLib.CreateSpecialOutputs(self, outputs.names, outputs.types) + else + self.Outputs = WireLib.AdjustSpecialOutputs(self, outputs.names, outputs.types) + end + + self._WireMapInterfaceEnt_TmpPorts = true + end +end + +function WIREENT:_WMI_SetInterface(interfaceEnt) + local wmidata = self._WireMapInterfaceEnt_Data + if not wmidata then + return + end + + local oldSettings = wmidata.oldSettings or {} + wmidata.oldSettings = oldSettings + + -- Only apply tool/physgun limits if the entity was spawned by the map. + -- This prevents impossible-to-remove spam by e.g. rigged dupes. + local isCreatedByMap = self:CreatedByMap() + + -- Protect in-/output entities from non-wire tools + if not self.m_tblToolsAllowed then + oldSettings.m_tblToolsAllowed = false + else + oldSettings.m_tblToolsAllowed = table.Copy(self.m_tblToolsAllowed) + end + + if interfaceEnt:FlagGetProtectFromTools() and isCreatedByMap then + self.m_tblToolsAllowed = self.m_tblToolsAllowed or {} + table.Add(self.m_tblToolsAllowed, g_wireTools) end + -- Protect in-/output entities from the physgun + oldSettings.PhysgunDisabled = self.PhysgunDisabled or false + + if interfaceEnt:FlagGetProtectFromPhysgun() and isCreatedByMap then + -- Still save the old values for restore, but do not change them if run time created/duped. + self.PhysgunDisabled = true + end - Entity._Settings_WireMapInterfaceEnt = Entity._Settings_WireMapInterfaceEnt or {} + self._WireMapInterfaceEnt = interfaceEnt + self._IsWireMapInterfaceSubEntity = true +end - if (bit.band(self.flags, 1) > 0) then -- Protect in-/output entities from non-wire tools - Entity._Settings_WireMapInterfaceEnt.m_tblToolsAllowed = Entity.m_tblToolsAllowed or false - Entity.m_tblToolsAllowed = {"wire", "wire_adv", "wire_debugger", "wire_wirelink", "gui_wiring", "multi_wire"} +function ENT:OverrideEnt(wireEnt) + if not IsValid(wireEnt) then + return end - if (bit.band(self.flags, 2) > 0) then -- Protect in-/output entities from the physgun - Entity._Settings_WireMapInterfaceEnt.PhysgunDisabled = Entity.PhysgunDisabled or false - Entity.PhysgunDisabled = true + if not wireEnt._IsWireMapInterfaceSubEntity then + WIREENT._WMI_OverrideEnt(wireEnt, self) end - Entity._WireMapInterfaceEnt = self + if not IsValid(self._WireMapInterfaceEnt) and wireEnt._WMI_SetInterface then + wireEnt:_WMI_SetInterface(self) + end end + +local function OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo, interfaceEnt) + if not IsValid(wireEnt) then + return + end + + if wireEnt._IsWireMapInterfaceSubEntity then + -- Already initialized + return + end + + local mapId = wireMapInterfaceEntDupeInfo.mapId + local entIdx = wireMapInterfaceEntDupeInfo.entIdx + local spawnId = wireMapInterfaceEntDupeInfo.spawnId + local interface = wireMapInterfaceEntDupeInfo.interface + + if not IsValid(interfaceEnt) then + interfaceEnt = WIREENT._WMI_FindWireMapInterfaceEnt(wireEnt, interface) + + if not IsValid(interfaceEnt) then + WIREENT._WMI_RemoveOverrides(wireEnt) + return + end + end + + if not interfaceEnt:IsWireableEntity(wireEnt) then + WIREENT._WMI_RemoveOverrides(wireEnt, interfaceEnt) + return + end + + if interfaceEnt:HashSubEntityMapId(entIdx, mapId) ~= spawnId then + WIREENT._WMI_RemoveOverrides(wireEnt, interfaceEnt) + return + end + + local wmidata = wireEnt._WireMapInterfaceEnt_Data or {} + wireEnt._WireMapInterfaceEnt_Data = wmidata + + local dupe = wmidata.dupe or {} + wmidata.dupe = dupe + + dupe.mapId = mapId + dupe.entIdx = entIdx + dupe.spawnId = spawnId + dupe.interfaceEnt = interfaceEnt + + -- Modify the entity like the Wire Map Interface does, but without attaching it yet. It's needed for wireEnt:PostEntityPaste() to be run properly. + -- The attachment is done in wireEnt:PostEntityPaste(), because it has the CreatedEntities table Wiremod needs. + WIREENT._WMI_OverrideEnt(wireEnt, interfaceEnt) + + -- Ensure that the temporary ports are added super early, so other wire entities can connect to them as soon as they are duped. + wireEnt:_WMI_AddTmpPorts(wireMapInterfaceEntDupeInfo.tmpPorts) +end + +function ENT:OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo) + OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo, self) +end + +duplicator.RegisterEntityModifier("WireMapInterfaceEntDupeInfo", function(ply, wireEnt, wireMapInterfaceEntDupeInfo) + -- Make sure the Wire Map Interface sub entity when duped, so it can be found and linked too. + OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo) +end) + +hook.Add("Wire_ApplyDupeInfo", "Wire_InitFromWireMapInterfaceEntDupeInfo", function(ply, inputEnt, outputEnt, inputData) + -- Make sure we initialize the Wire Map Interface sub entity before connecting our inputs to it. It will not initialize twice. + + local entityMods = inputEnt.EntityMods + if entityMods then + local wireMapInterfaceEntDupeInfo = entityMods.WireMapInterfaceEntDupeInfo + + if wireMapInterfaceEntDupeInfo then + OverrideEntFromDupe(inputEnt, wireMapInterfaceEntDupeInfo) + end + end + + local entityMods = outputEnt.EntityMods + if entityMods then + local wireMapInterfaceEntDupeInfo = entityMods.WireMapInterfaceEntDupeInfo + + if wireMapInterfaceEntDupeInfo then + OverrideEntFromDupe(outputEnt, wireMapInterfaceEntDupeInfo) + end + end +end) + diff --git a/lua/entities/info_wiremapinterface/gmodoutputs.lua b/lua/entities/info_wiremapinterface/gmodoutputs.lua new file mode 100644 index 0000000000..1439e7256a --- /dev/null +++ b/lua/entities/info_wiremapinterface/gmodoutputs.lua @@ -0,0 +1,75 @@ +-- Modified gmod base entity hammer outout code to extend it by custom functionalities. + +function ENT:FireSingleOutput(outputName, output, activator, data) + if output.times == 0 then + return false + end + + local targetName = output.entities + local entitiesToFire = nil + + if targetName == "!activator" then + entitiesToFire = {activator} + else + entitiesToFire = self:GetEntitiesByTargetnameOrClass(targetName) + end + + local params = output.param or "" + + if params == "" then + params = data or "" + end + + local inputName = output.input + local remove = false + + if entitiesToFire then + for _, ent in ipairs(entitiesToFire) do + if self:ProtectAgainstDangerousIO(ent, outputName, output, data) then + if self:IsLuaRunEntity(ent) then + self:PrepairEntityForFire(ent, outputName) + ent:Fire(inputName, params, output.delay, activator, self) + else + ent:Fire(inputName, params, output.delay, activator, self) + end + else + -- Remove the unsafe IO, to prevent error message spam. + remove = true + end + end + end + + if output.times ~= -1 then + output.times = output.times - 1 + end + + if remove then + return false + end + + return (output.times > 0) or (output.times == -1) +end + +-- This function is used to trigger an output. +function ENT:TriggerOutput(outputName, activator, data) + if not self.m_tOutputs then + return + end + + local outputNameLower = string.lower(outputName) + local outputList = self.m_tOutputs[outputNameLower] + + if not outputList then + return + end + + for idx = #outputList, 1, -1 do + local output = outputList[idx] + + if output and not self:FireSingleOutput(outputName, output, activator, data) then + -- Shift the indexes so this loop doesn't fail later + table.remove(outputList, idx) + end + end +end + diff --git a/lua/entities/info_wiremapinterface/init.lua b/lua/entities/info_wiremapinterface/init.lua index 794f211f93..82bf8391b1 100644 --- a/lua/entities/info_wiremapinterface/init.lua +++ b/lua/entities/info_wiremapinterface/init.lua @@ -1,462 +1,628 @@ --[[ -This is the wire map interface entity by Grocel. (info_wiremapinterface) +This is the wire map interface entity. (info_wiremapinterface) This point entity allows you to give other entities wire in-/outputs. Those wire ports allows you to control thinks on the map with Wiremod or to let the map return thinks to wire outputs. -It supports many datatypes and custom lua codes. -A lua code is run when its input triggers. -It has special globals: - WIRE_NAME = Input name - WIRE_VALUE = Input value - WIRE_WIRED = Is the input wired? - WIRE_CALLER = This entity - WIRE_ACTIVATOR = The entity that has the input +It supports many datatypes. +In case it triggers a lua_run entity it temporarily applies these special globals to the Lua environment: + WIRE_NAME -- Input name + WIRE_TYPE -- Input type (NORMAL, STRING, VECTOR, etc.) + WIRE_VALUE -- Input value + WIRE_WIRED -- Is the input wired? + WIRE_CALLER -- This entity + WIRE_ACTIVATOR -- The entity that has the Wire input + WIRE_DEVICE -- The entity where the input data was from, e.g. a Wiremod button + WIRE_OWNER -- The owner of the input device, e.g the player who spawned the Wiremod button +]] -Keep in mind that you have to know what you do -and that you have to activate spawnflag 8 to make it work. -Spawnflag 8 is better known as "Run given Lua codes (For advanced users!)" in the Hammer Editor. +local WireAddon = WireAddon +local WireLib = WireLib -Please don't change things unless you know what you do. You may break maps if do something wrong. -]] +ENT.Base = "base_point" +ENT.Type = "point" + +ENT.Spawnable = false +ENT.AdminOnly = true + +-- Wire Map Interfaces (info_wiremapinterface) should not be allowed to be duplicated/saved. +-- We use an info_wiremapinterface_savestate for that. +ENT.DisableDuplicator = true +ENT.DoNotDuplicate = true +ENT.IsWireMapInterface = nil + +if not WireAddon then + return +end include("convert.lua") include("entitycontrol.lua") include("entityoverride.lua") +include("gmodoutputs.lua") +include("io.lua") +include("networking.lua") +include("savestate.lua") + +ENT.IsWireMapInterface = true + +-- This entity supports more than the 8 ports you see in the editor. This value is the port limit. +ENT.MAX_PORTS = 255 + +-- Minimum delay between think calls. +ENT.MIN_THINK_TIME = 0.25 + +local cvar_allow_interface = CreateConVar( + "sv_wire_mapinterface", + "1", + {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_GAMEDLL}, + "Enable or disable all Wire Map Interface entities. Default: 1", + 0, + 1 +) + +-- Minimum time between Wiremod input triggers. +local cvar_min_trigger_time = CreateConVar( + "sv_wire_mapinterface_min_trigger_time", + "0.01", + {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_GAMEDLL}, + "Sets minimum time between Wiremod input triggers per Wire Map Interface entity and Wiremod input. Default: 0.01", + 0, + 1 +) + +-- The maximum number of entities per interface entitiy that can get wire ports +local cvar_max_sub_entities = CreateConVar( + "sv_wire_mapinterface_max_sub_entities", + "32", + {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_GAMEDLL}, + "Sets maximum count of sub entities per Wire Map Interface entity that can get wire ports. Default: 32", + 1, + 128 +) + +local g_classBlacklist = { + lua_run = true, -- No interface for lua_run! + func_water = true, -- No interface for water! + func_water_analog = true, -- No interface for water! + info_wiremapinterface = true, -- No interface for other Wire Map Interfaces! + info_wiremapinterface_savestate = true, -- No interface for Wire Map Interfaces savestate helper! + func_illusionary = true, -- No interface for Non-Solid +} + +local g_classBlacklistPatterns = { + "^(item_[%w_]+)", -- No interface for items! + "^(info_[%w_]+)", -- No interface for info entities! + "^(trigger_[%w_]+)", -- No interface for trigger entities! +} -local ALLOW_INTERFACE = CreateConVar("sv_wire_mapinterface", "1", {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_GAMEDLL}, "Aktivate or deaktivate the wire map interface. Default: 1") - - -local Ents = {} -hook.Add("PlayerInitialSpawn", "WireMapInterface_PlayerInitialSpawn", function(ply) - if (not IsValid(ply)) then return end - for Ent, time in ipairs(Ents) do - if (not IsValid(Ent)) then break end +-- This checks if you can give an entity wiremod abilities +function ENT:IsWireableEntity(ent) + if not IsValid(ent) then + -- No interface for invalid entities! + return false + end - timer.Simple(time + 0.1, function() - if (not IsValid(ply)) then return end - if (not IsValid(Ent)) then return end - if (not Ent.GiveWireInterfeceClient) then return end + if IsValid(ent._WireMapInterfaceEnt) and ent._WireMapInterfaceEnt ~= self then + -- Only one interface per entity! + return false + end - Ent:GiveWireInterfeceClient(ply) - end) + local hasPorts = WireLib.HasPorts(ent) or ent.IsWire or ent.Inputs or ent.Outputs + if not IsValid(ent._WireMapInterfaceEnt) and not ent._WireMapInterfaceEnt_TmpPorts and hasPorts then + -- Don't destroy wiremod entites! + return false end -end) --- This is a point entity -ENT.Base = "base_point" -ENT.Type = "point" + if not IsValid(ent:GetPhysicsObject()) then return false end -- Only entities with physics can get an interface! + if ent:GetPhysicsObjectCount() ~= 1 then return false end -- Only entities with single bone physics can get an interface! -local MAX_PORTS = 256 -- This entity supports more than the 8 ports you see in the editor. This value is the port limit. -local MAX_ENTITIES = 32 -- The maximum number of entities per interface entitiy that can get wire ports -local MIN_TRIGGER_TIME = 0.01 -- Minimum triggering Time for in- and outputs. + if ent:IsWorld() then return false end -- No interface for the worldspawn! + if ent:IsVehicle() then return false end -- No interface for vehicles! + if ent:IsNPC() then return false end -- No interface for NPCs! + if ent:IsPlayer() then return false end -- No interface for players! + if ent:IsWeapon() then return false end -- No interface for weapons! + if ent:IsConstraint() then return false end -- No interface for constraints! + if ent:IsRagdoll() then return false end -- No interface for ragdolls! --- This checks if you can give an entity wiremod abilities -function ENT:IsWireableEntity(Entity) - if (not IsValid(Entity)) then return false end -- No interface for invalid entities! - if (IsValid(Entity._WireMapInterfaceEnt) and (Entity._WireMapInterfaceEnt ~= self)) then return false end -- Only one interface per entity! - if (not IsValid(Entity._WireMapInterfaceEnt) and (WireLib.HasPorts(Entity) or Entity.IsWire or Entity.Inputs or Entity.Outputs)) then return false end -- Don't destroy wiremod entites! - - if (Entity:IsWorld()) then return false end -- No interface for the worldspawn! - if (Entity:IsVehicle()) then return false end -- No interface for vehicles! - if (Entity:IsNPC()) then return false end -- No interface for NPCs! - if (Entity:IsPlayer()) then return false end -- No interface for players! - if (Entity:IsWeapon()) then return false end -- No interface for weapons! - if (string.match(Entity:GetClass(), "^(item_[%w_]+)")) then return false end -- No interface for items! - if (Entity:IsConstraint()) then return false end -- No interface for constraints! - - if (Entity:GetPhysicsObjectCount() > 1) then return false end -- No interface for ragdolls! - if (IsValid(Entity:GetPhysicsObject())) then return true end -- Everything with a single physics object can get an interface! + local class = ent:GetClass() - return true -end + if g_classBlacklist[class] then + return false + end -function ENT:CheckEntLimid(CallOnMax, ...) - if (self.WireEntsCount > MAX_ENTITIES) then - MsgN(self.ErrorName..": Warning, to many wire entities linked!") - if (CallOnMax) then - CallOnMax(self, ...) + for _, pattern in ipairs(g_classBlacklistPatterns) do + if string.match(class, pattern) then + return false end - return false end - self.WireEntsCount = self.WireEntsCount + 1 + return true end --- Run the given lua code -local function RunLua(I, name, value, wired, self, Ent) - local lua = self.Ins[I].lua or "" - if ((lua == "") or not self.RunLuaCode) then return end - - local func = CompileString(lua, self.ErrorName.." (Input "..I..")", false) - local Err - if isfunction(func) then - -- Globals - WIRE_NAME = name -- Input name - WIRE_VALUE = value -- Input value - WIRE_WIRED = wired -- Is the input wired? - WIRE_CALLER = self -- This entity - WIRE_ACTIVATOR = Ent -- The entity that has the input - - local status, err = xpcall(func, debug.traceback) - if (not status) then - Err = err or "" - end +local g_warningColor = Color(255, 100, 100) - -- Remove globals - WIRE_NAME = nil - WIRE_VALUE = nil - WIRE_WIRED = nil - WIRE_CALLER = nil - WIRE_ACTIVATOR = nil - else - Err = func +function ENT:FormatEntityString(ent) + local entString = tostring(ent or NULL) + + if IsValid(ent) then + local name = ent:GetName() or "" + return string.format("%s[%s]", entString, name) end - if (Err and (Err ~= "")) then - ErrorNoHalt(Err.."\n") + return entString +end + +function ENT:FormatString(message, ...) + message = tostring(message or "") + + if message == "" then + return "" end + + local entString = self:FormatEntityString(self) + + message = string.format("Wire Map Interface: %s" .. message, entString, ...) + return message end --- Wire input -function ENT:TriggerWireInput(name, value, wired, Ent) - if (not WireAddon) then return end - if (not IsValid(Ent)) then return end - if ((not self.Active or not ALLOW_INTERFACE:GetBool()) and wired) then - self.SavedIn = self.SavedIn or {} - self.SavedIn[name] = {value, wired, Ent} +function ENT:PrintWarning(message, ...) + message = self:FormatString(message, ...) + if message == "" then return end - self.Wired = self.Wired or {} - self.Wired[Ent] = self.Wired[Ent] or {} - local WireRemoved = ((self.Wired[Ent][name] or false) ~= wired) and not wired - self.Wired[Ent][name] = wired - - self.Timer = self.Timer or {} - self.Timer.In = self.Timer.In or {} - if (((CurTime() - (self.Timer.In[name] or 0)) < (self.min_trigger_time or MIN_TRIGGER_TIME)) and not WireRemoved) then return end - self.Timer.In[name] = CurTime() - - self.Data = self.Data or {} - self.Data.In = self.Data.In or {} - if ((self.Data.In[name] == value) and not WireRemoved) then return end - self.Data.In[name] = value - - local I = self.InsIDs[name] or 0 - if ((I > 0) and (I <= MAX_PORTS) and self.InsExist[I]) then - local _, Convert, Toggle = self:Convert_WireToMap(self.Ins[I].type) - if (not Convert) then return end - local Output = "onwireinput"..I - - -- Map output - if (not wired) then - if (WireRemoved) then - if (not Toggle) then - self:TriggerOutput(Output, Ent, Convert(value)) - end - self:TriggerOutput("onresetwireinput"..I, Ent) + MsgC(g_warningColor, message, "\n") +end - RunLua(I, name, value, wired, self, Ent) - end - else - if (Toggle) then - if (Convert(value)) then - self:TriggerOutput(Output, Ent) +function ENT:CheckEntLimit(count, ent) + local maxSubEntities = self:GetMaxSubEntities() - RunLua(I, name, value, wired, self, Ent) - end - else - self:TriggerOutput(Output, Ent, Convert(value)) + if count >= maxSubEntities then + self:PrintWarning(": Warning, limit of %d linked wire entities reached! Can not add: %s ", maxSubEntities, self:FormatEntityString(ent)) + return false + end - RunLua(I, name, value, wired, self, Ent) - end + return true +end + +function ENT:CheckPortIdLimit(portId, warn) + portId = tonumber(portId or 0) or 0 + + if portId == 0 then + return false + end + + if portId > self.MAX_PORTS or portId < 0 then + if warn then + self:PrintWarning(": Warning, invaid portId given. Expected 0 < portId < %d, got %d!", self.MAX_PORTS, portId) end + + return false end + + return true end --- Wire output -function ENT:TriggerWireOutput(ent, i, val) - if (not IsValid(ent)) then return false end +function ENT:FlagGetProtectFromTools() + if not self:CreatedByMap() then + return false + end - local OutputName = self.Outs[i].name or "" - if (OutputName == "") then return false end + local flags = self:GetSpawnFlags() + return bit.band(flags, 1) == 1 -- Protect in-/output entities from non-wire tools +end - local _, Convert, Toggle = self:Convert_MapToWire(self.Outs[i].type) - if (not Convert) then return false end +function ENT:FlagGetProtectFromPhysgun() + if not self:CreatedByMap() then + return false + end - if (Toggle) then - Wire_TriggerOutput(ent, OutputName, Convert(self, ent, i)) - else - Wire_TriggerOutput(ent, OutputName, Convert(val)) + local flags = self:GetSpawnFlags() + return bit.band(flags, 2) == 2 -- Protect in-/output entities from the physgun +end + +function ENT:FlagGetRemoveEntities() + local flags = self:GetSpawnFlags() + return bit.band(flags, 4) == 4 -- Remove in-/output entities on remove +end + +-- Note: bit.band(flags, 8) == 8 Was used for running lua code. It must be left unused as could cause unexpected side effects on older maps. + +function ENT:FlagGetStartActive() + local flags = self:GetSpawnFlags() + return bit.band(flags, 16) == 16 -- Start Active +end + +function ENT:FlagGetRenderWires() + local flags = self:GetSpawnFlags() + return bit.band(flags, 32) == 32 -- Start Active +end + +function ENT:Initialize() + self.Active = self:FlagGetStartActive() + self.oldIsActive = self:IsActive() + + self.WireEntsRegister = self.WireEntsRegister or {} + self.WireEntName = self.WireEntName or "" + + self.WireInputRegisterTmp = self.WireInputRegisterTmp or {} + self.WireOutputRegisterTmp = self.WireOutputRegisterTmp or {} + + self.WireInputTriggerBuffer = self.WireInputTriggerBuffer or {} + + self.PortsUpdated = true + + self.NextNetworkTime = CurTime() + (1 + math.random() * 2) * (self.MIN_THINK_TIME * 4) + self:AttachToSaveStateEntity() +end + +function ENT:OnReloaded() + -- Easier for debugging. + self:InitNetworking() + self:AttachToSaveStateEntity() +end + +function ENT:IsActive() + return self.Active and cvar_allow_interface:GetBool() +end + +function ENT:GetMinTriggerTime() + local minTriggerTime = math.max( + self.MinTriggerTime or 0, + cvar_min_trigger_time:GetFloat(), + 0 + ) + + minTriggerTime = math.min(minTriggerTime, 1) + return minTriggerTime +end + +function ENT:GetMaxSubEntities() + local maxSubEntities = math.Clamp( + cvar_max_sub_entities:GetInt(), + 1, + 32 + ) + + return maxSubEntities +end + +function ENT:IsLuaRunEntity(ent) + if not IsValid(ent) then + return false end - return true + + return ent:GetClass() == "lua_run" end --- Map input -function ENT:AcceptInput(name, activator, caller, data) - if (not WireAddon) then return false end - name = string.lower(tostring(name or "")) - if (name == "") then return false end +function ENT:ProtectAgainstDangerousIO(targetEnt, outputName, output, data) + local outputNameLower = string.lower(outputName) - if (name == "activate") then - self.Active = true + if not string.StartsWith(outputNameLower, "onwireinput") then + -- This protection is only relevant for Hammer outputs linked to Wire inputs. return true end - if (name == "deactivate") then - self.Active = false + local inputName = output.input + local inputNameLower = string.lower(inputName) + + local params = output.param or "" + if params ~= "" then + -- We would run code from the override parameter set via Hammer, so it is safe. There is no direct user input. return true end - if (name == "toggle") then - self.Active = not self.Active - return true + if inputNameLower == "addoutput" then + -- This can be abused to do all sorts of stuff, so don't allow AddOutput from user input. This could even run unauthorized Lua code. + -- Warn the mapper about their mistake. + self:PrintWarning( + ", Hammer output '%s' -> Hammer input '%s@%s': Dangerous operation!\n Do not trigger AddOutput with user input.\n Change this trigger or use the override parameter instead.\n This trigger has been blocked and removed.", + outputName, + self:FormatEntityString(targetEnt), + inputName + ) + + return false end - if (self.Active and ALLOW_INTERFACE:GetBool()) then - local pattern = "(%d+)" - local I = tonumber(string.match(name, "triggerwireoutput"..pattern)) or 0 + if self:IsLuaRunEntity(targetEnt) and inputNameLower == "runpassedcode" then + -- Prevent an potential RCE: Block direct user input from being run as unauthorized Lua code. + -- Warn the mapper about their mistake. + self:PrintWarning( + ", Hammer output '%s' -> Hammer input '%s@%s': Dangerous operation!\n Do not run Lua code with user input!\n This would allow players to take over the Server.\n Use the override parameter or trigger RunCode instead.\n This trigger has been blocked and removed.", + outputName, + self:FormatEntityString(targetEnt), + inputName + ) - if I > 0 and I <= MAX_PORTS and self.OutsExist[I] then - self.Timer = self.Timer or {} - self.Timer.Out = self.Timer.Out or {} + return false + end - if ((CurTime() - (self.Timer.Out[name] or 0)) < (self.min_trigger_time or MIN_TRIGGER_TIME)) then return false end - self.Timer.Out[name] = CurTime() + return true +end - -- Wire output - for Ent, _ in pairs(self.WireEnts or {}) do - self:TriggerWireOutput(Ent, I, data) - end +function ENT:GetEntitiesByTargetnameOrClass(nameOrClass) + nameOrClass = tostring(nameOrClass or "") + + if nameOrClass == "" then + return nil + end + + if nameOrClass == "!null" then + -- Non-empty string for void entity. + return nil + end - return true + if nameOrClass == "!player" then + -- All players + + local players = player.GetAll() + if #players > 0 then + return players end + + return nil end - if (not self.WirePortsChanged) then return false end + if nameOrClass[1] == "!" then + local ent = self:GetFirstEntityByTargetnameOrClass(nameOrClass) + if not IsValid(ent) then + return nil + end - if (name == "addentity") then - local Ent, Func = self:AddSingleEntity(caller) + return {ent} + end - if (not IsValid(Ent) or not Func) then return false end - timer.Simple(0.02, function() Func( self, Ent, nil, true) end) + local byName = ents.FindByName(nameOrClass) + if #byName > 0 then + return byName + end - self:TriggerOutput("onwireentscreated", self) - self:TriggerOutput("onwireentsready", self) - return true + local byClass = ents.FindByClass(nameOrClass) + if #byClass > 0 then + return byClass end - if (name == "removeentity") then - self:RemoveSingleEntity(caller) - return true + return nil +end + +function ENT:GetFirstEntityByTargetnameOrClass(nameOrClass) + nameOrClass = tostring(nameOrClass or "") + + if nameOrClass == "" then + return nil end - if (name == "addentities") then - self:AddEntitiesByName(data) - return true + if nameOrClass == "!null" then + -- Non-empty string for void entity. + return nil end - if (name == "removeentities") then - self:RemoveEntitiesByName(data) - return true + if nameOrClass == "!self" then + -- This entity. + return self end - if (name == "removeallentities") then - self:RemoveAllEntities() - return true + if nameOrClass == "!player" then + -- First Player. + + local players = player.GetAll() + if #players > 0 then + local first = next(players) + if not IsValid(first) then + return nil + end + + return first + end + + return nil end - return false -end + if nameOrClass == "!caller" then + -- The last entity that called an Hammer input, + -- e.g a trigger_multiple brush. -function ENT:KeyValue(key, value) - if (not WireAddon) then return end + local lastCaller = self._lastCaller + if not IsValid(lastCaller) then + return nil + end - key = string.lower(tostring(key or "")) - value = tostring(value or "") + return lastCaller + end + + if nameOrClass == "!activator" then + -- The last entity that triggered !caller to call an Hammer input, + -- e.g. a player that passed though a trigger_multiple brush. - if ((key == "") or (value == "")) then return end + local lastActivator = self._lastActivator + if not IsValid(lastActivator) then + return nil + end - local pattern = "(%d+)" - local I = tonumber(string.match(key, "onwireinput"..pattern)) or 0 - if ((I > 0) and (I <= MAX_PORTS)) then - self:StoreOutput(key, value) + return lastActivator end - local I = tonumber(string.match(key, "onresetwireinput"..pattern)) or 0 - if ((I > 0) and (I <= MAX_PORTS)) then - self:StoreOutput(key, value) + if nameOrClass == "!input" then + -- The entity that has the Wire input. (!activator in Hammer Output) + + local lastWireInputEnt = self._lastWireInputEnt + if not IsValid(lastWireInputEnt) then + return nil + end + + return lastWireInputEnt end - if ((key == "onwireentscreated") or (key == "onwireentsremoved") or - (key == "onwireentsready") or (key == "onwireentsstartchanging")) then - self:StoreOutput(key, value) + if nameOrClass == "!device" then + -- The entity where the input data was from, e.g. a Wiremod button. + + local lastWireActivatorEnt = self._lastWireDeviceEnt + if not IsValid(lastWireActivatorEnt) then + return nil + end + + return lastWireActivatorEnt end - if (key == "wire_entity_name") then - self.WireEntName = value + if nameOrClass == "!owner" then + -- The owner of !device the !input entity is connected to, + -- e.g. the player who spawned the Wiremod button. + + local lastOwner = self._lastWireDeviceEntOwner + if not IsValid(lastOwner) then + return nil + end + + return lastOwner end - if (key == "min_trigger_time") then - self.min_trigger_time = math.max(tonumber(value) or 0, MIN_TRIGGER_TIME) - end - - local pattern = "(%d+)_(%l+)" - local I, name = string.match(key, "input"..pattern) - local I, name = tonumber(I) or 0, tostring(name or "") - if ((I > 0) and (I <= MAX_PORTS) and (name ~= "")) then - self.Ins = self.Ins or {} - self.InsIDs = self.InsIDs or {} - self.InsExist = self.InsExist or {} - - self.Ins[I] = self.Ins[I] or {} - if (name == "lua") then - self.Ins[I][name] = value - elseif (name == "type") then - self.Ins[I][name] = tonumber(value) - elseif (name == "desc") then - self.Ins[I][name] = value - elseif (name == "name") then - self.InsIDs[value] = I - self.InsExist[I] = true - self.Ins[I][name] = value + local byName = ents.FindByName(nameOrClass) + if #byName > 0 then + local first = next(byName) + if not IsValid(first) then + return nil end + + return first end - local I, name = string.match(key, "output"..pattern) - local I, name = tonumber(I) or 0, tostring(name or "") - if I > 0 and I <= MAX_PORTS and name ~= "" then - self.Outs = self.Outs or {} - self.OutsExist = self.OutsExist or {} - - self.Outs[I] = self.Outs[I] or {} - if (name == "type") then - self.Outs[I][name] = tonumber(value) - elseif (name == "desc") then - self.Outs[I][name] = value - elseif (name == "name") then - self.OutsExist[I] = true - self.Outs[I][name] = value + local byClass = ents.FindByClass(nameOrClass) + if #byClass > 0 then + local first = next(byClass) + if not IsValid(first) then + return nil end + + return first end + + return nil end -local Count = 1 -function ENT:Initialize() - if (not WireAddon) then return end - self.WireEnts = self.WireEnts or {} - self.WireEntsCount = 0 - self.WireEntName = self.WireEntName or "" +function ENT:AcceptInput(name, activator, caller, data) + self._lastActivator = activator + self._lastCaller = caller - self:UpdateData() - self.Active = (bit.band(self.flags, 16) > 0) -- Start Active - self.oldActive = self.Active - self.old_ALLOW_INTERFACE_bool = ALLOW_INTERFACE:GetBool() + name = string.lower(tostring(name or "")) + if name == "" then return false end - local Name = self:GetName() or "" - local ErrorName = "Wire Map Interface: "..tostring(self) + if name == "activate" then + self.Active = true + return true + end - if (Name == "") then - self.ErrorName = ErrorName - else - self.ErrorName = ErrorName.."['"..Name.."']" + if name == "deactivate" then + self.Active = false + return true end + if name == "toggle" then + self.Active = not self.Active + return true + end - local time = Count * 0.3 + 0.5 + if self:TriggerHammerInput(name, data) then + return true + end - if (self.WireEntName == "") then - self.WirePortsChanged = true - else - timer.Simple(time, function() - if (not IsValid(self)) then return end - self.WirePortsChanged = true + return false +end + +function ENT:KeyValue(key, value) + key = string.lower(tostring(key or "")) + value = tostring(value or "") + + if key == "" then return end - self:AddEntitiesByName(self.WireEntName) - end) + if self:StoreHammerOutputs(key, value) then + return end - Ents[self] = time - Count = Count + 1 + if key == "wire_entity_name" then + local oldValue = self.WireEntName or "" + self.WireEntName = value + + self.WireEntNameUpdated = oldValue ~= value + return + end + + if key == "min_trigger_time" then + self.MinTriggerTime = math.max(tonumber(value or 0) or 0, 0) + return + end + + if self:RegisterWireIO(key, value) then + return + end end --- To cleanup and get the in-/outputs information. -local function SplitTable(tab, self) - if (not IsValid(self)) then return end +function ENT:Think() + local active = self:IsActive() + + if active ~= self.oldIsActive then + if active then + self:ApplyWireOutputBufferAll() + end - if (not tab) then return end - if (#tab == 0) then return end - local tab = table.Copy(tab) + self.oldIsActive = active + end - local allowlua = self.RunLuaCode + if active then + local wireInputTriggerBuffer = self.WireInputTriggerBuffer + local wireInputRegister = self.WireInputRegister + local now = CurTime() - local names, types, descs = {}, {}, {} - local Index = 0 + for uid, triggerStateData in pairs(wireInputTriggerBuffer) do + if wireInputRegister.byUid[uid] then + local inputData = triggerStateData.inputData + local wireValue = triggerStateData.wireValue + local wireEnt = triggerStateData.wireEnt - for i = 1, #tab do - local Port = tab[i] - if (Port) then - local name = Port.name -- The port name for checking - if (name) then -- Do not add ports with no names - Index = Index + 1 + local debounce = inputData.debounce + local nextTime = debounce.nextTime or 0 - names[Index] = name -- The port name - types[Index] = self:Convert_MapToWire(Port.type) -- The port type - descs[Index] = Port.desc -- The port description - if (not allowlua) then - tab[i].lua = nil -- remove lua codes if the lua mode isn't on. + if nextTime <= now then + self:TriggerHammerOutputFromWire(inputData, wireValue, wireEnt) + wireInputTriggerBuffer[uid] = nil end else - tab[i] = nil -- Resort and cleanup the given table for later using + wireInputTriggerBuffer[uid] = nil end end end - return names, types, descs, tab -end + self:HandleWireEntsUpdated() + self:HandleShouldNetworkEntities() + self:HandlePortsUpdated() + self:HandleWireEntNameUpdated() -function ENT:UpdateData() - self.flags = self:GetSpawnFlags() - self.RunLuaCode = (bit.band(self.flags, 8) > 0) -- Run given Lua codes + self:PollWirelinkStatus() - self.Inames, self.Itypes, self.Idescs, self.Ins = SplitTable(self.Ins, self) - self.Onames, self.Otypes, self.Odescs, self.Outs = SplitTable(self.Outs, self) + self:NextThink(CurTime() + self.MIN_THINK_TIME) + return true end -function ENT:Think() - if (not WireAddon) then return end - - local ALLOW_INTERFACE_bool = ALLOW_INTERFACE:GetBool() - if ((self.Active ~= self.oldActive) or (ALLOW_INTERFACE_bool ~= self.old_ALLOW_INTERFACE_bool)) then - if (self.Active and ALLOW_INTERFACE_bool) then - self.SavedIn = self.SavedIn or {} - for name, values in pairs(self.SavedIn) do - self:TriggerWireInput(name, unpack(values)) - self.SavedIn[name] = nil - end - end - self.oldActive = self.Active - self.old_ALLOW_INTERFACE_bool = ALLOW_INTERFACE_bool - end +function ENT:CanTool() + -- Make sure there is no way to mess around with tools on this entity. + -- It is not a traceable entity. + + return false end function ENT:OnRemove() - if (not WireAddon) then return end - - self.flags = self:GetSpawnFlags() - - if (bit.band(self.flags, 4) > 0) then -- Remove in-/output entities on remove - for obj1, obj2 in pairs(self.WireEnts or {}) do - local Entity = (IsEntity(obj1) and obj1) or (IsEntity(obj2) and obj2) + local wireEnts = self:GetWiredEntities() - if (IsValid(Entity)) then - SafeRemoveEntity(Entity) - end + if self:FlagGetRemoveEntities() then + for _, wireEnt in ipairs(wireEnts) do + SafeRemoveEntity(wireEnt) end else self:RemoveAllEntities() end + + table.Empty(wireEnts) end + diff --git a/lua/entities/info_wiremapinterface/io.lua b/lua/entities/info_wiremapinterface/io.lua new file mode 100644 index 0000000000..9fc66f517e --- /dev/null +++ b/lua/entities/info_wiremapinterface/io.lua @@ -0,0 +1,936 @@ +-- I/O code between Hammer and Wiremod + +local WireLib = WireLib + +local function newPortRegister() + local register = {} + + register.byPortId = {} + register.byName = {} + register.byUid = {} + register.sequence = {} + + register.wire = {} + register.wire.names = {} + register.wire.types = {} + register.wire.descs = {} + + register.add = function(this, port) + if not port then + return + end + + local portId = port.portId + if not portId then + return + end + + local name = port.name or "" + if name == "" then + return + end + + local uid = port.uid + if not uid then + return + end + + local byPortId = this.byPortId + local byName = this.byName + local byUid = this.byUid + local sequence = this.sequence + + if byUid[uid] and byUid[uid].uid == uid then + return + end + + local typeName = port.typeName + local desc = port.desc or "" + + port = table.Copy(port) + + byPortId[portId] = port + byName[name] = port + byUid[uid] = port + + table.insert(sequence, port) + + local wire = this.wire + local names = wire.names + local types = wire.types + local descs = wire.descs + + local index = #names + 1 + names[index] = name -- The port name + types[index] = typeName -- The port type + descs[index] = desc -- The port description + end + + register.empty = function(this) + table.Empty(this.byPortId) + table.Empty(this.byName) + table.Empty(this.byUid) + table.Empty(this.sequence) + + local wire = register.wire + table.Empty(wire.names) + table.Empty(wire.types) + table.Empty(wire.descs) + end + + register.hasPorts = function(this) + local sequence = this.sequence + return #sequence > 0 + end + + return register +end + +function ENT:HandlePortsUpdated() + if not self.PortsUpdated then + return + end + + self.WireEntNameUpdated = true + + self:UpdatePorts() + + self.PortsUpdated = nil +end + +-- To cleanup and get the in-/outputs information. +local function copyAndSanitizeToPortRegister(register, registerTmp) + register = register or newPortRegister() + register:empty() + + if not registerTmp then + return register + end + + for i = 1, #registerTmp do + -- Ensure to keep things ordered, even with gaps + + local port = registerTmp[i] + register:add(port) + end + + return register +end + +function ENT:UpdatePorts() + local wireInputRegister = self.WireInputRegister + local wireOutputRegister = self.WireOutputRegister + + wireInputRegister = copyAndSanitizeToPortRegister(wireInputRegister, self.WireInputRegisterTmp) + wireOutputRegister = copyAndSanitizeToPortRegister(wireOutputRegister, self.WireOutputRegisterTmp) + + self.WireInputRegister = wireInputRegister + self.WireOutputRegister = wireOutputRegister + + local wireEnts = self:GetWiredEntities() + + for key, wireEnt in ipairs(wireEnts) do + wireEnt:_WMI_AddPorts(self.WireInputRegister, self.WireOutputRegister) + end +end + +local g_hashTmp = {} + +function ENT:GetPortUid(port) + if not port then + return nil + end + + local name = port.name or "" + local portId = port.portId + local portType = port.type + + if name == "" then + return nil + end + + if not portId then + return nil + end + + if not portType then + return nil + end + + table.Empty(g_hashTmp) + + table.insert(g_hashTmp, "WMI_") + table.insert(g_hashTmp, self:GetCreationID()) + table.insert(g_hashTmp, self:GetCreationTime()) + table.insert(g_hashTmp, name) + table.insert(g_hashTmp, portId) + table.insert(g_hashTmp, portType) + + return util.SHA1(table.concat(g_hashTmp, "_")) +end + +function ENT:PrepairOutputGlobals(inputData, wireValue, wireEnt, wireDevice, owner) + if not IsValid(wireEnt) then + return + end + + -- This can be usefull if a lua_run entity is triggered + -- Because entity.Fire and entity.AcceptInput are not run synchronously in the same frame, we use them in a custom entity.AcceptInput detour. + -- So we store them for later use in a custom entity.AcceptInput detour. + + local name = inputData.name + local typeName = inputData.typeName + local wired = inputData.wiredStateTotal + + local globals = { + WIRE_NAME = name, -- Input name. + WIRE_TYPE = typeName, -- Input type (NORMAL, STRING, VECTOR, etc.) + WIRE_VALUE = wireValue, -- Input value. + WIRE_WIRED = wired, -- Is the input wired? + WIRE_CALLER = self, -- This entity. + WIRE_ACTIVATOR = wireEnt, -- The entity that has the Wire input. + WIRE_DEVICE = wireDevice, -- The entity where the input data was from, e.g. a Wiremod button. + WIRE_OWNER = owner, -- The owner of the input device, e.g the player who spawned the Wiremod button. + } + + local hammerOutputName = inputData.hammerOutputName + local hammerResetOutputName = inputData.hammerResetOutputName + + self._OutputGlobals = self._OutputGlobals or {} + self._OutputGlobals[hammerOutputName] = globals + self._OutputGlobals[hammerResetOutputName] = globals +end + +function ENT:SetupOutputGlobals(globals) + -- This can be usefull if a lua_run entity is triggered + + local G = _G + + globals._old = { + -- In the case the call might be nested, or some other shenanigans happen, we store the old globals for restore after use. + + WIRE_NAME = G.WIRE_NAME, + WIRE_VALUE = G.WIRE_VALUE, + WIRE_TYPE = G.WIRE_TYPE, + WIRE_WIRED = G.WIRE_WIRED, + WIRE_CALLER = G.WIRE_CALLER, + WIRE_ACTIVATOR = G.WIRE_ACTIVATOR, + WIRE_DEVICE = G.WIRE_DEVICE, + WIRE_OWNER = G.WIRE_OWNER, + } + + G.WIRE_NAME = globals.WIRE_NAME + G.WIRE_VALUE = globals.WIRE_VALUE + G.WIRE_TYPE = globals.WIRE_TYPE + G.WIRE_WIRED = globals.WIRE_WIRED + G.WIRE_CALLER = globals.WIRE_CALLER + G.WIRE_ACTIVATOR = globals.WIRE_ACTIVATOR + G.WIRE_DEVICE = globals.WIRE_DEVICE + G.WIRE_OWNER = globals.WIRE_OWNER +end + +function ENT:KillOutputGlobals(globals) + local oldGlobals = globals and globals._old + + local G = _G + + if oldGlobals then + G.WIRE_NAME = oldGlobals.WIRE_NAME + G.WIRE_VALUE = oldGlobals.WIRE_VALUE + G.WIRE_TYPE = oldGlobals.WIRE_TYPE + G.WIRE_WIRED = oldGlobals.WIRE_WIRED + G.WIRE_CALLER = oldGlobals.WIRE_CALLER + G.WIRE_ACTIVATOR = oldGlobals.WIRE_ACTIVATOR + G.WIRE_DEVICE = oldGlobals.WIRE_DEVICE + G.WIRE_OWNER = oldGlobals.WIRE_OWNER + + globals._old = nil + else + G.WIRE_NAME = nil + G.WIRE_VALUE = nil + G.WIRE_TYPE = nil + G.WIRE_WIRED = nil + G.WIRE_CALLER = nil + G.WIRE_ACTIVATOR = nil + G.WIRE_DEVICE = nil + G.WIRE_OWNER = nil + end +end + +local function deturedAcceptInput(this, name, activator, caller, data, ...) + local wmidata = this._WireMapInterfaceEnt_FirePrepairData + + if not wmidata then + return false + end + + local oldAcceptInput = wmidata.AcceptInput + if not oldAcceptInput then + return false + end + + local globals = wmidata.globals + + if not globals then + this.AcceptInput = oldAcceptInput + return oldAcceptInput(this, name, activator, caller, data, ...) + end + + if not IsValid(caller) or not caller.IsWireMapInterface then + return oldAcceptInput(this, name, activator, caller, data, ...) + end + + -- We detoured the AcceptInput hook of this entity to make our WIRE_* globals (for lua_run) available during the input call. + caller:SetupOutputGlobals(globals) + + local status, errOrResult = pcall(oldAcceptInput, this, name, activator, caller, data, ...) + + if not status then + errOrResult = tostring(errOrResult or "") + + if errOrResult ~= "" then + message = caller:FormatString(": Lua error in target AcceptInput:\n Wire input '%s [%s]' -> Hammer output '%s@%s'\n %s", globals.WIRE_NAME, globals.WIRE_TYPE, caller:FormatEntityString(this), name, errOrResult) + ErrorNoHaltWithStack(message) + end + + return false + end + + caller:KillOutputGlobals(globals) + + return errOrResult +end + +function ENT:PrepairEntityForFire(targetEnt, outputName) + -- Because entity.Fire and entity.AcceptInput are not run synchronously in the same frame, we use add a custom entity.AcceptInput detour to lua_run entities. + + if not self._OutputGlobals then + return + end + + local globals = self._OutputGlobals[outputName] + if not globals then + return + end + + local wmidata = targetEnt._WireMapInterfaceEnt_FirePrepairData or {} + targetEnt._WireMapInterfaceEnt_FirePrepairData = wmidata + + wmidata.globals = globals + + local oldAcceptInput = wmidata.AcceptInput or targetEnt.AcceptInput + wmidata.AcceptInput = oldAcceptInput + + targetEnt.AcceptInput = deturedAcceptInput +end + +function ENT:TriggerWireOutputSafe(wireEnt, wireOutputName, wireValue, ...) + if not IsValid(wireEnt) then + return false + end + + local status, err = pcall(WireLib.TriggerOutput, wireEnt, wireOutputName, wireValue, ...) + + if status then + return true + end + + err = tostring(err or "") + + if err ~= "" then + message = self:FormatString(": Lua error at Wire output '%s':\n%s", wireOutputName, err) + ErrorNoHaltWithStack(message) + end + + return false +end + +function ENT:TriggerHammerOutputSafe(hammerOutputName, activator, hammerValue, ...) + if not IsValid(activator) then + return false + end + + local status, err = pcall(self.TriggerOutput, self, hammerOutputName, activator, hammerValue, ...) + if status then + return true + end + + err = tostring(err or "") + + if err ~= "" then + message = self:FormatString(": Lua error at Hammer output '%s':\n%s", hammerOutputName, err) + ErrorNoHaltWithStack(message) + end + + return false +end + +function ENT:ApplyWireOutputBufferSingle(wireEnt) + local wireOutputRegister = self.WireOutputRegister + + if not wireOutputRegister then + return + end + + for _, outputData in pairs(wireOutputRegister.sequence) do + local wireValue = outputData.bufferedWireValue + + self:TriggerWireOutputSingle(wireEnt, outputData, wireValue) + end +end + +function ENT:ApplyWireOutputBufferAll() + local wireOutputRegister = self.WireOutputRegister + + if not wireOutputRegister then + return + end + + for _, outputData in pairs(wireOutputRegister.sequence) do + local wireValue = outputData.bufferedWireValue + + self:TriggerWireOutputAll(outputData, wireValue) + end +end + +-- Wire input +function ENT:TriggerWireInput(wireInputName, wireValue, wired, wireEnt) + if not IsValid(wireEnt) then return end + + local wireInputRegister = self.WireInputRegister + if not wireInputRegister then + return + end + + local inputData = wireInputRegister.byName[wireInputName] + if not inputData then + return + end + + local portId = inputData.portId + if not self:CheckPortIdLimit(portId, false) then + return + end + + local uid = inputData.uid + local wireInputTriggerBuffer = self.WireInputTriggerBuffer + + local wiredState = inputData.wiredState or {} + inputData.wiredState = wiredState + + local oldWired = wiredState[wireEnt] or false + inputData.wiredStateTotalChanged = false + + if oldWired ~= wired then + inputData.wiredStateTotal = nil + inputData.wiredStateTotalChanged = true + + if wired then + wiredState[wireEnt] = true + else + wiredState[wireEnt] = nil + end + end + + -- Check if any of known entities are connected and cache it. + if inputData.wiredStateTotal == nil then + local wiredStateTotal = false + + for ent, thisWiredState in pairs(wiredState) do + if not IsValid(ent) then + thisWiredState = false + wiredState[ent] = nil + end + + if thisWiredState then + wiredStateTotal = true + end + end + + inputData.wiredStateTotal = wiredStateTotal + end + + -- The wired/unwired state across all entities as been changed. + local wiredStateTotalChanged = inputData.wiredStateTotalChanged or false + + if not self:IsActive() then + -- Keep the last given trigger if turned off, so we apply it after it was turned on. + + local triggerStateData = wireInputTriggerBuffer[uid] or {} + wireInputTriggerBuffer[uid] = triggerStateData + + triggerStateData.inputData = inputData + triggerStateData.wireValue = wireValue + triggerStateData.wireEnt = wireEnt + + return + end + + local debounce = inputData.debounce or {} + inputData.debounce = debounce + + if not wiredStateTotalChanged then + local oldWireValue = debounce.oldWireValue + + if oldWireValue ~= nil and self:IsEqualWireValue(inputData.type, oldWireValue, wireValue) then + return + end + + local nextTime = debounce.nextTime or 0 + if nextTime > CurTime() then + -- Keep the last given trigger during cool down, so we apply it after next tick, in case the signal "misses the bus". + + local triggerStateData = wireInputTriggerBuffer[uid] or {} + wireInputTriggerBuffer[uid] = triggerStateData + + triggerStateData.inputData = inputData + triggerStateData.wireValue = wireValue + triggerStateData.wireEnt = wireEnt + + return + end + end + + wireInputTriggerBuffer[uid] = nil + self:TriggerHammerOutputFromWire(inputData, wireValue, wireEnt) +end + +-- Wire output +function ENT:TriggerWireOutput(portId, hammerValue) + if not self:CheckPortIdLimit(portId, false) then + return + end + + local outputData = self.WireOutputRegister.byPortId[portId] + if not outputData then + return + end + + local wireOutputType = outputData.type + + local convertFunc, isToggle = self:GetMapToWireConverter(wireOutputType) + if not convertFunc then + return + end + + if isToggle then + -- Toggle state each time the output is triggered + outputData.toggleState = not outputData.toggleState + hammerValue = outputData.toggleState + end + + local debounce = outputData.debounce or {} + outputData.debounce = debounce + + local oldHammerValue = debounce.oldHammerValue + if oldHammerValue ~= nil and hammerValue == oldHammerValue then + return + end + + local wireValue = convertFunc(self, hammerValue) + outputData.bufferedWireValue = wireValue + + if not self:IsActive() then + return + end + + local oldWireValue = debounce.oldWireValue + if oldWireValue ~= nil and self:IsEqualWireValue(wireOutputType, oldWireValue, wireValue) then + return + end + + self:TriggerWireOutputAll(outputData, wireValue) + + debounce.oldHammerValue = hammerValue + debounce.oldWireValue = wireValue +end + +function ENT:TriggerWireOutputSingle(wireEnt, outputData, wireValue) + if not outputData then + return + end + + if wireValue == nil then + return + end + + local wireOutputName = outputData.name + + self:TriggerWireOutputSafe(wireEnt, wireOutputName, wireValue) +end + +function ENT:TriggerWireOutputAll(outputData, wireValue) + if not outputData then + return + end + + if wireValue == nil then + return + end + + local wireOutputName = outputData.name + local wireEnts = self:GetWiredEntities() + + for _, wireEnt in ipairs(wireEnts) do + self:TriggerWireOutputSafe(wireEnt, wireOutputName, wireValue) + end +end + +-- Hammer input +function ENT:TriggerHammerInput(name, data) + local portId = tonumber(string.match(name, "triggerwireoutput(%d+)")) or 0 + if self:CheckPortIdLimit(portId, false) then + self:TriggerWireOutput(portId, data) + return true + end + + local caller = self._lastCaller + + if name == "addentity" then + self:AddSingleEntity(caller) + return true + end + + if name == "removeentity" then + self:RemoveSingleEntity(caller) + return true + end + + if name == "addentities" then + self:AddEntitiesByName(data) + return true + end + + if name == "removeentities" then + self:RemoveEntitiesByName(data) + return true + end + + if name == "removeallentities" then + self:RemoveAllEntities() + return true + end + + return false +end + +-- Hammer output +function ENT:TriggerHammerOutputFromWire(inputData, wireValue, wireEnt) + if not inputData then + return + end + + local uid = inputData.uid + local wireConnectionChange = inputData.wiredStateTotalChanged or false + local wired = inputData.wiredStateTotal or false + local hammerOutputName = inputData.hammerOutputName + local hammerResetOutputName = inputData.hammerResetOutputName + + -- wireEnt gets the ownership of connected device + local wireDevice = wireEnt:_WMI_GetInputDevice() or NULL + local owner = IsValid(wireDevice) and WireLib.GetOwner(wireDevice) or NULL + + self._lastWireInputEnt = wireEnt + self._lastWireDeviceEnt = wireDevice + self._lastWireDeviceEntOwner = owner + + local convertFunc, isToggle = self:GetWireToMapConverter(inputData.type) + if not convertFunc then + return + end + + if isToggle then + wireValue = tobool(wireValue) and 1 or 0 + end + + local hammerValue = convertFunc(self, wireValue) + + local debounce = inputData.debounce or {} + inputData.debounce = debounce + + if not wireConnectionChange then + local oldHammerValue = debounce.oldHammerValue + if oldHammerValue ~= nil and hammerValue == oldHammerValue then + return + end + end + + self:PrepairOutputGlobals(inputData, wireValue, wireEnt, wireDevice, owner) + + if not wired then + if wireConnectionChange then + if not isToggle then + -- Do not trigger with "zero" on reset if we are in toggle mode + self:TriggerHammerOutputSafe(hammerOutputName, wireEnt, hammerValue) + end + + self:TriggerHammerOutputSafe(hammerResetOutputName, wireEnt) + end + else + if isToggle then + if hammerValue then + -- Will only trigger if this value is true and if it was false before + self:TriggerHammerOutputSafe(hammerOutputName, wireEnt) + end + else + self:TriggerHammerOutputSafe(hammerOutputName, wireEnt, hammerValue) + end + end + + self.WireInputTriggerBuffer[uid] = nil + + debounce.oldWireValue = wireValue + debounce.oldHammerValue = hammerValue + debounce.nextTime = CurTime() + self:GetMinTriggerTime() + + inputData.wiredStateTotalChanged = false +end + +-- Hammer keyvalues +function ENT:StoreHammerOutputs(key, value) + local portId = tonumber(string.match(key, "onwireinput(%d+)")) or 0 + if self:CheckPortIdLimit(portId, true) then + self:StoreOutput(key, value) + return true + end + + local portId = tonumber(string.match(key, "onresetwireinput(%d+)")) or 0 + if self:CheckPortIdLimit(portId, true) then + self:StoreOutput(key, value) + return true + end + + if key == "onwireentscreated" then + self:StoreOutput(key, value) + return true + end + + if key == "onwireentsremoved" then + self:StoreOutput(key, value) + return true + end + + if key == "onwireentsready" then + self:StoreOutput(key, value) + return true + end + + if key == "onwireentsstartchanging" then + self:StoreOutput(key, value) + return true + end + + return false +end + +local g_blacklistedPortNames = { + wirelink = true, + link = true, + entity = true, +} + +function ENT:RegisterWireInputs(key, value) + local portId, name = string.match(key, "input(%d+)_(%w+)") + + if not portId then + return + end + + portId = tonumber(portId or 0) or 0 + name = tostring(name or "") + + if not self:CheckPortIdLimit(portId, true) then + return false + end + + if name == "" then + return false + end + + local wireInputRegisterTmp = self.WireInputRegisterTmp or {} + self.WireInputRegisterTmp = wireInputRegisterTmp + + local inputData = wireInputRegisterTmp[portId] or {} + wireInputRegisterTmp[portId] = inputData + + if name == "lua" then + -- Used to run given Lua codes. + -- It is no longer supported as it is considered as unsafe. + + if value ~= "" then + inputData.warnAboutLua = true + end + elseif name == "type" then + inputData.type = tonumber(value) or 0 + inputData.typeName = self:GetWireTypenameByTypeId(inputData.type) + inputData.zeroWireValue = WireLib.GetDefaultForType(inputData.typeName) + elseif name == "desc" then + inputData.desc = value + elseif name == "name" then + if value ~= "" then + if not g_blacklistedPortNames[value] then + inputData.name = value + inputData.portId = portId + + inputData.hammerOutputName = "OnWireInput" .. portId + inputData.hammerResetOutputName = "OnResetWireInput" .. portId + + if not inputData.type then + inputData.type = 0 + inputData.typeName = self:GetWireTypenameByTypeId(inputData.type) + inputData.zeroWireValue = WireLib.GetDefaultForType(inputData.typeName) + end + else + table.Empty(inputData) + self:PrintWarning(": Can not add input '%s', as the name is reserved.", value) + end + else + table.Empty(inputData) + end + end + + if inputData.name then + if inputData.warnAboutLua then + self:PrintWarning(", input '%s [%s]': Running Lua code is no longer supported! Trigger an lua_run entity instead.", inputData.name, inputData.typeName) + inputData.warnAboutLua = nil + end + + inputData.uid = self:GetPortUid(inputData) + self.PortsUpdated = true + end + + return true +end + +function ENT:RegisterWireOutputs(key, value) + local portId, name = string.match(key, "output(%d+)_(%w+)") + + if not portId then + return + end + + portId = tonumber(portId or 0) or 0 + name = tostring(name or "") + + if not self:CheckPortIdLimit(portId, true) then + return false + end + + if name == "" then + return false + end + + local wireOutputRegisterTmp = self.WireOutputRegisterTmp or {} + self.WireOutputRegisterTmp = wireOutputRegisterTmp + + local outputData = wireOutputRegisterTmp[portId] or {} + wireOutputRegisterTmp[portId] = outputData + + if name == "type" then + outputData.type = tonumber(value) + outputData.typeName = self:GetWireTypenameByTypeId(outputData.type) + outputData.zeroWireValue = WireLib.GetDefaultForType(outputData.typeName) + elseif name == "desc" then + outputData.desc = value + elseif name == "name" then + if value ~= "" then + if not g_blacklistedPortNames[value] then + outputData.name = value + outputData.portId = portId + + if not outputData.type then + outputData.type = 0 + outputData.typeName = self:GetWireTypenameByTypeId(outputData.type) + outputData.zeroWireValue = WireLib.GetDefaultForType(outputData.typeName) + end + else + table.Empty(outputData) + self:PrintWarning(": Can not add output '%s', as the name is reserved.", value) + end + else + table.Empty(outputData) + end + end + + if outputData.name then + outputData.uid = self:GetPortUid(outputData) + self.PortsUpdated = true + end + + return true +end + +function ENT:RegisterWireIO(key, value) + if self:RegisterWireInputs(key, value) then + return true + end + + if self:RegisterWireOutputs(key, value) then + return true + end + + return false +end + +function ENT:IsConnectedWirelink() + local wireEnts = self:GetWiredEntities() + + for _, wireEnt in ipairs(wireEnts) do + local IsConnectedWirelink = wireEnt._WMI_IsConnectedWirelink + + if IsConnectedWirelink and IsConnectedWirelink(wireEnt) then + return true + end + end + + return false +end + +function ENT:PollWirelinkStatus() + if not self:IsActive() then + return + end + + local now = CurTime() + local nextWirelinkPoll = self.NextWirelinkPoll or 0 + + if nextWirelinkPoll > now then + return + end + + local isWirelinked = self:IsConnectedWirelink() + + local oldIsWirelinked = self.oldIsWirelinked or false + local wirelinkChanged = oldIsWirelinked ~= isWirelinked + self.oldIsWirelinked = isWirelinked + + if wirelinkChanged and not isWirelinked then + self:UnWirelinkAllWireInputs() + end + + self.NextWirelinkPoll = now + self.MIN_THINK_TIME * 8 +end + +function ENT:UnWirelinkAllWireInputs() + local wireEnts = self:GetWiredEntities() + local wireInputRegister = self.WireInputRegister + + if not wireInputRegister then + return + end + + for _, wireEnt in ipairs(wireEnts) do + for _, inputData in ipairs(wireInputRegister.sequence) do + local name = inputData.name + + local wireValue = wireEnt:_WMI_GetDirectLinkedInputValue(name) + + if wireValue == nil then + wireValue = inputData.zeroWireValue + end + + wireEnt:TriggerInput(name, wireValue) + end + end +end + diff --git a/lua/entities/info_wiremapinterface/networking.lua b/lua/entities/info_wiremapinterface/networking.lua new file mode 100644 index 0000000000..7b89b03544 --- /dev/null +++ b/lua/entities/info_wiremapinterface/networking.lua @@ -0,0 +1,89 @@ +-- Networking functions + +util.AddNetworkString("WireMapInterfaceEntities") + +local g_ReadyForNetworking = false + +local function initNetworking() + timer.Simple(0.1, function() + g_ReadyForNetworking = true + + local wireMapInterfaceEntities = ents.FindByClass("info_wiremapinterface") + for i, ent in ipairs(wireMapInterfaceEntities) do + local timerDelay = i / 2 + + timer.Simple(timerDelay, function() + if not IsValid(ent) then + return + end + + if not ent.IsWireMapInterface then + return + end + + ent.ShouldNetworkEntities = true + end) + end + end) +end + +hook.Add("PlayerInitialSpawn", "WireMapInterface_PlayerInitialSpawn", function() + initNetworking() +end) + +hook.Add("PostCleanupMap", "WireMapInterface_PostCleanupMap_SV", function() + g_ReadyForNetworking = false + initNetworking() +end) + +function ENT:InitNetworking() + initNetworking() +end + +function ENT:HandleShouldNetworkEntities() + if not g_ReadyForNetworking then + -- Avoid networking too early after startup. + return + end + + if not self.ShouldNetworkEntities then + return + end + + local now = CurTime() + local nextNetworkTime = self.NextNetworkTime + + if not nextNetworkTime or nextNetworkTime > now then + -- Debounce the network calls. + -- The client does not need the changed data that often. + return + end + + self:NetworkWireEntities() + self:AttachToSaveStateEntity() + + self.ShouldNetworkEntities = false + self.NextNetworkTime = now + self.MIN_THINK_TIME * 4 +end + +function ENT:NetworkWireEntities() + -- Network the list and properties of the wire entities. + -- We need we know about them on the client. For cable rendering, tools etc. + + net.Start("WireMapInterfaceEntities") + + net.WriteUInt(self:EntIndex(), MAX_EDICT_BITS) + net.WriteBool(self:FlagGetProtectFromTools()) + net.WriteBool(self:FlagGetProtectFromPhysgun()) + net.WriteBool(self:FlagGetRenderWires()) + net.WriteUInt(self:GetWiredEntityCount(), 6) + + local wireEnts = self:GetWiredEntities() + + for _, wireEnt in ipairs(wireEnts) do + net.WriteUInt(wireEnt:EntIndex(), MAX_EDICT_BITS) + end + + net.Broadcast() +end + diff --git a/lua/entities/info_wiremapinterface/savestate.lua b/lua/entities/info_wiremapinterface/savestate.lua new file mode 100644 index 0000000000..51fe5e8b0a --- /dev/null +++ b/lua/entities/info_wiremapinterface/savestate.lua @@ -0,0 +1,303 @@ +-- Dupe and save support and validation + +local WireLib = WireLib + +local g_saveStateEntity = nil +local g_mapName = nil + +function ENT:GetSaveStateEntity() + if IsValid(g_saveStateEntity) and not g_saveStateEntity:IsMarkedForDeletion() then + return g_saveStateEntity + end + + g_saveStateEntity = nil + + local entities = ents.FindByClass("info_wiremapinterface_savestate") + for _, ent in ipairs(entities) do + if IsValid(ent) and not ent:IsMarkedForDeletion() then + g_saveStateEntity = ent + return g_saveStateEntity + end + end + + local ent = ents.Create("info_wiremapinterface_savestate") + if not IsValid(ent) then + return nil + end + + ent:SetPos(self:GetPos()) + ent:Spawn() + ent:Activate() + + g_saveStateEntity = ent + return ent +end + +function ENT:AttachToSaveStateEntity() + local saveState = self:GetSaveStateEntity() + if IsValid(saveState) then + saveState:AddInterface(self) + end +end + +function ENT:GetSubEntityByIdCombo(entIdx, mapId, spawnId, createdEntities) + if not spawnId then + return nil + end + + entIdx = tonumber(entIdx) + mapId = tonumber(mapId) + + if not WireLib.WireMapInterfaceValidateId(entIdx) then + return nil + end + + if not WireLib.WireMapInterfaceValidateId(mapId) then + return nil + end + + if self:HashSubEntityMapId(entIdx, mapId) ~= spawnId then + return nil + end + + local wireEnt = nil + + if createdEntities then + wireEnt = createdEntities[entIdx] + end + + if not createdEntities or not IsValid(wireEnt) then + wireEnt = WireLib.GetWireMapInterfaceSubEntityBySpawnIdDuped(spawnId) + + if not IsValid(wireEnt) then + wireEnt = WireLib.GetWireMapInterfaceSubEntityByMapId(mapId, true) + + if not IsValid(wireEnt) then + wireEnt = WireLib.GetWireMapInterfaceSubEntityBySpawnId(spawnId) + + if not IsValid(wireEnt) then + return nil + end + end + end + end + + return wireEnt +end + +function ENT:OnSaveCopy() + if not self:CreatedByMap() then + return nil + end + + local entries = {} + local wireEnts = self:GetWiredEntities() + + for _, wireEnt in ipairs(wireEnts) do + local wireEntSpawnId = self:GetSubEntitySpawnId(wireEnt) + + if wireEntSpawnId then + local entList = entries[wireEntSpawnId] or {} + entries[wireEntSpawnId] = entList + + entList[wireEnt:EntIndex()] = wireEnt:_WMI_BuildDupeData(self) + end + end + + local saveData = { + spawnId = self:GetSpawnId(), + mapId = self:MapCreationID(), + entries = entries, + } + + return saveData +end + +function ENT:OnDuplicatedSave(saveData) + if not self:CreatedByMap() then + return + end + + if not saveData.entries then + return + end + + for wireEntSpawnId, entList in pairs(saveData.entries) do + for entIdx, dupeData in pairs(entList) do + local wireMapInterfaceEntDupeInfo = dupeData.wireMapInterfaceEntDupeInfo + + if wireMapInterfaceEntDupeInfo then + local mapId = wireMapInterfaceEntDupeInfo.mapId + local wireEnt = self:GetSubEntityByIdCombo(entIdx, mapId, wireEntSpawnId) + + if IsValid(wireEnt) and not wireEnt._WMI_ApplyDupeData then + self:OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo) + end + end + end + end +end + +function ENT:OnSavePaste(ply, saveData, createdEntities) + if not self:CreatedByMap() then + return + end + + if not saveData.entries then + return + end + + for wireEntSpawnId, entList in pairs(saveData.entries) do + for entIdx, dupeData in pairs(entList) do + local wireMapInterfaceEntDupeInfo = dupeData.wireMapInterfaceEntDupeInfo + + if wireMapInterfaceEntDupeInfo then + local mapId = wireMapInterfaceEntDupeInfo.mapId + local wireEnt = self:GetSubEntityByIdCombo(entIdx, mapId, wireEntSpawnId, createdEntities) + + if IsValid(wireEnt) and wireEnt._WMI_ApplyDupeData then + wireEnt:_WMI_ApplyDupeData(ply, dupeData, createdEntities, self) + end + end + end + end +end + +local function buildSpawnId(...) + if not g_mapName then + g_mapName = game.GetMap() + end + + local tmp = {...} + + tmp = table.concat(tmp, "_") + + local id = string.format( + "WMI_%s_%s_buildSpawnId", + g_mapName, + tmp + ) + + -- This not fire proof security, but this is more of a user convenience thing than anything else. + -- It just prevents unexpected issues from map foreign dupes made by user error. + -- This is used to check if the dupe/save belongs to the map and the particular Wire Map Interface instance. + -- Entities not passing the validation will still be pasted and spawned, but they will not get any addional Wiremod functionalities whatsoever. + -- The validation might not pass across map recompiles, especially if MapCreationIDs change. + + -- In theroy it would be quite possible to crack or bypass this validation with a forged dupe/save file. + -- For that you just would need to know how was hashed and how the targeted map has been built. + + -- However in practice it would be quite tedious to do so and it would also only be useful if the map has a vulnerability. + -- In this context the risks a rigged dupe file can pose are quite minimal. + + id = util.SHA1(id) + return id +end + +function ENT:HashMapId(interfaceMapId) + interfaceMapId = tonumber(interfaceMapId) + + if not WireLib.WireMapInterfaceValidateId(interfaceMapId) then + return nil + end + + -- Get a compact and unique spawn identifier per map and interface. + local id = buildSpawnId( + interfaceMapId, + "HashMapId" + ) + + return id +end + +function ENT:HashSubEntityMapId(subEntIdx, subEntMapId) + if not self:CreatedByMap() then + return nil + end + + subEntIdx = tonumber(subEntIdx) + subEntMapId = tonumber(subEntMapId) + + if not WireLib.WireMapInterfaceValidateId(subEntIdx) then + return nil + end + + if not WireLib.WireMapInterfaceValidateId(subEntMapId) then + return nil + end + + -- Get a compact and unique spawn identifier per map, interface and sub entity. + local id = buildSpawnId( + self:MapCreationID(), + subEntIdx, + subEntMapId, + "HashSubEntityMapId" + ) + + return id +end + +function ENT:GetSpawnId() + if not self:CreatedByMap() then + return nil + end + + if self.SpawnId then + return self.SpawnId + end + + local id = self:HashMapId(self:MapCreationID()) + + self.SpawnId = id + return id +end + +function ENT:GetSubEntitySpawnId(subEnt) + if not IsValid(subEnt) then + return nil + end + + if not subEnt._WMI_GetSpawnId then + return false + end + + local spawnId = subEnt:_WMI_GetSpawnId(self) + if not spawnId then + return nil + end + + return spawnId +end + +function ENT:ValidateDupedMapId(mapCreationID, spawnIdC) + if not mapCreationID then + return false + end + + local spawnIdA = self:GetSpawnId() + if not spawnIdA then + return false + end + + local spawnIdB = self:HashMapId(mapCreationID) + if not spawnIdB then + return false + end + + if spawnIdA ~= spawnIdB then + return false + end + + if spawnIdC ~= nil then + if spawnIdA ~= spawnIdC then + return false + end + + if spawnIdB ~= spawnIdC then + return false + end + end + + return true +end + diff --git a/lua/entities/info_wiremapinterface_savestate/init.lua b/lua/entities/info_wiremapinterface_savestate/init.lua new file mode 100644 index 0000000000..81ea9bde10 --- /dev/null +++ b/lua/entities/info_wiremapinterface_savestate/init.lua @@ -0,0 +1,207 @@ +-- This is a helper entity to store save state data wire map interface entities. (info_wiremapinterface_savestate) +-- Only one per map will be spawned during run time. This is needed because info_wiremapinterface can not and also should not be duplicated/saved. +-- When a game is saved, this entity will be save along with it. +-- When the entity is restored, it deploys its saved data to all interface entities it knows about. + +ENT.Base = "base_point" +ENT.Type = "point" + +ENT.Spawnable = false +ENT.AdminOnly = true + +-- Needed for save game support +ENT.DisableDuplicator = false + +local g_SaveStateEntity = nil +local g_interfaceEntities = {} +local g_saveState = {} + +function ENT:Initialize() + if self:CreatedByMap() then + self:Remove() + g_SaveStateEntity = nil + return + end + + if IsValid(g_SaveStateEntity) and g_SaveStateEntity ~= self then + -- Never allow more than one instance of this entity. + g_SaveStateEntity:Remove() + end + + g_SaveStateEntity = self +end + +function ENT:OnReloaded() + -- Easier for debugging. + self:Remove() + g_SaveStateEntity = nil + + local wireMapInterfaceEntities = ents.FindByClass("info_wiremapinterface") + for _, ent in ipairs(wireMapInterfaceEntities) do + if IsValid(ent) then + ent:OnReloaded() + end + end +end + +function ENT:AddInterface(interfaceEnt) + if not self:IsValidInterface(interfaceEnt) then + return + end + + local spawnId = interfaceEnt:GetSpawnId() + if not spawnId then + return + end + + if IsValid(g_interfaceEntities[spawnId]) then + return + end + + g_interfaceEntities[spawnId] = interfaceEnt +end + +function ENT:IsValidInterface(interfaceEnt) + if not IsValid(interfaceEnt) then + return false + end + + if not interfaceEnt.IsWireMapInterface then + return false + end + + if not interfaceEnt:CreatedByMap() then + return false + end + + return true +end + +function ENT:GetInterfaceByIdCombo(mapId, spawnId) + if not spawnId then + return nil + end + + mapId = tonumber(mapId or 0) or 0 + + if not WireLib.WireMapInterfaceValidateId(mapId) then + return nil + end + + local interfaceEnt = g_interfaceEntities[spawnId] + + if not self:IsValidInterface(interfaceEnt) then + interfaceEnt = ents.GetMapCreatedEntity(mapId) + + if not self:IsValidInterface(interfaceEnt) then + return nil + end + end + + if not interfaceEnt:ValidateDupedMapId(mapId, spawnId) then + return nil + end + + return interfaceEnt +end + +function ENT:PreEntityCopy() + if self:IsMarkedForDeletion() then + return + end + + duplicator.ClearEntityModifier(self, "WireMapInterfaceSaveStateInfo") + + local entries = {} + + for spawnId, interfaceEnt in pairs(g_interfaceEntities) do + if self:IsValidInterface(interfaceEnt) then + local saveData = interfaceEnt:OnSaveCopy() + + if saveData then + entries[spawnId] = saveData + + if not g_saveState[spawnId] then + g_saveState[spawnId] = saveData + end + end + else + g_interfaceEntities[spawnId] = nil + end + end + + duplicator.StoreEntityModifier(self, "WireMapInterfaceSaveStateInfo", { + interfaceEntries = entries + }) +end + +function ENT:OnDuplicated() + if self:IsMarkedForDeletion() then + return + end + + local entityMods = self.EntityMods + if not entityMods then + return + end + + local wireMapInterfaceSaveStateInfo = entityMods.WireMapInterfaceSaveStateInfo + if not wireMapInterfaceSaveStateInfo then + return + end + + local entries = wireMapInterfaceSaveStateInfo.interfaceEntries + if not entries then + return + end + + for spawnId, saveData in pairs(entries) do + local interfaceEnt = self:GetInterfaceByIdCombo(saveData.mapId, spawnId) + if interfaceEnt then + g_interfaceEntities[spawnId] = interfaceEnt + g_saveState[spawnId] = saveData + + interfaceEnt:OnDuplicatedSave(saveData) + end + end +end + +function ENT:PostEntityPaste(ply, ent, createdEntities) + if self:IsMarkedForDeletion() then + return + end + + for spawnId, saveData in pairs(g_saveState) do + local interfaceEnt = self:GetInterfaceByIdCombo(saveData.mapId, spawnId) + + if interfaceEnt then + g_interfaceEntities[spawnId] = interfaceEnt + interfaceEnt:OnSavePaste(ply, saveData, createdEntities) + end + end +end + +local g_wireMapInterfaceSaveStateTimer = "WireMapInterface_SaveState_CanTool_Timer" + +function ENT:CanTool() + -- This is for saves only. + -- Stop dublicator tools for doing weird stuff. Such as area copy. + + -- Block dublicator tools. + self.DoNotDuplicate = true + + timer.Remove(g_wireMapInterfaceSaveStateTimer) + timer.Create(g_wireMapInterfaceSaveStateTimer, 0.01, 1, function() + timer.Remove(g_wireMapInterfaceSaveStateTimer) + + if not IsValid(self) then + return + end + + -- Revert shortly after, so save games still work. + self.DoNotDuplicate = false + end) + + -- Other Tools can not access this entity anyway as it is not a traceable entity + return false +end \ No newline at end of file diff --git a/lua/wire/client/cl_wire_map_interface.lua b/lua/wire/client/cl_wire_map_interface.lua index 6d8a7153ae..ea12af342e 100644 --- a/lua/wire/client/cl_wire_map_interface.lua +++ b/lua/wire/client/cl_wire_map_interface.lua @@ -1,89 +1,231 @@ --- The client part of the wire map interface. --- It's for the clientside wire ports adding and removing, also for the rendering stuff. --- It's in in this folder, because point entities are serverside only. +-- Clientside functionalities of Wire Map Interface +-- This is mostly for predection and rendering --- Removing wire stuff and other changes that were done. -local OverRiddenEnts = {} -local function RemoveWire(Entity) - if (not IsValid(Entity)) then return end +local WIRE_CLIENT_INSTALLED = WIRE_CLIENT_INSTALLED - local ID = Entity:EntIndex() +if not WIRE_CLIENT_INSTALLED then + return +end + +local g_wireTools = { + "wire", + "wire_adv", + "wire_debugger", + "wire_wirelink", + "multi_wire", +} + +local g_wiredEntities = {} +local g_wiredEntitiesRemove = {} +local g_nextThink = 0 + +-- Remove wire stuff and other changes that were done. +local function RemoveWire(item) + local wmiId = item.wmiId + local entId = item.entId + + local ent = item.ent + + item.init = nil + item.ent = nil + item.renderWires = nil + + if not IsValid(ent) then + ent = ents.GetByIndex(entId) + end - Entity._NextRBUpdate = nil - Entity.ppp = nil - OverRiddenEnts[ID] = nil - WireLib._RemoveWire(ID) -- Remove entity, so it doesn't count as a wire able entity anymore. + if IsValid(ent) then + local oldSettings = item.oldSettings or {} - for key, value in pairs(Entity._Settings_WireMapInterfaceEnt or {}) do - if (not value or (value == 0) or (value == "")) then - Entity[key] = nil + if not oldSettings.m_tblToolsAllowed then + ent.m_tblToolsAllowed = false else - Entity[key] = value + ent.m_tblToolsAllowed = oldSettings.m_tblToolsAllowed + end + + if not oldSettings.PhysgunDisabled then + ent.PhysgunDisabled = nil + else + ent.PhysgunDisabled = oldSettings.PhysgunDisabled + end + end + + item.oldSettings = nil + item.nextRenderBoundsUpdate = nil + + local wmiWiredEntitiesRemove = g_wiredEntitiesRemove[wmiId] + if wmiWiredEntitiesRemove then + wmiWiredEntitiesRemove[entId] = nil + + if table.IsEmpty(wmiWiredEntitiesRemove) then + g_wiredEntitiesRemove[wmiId] = nil + end + end + + local wmiWiredEntities = g_wiredEntities[wmiId] + if wmiWiredEntities then + wmiWiredEntities[entId] = nil + + if table.IsEmpty(wmiWiredEntities) then + g_wiredEntities[wmiId] = nil end end - Entity._Settings_WireMapInterfaceEnt = nil end --- Adding wire stuff and changes. -usermessage.Hook("WireMapInterfaceEnt", function(data) - local Entity = data:ReadEntity() - local Flags = data:ReadChar() - local Remove = (Flags == -1) - if (not WIRE_CLIENT_INSTALLED) then return end - if (not IsValid(Entity)) then return end +-- Add wire stuff for rendering and prediction. +local function AddWire(item) + local wmiId = item.wmiId + local entId = item.entId + local protectFromTools = item.protectFromTools + local protectFromPhysgun = item.protectFromPhysgun - if (Remove) then - RemoveWire(Entity) + local ent = ents.GetByIndex(entId) + if not IsValid(ent) then return end - Entity._Settings_WireMapInterfaceEnt = {} + item.ent = ent + item.init = true + + local oldSettings = item.oldSettings or {} + item.oldSettings = oldSettings + + local isCreatedByMap = ent:CreatedByMap() - if (bit.band(Flags, 1) > 0) then -- Protect in-/output entities from non-wire tools - Entity._Settings_WireMapInterfaceEnt.m_tblToolsAllowed = Entity.m_tblToolsAllowed or false - Entity.m_tblToolsAllowed = {"wire", "wire_adv", "wire_debugger", "wire_wirelink", "gui_wiring", "multi_wire"} + -- Protect in-/output entities from non-wire tools + if not ent.m_tblToolsAllowed then + oldSettings.m_tblToolsAllowed = false + else + oldSettings.m_tblToolsAllowed = table.Copy(ent.m_tblToolsAllowed) end - if (bit.band(Flags, 2) > 0) then -- Protect in-/output entities from the physgun - Entity._Settings_WireMapInterfaceEnt.PhysgunDisabled = Entity.PhysgunDisabled or false - Entity.PhysgunDisabled = true + if protectFromTools and isCreatedByMap then + ent.m_tblToolsAllowed = ent.m_tblToolsAllowed or {} + table.Add(ent.m_tblToolsAllowed, g_wireTools) end - local ID = Entity:EntIndex() - if (bit.band(Flags, 32) > 0) then -- Render Wires - OverRiddenEnts[ID] = true - else - OverRiddenEnts[ID] = nil + -- Protect in-/output entities from the physgun + oldSettings.PhysgunDisabled = ent.PhysgunDisabled or false + + if protectFromPhysgun and isCreatedByMap then + ent.PhysgunDisabled = true + end + + item.nextRenderBoundsUpdate = 0 + + local wmiWiredEntitiesRemove = g_wiredEntitiesRemove[wmiId] + if wmiWiredEntitiesRemove then + wmiWiredEntitiesRemove[entId] = nil + + if table.IsEmpty(wmiWiredEntitiesRemove) then + g_wiredEntitiesRemove[wmiId] = nil + end + end +end + +net.Receive("WireMapInterfaceEntities", function() + local wmiId = net.ReadUInt(MAX_EDICT_BITS) + local protectFromTools = net.ReadBool() + local protectFromPhysgun = net.ReadBool() + local renderWires = net.ReadBool() + local count = net.ReadUInt(6) + + local wmiWiredEntities = g_wiredEntities[wmiId] or {} + g_wiredEntities[wmiId] = wmiWiredEntities + + local wmiWiredEntitiesRemove = g_wiredEntitiesRemove[wmiId] or {} + g_wiredEntitiesRemove[wmiId] = wmiWiredEntitiesRemove + + for entId, item in pairs(wmiWiredEntities) do + -- Clear all that belongs to the current WMI. + wmiWiredEntitiesRemove[entId] = item + end + + for i = 1, count do + local entId = net.ReadUInt(MAX_EDICT_BITS) + + -- Unclear listed items. + wmiWiredEntitiesRemove[entId] = nil + + -- Add listed items. + local item = wmiWiredEntities[entId] or {} + wmiWiredEntities[entId] = item + + item.entId = entId + item.wmiId = wmiId + item.protectFromTools = protectFromTools + item.protectFromPhysgun = protectFromPhysgun + item.renderWires = renderWires end end) --- Render bounds updating -hook.Add("Think", "WireMapInterface_Think", function() - for ID, _ in pairs(OverRiddenEnts) do - local self = Entity(ID) - if (not IsValid(self) or not WIRE_CLIENT_INSTALLED) then - OverRiddenEnts[ID] = nil +local function pollWireItems() + for _, wmiWiredEntitiesRemove in pairs(g_wiredEntitiesRemove) do + for _, item in pairs(wmiWiredEntitiesRemove) do + RemoveWire(item) + end + end - return + for wmiId, wmiWiredEntities in pairs(g_wiredEntities) do + for entId, item in pairs(wmiWiredEntities) do + if not IsValid(item.ent) then + if item.init then + -- Entity disappeared unexpectedly, so unregister it. + local wmiWiredEntitiesRemove = g_wiredEntitiesRemove[wmiId] or {} + g_wiredEntitiesRemove[wmiId] = wmiWiredEntitiesRemove + + wmiWiredEntitiesRemove[entId] = item + else + AddWire(item) + end + end end + end +end + +hook.Add("PostCleanupMap", "WireMapInterface_PostCleanupMap_CL", function() + table.Empty(g_wiredEntities) + table.Empty(g_wiredEntitiesRemove) + g_nextThink = 0 +end) + +hook.Add("Think", "WireMapInterface_Think", function() + local now = CurTime() + + if now < g_nextThink then + return + end + + g_nextThink = now + 1 + + pollWireItems() - if (CurTime() >= (self._NextRBUpdate or 0)) then - self._NextRBUpdate = CurTime() + math.random(30,100) / 10 - Wire_UpdateRenderBounds(self) + -- Render bounds updating + for _, wmiWiredEntities in pairs(g_wiredEntities) do + for _, item in pairs(wmiWiredEntities) do + if item.init and item.renderWires and now >= item.nextRenderBoundsUpdate then + local ent = item.ent + + if IsValid(ent) and not ent:IsDormant() then + Wire_UpdateRenderBounds(ent) + item.nextRenderBoundsUpdate = now + math.random(30, 100) / 10 + end + end end end end) -- Rendering hook.Add("PostDrawOpaqueRenderables", "WireMapInterface_Draw", function() - for ID, _ in pairs(OverRiddenEnts) do - local self = Entity(ID) - if (not IsValid(self) or not WIRE_CLIENT_INSTALLED) then - OverRiddenEnts[ID] = nil - - return + for _, wmiWiredEntities in pairs(g_wiredEntities) do + for _, item in pairs(wmiWiredEntities) do + if item.init and item.renderWires then + local ent = item.ent + + if IsValid(ent) and not ent:IsDormant() then + Wire_Render(ent) + end + end end - - Wire_Render(self) end end) diff --git a/lua/wire/server/wire_map_interface.lua b/lua/wire/server/wire_map_interface.lua new file mode 100644 index 0000000000..0461264a3d --- /dev/null +++ b/lua/wire/server/wire_map_interface.lua @@ -0,0 +1,166 @@ +-- Serverside functionalities of Wire Map Interface + +WireLib._WireMapInterfaceSpawnIdRegister = WireLib._WireMapInterfaceSpawnIdRegister or {} + +local g_spawnIdRegisterBySpawnId = WireLib._WireMapInterfaceSpawnIdRegister.bySpawnId or {} +WireLib._WireMapInterfaceSpawnIdRegister.bySpawnId = g_spawnIdRegisterBySpawnId + +local g_spawnIdRegisterBySpawnIdDuped = WireLib._WireMapInterfaceSpawnIdRegister.bySpawnIdDuped or {} +WireLib._WireMapInterfaceSpawnIdRegister.bySpawnIdDuped = g_spawnIdRegisterBySpawnIdDuped + +local g_spawnIdRegisterByMapId = WireLib._WireMapInterfaceSpawnIdRegister.byMapId or {} +WireLib._WireMapInterfaceSpawnIdRegister.byMapId = g_spawnIdRegisterByMapId + +local g_nextCleanup = 0 + +local function CleanupRegister() + local now = CurTime() + + if now < g_nextCleanup then + -- Only cleanup if the last call was more than 10 minutes ago. + return + end + + for wireEntSpawnId, wireEnt in ipairs(g_spawnIdRegisterBySpawnId) do + if not IsValid(wireEnt) then + g_spawnIdRegisterBySpawnId[wireEntSpawnId] = nil + end + end + + for wireEntSpawnIdDuped, wireEnt in ipairs(g_spawnIdRegisterBySpawnIdDuped) do + if not IsValid(wireEnt) then + g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] = nil + end + end + + for wireEntMapId, wireEnt in ipairs(g_spawnIdRegisterByMapId) do + if not IsValid(wireEnt) then + g_spawnIdRegisterByMapId[wireEntMapId] = nil + end + end + + g_nextCleanup = now + 60 * 10 +end + +function WireLib.RegisterWireMapInterfaceSpawnId(wireEntSpawnId, wireEntSpawnIdDuped, wireEntMapId, wireEnt) + if not wireEntSpawnId then + return + end + + if not wireEntMapId then + return + end + + if not IsValid(wireEnt) then + return + end + + g_spawnIdRegisterBySpawnId[wireEntSpawnId] = wireEnt + + if wireEntSpawnIdDuped then + g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] = wireEnt + end + + g_spawnIdRegisterByMapId[wireEntMapId] = wireEnt + + CleanupRegister() +end + +function WireLib.UnregisterWireMapInterfaceSpawnId(wireEntSpawnId, wireEntSpawnIdDuped, wireEntMapId) + if not wireEntSpawnId then + return + end + + if not wireEntMapId then + return + end + + g_spawnIdRegisterBySpawnId[wireEntSpawnId] = nil + + if wireEntSpawnIdDuped then + g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] = nil + end + + g_spawnIdRegisterByMapId[wireEntMapId] = nil + + CleanupRegister() +end + +function WireLib.GetWireMapInterfaceSubEntityBySpawnId(wireEntSpawnId) + if not wireEntSpawnId then + return nil + end + + local wireEnt = g_spawnIdRegisterBySpawnId[wireEntSpawnId] + if not IsValid(wireEnt) then + g_spawnIdRegisterBySpawnId[wireEntSpawnId] = nil + CleanupRegister() + + return nil + end + + return wireEnt +end + +function WireLib.GetWireMapInterfaceSubEntityBySpawnIdDuped(wireEntSpawnIdDuped) + if not wireEntSpawnIdDuped then + return nil + end + + local wireEnt = g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] + if not IsValid(wireEnt) then + g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] = nil + CleanupRegister() + + return nil + end + + return wireEnt +end + +function WireLib.GetWireMapInterfaceSubEntityByMapId(wireEntMapId, alsoFindViaEngine) + if not wireEntMapId then + return nil + end + + local wireEnt = g_spawnIdRegisterByMapId[wireEntMapId] + if not IsValid(wireEnt) then + g_spawnIdRegisterByMapId[wireEntMapId] = nil + CleanupRegister() + + if alsoFindViaEngine then + wireEnt = ents.GetMapCreatedEntity(wireEntMapId) + + if not IsValid(wireEnt) then + return nil + end + end + end + + return wireEnt +end + +function WireLib.WireMapInterfaceValidateId(id) + if not id then + return false + end + + id = tonumber(id or 0) or 0 + + if id ~= id then + -- Is it NaN? + return false + end + + if id < -1 then + -- Ids of -1 are valid, lower than that is not. + return false + end + + if id > 0xFFFF then + -- Legit ids > 65k are extremly unlikely or even impossible to happen. + return false + end + + return true +end \ No newline at end of file diff --git a/lua/wire/server/wirelib.lua b/lua/wire/server/wirelib.lua index 5138d4fdca..5b0c68fe8c 100644 --- a/lua/wire/server/wirelib.lua +++ b/lua/wire/server/wirelib.lua @@ -837,19 +837,52 @@ function WireLib.Weld(ent, traceEntity, tracePhysicsBone, DOR, collision, AllowW end end +local function LookupEntityByIdOrWmiId(entIdx, spawnId, GetEntByID) + local ent = GetEntByID(entIdx) + + if IsValid(ent) then + return ent + end + + -- Used for the Wire Map Interface. + -- Linked entities might be part of the map but not of the dupe/save file ("createdEntities"), + -- because the linked entity might be not duplicatable, but still belongs to the contraption. + -- In this case lookup the entity in an additional list aswell. This fixes wire entities not connecting to Wire Map Interface entities on paste/startup. + + ent = WireLib.GetWireMapInterfaceSubEntityBySpawnIdDuped(spawnId) + if ent then + -- The spawnId is a custom id similar to ent:MapCreationID(), but it mostly survives duping. + return ent + end + + ent = WireLib.GetWireMapInterfaceSubEntityBySpawnId(spawnId) + if ent then + return ent + end + + return nil +end function WireLib.BuildDupeInfo( Ent ) if not Ent.Inputs then return {} end local info = { Wires = {} } for portname,input in pairs(Ent.Inputs) do - if (IsValid(input.Src)) then + local SrcEntity = input.Src + + if (IsValid(SrcEntity)) then + local wmiSpawnId = nil + if SrcEntity._IsWireMapInterfaceSubEntity then + wmiSpawnId = SrcEntity:_WMI_GetSpawnId() + end + info.Wires[portname] = { StartPos = input.StartPos, Material = input.Material, Color = input.Color, Width = input.Width, - Src = input.Src:EntIndex(), + Src = SrcEntity:EntIndex(), + SrcWmiSpawnId = wmiSpawnId, SrcId = input.SrcId, SrcPos = Vector(0, 0, 0), } @@ -858,8 +891,19 @@ function WireLib.BuildDupeInfo( Ent ) info.Wires[portname].Path = {} for _,v in ipairs(input.Path) do - if (IsValid(v.Entity)) then - table.insert(info.Wires[portname].Path, { Entity = v.Entity:EntIndex(), Pos = v.Pos }) + local vEntity = v.Entity + + if (IsValid(vEntity)) then + local vEntityWmiSpawnId = nil + if vEntity._IsWireMapInterfaceSubEntity then + vEntityWmiSpawnId = vEntity:_WMI_GetSpawnId() + end + + table.insert(info.Wires[portname].Path, { + Entity = vEntity:EntIndex(), + EntityWmiSpawnId = vEntityWmiSpawnId, + Pos = v.Pos + }) end end @@ -875,72 +919,84 @@ function WireLib.BuildDupeInfo( Ent ) return info end -function WireLib.ApplyDupeInfo( ply, ent, info, GetEntByID ) - if info.extended and not ent.extended then - WireLib.CreateWirelinkOutput( ply, ent, {true} ) -- old dupe compatibility; use the new function +hook.Add("Wire_ApplyDupeInfo", "Wire_AddWirelink", function(ply, inputEnt, outputEnt, inputData) + -- Wirelink and entity outputs + + -- These are required if whichever duplicator you're using does not do entity modifiers before it runs PostEntityPaste + -- because if so, the wirelink and entity outputs may not have been created yet + + if inputData.SrcId == "link" or inputData.SrcId == "wirelink" then -- If the target entity has no wirelink output, create one (& more old dupe compatibility) + inputData.SrcId = "wirelink" + if not outputEnt.extended then + WireLib.CreateWirelinkOutput( ply, outputEnt, {true} ) + end + elseif inputData.SrcId == "entity" and ((outputEnt.Outputs and not outputEnt.Outputs.entity) or not outputEnt.Outputs) then -- if the input name is 'entity', and the target entity doesn't have that output... + WireLib.CreateEntityOutput( ply, outputEnt, {true} ) + end +end) + +function WireLib.ApplyDupeInfo( ply, inputEnt, info, GetEntByID ) + if info.extended and not inputEnt.extended then + WireLib.CreateWirelinkOutput( ply, inputEnt, {true} ) -- old dupe compatibility; use the new function + end + + if not info.Wires then + return end local idx = 0 - if IsValid(ply) then idx = ply:UniqueID() end -- Map Save loading does not have a ply - if (info.Wires) then - for k,input in pairs(info.Wires) do - k=tostring(k) -- For some reason duplicator will parse strings containing numbers as numbers? - local ent2 = GetEntByID(input.Src) - - -- Input alias - if ent.Inputs and not ent.Inputs[k] then -- if the entity has any inputs and the input 'k' is not one of them... - if ent.InputAliases and ent.InputAliases[k] then - k = ent.InputAliases[k] - else - Msg("ApplyDupeInfo: Error, Could not find input '" .. k .. "' on entity type: '" .. ent:GetClass() .. "'\n") - continue - end - end + if IsValid(ply) then + -- Map Save loading does not have a ply + idx = ply:UniqueID() + end - if IsValid( ent2 ) then - -- Wirelink and entity outputs + for k, inputData in pairs(info.Wires) do + k = tostring(k) -- For some reason duplicator will parse strings containing numbers as numbers? + local outputEnt = LookupEntityByIdOrWmiId(inputData.Src, inputData.SrcWmiSpawnId, GetEntByID) - -- These are required if whichever duplicator you're using does not do entity modifiers before it runs PostEntityPaste - -- because if so, the wirelink and entity outputs may not have been created yet + -- Input alias + if inputEnt.Inputs and not inputEnt.Inputs[k] then -- if the entity has any inputs and the input 'k' is not one of them... + if inputEnt.InputAliases and inputEnt.InputAliases[k] then + k = inputEnt.InputAliases[k] + else + Msg("ApplyDupeInfo: Error, Could not find input '" .. k .. "' on entity type: '" .. inputEnt:GetClass() .. "'\n") + continue + end + end - if input.SrcId == "link" or input.SrcId == "wirelink" then -- If the target entity has no wirelink output, create one (& more old dupe compatibility) - input.SrcId = "wirelink" - if not ent2.extended then - WireLib.CreateWirelinkOutput( ply, ent2, {true} ) - end - elseif input.SrcId == "entity" and ((ent2.Outputs and not ent2.Outputs.entity) or not ent2.Outputs) then -- if the input name is 'entity', and the target entity doesn't have that output... - WireLib.CreateEntityOutput( ply, ent2, {true} ) - end + if IsValid( outputEnt ) then + -- Sometimes you have to prepair the connection entities, before actually linking them during duplication. + -- Such cases are the Wire Map Interface and Wirelink support. + hook.Run("Wire_ApplyDupeInfo", ply, inputEnt, outputEnt, inputData) - -- Output alias - if ent2.Outputs and not ent2.Outputs[input.SrcId] then -- if the target entity has any outputs and the output 'input.SrcId' is not one of them... - if ent2.OutputAliases and ent2.OutputAliases[input.SrcId] then - input.SrcId = ent2.OutputAliases[input.SrcId] - else - Msg("ApplyDupeInfo: Error, Could not find output '" .. input.SrcId .. "' on entity type: '" .. ent2:GetClass() .. "'\n") - continue - end + -- Output alias + if outputEnt.Outputs and not outputEnt.Outputs[inputData.SrcId] then -- if the target entity has any outputs and the output 'inputData.SrcId' is not one of them... + if outputEnt.OutputAliases and outputEnt.OutputAliases[inputData.SrcId] then + inputData.SrcId = outputEnt.OutputAliases[inputData.SrcId] + else + Msg("ApplyDupeInfo: Error, Could not find output '" .. inputData.SrcId .. "' on entity type: '" .. outputEnt:GetClass() .. "'\n") + continue end end + end - WireLib.Link_Start(idx, ent, input.StartPos, k, input.Material, input.Color, input.Width) + WireLib.Link_Start(idx, inputEnt, inputData.StartPos, k, inputData.Material, inputData.Color, inputData.Width) - if input.Path then - for _,v in ipairs(input.Path) do - local ent2 = GetEntByID(v.Entity) - if IsValid(ent2) then - WireLib.Link_Node(idx, ent2, v.Pos) - else - Msg("ApplyDupeInfo: Error, Could not find the entity for wire path\n") - end + if inputData.Path then + for _,v in ipairs(inputData.Path) do + local outputEnt = LookupEntityByIdOrWmiId(v.Entity, v.EntityWmiSpawnId, GetEntByID) + if IsValid(outputEnt) then + WireLib.Link_Node(idx, outputEnt, v.Pos) + else + Msg("ApplyDupeInfo: Error, Could not find the entity for wire path\n") end end + end - if IsValid(ent2) then - WireLib.Link_End(idx, ent2, input.SrcPos, input.SrcId) - else - Msg("ApplyDupeInfo: Error, Could not find the output entity\n") - end + if IsValid(outputEnt) then + WireLib.Link_End(idx, outputEnt, inputData.SrcPos, inputData.SrcId) + else + Msg("ApplyDupeInfo: Error, Could not find the output entity\n") end end end diff --git a/wiremod.fgd b/wiremod.fgd index 42d45d357c..6fa4e18a63 100644 --- a/wiremod.fgd +++ b/wiremod.fgd @@ -2,7 +2,7 @@ : "Creates an interface between the map and Wiremod." [ wire_entity_name(target_destination) : "Wire Entity" : "" : "The name of an entity or of the entities that will get wire ports." - min_trigger_time(float) : "Minimum Trigger Time" : "0.01" : "Set the minimum time in seconds between two in-/output triggers. It's usefull to prevent lags." + min_trigger_time(float) : "Minimum Trigger Time" : "0.01" : "Set the minimum time in seconds between two Wiremod input triggers. It's usefull to prevent lags." // Wire Inputs input1_name(string) : "Wire Input 1 Name" : "" : "Sets the name of Wire Input 1." @@ -19,9 +19,6 @@ 8 : "Array" ] input1_desc(string) : "Wire Input 1 Description" : "" : "Sets the description of Wire Input 1." - input1_lua(string) : "Wire Input 1 Code" : "" // Hammer doens't support new lines, so I have to use spaces. - : "For advanced users! Sets the Lua code that gets called when Wire Input 1 gets triggered. The globals are: WIRE_NAME = input name, WIRE_VALUE = input value, WIRE_WIRED = true if input is wired, WIRE_CALLER = this entity, WIRE_ACTIVATOR = the input entity" - input2_name(string) : "Wire Input 2 Name" : "" : "Sets the name of Wire Input 2." input2_type(choices) : "Wire Input 2 Type" : 0 : "Sets the type of Wire Input 2." = @@ -37,9 +34,6 @@ 8 : "Array" ] input2_desc(string) : "Wire Input 2 Description" : "" : "Sets the description of Wire Input 2." - input2_lua(string) : "Wire Input 2 Code" : "" // Hammer doens't support new lines, so I have to use spaces. - : "For advanced users! Sets the Lua code that gets called when Wire Input 2 gets triggered. The globals are: WIRE_NAME = input name, WIRE_VALUE = input value, WIRE_WIRED = true if input is wired, WIRE_CALLER = this entity, WIRE_ACTIVATOR = the input entity" - input3_name(string) : "Wire Input 3 Name" : "" : "Sets the name of Wire Input 3." input3_type(choices) : "Wire Input 3 Type" : 0 : "Sets the type of Wire Input 3." = @@ -55,9 +49,6 @@ 8 : "Array" ] input3_desc(string) : "Wire Input 3 Description" : "" : "Sets the description of Wire Input 3." - input3_lua(string) : "Wire Input 3 Code" : "" // Hammer doens't support new lines, so I have to use spaces. - : "For advanced users! Sets the Lua code that gets called when Wire Input 3 gets triggered. The globals are: WIRE_NAME = input name, WIRE_VALUE = input value, WIRE_WIRED = true if input is wired, WIRE_CALLER = this entity, WIRE_ACTIVATOR = the input entity" - input4_name(string) : "Wire Input 4 Name" : "" : "Sets the name of Wire Input 4." input4_type(choices) : "Wire Input 4 Type" : 0 : "Sets the type of Wire Input 4." = @@ -73,9 +64,6 @@ 8 : "Array" ] input4_desc(string) : "Wire Input 4 Description" : "" : "Sets the description of Wire Input 4." - input4_lua(string) : "Wire Input 4 Code" : "" // Hammer doens't support new lines, so I have to use spaces. - : "For advanced users! Sets the Lua code that gets called when Wire Input 4 gets triggered. The globals are: WIRE_NAME = input name, WIRE_VALUE = input value, WIRE_WIRED = true if input is wired, WIRE_CALLER = this entity, WIRE_ACTIVATOR = the input entity" - input5_name(string) : "Wire Input 5 Name" : "" : "Sets the name of Wire Input 5." input5_type(choices) : "Wire Input 5 Type" : 0 : "Sets the type of Wire Input 5." = @@ -91,9 +79,6 @@ 8 : "Array" ] input5_desc(string) : "Wire Input 5 Description" : "" : "Sets the description of Wire Input 5." - input5_lua(string) : "Wire Input 5 Code" : "" // Hammer doens't support new lines, so I have to use spaces. - : "For advanced users! Sets the Lua code that gets called when Wire Input 5 gets triggered. The globals are: WIRE_NAME = input name, WIRE_VALUE = input value, WIRE_WIRED = true if input is wired, WIRE_CALLER = this entity, WIRE_ACTIVATOR = the input entity" - input6_name(string) : "Wire Input 6 Name" : "" : "Sets the name of Wire Input 6." input6_type(choices) : "Wire Input 6 Type" : 0 : "Sets the type of Wire Input 6." = @@ -109,9 +94,6 @@ 8 : "Array" ] input6_desc(string) : "Wire Input 6 Description" : "" : "Sets the description of Wire Input 6." - input6_lua(string) : "Wire Input 6 Code" : "" // Hammer doens't support new lines, so I have to use spaces. - : "For advanced users! Sets the Lua code that gets called when Wire Input 6 gets triggered. The globals are: WIRE_NAME = input name, WIRE_VALUE = input value, WIRE_WIRED = true if input is wired, WIRE_CALLER = this entity, WIRE_ACTIVATOR = the input entity" - input7_name(string) : "Wire Input 7 Name" : "" : "Sets the name of Wire Input 7." input7_type(choices) : "Wire Input 7 Type" : 0 : "Sets the type of Wire Input 7." = @@ -127,9 +109,6 @@ 8 : "Array" ] input7_desc(string) : "Wire Input 7 Description" : "" : "Sets the description of Wire Input 7." - input7_lua(string) : "Wire Input 7 Code" : "" // Hammer doens't support new lines, so I have to use spaces. - : "For advanced users! Sets the Lua code that gets called when Wire Input 7 gets triggered. The globals are: WIRE_NAME = input name, WIRE_VALUE = input value, WIRE_WIRED = true if input is wired, WIRE_CALLER = this entity, WIRE_ACTIVATOR = the input entity" - input8_name(string) : "Wire Input 8 Name" : "" : "Sets the name of Wire Input 8." input8_type(choices) : "Wire Input 8 Type" : 0 : "Sets the type of Wire Input 8." = @@ -145,9 +124,6 @@ 8 : "Array" ] input8_desc(string) : "Wire Input 8 Description" : "" : "Sets the description of Wire Input 8." - input8_lua(string) : "Wire Input 8 Code" : "" // Hammer doens't support new lines, so I have to use spaces. - : "For advanced users! Sets the Lua code that gets called when Wire Input 8 gets triggered. The globals are: WIRE_NAME = input name, WIRE_VALUE = input value, WIRE_WIRED = true if input is wired, WIRE_CALLER = this entity, WIRE_ACTIVATOR = the input entity" - // Wire Outputs @@ -299,11 +275,11 @@ output OnResetWireInput7(void) : "Fires when Wire Input 7 gets reset." output OnResetWireInput8(void) : "Fires when Wire Input 8 gets reset." - output OnWireEntsCreated(void) : "Fires when Wire In-/Outputs entities have been created. The entity is ready to use." - output OnWireEntsRemoved(void) : "Fires when Wire In-/Outputs entities have been removed. The entity is ready to use." - output OnWireEntsReady(void) : "Fires when Wire In-/Outputs entities have been created or removed. The entity is ready to use." + output OnWireEntsCreated(void) : "Fires when Wire In-/Outputs entities have been created." + output OnWireEntsRemoved(void) : "Fires when Wire In-/Outputs entities have been removed." + output OnWireEntsReady(void) : "Fires when Wire In-/Outputs entities have been created or removed." - output OnWireEntsStartChanging(void) : "Fires when the wire map interface entity is adding or removing Wire In-/Outputs entities. The entity can't be used yet." + output OnWireEntsStartChanging(void) : "Fires when the wire map interface entity starts adding or removing Wire In-/Outputs entities." // Entity Inputs @@ -331,10 +307,9 @@ // Spawnflags spawnflags(flags) = [ - 1 : "Protect in-/output entities from non-wire tools (Useful to allow wire tools on already protected stuff)" : 0 + 1 : "Protect in-/output entities from non-wire and duplicator tools" : 0 2 : "Protect in-/output entities from the physgun" : 0 4 : "Remove in-/output entities on remove" : 1 - 8 : "Run given Lua codes (For advanced users!)" : 0 16 : "Start Active" : 1 32 : "Render Wires" : 1 ] From d3cda7601c1f95c756ad964b437bf9461731ce56 Mon Sep 17 00:00:00 2001 From: Grocel Date: Sun, 22 Jun 2025 16:21:58 +0200 Subject: [PATCH 02/22] WMI: Only add hooks if an info_wiremapinterface spawns. --- .../info_wiremapinterface/entityoverride.lua | 48 +++--- lua/entities/info_wiremapinterface/init.lua | 2 + lua/wire/client/cl_wire_map_interface.lua | 146 ++++++++++-------- 3 files changed, 109 insertions(+), 87 deletions(-) diff --git a/lua/entities/info_wiremapinterface/entityoverride.lua b/lua/entities/info_wiremapinterface/entityoverride.lua index df3b2f593e..3dd847b382 100644 --- a/lua/entities/info_wiremapinterface/entityoverride.lua +++ b/lua/entities/info_wiremapinterface/entityoverride.lua @@ -829,30 +829,40 @@ function ENT:OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo) OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo, self) end -duplicator.RegisterEntityModifier("WireMapInterfaceEntDupeInfo", function(ply, wireEnt, wireMapInterfaceEntDupeInfo) - -- Make sure the Wire Map Interface sub entity when duped, so it can be found and linked too. - OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo) -end) +local g_dupeHooksAdded = false -hook.Add("Wire_ApplyDupeInfo", "Wire_InitFromWireMapInterfaceEntDupeInfo", function(ply, inputEnt, outputEnt, inputData) - -- Make sure we initialize the Wire Map Interface sub entity before connecting our inputs to it. It will not initialize twice. +function ENT:AddDupeHooks() + if g_dupeHooksAdded then + return + end + + duplicator.RegisterEntityModifier("WireMapInterfaceEntDupeInfo", function(ply, wireEnt, wireMapInterfaceEntDupeInfo) + -- Make sure to prepair the Wire Map Interface sub entity when duped, so it can be found and linked too. + OverrideEntFromDupe(wireEnt, wireMapInterfaceEntDupeInfo) + end) + + hook.Add("Wire_ApplyDupeInfo", "Wire_InitFromWireMapInterfaceEntDupeInfo", function(ply, inputEnt, outputEnt, inputData) + -- Make sure we initialize the Wire Map Interface sub entity before connecting our inputs to it. It will not initialize twice. - local entityMods = inputEnt.EntityMods - if entityMods then - local wireMapInterfaceEntDupeInfo = entityMods.WireMapInterfaceEntDupeInfo + local entityMods = inputEnt.EntityMods + if entityMods then + local wireMapInterfaceEntDupeInfo = entityMods.WireMapInterfaceEntDupeInfo - if wireMapInterfaceEntDupeInfo then - OverrideEntFromDupe(inputEnt, wireMapInterfaceEntDupeInfo) + if wireMapInterfaceEntDupeInfo then + OverrideEntFromDupe(inputEnt, wireMapInterfaceEntDupeInfo) + end end - end - local entityMods = outputEnt.EntityMods - if entityMods then - local wireMapInterfaceEntDupeInfo = entityMods.WireMapInterfaceEntDupeInfo + local entityMods = outputEnt.EntityMods + if entityMods then + local wireMapInterfaceEntDupeInfo = entityMods.WireMapInterfaceEntDupeInfo - if wireMapInterfaceEntDupeInfo then - OverrideEntFromDupe(outputEnt, wireMapInterfaceEntDupeInfo) + if wireMapInterfaceEntDupeInfo then + OverrideEntFromDupe(outputEnt, wireMapInterfaceEntDupeInfo) + end end - end -end) + end) + + g_dupeHooksAdded = true +end diff --git a/lua/entities/info_wiremapinterface/init.lua b/lua/entities/info_wiremapinterface/init.lua index 82bf8391b1..ae114a5dfa 100644 --- a/lua/entities/info_wiremapinterface/init.lua +++ b/lua/entities/info_wiremapinterface/init.lua @@ -255,6 +255,8 @@ function ENT:Initialize() self.PortsUpdated = true self.NextNetworkTime = CurTime() + (1 + math.random() * 2) * (self.MIN_THINK_TIME * 4) + + self:AddDupeHooks() self:AttachToSaveStateEntity() end diff --git a/lua/wire/client/cl_wire_map_interface.lua b/lua/wire/client/cl_wire_map_interface.lua index ea12af342e..165ad577bb 100644 --- a/lua/wire/client/cl_wire_map_interface.lua +++ b/lua/wire/client/cl_wire_map_interface.lua @@ -18,6 +18,7 @@ local g_wireTools = { local g_wiredEntities = {} local g_wiredEntitiesRemove = {} local g_nextThink = 0 +local g_hooksAdded = false -- Remove wire stuff and other changes that were done. local function RemoveWire(item) @@ -123,42 +124,6 @@ local function AddWire(item) end end -net.Receive("WireMapInterfaceEntities", function() - local wmiId = net.ReadUInt(MAX_EDICT_BITS) - local protectFromTools = net.ReadBool() - local protectFromPhysgun = net.ReadBool() - local renderWires = net.ReadBool() - local count = net.ReadUInt(6) - - local wmiWiredEntities = g_wiredEntities[wmiId] or {} - g_wiredEntities[wmiId] = wmiWiredEntities - - local wmiWiredEntitiesRemove = g_wiredEntitiesRemove[wmiId] or {} - g_wiredEntitiesRemove[wmiId] = wmiWiredEntitiesRemove - - for entId, item in pairs(wmiWiredEntities) do - -- Clear all that belongs to the current WMI. - wmiWiredEntitiesRemove[entId] = item - end - - for i = 1, count do - local entId = net.ReadUInt(MAX_EDICT_BITS) - - -- Unclear listed items. - wmiWiredEntitiesRemove[entId] = nil - - -- Add listed items. - local item = wmiWiredEntities[entId] or {} - wmiWiredEntities[entId] = item - - item.entId = entId - item.wmiId = wmiId - item.protectFromTools = protectFromTools - item.protectFromPhysgun = protectFromPhysgun - item.renderWires = renderWires - end -end) - local function pollWireItems() for _, wmiWiredEntitiesRemove in pairs(g_wiredEntitiesRemove) do for _, item in pairs(wmiWiredEntitiesRemove) do @@ -183,49 +148,94 @@ local function pollWireItems() end end -hook.Add("PostCleanupMap", "WireMapInterface_PostCleanupMap_CL", function() - table.Empty(g_wiredEntities) - table.Empty(g_wiredEntitiesRemove) - g_nextThink = 0 -end) +local function AddHooks() + hook.Add("PostCleanupMap", "WireMapInterface_PostCleanupMap_CL", function() + table.Empty(g_wiredEntities) + table.Empty(g_wiredEntitiesRemove) + g_nextThink = 0 + end) -hook.Add("Think", "WireMapInterface_Think", function() - local now = CurTime() + hook.Add("Think", "WireMapInterface_Think", function() + local now = CurTime() - if now < g_nextThink then - return - end + if now < g_nextThink then + return + end - g_nextThink = now + 1 + g_nextThink = now + 1 - pollWireItems() + pollWireItems() - -- Render bounds updating - for _, wmiWiredEntities in pairs(g_wiredEntities) do - for _, item in pairs(wmiWiredEntities) do - if item.init and item.renderWires and now >= item.nextRenderBoundsUpdate then - local ent = item.ent + -- Render bounds updating + for _, wmiWiredEntities in pairs(g_wiredEntities) do + for _, item in pairs(wmiWiredEntities) do + if item.init and item.renderWires and now >= item.nextRenderBoundsUpdate then + local ent = item.ent - if IsValid(ent) and not ent:IsDormant() then - Wire_UpdateRenderBounds(ent) - item.nextRenderBoundsUpdate = now + math.random(30, 100) / 10 + if IsValid(ent) and not ent:IsDormant() then + Wire_UpdateRenderBounds(ent) + item.nextRenderBoundsUpdate = now + math.random(30, 100) / 10 + end end end end - end -end) - --- Rendering -hook.Add("PostDrawOpaqueRenderables", "WireMapInterface_Draw", function() - for _, wmiWiredEntities in pairs(g_wiredEntities) do - for _, item in pairs(wmiWiredEntities) do - if item.init and item.renderWires then - local ent = item.ent - - if IsValid(ent) and not ent:IsDormant() then - Wire_Render(ent) + end) + + -- Rendering + hook.Add("PostDrawOpaqueRenderables", "WireMapInterface_Draw", function() + for _, wmiWiredEntities in pairs(g_wiredEntities) do + for _, item in pairs(wmiWiredEntities) do + if item.init and item.renderWires then + local ent = item.ent + + if IsValid(ent) and not ent:IsDormant() then + Wire_Render(ent) + end end end end + end) + + g_hooksAdded = true +end + +net.Receive("WireMapInterfaceEntities", function() + local wmiId = net.ReadUInt(MAX_EDICT_BITS) + local protectFromTools = net.ReadBool() + local protectFromPhysgun = net.ReadBool() + local renderWires = net.ReadBool() + local count = net.ReadUInt(6) + + local wmiWiredEntities = g_wiredEntities[wmiId] or {} + g_wiredEntities[wmiId] = wmiWiredEntities + + local wmiWiredEntitiesRemove = g_wiredEntitiesRemove[wmiId] or {} + g_wiredEntitiesRemove[wmiId] = wmiWiredEntitiesRemove + + for entId, item in pairs(wmiWiredEntities) do + -- Clear all that belongs to the current WMI. + wmiWiredEntitiesRemove[entId] = item + end + + for i = 1, count do + local entId = net.ReadUInt(MAX_EDICT_BITS) + + -- Unclear listed items. + wmiWiredEntitiesRemove[entId] = nil + + -- Add listed items. + local item = wmiWiredEntities[entId] or {} + wmiWiredEntities[entId] = item + + item.entId = entId + item.wmiId = wmiId + item.protectFromTools = protectFromTools + item.protectFromPhysgun = protectFromPhysgun + item.renderWires = renderWires + end + + if not g_hooksAdded and not table.IsEmpty(wmiWiredEntities) then + AddHooks() end end) + From 0da860d31c65d873ab1d57b18e2a6b45ad8d4b11 Mon Sep 17 00:00:00 2001 From: Grocel <2457653+Grocel@users.noreply.github.com> Date: Wed, 2 Jul 2025 01:46:48 +0200 Subject: [PATCH 03/22] shorter id checks Co-authored-by: thegrb93 --- lua/wire/server/wire_map_interface.lua | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/lua/wire/server/wire_map_interface.lua b/lua/wire/server/wire_map_interface.lua index 0461264a3d..8fcf4b67d3 100644 --- a/lua/wire/server/wire_map_interface.lua +++ b/lua/wire/server/wire_map_interface.lua @@ -141,26 +141,7 @@ function WireLib.GetWireMapInterfaceSubEntityByMapId(wireEntMapId, alsoFindViaEn end function WireLib.WireMapInterfaceValidateId(id) - if not id then - return false - end - - id = tonumber(id or 0) or 0 - - if id ~= id then - -- Is it NaN? - return false - end - - if id < -1 then - -- Ids of -1 are valid, lower than that is not. - return false - end - - if id > 0xFFFF then - -- Legit ids > 65k are extremly unlikely or even impossible to happen. - return false - end - - return true + id = tonumber(id) + if not id then return false end + return id==id and id >= -1 and id <= 0xFFFF end \ No newline at end of file From 3d0b1dd403d6c90ece8a43dec593934e2284447e Mon Sep 17 00:00:00 2001 From: Grocel <2457653+Grocel@users.noreply.github.com> Date: Wed, 2 Jul 2025 01:49:31 +0200 Subject: [PATCH 04/22] Only cleanup existing values Co-authored-by: thegrb93 --- lua/wire/server/wire_map_interface.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wire/server/wire_map_interface.lua b/lua/wire/server/wire_map_interface.lua index 8fcf4b67d3..9d35017f34 100644 --- a/lua/wire/server/wire_map_interface.lua +++ b/lua/wire/server/wire_map_interface.lua @@ -92,7 +92,7 @@ function WireLib.GetWireMapInterfaceSubEntityBySpawnId(wireEntSpawnId) end local wireEnt = g_spawnIdRegisterBySpawnId[wireEntSpawnId] - if not IsValid(wireEnt) then + if wireEnt and not wireEnt:IsValid() then g_spawnIdRegisterBySpawnId[wireEntSpawnId] = nil CleanupRegister() From 15c5f67133fd830d3117208845a7fe6ee19c2b3e Mon Sep 17 00:00:00 2001 From: Grocel Date: Wed, 2 Jul 2025 01:57:16 +0200 Subject: [PATCH 05/22] add spacing --- lua/wire/server/wire_map_interface.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/wire/server/wire_map_interface.lua b/lua/wire/server/wire_map_interface.lua index 9d35017f34..4cbac916a5 100644 --- a/lua/wire/server/wire_map_interface.lua +++ b/lua/wire/server/wire_map_interface.lua @@ -1,5 +1,4 @@ -- Serverside functionalities of Wire Map Interface - WireLib._WireMapInterfaceSpawnIdRegister = WireLib._WireMapInterfaceSpawnIdRegister or {} local g_spawnIdRegisterBySpawnId = WireLib._WireMapInterfaceSpawnIdRegister.bySpawnId or {} @@ -143,5 +142,5 @@ end function WireLib.WireMapInterfaceValidateId(id) id = tonumber(id) if not id then return false end - return id==id and id >= -1 and id <= 0xFFFF + return id == id and id >= -1 and id <= 0xFFFF end \ No newline at end of file From 2d65701210f5e77eb4f7c93c6f04746f4f165b7f Mon Sep 17 00:00:00 2001 From: Grocel Date: Wed, 2 Jul 2025 02:34:28 +0200 Subject: [PATCH 06/22] Refactored wire_map_interface.lua in suggested style Co-Authored-By: thegrb93 --- .../info_wiremapinterface/entityoverride.lua | 6 +- .../info_wiremapinterface/savestate.lua | 13 +- lua/wire/server/wire_map_interface.lua | 174 +++++------------- lua/wire/server/wirelib.lua | 8 +- 4 files changed, 64 insertions(+), 137 deletions(-) diff --git a/lua/entities/info_wiremapinterface/entityoverride.lua b/lua/entities/info_wiremapinterface/entityoverride.lua index 3dd847b382..55524255c2 100644 --- a/lua/entities/info_wiremapinterface/entityoverride.lua +++ b/lua/entities/info_wiremapinterface/entityoverride.lua @@ -1,6 +1,7 @@ -- Stuff that the entity gets for its wire stuff. local WireLib = WireLib +local WireMapInterfaceLookup = WireLib.WireMapInterfaceLookup local WIREENT = {} @@ -552,7 +553,7 @@ function WIREENT:_WMI_RemoveOverrides(interfaceEnt) local spawnIdDuped = WIREENT._WMI_GetSpawnIdDuped(self, interfaceEnt) local mapId = WIREENT._WMI_MapCreationIdDuped(self) - WireLib.UnregisterWireMapInterfaceSpawnId(spawnId, spawnIdDuped, mapId, self) + WireMapInterfaceLookup:remove(spawnId, spawnIdDuped, mapId, self) WireLib.Remove(self) if IsValid(interfaceEnt) then @@ -616,8 +617,7 @@ function WIREENT:_WMI_OverrideEnt(interfaceEnt) local mapId = WIREENT._WMI_MapCreationIdDuped(self) self._WireMapInterfaceEnt_MapId = mapId - - WireLib.RegisterWireMapInterfaceSpawnId(spawnId, spawnIdDuped, mapId, self) + WireMapInterfaceLookup:add(spawnId, spawnIdDuped, mapId, self) local oldMembers = wmidata.oldMembers or {} wmidata.oldMembers = oldMembers diff --git a/lua/entities/info_wiremapinterface/savestate.lua b/lua/entities/info_wiremapinterface/savestate.lua index 51fe5e8b0a..69d1a0f98e 100644 --- a/lua/entities/info_wiremapinterface/savestate.lua +++ b/lua/entities/info_wiremapinterface/savestate.lua @@ -1,6 +1,7 @@ -- Dupe and save support and validation local WireLib = WireLib +local WireMapInterfaceLookup = WireLib.WireMapInterfaceLookup local g_saveStateEntity = nil local g_mapName = nil @@ -67,16 +68,20 @@ function ENT:GetSubEntityByIdCombo(entIdx, mapId, spawnId, createdEntities) end if not createdEntities or not IsValid(wireEnt) then - wireEnt = WireLib.GetWireMapInterfaceSubEntityBySpawnIdDuped(spawnId) + wireEnt = WireMapInterfaceLookup:getBySpawnIDDuped(spawnId) if not IsValid(wireEnt) then - wireEnt = WireLib.GetWireMapInterfaceSubEntityByMapId(mapId, true) + wireEnt = WireMapInterfaceLookup:getByMapID(mapId) if not IsValid(wireEnt) then - wireEnt = WireLib.GetWireMapInterfaceSubEntityBySpawnId(spawnId) + wireEnt = ents.GetMapCreatedEntity(mapId) if not IsValid(wireEnt) then - return nil + wireEnt = WireMapInterfaceLookup:getBySpawnID(spawnId) + + if not IsValid(wireEnt) then + return nil + end end end end diff --git a/lua/wire/server/wire_map_interface.lua b/lua/wire/server/wire_map_interface.lua index 4cbac916a5..7296473958 100644 --- a/lua/wire/server/wire_map_interface.lua +++ b/lua/wire/server/wire_map_interface.lua @@ -1,146 +1,64 @@ -- Serverside functionalities of Wire Map Interface -WireLib._WireMapInterfaceSpawnIdRegister = WireLib._WireMapInterfaceSpawnIdRegister or {} -local g_spawnIdRegisterBySpawnId = WireLib._WireMapInterfaceSpawnIdRegister.bySpawnId or {} -WireLib._WireMapInterfaceSpawnIdRegister.bySpawnId = g_spawnIdRegisterBySpawnId +local function validateId(id) + id = tonumber(id) + if not id then return false end + return id == id and id >= -1 and id <= 0xFFFF +end -local g_spawnIdRegisterBySpawnIdDuped = WireLib._WireMapInterfaceSpawnIdRegister.bySpawnIdDuped or {} -WireLib._WireMapInterfaceSpawnIdRegister.bySpawnIdDuped = g_spawnIdRegisterBySpawnIdDuped +WireLib.WireMapInterfaceValidateId = validateId -local g_spawnIdRegisterByMapId = WireLib._WireMapInterfaceSpawnIdRegister.byMapId or {} -WireLib._WireMapInterfaceSpawnIdRegister.byMapId = g_spawnIdRegisterByMapId +WireLib.WireMapInterfaceLookup = { + entsBySpawnID = {}, + entsBySpawnIDDuped = {}, + entsByMapID = {}, -local g_nextCleanup = 0 + add = function(self, wireEntSpawnId, wireEntSpawnIdDuped, wireEntMapId, wireEnt) + if not wireEntSpawnId then + -- Never add entities without spawn id, e.g. when not assignable. + return + end -local function CleanupRegister() - local now = CurTime() + self.entsBySpawnID[wireEntSpawnId] = wireEnt - if now < g_nextCleanup then - -- Only cleanup if the last call was more than 10 minutes ago. - return - end + if wireEntSpawnIdDuped then + self.entsBySpawnIDDuped[wireEntSpawnIdDuped] = wireEnt + end - for wireEntSpawnId, wireEnt in ipairs(g_spawnIdRegisterBySpawnId) do - if not IsValid(wireEnt) then - g_spawnIdRegisterBySpawnId[wireEntSpawnId] = nil + if validateId(wireEntMapId) then + self.entsByMapID[wireEntMapId] = wireEnt end - end - for wireEntSpawnIdDuped, wireEnt in ipairs(g_spawnIdRegisterBySpawnIdDuped) do - if not IsValid(wireEnt) then - g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] = nil - end - end + wireEnt:CallOnRemove("WireMapInterfaceLookupRemove", function() + self:remove(wireEntSpawnId, wireEntSpawnIdDuped, wireEntMapId, wireEnt) + end) + end, - for wireEntMapId, wireEnt in ipairs(g_spawnIdRegisterByMapId) do - if not IsValid(wireEnt) then - g_spawnIdRegisterByMapId[wireEntMapId] = nil + remove = function(self, wireEntSpawnId, wireEntSpawnIdDuped, wireEntMapId, wireEnt) + if not wireEntSpawnId then + self.entsBySpawnID[wireEntSpawnId] = nil end - end - - g_nextCleanup = now + 60 * 10 -end - -function WireLib.RegisterWireMapInterfaceSpawnId(wireEntSpawnId, wireEntSpawnIdDuped, wireEntMapId, wireEnt) - if not wireEntSpawnId then - return - end - - if not wireEntMapId then - return - end - - if not IsValid(wireEnt) then - return - end - - g_spawnIdRegisterBySpawnId[wireEntSpawnId] = wireEnt - - if wireEntSpawnIdDuped then - g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] = wireEnt - end - - g_spawnIdRegisterByMapId[wireEntMapId] = wireEnt - - CleanupRegister() -end - -function WireLib.UnregisterWireMapInterfaceSpawnId(wireEntSpawnId, wireEntSpawnIdDuped, wireEntMapId) - if not wireEntSpawnId then - return - end - - if not wireEntMapId then - return - end - - g_spawnIdRegisterBySpawnId[wireEntSpawnId] = nil - - if wireEntSpawnIdDuped then - g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] = nil - end - g_spawnIdRegisterByMapId[wireEntMapId] = nil + if wireEntSpawnIdDuped then + self.entsBySpawnIDDuped[wireEntSpawnIdDuped] = nil + end - CleanupRegister() -end - -function WireLib.GetWireMapInterfaceSubEntityBySpawnId(wireEntSpawnId) - if not wireEntSpawnId then - return nil - end - - local wireEnt = g_spawnIdRegisterBySpawnId[wireEntSpawnId] - if wireEnt and not wireEnt:IsValid() then - g_spawnIdRegisterBySpawnId[wireEntSpawnId] = nil - CleanupRegister() - - return nil - end - - return wireEnt -end - -function WireLib.GetWireMapInterfaceSubEntityBySpawnIdDuped(wireEntSpawnIdDuped) - if not wireEntSpawnIdDuped then - return nil - end - - local wireEnt = g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] - if not IsValid(wireEnt) then - g_spawnIdRegisterBySpawnIdDuped[wireEntSpawnIdDuped] = nil - CleanupRegister() - - return nil - end - - return wireEnt -end + if validateId(wireEntMapId) then + self.entsByMapID[wireEntMapId] = nil + end -function WireLib.GetWireMapInterfaceSubEntityByMapId(wireEntMapId, alsoFindViaEngine) - if not wireEntMapId then - return nil - end + wireEnt:RemoveCallOnRemove("WireMapInterfaceLookupRemove") + end, - local wireEnt = g_spawnIdRegisterByMapId[wireEntMapId] - if not IsValid(wireEnt) then - g_spawnIdRegisterByMapId[wireEntMapId] = nil - CleanupRegister() + getBySpawnID = function(self, wireEntSpawnId) + return self.entsBySpawnID[wireEntSpawnId] + end, - if alsoFindViaEngine then - wireEnt = ents.GetMapCreatedEntity(wireEntMapId) + getBySpawnIDDuped = function(self, wireEntSpawnIdDuped) + return self.entsBySpawnIDDuped[wireEntSpawnIdDuped] + end, - if not IsValid(wireEnt) then - return nil - end - end - end - - return wireEnt -end - -function WireLib.WireMapInterfaceValidateId(id) - id = tonumber(id) - if not id then return false end - return id == id and id >= -1 and id <= 0xFFFF -end \ No newline at end of file + getByMapID = function(self, wireEntMapId) + return self.entsByMapID[wireEntMapId] + end, +} diff --git a/lua/wire/server/wirelib.lua b/lua/wire/server/wirelib.lua index a0ad88106b..b63b3107b8 100644 --- a/lua/wire/server/wirelib.lua +++ b/lua/wire/server/wirelib.lua @@ -853,13 +853,17 @@ local function LookupEntityByIdOrWmiId(entIdx, spawnId, GetEntByID) -- because the linked entity might be not duplicatable, but still belongs to the contraption. -- In this case lookup the entity in an additional list aswell. This fixes wire entities not connecting to Wire Map Interface entities on paste/startup. - ent = WireLib.GetWireMapInterfaceSubEntityBySpawnIdDuped(spawnId) + if not spawnId then + return nil + end + + ent = WireLib.WireMapInterfaceLookup:getBySpawnIDDuped(spawnId) if ent then -- The spawnId is a custom id similar to ent:MapCreationID(), but it mostly survives duping. return ent end - ent = WireLib.GetWireMapInterfaceSubEntityBySpawnId(spawnId) + ent = WireLib.WireMapInterfaceLookup:getBySpawnID(spawnId) if ent then return ent end From bdb655f1dfef49aa0a7b9989911429c15cbfa19a Mon Sep 17 00:00:00 2001 From: Grocel <2457653+Grocel@users.noreply.github.com> Date: Wed, 2 Jul 2025 03:33:58 +0200 Subject: [PATCH 07/22] no need to check for NaN, so remove it Co-authored-by: thegrb93 --- lua/wire/server/wire_map_interface.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/wire/server/wire_map_interface.lua b/lua/wire/server/wire_map_interface.lua index 7296473958..e3b496d4e8 100644 --- a/lua/wire/server/wire_map_interface.lua +++ b/lua/wire/server/wire_map_interface.lua @@ -2,8 +2,7 @@ local function validateId(id) id = tonumber(id) - if not id then return false end - return id == id and id >= -1 and id <= 0xFFFF + return id ~= nil and id >= -1 and id <= 0xFFFF end WireLib.WireMapInterfaceValidateId = validateId From 188374afd344a54b49c8d392cf6db8283ab96f5e Mon Sep 17 00:00:00 2001 From: Grocel <2457653+Grocel@users.noreply.github.com> Date: Fri, 4 Jul 2025 00:37:35 +0200 Subject: [PATCH 08/22] validate `info.Wires` to be a table Co-authored-by: thegrb93 --- lua/wire/server/wirelib.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wire/server/wirelib.lua b/lua/wire/server/wirelib.lua index b63b3107b8..b37d6f64a0 100644 --- a/lua/wire/server/wirelib.lua +++ b/lua/wire/server/wirelib.lua @@ -948,7 +948,7 @@ function WireLib.ApplyDupeInfo( ply, inputEnt, info, GetEntByID ) WireLib.CreateWirelinkOutput( ply, inputEnt, {true} ) -- old dupe compatibility; use the new function end - if not info.Wires then + if not istable(info.Wires) then return end From 49165af7564197aa3a2f8b326063fe46b11f2140 Mon Sep 17 00:00:00 2001 From: Grocel <2457653+Grocel@users.noreply.github.com> Date: Fri, 4 Jul 2025 00:43:59 +0200 Subject: [PATCH 09/22] More compact `SrcWmiSpawnId` setter Co-authored-by: thegrb93 --- lua/wire/server/wirelib.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wire/server/wirelib.lua b/lua/wire/server/wirelib.lua index b37d6f64a0..a253a41c4e 100644 --- a/lua/wire/server/wirelib.lua +++ b/lua/wire/server/wirelib.lua @@ -890,7 +890,7 @@ function WireLib.BuildDupeInfo( Ent ) Color = input.Color, Width = input.Width, Src = SrcEntity:EntIndex(), - SrcWmiSpawnId = wmiSpawnId, + SrcWmiSpawnId = SrcEntity._IsWireMapInterfaceSubEntity and SrcEntity:_WMI_GetSpawnId() or nil, SrcId = input.SrcId, SrcPos = Vector(0, 0, 0), } From b6617039d5759b9b113f2bc48b5b969a5720b353 Mon Sep 17 00:00:00 2001 From: Grocel <2457653+Grocel@users.noreply.github.com> Date: Fri, 4 Jul 2025 00:44:33 +0200 Subject: [PATCH 10/22] More compact `EntityWmiSpawnId` setter Co-authored-by: thegrb93 --- lua/wire/server/wirelib.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wire/server/wirelib.lua b/lua/wire/server/wirelib.lua index a253a41c4e..4ab4bacf85 100644 --- a/lua/wire/server/wirelib.lua +++ b/lua/wire/server/wirelib.lua @@ -909,7 +909,7 @@ function WireLib.BuildDupeInfo( Ent ) table.insert(info.Wires[portname].Path, { Entity = vEntity:EntIndex(), - EntityWmiSpawnId = vEntityWmiSpawnId, + EntityWmiSpawnId = vEntity._IsWireMapInterfaceSubEntity and vEntity:_WMI_GetSpawnId() or nil, Pos = v.Pos }) end From 42db92f14aec49cc87e7e0ec6a99ac84c561cbfb Mon Sep 17 00:00:00 2001 From: Grocel Date: Fri, 4 Jul 2025 00:51:24 +0200 Subject: [PATCH 11/22] Remove unused vars --- lua/wire/server/wirelib.lua | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/lua/wire/server/wirelib.lua b/lua/wire/server/wirelib.lua index 4ab4bacf85..99f2f4811d 100644 --- a/lua/wire/server/wirelib.lua +++ b/lua/wire/server/wirelib.lua @@ -878,12 +878,7 @@ function WireLib.BuildDupeInfo( Ent ) for portname,input in pairs(Ent.Inputs) do local SrcEntity = input.Src - if (IsValid(SrcEntity)) then - local wmiSpawnId = nil - if SrcEntity._IsWireMapInterfaceSubEntity then - wmiSpawnId = SrcEntity:_WMI_GetSpawnId() - end - + if IsValid(SrcEntity) then info.Wires[portname] = { StartPos = input.StartPos, Material = input.Material, @@ -895,18 +890,13 @@ function WireLib.BuildDupeInfo( Ent ) SrcPos = Vector(0, 0, 0), } - if (input.Path) then + if input.Path then info.Wires[portname].Path = {} for _,v in ipairs(input.Path) do local vEntity = v.Entity - if (IsValid(vEntity)) then - local vEntityWmiSpawnId = nil - if vEntity._IsWireMapInterfaceSubEntity then - vEntityWmiSpawnId = vEntity:_WMI_GetSpawnId() - end - + if IsValid(vEntity) then table.insert(info.Wires[portname].Path, { Entity = vEntity:EntIndex(), EntityWmiSpawnId = vEntity._IsWireMapInterfaceSubEntity and vEntity:_WMI_GetSpawnId() or nil, From 9a648f276638aed81ff687a072dcdf67871ac3de Mon Sep 17 00:00:00 2001 From: Grocel Date: Sun, 6 Jul 2025 08:36:47 +0200 Subject: [PATCH 12/22] Less garry code for custom hammer output logic. Co-Authored-By: march <106459595+marchc1@users.noreply.github.com> --- lua/entities/info_wiremapinterface/gmodoutputs.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/entities/info_wiremapinterface/gmodoutputs.lua b/lua/entities/info_wiremapinterface/gmodoutputs.lua index 1439e7256a..c7535faaef 100644 --- a/lua/entities/info_wiremapinterface/gmodoutputs.lua +++ b/lua/entities/info_wiremapinterface/gmodoutputs.lua @@ -39,7 +39,7 @@ function ENT:FireSingleOutput(outputName, output, activator, data) end end - if output.times ~= -1 then + if output.times > 0 then output.times = output.times - 1 end @@ -47,7 +47,8 @@ function ENT:FireSingleOutput(outputName, output, activator, data) return false end - return (output.times > 0) or (output.times == -1) + -- Less then 0 are valid to, e.g. unlimited times. + return output.times ~= 0 end -- This function is used to trigger an output. From 1c2344c13009a255879f085e1587a5f613b4a968 Mon Sep 17 00:00:00 2001 From: Grocel Date: Sun, 6 Jul 2025 09:31:12 +0200 Subject: [PATCH 13/22] Get rid of timers and global state in networking Co-Authored-By: march <106459595+marchc1@users.noreply.github.com> --- lua/entities/info_wiremapinterface/init.lua | 2 +- .../info_wiremapinterface/networking.lua | 51 ++++--------------- 2 files changed, 12 insertions(+), 41 deletions(-) diff --git a/lua/entities/info_wiremapinterface/init.lua b/lua/entities/info_wiremapinterface/init.lua index ae114a5dfa..4f30476334 100644 --- a/lua/entities/info_wiremapinterface/init.lua +++ b/lua/entities/info_wiremapinterface/init.lua @@ -262,7 +262,7 @@ end function ENT:OnReloaded() -- Easier for debugging. - self:InitNetworking() + self.ShouldNetworkEntities = true self:AttachToSaveStateEntity() end diff --git a/lua/entities/info_wiremapinterface/networking.lua b/lua/entities/info_wiremapinterface/networking.lua index 7b89b03544..0eca2c33b8 100644 --- a/lua/entities/info_wiremapinterface/networking.lua +++ b/lua/entities/info_wiremapinterface/networking.lua @@ -2,50 +2,21 @@ util.AddNetworkString("WireMapInterfaceEntities") -local g_ReadyForNetworking = false - -local function initNetworking() - timer.Simple(0.1, function() - g_ReadyForNetworking = true - - local wireMapInterfaceEntities = ents.FindByClass("info_wiremapinterface") - for i, ent in ipairs(wireMapInterfaceEntities) do - local timerDelay = i / 2 - - timer.Simple(timerDelay, function() - if not IsValid(ent) then - return - end - - if not ent.IsWireMapInterface then - return - end - - ent.ShouldNetworkEntities = true - end) - end - end) +local function resetNetworking() + local wireMapInterfaceEntities = ents.FindByClass("info_wiremapinterface") + for i, ent in ipairs(wireMapInterfaceEntities) do + local delay = 1 + i / 2 + + -- Trigger a networking call in a staggered + debounced matter. + ent.ShouldNetworkEntities = true + ent.NextNetworkTime = CurTime() + 1 + delay + end end -hook.Add("PlayerInitialSpawn", "WireMapInterface_PlayerInitialSpawn", function() - initNetworking() -end) - -hook.Add("PostCleanupMap", "WireMapInterface_PostCleanupMap_SV", function() - g_ReadyForNetworking = false - initNetworking() -end) - -function ENT:InitNetworking() - initNetworking() -end +hook.Add("PlayerInitialSpawn", "WireMapInterface_PlayerInitialSpawn", resetNetworking) +hook.Add("PostCleanupMap", "WireMapInterface_PostCleanupMap_SV", resetNetworking) function ENT:HandleShouldNetworkEntities() - if not g_ReadyForNetworking then - -- Avoid networking too early after startup. - return - end - if not self.ShouldNetworkEntities then return end From 8166dfdf4929f04d98e46c05bfbb1b6f4fcaccc5 Mon Sep 17 00:00:00 2001 From: Grocel Date: Sun, 6 Jul 2025 19:20:20 +0200 Subject: [PATCH 14/22] Better more reliable networking for PlayerInitialSpawn. Don't network to all players on player spawn. --- .../info_wiremapinterface/entitycontrol.lua | 4 +- lua/entities/info_wiremapinterface/init.lua | 7 ++- .../info_wiremapinterface/networking.lua | 55 +++++++++++++++++-- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/lua/entities/info_wiremapinterface/entitycontrol.lua b/lua/entities/info_wiremapinterface/entitycontrol.lua index bf4d271abf..99ff024b8f 100644 --- a/lua/entities/info_wiremapinterface/entitycontrol.lua +++ b/lua/entities/info_wiremapinterface/entitycontrol.lua @@ -115,7 +115,7 @@ function ENT:AddSingleEntity(wireEnt) self.WireEntsUpdated = true self.WireEntsAdded = true - self.ShouldNetworkEntities = true + self:RequestNetworkEntities() self.WireEntsSorted = nil self.WireEntsCount = nil end @@ -213,7 +213,7 @@ function ENT:UnregisterWireEntityInternal(wireEnt) self.WireEntsUpdated = true self.WireEntsRemoved = true - self.ShouldNetworkEntities = true + self:RequestNetworkEntities() self.WireEntsSorted = nil self.WireEntsCount = nil diff --git a/lua/entities/info_wiremapinterface/init.lua b/lua/entities/info_wiremapinterface/init.lua index 4f30476334..d4b8fc5389 100644 --- a/lua/entities/info_wiremapinterface/init.lua +++ b/lua/entities/info_wiremapinterface/init.lua @@ -254,6 +254,11 @@ function ENT:Initialize() self.PortsUpdated = true + local recipientFilter = RecipientFilter() + recipientFilter:RemoveAllPlayers() + + self.NetworkRecipientFilter = recipientFilter + self.NextNetworkTime = CurTime() + (1 + math.random() * 2) * (self.MIN_THINK_TIME * 4) self:AddDupeHooks() @@ -262,7 +267,7 @@ end function ENT:OnReloaded() -- Easier for debugging. - self.ShouldNetworkEntities = true + self:RequestNetworkEntities() self:AttachToSaveStateEntity() end diff --git a/lua/entities/info_wiremapinterface/networking.lua b/lua/entities/info_wiremapinterface/networking.lua index 0eca2c33b8..0500cafd93 100644 --- a/lua/entities/info_wiremapinterface/networking.lua +++ b/lua/entities/info_wiremapinterface/networking.lua @@ -2,19 +2,30 @@ util.AddNetworkString("WireMapInterfaceEntities") -local function resetNetworking() +local function resetNetworking(ply) local wireMapInterfaceEntities = ents.FindByClass("info_wiremapinterface") for i, ent in ipairs(wireMapInterfaceEntities) do local delay = 1 + i / 2 -- Trigger a networking call in a staggered + debounced matter. - ent.ShouldNetworkEntities = true - ent.NextNetworkTime = CurTime() + 1 + delay + local nextNetworkTime = math.max(ent.NextNetworkTime, CurTime() + delay) + ent:RequestNetworkEntities(ply, nextNetworkTime) end end -hook.Add("PlayerInitialSpawn", "WireMapInterface_PlayerInitialSpawn", resetNetworking) -hook.Add("PostCleanupMap", "WireMapInterface_PostCleanupMap_SV", resetNetworking) +gameevent.Listen("player_activate") +hook.Add("player_activate", "WireMapInterface_PlayerActivate", function(data) + -- Make sure the newly spawned player is ready for networking. + -- Networking in PlayerInitialSpawn is said to be unreliable. + + local ply = Player(data.userid) + resetNetworking(ply) +end) + +hook.Add("PostCleanupMap", "WireMapInterface_PostCleanupMap_SV", function() + -- Reset networking on map clear. It repairs it in case it got desynced. + resetNetworking() +end) function ENT:HandleShouldNetworkEntities() if not self.ShouldNetworkEntities then @@ -37,10 +48,33 @@ function ENT:HandleShouldNetworkEntities() self.NextNetworkTime = now + self.MIN_THINK_TIME * 4 end +function ENT:RequestNetworkEntities(ply, networkAtTime) + self.ShouldNetworkEntities = true + + if networkAtTime then + self.NextNetworkTime = networkAtTime + end + + local recipientFilter = self.NetworkRecipientFilter + + if not IsValid(ply) then + recipientFilter:AddAllPlayers() + return + end + + recipientFilter:AddPlayer(ply) +end + function ENT:NetworkWireEntities() -- Network the list and properties of the wire entities. -- We need we know about them on the client. For cable rendering, tools etc. + local recipientFilter = self.NetworkRecipientFilter + + if recipientFilter:GetCount() <= 0 then + return + end + net.Start("WireMapInterfaceEntities") net.WriteUInt(self:EntIndex(), MAX_EDICT_BITS) @@ -55,6 +89,15 @@ function ENT:NetworkWireEntities() net.WriteUInt(wireEnt:EntIndex(), MAX_EDICT_BITS) end - net.Broadcast() + net.Send(recipientFilter) + + PrintTable({ + networkWireEntities = "networkWireEntities", + self = self, + recipientFilter = recipientFilter, + players = recipientFilter:GetPlayers() or "nil", + }) + + recipientFilter:RemoveAllPlayers() end From 03c5331821e91099151d37285797304290a74985 Mon Sep 17 00:00:00 2001 From: Grocel Date: Sun, 6 Jul 2025 19:35:05 +0200 Subject: [PATCH 15/22] Remove debug prints --- lua/entities/info_wiremapinterface/networking.lua | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lua/entities/info_wiremapinterface/networking.lua b/lua/entities/info_wiremapinterface/networking.lua index 0500cafd93..9b7a71c584 100644 --- a/lua/entities/info_wiremapinterface/networking.lua +++ b/lua/entities/info_wiremapinterface/networking.lua @@ -91,13 +91,6 @@ function ENT:NetworkWireEntities() net.Send(recipientFilter) - PrintTable({ - networkWireEntities = "networkWireEntities", - self = self, - recipientFilter = recipientFilter, - players = recipientFilter:GetPlayers() or "nil", - }) - recipientFilter:RemoveAllPlayers() end From e176a0cd51c512dc18f36cb5cde0aeab112bc000 Mon Sep 17 00:00:00 2001 From: Grocel Date: Tue, 8 Jul 2025 08:23:26 +0200 Subject: [PATCH 16/22] avoid table.insert c call Co-Authored-By: march <106459595+marchc1@users.noreply.github.com> --- lua/entities/info_wiremapinterface/io.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/entities/info_wiremapinterface/io.lua b/lua/entities/info_wiremapinterface/io.lua index 9fc66f517e..15e224e487 100644 --- a/lua/entities/info_wiremapinterface/io.lua +++ b/lua/entities/info_wiremapinterface/io.lua @@ -53,7 +53,7 @@ local function newPortRegister() byName[name] = port byUid[uid] = port - table.insert(sequence, port) + sequence[#sequence + 1] = port local wire = this.wire local names = wire.names @@ -159,12 +159,12 @@ function ENT:GetPortUid(port) table.Empty(g_hashTmp) - table.insert(g_hashTmp, "WMI_") - table.insert(g_hashTmp, self:GetCreationID()) - table.insert(g_hashTmp, self:GetCreationTime()) - table.insert(g_hashTmp, name) - table.insert(g_hashTmp, portId) - table.insert(g_hashTmp, portType) + g_hashTmp[1] = "WMI_" + g_hashTmp[2] = self:GetCreationID() + g_hashTmp[3] = self:GetCreationTime() + g_hashTmp[4] = name + g_hashTmp[5] = portId + g_hashTmp[6] = portType return util.SHA1(table.concat(g_hashTmp, "_")) end From e05d0eef3737ce7b30fa3303b5d0435e475ef9e6 Mon Sep 17 00:00:00 2001 From: Grocel Date: Sun, 13 Jul 2025 02:56:03 +0200 Subject: [PATCH 17/22] Remove WIRE_CLIENT_INSTALLED leftover, clarify WireAddon check Co-Authored-By: Redox <69946827+wrefgtzweve@users.noreply.github.com> --- lua/entities/info_wiremapinterface/init.lua | 1 + lua/wire/client/cl_wire_map_interface.lua | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lua/entities/info_wiremapinterface/init.lua b/lua/entities/info_wiremapinterface/init.lua index d4b8fc5389..7a9c041310 100644 --- a/lua/entities/info_wiremapinterface/init.lua +++ b/lua/entities/info_wiremapinterface/init.lua @@ -33,6 +33,7 @@ ENT.DoNotDuplicate = true ENT.IsWireMapInterface = nil if not WireAddon then + -- Avoids problems with early map spawned entities in case Wiremod fails to load. return end diff --git a/lua/wire/client/cl_wire_map_interface.lua b/lua/wire/client/cl_wire_map_interface.lua index 165ad577bb..141a2f1bd7 100644 --- a/lua/wire/client/cl_wire_map_interface.lua +++ b/lua/wire/client/cl_wire_map_interface.lua @@ -1,12 +1,6 @@ -- Clientside functionalities of Wire Map Interface -- This is mostly for predection and rendering -local WIRE_CLIENT_INSTALLED = WIRE_CLIENT_INSTALLED - -if not WIRE_CLIENT_INSTALLED then - return -end - local g_wireTools = { "wire", "wire_adv", From 4f7705573ea8b05f233b92468eac321dc9c96fb5 Mon Sep 17 00:00:00 2001 From: Grocel Date: Mon, 14 Jul 2025 13:53:56 +0200 Subject: [PATCH 18/22] Remove `outputNameLower ` var Co-Authored-By: DarthTealc <4651516+darthtealc@users.noreply.github.com> --- lua/entities/info_wiremapinterface/init.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/entities/info_wiremapinterface/init.lua b/lua/entities/info_wiremapinterface/init.lua index 7a9c041310..6397a8a555 100644 --- a/lua/entities/info_wiremapinterface/init.lua +++ b/lua/entities/info_wiremapinterface/init.lua @@ -306,9 +306,7 @@ function ENT:IsLuaRunEntity(ent) end function ENT:ProtectAgainstDangerousIO(targetEnt, outputName, output, data) - local outputNameLower = string.lower(outputName) - - if not string.StartsWith(outputNameLower, "onwireinput") then + if not string.StartsWith(string.lower(outputName), "onwireinput") then -- This protection is only relevant for Hammer outputs linked to Wire inputs. return true end From 61520268888099b71e1360b82a258e43afc5c2e9 Mon Sep 17 00:00:00 2001 From: Grocel Date: Mon, 14 Jul 2025 14:02:32 +0200 Subject: [PATCH 19/22] More compact if statements. Co-Authored-By: DarthTealc <4651516+darthtealc@users.noreply.github.com> --- lua/entities/info_wiremapinterface/convert.lua | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lua/entities/info_wiremapinterface/convert.lua b/lua/entities/info_wiremapinterface/convert.lua index 216a81a0bb..84c1e4ecd6 100644 --- a/lua/entities/info_wiremapinterface/convert.lua +++ b/lua/entities/info_wiremapinterface/convert.lua @@ -8,11 +8,7 @@ local function isEqualList(_, listA, listB) return true end - if listA == nil then - return false - end - - if listB == nil then + if not listA or not listB then return false end @@ -36,11 +32,7 @@ local function isEqualGeneric(_, varA, varB) end local function isEqualEntity(_, entA, entB) - if not IsValid(entA) then - return false - end - - if not IsValid(entB) then + if not IsValid(entA) or not IsValid(entB) then return false end From a7a4959ff427a77db0ef5993caddcc3546ada439 Mon Sep 17 00:00:00 2001 From: Grocel Date: Mon, 14 Jul 2025 14:18:14 +0200 Subject: [PATCH 20/22] Replace anti-dupe hack with something more reasonable. --- .../info_wiremapinterface_savestate/init.lua | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/lua/entities/info_wiremapinterface_savestate/init.lua b/lua/entities/info_wiremapinterface_savestate/init.lua index 81ea9bde10..309584118a 100644 --- a/lua/entities/info_wiremapinterface_savestate/init.lua +++ b/lua/entities/info_wiremapinterface_savestate/init.lua @@ -1,5 +1,5 @@ -- This is a helper entity to store save state data wire map interface entities. (info_wiremapinterface_savestate) --- Only one per map will be spawned during run time. This is needed because info_wiremapinterface can not and also should not be duplicated/saved. +-- Only one per map will be spawned during run time. This is needed because info_wiremapinterface can not and also must not be duplicated/saved. -- When a game is saved, this entity will be save along with it. -- When the entity is restored, it deploys its saved data to all interface entities it knows about. @@ -12,6 +12,11 @@ ENT.AdminOnly = true -- Needed for save game support ENT.DisableDuplicator = false +-- This entity is for saves only. +-- So block all tools, especially dublicator tools and its area copy feature. +-- This entity not traceable nor visible, so other tools would not matter. +ENT.m_tblToolsAllowed = {} + local g_SaveStateEntity = nil local g_interfaceEntities = {} local g_saveState = {} @@ -181,27 +186,3 @@ function ENT:PostEntityPaste(ply, ent, createdEntities) end end -local g_wireMapInterfaceSaveStateTimer = "WireMapInterface_SaveState_CanTool_Timer" - -function ENT:CanTool() - -- This is for saves only. - -- Stop dublicator tools for doing weird stuff. Such as area copy. - - -- Block dublicator tools. - self.DoNotDuplicate = true - - timer.Remove(g_wireMapInterfaceSaveStateTimer) - timer.Create(g_wireMapInterfaceSaveStateTimer, 0.01, 1, function() - timer.Remove(g_wireMapInterfaceSaveStateTimer) - - if not IsValid(self) then - return - end - - -- Revert shortly after, so save games still work. - self.DoNotDuplicate = false - end) - - -- Other Tools can not access this entity anyway as it is not a traceable entity - return false -end \ No newline at end of file From 37d0049ec394ba9774f59305d4f8c6f1efa0bad8 Mon Sep 17 00:00:00 2001 From: Grocel Date: Mon, 14 Jul 2025 14:26:07 +0200 Subject: [PATCH 21/22] Also remove `ENT:CanTool()` on the main entity. --- lua/entities/info_wiremapinterface/init.lua | 26 ++++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lua/entities/info_wiremapinterface/init.lua b/lua/entities/info_wiremapinterface/init.lua index 6397a8a555..f5198c96fb 100644 --- a/lua/entities/info_wiremapinterface/init.lua +++ b/lua/entities/info_wiremapinterface/init.lua @@ -37,14 +37,11 @@ if not WireAddon then return end -include("convert.lua") -include("entitycontrol.lua") -include("entityoverride.lua") -include("gmodoutputs.lua") -include("io.lua") -include("networking.lua") -include("savestate.lua") +-- Make sure there is no way to mess around with tools, especially dublicator tools. +-- This entity not traceable nor visible, so tools would not matter. +ENT.m_tblToolsAllowed = {} +-- Esay way to check if it is a wire map interface. ENT.IsWireMapInterface = true -- This entity supports more than the 8 ports you see in the editor. This value is the port limit. @@ -53,6 +50,14 @@ ENT.MAX_PORTS = 255 -- Minimum delay between think calls. ENT.MIN_THINK_TIME = 0.25 +include("convert.lua") +include("entitycontrol.lua") +include("entityoverride.lua") +include("gmodoutputs.lua") +include("io.lua") +include("networking.lua") +include("savestate.lua") + local cvar_allow_interface = CreateConVar( "sv_wire_mapinterface", "1", @@ -611,13 +616,6 @@ function ENT:Think() return true end -function ENT:CanTool() - -- Make sure there is no way to mess around with tools on this entity. - -- It is not a traceable entity. - - return false -end - function ENT:OnRemove() local wireEnts = self:GetWiredEntities() From 73973d9d902a65d9c6310b2d2ff6d1beccd9c93c Mon Sep 17 00:00:00 2001 From: Grocel Date: Mon, 14 Jul 2025 14:46:40 +0200 Subject: [PATCH 22/22] Never remove entities that's aren't map spawned. --- lua/entities/info_wiremapinterface/init.lua | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lua/entities/info_wiremapinterface/init.lua b/lua/entities/info_wiremapinterface/init.lua index f5198c96fb..c9fa5abc6b 100644 --- a/lua/entities/info_wiremapinterface/init.lua +++ b/lua/entities/info_wiremapinterface/init.lua @@ -211,39 +211,51 @@ function ENT:CheckPortIdLimit(portId, warn) return true end +-- Protect in-/output entities from non-wire tools function ENT:FlagGetProtectFromTools() if not self:CreatedByMap() then + -- Prevent abuse by runtime-spawned instances. return false end local flags = self:GetSpawnFlags() - return bit.band(flags, 1) == 1 -- Protect in-/output entities from non-wire tools + return bit.band(flags, 1) == 1 end +-- Protect in-/output entities from the physgun function ENT:FlagGetProtectFromPhysgun() if not self:CreatedByMap() then return false end local flags = self:GetSpawnFlags() - return bit.band(flags, 2) == 2 -- Protect in-/output entities from the physgun + return bit.band(flags, 2) == 2 end +-- Remove in-/output entities on remove function ENT:FlagGetRemoveEntities() + if not self:CreatedByMap() then + return false + end + local flags = self:GetSpawnFlags() - return bit.band(flags, 4) == 4 -- Remove in-/output entities on remove + return bit.band(flags, 4) == 4 end --- Note: bit.band(flags, 8) == 8 Was used for running lua code. It must be left unused as could cause unexpected side effects on older maps. +-- Note: +-- bit.band(flags, 8) == 8 Was used for running lua code. +-- It must be left unused as it could cause unexpected side effects on older maps. +-- Start Active function ENT:FlagGetStartActive() local flags = self:GetSpawnFlags() - return bit.band(flags, 16) == 16 -- Start Active + return bit.band(flags, 16) == 16 end +-- Render wires clientside function ENT:FlagGetRenderWires() local flags = self:GetSpawnFlags() - return bit.band(flags, 32) == 32 -- Start Active + return bit.band(flags, 32) == 32 end function ENT:Initialize() @@ -621,7 +633,9 @@ function ENT:OnRemove() if self:FlagGetRemoveEntities() then for _, wireEnt in ipairs(wireEnts) do - SafeRemoveEntity(wireEnt) + if wireEnt:IsValid() and not wireEnt:IsMarkedForDeletion() and wireEnt:CreatedByMap() then + wireEnt:Remove() + end end else self:RemoveAllEntities()