From 99e005e6f6a3139396ba0ef9c5da18631badf349 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 1 Jun 2025 00:32:03 -0400 Subject: [PATCH 01/35] zoomslider validity check --- lua/pac3/editor/client/panels/editor.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index 4821e2478..a567177b0 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -22,6 +22,7 @@ local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", local remember_width = CreateConVar("pac_editor_remember_width", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's width") function pace.RefreshZoomBounds(zoomslider) + if not IsValid(zoomslider) then return end if pace.Editor then if not zoomslider then zoomslider = pace.Editor.zoomslider From b106e42cfd6740245d8394dce09351335e667914 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 1 Jun 2025 03:42:14 -0400 Subject: [PATCH 02/35] cleanup damagezone hit markers more handle with part's onthink, also cleaned up on remove, dumps all hit markers into a shared table for a more thorough pass add the RefreshZone DoT mode which repeats the damagezone pass later (position may change, entities may come in or out), as opposed to only one sampling and repeating damage on these same entities add a nicename detailing key information of the damage zone when applicable: DoT, type, damage+scaling, "do not kill" critical conditions --- lua/pac3/core/client/parts/damage_zone.lua | 78 ++++++++++++++++++++-- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index f54933f4e..a12bf1652 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -125,10 +125,12 @@ BUILDER:StartStorableVars() :GetSet("CriticalHealth",1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,65535)) end}) :GetSet("MaxHpScaling", 0, {editor_clamp = {0,1}}) :SetPropertyGroup("DamageOverTime") - :GetSet("DOTMode", false, {editor_friendly = "DoT mode", - description = "Repeats your damage a few times. Subject to serverside convar."}) - :GetSet("DOTTime", 0, {editor_friendly = "DoT time", editor_clamp = {0,32}, description = "delay between each repeated damage"}) - :GetSet("DOTCount", 0, {editor_friendly = "DoT count", editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,127)) end, description = "number of repeated damage instances"}) + :GetSet("DOTMode", false, {description = "Damage over Time\nRepeats your damage a few times. Subject to serverside convar."}) + :GetSet("DOTMethod", "Debuff", { + enums = {["Debuff"] = "Debuff", ["RefreshZone"] = "RefreshZone"}, + description = "Whether the DoT means to repeat the damage on the target (handled by the server, starting from one damagezone action), or it means to retrigger the zone (handled by you, the client, throughout multiple damagezone actions).\nDebuff is like the target is burning, RefreshZone is like the area is on fire (but doesn't \"ignite\" targets)"}) + :GetSet("DOTTime", 0, {editor_clamp = {0,32}, description = "delay between each repeated damage"}) + :GetSet("DOTCount", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,127)) end, description = "number of repeated damage instances"}) :GetSet("NoInitialDOT", false, {description = "Skips the first instance (the instant one) of damage to achieve a delayed damage for example."}) :SetPropertyGroup("HitOutcome") :GetSetPart("HitSoundPart") @@ -221,6 +223,7 @@ local global_hitmarker_CSEnt_seed = 0 local spawn_queue = {} local tick = 0 +local hitparts_dump = {} --multiple entities targeted + hit marker creating parts and setting up every time = FRAME DROPS --so we tried the budget method, it didn't change the fact that it costs a lot. @@ -288,6 +291,8 @@ function PART:FindOrCreateFloatingPart(owner, ent, part_uid, id, parent_ent) --what if we don't! local tbl = pac.GetPartFromUniqueID(pac.Hash(owner), part_uid):ToTable() local group = pac.CreatePart("group", owner) --print("\tcreated a group for " .. id) + table.insert(hitparts_dump, {self, group, ent}) + self.force_cleanup_hitparts = CurTime() + math.max(self.HitMarkerLifetime, self.KillMarkerLifetime) group:SetShowInEditor(false) @@ -515,6 +520,21 @@ function PART:ClearHitMarkers() end ply.hitmarker_partpool = nil ply.hitparts = nil + --second pass + local remaining_parts = {} + for i,v in ipairs(hitparts_dump) do + if v[2]:IsValid() then + if self == v[1] then + v[2]:Remove() + hitparts_dump[i] = nil + end + else + hitparts_dump[i] = nil + end + --if it survives, reinsert it + if hitparts_dump[i] then table.insert(remaining_parts, v) end + end + hitparts_dump = remaining_parts end local function RecursedHitmarker(part) @@ -649,8 +669,8 @@ function PART:SendNetMessage() net.WriteBool(self.ReverseDoNotKill) net.WriteUInt(self.CriticalHealth, 16) net.WriteBool(self.RemoveNPCWeaponsOnKill) - net.WriteBool(self.DOTMode) - net.WriteBool(self.NoInitialDOT) + net.WriteBool(self.DOTMode and (self.DOTMethod == "Debuff")) + net.WriteBool(self.NoInitialDOT and (self.DOTMethod == "Debuff")) net.WriteUInt(self.DOTCount, 7) net.WriteUInt(math.ceil(math.Clamp(64*self.DOTTime, 0, 2047)), 11) net.WriteString(string.sub(self.UniqueID,0,6)) @@ -660,6 +680,9 @@ function PART:SendNetMessage() end function PART:OnShow() + self.remaining_DOT_count = self.DOTCount + self.next_DOT = self.NoInitialDOT and CurTime() + self.DOTTime or CurTime() - 1 + if pace.still_loading_wearing then return end if self.validTime > SysTime() then return end @@ -670,6 +693,8 @@ function PART:OnShow() if self.stop_until then self:GetPlayerOwner().stop_hit_markers_admonishment_message_up = nil end if (self:GetPlayerOwner().stop_hit_markers_admonishment_message_up) or self.stop_until > CurTime() then return end + if self.DOTMethod == "RefreshZone" then return end --handle with Think + if self:GetRootPart():GetOwner() ~= self:GetPlayerOwner() then --dumb workaround for when it activates before it realizes it needs to be hidden first timer.Simple(0.01, function() --wait to check if needs to be hidden first if self:IsHidden() or self:IsDrawHidden() then return end @@ -709,7 +734,6 @@ function PART:SetAttachPartsToTargetEntity(b) end --revertable to projectile part's version which wastes time creating new parts but has less issues -local_hitmarks = {} function PART:LegacyAttachToEntity(part, ent) if not part:IsValid() then return false end @@ -718,6 +742,8 @@ function PART:LegacyAttachToEntity(part, ent) local tbl = part:ToTable() local group = pac.CreatePart("group", self:GetPlayerOwner()) + table.insert(hitparts_dump, {self, group, ent}) + self.force_cleanup_hitparts = CurTime() + math.max(self.HitMarkerLifetime, self.KillMarkerLifetime) group:SetShowInEditor(false) local part_clone = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl)) @@ -939,6 +965,14 @@ net.Receive("pac_hit_results", function(len) end) concommand.Add("pac_cleanup_damagezone_hitmarks", function() + print(hitparts_dump, #hitparts_dump .. " parts detected") + for i,v in ipairs(hitparts_dump) do + if v[2]:IsValid() then + v[2]:Remove() + end + hitparts_dump[i] = nil + end + if LocalPlayer().hitparts then for i,v in pairs(LocalPlayer().hitparts) do v.specimen_part:Remove() @@ -963,6 +997,7 @@ function PART:OnRemove() for _,v in pairs(renderhooks) do pac.RemoveHook(v, "pace_draw_hitbox") end + self:ClearHitMarkers() end local previousRenderingHook @@ -1151,6 +1186,34 @@ end function PART:OnThink() if self.Preview then self:PreviewHitbox() end + if self.DOTMethod == "RefreshZone" then + if self.DOTTime == 0 then return end --get outta here with those zero delays + if (CurTime() > self.next_DOT) and (self.remaining_DOT_count > 0) then + self:SendNetMessage() + self.remaining_DOT_count = self.remaining_DOT_count - 1 + self.next_DOT = CurTime() + self.DOTTime + end + end + if self.force_cleanup_hitparts < CurTime() then self:ClearHitMarkers() end +end + +function PART:GetNiceName() + local str = "" + if self.DOTMode then + str = str .. " [DoT " .. self.DOTCount .. "x : " .. self.DOTTime .. "s]" + end + str = str .. " " .. self.DamageType .. " " .. self.Damage + if self.MaxHpScaling ~= 0 then str = str .. " + " .. 100*self.MaxHpScaling .. "% max HP" end + if self.ReverseDoNotKill then + if self.DamageType == "heal" then + str = str .. " [if HP > " .. self.CriticalHealth .. "]" + else + str = str .. " [if HP < " .. self.CriticalHealth .. "]" + end + elseif self.DoNotKill then + str = str .. " [stop at " .. self.CriticalHealth .. " HP]" + end + return "damage zone" .. str end function PART:BuildCylinder(obj) @@ -1219,6 +1282,7 @@ function PART:BuildCone(obj) end function PART:Initialize() + self.force_cleanup_hitparts = 0 self.hitmarkers = {} if not GetConVar("pac_sv_damage_zone"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("damage zones are disabled on this server!") end self.validTime = SysTime() + 5 --jank fix to try to stop activation on load From a613a66b112ff8786458a84f554701be96efcb1b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 1 Jun 2025 04:02:19 -0400 Subject: [PATCH 03/35] proxy optimization if a part-finding function fails repeatedly to find a valid part to cache, stop searching after 250 tries --- lua/pac3/core/client/parts/proxy.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index eddbef0b6..0ae63122d 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -108,6 +108,7 @@ function PART:GetOrFindCachedPart(uid_or_name) self.found_cached_parts = self.found_cached_parts or {} if self.found_cached_parts[uid_or_name] then self.erroring_cached_parts[uid_or_name] = nil return self.found_cached_parts[uid_or_name] end if self.erroring_cached_parts[uid_or_name] then return end + if self.bad_uid_search and self.bad_uid_search > 250 then return end local owner = self:GetPlayerOwner() part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name) @@ -119,6 +120,11 @@ function PART:GetOrFindCachedPart(uid_or_name) end if not part:IsValid() then self.erroring_cached_parts[uid_or_name] = true + self.bad_uid_search = self.bad_uid_search or 0 + self.bad_uid_search = self.bad_uid_search + 1 + if self:GetPlayerOwner() == LocalPlayer() then + pace.FlashNotification("performance warning! " .. tostring(self) .. " keeps searching for parts not finding anything! " .. tostring(uid_or_name) .. " may be unused!") + end else self.found_cached_parts[uid_or_name] = part return part @@ -1596,6 +1602,10 @@ local allowed = { function PART:SetExpression(str, slot) str = string.Trim(str,"\n") + self.bad_uid_search = nil + self.found_cached_parts = {} + self.erroring_cached_parts = {} + if self == pace.current_part and (pace.ActiveSpecialPanel and pace.ActiveSpecialPanel.luapad) and str ~= "" then --update luapad text if we update the expression from the properties if slot == pace.ActiveSpecialPanel.luapad.keynumber then --this check prevents cross-contamination From e87ec47908433c6ec91a63ddf063b823a6a92318 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 2 Jun 2025 18:23:39 -0400 Subject: [PATCH 04/35] fix a quicksetup variable --- lua/pac3/editor/client/parts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 44564e8ae..f4bee0ccd 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -3770,7 +3770,7 @@ end)]]) local group = pac.CreatePart("group") group:SetParent(obj.Parent) obj:SetParent(group) - local axismodel = pac.CreatePart("model2") axismodel:SetParent(obj) newnode:SetModel("models/editor/axis_helper_thick.mdl") newnode:SetSize(5) + local axismodel = pac.CreatePart("model2") axismodel:SetParent(obj) axismodel:SetModel("models/editor/axis_helper_thick.mdl") axismodel:SetSize(5) for i=1,5,1 do local newnode = pac.CreatePart("model2") newnode:SetParent(obj.Parent) newnode:SetModel("models/empty.mdl") newnode:SetName("test_node_"..i) From 1fb34d5c8f3d6992cb3e49834d925ad15fa2e122 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 2 Jun 2025 22:36:31 -0400 Subject: [PATCH 05/35] clamp multiline text editor on screen height the OK button could be inaccessible on small monitors --- lua/pac3/editor/client/panels/extra_properties.lua | 7 ++++--- lua/pac3/editor/client/panels/properties.lua | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 66fd611cf..8fa6f6749 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -616,14 +616,15 @@ do --generic multiline text local DButtonOK = vgui.Create("DButton", pnl) DText:SetMaximumCharCount(50000) - pnl:SetSize(1200,800) + local h = math.min(ScrH() - 100, 800) + pnl:SetSize(1200,h) pnl:SetTitle("Long text with newline support for " .. self.CurrentKey .. ". If the text is too long, do not touch the label after this!") pnl:SetPos(200, 100) DButtonOK:SetText("OK") DButtonOK:SetSize(80,20) - DButtonOK:SetPos(500, 775) + DButtonOK:SetPos(500, h - 25) DText:SetPos(5,25) - DText:SetSize(1190,700) + DText:SetSize(1190,h - 50) DText:SetMultiline(true) DText:SetContentAlignment(7) pnl:MakePopup() diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 9d604c403..c9313e967 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -1944,14 +1944,15 @@ do -- base editable local DButtonOK = vgui.Create("DButton", pnl) DText:SetMaximumCharCount(50000) - pnl:SetSize(1200,800) + local h = math.min(ScrH() - 100, 800) + pnl:SetSize(1200,h) pnl:SetTitle("Long text with newline support for " .. self.CurrentKey .. ". Do not touch the label after this!") pnl:SetPos(200, 100) DButtonOK:SetText("OK") DButtonOK:SetSize(80,20) - DButtonOK:SetPos(500, 775) + DButtonOK:SetPos(500, h - 25) DText:SetPos(5,25) - DText:SetSize(1190,700) + DText:SetSize(1190,h - 50) DText:SetMultiline(true) DText:SetContentAlignment(7) pnl:MakePopup() From 1e4e91cc991b940aa8b466d66a0b7cb397fce866 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 5 Jun 2025 23:37:32 -0400 Subject: [PATCH 06/35] Update command.lua --- lua/pac3/core/client/parts/command.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index 041af61fc..9d82db3bd 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -75,7 +75,7 @@ end function PART:SetAppendedNumber(val) if self.AppendedNumber ~= val then self.AppendedNumber = val - if self:GetPlayerOwner() == pac.LocalPlayer and self.DynamicMode then + if self:GetPlayerOwner() == pac.LocalPlayer and self.DynamicMode and not self:IsHidden() then self:Execute() end end From 556fc95e2fb12fac25a65b6429eb33728f93fd3d Mon Sep 17 00:00:00 2001 From: thecraftianman <64441307+thecraftianman@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:46:48 -0400 Subject: [PATCH 07/35] Add spawnmenu icon to hands SWEP (#1415) --- lua/pac3/extra/shared/hands.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/extra/shared/hands.lua b/lua/pac3/extra/shared/hands.lua index 3c4132d1e..af2b7139d 100644 --- a/lua/pac3/extra/shared/hands.lua +++ b/lua/pac3/extra/shared/hands.lua @@ -10,6 +10,7 @@ SWEP.PrintName = "Hands" SWEP.DrawAmmo = false SWEP.DrawCrosshair = true SWEP.DrawWeaponInfoBox = true +SWEP.IconOverride = "gui/hand_human_left.png" SWEP.SlotPos = 1 SWEP.Slot = 1 From adff17e75827ace1f135c409339c8b66e6f24f2a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 8 Jun 2025 17:54:34 -0400 Subject: [PATCH 08/35] better part-hidden navigation when a part is hidden, it's now a button that will trigger the "jump to" navigation menu in the property's line --- lua/pac3/editor/client/panels/properties.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index c9313e967..ae0e545f0 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -721,10 +721,17 @@ do -- list if not table.IsEmpty(reasons_hidden) then pnl:SetTooltip("Hidden by:" .. table.ToString(reasons_hidden, "", true)) local label = pnl:CreateAlternateLabel("hidden") - label.DoRightClick = function() + + local goto_btn = vgui.Create("DButton", pnl) + goto_btn:SetText("") + goto_btn:SetSize(self:GetItemHeight(), self:GetItemHeight()) + goto_btn:Dock(RIGHT) + goto_btn:SetImage("icon16/arrow_turn_right.png") + + goto_btn.DoClick = function() local menu = DermaMenu() menu:SetPos(input.GetCursorPos()) - for part,reason in pairs(tbl) do + for part,reason in pairs(reasons_hidden) do if part ~= pace.current_part then menu:AddOption("jump to " .. tostring(part), function() pace.GoToPart(part) From 97fbcaabde6ffc4e0a92ea2c032ce13b49def3f9 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 8 Jun 2025 18:07:53 -0400 Subject: [PATCH 09/35] restructure bookmarks/favourite assets this change will be in two parts because the diffs are an absolute mess --- lua/pac3/editor/client/panels/properties.lua | 355 +++---------------- 1 file changed, 46 insertions(+), 309 deletions(-) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index ae0e545f0..136cd4049 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -250,6 +250,9 @@ local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight) return btn end +local function populate_bookmarks(menu, mode, self) +end + function pace.CreateSearchList(property, key, name, add_columns, get_list, get_current, add_line, select_value, select_value_search) select_value = select_value or function(val, key) return val end select_value_search = select_value_search or select_value @@ -742,6 +745,44 @@ do -- list end end pace.current_part.hide_property_pnl = var + elseif key == "Model" then + local btn2 = vgui.Create("DImageButton", pnl) + btn2:SetSize(self:GetItemHeight(), self:GetItemHeight()) + btn2:Dock(RIGHT) pnl:DockPadding(0,0,self:GetItemHeight(),0) + btn2:SetTooltip("bookmarks") + btn2:SetImage("icon16/cart_go.png") + btn2.DoClick = function() + local menu = DermaMenu() + menu:SetPos(input.GetCursorPos()) + menu:MakePopup() + populate_bookmarks(menu, "models", pace.current_part) + end + elseif key == "Material" or key == "SpritePath" then + local btn2 = vgui.Create("DImageButton", pnl) + btn2:SetSize(self:GetItemHeight(), self:GetItemHeight()) + btn2:Dock(RIGHT) pnl:DockPadding(0,0,self:GetItemHeight(),0) + btn2:SetTooltip("bookmarks") + btn2:SetImage("icon16/cart_go.png") + btn2.DoClick = function() + local menu = DermaMenu() + menu:SetPos(input.GetCursorPos()) + menu:MakePopup() + populate_bookmarks(menu, "materials", pace.current_part) + end + elseif string.find(pace.current_part.ClassName, "sound") then + if key == "Sound" or key == "Path" then + local btn2 = vgui.Create("DImageButton", pnl) + btn2:SetSize(self:GetItemHeight(), self:GetItemHeight()) + btn2:Dock(RIGHT) pnl:DockPadding(0,0,self:GetItemHeight(),0) + btn2:SetTooltip("bookmarks") + btn2:SetImage("icon16/cart_go.png") + btn2.DoClick = function() + local menu = DermaMenu() + menu:SetPos(input.GetCursorPos()) + menu:MakePopup() + populate_bookmarks(menu, "sound", pace.current_part) + end + end end end @@ -1624,320 +1665,16 @@ do -- base editable end if self.CurrentKey == "Model" then - pace.bookmarked_ressources = pace.bookmarked_ressources or {} - if not pace.bookmarked_ressources["models"] then - pace.bookmarked_ressources["models"] = { - "models/pac/default.mdl", - "models/pac/plane.mdl", - "models/pac/circle.mdl", - "models/hunter/blocks/cube025x025x025.mdl", - "models/editor/axis_helper.mdl", - "models/editor/axis_helper_thick.mdl" - } - end - - local menu2, pnl = menu:AddSubMenu(L"Load favourite models", function() - end) - pnl:SetImage("icon16/cart_go.png") - - local pm = pace.current_part:GetPlayerOwner():GetModel() - local pm_selected = player_manager.TranslatePlayerModel(GetConVar("cl_playermodel"):GetString()) - - if pm_selected ~= pm then - menu2:AddOption("Selected playermodel - " .. string.gsub(string.GetFileFromFilename(pm_selected), ".mdl", ""), function() - pace.current_part:SetModel(pm_selected) - pace.current_part.pace_properties["Model"]:SetValue(pm_selected) - pace.PopulateProperties(pace.current_part) - - end):SetImage("materials/spawnicons/"..string.gsub(pm_selected, ".mdl", "")..".png") - end - - if IsValid(pace.current_part:GetRootPart():GetOwner()) then - local root_model = pace.current_part:GetRootPart():GetOwner():GetModel() - if root_model ~= pm then - if not file.Exists("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png", "GAME") then - pace.FlashNotification("missing spawn icon") - local spawnicon = vgui.Create("SpawnIcon") - spawnicon:SetPos(0,0) - spawnicon:SetModel(root_model) - spawnicon:RebuildSpawnIcon() - timer.Simple(2, function() - spawnicon:Remove() - end) - end - local pnl = menu2:AddOption("root owner model - " .. string.gsub(string.GetFileFromFilename(root_model), ".mdl", ""), function() - pace.current_part:SetModel(root_model) - pace.current_part.pace_properties["Model"]:SetValue(root_model) - pace.PopulateProperties(pace.current_part) - - end) - pnl:SetImage("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png") - timer.Simple(0, function() - pnl:SetImage("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png") - end) - end - end - - menu2:AddOption("Active playermodel - " .. string.gsub(string.GetFileFromFilename(pm), ".mdl", ""), function() - pace.current_part:SetModel(pm) - pace.current_part.pace_properties["Model"]:SetValue(pm) - pace.PopulateProperties(pace.current_part) - end):SetImage("materials/spawnicons/"..string.gsub(pm, ".mdl", "")..".png") - - if IsValid(pac.LocalPlayer:GetActiveWeapon()) then - local wep = pac.LocalPlayer:GetActiveWeapon() - local wep_mdl = wep:GetModel() - menu2:AddOption("Active weapon - " .. wep:GetClass() .. " - model - " .. string.gsub(string.GetFileFromFilename(wep_mdl), ".mdl", ""), function() - pace.current_part:SetModel(wep_mdl) - pace.current_part.pace_properties["Model"]:SetValue(wep_mdl) - pace.PopulateProperties(pace.current_part) - end):SetImage("materials/spawnicons/"..string.gsub(wep_mdl, ".mdl", "")..".png") - end - - for id,mdl in ipairs(pace.bookmarked_ressources["models"]) do - if string.sub(mdl, 1, 7) == "folder:" then - mdl = string.sub(mdl, 8, #mdl) - local menu3, pnl2 = menu2:AddSubMenu(string.GetFileFromFilename(mdl), function() - end) - pnl2:SetImage("icon16/folder.png") - - local files = get_files_recursively(nil, mdl, "mdl") - - for i,file in ipairs(files) do - menu3:AddOption(string.GetFileFromFilename(file), function() - self:SetValue(file) - pace.current_part:SetModel(file) - timer.Simple(0.2, function() - pace.current_part.pace_properties["Model"]:SetValue(file) - pace.PopulateProperties(pace.current_part) - end) - end):SetImage("materials/spawnicons/"..string.gsub(file, ".mdl", "")..".png") - end - else - menu2:AddOption(string.GetFileFromFilename(mdl), function() - self:SetValue(mdl) - pace.current_part:SetModel(mdl) - timer.Simple(0.2, function() - pace.current_part.pace_properties["Model"]:SetValue(mdl) - pace.PopulateProperties(pace.current_part) - end) - end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") - end - end + populate_bookmarks(menu, "models", self) end if self.CurrentKey == "Material" or self.CurrentKey == "SpritePath" then - pace.bookmarked_ressources = pace.bookmarked_ressources or {} - if not pace.bookmarked_ressources["materials"] then - pace.bookmarked_ressources["materials"] = { - "models/debug/debugwhite.vmt", - "vgui/null.vmt", - "debug/env_cubemap_model.vmt", - "models/wireframe.vmt", - "cable/physbeam.vmt", - "cable/cable2.vmt", - "effects/tool_tracer.vmt", - "effects/flashlight/logo.vmt", - "particles/flamelet[1,5]", - "sprites/key_[0,9]", - "vgui/spawnmenu/generating.vmt", - "vgui/spawnmenu/hover.vmt", - "metal" - } - end - - local menu2, pnl = menu:AddSubMenu(L"Load favourite materials", function() - end) - pnl:SetImage("icon16/cart_go.png") - - for id,mat in ipairs(pace.bookmarked_ressources["materials"]) do - mat = string.gsub(mat, "^materials/", "") - local mat_no_ext = string.StripExtension(mat) - - if string.sub(mat, 1, 7) == "folder:" then - local path = string.sub(mat, 8, #mat) - local menu3, pnl2 = menu2:AddSubMenu(string.GetFileFromFilename(path), function() - end) - pnl2:SetImage("icon16/folder.png") pnl2:SetTooltip(mat) - - local files = get_files_recursively(nil, path, {"vmt"}) - - for i,file in ipairs(files) do - local mat_no_ext = string.StripExtension(string.sub(file,11,#file)) --"materials/" - menu3:AddOption(mat_no_ext, function() - self:SetValue(mat_no_ext) - if self.CurrentKey == "Material" then - pace.current_part:SetMaterial(mat_no_ext) - elseif self.CurrentKey == "SpritePath" then - pace.current_part:SetSpritePath(mat_no_ext) - end - end):SetMaterial(mat_no_ext) - end - elseif string.find(mat, "%[%d+,%d+%]") then --find the bracket notation - mat_no_ext = string.gsub(mat_no_ext, "%[%d+,%d+%]", "") - pace.AddSubmenuWithBracketExpansion(menu2, function(str) - str = str or "" - str = string.StripExtension(string.gsub(str, "^materials/", "")) - self:SetValue(str) - if self.CurrentKey == "Material" then - pace.current_part:SetMaterial(str) - elseif self.CurrentKey == "SpritePath" then - pace.current_part:SetSpritePath(str) - end - end, mat_no_ext, "vmt", "materials") - - else - menu2:AddOption(string.StripExtension(mat), function() - self:SetValue(mat_no_ext) - if self.CurrentKey == "Material" then - pace.current_part:SetMaterial(mat_no_ext) - elseif self.CurrentKey == "SpritePath" then - pace.current_part:SetSpritePath(mat_no_ext) - end - end):SetMaterial(mat) - end - - end - - local pac_materials = {} - local has_pac_materials = false - - local class_shaders = { - ["material"] = "VertexLitGeneric", - ["material_3d"] = "VertexLitGeneric", - ["material_2d"] = "UnlitGeneric", - ["material_eye refract"] = "EyeRefract", - ["material_refract"] = "Refract", - } - - for _,part in pairs(pac.GetLocalParts()) do - if part.Name ~= "" and string.find(part.ClassName, "material") then - if pac_materials[class_shaders[part.ClassName]] == nil then pac_materials[class_shaders[part.ClassName]] = {} end - has_pac_materials = true - pac_materials[class_shaders[part.ClassName]][part:GetName()] = {part = part, shader = class_shaders[part.ClassName]} - end - end - if has_pac_materials then - menu2:AddSpacer() - for shader,mats in pairs(pac_materials) do - local shader_submenu = menu2:AddSubMenu("pac3 materials - " .. shader) - for mat,tbl in pairs(mats) do - local part = tbl.part - local pnl2 = shader_submenu:AddOption(mat, function() - self:SetValue(mat) - if self.CurrentKey == "Material" then - pace.current_part:SetMaterial(mat) - elseif self.CurrentKey == "SpritePath" then - pace.current_part:SetSpritePath(mat) - end - end) - pnl2:SetMaterial(pac.Material(mat, part)) - pnl2:SetTooltip(tbl.shader) - end - end - end - - if self.CurrentKey == "Material" and pace.current_part.ClassName == "particles" then - pnl:SetTooltip("Appropriate shaders for particles are UnlitGeneric materials.\nOOtherwise, they should usually be additive or use VertexAlpha") - elseif self.CurrentKey == "SpritePath" then - pnl:SetTooltip("Appropriate shaders for sprites are UnlitGeneric materials.\nOOtherwise, they should usually be additive or use VertexAlpha") - end + populate_bookmarks(menu, "materials", self) end if string.find(pace.current_part.ClassName, "sound") then if self.CurrentKey == "Sound" or self.CurrentKey == "Path" then - pace.bookmarked_ressources = pace.bookmarked_ressources or {} - if not pace.bookmarked_ressources["sound"] then - pace.bookmarked_ressources["sound"] = { - "music/hl1_song11.mp3", - "music/hl2_song23_suitsong3.mp3", - "music/hl2_song1.mp3", - "npc/combine_gunship/dropship_engine_near_loop1.wav", - "ambient/alarms/warningbell1.wav", - "phx/epicmetal_hard7.wav", - "phx/explode02.wav" - } - end - - local menu2, pnl = menu:AddSubMenu(L"Load favourite sounds", function() - end) - pnl:SetImage("icon16/cart_go.png") - - for id,snd in ipairs(pace.bookmarked_ressources["sound"]) do - local extension = string.GetExtensionFromFilename(snd) - local snd_no_ext = string.StripExtension(snd) - local single_menu = not favorites_menu_expansion:GetBool() - - if string.sub(snd, 1, 7) == "folder:" then - snd = string.sub(snd, 8, #snd) - local menu3, pnl2 = menu2:AddSubMenu(string.GetFileFromFilename(snd), function() - end) - pnl2:SetImage("icon16/folder.png") pnl2:SetTooltip(snd) - - local files = get_files_recursively(nil, snd, {"wav", "mp3", "ogg"}) - - for i,file in ipairs(files) do - file = string.sub(file,7,#file) --"sound/" - local icon = "icon16/sound.png" - if string.find(file, "music") or string.find(file, "theme") then - icon = "icon16/music.png" - elseif string.find(file, "loop") then - icon = "icon16/arrow_rotate_clockwise.png" - end - local pnl3 = menu3:AddOption(string.GetFileFromFilename(file), function() - self:SetValue(file) - if self.CurrentKey == "Sound" then - pace.current_part:SetSound(file) - elseif self.CurrentKey == "Path" then - pace.current_part:SetPath(file) - end - end) - pnl3:SetImage(icon) pnl3:SetTooltip(file) - end - elseif string.find(snd_no_ext, "%[%d+,%d+%]") then --find the bracket notation - pace.AddSubmenuWithBracketExpansion(menu2, function(str) - self:SetValue(str) - if self.CurrentKey == "Sound" then - pace.current_part:SetSound(str) - elseif self.CurrentKey == "Path" then - pace.current_part:SetPath(str) - end - end, snd_no_ext, extension, "sound") - - elseif not single_menu and string.find(snd_no_ext, "%d+") then --find a file ending in a number - --expand only if we want it with the cvar - pace.AddSubmenuWithBracketExpansion(menu2, function(str) - self:SetValue(str) - if self.CurrentKey == "Sound" then - pace.current_part:SetSound(str) - elseif self.CurrentKey == "Path" then - pace.current_part:SetPath(str) - end - end, snd_no_ext, extension, "sound") - - else - - local icon = "icon16/sound.png" - - if string.find(snd, "music") or string.find(snd, "theme") then - icon = "icon16/music.png" - elseif string.find(snd, "loop") then - icon = "icon16/arrow_rotate_clockwise.png" - end - - menu2:AddOption(snd, function() - self:SetValue(snd) - if self.CurrentKey == "Sound" then - pace.current_part:SetSound(snd) - elseif self.CurrentKey == "Path" then - pace.current_part:SetPath(snd) - end - - end):SetIcon(icon) - end - - - end + populate_bookmarks(menu, "sound", self) end end @@ -2106,7 +1843,7 @@ do -- base editable local inset_x = self:GetTextInset() pac.AddHook('Think', hookID, function(code) - if not IsValid(self) or not IsValid(textEntry) then return pac.RemoveHook('Think', hookID) end + if not IsValid(self) or not IsValid(textEntry) or self.CurrentKey == nil then return pac.RemoveHook('Think', hookID) end if textEntry:IsHovered() or self:IsHovered() then return end if delay > os.clock() then return end if not input.IsMouseDown(MOUSE_LEFT) and not input.IsKeyDown(KEY_ESCAPE) then return end @@ -2163,7 +1900,7 @@ do -- base editable --draw a rectangle with property key's name and arrows to show where the line is scrolling out of bounds pac.AddHook('PostRenderVGUI', hookID .. "2", function(code) - if not IsValid(self) or not IsValid(pnl) then pac.RemoveHook('Think', hookID .. "2") return end + if not IsValid(self) or not IsValid(pnl) or self.CurrentKey == nil then pac.RemoveHook('Think', hookID .. "2") return end local _,prop_y = pace.properties:LocalToScreen(0,0) local x, y = self:LocalToScreen() local overflow = y < prop_y or y > ScrH() - self:GetTall() From 08885121ec5f455010aa10682d14d68de7f69519 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 8 Jun 2025 18:13:07 -0400 Subject: [PATCH 10/35] restructure bookmarks/favourite assets (part 2) they will show up as buttons beside the [...] button to be more visible instead of text label right click which people don't know about --- lua/pac3/editor/client/panels/properties.lua | 312 +++++++++++++++++++ 1 file changed, 312 insertions(+) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 136cd4049..e249440dc 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -251,6 +251,317 @@ local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight) end local function populate_bookmarks(menu, mode, self) + if mode == "models" then + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + if not pace.bookmarked_ressources["models"] then + pace.bookmarked_ressources["models"] = { + "models/pac/default.mdl", + "models/pac/plane.mdl", + "models/pac/circle.mdl", + "models/hunter/blocks/cube025x025x025.mdl", + "models/editor/axis_helper.mdl", + "models/editor/axis_helper_thick.mdl" + } + end + + local menu2, pnl = menu:AddSubMenu(L"Load favourite models", function() + end) + pnl:SetImage("icon16/cart_go.png") + + local pm = pace.current_part:GetPlayerOwner():GetModel() + local pm_selected = player_manager.TranslatePlayerModel(GetConVar("cl_playermodel"):GetString()) + + if pm_selected ~= pm then + menu2:AddOption("Selected playermodel - " .. string.gsub(string.GetFileFromFilename(pm_selected), ".mdl", ""), function() + pace.current_part:SetModel(pm_selected) + pace.current_part.pace_properties["Model"]:SetValue(pm_selected) + pace.PopulateProperties(pace.current_part) + + end):SetImage("materials/spawnicons/"..string.gsub(pm_selected, ".mdl", "")..".png") + end + + if IsValid(pace.current_part:GetRootPart():GetOwner()) then + local root_model = pace.current_part:GetRootPart():GetOwner():GetModel() + if root_model ~= pm then + if not file.Exists("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png", "GAME") then + pace.FlashNotification("missing spawn icon") + local spawnicon = vgui.Create("SpawnIcon") + spawnicon:SetPos(0,0) + spawnicon:SetModel(root_model) + spawnicon:RebuildSpawnIcon() + timer.Simple(2, function() + spawnicon:Remove() + end) + end + local pnl = menu2:AddOption("root owner model - " .. string.gsub(string.GetFileFromFilename(root_model), ".mdl", ""), function() + pace.current_part:SetModel(root_model) + pace.current_part.pace_properties["Model"]:SetValue(root_model) + pace.PopulateProperties(pace.current_part) + + end) + pnl:SetImage("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png") + timer.Simple(0, function() + pnl:SetImage("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png") + end) + end + end + + menu2:AddOption("Active playermodel - " .. string.gsub(string.GetFileFromFilename(pm), ".mdl", ""), function() + pace.current_part:SetModel(pm) + pace.current_part.pace_properties["Model"]:SetValue(pm) + pace.PopulateProperties(pace.current_part) + end):SetImage("materials/spawnicons/"..string.gsub(pm, ".mdl", "")..".png") + + if IsValid(pac.LocalPlayer:GetActiveWeapon()) then + local wep = pac.LocalPlayer:GetActiveWeapon() + local wep_mdl = wep:GetModel() + menu2:AddOption("Active weapon - " .. wep:GetClass() .. " - model - " .. string.gsub(string.GetFileFromFilename(wep_mdl), ".mdl", ""), function() + pace.current_part:SetModel(wep_mdl) + pace.current_part.pace_properties["Model"]:SetValue(wep_mdl) + pace.PopulateProperties(pace.current_part) + end):SetImage("materials/spawnicons/"..string.gsub(wep_mdl, ".mdl", "")..".png") + end + + for id,mdl in ipairs(pace.bookmarked_ressources["models"]) do + if string.sub(mdl, 1, 7) == "folder:" then + mdl = string.sub(mdl, 8, #mdl) + local menu3, pnl2 = menu2:AddSubMenu(string.GetFileFromFilename(mdl), function() + end) + pnl2:SetImage("icon16/folder.png") + + local files = get_files_recursively(nil, mdl, "mdl") + + for i,file in ipairs(files) do + menu3:AddOption(string.GetFileFromFilename(file), function() + self:SetValue(file) + pace.current_part:SetModel(file) + timer.Simple(0.2, function() + pace.current_part.pace_properties["Model"]:SetValue(file) + pace.PopulateProperties(pace.current_part) + end) + end):SetImage("materials/spawnicons/"..string.gsub(file, ".mdl", "")..".png") + end + else + menu2:AddOption(string.GetFileFromFilename(mdl), function() + self:SetValue(mdl) + pace.current_part:SetModel(mdl) + timer.Simple(0.2, function() + pace.current_part.pace_properties["Model"]:SetValue(mdl) + pace.PopulateProperties(pace.current_part) + end) + end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") + end + end + elseif mode == "materials" then + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + if not pace.bookmarked_ressources["materials"] then + pace.bookmarked_ressources["materials"] = { + "models/debug/debugwhite.vmt", + "vgui/null.vmt", + "debug/env_cubemap_model.vmt", + "models/wireframe.vmt", + "cable/physbeam.vmt", + "cable/cable2.vmt", + "effects/tool_tracer.vmt", + "effects/flashlight/logo.vmt", + "particles/flamelet[1,5]", + "sprites/key_[0,9]", + "vgui/spawnmenu/generating.vmt", + "vgui/spawnmenu/hover.vmt", + "metal" + } + end + + local menu2, pnl = menu:AddSubMenu(L"Load favourite materials", function() + end) + pnl:SetImage("icon16/cart_go.png") + + for id,mat in ipairs(pace.bookmarked_ressources["materials"]) do + mat = string.gsub(mat, "^materials/", "") + local mat_no_ext = string.StripExtension(mat) + + if string.sub(mat, 1, 7) == "folder:" then + local path = string.sub(mat, 8, #mat) + local menu3, pnl2 = menu2:AddSubMenu(string.GetFileFromFilename(path), function() + end) + pnl2:SetImage("icon16/folder.png") pnl2:SetTooltip(mat) + + local files = get_files_recursively(nil, path, {"vmt"}) + + for i,file in ipairs(files) do + local mat_no_ext = string.StripExtension(string.sub(file,11,#file)) --"materials/" + menu3:AddOption(mat_no_ext, function() + self:SetValue(mat_no_ext) + if self.CurrentKey == "Material" then + pace.current_part:SetMaterial(mat_no_ext) + elseif self.CurrentKey == "SpritePath" then + pace.current_part:SetSpritePath(mat_no_ext) + end + end):SetMaterial(mat_no_ext) + end + elseif string.find(mat, "%[%d+,%d+%]") then --find the bracket notation + mat_no_ext = string.gsub(mat_no_ext, "%[%d+,%d+%]", "") + pace.AddSubmenuWithBracketExpansion(menu2, function(str) + str = str or "" + str = string.StripExtension(string.gsub(str, "^materials/", "")) + self:SetValue(str) + if self.CurrentKey == "Material" then + pace.current_part:SetMaterial(str) + elseif self.CurrentKey == "SpritePath" then + pace.current_part:SetSpritePath(str) + end + end, mat_no_ext, "vmt", "materials") + + else + menu2:AddOption(string.StripExtension(mat), function() + self:SetValue(mat_no_ext) + if self.CurrentKey == "Material" then + pace.current_part:SetMaterial(mat_no_ext) + elseif self.CurrentKey == "SpritePath" then + pace.current_part:SetSpritePath(mat_no_ext) + end + end):SetMaterial(mat) + end + + end + + local pac_materials = {} + local has_pac_materials = false + + local class_shaders = { + ["material"] = "VertexLitGeneric", + ["material_3d"] = "VertexLitGeneric", + ["material_2d"] = "UnlitGeneric", + ["material_eye refract"] = "EyeRefract", + ["material_refract"] = "Refract", + } + + for _,part in pairs(pac.GetLocalParts()) do + if part.Name ~= "" and string.find(part.ClassName, "material") then + if pac_materials[class_shaders[part.ClassName]] == nil then pac_materials[class_shaders[part.ClassName]] = {} end + has_pac_materials = true + pac_materials[class_shaders[part.ClassName]][part:GetName()] = {part = part, shader = class_shaders[part.ClassName]} + end + end + if has_pac_materials then + menu2:AddSpacer() + for shader,mats in pairs(pac_materials) do + local shader_submenu = menu2:AddSubMenu("pac3 materials - " .. shader) + for mat,tbl in pairs(mats) do + local part = tbl.part + local pnl2 = shader_submenu:AddOption(mat, function() + self:SetValue(mat) + if self.CurrentKey == "Material" then + pace.current_part:SetMaterial(mat) + elseif self.CurrentKey == "SpritePath" then + pace.current_part:SetSpritePath(mat) + end + end) + pnl2:SetMaterial(pac.Material(mat, part)) + pnl2:SetTooltip(tbl.shader) + end + end + end + + if self.CurrentKey == "Material" and pace.current_part.ClassName == "particles" then + pnl:SetTooltip("Appropriate shaders for particles are UnlitGeneric materials.\nOOtherwise, they should usually be additive or use VertexAlpha") + elseif self.CurrentKey == "SpritePath" then + pnl:SetTooltip("Appropriate shaders for sprites are UnlitGeneric materials.\nOOtherwise, they should usually be additive or use VertexAlpha") + end + elseif mode == "sound" then + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + if not pace.bookmarked_ressources["sound"] then + pace.bookmarked_ressources["sound"] = { + "music/hl1_song11.mp3", + "music/hl2_song23_suitsong3.mp3", + "music/hl2_song1.mp3", + "npc/combine_gunship/dropship_engine_near_loop1.wav", + "ambient/alarms/warningbell1.wav", + "phx/epicmetal_hard7.wav", + "phx/explode02.wav" + } + end + + local menu2, pnl = menu:AddSubMenu(L"Load favourite sounds", function() + end) + pnl:SetImage("icon16/cart_go.png") + + for id,snd in ipairs(pace.bookmarked_ressources["sound"]) do + local extension = string.GetExtensionFromFilename(snd) + local snd_no_ext = string.StripExtension(snd) + local single_menu = not favorites_menu_expansion:GetBool() + + if string.sub(snd, 1, 7) == "folder:" then + snd = string.sub(snd, 8, #snd) + local menu3, pnl2 = menu2:AddSubMenu(string.GetFileFromFilename(snd), function() + end) + pnl2:SetImage("icon16/folder.png") pnl2:SetTooltip(snd) + + local files = get_files_recursively(nil, snd, {"wav", "mp3", "ogg"}) + + for i,file in ipairs(files) do + file = string.sub(file,7,#file) --"sound/" + local icon = "icon16/sound.png" + if string.find(file, "music") or string.find(file, "theme") then + icon = "icon16/music.png" + elseif string.find(file, "loop") then + icon = "icon16/arrow_rotate_clockwise.png" + end + local pnl3 = menu3:AddOption(string.GetFileFromFilename(file), function() + self:SetValue(file) + if self.CurrentKey == "Sound" then + pace.current_part:SetSound(file) + elseif self.CurrentKey == "Path" then + pace.current_part:SetPath(file) + end + end) + pnl3:SetImage(icon) pnl3:SetTooltip(file) + end + elseif string.find(snd_no_ext, "%[%d+,%d+%]") then --find the bracket notation + pace.AddSubmenuWithBracketExpansion(menu2, function(str) + self:SetValue(str) + if self.CurrentKey == "Sound" then + pace.current_part:SetSound(str) + elseif self.CurrentKey == "Path" then + pace.current_part:SetPath(str) + end + end, snd_no_ext, extension, "sound") + + elseif not single_menu and string.find(snd_no_ext, "%d+") then --find a file ending in a number + --expand only if we want it with the cvar + pace.AddSubmenuWithBracketExpansion(menu2, function(str) + self:SetValue(str) + if self.CurrentKey == "Sound" then + pace.current_part:SetSound(str) + elseif self.CurrentKey == "Path" then + pace.current_part:SetPath(str) + end + end, snd_no_ext, extension, "sound") + + else + + local icon = "icon16/sound.png" + + if string.find(snd, "music") or string.find(snd, "theme") then + icon = "icon16/music.png" + elseif string.find(snd, "loop") then + icon = "icon16/arrow_rotate_clockwise.png" + end + + menu2:AddOption(snd, function() + self:SetValue(snd) + if self.CurrentKey == "Sound" then + pace.current_part:SetSound(snd) + elseif self.CurrentKey == "Path" then + pace.current_part:SetPath(snd) + end + + end):SetIcon(icon) + end + + + end + end end function pace.CreateSearchList(property, key, name, add_columns, get_list, get_current, add_line, select_value, select_value_search) @@ -727,6 +1038,7 @@ do -- list local goto_btn = vgui.Create("DButton", pnl) goto_btn:SetText("") + goto_btn:SetTooltip("jump to...") goto_btn:SetSize(self:GetItemHeight(), self:GetItemHeight()) goto_btn:Dock(RIGHT) goto_btn:SetImage("icon16/arrow_turn_right.png") From 9e8961a54c7de6cd425ce6bd7589ed5781eff5ee Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 9 Jun 2025 00:04:16 -0400 Subject: [PATCH 11/35] fix wrong variable --- lua/pac3/editor/client/panels/properties.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index e249440dc..6a99ece6f 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -1067,7 +1067,7 @@ do -- list local menu = DermaMenu() menu:SetPos(input.GetCursorPos()) menu:MakePopup() - populate_bookmarks(menu, "models", pace.current_part) + populate_bookmarks(menu, "models", var) end elseif key == "Material" or key == "SpritePath" then local btn2 = vgui.Create("DImageButton", pnl) @@ -1079,7 +1079,7 @@ do -- list local menu = DermaMenu() menu:SetPos(input.GetCursorPos()) menu:MakePopup() - populate_bookmarks(menu, "materials", pace.current_part) + populate_bookmarks(menu, "materials", var) end elseif string.find(pace.current_part.ClassName, "sound") then if key == "Sound" or key == "Path" then @@ -1092,7 +1092,7 @@ do -- list local menu = DermaMenu() menu:SetPos(input.GetCursorPos()) menu:MakePopup() - populate_bookmarks(menu, "sound", pace.current_part) + populate_bookmarks(menu, "sound", var) end end end From dd3ae0db97e750828c1875775e190c99ddfe745a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 13 Jun 2025 23:53:07 -0400 Subject: [PATCH 12/35] property link to create proxy you can create a proxy fast with right click on the number/vector property --- lua/pac3/editor/client/panels/properties.lua | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 6a99ece6f..7595df4b7 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -2067,6 +2067,14 @@ do -- base editable self.OnValueChanged(self:GetValue()) end):SetImage("icon16/arrow_switch.png") + menu:AddOption(L"apply proxy", function() + local proxy = pac.CreatePart("proxy") + proxy:SetParent(pace.current_part) + proxy:SetVariableName(self.CurrentKey) + proxy:SetExpression(self:GetValue()) + pace.OnPartSelected(proxy) pace.PopulateProperties(proxy) + end):SetImage("icon16/calculator.png") + if self.CurrentKey == "Size" then if pace.current_part.ClassName == "sprite" then menu:AddOption(L"apply size to scales", function() @@ -2552,6 +2560,26 @@ do -- vector end end end) pnl:SetImage(pace.MiscIcons.paste) pnl:SetTooltip(pace.clipboardtooltip) + + menu:AddOption(L"apply proxy", function() + local proxy = pac.CreatePart("proxy") + proxy:SetParent(pace.current_part) + proxy:SetVariableName(self.CurrentKey) + + local val = pac.CopyValue(self.vector) + + if isnumber(val) then + proxy:SetExpression(tostring(val)) + elseif type == "angle" then + proxy:SetExpression(math.Round(val.p, 4) .. "," .. math.Round(val.y, 4) .. "," .. math.Round(val.r, 4)) + elseif type == "vector" then + proxy:SetExpression(math.Round(val.x, 4) .. "," .. math.Round(val.y, 4) .. "," .. math.Round(val.z, 4)) + elseif type == "color" or type == "color2" then + proxy:SetExpression(math.Round(val.r, 4) .. "," .. math.Round(val.g, 4) .. "," .. math.Round(val.b, 4)) + end + + pace.OnPartSelected(proxy) pace.PopulateProperties(proxy) + end):SetImage("icon16/calculator.png") menu:AddSpacer() menu:AddOption(L"reset", function() if pace.current_part and pace.current_part.DefaultVars[self.CurrentKey] then From 2d85effe91c2205178a1bcb522671497e4f43eca Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 14 Jun 2025 00:54:50 -0400 Subject: [PATCH 13/35] add part lookup to proxy property function --- lua/pac3/core/client/parts/proxy.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 0ae63122d..8edd218ed 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -281,11 +281,14 @@ end PART.Inputs = {} -PART.Inputs.property = function(self, property_name, field) +PART.Inputs.property = function(self, property_name, field, uid) local part = self.TargetEntity:IsValid() and self.TargetEntity or self:GetParent() + if uid then + part = self:GetOrFindCachedPart(uid) + end - if part:IsValid() and part.GetProperty and property_name then + if part and part:IsValid() and part.GetProperty and property_name then local v = part:GetProperty(property_name) local T = type(v) From ed9b996315675a51802686a0b3f393c89fbba7d0 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 14 Jun 2025 02:04:27 -0400 Subject: [PATCH 14/35] inject proxy functions for use in luapad it makes the function names blue and pretty --- lua/pac3/core/client/parts/proxy.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 8edd218ed..32dbc3a6d 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -1643,6 +1643,8 @@ function PART:SetExpression(str, slot) for name, func in pairs(PART.Inputs) do lib[name] = function(...) return func(self, ...) end end + --we'll use that in the luapad syntax highlighting + self.lib = lib local ok, res = pac.CompileExpression(str, lib) if ok then From f132f0ab4aca765421d758033377aa1c4df8d537 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 14 Jun 2025 16:12:14 -0400 Subject: [PATCH 15/35] proxy: sample_and_hold duration is seeded separating instances of drifts is rather important... --- lua/pac3/core/client/parts/proxy.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 32dbc3a6d..3345460ea 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -456,16 +456,21 @@ PART.Inputs.sample_and_hold = function(self, seed, duration, min, max, ease) self.samplehold = self.samplehold or {} self.samplehold_prev = self.samplehold_prev or {} + self.samplehold_duration = self.samplehold_duration or {} self.samplehold_prev[seed] = self.samplehold_prev[seed] or {value = min, refresh = CurTime()} self.samplehold[seed] = self.samplehold[seed] or {value = min + math.random()*(max-min), refresh = CurTime() + duration} + self.samplehold_duration[seed] = self.samplehold_duration[seed] or CurTime() + local prev = self.samplehold_prev[seed].value - local frac = 1 - (self.samplehold[seed].refresh - CurTime()) / duration + local frac = 1 - (self.samplehold[seed].refresh - CurTime()) / self.samplehold_duration[seed] local delta = self.samplehold[seed].value - prev if CurTime() > self.samplehold[seed].refresh then self.samplehold_prev[seed] = self.samplehold[seed] - self.samplehold[seed] = {value = min + math.random()*(max-min), refresh = CurTime() + duration} + self.samplehold_duration[seed] = duration + self.samplehold[seed] = {value = min + math.random()*(max-min), refresh = CurTime() + self.samplehold_duration[seed]} + end if not ease then return self.samplehold[seed].value From f3395719bc64394b3a61ed4237875f3aeb65538b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 14 Jun 2025 20:47:47 -0400 Subject: [PATCH 16/35] add property "apply proxy" links to booleans many options basic 1/0 or basic 0/1 if_else template (custom pseudo-event) if_event templates (mirror bool from event to property) if the key is Hide, the new proxy will be placed alongside the part to prevent it stopping from thinking --- lua/pac3/editor/client/panels/properties.lua | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 7595df4b7..5a96c6a40 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -2936,6 +2936,119 @@ do -- boolean end):SetImage("icon16/arrow_turn_right.png") end end + local menu2, pnl = menu:AddSubMenu(L"apply proxy") pnl:SetImage("icon16/calculator.png") + local state_str = self:GetValue() and "1" or "0" + local invert_str = self:GetValue() and "0" or "1" + menu2:AddOption(state_str .. " show, " .. invert_str .. " hide", function() + local proxy = pac.CreatePart("proxy") + proxy:SetParent(pace.current_part) + if self.CurrentKey == "Hide" then + if proxy:HasParent() then + proxy:SetParent(pace.current_part:GetParent()) + else + local group = pac.CreatePart("group") proxy:SetParent(group) + end + proxy:SetTargetPart(pace.current_part) + end + proxy:SetVariableName(self.CurrentKey) + proxy:SetExpression(state_str) proxy:SetExpressionOnHide(invert_str) + pace.OnPartSelected(proxy) pace.PopulateProperties(proxy) + end):SetImage("icon16/calculator.png") + menu2:AddOption("(invert) " .. invert_str .. " show, " .. state_str .. " hide", function() + local proxy = pac.CreatePart("proxy") + proxy:SetParent(pace.current_part) + if self.CurrentKey == "Hide" then + if proxy:HasParent() then + proxy:SetParent(pace.current_part:GetParent()) + else + local group = pac.CreatePart("group") proxy:SetParent(group) + end + proxy:SetTargetPart(pace.current_part) + end + proxy:SetVariableName(self.CurrentKey) + proxy:SetExpression(invert_str) proxy:SetExpressionOnHide(state_str) + pace.OnPartSelected(proxy) pace.PopulateProperties(proxy) + end):SetImage("icon16/calculator.png") + menu2:AddOption("if_else template", function() + local proxy = pac.CreatePart("proxy") + proxy:SetParent(pace.current_part) + if self.CurrentKey == "Hide" then + if proxy:HasParent() then + proxy:SetParent(pace.current_part:GetParent()) + else + local group = pac.CreatePart("group") proxy:SetParent(group) + end + proxy:SetTargetPart(pace.current_part) + end + proxy:SetVariableName(self.CurrentKey) + proxy:SetExpression("if_else(50, \">\", 10, 1, 0)") + pace.FlashNotification("in the provided example, replace 50 with what to compare (usually a function), 10 with the test value.") + pace.OnPartSelected(proxy) pace.PopulateProperties(proxy) + end):SetImage("icon16/calculator.png") + + local menu3, pnl3 = menu2:AddSubMenu("if_event") pnl3:SetImage("icon16/clock_red.png") + menu3:AddOption("if_event template", function() + local proxy = pac.CreatePart("proxy") + proxy:SetParent(pace.current_part) + if self.CurrentKey == "Hide" then + if proxy:HasParent() then + proxy:SetParent(pace.current_part:GetParent()) + else + local group = pac.CreatePart("group") proxy:SetParent(group) + end + proxy:SetTargetPart(pace.current_part) + end + proxy:SetVariableName(self.CurrentKey) + proxy:SetExpression("if_event(\"event_name_or_uid\", 0, 1)") + pace.FlashNotification("in the provided example, replace event_name_or_uid with an existing event name or uid") + end):SetImage("icon16/clock_edit.png") + menu3:AddOption("if_event (creates an event)", function() + local proxy = pac.CreatePart("proxy") + local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) + proxy:SetParent(pace.current_part) event:SetParent(proxy) + if self.CurrentKey == "Hide" then + if proxy:HasParent() then + proxy:SetParent(pace.current_part:GetParent()) + else + local group = pac.CreatePart("group") proxy:SetParent(group) + end + proxy:SetTargetPart(pace.current_part) + end + proxy:SetVariableName(self.CurrentKey) + proxy:SetExpression("if_event(\"" .. event.UniqueID .. "\", 0, 1)") + pace.OnPartSelected(event) pace.PopulateProperties(event) + pace.FlashProperty(event, "Event", true) + end):SetImage("icon16/clock_go.png") + local menu4, pnl4 = menu3:AddSubMenu("from existing events") pnl4:SetImage("icon16/text_list_bullets.png") + for uid, part in pairs(pac.GetLocalParts()) do + if part.ClassName ~= "event" then continue end + local b = part.raw_event_condition + if part.Invert then + b = not b + end + local parents_str = "parents:\n" + local parent = part + while parent:HasParent() do + parent = parent:GetParent() + parents_str = parents_str .. "\n<" .. parent.ClassName .. "> " .. parent:GetName() + end + local pnl5 = menu4:AddOption(part:GetName(), function() + local proxy = pac.CreatePart("proxy") + proxy:SetParent(pace.current_part) + if self.CurrentKey == "Hide" then + if proxy:HasParent() then + proxy:SetParent(pace.current_part:GetParent()) + else + local group = pac.CreatePart("group") proxy:SetParent(group) + end + proxy:SetTargetPart(pace.current_part) + end + proxy:SetVariableName(self.CurrentKey) + proxy:SetExpression("if_event(\"" .. uid .. "\", 0, 1)") + end) pnl5:SetImage(b and "icon16/clock_red.png" or "icon16/clock_link.png") + pnl5:SetTooltip(parents_str) + end + menu:AddSpacer() menu:AddOption(L"reset", function() if pace.current_part and (pace.current_part.DefaultVars[self.CurrentKey] ~= nil) then local val = pac.CopyValue(pace.current_part.DefaultVars[self.CurrentKey]) From b7ac694f6106f01a3e511004e6ee5807a9b587bf Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 6 Jul 2025 17:45:53 -0400 Subject: [PATCH 17/35] sort outfit folders in the load/save editor menu --- lua/pac3/editor/client/saved_parts.lua | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 4ef455512..f03455e0f 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -448,7 +448,24 @@ local function populate_part(menu, part, override_part, clear) end local function populate_parts(menu, tbl, override_part, clear) - for key, data in pairs(tbl) do + local files = {} + local folders = {} + local sorted_tbl = {} + for k,v in pairs(tbl) do + if isstring(k) then + folders[k] = v + elseif isnumber(k) then + files[k] = v + end + end + + for k,v in ipairs(files) do table.insert(sorted_tbl, {k,v}) end + for k,v in SortedPairs(folders) do table.insert(sorted_tbl, {k,v}) end + + --for key, data in pairs(tbl) do + for i, tab in ipairs(sorted_tbl) do + local key = tab[1] + local data = tab[2] if not data.Path then local menu, pnl = menu:AddSubMenu(key, function() end, data) pnl:SetImage(pace.MiscIcons.load) From b36f32b9f931319898356665a20a3da4e5fe6d25 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 6 Jul 2025 20:25:20 -0400 Subject: [PATCH 18/35] sort folders in save menu there was another function to apply the folder sorting code to --- lua/pac3/editor/client/saved_parts.lua | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index f03455e0f..2f8578a26 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -689,7 +689,24 @@ local function populate_parts(menu, tbl, dir, override_part) :SetImage(pace.MiscIcons.copy) menu:AddSpacer() - for key, data in pairs(tbl) do + local files = {} + local folders = {} + local sorted_tbl = {} + for k,v in pairs(tbl) do + if isstring(k) then + folders[k] = v + elseif isnumber(k) then + files[k] = v + end + end + + for k,v in ipairs(files) do table.insert(sorted_tbl, {k,v}) end + for k,v in SortedPairs(folders) do table.insert(sorted_tbl, {k,v}) end + + --for key, data in pairs(tbl) do + for i, tab in ipairs(sorted_tbl) do + local key = tab[1] + local data = tab[2] if not data.Path then local menu, pnl = menu:AddSubMenu(key, function() end, data) pnl:SetImage(pace.MiscIcons.load) From 488a2bb50f8ddf0442c8403180148e70533e8ca9 Mon Sep 17 00:00:00 2001 From: textstack <46581273+textstack@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:51:52 -0400 Subject: [PATCH 19/35] owner validity check for viewer_steamid event --- lua/pac3/core/client/parts/event.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index d3faed68f..cf0cf2e6f 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2898,8 +2898,9 @@ PART.OldEvents = { tutorial = "activates when the local player has the steamID specified", arguments = {{find = "string"}, {include_owner = "boolean"}}, callback = function(self, ent, find, include_owner) - if include_owner then - find = find .. ";" .. self:GetOwner():SteamID() + local owner = self:GetOwner() + if include_owner and owner:IsValid() then + find = find .. ";" .. owner:SteamID() end return self:StringOperator(pac.LocalPlayer:SteamID(), find) From 7461c93bd9135c32d6a146e6a94c56e19649e264 Mon Sep 17 00:00:00 2001 From: textstack <46581273+textstack@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:57:40 -0400 Subject: [PATCH 20/35] change method for viewer_steamid owner --- lua/pac3/core/client/parts/event.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index cf0cf2e6f..981b03e3b 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2898,7 +2898,7 @@ PART.OldEvents = { tutorial = "activates when the local player has the steamID specified", arguments = {{find = "string"}, {include_owner = "boolean"}}, callback = function(self, ent, find, include_owner) - local owner = self:GetOwner() + local owner = self:GetPlayerOwner() if include_owner and owner:IsValid() then find = find .. ";" .. owner:SteamID() end From 3154d511cd897a083d5b43ac3b676691d8a67734 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 31 Jul 2025 23:04:05 -0400 Subject: [PATCH 21/35] right click shortcut: edit sprite/particle/beam material creates a material_2d part for it and auto-links it --- lua/pac3/editor/client/panels/properties.lua | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 5a96c6a40..7c82952ad 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -1982,6 +1982,33 @@ do -- base editable if self.CurrentKey == "Material" or self.CurrentKey == "SpritePath" then populate_bookmarks(menu, "materials", self) + + local part_material = pace.current_part:GetProperty(self.CurrentKey) + local mat_name = part_material:match(".+/(.+)") or "" + mat_name = mat_name .. "_" .. string.sub(pace.current_part.UniqueID,1,6) + mat_name = string.Replace(mat_name, " ", "") + local menu2, pnl = menu:AddSubMenu("Edit Material (will be named " .. mat_name .. ")", function() + local newmaterial = pac.CreatePart("material_2d") newmaterial:SetParent(pace.current_part) + newmaterial:SetName(mat_name) + newmaterial:SetProperty("LoadVmt", part_material) + pace.current_part:SetProperty(self.CurrentKey, mat_name) + end) + pnl:SetImage("icon16/paintcan.png") + + menu2:AddOption("Make transparent (vertex alpha) (for transparent textures)", function() + local newmaterial = pac.CreatePart("material_2d") newmaterial:SetParent(pace.current_part) + newmaterial:SetName(mat_name) + newmaterial:SetProperty("LoadVmt", part_material) + pace.current_part:SetProperty(self.CurrentKey, mat_name) + newmaterial:Setvertexalpha(true) + end):SetImage("icon16/paintcan.png") + menu2:AddOption("Make transparent (additive) (for black backgrounds)", function() + local newmaterial = pac.CreatePart("material_2d") newmaterial:SetParent(pace.current_part) + newmaterial:SetName(mat_name) + newmaterial:SetProperty("LoadVmt", part_material) + pace.current_part:SetProperty(self.CurrentKey, mat_name) + newmaterial:Setadditive(true) + end):SetImage("icon16/paintcan.png") end if string.find(pace.current_part.ClassName, "sound") then From 3f44d6d386daf144f5d1fe42363e2b5ba5e16e43 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 1 Aug 2025 02:46:03 -0400 Subject: [PATCH 22/35] fix quick setup's edit materials option it cleared existing submaterial assignations, which shouldn't happen it allowed multiple submaterial edits but since it cleared the initial working table, it only really worked if using the right clickable option that doesn't close the DMenu. the intended method is that we can surgically edit any material at any time without such troublesome workarounds as re-pasting old submaterial assignation --- lua/pac3/editor/client/parts.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index f4bee0ccd..773d445dd 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2690,9 +2690,8 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) local submat_toggler_proxy local submat_toggler_event - local submaterials = {} + local submaterials = string.Split(obj:GetMaterials(),";") for i,mat2 in ipairs(mats) do - table.insert(submaterials,"") local kw = string.GetFileFromFilename(mat2) AddOptionRightClickable(kw, function() if not submat_toggler_proxy then @@ -2711,7 +2710,7 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) else obj:SetMaterials(table.concat(submaterials, ";")) end - + end, submat_togglers):SetIcon("icon16/paintcan.png") end From 77a7afd158e0fb6d16fe4e51bd4f263ba7c0ce3f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 3 Aug 2025 01:03:59 -0400 Subject: [PATCH 23/35] fix sprite material initialization when the material is a pac material inside the sprite, initialization order messes up and leaves an invalid material, so waiting for the material to exist and reassign it seems to fix it --- lua/pac3/core/client/parts/sprite.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/pac3/core/client/parts/sprite.lua b/lua/pac3/core/client/parts/sprite.lua index 4aa85dd2e..91fce03c1 100644 --- a/lua/pac3/core/client/parts/sprite.lua +++ b/lua/pac3/core/client/parts/sprite.lua @@ -72,6 +72,9 @@ end function PART:Initialize() self:SetSpritePath(self.SpritePath) + if not IsValid(self.Materialm) then + timer.Simple(0, function() self:SetSpritePath(self.SpritePath) end) + end end function PART:SetSpritePath(var) From b4b480c3161e26699125a4fffcb02eab4f0fa59b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 14 Aug 2025 19:56:32 -0400 Subject: [PATCH 24/35] nil string check --- lua/pac3/core/client/parts/event.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 981b03e3b..dc7890622 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -3497,7 +3497,7 @@ function PART:GetNiceName() if not PART.Events[event_name] then return "unknown event" end - return self:GetTargetingModePrefix() .. PART.Events[event_name]:GetNiceName(self, get_owner(self)) + return self:GetTargetingModePrefix() .. (PART.Events[event_name]:GetNiceName(self, get_owner(self)) or "") end local function is_hidden_by_something_else(part, ignored_part) From 34318e981b0c2e4e076034ebd1ddc3d901a3a769 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 16 Aug 2025 18:11:40 -0400 Subject: [PATCH 25/35] is_turning events --- lua/pac3/core/client/parts/event.lua | 107 +++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index dc7890622..6e6d846a4 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -947,6 +947,113 @@ PART.OldEvents = { end, }, + is_turning = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "checks eye angle movements on pitch and yaw combined with pythagoras theorem as absolute terms. so it won't go into negatives", + arguments = {{amount = "number"}}, + callback = function(self, ent, num) + ent = try_viewmodel(ent) + local ang = ent:EyeAngles() + self.last_turning_ang = self.last_turning_ang or ang + + --pythagoras theorem + local ang_difference = math.sqrt( + math.AngleDifference(ang.p,self.last_turning_ang.p)^2 + + math.abs(math.AngleDifference(ang.y,self.last_turning_ang.y))^2 + ) / FrameTime() + + self.last_turning_ang = ang + self.turning_ang_diff = ang_difference + + return self:NumberOperator(ang_difference, num) + end, + nice = function(self, ent, amount) + if self.turning_ang_diff == nil then return "" end + return "is_turning {" .. math.Round(self.turning_ang_diff,2) .. " | " .. amount .. "}" + end + }, + is_turning_pitch = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "checks eye angle movements on pitch.", + arguments = {{pitch_amount = "number"}, {absolute = "boolean"}}, + callback = function(self, ent, pitch_amount, absolute) + ent = try_viewmodel(ent) + local ang = ent:EyeAngles() + self.last_turning_ang = self.last_turning_ang or ang + + local ang_difference_y = 0 + if absolute then + ang_difference_y = math.abs(math.AngleDifference(ang.p, self.last_turning_ang.p)) / FrameTime() + else + ang_difference_y = math.AngleDifference(ang.p, self.last_turning_ang.p) / FrameTime() + end + + self.last_turning_ang = ang + self.turning_ang_diff_y = ang_difference_y + + return self:NumberOperator(ang_difference_y, pitch_amount) + end, + nice = function(self, ent, pitch_amount, absolute) + if self.turning_ang_diff_y == nil then return "" end + return "is_turning_yaw {" .. math.Round(self.turning_ang_diff_y,2) .. " | " .. pitch_amount .. "}" + end + }, + is_turning_yaw = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "checks eye angle movements on yaw.", + arguments = {{yaw_amount = "number"}, {absolute = "boolean"}}, + callback = function(self, ent, yaw_amount, absolute) + ent = try_viewmodel(ent) + local ang = ent:EyeAngles() + self.last_turning_ang = self.last_turning_ang or ang + + local ang_difference_x = 0 + if absolute then + ang_difference_x = math.abs(math.AngleDifference(ang.y, self.last_turning_ang.y)) / FrameTime() + else + ang_difference_x = math.AngleDifference(ang.y, self.last_turning_ang.y) / FrameTime() + end + + self.last_turning_ang = ang + self.turning_ang_diff_x = ang_difference_x + + return self:NumberOperator(ang_difference_x, yaw_amount) + end, + nice = function(self, ent, yaw_amount, absolute) + if self.turning_ang_diff_x == nil then return "" end + return "is_turning_yaw {" .. math.Round(self.turning_ang_diff_x,2) .. " | " .. yaw_amount .. "}" + end + }, + is_turning_xy = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "checks eye angle movements on pitch or yaw. there are separate thresholds for each component", + arguments = {{pitch_amount = "number"}, {yaw_amount = "number"}, {absolute = "boolean"}}, + callback = function(self, ent, pitch_amount, yaw_amount, absolute) + ent = try_viewmodel(ent) + local ang = ent:EyeAngles() + self.last_turning_ang = self.last_turning_ang or ang + + local ang_difference_x = 0 + local ang_difference_y = math.abs(ang.p - self.last_turning_ang.p) / FrameTime() + + if absolute then + ang_difference_x = math.abs(math.AngleDifference(ang.y, self.last_turning_ang.y)) / FrameTime() + else + ang_difference_x = math.AngleDifference(ang.y, self.last_turning_ang.y) / FrameTime() + end + + self.last_turning_ang = ang + self.turning_ang_diff_x = ang_difference_x + self.turning_ang_diff_y = ang_difference_y + + return self:NumberOperator(ang_difference_x, yaw_amount) or self:NumberOperator(ang_difference_y, pitch_amount) + end, + nice = function(self, ent, pitch_amount, yaw_amount) + if self.turning_ang_diff_x == nil or self.turning_ang_diff_y == nil then return "" end + return "is_turning_xy {" .. math.Round(self.turning_ang_diff_x,2) .. ", " .. math.Round(self.turning_ang_diff_y,2) .. "} | {" .. yaw_amount .. ", " .. pitch_amount .. "}" + end + }, + is_under_water = { operator_type = "number", preferred_operator = "above", tutorial_explanation = "is_under_water activates when you're under a certain level of water.\nas you get deeper, the number is higher.\n0 is dry\n1 is slightly submerged (at least to the feet)\n2 is mostly submerged (at least to the waist)\n3 is completely submerged", From d9e7d6b01c5bc3f50b33182ca0c38eb5af8e418e Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 16 Aug 2025 23:50:40 -0400 Subject: [PATCH 26/35] add some partmenu actions from the doubleclick actions also move icon list to a table in parts.lua (the same file that handles partmenu stuff), to avoid needing to also edit settings.lua every time I want to add a partmenu action from now on --- lua/pac3/editor/client/parts.lua | 55 +++++++++++++++++++++++++++++ lua/pac3/editor/client/settings.lua | 55 +---------------------------- 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 773d445dd..c0d13a5eb 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -18,6 +18,40 @@ if not file.Exists("pac3_config/pac_editor_partmenu_layouts.txt", "DATA") then pace.operations_order = pace.operations_default end +pace.partmenu_action_images = { + save = pace.MiscIcons.save, + load = pace.MiscIcons.load, + wear = pace.MiscIcons.wear, + remove = pace.MiscIcons.clear, + copy = pace.MiscIcons.copy, + paste = pace.MiscIcons.paste, + cut = "icon16/cut.png", + paste_properties = pace.MiscIcons.replace, + clone = pace.MiscIcons.clone, + partsize_info = "icon16/drive.png", + bulk_apply_properties= "icon16/application_form.png", + bulk_select = "icon16/table_multiple.png", + spacer = "icon16/application_split.png", + hide_editor = "icon16/application_delete.png", + expand_all = "icon16/arrow_down.png", + collapse_all = "icon16/arrow_in.png", + copy_uid = pace.MiscIcons.uniqueid, + help_part_info = "icon16/information.png", + reorder_movables = "icon16/application_double.png", + criteria_process = "icon16/text_list_numbers.png", + bulk_morph = "icon16/chart_line.png", + arraying_menu = "icon16/shape_group.png", + view_lockon = "icon16/zoom.png", + view_goto = "icon16/arrow_turn_right.png", + rename = "icon16/text_align_center.png", + showhide = "icon16/clock_red.png", + notes = "icon16/page_white_edit.png", +} + +function pace.GetPartMenuOptionImage(str) + return pace.partmenu_action_images[str] or "icon16/world.png" +end + local hover_color = CreateConVar( "pac_hover_color", "255 255 255", FCVAR_ARCHIVE, "R G B value of the highlighting when hovering over pac3 parts, there are also special options: none, ocean, funky, rave, rainbow") CreateConVar( "pac_hover_pulserate", 20, FCVAR_ARCHIVE, "pulse rate of the highlighting when hovering over pac3 parts") CreateConVar( "pac_hover_halo_limit", 100, FCVAR_ARCHIVE, "max number of parts before hovering over pac3 parts stops computing to avoid lag") @@ -4524,6 +4558,27 @@ function pace.addPartMenuComponent(menu, obj, option_name) end end + elseif option_name == "rename" then + menu:AddOption(L"rename", function() + local old_func = pace.doubleclickfunc + RunConsoleCommand("pac_doubleclick_action", "rename") + timer.Simple(0, function() obj:OnDoubleClickBaseClass() end) + timer.Simple(0.2, function() RunConsoleCommand("pac_doubleclick_action", old_func) end) + end):SetIcon("icon16/text_align_center.png") + elseif option_name == "showhide" then + menu:AddOption(L"show/hide", function() + local old_func = pace.doubleclickfunc + RunConsoleCommand("pac_doubleclick_action", "showhide") + timer.Simple(0, function() obj:OnDoubleClickBaseClass() end) + timer.Simple(0.2, function() RunConsoleCommand("pac_doubleclick_action", old_func) end) + end):SetIcon("icon16/clock_red.png") + elseif option_name == "notes" then + menu:AddOption(L"write notes", function() + local old_func = pace.doubleclickfunc + RunConsoleCommand("pac_doubleclick_action", "notes") + timer.Simple(0, function() obj:OnDoubleClickBaseClass() end) + timer.Simple(0.2, function() RunConsoleCommand("pac_doubleclick_action", old_func) end) + end):SetIcon("icon16/page_white_edit.png") end end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 12df1c152..4fb76aa65 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1654,65 +1654,12 @@ function pace.FillEditorSettings(pnl) end end - local function FindImage(option_name) - if option_name == "save" then - return pace.MiscIcons.save - elseif option_name == "load" then - return pace.MiscIcons.load - elseif option_name == "wear" then - return pace.MiscIcons.wear - elseif option_name == "remove" then - return pace.MiscIcons.clear - elseif option_name == "copy" then - return pace.MiscIcons.copy - elseif option_name == "paste" then - return pace.MiscIcons.paste - elseif option_name == "cut" then - return "icon16/cut.png" - elseif option_name == "paste_properties" then - return pace.MiscIcons.replace - elseif option_name == "clone" then - return pace.MiscIcons.clone - elseif option_name == "partsize_info" then - return"icon16/drive.png" - elseif option_name == "bulk_apply_properties" then - return "icon16/application_form.png" - elseif option_name == "bulk_select" then - return "icon16/table_multiple.png" - elseif option_name == "spacer" then - return "icon16/application_split.png" - elseif option_name == "hide_editor" then - return "icon16/application_delete.png" - elseif option_name == "expand_all" then - return "icon16/arrow_down.png" - elseif option_name == "collapse_all" then - return "icon16/arrow_in.png" - elseif option_name == "copy_uid" then - return pace.MiscIcons.uniqueid - elseif option_name == "help_part_info" then - return "icon16/information.png" - elseif option_name == "reorder_movables" then - return "icon16/application_double.png" - elseif option_name == "criteria_process" then - return "icon16/text_list_numbers.png" - elseif option_name == "bulk_morph" then - return "icon16/chart_line.png" - elseif option_name == "arraying_menu" then - return "icon16/shape_group.png" - elseif option_name == "view_lockon" then - return "icon16/zoom.png" - elseif option_name == "view_goto" then - return "icon16/arrow_turn_right.png" - end - return "icon16/world.png" - end - partmenu_choices:SetY(50) partmenu_choices:SetX(10) for i,v in pairs(pace.operations_all_operations) do local pnl = vgui.Create("DButton", f) pnl:SetText(string.Replace(string.upper(v),"_"," ")) - pnl:SetImage(FindImage(v)) + pnl:SetImage(pace.GetPartMenuOptionImage(v)) pnl:SetTooltip("Left click to add at the end\nRight click to insert at the beginning") function pnl:DoClick() From 5ca3c51ed657c23dd0e70ed96a9f8557f8712831 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 16 Aug 2025 23:51:13 -0400 Subject: [PATCH 27/35] Update parts.lua --- lua/pac3/editor/client/parts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index c0d13a5eb..7be37efe9 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -6,7 +6,7 @@ pace.BulkSelectList = {} pace.BulkSelectUIDs = {} pace.BulkSelectClipboard = {} local refresh_halo_hook = true -pace.operations_all_operations = {"wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "save", "load", "remove", "bulk_select", "bulk_apply_properties", "partsize_info", "hide_editor", "expand_all", "collapse_all", "copy_uid", "help_part_info", "reorder_movables", "arraying_menu", "criteria_process", "bulk_morph", "view_goto", "view_lockon"} +pace.operations_all_operations = {"wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "save", "load", "remove", "bulk_select", "bulk_apply_properties", "partsize_info", "hide_editor", "expand_all", "collapse_all", "copy_uid", "help_part_info", "reorder_movables", "arraying_menu", "criteria_process", "bulk_morph", "view_goto", "view_lockon", "rename", "showhide", "notes"} pace.operations_default = {"help_part_info", "wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "spacer", "bulk_select", "bulk_apply_properties", "spacer", "save", "load", "spacer", "remove"} pace.operations_legacy = {"wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "spacer", "save", "load", "spacer", "remove"} From 5168420817d63eade3bbf157d492e6d0636e313d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 20 Aug 2025 19:36:53 -0400 Subject: [PATCH 28/35] optimize event uid search catastrophic lag occurs when part search repeatedly fails, severity scaling with localparts size it was already handled with proxy but now I transferred it to event --- lua/pac3/core/client/parts/event.lua | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 6e6d846a4..f23fec615 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -317,12 +317,11 @@ end function PART:GetOrFindCachedPart(uid_or_name) local part = nil - local existing_part = self.found_cached_parts[uid_or_name] - if existing_part then - if existing_part ~= NULL and existing_part:IsValid() then - return existing_part - end - end + self.erroring_cached_parts = {} + self.found_cached_parts = self.found_cached_parts or {} + if self.found_cached_parts[uid_or_name] then self.erroring_cached_parts[uid_or_name] = nil return self.found_cached_parts[uid_or_name] end + if self.erroring_cached_parts[uid_or_name] then return end + if self.bad_uid_search and self.bad_uid_search > 250 then return end local owner = self:GetPlayerOwner() part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name) @@ -332,7 +331,14 @@ function PART:GetOrFindCachedPart(uid_or_name) self.found_cached_parts[uid_or_name] = part return part end - if part:IsValid() then + if not part:IsValid() then + self.erroring_cached_parts[uid_or_name] = true + self.bad_uid_search = self.bad_uid_search or 0 + self.bad_uid_search = self.bad_uid_search + 1 + if self:GetPlayerOwner() == LocalPlayer() then + pace.FlashNotification("performance warning! " .. tostring(self) .. " keeps searching for parts not finding anything! " .. tostring(uid_or_name) .. " may be unused!") + end + else self.found_cached_parts[uid_or_name] = part return part end @@ -2893,7 +2899,7 @@ PART.OldEvents = { local true_count = 0 for i,uid in ipairs(uid_splits) do local part = self:GetOrFindCachedPart(uid) - if part:IsValid() then + if IsValid(part) then local raw = part.raw_event_condition local b = false if ignore_inverts then From 84df765a915fd3b8ed0b977f26279fc19174353b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 24 Aug 2025 16:22:40 -0400 Subject: [PATCH 29/35] expand text newline support word wrap or force newline will enable other modes than drawdrawtext to make newlines, by manually drawing each line, supporting line spacing wrap method caches calculated lines. it's rate limited in multiplayer, and length-limited because it's a bit expensive to calculate fix ignoreZ --- lua/pac3/core/client/parts/text.lua | 309 +++++++++++++++++++++------- 1 file changed, 240 insertions(+), 69 deletions(-) diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index c79ce9239..a8ac542fc 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -190,6 +190,13 @@ BUILDER:StartStorableVars() :GetSet("VectorBrackets", "()") :GetSet("VectorSeparator", ",") :GetSet("UseBracketsOnNonVectors", false) + :GetSet("ForceNewline", false, {description = "manually draw newlines"}) + :GetSet("LineSpacing", 1) + + :SetPropertyGroup("wrap") + :GetSet("Wrap", false, {description = "force newline if text exceeds set width or there is a newline character"}) + :GetSet("WrapWidth", 500, {editor_round = true, editor_onchange = function(self, val) return math.floor(val) end, description = "text size is dependent on font. for the 3D2D text, size doesn't matter"}) + :GetSet("WrapByWords", false, {description = "split by spaces after splitting by newline characters"}) :SetPropertyGroup("data source") :GetSet("TextOverride", "Text", {enums = { @@ -389,12 +396,13 @@ end --before using a font, we need to check if it exists --font creation time should mark them function PART:SetFont(str) + self.request_line_recalculation = true self.Font = str self:SetError() self:CheckFont() end - + local lastfontcreationtime = 0 function PART:CheckFont() if self.CreateCustomFont then @@ -658,92 +666,139 @@ function PART:OnDraw() DisplayText = string.Replace(DisplayText,"? ","?\n") end + if self.Wrap or self.ForceNewline then + if (self.lines == nil) or (self.previous_str ~= DisplayText) or self.request_line_recalculation then + self.lines = self:WrapString(DisplayText, self.WrapWidth) + end + end + if DisplayText ~= "" then local w, h = surface.GetTextSize(DisplayText) if not w or not h then return end if self.DrawMode == "DrawTextOutlined" then - cam_Start3D(EyePos(), EyeAngles()) - cam_Start3D2D(pos, ang, self.Size) - local oldState = DisableClipping(true) - - draw_SimpleTextOutlined(DisplayText, self.UsedFont, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) - render_CullMode(1) -- MATERIAL_CULLMODE_CW - - draw_SimpleTextOutlined(DisplayText, self.UsedFont, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) - render_CullMode(0) -- MATERIAL_CULLMODE_CCW - - DisableClipping(oldState) - cam_End3D2D() - cam_End3D() - cam_Start3D(EyePos(), EyeAngles()) - cam_Start3D2D(pos, ang, self.Size) - local oldState = DisableClipping(true) - - draw.SimpleText(DisplayText, self.UsedFont, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) - render_CullMode(1) -- MATERIAL_CULLMODE_CW - - draw.SimpleText(DisplayText, self.UsedFont, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) - render_CullMode(0) -- MATERIAL_CULLMODE_CCW + local y = 0 + local function drawtext(str) + cam_Start3D(EyePos(), EyeAngles()) + if self.IgnoreZ then cam.IgnoreZ(true) end + cam_Start3D2D(pos, ang, self.Size) + local oldState = DisableClipping(true) + + draw_SimpleTextOutlined(str, self.UsedFont, 0,y, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) + render_CullMode(1) -- MATERIAL_CULLMODE_CW + + draw_SimpleTextOutlined(str, self.UsedFont, 0,y, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) + render_CullMode(0) -- MATERIAL_CULLMODE_CCW + + DisableClipping(oldState) + cam_End3D2D() + cam.IgnoreZ(false) + cam_End3D() + cam_Start3D(EyePos(), EyeAngles()) + if self.IgnoreZ then cam.IgnoreZ(true) end + cam_Start3D2D(pos, ang, self.Size) + local oldState = DisableClipping(true) + + draw.SimpleText(str, self.UsedFont, 0,y, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) + render_CullMode(1) -- MATERIAL_CULLMODE_CW + + draw.SimpleText(str, self.UsedFont, 0,y, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) + render_CullMode(0) -- MATERIAL_CULLMODE_CCW + + DisableClipping(oldState) + cam_End3D2D() + cam.IgnoreZ(false) + cam_End3D() + if self.IgnoreZ then cam.IgnoreZ(false) end + end - DisableClipping(oldState) - cam_End3D2D() - cam_End3D() + if not self.Wrap and not self.ForceNewline then + drawtext(DisplayText) + else + if (self.lines == nil) or (self.previous_str ~= DisplayText) then + self.lines = self:WrapString(DisplayText, self.WrapWidth) + end + for i, str in ipairs(self.lines) do + drawtext(str) + local w, h = surface.GetTextSize(str) + if h then y = y + self.LineSpacing*h end + end + end + elseif self.DrawMode == "SurfaceText" or self.DrawMode == "DrawTextOutlined2D" or self.DrawMode == "DrawDrawText" then pac.AddHook("HUDPaint", "pac.DrawText"..self.UniqueID, function() surface.SetFont(self.UsedFont) surface.SetTextColor(self.Color.r, self.Color.g, self.Color.b, 255*self.Alpha) - + local pos2d = self:GetDrawPosition():ToScreen() local pos2d_original = table.Copy(pos2d) - local w, h = surface.GetTextSize(DisplayText) - if not h or not w then return end - - if self.HorizontalTextAlign == 0 then --left - pos2d.x = pos2d.x - elseif self.HorizontalTextAlign == 1 then --center - pos2d.x = pos2d.x - w/2 - elseif self.HorizontalTextAlign == 2 then --right - pos2d.x = pos2d.x - w - end - if self.VerticalTextAlign == 1 then --center - pos2d.y = pos2d.y - h/2 - elseif self.VerticalTextAlign == 3 then --top - pos2d.y = pos2d.y - elseif self.VerticalTextAlign == 4 then --bottom - pos2d.y = pos2d.y - h - end + local function drawtext(str) + local w, h = surface.GetTextSize(str) + if not h or not w then return end + + local X = pos2d.x + local Y = pos2d.y + + if self.HorizontalTextAlign == 0 then --left + X = pos2d.x + elseif self.HorizontalTextAlign == 1 then --center + X = pos2d.x - w/2 + elseif self.HorizontalTextAlign == 2 then --right + X = pos2d.x - w + end + + if self.VerticalTextAlign == 1 then --center + Y = pos2d.y - h/2 + elseif self.VerticalTextAlign == 3 then --top + Y = pos2d.y + elseif self.VerticalTextAlign == 4 then --bottom + Y = pos2d.y - h + end + + surface.SetTextPos(X, Y) + local dist = (pac.EyePos - self:GetWorldPosition()):Length() + + --clamp down the part's requested values with the viewer client's cvar + local fadestartdist = math.max(draw_distance:GetInt() - 200,0) + local fadeenddist = math.max(draw_distance:GetInt(),0) + + if dist < fadeenddist then + if dist < fadestartdist then + if self.DrawMode == "DrawTextOutlined2D" then + draw.SimpleTextOutlined(str, self.UsedFont, X, Y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha), TEXT_ALIGN_TOP, TEXT_ALIGN_LEFT, self.Outline, Color(self.OutlineColor.r,self.OutlineColor.g,self.OutlineColor.b, 255*self.OutlineAlpha)) + elseif self.DrawMode == "SurfaceText" then + surface.DrawText(str, self.ForceAdditive) + elseif self.DrawMode == "DrawDrawText" then + draw.DrawText(str, self.UsedFont, pos2d_original.x, Y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha), self.HorizontalTextAlign) + end + else + local fade = math.pow(math.Clamp(1 - (dist-fadestartdist)/math.max(fadeenddist - fadestartdist,0.1),0,1),3) + if self.DrawMode == "DrawTextOutlined2D" then + draw.SimpleTextOutlined(str, self.UsedFont, X, Y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha*fade), TEXT_ALIGN_TOP, TEXT_ALIGN_LEFT, self.Outline, Color(self.OutlineColor.r,self.OutlineColor.g,self.OutlineColor.b, 255*self.OutlineAlpha*fade)) + elseif self.DrawMode == "SurfaceText" then + surface.SetTextColor(self.Color.r * fade, self.Color.g * fade, self.Color.b * fade) + surface.DrawText(str, true) + elseif self.DrawMode == "DrawDrawText" then + draw.DrawText(str, self.UsedFont, X, Y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha*fade), TEXT_ALIGN_LEFT) + end - surface.SetTextPos(pos2d.x, pos2d.y) - local dist = (pac.EyePos - self:GetWorldPosition()):Length() - - --clamp down the part's requested values with the viewer client's cvar - local fadestartdist = math.max(draw_distance:GetInt() - 200,0) - local fadeenddist = math.max(draw_distance:GetInt(),0) - - if dist < fadeenddist then - if dist < fadestartdist then - if self.DrawMode == "DrawTextOutlined2D" then - draw.SimpleTextOutlined(DisplayText, self.UsedFont, pos2d.x, pos2d.y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha), TEXT_ALIGN_TOP, TEXT_ALIGN_LEFT, self.Outline, Color(self.OutlineColor.r,self.OutlineColor.g,self.OutlineColor.b, 255*self.OutlineAlpha)) - elseif self.DrawMode == "SurfaceText" then - surface.DrawText(DisplayText, self.ForceAdditive) - elseif self.DrawMode == "DrawDrawText" then - draw.DrawText(DisplayText, self.UsedFont, pos2d_original.x, pos2d.y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha), self.HorizontalTextAlign) - end - else - local fade = math.pow(math.Clamp(1 - (dist-fadestartdist)/math.max(fadeenddist - fadestartdist,0.1),0,1),3) - if self.DrawMode == "DrawTextOutlined2D" then - draw.SimpleTextOutlined(DisplayText, self.UsedFont, pos2d.x, pos2d.y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha*fade), TEXT_ALIGN_TOP, TEXT_ALIGN_LEFT, self.Outline, Color(self.OutlineColor.r,self.OutlineColor.g,self.OutlineColor.b, 255*self.OutlineAlpha*fade)) - elseif self.DrawMode == "SurfaceText" then - surface.SetTextColor(self.Color.r * fade, self.Color.g * fade, self.Color.b * fade) - surface.DrawText(DisplayText, true) - elseif self.DrawMode == "DrawDrawText" then - draw.DrawText(DisplayText, self.UsedFont, pos2d.x, pos2d.y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha*fade), TEXT_ALIGN_LEFT) end + end + end + if not self.Wrap and not self.ForceNewline then + drawtext(DisplayText) + else + if (self.lines == nil) or (self.previous_str ~= DisplayText) then + self.lines = self:WrapString(DisplayText, self.WrapWidth) + end + for i, str in ipairs(self.lines) do + drawtext(str) + local w, h = surface.GetTextSize(str) + if h then pos2d.y = pos2d.y + self.LineSpacing*h end end end end) @@ -752,9 +807,12 @@ function PART:OnDraw() pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) end else pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) end + self.previous_str = DisplayText end function PART:Initialize() + self.lines = nil + self.previous_str = "" if self.Font == "default" then self.Font = "DermaDefault" end self:TryCreateFont() self.anotherwarning = false @@ -820,7 +878,120 @@ function PART:OnRemove() end end function PART:SetText(str) + self.request_line_recalculation = true self.Text = str end +function PART:SetWrapWidth(num) + self.WrapWidth = math.Round(num,0) + self.request_line_recalculation = true +end +function PART:SetWrapByWords(b) + self.WrapByWords = b + self.request_line_recalculation = true +end +function PART:SetForceNewline(b) + self.ForceNewline = b + self.request_line_recalculation = true +end + +local font = "" +local wrap_calculation_time = 0 +local frame_reset = 0 +function PART:WrapString(str, max_w, font_override) + local stime = SysTime() + if self.UsedFont == "" then self.previous_str = nil return {} end + if not self.ForceNewline and #str > 5000 then self.previous_str = nil return {} end + if stime < wrap_calculation_time then return self.lines or {} end --rate limit + if font_override then + surface.SetFont(font_override) + else + surface.SetFont(self.UsedFont) + end + + local lines = string.Split(str, "\n") + local lines_pushed = {} + + --newline first pass + for i,v in ipairs(lines) do + local words = {} + if self.Wrap and self.WrapByWords then + words = string.Split(v, " ") + else + words = {v} + end + + if self.Wrap then + if self.WrapByWords then + local guard = 0 + local remain_tbl = words + local offset = 0 + + while (#remain_tbl > 0 and guard < 200) do + local remain_tbl_temp = {} + for i2=#words,1,-1 do --longest to shortest possible sentence + local sentence = {} + --i2 is the decreasing limit + for i3=offset,#words,1 do + --i3 is the increasing counter + if i3 < i2 then + table.insert(sentence,words[i3]) + else + table.insert(remain_tbl_temp,words[i3]) + end + end + local concatenated = table.concat(sentence, " ") + local w,_ = surface.GetTextSize(concatenated) + if w > max_w then + continue + else + offset = i2 + table.insert(lines_pushed, concatenated) + remain_tbl = remain_tbl_temp + break + end + end + if #remain_tbl_temp == 1 then lines_pushed[#lines_pushed] = lines_pushed[#lines_pushed] .. " " .. remain_tbl_temp[1] break end + + guard = guard + 1 + end + else + for i2, word in ipairs(words) do + local word = word + local remain = word + local w,_ = surface.GetTextSize(word) + + if w > max_w then --overflow + local guard = 0 + while (#remain > 0 and guard < 15) do + for c=#word,1,-1 do + local split_word = string.sub(word,1,c) + local w2,_ = surface.GetTextSize(split_word) + if w2 > max_w then + continue + else + table.insert(lines_pushed, split_word) + remain = string.sub(word,c+1,#word) + word = remain + break + end + end + guard = guard + 1 + end + else + table.insert(lines_pushed, remain) + end + end + end + else + table.insert(lines_pushed, v) + end + end + local delta = SysTime() - stime + + if game.SinglePlayer() then wrap_calculation_time = SysTime() else wrap_calculation_time = SysTime() + 0.5 end + self.request_line_recalculation = false + return lines_pushed +end + BUILDER:Register() From 92d5d4eab966403443acb0fd10834fa08ab1cfb2 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 24 Aug 2025 17:28:24 -0400 Subject: [PATCH 30/35] limit the cache part fail warnings to after wear is done it needs a bit of time to load other parts before determining the part reference is actually wrong --- lua/pac3/core/client/parts/event.lua | 2 +- lua/pac3/core/client/parts/proxy.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index f23fec615..fbd5ff4e5 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -335,7 +335,7 @@ function PART:GetOrFindCachedPart(uid_or_name) self.erroring_cached_parts[uid_or_name] = true self.bad_uid_search = self.bad_uid_search or 0 self.bad_uid_search = self.bad_uid_search + 1 - if self:GetPlayerOwner() == LocalPlayer() then + if self:GetPlayerOwner() == LocalPlayer() and not pace.still_loading_wearing then pace.FlashNotification("performance warning! " .. tostring(self) .. " keeps searching for parts not finding anything! " .. tostring(uid_or_name) .. " may be unused!") end else diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 3345460ea..e1cb18255 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -122,7 +122,7 @@ function PART:GetOrFindCachedPart(uid_or_name) self.erroring_cached_parts[uid_or_name] = true self.bad_uid_search = self.bad_uid_search or 0 self.bad_uid_search = self.bad_uid_search + 1 - if self:GetPlayerOwner() == LocalPlayer() then + if self:GetPlayerOwner() == LocalPlayer() and not pace.still_loading_wearing then pace.FlashNotification("performance warning! " .. tostring(self) .. " keeps searching for parts not finding anything! " .. tostring(uid_or_name) .. " may be unused!") end else From c069de073b8126a75c8e77e05536e32d6691eb74 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 29 Aug 2025 00:21:02 -0400 Subject: [PATCH 31/35] remove DoT instances on death --- lua/pac3/extra/shared/net_combat.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 663e7e0f3..79fff09d8 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -145,6 +145,7 @@ if SERVER then local calcview_consents = {} local active_force_ids = {} local active_grabbed_ents = {} + local active_dots = {} local friendly_NPC_preferences = {} @@ -862,6 +863,19 @@ if SERVER then end end) + gameevent.Listen("entity_killed") + hook.Add( "entity_killed", "entity_killed_example", function( data ) + local victim_index = data.entindex_killed // Same as Victim:EntIndex() / the entity / player victim + local ent = Entity(victim_index) + if ent:IsValid() then + if active_dots[ent] then + for timer_entid,_ in pairs(active_dots[ent]) do + timer.Remove(timer_entid) + end + end + end + end) + local function MergeTargetsByID(tbl1, tbl2) for i,v in ipairs(tbl2) do tbl1[v:EntIndex()] = v @@ -1210,18 +1224,22 @@ if SERVER then ply_prog_count = ply_prog_count + 1 else if tbl.DOTMode then + active_dots[ent] = active_dots[ent] or {} local counts = tbl.NoInitialDOT and tbl.DOTCount or tbl.DOTCount-1 local timer_entid = tbl.UniqueID .. "_" .. ent:GetClass() .. "_" .. ent:EntIndex() if counts <= 0 then --nuh uh, timer 0 means infinite repeat timer.Remove(timer_entid) + active_dots[ent][timer_entid] = nil else if timer.Exists(timer_entid) then timer.Adjust(tbl.UniqueID, tbl.DOTTime, counts) + active_dots[ent][timer_entid] = tbl else timer.Create(timer_entid, tbl.DOTTime, counts, function() if not IsValid(ent) then timer.Remove(timer_entid) return end DoDamage(ent) end) + active_dots[ent][timer_entid] = tbl end end From 91091f45a7e475d431ba9857bb91e048dbd691ea Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 31 Aug 2025 14:31:55 -0400 Subject: [PATCH 32/35] auto-link special tracked parts on creation mainly to fix temporary damagezones (from projectiles) which we want to use with damage_zone_hit events for example the caching method, while better than looking through local parts, had its flaws --- lua/pac3/core/client/part_pool.lua | 17 +++++++++++++++++ lua/pac3/core/client/parts/damage_zone.lua | 3 +++ lua/pac3/core/client/parts/lock.lua | 2 ++ 3 files changed, 22 insertions(+) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index c2333916e..cc9863fc5 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -645,6 +645,7 @@ function pac.EnablePartsByClass(classname, enable) end end +local known_special_link_parts = {} function pac.LinkSpecialTrackedPartsForEvent(part, ply) part.erroring_cached_parts = {} part.found_cached_parts = {} @@ -654,11 +655,27 @@ function pac.LinkSpecialTrackedPartsForEvent(part, ply) ["damage_zone"] = true, ["lock"] = true } + known_special_link_parts[ply] = known_special_link_parts[ply] or {} + known_special_link_parts[ply][part] = part.specialtrackedparts for _,part2 in pairs(all_parts) do if ply == part2:GetPlayerOwner() and tracked_classes[part2.ClassName] then table.insert(part.specialtrackedparts,part2) end end + known_special_link_parts[ply][part] = part.specialtrackedparts +end +function pac.InsertSpecialTrackedPart(ply, append_part, remove) + if append_part then + if known_special_link_parts[ply] then + for part,tbl in pairs(known_special_link_parts[ply]) do + if remove then table.RemoveByValue(part.specialtrackedparts, append_part) continue end + if part:IsValid() then + table.insert(part.specialtrackedparts, append_part) + end + end + end + return + end end --a centralized function to cache a part in a prebuilt list so we can access relevant parts already narrowed down instead of searching through all parts / localparts diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index a12bf1652..7df8de30d 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -998,6 +998,8 @@ function PART:OnRemove() pac.RemoveHook(v, "pace_draw_hitbox") end self:ClearHitMarkers() + --remove itself + pac.InsertSpecialTrackedPart(self:GetPlayerOwner(), self, true) end local previousRenderingHook @@ -1295,6 +1297,7 @@ function PART:Initialize() end end) + pac.InsertSpecialTrackedPart(self:GetPlayerOwner(), self) end function PART:SetRadius(val) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 2d428b208..f381f7bb5 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -478,6 +478,7 @@ function PART:reset_ent_ang() end function PART:OnRemove() + pac.InsertSpecialTrackedPart(self:GetPlayerOwner(), self, true) end function PART:DecideTarget() @@ -595,6 +596,7 @@ function PART:Initialize() end end if not convar_lock:GetBool() then self:SetError("lock parts are disabled on this server!") end + pac.InsertSpecialTrackedPart(self:GetPlayerOwner(), self) end From 30d9909300b3445096a13739a096c9592bb3e34d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 6 Sep 2025 18:27:47 -0400 Subject: [PATCH 33/35] fix damagezone initialization in hitparts this should make the counter-workaround take effect under normal circumstances of non-attached markers --- lua/pac3/core/client/parts/damage_zone.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 7df8de30d..eab19fc12 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -821,6 +821,7 @@ net.Receive("pac_hit_results", function(len) local cs_ent = false if not self.AttachPartsToTargetEntity then ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") + ent.is_pac_hitmarker = true cs_ent = true ent:SetNoDraw(true) ent:SetPos(pos) From 941c2ca18ff27fdd514759bdfba9bfc40f747bd4 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 26 Sep 2025 00:12:11 -0400 Subject: [PATCH 34/35] mdl "vararg" content length theoretical fix I didn't reliably get the red texts or popups blocking the mdl load so I'm not 100% sure about replicability especially since it's been reported that it doesn't happen on all servers but I've had multiple reports and the theory is simple enough, so since I'm pretty sure this is where it happens I'm pretty sure this will fix the bug. --- lua/pac3/core/shared/http.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lua/pac3/core/shared/http.lua b/lua/pac3/core/shared/http.lua index fafc7db06..35d5ddd55 100644 --- a/lua/pac3/core/shared/http.lua +++ b/lua/pac3/core/shared/http.lua @@ -106,6 +106,16 @@ function pac.getContentLength(url, cb, failcb) if string.lower(key) == "content-length" then length = tonumber(value) + --we started to get stuff like "7182199,0" from Google Drive, triggering the failstate because tonumber doesn't like varargs + if not length then + if string.find(value, ",") then + local args = string.Split(value, ",") + if args[1] then + length = tonumber(args[1]) + end + end + end + if not length or math.floor(length) ~= length then return failcb(string.format("malformed server reply with header content-length (got %q, expected valid integer number)", value), true) end From 1a0ec8304c673799246948bc1fdee81cd97f84a6 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 26 Sep 2025 17:21:50 -0400 Subject: [PATCH 35/35] suppress blacklisted players from post-filter wears I couldn't get people to test yet but I blacklisted myself to check, I'll have to assume the wear filter code still works --- lua/pac3/editor/client/wear.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index 8a03a5d73..5b955af89 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -138,7 +138,8 @@ do -- from server pac.dprint("received outfit %q from %s with %i number of children to set on %s", part_data.self.Name or "", tostring(owner), table.Count(part_data.children), part_data.self.OwnerName or "") if pace.CallHook("WearPartFromServer", owner, part_data, data) == false then return end - + if pace.ShouldIgnorePlayer(owner) then pace.RemovePartFromServer(owner, "__ALL__", data) return end + local dupepart = pac.GetPartFromUniqueID(data.player_uid, part_data.self.UniqueID) if dupepart:IsValid() then