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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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 From 590ea341793711ca23baf8491728d3e24f78b608 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:51:17 +0300 Subject: [PATCH 18/19] Use OOP style add new e2 functions --- .../gmod_wire_expression2/core/core.lua | 23 ++++ lua/entities/gmod_wire_expression2/init.lua | 112 +++++++++++------- lua/wire/client/e2descriptions.lua | 2 + 3 files changed, 94 insertions(+), 43 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/core/core.lua b/lua/entities/gmod_wire_expression2/core/core.lua index e4f33b96f4..ddeebbf27d 100644 --- a/lua/entities/gmod_wire_expression2/core/core.lua +++ b/lua/entities/gmod_wire_expression2/core/core.lua @@ -210,6 +210,29 @@ e2function number entity:cpuUsage() return this.context.timebench end +__e2setcost(100) -- approximation + +[nodiscard] +e2function number totalCpuUsage() + local owner = self.player + if not IsValid(owner) then return self.timebench end + + return E2Lib.PlayerChips[owner]:getTotalTime() +end + +[nodiscard] +e2function number entity:totalCpuUsage() + if not IsValid(this) or not this:IsPlayer() then return 0 end + + -- To avoid creating new table + local chips = rawget(E2Lib.PlayerChips, this) + if not chips then return 0 end + + return chips:getTotalTime() +end + +__e2setcost(1) + --- If used as a while loop condition, stabilizes the expression around hardquota used. [nodiscard] e2function number perf() diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index 08cdd67876..f646ab2514 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -79,8 +79,7 @@ function ENT:Initialize() local owner = self.player if IsValid(owner) then - E2Lib.PlayerChips[owner] = E2Lib.PlayerChips[owner] or {} - table.insert(E2Lib.PlayerChips[owner], self) + E2Lib.PlayerChips[owner]:add(self) end end @@ -297,49 +296,83 @@ function ENT:Think() return true end -E2Lib.PlayerChips = E2Lib.PlayerChips or {} +local PlayerChips = {} +PlayerChips.__index = PlayerChips -hook.Add("Think", "E2_Think", function() - if e2_timequota < 0 then return end +function PlayerChips:new() + return setmetatable({chips = {}}, self) +end + +function PlayerChips:getTotalTime() + local total_time = 0 + + for _, chip in ipairs(self.chips) do + local tab = chip:GetTable() + if tab.error then continue end + + local context = tab.context + if not context then continue end + + total_time = total_time + context.timebench + end + + return total_time +end - for ply, chips in pairs(E2Lib.PlayerChips) do - local total_time = 0 +function PlayerChips:findMaxTimeChip() + local max_chip, max_time = nil, 0 - for _, chip in ipairs(chips) do - local tab = chip:GetTable() - if tab.error then continue end + for _, chip in ipairs(self.chips) do + local tab = chip:GetTable() + if tab.error then continue end - local context = tab.context - if not context then continue end + local context = tab.context + if not context then continue end - total_time = total_time + context.timebench + if context.timebench > max_time then + max_time = context.timebench + max_chip = chip end + end + + return max_chip, max_time +end - while total_time > e2_timequota do - local max_time = 0 - local max_chip +function PlayerChips:checkCpuTime() + local total_time = self:getTotalTime() + + while total_time > e2_timequota do + local max_chip, max_time = self:findMaxTimeChip() + 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 - for _, chip in ipairs(chips) do - local tab = chip:GetTable() - if tab.error then continue end +function PlayerChips:add(chip) + table.insert(self.chips, chip) +end - local context = tab.context - if not context then continue end +function PlayerChips:remove(rmchip) + for index, chip in ipairs(self.chips) do + if rmchip == chip then + table.remove(self.chips, index) + break + end + end +end - if context.timebench > max_time then - max_time = context.timebench - max_chip = chip - end - end +E2Lib.PlayerChips = setmetatable({}, {__index = function(self, ply) local chips = PlayerChips:new() self[ply] = chips return chips 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 +hook.Add("Think", "E2_Think", function() + if e2_timequota > 0 then + for ply, chips in pairs(E2Lib.PlayerChips) do + chips:checkCpuTime() end end end) @@ -361,13 +394,7 @@ function ENT:OnRemove() if not IsValid(owner) then return end local chips = E2Lib.PlayerChips[owner] - - for index, chip in ipairs(chips) do - if chip == self then - table.remove(chips, index) - break - end - end + chips:remove(self) if #chips == 0 then E2Lib.PlayerChips[owner] = nil @@ -797,8 +824,7 @@ 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 {} - table.insert(E2Lib.PlayerChips[ply], ent) + E2Lib.PlayerChips[ply]:add(ent) ent:SetNWEntity("player", ply) ent.player = ply end diff --git a/lua/wire/client/e2descriptions.lua b/lua/wire/client/e2descriptions.lua index eee5b57f2b..39fb3f5fa5 100644 --- a/lua/wire/client/e2descriptions.lua +++ b/lua/wire/client/e2descriptions.lua @@ -939,6 +939,8 @@ E2Helper.Descriptions["setName(e:s)"] = "Set the name of another E2 or component E2Helper.Descriptions["setOverlayText(s)"] = "Set the overlay text of the E2" E2Helper.Descriptions["cpuUsage()"] = "Returns the average time per tick the server spends running this E2, in seconds (multiply it by 1000000 to get the same value as is displayed on the E2 overlay)" E2Helper.Descriptions["cpuUsage(e:)"] = "Returns the average time per tick the server spends running the specified E2, in seconds (multiply it by 1000000 to get the same value as is displayed on the E2 overlay)" +E2Helper.Descriptions["totalCpuUsage()"] = "Returns the average time per tick the server spends running all yours E2s, in seconds (multiply it by 1000000 to get the same value as is displayed on the E2 overlay)" +E2Helper.Descriptions["totalCpuUsage(e:)"] = "Returns the average time per tick the server spends running specified player E2s, in seconds (multiply it by 1000000 to get the same value as is displayed on the E2 overlay)" E2Helper.Descriptions["error(s)"] = "Shuts down the E2 with specified script error message" E2Helper.Descriptions["assert(n)"] = "If the argument is 0, shut down the E2 with an error message" E2Helper.Descriptions["assert(ns)"] = "If the first argument is 0, shut down the E2 with the given error message string" From 2914f28d05c6e02df6ff77b2f7594f299726c164 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:18:53 +0300 Subject: [PATCH 19/19] Don't chips subtable --- lua/entities/gmod_wire_expression2/init.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/init.lua b/lua/entities/gmod_wire_expression2/init.lua index f646ab2514..1a8a80b923 100644 --- a/lua/entities/gmod_wire_expression2/init.lua +++ b/lua/entities/gmod_wire_expression2/init.lua @@ -300,13 +300,13 @@ local PlayerChips = {} PlayerChips.__index = PlayerChips function PlayerChips:new() - return setmetatable({chips = {}}, self) + return setmetatable({}, self) end function PlayerChips:getTotalTime() local total_time = 0 - for _, chip in ipairs(self.chips) do + for _, chip in ipairs(self) do local tab = chip:GetTable() if tab.error then continue end @@ -322,7 +322,7 @@ end function PlayerChips:findMaxTimeChip() local max_chip, max_time = nil, 0 - for _, chip in ipairs(self.chips) do + for _, chip in ipairs(self) do local tab = chip:GetTable() if tab.error then continue end @@ -355,13 +355,13 @@ function PlayerChips:checkCpuTime() end function PlayerChips:add(chip) - table.insert(self.chips, chip) + table.insert(self, chip) end -function PlayerChips:remove(rmchip) - for index, chip in ipairs(self.chips) do - if rmchip == chip then - table.remove(self.chips, index) +function PlayerChips:remove(remove_chip) + for index, chip in ipairs(self) do + if remove_chip == chip then + table.remove(self, index) break end end