From bc70df10e95b1cd3d8b3840be7d1fab2dcfa5686 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:35:37 +0300 Subject: [PATCH 01/17] Add per player time quota for E2 I think this would be beneficial for servers, because servers care more about the overall load from a player than about their specific chip. Currently, players can simply spread the load across multiple chips to bypass the quota limit, but this won't eliminate it; it will only increase the overhead --- .../gmod_wire_expression2/core/core.lua | 5 ++++ lua/entities/gmod_wire_expression2/init.lua | 29 +++++++++++++++++++ lua/entities/gmod_wire_expression2/shared.lua | 1 + 3 files changed, 35 insertions(+) diff --git a/lua/entities/gmod_wire_expression2/core/core.lua b/lua/entities/gmod_wire_expression2/core/core.lua index e4f33b96f4..5d77743d7a 100644 --- a/lua/entities/gmod_wire_expression2/core/core.lua +++ b/lua/entities/gmod_wire_expression2/core/core.lua @@ -272,6 +272,11 @@ e2function number timeQuota() return e2_timequota end +[nodiscard] +e2function number totalQuota() + return e2_totalquota +end + __e2setcost(nil) registerCallback("postinit", function() diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 3719681b4b..4539e89dcb 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -8,6 +8,7 @@ e2_softquota = nil e2_hardquota = nil e2_tickquota = nil e2_timequota = nil +e2_totalquota = nil do local wire_expression2_unlimited = GetConVar("wire_expression2_unlimited") @@ -15,6 +16,7 @@ do local wire_expression2_quotahard = GetConVar("wire_expression2_quotahard") local wire_expression2_quotatick = GetConVar("wire_expression2_quotatick") local wire_expression2_quotatime = GetConVar("wire_expression2_quotatime") + local wire_expression2_quotatime_total = GetConVar("wire_expression2_quotatime_total") local function updateQuotas() if wire_expression2_unlimited:GetBool() then @@ -22,11 +24,13 @@ do e2_hardquota = 1000000 e2_tickquota = 100000 e2_timequota = -1 + e2_playquota = -1 else e2_softquota = wire_expression2_quotasoft:GetInt() e2_hardquota = wire_expression2_quotahard:GetInt() e2_tickquota = wire_expression2_quotatick:GetInt() e2_timequota = wire_expression2_quotatime:GetInt() * 0.001 + e2_totalquota = wire_expression2_quotatime_total:GetInt() * 0.001 end end cvars.AddChangeCallback("wire_expression2_unlimited", updateQuotas) @@ -34,6 +38,7 @@ do cvars.AddChangeCallback("wire_expression2_quotahard", updateQuotas) cvars.AddChangeCallback("wire_expression2_quotatick", updateQuotas) cvars.AddChangeCallback("wire_expression2_quotatime", updateQuotas) + cvars.AddChangeCallback("wire_expression2_quotatime_total", updateQuotas) updateQuotas() end @@ -292,6 +297,30 @@ function ENT:Think() self:PCallHook("destruct") end + if e2_totalquota > 0 then + local quota_total = self.player.E2TotalQuota + + if not quota_total then + quota_total = {-1, 0} + self.player.E2TotalQuota = quota_total + end + + local current_tick = engine.TickCount() + + if current_tick > quota_total[1] then + quota_total[1] = current_tick + quota_total[2] = 0 + end + + local total_quota = quota_total[2] + context.timebench + quota_total[2] = total_quota + + if total_quota > e2_totalquota then + self:Error("Expression 2 (" .. selfTbl.name .. "): total quota exceeded", "total quota exceeded") + self:PCallHook("destruct") + end + end + return true end diff --git a/lua/entities/gmod_wire_expression2/shared.lua b/lua/entities/gmod_wire_expression2/shared.lua index fd53d42fc4..68f242d21e 100644 --- a/lua/entities/gmod_wire_expression2/shared.lua +++ b/lua/entities/gmod_wire_expression2/shared.lua @@ -13,6 +13,7 @@ CreateConVar("wire_expression2_quotasoft", "10000", {FCVAR_REPLICATED}) CreateConVar("wire_expression2_quotahard", "100000", {FCVAR_REPLICATED}) CreateConVar("wire_expression2_quotatick", "25000", {FCVAR_REPLICATED}) CreateConVar("wire_expression2_quotatime", "-1", {FCVAR_REPLICATED}, "Time in (ms) the e2 can consume before killing (-1 is infinite)") +CreateConVar("wire_expression2_quotatime_total", "-1", {FCVAR_REPLICATED}, "Time in (ms) that all E2s of one player can consume before killing (-1 is infinite)") include("core/e2lib.lua") include("base/debug.lua") From 5743d7887fc36c2f4337f6eecb9fb516b20604b7 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:44:32 +0300 Subject: [PATCH 02/17] Use CurTime for it --- lua/entities/gmod_wire_expression2/init.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 4539e89dcb..ea9e5b6865 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -276,7 +276,9 @@ end function ENT:Think() BaseClass.Think(self) - self:NextThink(CurTime() + 0.030303) + + local current_time = CurTime() + self:NextThink(current_time + 0.030303) local selfTbl = self:GetTable() local context = selfTbl.context @@ -305,10 +307,8 @@ function ENT:Think() self.player.E2TotalQuota = quota_total end - local current_tick = engine.TickCount() - - if current_tick > quota_total[1] then - quota_total[1] = current_tick + if current_time >= quota_total[1] then + quota_total[1] = current_time + 0.030303 quota_total[2] = 0 end From 45ae313fec60615570aef64b8ca7f513eaac20b5 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Sat, 21 Mar 2026 11:31:04 +0300 Subject: [PATCH 03/17] Count the execution cycle in ticks for accuracy --- lua/entities/gmod_wire_expression2/init.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index ea9e5b6865..9fdf25aa34 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -274,11 +274,12 @@ function ENT:ExecuteEvent(evt, args) end end +-- Execution delay of E2 in ticks (for total quota counting) +local execution_delay = math.floor(1 / engine.TickInterval() * 0.030303) + function ENT:Think() BaseClass.Think(self) - - local current_time = CurTime() - self:NextThink(current_time + 0.030303) + self:NextThink(CurTime() + 0.030303) local selfTbl = self:GetTable() local context = selfTbl.context @@ -301,14 +302,15 @@ function ENT:Think() if e2_totalquota > 0 then local quota_total = self.player.E2TotalQuota + local tick_count = engine.TickCount() if not quota_total then quota_total = {-1, 0} self.player.E2TotalQuota = quota_total end - if current_time >= quota_total[1] then - quota_total[1] = current_time + 0.030303 + if tick_count >= quota_total[1] then + quota_total[1] = tick_count + execution_delay quota_total[2] = 0 end From 1399fa0a8d0359ba42441020493c321c7797deb7 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Sat, 21 Mar 2026 11:37:32 +0300 Subject: [PATCH 04/17] Don't round this --- lua/entities/gmod_wire_expression2/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 9fdf25aa34..12b547f5e0 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -275,7 +275,7 @@ function ENT:ExecuteEvent(evt, args) end -- Execution delay of E2 in ticks (for total quota counting) -local execution_delay = math.floor(1 / engine.TickInterval() * 0.030303) +local execution_delay = 1 / engine.TickInterval() * 0.030303 function ENT:Think() BaseClass.Think(self) From cf6eae8760beb6cfcf2b358e5467eb3fb9683e56 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Sat, 21 Mar 2026 11:41:01 +0300 Subject: [PATCH 05/17] Use selfTbl --- lua/entities/gmod_wire_expression2/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 12b547f5e0..754d176a8f 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -301,12 +301,12 @@ function ENT:Think() end if e2_totalquota > 0 then - local quota_total = self.player.E2TotalQuota + local quota_total = selfTbl.player.E2TotalQuota local tick_count = engine.TickCount() if not quota_total then quota_total = {-1, 0} - self.player.E2TotalQuota = quota_total + selfTbl.player.E2TotalQuota = quota_total end if tick_count >= quota_total[1] then From a71e0f7feaebf665e77da329b28b95b0f37181b4 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:46:16 +0300 Subject: [PATCH 06/17] Fix typo --- lua/entities/gmod_wire_expression2/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 754d176a8f..0862d0b09d 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -24,7 +24,7 @@ do e2_hardquota = 1000000 e2_tickquota = 100000 e2_timequota = -1 - e2_playquota = -1 + e2_totalquota = -1 else e2_softquota = wire_expression2_quotasoft:GetInt() e2_hardquota = wire_expression2_quotahard:GetInt() From 33ca006b82cb92de83e4b508e5e35b0007addddf Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:33:19 +0300 Subject: [PATCH 07/17] Use the total load from all E2s of one player, also calculating the average load (code from Redox, slightly modified) --- .../gmod_wire_expression2/core/core.lua | 5 - lua/entities/gmod_wire_expression2/init.lua | 164 ++++++++++-------- lua/entities/gmod_wire_expression2/shared.lua | 3 +- 3 files changed, 96 insertions(+), 76 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/core/core.lua b/lua/entities/gmod_wire_expression2/core/core.lua index 5d77743d7a..e4f33b96f4 100644 --- a/lua/entities/gmod_wire_expression2/core/core.lua +++ b/lua/entities/gmod_wire_expression2/core/core.lua @@ -272,11 +272,6 @@ e2function number timeQuota() return e2_timequota end -[nodiscard] -e2function number totalQuota() - return e2_totalquota -end - __e2setcost(nil) registerCallback("postinit", function() diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 0862d0b09d..07ab857479 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -8,7 +8,6 @@ e2_softquota = nil e2_hardquota = nil e2_tickquota = nil e2_timequota = nil -e2_totalquota = nil do local wire_expression2_unlimited = GetConVar("wire_expression2_unlimited") @@ -16,7 +15,6 @@ do local wire_expression2_quotahard = GetConVar("wire_expression2_quotahard") local wire_expression2_quotatick = GetConVar("wire_expression2_quotatick") local wire_expression2_quotatime = GetConVar("wire_expression2_quotatime") - local wire_expression2_quotatime_total = GetConVar("wire_expression2_quotatime_total") local function updateQuotas() if wire_expression2_unlimited:GetBool() then @@ -24,13 +22,11 @@ do e2_hardquota = 1000000 e2_tickquota = 100000 e2_timequota = -1 - e2_totalquota = -1 else e2_softquota = wire_expression2_quotasoft:GetInt() e2_hardquota = wire_expression2_quotahard:GetInt() e2_tickquota = wire_expression2_quotatick:GetInt() e2_timequota = wire_expression2_quotatime:GetInt() * 0.001 - e2_totalquota = wire_expression2_quotatime_total:GetInt() * 0.001 end end cvars.AddChangeCallback("wire_expression2_unlimited", updateQuotas) @@ -38,7 +34,6 @@ do cvars.AddChangeCallback("wire_expression2_quotahard", updateQuotas) cvars.AddChangeCallback("wire_expression2_quotatick", updateQuotas) cvars.AddChangeCallback("wire_expression2_quotatime", updateQuotas) - cvars.AddChangeCallback("wire_expression2_quotatime_total", updateQuotas) updateQuotas() end @@ -80,6 +75,14 @@ function ENT:Initialize() self.error = true self:UpdateOverlay(true) self:SetColor(Color(255, 0, 0, self:GetColor().a)) + + local owner = self.player + + if IsValid(owner) then + E2Lib.PlayerChips[owner] = E2Lib.PlayerChips[owner] or {} + E2Lib.PlayerUsage[owner] = E2Lib.PlayerUsage[owner] or {} + table.insert(E2Lib.PlayerChips[owner], self) + end end function ENT:OnRestore() @@ -274,9 +277,6 @@ function ENT:ExecuteEvent(evt, args) end end --- Execution delay of E2 in ticks (for total quota counting) -local execution_delay = 1 / engine.TickInterval() * 0.030303 - function ENT:Think() BaseClass.Think(self) self:NextThink(CurTime() + 0.030303) @@ -295,36 +295,81 @@ function ENT:Think() context.prf = 0 context.time = 0 - if e2_timequota > 0 and context.timebench > e2_timequota then - self:Error("Expression 2 (" .. selfTbl.name .. "): time quota exceeded", "time quota exceeded") - self:PCallHook("destruct") - end + return true +end - if e2_totalquota > 0 then - local quota_total = selfTbl.player.E2TotalQuota - local tick_count = engine.TickCount() +E2Lib.PlayerChips = E2Lib.PlayerChips or {} +E2Lib.PlayerUsage = E2Lib.PlayerUsage or {} +E2Lib.PlayerTickUsage = E2Lib.PlayerTickUsage or {} - if not quota_total then - quota_total = {-1, 0} - selfTbl.player.E2TotalQuota = quota_total - end +local function get_median(values) + if #values == 0 then return 0 end + if #values == 1 then return values[1] end + if #values ~= 11 then return 0 end + + local sorted = table.Copy(values) + table.sort(sorted) + + return sorted[math.ceil(#sorted / 2)] +end + +local function inser_rolling_average( tbl, value ) + table.insert( tbl, value ) - if tick_count >= quota_total[1] then - quota_total[1] = tick_count + execution_delay - quota_total[2] = 0 + if #tbl > 11 then + table.remove( tbl, 1 ) + end +end + +E2Lib.registerCallback("postexecute", function(context) + local owner = context.player + if not owner then return end + + E2Lib.PlayerTickUsage[owner] = (E2Lib.PlayerTickUsage[owner] or 0) + context.time + E2Lib.PlayerUsage[owner] = E2Lib.PlayerUsage[owner] or {} +end) + +hook.Add("Think", "E2_Think", function() + if e2_timequota < 0 then return end + + for ply, chips in pairs( E2Lib.PlayerUsage ) do + if E2Lib.PlayerTickUsage[ply] then + inser_rolling_average(chips, E2Lib.PlayerTickUsage[ply]) + E2Lib.PlayerTickUsage[ply] = nil + else + inser_rolling_average(chips, 0) end - local total_quota = quota_total[2] + context.timebench - quota_total[2] = total_quota + local median = get_median(chips) + + if median > e2_timequota then + local chips = E2Lib.PlayerChips[ply] + + if chips then + local max_time = 0 + local max_chip + + for _, chip in pairs(chips) do + if chip.error then continue end + + local context = chip.context + if not context then continue end + + if context.timebench > max_time then + max_time = context.timebench + max_chip = chip + end + end - if total_quota > e2_totalquota then - self:Error("Expression 2 (" .. selfTbl.name .. "): total quota exceeded", "total quota exceeded") - self:PCallHook("destruct") + if max_chip then + max_chip:Error("Expression 2 (" .. max_chip.name .. "): Per-player time quota exceeded", "per-player time quota exceeded") + max_chip:Destruct() + E2Lib.PlayerUsage[ply] = {} + end + end end end - - return true -end +end) local CallHook = wire_expression2_CallHook function ENT:CallHook(hookname, ...) @@ -339,6 +384,16 @@ function ENT:OnRemove() self:Destruct() end + local owner = self.player + if not IsValid(owner) then return end + + for index, chip in ipairs(E2Lib.PlayerChips[owner]) do + if chip == self then + table.remove(E2Lib.PlayerChips[owner], index) + break + end + end + BaseClass.OnRemove(self) end @@ -749,52 +804,23 @@ end --[[ Player Disconnection Magic --]] -local cvar = CreateConVar("wire_expression2_pause_on_disconnect", 0, 0, "Decides if chips should pause execution on their owner's disconnect.\n0 = no, 1 = yes, 2 = non-admins only.") --- This is a global function so it can be overwritten for greater control over whose chips are frozenated -function wire_expression2_ShouldFreezeChip(ply) - return not ply:IsAdmin() -end +hook.Add("PlayerDisconnected", "Wire_Expression2_Player_Disconnected", function(ply) + E2Lib.PlayerChips[ply] = nil + E2Lib.PlayerUsage[ply] = nil --- It uses EntityRemoved because PlayerDisconnected doesn't catch all disconnects. -hook.Add("EntityRemoved", "Wire_Expression2_Player_Disconnected", function(ent) - if (not (ent and ent:IsPlayer())) then - return - end - local ret = cvar:GetInt() - if (ret == 0 or (ret == 2 and not wire_expression2_ShouldFreezeChip(ent))) then - return - end for _, v in ipairs(ents.FindByClass("gmod_wire_expression2")) do - if (v.player == ent) then - v:SetOverlayText(v.name .. "\n(Owner disconnected.)") - local oldColor = v:GetColor() - v:SetColor(Color(255, 0, 0, v:GetColor().a)) - v.disconnectPaused = oldColor - v.error = true + if v.player == ent and not v.error then + v:Error("Owner disconnected") + v:Destruct() end end end) hook.Add("PlayerAuthed", "Wire_Expression2_Player_Authed", function(ply, sid, uid) for _, ent in ipairs(ents.FindByClass("gmod_wire_expression2")) do - if ent.uid == uid and ent.context then - ent.context.player = ply - ent.player = ply + if ent.uid == uid then ent:SetNWEntity("player", ply) - ent:SetPlayer(ply) - - if ent.disconnectPaused then - ent:SetColor(ent.disconnectPaused) - ent:SetRenderMode(ent:GetColor().a == 255 and RENDERMODE_NORMAL or RENDERMODE_TRANSALPHA) - ent.error = false - ent.disconnectPaused = nil - ent:SetOverlayText(ent.name) - end - end - end - for _, ent in ipairs(ents.FindByClass("gmod_wire_hologram")) do - if ent.steamid == sid then - ent:SetPlayer(ply) + ent.player = ply end end end) @@ -812,10 +838,10 @@ function MakeWireExpression2(player, Pos, Ang, model, buffer, name, inputs, outp self:SetModel(model) self:SetAngles(Ang) self:SetPos(Pos) - self:Spawn() self:SetPlayer(player) - self.player = player self:SetNWEntity("player", player) + self.player = player + self:Spawn() if isstring( buffer ) then -- if someone dupes an E2 with compile errors, then all these values will be invalid buffer = string.Replace(string.Replace(buffer, string.char(163), "\""), string.char(128), "\n") diff --git a/lua/entities/gmod_wire_expression2/shared.lua b/lua/entities/gmod_wire_expression2/shared.lua index 68f242d21e..58358a08fd 100644 --- a/lua/entities/gmod_wire_expression2/shared.lua +++ b/lua/entities/gmod_wire_expression2/shared.lua @@ -12,8 +12,7 @@ CreateConVar("wire_expression2_unlimited", "0", {FCVAR_REPLICATED}) CreateConVar("wire_expression2_quotasoft", "10000", {FCVAR_REPLICATED}) CreateConVar("wire_expression2_quotahard", "100000", {FCVAR_REPLICATED}) CreateConVar("wire_expression2_quotatick", "25000", {FCVAR_REPLICATED}) -CreateConVar("wire_expression2_quotatime", "-1", {FCVAR_REPLICATED}, "Time in (ms) the e2 can consume before killing (-1 is infinite)") -CreateConVar("wire_expression2_quotatime_total", "-1", {FCVAR_REPLICATED}, "Time in (ms) that all E2s of one player can consume before killing (-1 is infinite)") +CreateConVar("wire_expression2_quotatime", "-1", {FCVAR_REPLICATED}, "Time in (ms) that all E2s of one player can consume before killing (-1 is infinite)") include("core/e2lib.lua") include("base/debug.lua") From 8c2abaa9bfde15a9a73d349b7340e030178e012a Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:37:24 +0300 Subject: [PATCH 08/17] Code styling/small optimization --- lua/entities/gmod_wire_expression2/init.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 07ab857479..35da5d4916 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -303,9 +303,9 @@ E2Lib.PlayerUsage = E2Lib.PlayerUsage or {} E2Lib.PlayerTickUsage = E2Lib.PlayerTickUsage or {} local function get_median(values) - if #values == 0 then return 0 end - if #values == 1 then return values[1] end - if #values ~= 11 then return 0 end + local length = #values + if length == 0 or length ~= 11 then return 0 end + if length == 1 then return values[1] end local sorted = table.Copy(values) table.sort(sorted) @@ -313,11 +313,11 @@ local function get_median(values) return sorted[math.ceil(#sorted / 2)] end -local function inser_rolling_average( tbl, value ) - table.insert( tbl, value ) +local function inser_rolling_average(tab, value) + table.insert(tab, value) if #tbl > 11 then - table.remove( tbl, 1 ) + table.remove(tab, 1) end end @@ -332,7 +332,7 @@ end) hook.Add("Think", "E2_Think", function() if e2_timequota < 0 then return end - for ply, chips in pairs( E2Lib.PlayerUsage ) do + for ply, chips in pairs(E2Lib.PlayerUsage) do if E2Lib.PlayerTickUsage[ply] then inser_rolling_average(chips, E2Lib.PlayerTickUsage[ply]) E2Lib.PlayerTickUsage[ply] = nil From 00332dfbcffe13caab2467367c58f4b75c357df2 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:41:21 +0300 Subject: [PATCH 09/17] Typo fix/use tabs --- lua/entities/gmod_wire_expression2/init.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 35da5d4916..6d9b0d1a7d 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -304,21 +304,21 @@ E2Lib.PlayerTickUsage = E2Lib.PlayerTickUsage or {} local function get_median(values) local length = #values - if length == 0 or length ~= 11 then return 0 end - if length == 1 then return values[1] end + if length == 0 or length ~= 11 then return 0 end + if length == 1 then return values[1] end - local sorted = table.Copy(values) - table.sort(sorted) + local sorted = table.Copy(values) + table.sort(sorted) - return sorted[math.ceil(#sorted / 2)] + return sorted[math.ceil(#sorted / 2)] end local function inser_rolling_average(tab, value) - table.insert(tab, value) + table.insert(tab, value) - if #tbl > 11 then - table.remove(tab, 1) - end + if #tab > 11 then + table.remove(tab, 1) + end end E2Lib.registerCallback("postexecute", function(context) From 9d6408c06fb71f17d88ec71fcfcf9bba1f0213c4 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:42:31 +0300 Subject: [PATCH 10/17] Fix one more typo --- lua/entities/gmod_wire_expression2/init.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 6d9b0d1a7d..3a12563bd1 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -313,7 +313,7 @@ local function get_median(values) return sorted[math.ceil(#sorted / 2)] end -local function inser_rolling_average(tab, value) +local function insert_rolling_average(tab, value) table.insert(tab, value) if #tab > 11 then @@ -334,10 +334,10 @@ hook.Add("Think", "E2_Think", function() for ply, chips in pairs(E2Lib.PlayerUsage) do if E2Lib.PlayerTickUsage[ply] then - inser_rolling_average(chips, E2Lib.PlayerTickUsage[ply]) + insert_rolling_average(chips, E2Lib.PlayerTickUsage[ply]) E2Lib.PlayerTickUsage[ply] = nil else - inser_rolling_average(chips, 0) + insert_rolling_average(chips, 0) end local median = get_median(chips) From b3d1230bbeb920c946e70f3b0cad97d5f115d79c Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:48:02 +0300 Subject: [PATCH 11/17] Optimizations --- lua/entities/gmod_wire_expression2/init.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 3a12563bd1..b053ac2a25 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -307,10 +307,15 @@ local function get_median(values) if length == 0 or length ~= 11 then return 0 end if length == 1 then return values[1] end - local sorted = table.Copy(values) + local sorted = {} + + for i = 1, length do + sorted[i] = values[i] + end + table.sort(sorted) - return sorted[math.ceil(#sorted / 2)] + return sorted[math.ceil(length / 2)] end local function insert_rolling_average(tab, value) From ef9bb70153ece8013ac90f3ac41ce91d4b0b6599 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:52:15 +0300 Subject: [PATCH 12/17] One more small optimization --- lua/entities/gmod_wire_expression2/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index b053ac2a25..6e41211820 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -319,9 +319,9 @@ local function get_median(values) end local function insert_rolling_average(tab, value) - table.insert(tab, value) + local length = table.insert(tab, value) - if #tab > 11 then + if length > 11 then table.remove(tab, 1) end end From c785332d99b52924119ac0c63861b190d25f377b Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Tue, 24 Mar 2026 00:05:17 +0300 Subject: [PATCH 13/17] Do some cleanup --- lua/entities/gmod_wire_expression2/init.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 6e41211820..71da8cca42 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -392,13 +392,20 @@ function ENT:OnRemove() local owner = self.player if not IsValid(owner) then return end - for index, chip in ipairs(E2Lib.PlayerChips[owner]) do + local chips = E2Lib.PlayerChips[owner] + + for index, chip in ipairs(chips) do if chip == self then - table.remove(E2Lib.PlayerChips[owner], index) + table.remove(chips, index) break end end + if #chips == 0 then + E2Lib.PlayerChips[owner] = nil + E2Lib.PlayerUsage[owner] = nil + end + BaseClass.OnRemove(self) end @@ -824,6 +831,9 @@ end) hook.Add("PlayerAuthed", "Wire_Expression2_Player_Authed", function(ply, sid, uid) for _, ent in ipairs(ents.FindByClass("gmod_wire_expression2")) do if ent.uid == uid then + E2Lib.PlayerChips[ply] = E2Lib.PlayerChips[ply] or {} + E2Lib.PlayerUsage[ply] = E2Lib.PlayerUsage[ply] or {} + table.insert(E2Lib.PlayerChips[ply], ent) ent:SetNWEntity("player", ply) ent.player = ply end From bcc5d2ea037ece4f502d966e40a24fa3ed946a66 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:54:27 +0300 Subject: [PATCH 14/17] Don't use median --- lua/entities/gmod_wire_expression2/init.lua | 65 ++++++++------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 71da8cca42..c43d9cc110 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -80,7 +80,6 @@ function ENT:Initialize() if IsValid(owner) then E2Lib.PlayerChips[owner] = E2Lib.PlayerChips[owner] or {} - E2Lib.PlayerUsage[owner] = E2Lib.PlayerUsage[owner] or {} table.insert(E2Lib.PlayerChips[owner], self) end end @@ -299,8 +298,6 @@ function ENT:Think() end E2Lib.PlayerChips = E2Lib.PlayerChips or {} -E2Lib.PlayerUsage = E2Lib.PlayerUsage or {} -E2Lib.PlayerTickUsage = E2Lib.PlayerTickUsage or {} local function get_median(values) local length = #values @@ -326,52 +323,43 @@ local function insert_rolling_average(tab, value) end end -E2Lib.registerCallback("postexecute", function(context) - local owner = context.player - if not owner then return end - - E2Lib.PlayerTickUsage[owner] = (E2Lib.PlayerTickUsage[owner] or 0) + context.time - E2Lib.PlayerUsage[owner] = E2Lib.PlayerUsage[owner] or {} -end) - hook.Add("Think", "E2_Think", function() if e2_timequota < 0 then return end - for ply, chips in pairs(E2Lib.PlayerUsage) do - if E2Lib.PlayerTickUsage[ply] then - insert_rolling_average(chips, E2Lib.PlayerTickUsage[ply]) - E2Lib.PlayerTickUsage[ply] = nil - else - insert_rolling_average(chips, 0) - end + for ply, chips in pairs(E2Lib.PlayerChips) do + local total_time = 0 - local median = get_median(chips) + for _, chip in ipairs(chips) do + local tab = chip:GetTable() + if tab.error then continue end - if median > e2_timequota then - local chips = E2Lib.PlayerChips[ply] + local context = tab.context + if not context then continue end - if chips then - local max_time = 0 - local max_chip + total_time = total_time + context.timebench + end - for _, chip in pairs(chips) do - if chip.error then continue end + if total_time > e2_timequota then + local max_time = 0 + local max_chip - local context = chip.context - if not context then continue end + for _, chip in ipairs(chips) do + local tab = chip:GetTable() + if tab.error then continue end - if context.timebench > max_time then - max_time = context.timebench - max_chip = chip - end - end + local context = tab.context + if not context then continue end - if max_chip then - max_chip:Error("Expression 2 (" .. max_chip.name .. "): Per-player time quota exceeded", "per-player time quota exceeded") - max_chip:Destruct() - E2Lib.PlayerUsage[ply] = {} + if context.timebench > max_time then + max_time = context.timebench + max_chip = chip end end + + if max_chip then + max_chip:Error("Expression 2 (" .. max_chip.name .. "): Per-player time quota exceeded", "per-player time quota exceeded") + max_chip:Destruct() + end end end end) @@ -403,7 +391,6 @@ function ENT:OnRemove() if #chips == 0 then E2Lib.PlayerChips[owner] = nil - E2Lib.PlayerUsage[owner] = nil end BaseClass.OnRemove(self) @@ -818,7 +805,6 @@ end --]] hook.Add("PlayerDisconnected", "Wire_Expression2_Player_Disconnected", function(ply) E2Lib.PlayerChips[ply] = nil - E2Lib.PlayerUsage[ply] = nil for _, v in ipairs(ents.FindByClass("gmod_wire_expression2")) do if v.player == ent and not v.error then @@ -832,7 +818,6 @@ hook.Add("PlayerAuthed", "Wire_Expression2_Player_Authed", function(ply, sid, ui for _, ent in ipairs(ents.FindByClass("gmod_wire_expression2")) do if ent.uid == uid then E2Lib.PlayerChips[ply] = E2Lib.PlayerChips[ply] or {} - E2Lib.PlayerUsage[ply] = E2Lib.PlayerUsage[ply] or {} table.insert(E2Lib.PlayerChips[ply], ent) ent:SetNWEntity("player", ply) ent.player = ply From 90a7f2b904b7344a933621190255409b56acb0e0 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:55:00 +0300 Subject: [PATCH 15/17] Remove now unused --- lua/entities/gmod_wire_expression2/init.lua | 24 --------------------- 1 file changed, 24 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index c43d9cc110..504042e71f 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -299,30 +299,6 @@ end E2Lib.PlayerChips = E2Lib.PlayerChips or {} -local function get_median(values) - local length = #values - if length == 0 or length ~= 11 then return 0 end - if length == 1 then return values[1] end - - local sorted = {} - - for i = 1, length do - sorted[i] = values[i] - end - - table.sort(sorted) - - return sorted[math.ceil(length / 2)] -end - -local function insert_rolling_average(tab, value) - local length = table.insert(tab, value) - - if length > 11 then - table.remove(tab, 1) - end -end - hook.Add("Think", "E2_Think", function() if e2_timequota < 0 then return end From 007b1d3817ca8bb360d6a99799593d62bc35189f Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Tue, 24 Mar 2026 03:17:57 +0300 Subject: [PATCH 16/17] Kill chips until the cpu time drops to the maximum threshold --- lua/entities/gmod_wire_expression2/init.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 504042e71f..fdc1c90f62 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -315,7 +315,7 @@ hook.Add("Think", "E2_Think", function() total_time = total_time + context.timebench end - if total_time > e2_timequota then + while total_time > e2_timequota do local max_time = 0 local max_chip @@ -333,8 +333,12 @@ hook.Add("Think", "E2_Think", function() end if max_chip then + total_time = total_time - max_time max_chip:Error("Expression 2 (" .. max_chip.name .. "): Per-player time quota exceeded", "per-player time quota exceeded") max_chip:Destruct() + else + -- It shouldn't happen, but if something breaks, it will prevent an infinity loop + break end end end From d720a8103cad981ab67891f9d7344ca9e65b6151 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Tue, 24 Mar 2026 05:01:10 +0300 Subject: [PATCH 17/17] Fix typo --- lua/entities/gmod_wire_expression2/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index fdc1c90f62..08cdd67876 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -787,7 +787,7 @@ hook.Add("PlayerDisconnected", "Wire_Expression2_Player_Disconnected", function( E2Lib.PlayerChips[ply] = nil for _, v in ipairs(ents.FindByClass("gmod_wire_expression2")) do - if v.player == ent and not v.error then + if v.player == ply and not v.error then v:Error("Owner disconnected") v:Destruct() end