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/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 diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index f54933f4e..eab19fc12 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)) @@ -795,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) @@ -939,6 +966,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 +998,9 @@ function PART:OnRemove() for _,v in pairs(renderhooks) do pac.RemoveHook(v, "pace_draw_hitbox") end + self:ClearHitMarkers() + --remove itself + pac.InsertSpecialTrackedPart(self:GetPlayerOwner(), self, true) end local previousRenderingHook @@ -1151,6 +1189,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 +1285,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 @@ -1231,6 +1298,7 @@ function PART:Initialize() end end) + pac.InsertSpecialTrackedPart(self:GetPlayerOwner(), self) end function PART:SetRadius(val) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index d3faed68f..fbd5ff4e5 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() 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 self.found_cached_parts[uid_or_name] = part return part end @@ -947,6 +953,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", @@ -2786,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 @@ -2898,8 +3011,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:GetPlayerOwner() + if include_owner and owner:IsValid() then + find = find .. ";" .. owner:SteamID() end return self:StringOperator(pac.LocalPlayer:SteamID(), find) @@ -3496,7 +3610,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) 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 diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index eddbef0b6..e1cb18255 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() 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 self.found_cached_parts[uid_or_name] = part return part @@ -275,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) @@ -447,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 @@ -1596,6 +1610,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 @@ -1630,6 +1648,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 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) 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() 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 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 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..7c82952ad 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -250,6 +250,320 @@ local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight) return btn 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) select_value = select_value or function(val, key) return val end select_value_search = select_value_search or select_value @@ -721,10 +1035,18 @@ 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:SetTooltip("jump to...") + 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) @@ -735,6 +1057,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", var) + 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", var) + 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", var) + end + end end end @@ -1617,320 +1977,43 @@ 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) + + 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 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 @@ -1944,14 +2027,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() @@ -2010,6 +2094,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() @@ -2098,7 +2190,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 @@ -2155,7 +2247,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() @@ -2495,6 +2587,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 @@ -2851,6 +2963,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]) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 44564e8ae..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"} @@ -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") @@ -2690,9 +2724,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 +2744,7 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) else obj:SetMaterials(table.concat(submaterials, ";")) end - + end, submat_togglers):SetIcon("icon16/paintcan.png") end @@ -3770,7 +3803,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) @@ -4525,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/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 4ef455512..2f8578a26 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) @@ -672,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) 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() 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 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 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