From b84080de89adc6ecd4e291925ffad81b4eb2ad47 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 9 Apr 2023 22:32:14 -0400 Subject: [PATCH 001/300] update branch --- lua/pac3/core/client/parts/command.lua | 6 + lua/pac3/core/client/parts/damage_zone.lua | 389 +++++++++ lua/pac3/core/client/parts/event.lua | 73 +- lua/pac3/core/client/parts/hitscan.lua | 139 +++ .../client/parts/interpolated_multibone.lua | 167 ++++ lua/pac3/core/client/parts/legacy/entity.lua | 2 +- lua/pac3/core/client/parts/lock.lua | 233 +++++ lua/pac3/core/client/parts/particles.lua | 29 +- lua/pac3/core/client/parts/player_config.lua | 2 +- lua/pac3/core/client/parts/projectile.lua | 18 +- lua/pac3/core/client/parts/proxy.lua | 116 ++- lua/pac3/core/client/parts/text.lua | 84 +- lua/pac3/core/server/init.lua | 1 + lua/pac3/core/shared/util.lua | 22 +- lua/pac3/editor/client/examples.lua | 805 +++++------------- lua/pac3/editor/client/panels/editor.lua | 2 +- .../editor/client/panels/extra_properties.lua | 29 +- lua/pac3/editor/client/parts.lua | 500 ++++++++++- lua/pac3/editor/client/settings.lua | 9 +- lua/pac3/editor/client/shortcuts.lua | 172 ++++ lua/pac3/editor/client/wear.lua | 8 +- lua/pac3/editor/server/wear.lua | 2 - lua/pac3/extra/shared/init.lua | 1 + lua/pac3/extra/shared/net_combat.lua | 368 ++++++++ lua/pac3/extra/shared/projectiles.lua | 56 +- 25 files changed, 2520 insertions(+), 713 deletions(-) create mode 100644 lua/pac3/core/client/parts/damage_zone.lua create mode 100644 lua/pac3/core/client/parts/hitscan.lua create mode 100644 lua/pac3/core/client/parts/interpolated_multibone.lua create mode 100644 lua/pac3/core/client/parts/lock.lua create mode 100644 lua/pac3/extra/shared/net_combat.lua diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index f62eff8dd..83f60c02c 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -51,6 +51,12 @@ function PART:ShouldHighlight(str) return _G[str] ~= nil end +function PART:GetNiceName() + if self.UseLua then return ("lua: " .. self.String) end + return "command: " .. self.String + +end + local sv_allowcslua = GetConVar("sv_allowcslua") function PART:Execute() diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua new file mode 100644 index 000000000..83a6a69d0 --- /dev/null +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -0,0 +1,389 @@ +local BUILDER, PART = pac.PartTemplate("base_movable") + +PART.ClassName = "damage_zone" +PART.Group = 'advanced' +PART.Icon = 'icon16/package.png' + +local renderhooks = { + "PostDraw2DSkyBox", + "PostDrawOpaqueRenderables", + "PostDrawSkyBox", + "PostDrawTranslucentRenderables", + "PostDrawViewModel", + "PostPlayerDraw", + "PreDrawEffects", + "PreDrawHalos", + "PreDrawOpaqueRenderables", + "PreDrawSkyBox", + "PreDrawTranslucentRenderables", + "PreDrawViewModel" +} + + +BUILDER:StartStorableVars() + :SetPropertyGroup("Targets") + :GetSet("AffectSelf",false) + :GetSet("Players",true) + :GetSet("NPC",true) + :SetPropertyGroup("Shape and Sampling") + :GetSet("Radius", 20) + :GetSet("Length", 50) + :GetSet("HitboxMode", "Box", {enums = { + ["Box"] = "Box", + ["Sphere"] = "Sphere", + ["Cylinder (Raycasts Only)"] = "Cylinder", + ["Cylinder (Hybrid)"] = "CylinderHybrid", + ["Cylinder (From Spheres)"] = "CylinderSpheres", + ["Cone (Raycasts Only)"] = "Cone", + ["Cone (Hybrid)"] = "ConeHybrid", + ["Cone (From Spheres)"] = "ConeSpheres", + ["Ray"] = "Ray" + }}) + :GetSet("Detail", 20) + :GetSet("ExtraSteps",0) + :GetSet("RadialRandomize", 1) + :GetSet("PhaseRandomize", 1) + :SetPropertyGroup("Behaviour") + :GetSet("Delay", 0) + :GetSet("OverrideKnockback", true) + :GetSet("KnockbackAmount", Vector(0,0,0)) + :SetPropertyGroup("Preview Rendering") + :GetSet("NoPreview", false) + :GetSet("RenderingHook", "PostDrawOpaqueRenderables", {enums = { + ["PostDraw2DSkyBox"] = "PostDraw2DSkyBox", + ["PostDrawOpaqueRenderables"] = "PostDrawOpaqueRenderables", + ["PostDrawSkyBox"] = "PostDrawSkyBox", + ["PostDrawTranslucentRenderables"] = "PostDrawTranslucentRenderables", + ["PostDrawViewModel"] = "PostDrawViewModel", + ["PostPlayerDraw"] = "PostPlayerDraw", + ["PreDrawEffects"] = "PreDrawEffects", + ["PreDrawHalos"] = "PreDrawHalos", + ["PreDrawOpaqueRenderables"] = "PreDrawOpaqueRenderables", + ["PreDrawSkyBox"] = "PreDrawSkyBox", + ["PreDrawTranslucentRenderables"] = "PreDrawTranslucentRenderables", + ["PreDrawViewModel"] = "PreDrawViewModel" + }}) + :SetPropertyGroup("DamageInfo") + :GetSet("Bullet", true) + :GetSet("Damage", 0) + :GetSet("DamageType", "generic", {enums = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamage + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + }}) +BUILDER:EndStorableVars() + +function PART:OnShow() + if not self.NoPreview then + self:PreviewHitbox() + end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + local tbl = {} + for key in pairs(self:GetStorableVars()) do + tbl[key] = self[key] + end + net.Start("pac_request_zone_damage") + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + net.WriteTable(tbl) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() +end + +function PART:OnHide() + --self:BuildCylinder() + hook.Remove(self.RenderingHook, "pace_draw_hitbox") + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox") + end +end + +function PART:OnRemove() + --self:BuildCylinder() + hook.Remove(self.RenderingHook, "pace_draw_hitbox") + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox") + end +end + +local previousRenderingHook + +function PART:PreviewHitbox() + if previousRenderingHook ~= self.RenderingHook then + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox") + end + previousRenderingHook = self.RenderingHook + end + + hook.Add(self.RenderingHook, "pace_draw_hitbox", function() + self:GetWorldPosition() + if self.HitboxMode == "Box" then + local mat = Matrix() + mat:Rotate(self:GetWorldAngles()) + --mat:Rotate(Angle(SysTime()*100,0,0)) + local mins = Vector(-self.Radius, -self.Radius, -self.Radius) + local maxs = Vector(self.Radius, self.Radius, self.Radius) + render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0) --[[mat:GetAngles()]], mins, maxs, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Sphere" then + render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Cylinder" or self.HitboxMode == "CylinderHybrid" then + local obj = Mesh() + self:BuildCylinder(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if LocalPlayer() == self:GetPlayerOwner() then + if self.Radius ~= 0 then + local sides = self.Detail + if self.Detail < 1 then sides = 1 end + + local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + end + steps = math.max(steps + math.abs(self.ExtraSteps),1) + + --print("steps",steps, "total casts will be "..steps*self.Detail) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + y = self:GetWorldAngles():Up() *math.sin(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + local startpos = self:GetWorldPosition() + x + y + local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y + render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false ) + end + end + if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then + --fast sphere check on the wide end + if self.Length/self.Radius >= 2 then + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - self.Radius), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Radius), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if self.Radius ~= 0 then + local counter = 0 + for i=math.floor(self.Length / self.Radius) - 1,1,-1 do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Radius*i), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if counter == 100 then break end + counter = counter + 1 + end + end + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + end + elseif self.Radius == 0 then render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) end + end + elseif self.HitboxMode == "CylinderSpheres" then + local obj = Mesh() + self:BuildCylinder(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Length ~= 0 and self.Radius ~= 0 then + local counter = 0 + --render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + for i=0,1,1/(math.abs(self.Length/self.Radius)) do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if counter == 200 then break end + counter = counter + 1 + end + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Cone" or self.HitboxMode == "ConeHybrid" then + local obj = Mesh() + self:BuildCone(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if LocalPlayer() == self:GetPlayerOwner() then + if self.Radius ~= 0 then + local sides = self.Detail + if self.Detail < 1 then sides = 1 end + local startpos = self:GetWorldPosition() + local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + end + steps = math.max(steps + math.abs(self.ExtraSteps),1) + + --print("steps",steps, "total casts will be "..steps*self.Detail) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + y = self:GetWorldAngles():Up() *math.sin(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y + render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false ) + end + end + if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then + --fast sphere check on the wide end + local radius_multiplier = math.atan(math.abs(self.Length/self.Radius)) / (1.5 + 0.1*math.sqrt(self.Length/self.Radius)) + if self.Length/self.Radius > 0.5 then + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - self.Radius * radius_multiplier), self.Radius * radius_multiplier, 10, 10, Color( 255, 255, 255 ) ) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + end + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + end + elseif self.HitboxMode == "ConeSpheres" then + local obj = Mesh() + self:BuildCone(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Radius ~= 0 then + local steps + steps = math.Clamp(4*math.ceil(self.Length / (self.Radius or 1)),1,50) + for i = 1,0,-1/steps do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + + steps = math.Clamp(math.ceil(self.Length / (self.Radius or 1)),1,4) + for i = 0,1/8,1/128 do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Ray" then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + end) + timer.Simple(5, function() if self.NoPreview then hook.Remove(self.RenderingHook, "pace_draw_hitbox") end end) +end + +function PART:BuildCylinder(obj) + local sides = 30 + local circle_tris = {} + for i=1,sides,1 do + local vert1 = {pos = Vector(0, self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(0, self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + local vert3 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert4 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + --print(vert1.pos,vert3.pos,vert2.pos,vert4.pos) + --{vert1,vert2,vert3} + --{vert4,vert3,vert2} + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + + table.insert(circle_tris, vert4) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert4) + + --circle_tris[8*(i-1) + 1] = vert1 + --circle_tris[8*(i-1) + 2] = vert2 + --circle_tris[8*(i-1) + 3] = vert3 + --circle_tris[8*(i-1) + 4] = vert4 + --circle_tris[8*(i-1) + 5] = vert3 + --circle_tris[8*(i-1) + 6] = vert2 + end + obj:BuildFromTriangles( circle_tris ) +end + +function PART:BuildCone(obj) + local sides = 30 + local circle_tris = {} + local verttip = {pos = Vector(0,0,0), u = 0, v = 0 } + for i=1,sides,1 do + local vert1 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + --print(vert1.pos,vert3.pos,vert2.pos,vert4.pos) + --{vert1,vert2,vert3} + --{vert4,vert3,vert2} + table.insert(circle_tris, verttip) + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + table.insert(circle_tris, verttip) + + --circle_tris[8*(i-1) + 1] = vert1 + --circle_tris[8*(i-1) + 2] = vert2 + --circle_tris[8*(i-1) + 3] = vert3 + --circle_tris[8*(i-1) + 4] = vert4 + --circle_tris[8*(i-1) + 5] = vert3 + --circle_tris[8*(i-1) + 6] = vert2 + end + obj:BuildFromTriangles( circle_tris ) +end + + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index ae785353d..4ba8f4f1c 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -26,7 +26,7 @@ BUILDER:StartStorableVars() return output end}) BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end}) - BUILDER:GetSet("Arguments", "", {hidden = true}) + BUILDER:GetSet("Arguments", "", {hidden = false}) BUILDER:GetSet("Invert", true) BUILDER:GetSet("RootOwner", true) BUILDER:GetSet("AffectChildrenOnly", false) @@ -35,12 +35,12 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:SetEvent(event) - local reset = (self.Arguments == "") or + local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) self.Event = event self:SetWarning() - self:GetDynamicProperties(reset) + self:GetDynamicProperties(reset) end local function get_default(typ) @@ -95,8 +95,8 @@ function PART:GetDynamicProperties(reset_to_default) } local arg = tbl[key] - if arg.get() == nil or reset_to_default then - if udata.default then + if arg.get() == nil or reset_to_default then + if udata.default then arg.set(udata.default) else arg.set(nil) @@ -204,6 +204,10 @@ PART.OldEvents = { timerx = { arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}}, + userdata = { + {default = 0, timerx_property = "seconds"}, + {default = true, timerx_property = "reset_on_hide"} + }, nice = function(self, ent, seconds) return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?" end, @@ -490,8 +494,16 @@ PART.OldEvents = { ranger = { arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}}, - userdata = {{editor_panel = "ranger", ranger_property = "distance"}, {editor_panel = "ranger", ranger_property = "compare"}}, + userdata = { + {default = 15, ranger_property = "distance"}, + {default = 5, ranger_property = "compare"}, + {default = false, ranger_property = "npcs_and_players_only"} + }, callback = function(self, ent, distance, compare, npcs_and_players_only) + if npcs_and_players_only == nil then + self.Arguments = self.Arguments .. "@@0" + self.npcs_and_players_only = false + end local parent = self:GetParentEx() if parent:IsValid() and parent.GetWorldPosition then @@ -572,12 +584,45 @@ PART.OldEvents = { endpos = startpos, maxs = maxs, mins = mins, - filter = ent + filter = {self:GetRootPart():GetOwner(),ent} + } ) + return tr.Hit + end, + }, + + is_touching_scalable = { + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}}, + userdata = {{editor_panel = "is_touching"}, {x = "x_stretch"}, {y = "y_stretch"}, {z = "z_stretch"}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch) + extra_radius = extra_radius or 0 + x_stretch = x_stretch or 1 + y_stretch = y_stretch or 1 + z_stretch = z_stretch or 1 + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + local startpos = ent:WorldSpaceCenter() + + radius = math.max(extra_radius, 1) + mins = mins * radius + maxs = maxs * radius + + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {self:GetRootPart():GetOwner(),ent} } ) return tr.Hit end, }, + is_explicit = { + callback = function(self, ent) + return GetConVar("pac_hide_disturbing"):GetBool() + end + }, + is_in_noclip = { callback = function(self, ent) ent = try_viewmodel(ent) @@ -824,8 +869,8 @@ PART.OldEvents = { command = { arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}}, userdata = { - {default = "change_me", editor_friendly = "CommandName"}, - {default = 0.1, editor_friendly = "EventDuration"}, + {default = "change_me", editor_friendly = "CommandName"}, + {default = 0.1, editor_friendly = "EventDuration"}, {default = false, group = "event wheel", editor_friendly = "HideInEventWheel"} }, nice = function(self, ent, find, time) @@ -834,6 +879,14 @@ PART.OldEvents = { return "command: " .. find .. " | " .. "duration: " .. time end, callback = function(self, ent, find, time) + if time == nil then + self.Arguments = self.Arguments .. "@@0" + time = 0 + end + if hide_in_eventwheel == nil then + self.Arguments = self.Arguments .. "@@0" + hide_in_eventwheel = false + end time = time or 0.1 local ply = self:GetPlayerOwner() @@ -2207,4 +2260,4 @@ do concommand.Add("+pac_events", pac.openEventSelectionWheel) concommand.Add("-pac_events", pac.closeEventSelectionWheel) -end +end \ No newline at end of file diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua new file mode 100644 index 000000000..c201509b8 --- /dev/null +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -0,0 +1,139 @@ +language.Add("pac_hitscan", "Hitscan") +local ent +local bulletinfo = {} +--local vector_origin = vector_origin +--local angle_origin = Angle(0,0,0) +--local WorldToLocal = WorldToLocal + +local BUILDER, PART = pac.PartTemplate("base_drawable") + +PART.ClassName = "hitscan" +PART.Group = 'advanced' +PART.Icon = 'icon16/user_gray.png' + +BUILDER:StartStorableVars() + :GetSet("ServerBullets",true) + :SetPropertyGroup("bullet properties") + :GetSet("BulletImpact", false) + :GetSet("Damage", 0) + :GetSet("Force",1000) + :GetSet("DamageType", "generic", {enums = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamage + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + } + }) + :GetSet("Spread", 0) + :GetSet("SpreadX", 1) + :GetSet("SpreadY", 1) + :GetSet("NumberBullets", 1) + :GetSet("TracerSparseness", 1) + :GetSet("MaxDistance", 10000) + :GetSet("TracerName", "Tracer", {enums = { + ["Default bullet tracer"] = "Tracer", + ["AR2 pulse-rifle tracer"] = "AR2Tracer", + ["Helicopter tracer"] = "HelicopterTracer", + ["Airboat gun tracer"] = "AirboatGunTracer", + ["Airboat gun heavy tracer"] = "AirboatGunHeavyTracer", + ["Gauss tracer"] = "GaussTracer", + ["Hunter tracer"] = "HunterTracer", + ["Strider tracer"] = "StriderTracer", + ["Gunship tracer"] = "GunshipTracer", + ["Toolgun tracer"] = "ToolTracer", + ["Laser tracer"] = "LaserTracer" + }}) +BUILDER:EndStorableVars() + +function PART:OnShow() + self:UpdateBulletInfo() + self:GetWorldPosition() + self:GetWorldAngles() + self:Shoot(self:GetDrawPosition()) +end + +function PART:OnDraw() + self:GetWorldPosition() + self:GetWorldAngles() +end + +function PART:Shoot(pos, ang) + bulletinfo.Tracer = self.TracerSparseness + bulletinfo.Src = pos + bulletinfo.Dir = ang:Forward() + bulletinfo.Spread = Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0) + + ent = self:GetOwner() + if self.ServerBullets then + print("WE NEED A BULLET IN THE SERVER!") + net.Start("pac_hitscan") + net.WriteEntity(ent) + net.WriteTable(bulletinfo) + net.SendToServer() + else + ent:FireBullets(bulletinfo) + end +end + +function PART:UpdateBulletInfo() + bulletinfo.Attacker = ent + if self.Damage == 0 then + else bulletinfo.Damage = self.Damage end + + bulletinfo.Force = self.Force + bulletinfo.Distance = self.MaxDistance + bulletinfo.Num = self.NumberBullets + bulletinfo.Tracer = self.TracerSparseness --tracer every x bullets + bulletinfo.TracerName = self.TracerName + + --[[bulletinfo.ammodata = { + name = "" + dmgtype = self.DamageType + tracer = TRACER_LINE_AND_WHIZ + maxcarry = -2 + + }]]-- +end + + +BUILDER:Register() + diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua new file mode 100644 index 000000000..29849056c --- /dev/null +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -0,0 +1,167 @@ +nodes = {} + +local cam_PushModelMatrix = cam.PushModelMatrix +local cam_PopModelMatrix = cam.PopModelMatrix +local Vector = Vector +local Angle = Angle +local EF_BONEMERGE = EF_BONEMERGE +local NULL = NULL +local Color = Color +local Matrix = Matrix +local vector_origin = vector_origin + +local BUILDER, PART = pac.PartTemplate("base_movable") + +PART.ClassName = "interpolated_multibone" +PART.Group = 'advanced' +PART.Icon = 'icon16/table_multiple.png' +PART.is_model_part = false + +PART.ManualDraw = true +PART.HandleModifiersManually = true + +BUILDER:StartStorableVars() + :SetPropertyGroup("test") + :GetSet("Test1", false) + :GetSet("Test2", false) + :GetSet("Force000", false) + :SetPropertyGroup("Interpolation") + :GetSet("LerpValue",0) + :GetSet("InterpolatePosition", true) + :GetSet("InterpolateAngles", true) + :SetPropertyGroup("Nodes") + :GetSetPart("Node1") + :GetSetPart("Node2") + :GetSetPart("Node3") + :GetSetPart("Node4") + :GetSetPart("Node5") +:EndStorableVars() + +--PART:GetWorldPosition() +--PART:GetWorldAngles() +function PART:Initialize() + print("a multiboner is born") + + self.pos = Vector() + self.vel = Vector() + + self.ang = Angle() + self.angvel = Angle() + --[[] + self:SetLerpValue(self.LerpValue or 0) + self:SetInterpolatePosition(self.InterpolatePosition or true) + self:SetInterpolateAngles(self.InterpolateAngles or true)]] +end + +function PART:OnDraw() + self:GetWorldPosition() + self:GetWorldAngles() + --self:ModifiersPreEvent("OnDraw") + --self:ModifiersPostEvent("OnDraw") +end + +function PART:OnThink() + self:OnDraw() + + if self.Force000 then + print("forcing 0 0 0 world position") + self:SetWorldPos(0,0,0) + end + --self:GetWorldPosition() + --self:GetWorldAngles() + --self:GetDrawPosition() + --print(self.pos.x, self.pos.y, self.pos.z) + --print(self:GetDrawPosition().x, self:GetDrawPosition().y, self:GetDrawPosition().z) +end + +function PART:SetWorldPos(x,y,z) + self.pos.x = x + self.pos.y = y + self.pos.z = z +end + +function PART:Interpolate(stage, proportion) +end + +--we need to know the stage and proportion (progress) +--e.g. lerp 0.5 is stage 0, proportion 0.5 because it's 50% toward Node 1 +--e.g. lerp 2.2 is stage 2, proportion 0.2 because it's 20% toward Node 3 +function PART:GetInterpolationParameters() + --[[stage = math.max(0,math.floor(self.LerpValue)) + proportion = math.max(0,self.LerpValue) % 1 + print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1)) + print("proportion is " .. proportion) + return stage, proportion]]-- +end + +function PART:GetNodeAngle(nodenumber) + --print("node" .. nodenumber .. " angle " .. self.__['Node'..nodenumber].Angles) + --print("node" .. nodenumber .. " world angle " .. self.__['Node'..nodenumber]:GetWorldAngles()) + + --return self.Node1:GetWorldAngles() +end + +function PART:GetNodePosition(nodenumber) + --print("node" .. nodenumber .. " position " .. self.__['Node'..nodenumber].Position) + --print("node" .. nodenumber .. " world position " .. self.__['Node'..nodenumber]:GetWorldPosition()) + --return self.Node1:GetWorldPosition() +end + +function PART:InterpolateFromLerp(lerp) +end + +function PART:InterpolateFromNodes(firstnode, secondnode, proportion) + --position_interpolated = InterpolateFromStage("position", stage, self.Lerp) +end + +function PART:InterpolateFromStage(stage, proportion) + self:InterpolateFromNodes(stage, stage + 1) +end + +function PART:InterpolateAngle() + +end + + + +--[[function PART:ApplyMatrix() + print("MATRIX???") + local ent = self:GetOwner() + if not ent:IsValid() then return end + + local mat = Matrix() + + if self.ClassName ~= "model2" then + mat:Translate(self.Position + self.PositionOffset) + mat:Rotate(self.Angles + self.AngleOffset) + end + + if mat:IsIdentity() then + ent:DisableMatrix("RenderMultiply") + else + ent:EnableMatrix("RenderMultiply", mat) + end +end--]] + +function PART:SetLerpValue(var) + --[[print("adjusted lerp value. "..type(var).." "..var) + self.LerpValue = var + assert(self.LerpValue == var) + assert(self.LerpValue ~= nil) + self:Interpolate(self:GetInterpolationParameters())]]-- +end + +function PART:SetInterpolatePosition(b) + --print(type(b).." "..b) + self.InterpolatePosition = b +end + +function PART:SetInterpolateAngles(b) + --print(type(b).." "..b) + self.InterpolateAngles = b +end + + + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/legacy/entity.lua b/lua/pac3/core/client/parts/legacy/entity.lua index 8c3166643..3490e1302 100644 --- a/lua/pac3/core/client/parts/legacy/entity.lua +++ b/lua/pac3/core/client/parts/legacy/entity.lua @@ -89,7 +89,7 @@ function BUILDER:EntityField(name, field) end BUILDER:EntityField("InverseKinematics", "enable_ik") -BUILDER:EntityField("MuteFootsteps", "hide_weapon") +BUILDER:EntityField("MuteFootsteps", "pac_mute_footsteps") BUILDER:EntityField("AnimationRate", "global_animation_rate") BUILDER:EntityField("RunSpeed", "run_speed") diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua new file mode 100644 index 000000000..b0949f2ec --- /dev/null +++ b/lua/pac3/core/client/parts/lock.lua @@ -0,0 +1,233 @@ +include("pac3/extra/shared/net_combat.lua") +--pac3/extra/shared/net_combat.lua + + + +local target_ent = nil +local pac = pac +local Vector = Vector +local Angle = Angle +local NULL = NULL +local Matrix = Matrix + +local BUILDER, PART = pac.PartTemplate("base_movable") + +PART.ClassName = "lock" +PART.Group = 'advanced' +PART.Icon = 'icon16/lock.png' + + +BUILDER:StartStorableVars() + :SetPropertyGroup("Conditions") + :GetSet("Players", false) + :GetSet("PhysicsProps", false) + :GetSet("NPC", false) + :SetPropertyGroup("DetectionOrigin") + :GetSet("Radius", 20) + :GetSet("RadiusOffsetDown", false, {description = "Lowers the detect origin by the radius distance"}) + :GetSetPart("TargetPart") + :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."}) + :SetPropertyGroup("Behaviour") + :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"}) + :GetSet("RelativeGrab", false) + :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before grabbing are re-applied"}) + :GetSet("Mode", "None", {enums = {["None"] = "None", ["Grab"] = "Grab", ["Teleport"] = "Teleport"}}) +BUILDER:EndStorableVars() + +local valid_ent = false +local grabbing = false +local last_request_time = SysTime() +local last_entsearch = SysTime() +local default_ang = Angle(0,0,0) + +function PART:OnThink() + + self:GetWorldPosition() + self:GetWorldAngles() + if self.Mode == "Grab" then + if not valid_ent and self.ContinuousSearch then --no hit and can search = search more and try the move later + self:DecideTarget() + self:CheckEntValidity() + return + elseif not valid_ent and not self.ContinuousSearch then --if initial think failed to find and can't search = stop + --print("end of the line. ", not valid_ent, not self.ContinuousSearch, not valid_ent and not self.ContinuousSearch) + return + end + end + --good hit and can search = search more and try the move later + self:CheckEntValidity() + + --self:DecideTarget() + if self.Mode == "Grab" then + if not grabbing and not self.OverrideAngles then default_ang = self.target_ent:GetAngles() end + + relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity() + if not self.RelativeGrab then + relative_transform_matrix = Matrix() + relative_transform_matrix:Identity() + end + + offset_matrix = Matrix() + offset_matrix:Translate(self:GetWorldPosition()) + offset_matrix:Rotate(self:GetWorldAngles()) + offset_matrix:Mul(relative_transform_matrix) + + local relative_offset_pos = offset_matrix:GetTranslation() + local relative_offset_ang = offset_matrix:GetAngles() + + net.Start("pac_request_position_override_on_entity") + net.WriteVector(relative_offset_pos) + net.WriteAngle(relative_offset_ang) + local can_rotate = self.OverrideAngles + if self.target_ent:IsPlayer() then can_rotate = false end + net.WriteBool(can_rotate) + net.WriteEntity(self.target_ent) + net.WriteEntity(self:GetRootPart():GetOwner()) + --print(self:GetRootPart():GetOwner()) + net.SendToServer() + if self.Players and self.target_ent:IsPlayer() then + local mat = Matrix() + mat:Identity() + mat:Rotate(Angle(0,270,0)) + mat:Rotate(self:GetWorldAngles()) + self.target_ent:EnableMatrix("RenderMultiply", mat) + end + last_request_time = SysTime() + grabbing = true + teleported = false + elseif self.Mode == "Teleport" and not teleported then + + self.target_ent = nil + net.Start("pac_request_position_override_on_entity") + net.WriteVector(self:GetWorldPosition()) + local ang_yaw_only = self:GetWorldAngles() + ang_yaw_only.p = 0 + ang_yaw_only.r = 0 + net.WriteAngle(ang_yaw_only) + net.WriteBool(self.OverrideAngles) + net.WriteEntity(self:GetPlayerOwner()) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() + self:GetPlayerOwner():SetAngles( ang_yaw_only ) + + teleported = true + grabbing = false + end +end + +--continuous : keep trying until hit +--not : try only once + + +function PART:OnShow() + self.target_ent = nil + self.relative_transform_matrix = Matrix():Identity() + self:DecideTarget() + self:CheckEntValidity() + self:CalculateRelativeOffset() +end + +function PART:OnHide() + teleported = false + grabbing = false + if self.target_ent == nil then return end + timer.Simple(math.min(self.RestoreDelay,5), function() + if self.target_ent == nil then return end + if self.target_ent:IsValid() then + net.Start("pac_request_angle_reset_on_entity") + net.WriteAngle(default_ang) + net.WriteFloat(self.RestoreDelay) + net.WriteEntity(self.target_ent) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() + if self.Players then + self.target_ent:DisableMatrix("RenderMultiply") + end + end + end) +end + +function PART:OnRemove() +end + +function PART:DecideTarget() + --print("search") + local ents = ents.GetAll() + local ents_candidates = {} + local chosen_ent = nil + --filter entities + for i, ent_candidate in ipairs(ents) do + --print(ent_candidate:GetClass()) + if ent_candidate:IsValid() then + local origin + + if self.TargetPart and self.TargetPart:IsValid() then + origin = self.TargetPart:GetWorldPosition() + else + origin = self:GetWorldPosition() + end + + if self.RadiusOffsetDown then origin:Add(Vector(0,0,-self.Radius)) end + + if ent_candidate:GetPos():Distance( origin ) < self.Radius then + if self.Players and ent_candidate:IsPlayer() then + --we don't want to grab ourselves + if (self:GetPlayerOwner() ~= self:GetRootPart():GetOwner()) then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + elseif (self:GetPlayerOwner() ~= ent_candidate) then --if it's another player, good + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + end + elseif self.PhysicsProps and ent_candidate:GetClass() == "prop_physics" then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + elseif self.NPC and ent_candidate:IsNPC() then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + end + end + end + end + local closest_distance = math.huge + + --sort for the closest + for i,ent_candidate in ipairs(ents_candidates) do + --print("trying", ent_candidate, ent_candidate:GetClass(), (ent_candidate:GetPos()):Distance( self:GetWorldPosition()), " from part") + local test_distance = (ent_candidate:GetPos()):Distance( self:GetWorldPosition()) + if (test_distance < closest_distance) then + closest_distance = test_distance + chosen_ent = ent_candidate + end + end + + if chosen_ent ~= nil then + self.target_ent = chosen_ent + print("selected ", chosen_ent, "dist ", (chosen_ent:GetPos()):Distance( self:GetWorldPosition() )) + valid_ent = true + else + self.target_ent = nil + valid_ent = false + end +end + +function PART:CheckEntValidity() + + if self.target_ent == nil then + valid_ent = false + elseif self.target_ent:EntIndex() == 0 then + valid_ent = false + elseif self.target_ent:IsValid() then + valid_ent = true + end +end + +function PART:CalculateRelativeOffset() + if self.target_ent == nil then self.relative_transform_matrix = Matrix() return end + self.relative_transform_matrix = Matrix() + self.relative_transform_matrix:Rotate(self.target_ent:GetAngles() - self:GetWorldAngles()) + self.relative_transform_matrix:Translate(self.target_ent:GetPos() - self:GetWorldPosition()) + print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles()) +end + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/particles.lua b/lua/pac3/core/client/parts/particles.lua index 38c166445..828e910c0 100644 --- a/lua/pac3/core/client/parts/particles.lua +++ b/lua/pac3/core/client/parts/particles.lua @@ -17,6 +17,7 @@ BUILDER:StartStorableVars() BUILDER:PropertyOrder("ParentName") BUILDER:GetSet("Follow", false) BUILDER:GetSet("Additive", false) + BUILDER:GetSet("FireOnce", false) BUILDER:GetSet("FireDelay", 0.2) BUILDER:GetSet("NumberParticles", 1) BUILDER:GetSet("PositionSpread", 0) @@ -36,6 +37,9 @@ BUILDER:StartStorableVars() BUILDER:GetSet("StickEndSize", 0) BUILDER:GetSet("StickStartAlpha", 255) BUILDER:GetSet("StickEndAlpha", 0) + BUILDER:SetPropertyGroup("attract") + BUILDER:GetSet("AttractPart") + BUILDER:GetSet("AttractForce", 0) BUILDER:SetPropertyGroup("appearance") BUILDER:GetSet("Material", "effects/slime1") BUILDER:GetSet("StartAlpha", 255) @@ -49,6 +53,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("DoubleSided", true) BUILDER:GetSet("DrawManual", false) BUILDER:SetPropertyGroup("rotation") + BUILDER:GetSet("ZeroAngle",true) BUILDER:GetSet("RandomRollSpeed", 0) BUILDER:GetSet("RollDelta", 0) BUILDER:GetSet("ParticleAngleVelocity", Vector(50, 50, 50)) @@ -141,6 +146,7 @@ function PART:Set3D(b) end function PART:OnShow(from_rendering) + self.CanKeepFiring = true if not from_rendering then self.NextShot = 0 local pos, ang = self:GetDrawPosition() @@ -206,10 +212,11 @@ function PART:SetMaterial(var) end function PART:EmitParticles(pos, ang, real_ang) + if not self.FireOnce then self.CanKeepFiring = true end local emt = self:GetEmitter() if not emt then return end - if self.NextShot < pac.RealTime then + if self.NextShot < pac.RealTime and self.CanKeepFiring then if self.Material == "" then return end if self.Velocity == 500.01 then return end @@ -222,7 +229,11 @@ function PART:EmitParticles(pos, ang, real_ang) end for _ = 1, self.NumberParticles do - + local mats = self.Material:Split(";") + if #mats > 1 then + self.Materialm = pac.Material(table.Random(mats), self) + self:CallRecursive("OnMaterialChanged") + end local vec = Vector() if self.Spread ~= 0 then @@ -306,14 +317,15 @@ function PART:EmitParticles(pos, ang, real_ang) particle:SetRoll(self.RandomRollSpeed * 36) end - if self.RollDelta ~= 0 then + if self.RollDelta ~= 0 then particle:SetRollDelta(self.RollDelta + roll) end - + particle:SetAirResistance(self.AirResistance) particle:SetBounce(self.Bounce) particle:SetGravity(self.Gravity) - particle:SetAngles(particle:GetAngles() + self.ParticleAngle) + if self.ZeroAngle then particle:SetAngles(Angle(0,0,0)) + else particle:SetAngles(particle:GetAngles() + self.ParticleAngle) end particle:SetLighting(self.Lighting) if not self.Follow then @@ -346,7 +358,12 @@ function PART:EmitParticles(pos, ang, real_ang) end end - self.NextShot = pac.RealTime + self.FireDelay + if self.FireDelay == 0 then + self.CanKeepFiring = false + else + self.NextShot = pac.RealTime + self.FireDelay + self.CanKeepFiring = true + end end end diff --git a/lua/pac3/core/client/parts/player_config.lua b/lua/pac3/core/client/parts/player_config.lua index ccfde805f..77468adc6 100644 --- a/lua/pac3/core/client/parts/player_config.lua +++ b/lua/pac3/core/client/parts/player_config.lua @@ -56,7 +56,7 @@ function BUILDER:EntityField(name, field) end BUILDER:EntityField("InverseKinematics", "enable_ik") -BUILDER:EntityField("MuteFootsteps", "hide_weapon") +BUILDER:EntityField("MuteFootsteps", "pac_mute_footsteps") BUILDER:EntityField("AnimationRate", "global_animation_rate") BUILDER:EntityField("FallApartOnDeath", "death_physics_parts") BUILDER:EntityField("DeathRagdollizeParent", "death_ragdollize") diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index b5b35b4d8..073b0c458 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -68,11 +68,12 @@ BUILDER:StartStorableVars() } }) BUILDER:GetSet("Spread", 0) + BUILDER:GetSet("NumberProjectiles", 1) BUILDER:GetSet("Delay", 0) BUILDER:GetSet("Maximum", 0) BUILDER:GetSet("Mass", 100) BUILDER:GetSet("Attract", 0) - BUILDER:GetSet("AttractMode", "projectile_nearest", {enums = { + BUILDER:GetSet("AttractMode", "closest_to_projectile", {enums = { hitpos = "hitpos", hitpos_radius = "hitpos_radius", closest_to_projectile = "closest_to_projectile", @@ -84,6 +85,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("CollideWithOwner", false) BUILDER:GetSet("CollideWithSelf", false) BUILDER:GetSet("RemoveOnCollide", false) + BUILDER:EndStorableVars() PART.Translucent = false @@ -103,7 +105,11 @@ function PART:OnShow(from_rendering) part:Draw("opaque") end end - self:Shoot(self:GetDrawPosition()) + if self.NumberProjectiles <= 0 then self.NumberProjectiles = 0 end + if self.NumberProjectiles <= 50 then + local pos,ang = self:GetDrawPosition() + self:Shoot(pos,ang,self.NumberProjectiles) + else chat.AddText(Color(255,0,0),"[PAC3] Trying to spawn too many projectiles! The limit is " .. 50) end end end @@ -147,8 +153,9 @@ end local enable = CreateClientConVar("pac_sv_projectiles", 0, true) -function PART:Shoot(pos, ang) +function PART:Shoot(pos, ang, multi_projectile_count) local physics = self.Physical + local multi_projectile_count = multi_projectile_count or 1 if physics then if pac.LocalPlayer ~= self:GetPlayerOwner() then return end @@ -159,6 +166,7 @@ function PART:Shoot(pos, ang) end net.Start("pac_projectile") + net.WriteUInt(multi_projectile_count,7) net.WriteVector(pos) net.WriteAngle(ang) net.WriteTable(tbl) @@ -284,7 +292,9 @@ function PART:Shoot(pos, ang) end if self.Delay == 0 then - spawn() + for i = multi_projectile_count,1,-1 do + spawn() + end else timer.Simple(self.Delay, spawn) end diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 014fbb7d4..3f826cdc8 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -458,6 +458,77 @@ do -- end end + + + +--[[ +self.truevel_ent = nil +self.truevel_last_ent = nil +self.truevel_next_log = 0 +do --true velocity (tm) + function seek_past_neighboring_timestamp() + local offset = math.floor(self.VelocityRoughness) + local begin_seek_position = math.floor(SysTime() * 100) + if self.truevel_ent.position_timestamps[begin_seek_position - offset] ~= nil then + return self.truevel_ent.position_timestamps[begin_seek_position - offset] + else + local latest_past_timestamp + for stamp,logged_pos in ipairs(self.truevel_ent.position_timestamps) do + if self.truevel_ent.position_timestamps[stamp] ~= nil then + if stamp < begin_seek_position - offset then + + end + end + end + end + end + + function PART:GetTrueVelocity() + self.true_vel = self.true_vel or Vector() + local systime = SysTime() + local logtime = math.floor(systime * 100) + self.truevel_next_log = self.truevel_next_log or systime + if systime < self.truevel_next_log then return self.true_vel + else self.truevel_next_log = systime + 0.05 end + print("log time ", logtime) + if self.truevel_ent then self.truevel_ent.position_timestamps = ent.position_timestamps or {} end + + self.last_pos = seek_past_neighboring_timestamp() + + local pos + + if self.RootOwner then + self.truevel_ent = self:GetRootPart():GetOwner() + pos = self:GetRootPart():GetOwner():GetPos() + elseif self.truevel_ent ~= nil then + self.truevel_ent = self:GetOwner() + pos = self:GetOwner():GetWorldPosition() or self:GetOwner():GetPos() + end + + self.truevel_last_ent = self.truevel_ent + self.truevel_ent.position_timestamps[logtime] = pos + + self.true_vel = pos - self.truevel_ent.position_timestamps[seek_past_neighboring_timestamp()] + + return self.true_vel or Vector() + end + + PART.Inputs.owner_true_velocity_length = function(self) + + if self:GetPhysicalTarget():IsValid() then + ent = self:GetPhysicalTarget() + end + if self.RootOwner then + ent = self:GetRootPart():GetOwner() + end + return self:GetTrueVelocity():Length() + end +end +]] + + + + do -- velocity PART.Inputs.parent_velocity_length = function(self) return self:GetVelocity(self:GetPhysicalTarget()):Length() @@ -510,12 +581,23 @@ end PART.Inputs.pose_parameter = function(self, name) if not name then return 0 end - local owner = get_owner(self) + local owner = self:GetPlayerOwner() if owner:IsValid() and owner.GetPoseParameter then return owner:GetPoseParameter(name) end return 0 end +PART.Inputs.pose_parameter_true = function(self, name) + if not name then return 0 end + local owner = self:GetPlayerOwner() + if owner:IsValid() then + mini,maxi = owner:GetPoseParameterRange(owner:LookupPoseParameter(name)) + actual_value = mini + (maxi - mini)*(owner:GetPoseParameter(name)) + return actual_value + else end + return 0 +end + PART.Inputs.command = function(self) local ply = self:GetPlayerOwner() if ply.pac_proxy_events then @@ -755,38 +837,6 @@ do end end -PART.Inputs.flat_dot_forward = function(self) - local part = get_owner(self) - - if part:IsValid() then - local ang = part:IsPlayer() and part:EyeAngles() or part:GetAngles() - ang.p = 0 - ang.r = 0 - local dir = pac.EyePos - part:EyePos() - dir[3] = 0 - dir:Normalize() - return dir:Dot(ang:Forward()) - end - - return 0 -end - -PART.Inputs.flat_dot_right = function(self) - local part = get_owner(self) - - if part:IsValid() then - local ang = part:IsPlayer() and part:EyeAngles() or part:GetAngles() - ang.p = 0 - ang.r = 0 - local dir = pac.EyePos - part:EyePos() - dir[3] = 0 - dir:Normalize() - return dir:Dot(ang:Right()) - end - - return 0 -end - net.Receive("pac_proxy", function() local ply = net.ReadEntity() local str = net.ReadString() diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index ce5116ae9..258bcfdcd 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -7,7 +7,7 @@ local DisableClipping = DisableClipping local render_CullMode = render.CullMode local cam_End3D2D = cam.End3D2D local cam_End3D = cam.End3D -local TEXT_ALIGN_CENTER = TEXT_ALIGN_CENTER +--local Text_Align = TEXT_ALIGN_CENTER local surface_SetFont = surface.SetFont local Color = Color @@ -18,9 +18,36 @@ PART.Group = 'effects' PART.Icon = 'icon16/text_align_center.png' BUILDER:StartStorableVars() - BUILDER:GetSet("Text", "") - BUILDER:GetSet("Font", "default") - BUILDER:GetSet("Size", 1, {editor_sensitivity = 0.25}) + :SetPropertyGroup("generic") + :PropertyOrder("Name") + :PropertyOrder("Hide") + :GetSet("Text", "") + :GetSet("Font", "default") + :GetSet("Size", 1, {editor_sensitivity = 0.25}) + + + :SetPropertyGroup("text layout") + :GetSet("HorizontalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Left"] = "0", ["Center"] = "1", ["Right"] = "2"}}) + :GetSet("VerticalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Center"] = "1", ["Top"] = "3", ["Bottom"] = "4"}}) + :GetSet("ConcatenateTextAndOverrideValue",false,{editor_friendly = "CombinedText"}) + + :SetPropertyGroup("data source") + :GetSet("TextOverride", "Text", {enums = { + ["Text"] = "Text", + ["Health"] = "Health", + ["Maximum Health"] = "MaxHealth", + ["Armor"] = "Armor", + ["Maximum Armor"] = "MaxArmor", + ["Timerx"] = "Timerx", + ["CurTime"] = "CurTime", + ["RealTime"] = "RealTime", + ["Clip current Ammo"] = "Ammo", + ["Clip Size"] = "ClipSize", + ["Ammo Reserve"] = "AmmoReserve", + ["Proxy value (Using DynamicTextValue)"] = "Proxy"}}) + :GetSet("DynamicTextValue", 0) + + :SetPropertyGroup("appearance") BUILDER:GetSet("Outline", 0) BUILDER:GetSet("Color", Vector(255, 255, 255), {editor_panel = "color"}) BUILDER:GetSet("Alpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}}) @@ -34,7 +61,9 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:GetNiceName() - return '"' .. self:GetText() .. '"' + if self.TextOverride ~= "Text" then return self.TextOverride end + + return 'Text: "' .. self:GetText() .. '"' end function PART:SetColor(v) @@ -73,6 +102,7 @@ end function PART:SetFont(str) if not pcall(surface_SetFont, str) then + pac.Message(Color(255,150,0),"[PAC3] "..str.." Font not found! Reverting to DermaDefault!") str = "DermaDefault" end @@ -81,16 +111,45 @@ end function PART:OnDraw() local pos, ang = self:GetDrawPosition() + local DisplayText = self.Text or "" + if self.TextOverride == "Text" then goto DRAW end + + if self.TextOverride == "Health"then DisplayText = self:GetPlayerOwner():Health() + elseif self.TextOverride == "MaxHealth" then + DisplayText = self:GetPlayerOwner():GetMaxHealth() + elseif self.TextOverride == "Ammo" then + DisplayText = self:GetPlayerOwner():GetActiveWeapon():Clip1() + elseif self.TextOverride == "ClipSize" then + DisplayText = self:GetPlayerOwner():GetActiveWeapon():GetMaxClip1() + elseif self.TextOverride == "AmmoReserve" then + DisplayText = self:GetPlayerOwner():GetAmmoCount(self:GetPlayerOwner():GetActiveWeapon():GetPrimaryAmmoType()) + elseif self.TextOverride == "Armor" then + DisplayText = self:GetPlayerOwner():Armor() + elseif self.TextOverride == "MaxArmor" then + DisplayText = self:GetPlayerOwner():GetMaxArmor() + elseif self.TextOverride == "Timerx" then + DisplayText = ""..math.Round(CurTime() - self.time,2) + elseif self.TextOverride == "CurTime" then + DisplayText = ""..math.Round(CurTime(),2) + elseif self.TextOverride == "RealTime" then + DisplayText = ""..math.Round(RealTime(),2) + elseif self.TextOverride == "Proxy" then + --print(type(self.DynamicTextValue)) + DisplayText = ""..math.Round(self.DynamicTextValue,2) + end - if self.Text ~= "" then + if self.ConcatenateTextAndOverrideValue then DisplayText = ""..self.Text..DisplayText end + + ::DRAW:: + if DisplayText ~= "" then cam_Start3D(EyePos(), EyeAngles()) cam_Start3D2D(pos, ang, self.Size) local oldState = DisableClipping(true) - draw_SimpleTextOutlined(self.Text, self.Font, 0,0, self.ColorC, TEXT_ALIGN_CENTER,TEXT_ALIGN_CENTER, self.Outline, self.OutlineColorC) + draw_SimpleTextOutlined(DisplayText, self.Font, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) render_CullMode(1) -- MATERIAL_CULLMODE_CW - draw_SimpleTextOutlined(self.Text, self.Font, 0,0, self.ColorC, TEXT_ALIGN_CENTER,TEXT_ALIGN_CENTER, self.Outline, self.OutlineColorC) + draw_SimpleTextOutlined(DisplayText, self.Font, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) render_CullMode(0) -- MATERIAL_CULLMODE_CCW DisableClipping(oldState) @@ -99,6 +158,15 @@ function PART:OnDraw() end end +function PART:OnThink() + self:OnDraw() +end + +function PART:OnShow() + self.time = CurTime() + self:OnDraw() +end + function PART:SetText(str) self.Text = str end diff --git a/lua/pac3/core/server/init.lua b/lua/pac3/core/server/init.lua index 4e60b8688..7abebcfee 100644 --- a/lua/pac3/core/server/init.lua +++ b/lua/pac3/core/server/init.lua @@ -17,6 +17,7 @@ include("pac3/core/shared/init.lua") include("effects.lua") include("event.lua") include("net_messages.lua") +include("net_combat.lua") include("test_suite_backdoor.lua") include("in_skybox.lua") diff --git a/lua/pac3/core/shared/util.lua b/lua/pac3/core/shared/util.lua index 37e4d0cce..6e8e7d0a9 100644 --- a/lua/pac3/core/shared/util.lua +++ b/lua/pac3/core/shared/util.lua @@ -135,8 +135,6 @@ for _, params in pairs(shader_params.base) do end end -texture_keys["include"] = "include" - -- for pac_restart PAC_MDL_SALT = PAC_MDL_SALT or 0 @@ -465,7 +463,6 @@ function pac.DownloadMDL(url, callback, onfail, ply) table.insert(found_vmt_directories, {dir = dir}) f:seek(old_pos) end - table.sort(found_vmt_directories, function(a,b) return #a.dir>#b.dir end) f:seek(old_pos) end @@ -604,7 +601,7 @@ function pac.DownloadMDL(url, callback, onfail, ply) end for shader_param in pairs(texture_keys) do - data.buffer = data.buffer:gsub('("?%$?%f[%w_]' .. shader_param .. '%f[^%w_]"?%s+"?)([^"%c]+)("?%s?)', function(l, vtf_path, r) + data.buffer = data.buffer:gsub('("?%$' .. shader_param .. '"?%s+")(.-)(")', function(l, vtf_path, r) if vtf_path == "env_cubemap" then return end @@ -621,19 +618,12 @@ function pac.DownloadMDL(url, callback, onfail, ply) end end - if not new_path then - for _, info in ipairs(files) do + for _, info in ipairs(files) do + if info.file_name:EndsWith(".vtf") then local vtf_name = (vtf_path:match(".+/(.+)") or vtf_path) - if info.file_name:EndsWith(".vtf") then - if info.file_name == vtf_name .. ".vtf" or info.file_name == vtf_name then - new_path = dir .. vtf_name - break - end - elseif (info.file_name:EndsWith(".vmt") and l:StartWith("include")) then - if info.file_name == vtf_name then - new_path = "materials/" .. dir .. vtf_name - break - end + if info.file_name == vtf_name .. ".vtf" then + new_path = dir .. vtf_name + break end end end diff --git a/lua/pac3/editor/client/examples.lua b/lua/pac3/editor/client/examples.lua index d6b399c25..c940787f1 100644 --- a/lua/pac3/editor/client/examples.lua +++ b/lua/pac3/editor/client/examples.lua @@ -1931,631 +1931,238 @@ pace.example_outfits["skis"] = { } pace.example_outfits["southpark"] = { - [1] = { - ["children"] = { - [1] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["Jiggle"] = false, - ["DrawOrder"] = 0, - ["UniqueID"] = "3513732839", - ["TargetEntityUID"] = "", - ["AimPartName"] = "", - ["FollowPartUID"] = "", - ["Bone"] = "head", - ["ScaleChildren"] = false, - ["AngleOffset"] = Angle(0, 0, 0), - ["MoveChildrenToOrigin"] = false, - ["Position"] = Vector(0, 0, 0), - ["AimPartUID"] = "", - ["Angles"] = Angle(0, 0, 0), - ["Hide"] = false, - ["Name"] = "", - ["Scale"] = Vector(1, 1, 1), - ["EditorExpand"] = false, - ["ClassName"] = "bone", - ["Size"] = 0, - ["PositionOffset"] = Vector(0, 0, 0), - ["IsDisturbing"] = false, - ["AlternativeBones"] = false, - ["EyeAngles"] = false, - ["FollowAnglesOnly"] = false, - }, +[1] = { + ["children"] = { + [1] = { + ["children"] = { + [1] = { + ["children"] = { + }, + ["self"] = { + ["ClassName"] = "bone", + ["UniqueID"] = "3513732839", + ["Size"] = 0, }, - }, - ["self"] = { - ["HidePhysgunBeam"] = false, - ["Skin"] = 0, - ["UniqueID"] = "2782871480", - ["HideBullets"] = false, - ["FallApartOnDeath"] = false, - ["DeathRagdollizeParent"] = false, - ["WalkSpeed"] = 0, - ["BlendMode"] = "", - ["EyeAngles"] = false, - ["HideEntity"] = false, - ["AimPartUID"] = "", - ["Model"] = "", - ["LodOverride"] = -1, - ["Name"] = "", - ["MuteSounds"] = false, - ["AllowOggWhenMuted"] = false, - ["AngleOffset"] = Angle(0, 0, 0), - ["InverseKinematics"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["Brightness"] = 1, - ["DoubleFace"] = false, - ["IgnoreZ"] = false, - ["DrawOrder"] = 0, - ["EditorExpand"] = true, - ["HideRagdollOnDeath"] = false, - ["IsDisturbing"] = false, - ["RelativeBones"] = true, - ["TargetEntityUID"] = "", - ["DrawShadow"] = true, - ["Alpha"] = 0, - ["Material"] = "", - ["Translucent"] = false, - ["SuppressFrames"] = false, - ["CrouchSpeed"] = 0, - ["Bone"] = "head", - ["NoTextureFiltering"] = false, - ["MuteFootsteps"] = false, - ["Invert"] = false, - ["DrawWeapon"] = true, - ["Position"] = Vector(0, 0, 0), - ["DrawPlayerOnDeath"] = false, - ["Weapon"] = false, - ["Hide"] = false, - ["Angles"] = Angle(0, 0, 0), - ["Scale"] = Vector(1, 1, 1), - ["AimPartName"] = "", - ["RunSpeed"] = 0, - ["Size"] = 0.44, - ["UseLegacyScale"] = false, - ["ClassName"] = "entity", - ["AnimationRate"] = 1, - ["EyeTargetUID"] = "", - ["SprintSpeed"] = 0, }, }, - [2] = { - ["children"] = { - [1] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "3210564156", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = true, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = false, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "AngleOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - }, + ["self"] = { + ["Alpha"] = 0, + ["EditorExpand"] = true, + ["UniqueID"] = "2782871480", + ["Fullbright"] = true, + ["Size"] = 0.44, + ["ClassName"] = "entity", + }, + }, + [2] = { + ["children"] = { + [1] = { + ["children"] = { + [1] = { + ["children"] = { }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["AffectChildrenOnly"] = false, - ["DrawOrder"] = 0, - ["TargetPartUID"] = "", - ["Name"] = "", - ["Event"] = "flat_dot_right", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["RootOwner"] = true, - ["EditorExpand"] = true, - ["ClassName"] = "event", - ["Arguments"] = "-0.707", - ["Invert"] = true, - ["IsDisturbing"] = false, - ["Operator"] = "below", - ["UniqueID"] = "1222338801", - ["ZeroEyePitch"] = false, - }, + ["self"] = { + ["RootOwner"] = true, + ["UniqueID"] = "3210564156", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + ["ClassName"] = "proxy", + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "AngleOffset", }, }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = true, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "LOCALEYES_YAW", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "left", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 0.999, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 0), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = true, - ["Scale"] = Vector(1, 1, -0.0099999997764826), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 1, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(90, 90, 90), - ["TextureFilter"] = 3, - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["UniqueID"] = "537182332", + [2] = { + ["children"] = { + }, + ["self"] = { + ["Arguments"] = "-0.7", + ["UniqueID"] = "1222338801", + ["Event"] = "dot_right", + ["Operator"] = "above", + ["ClassName"] = "event", + ["EditorExpand"] = true, + }, }, }, - [2] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["AffectChildrenOnly"] = false, - ["DrawOrder"] = 0, - ["TargetPartUID"] = "", - ["Name"] = "", - ["Event"] = "flat_dot_forward", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["RootOwner"] = true, - ["EditorExpand"] = true, - ["ClassName"] = "event", - ["Arguments"] = "-0.7071067812", - ["Invert"] = true, - ["IsDisturbing"] = false, - ["Operator"] = "equal or below", - ["UniqueID"] = "34fd29c1896140b8e5b639659a278ff810f4db34e73de20b372f6d0d0f0b8bad", - ["ZeroEyePitch"] = false, - }, + ["self"] = { + ["Invert"] = true, + ["UniqueID"] = "537182332", + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["EditorExpand"] = true, + ["Name"] = "left", + ["Scale"] = Vector(1, 1, -0.0099999997764826), + ["Alpha"] = 0.999, + ["ClassName"] = "model", + ["AngleOffset"] = Angle(90, 90, 90), + ["AimPartName"] = "LOCALEYES_YAW", + ["Bone"] = "none", + ["Fullbright"] = true, + ["Translucent"] = true, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", + }, + }, + [2] = { + ["children"] = { + }, + ["self"] = { + ["EditorExpand"] = true, + ["UniqueID"] = "2385081529", + ["Expression"] = "0,0,owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or 0) or 0", + ["ClassName"] = "proxy", + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "PositionOffset", + }, + }, + [3] = { + ["children"] = { + [1] = { + ["children"] = { }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "75353876", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = true, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = true, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "AngleOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - }, + ["self"] = { + ["EditorExpand"] = true, + ["UniqueID"] = "75353876", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + ["ClassName"] = "proxy", + ["RootOwner"] = true, + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "AngleOffset", }, }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = false, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "LOCALEYES_YAW", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "back", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 0.99, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/back.png", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 0), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = true, - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 1, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(90, 90, 90), - ["TextureFilter"] = 3, - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["UniqueID"] = "2325223398", + [2] = { + ["children"] = { + }, + ["self"] = { + ["Arguments"] = "0.7", + ["UniqueID"] = "3855963026", + ["Event"] = "dot_forward", + ["Operator"] = "below", + ["ClassName"] = "event", + ["EditorExpand"] = true, + }, }, }, - [3] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["AffectChildrenOnly"] = false, - ["DrawOrder"] = 0, - ["TargetPartUID"] = "", - ["Name"] = "", - ["Event"] = "flat_dot_right", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["RootOwner"] = true, - ["EditorExpand"] = true, - ["ClassName"] = "event", - ["Arguments"] = "0.707", - ["Invert"] = true, - ["IsDisturbing"] = false, - ["Operator"] = "above", - ["UniqueID"] = "2514734784", - ["ZeroEyePitch"] = false, - }, + ["self"] = { + ["UniqueID"] = "2325223398", + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["EditorExpand"] = true, + ["Name"] = "back", + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["Alpha"] = 0.99, + ["AngleOffset"] = Angle(90, 90, 90), + ["Fullbright"] = true, + ["AimPartName"] = "LOCALEYES_YAW", + ["ClassName"] = "model", + ["Bone"] = "none", + ["Translucent"] = true, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/back.png", + }, + }, + [4] = { + ["children"] = { + [1] = { + ["children"] = { }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "2809374115", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = true, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = false, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "AngleOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - }, + ["self"] = { + ["Arguments"] = "0.7", + ["UniqueID"] = "2514734784", + ["Event"] = "dot_right", + ["Operator"] = "below", + ["ClassName"] = "event", + ["EditorExpand"] = true, }, }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = false, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "LOCALEYES_YAW", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "right", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 0.999, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 0), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = true, - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 1, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(90, 90, 90), - ["TextureFilter"] = 3, - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["UniqueID"] = "1916911126", + [2] = { + ["children"] = { + }, + ["self"] = { + ["RootOwner"] = true, + ["UniqueID"] = "2809374115", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + ["ClassName"] = "proxy", + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "AngleOffset", + }, }, }, - [4] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "2385081529", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = false, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = true, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "PositionOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "0,0,owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or 0) or 0", - }, - }, - [5] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["AffectChildrenOnly"] = false, - ["DrawOrder"] = 0, - ["TargetPartUID"] = "", - ["Name"] = "", - ["Event"] = "flat_dot_forward", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["RootOwner"] = true, - ["EditorExpand"] = true, - ["ClassName"] = "event", - ["Arguments"] = "0.7071067812", - ["Invert"] = true, - ["IsDisturbing"] = false, - ["Operator"] = "equal or above", - ["UniqueID"] = "2334772782", - ["ZeroEyePitch"] = false, - }, + ["self"] = { + ["UniqueID"] = "1916911126", + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["EditorExpand"] = true, + ["Name"] = "right", + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["Alpha"] = 0.999, + ["AngleOffset"] = Angle(90, 90, 90), + ["Fullbright"] = true, + ["AimPartName"] = "LOCALEYES_YAW", + ["ClassName"] = "model", + ["Bone"] = "none", + ["Translucent"] = true, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", + }, + }, + [5] = { + ["children"] = { + [1] = { + ["children"] = { }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "4261893074", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = false, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = true, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "AngleOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - }, + ["self"] = { + ["Arguments"] = "-0.7", + ["UniqueID"] = "2334772782", + ["Event"] = "dot_forward", + ["Operator"] = "above", + ["ClassName"] = "event", + ["EditorExpand"] = true, }, }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = false, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "LOCALEYES_YAW", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "front", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 0.999, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/front.png", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 0), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = true, - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 1, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(90, 90, 90), - ["TextureFilter"] = 3, - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["UniqueID"] = "2579545900", + [2] = { + ["children"] = { + }, + ["self"] = { + ["EditorExpand"] = true, + ["UniqueID"] = "4261893074", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + ["ClassName"] = "proxy", + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "AngleOffset", + }, }, }, - }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = false, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "body", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 1, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = false, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 24), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = false, - ["Scale"] = Vector(1, 1, 1), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 0, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(0, 0, 0), - ["TextureFilter"] = 3, - ["Model"] = "models/pac/default.mdl", - ["UniqueID"] = "49506760", + ["self"] = { + ["UniqueID"] = "2579545900", + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["EditorExpand"] = true, + ["Name"] = "front", + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["Alpha"] = 0.999, + ["AngleOffset"] = Angle(90, 90, 90), + ["Fullbright"] = true, + ["AimPartName"] = "LOCALEYES_YAW", + ["ClassName"] = "model", + ["Bone"] = "none", + ["Translucent"] = true, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/front.png", + }, }, }, + ["self"] = { + ["UniqueID"] = "49506760", + ["ClassName"] = "model", + ["Position"] = Vector(0, 0, 24), + ["Model"] = "models/pac/default.mdl", + ["Size"] = 0, + ["Bone"] = "none", + ["Name"] = "body", + ["EditorExpand"] = true, + }, }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "743553614", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["EditorExpand"] = true, - ["OwnerName"] = "self", - ["IsDisturbing"] = false, - ["Name"] = "my outfit", - ["Duplicate"] = false, - ["ClassName"] = "group", - }, - }, + }, + ["self"] = { + ["EditorExpand"] = true, + ["UniqueID"] = "743553614", + ["ClassName"] = "group", + ["Name"] = "my outfit", + ["Description"] = "add parts to me!", + }, +}, } diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index dab095bdb..81a0018cd 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -481,4 +481,4 @@ function PANEL:Paint(w,h) --DFrame.Paint(self, w,h) end -pace.RegisterPanel(PANEL) +pace.RegisterPanel(PANEL) \ No newline at end of file diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 48e9dceca..690511b1f 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -714,9 +714,12 @@ do -- event is_touching if part ~= last_part then stop() return end if not part:IsValid() then stop() return end if part.ClassName ~= "event" then stop() return end - if part:GetEvent() ~= "is_touching" then stop() return end + if not (part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable") then stop() return end local extra_radius = part:GetProperty("extra_radius") or 0 + local x_stretch = part:GetProperty("x_stretch") or 1 + local y_stretch = part:GetProperty("y_stretch") or 1 + local z_stretch = part:GetProperty("z_stretch") or 1 local ent if part.RootOwner then ent = part:GetRootPart():GetOwner() @@ -725,28 +728,36 @@ do -- event is_touching end if not IsValid(ent) then stop() return end - local radius = ent:BoundingRadius() + local radius if radius == 0 and IsValid(ent.pac_projectile) then radius = ent.pac_projectile:GetRadius() end - radius = math.max(radius + extra_radius + 1, 1) + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + + if part:GetEvent() == "is_touching" then + radius = math.max(ent:BoundingRadius() + extra_radius + 1, 1) + mins = mins * radius + maxs = maxs * radius + end + if part:GetEvent() == "is_touching_scalable" then + radius = math.max(extra_radius, 1) + mins = mins * radius + maxs = maxs * radius + end - local mins = Vector(-1,-1,-1) - local maxs = Vector(1,1,1) local startpos = ent:WorldSpaceCenter() - mins = mins * radius - maxs = maxs * radius local tr = util.TraceHull( { start = startpos, endpos = startpos, maxs = maxs, mins = mins, - filter = ent + filter = {part:GetRootPart():GetOwner(),ent} } ) - + if self.udata then render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, tr.Hit and Color(255,0,0) or Color(255,255,255), true ) end diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 845c058b5..6a24acf0d 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1,4 +1,16 @@ local L = pace.LanguageString +local BulkSelectList = {} +local BulkSelectUIDs = {} +pace.BulkSelectClipboard = {} +refresh_halo_hook = true + + +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") + +CreateConVar( "pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") +CreateConVar( "pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") -- load only when hovered above local function add_expensive_submenu_load(pnl, callback) @@ -11,6 +23,7 @@ local function add_expensive_submenu_load(pnl, callback) end function pace.WearParts(temp_wear_filter) + local allowed, reason = pac.CallHook("CanWearParts", pac.LocalPlayer) if allowed == false then @@ -98,13 +111,65 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) end pace.RefreshTree() - + timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) return part end +local last_span_select_part +local last_select_was_span = false +local last_direction + function pace.OnPartSelected(part, is_selecting) - local parent = part:GetRootPart() + if input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then + --jumping multi-select if holding shift + ctrl + if input.IsControlDown() and input.IsShiftDown() and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then + --ripped some local functions from tree.lua + local added_nodes = {} + for i,v in ipairs(pace.tree.added_nodes) do + if v.part and v:IsVisible() and v:IsExpanded() then + table.insert(added_nodes, v) + end + end + + local startnodenumber = table.KeyFromValue( added_nodes, pace.current_part.pace_tree_node) + local endnodenumber = table.KeyFromValue( added_nodes, part.pace_tree_node) + + table.sort(added_nodes, function(a, b) return select(2, a:LocalToScreen()) < select(2, b:LocalToScreen()) end) + + local i = startnodenumber + + direction = math.Clamp(endnodenumber - startnodenumber,-1,1) + if direction == 0 then last_direction = direction return end + last_direction = last_direction or 0 + if last_span_select_part == nil then last_span_select_part = part end + + if last_select_was_span then + if last_direction == -direction then + pace.DoBulkSelect(pace.current_part, true) + end + if last_span_select_part == pace.current_part then + pace.DoBulkSelect(pace.current_part, true) + end + end + while (i ~= endnodenumber) do + pace.DoBulkSelect(added_nodes[i].part, true) + i = i + direction + end + pace.DoBulkSelect(part) + last_direction = direction + last_select_was_span = true + else + pace.DoBulkSelect(part) + last_select_was_span = false + end + + else last_select_was_span = false end + last_span_select_part = part + + + + local parent = part:GetRootPart() if parent:IsValid() and (parent.OwnerName == "viewmodel" or parent.OwnerName == "hands") then pace.editing_viewmodel = parent.OwnerName == "viewmodel" pace.editing_hands = parent.OwnerName == "hands" @@ -132,6 +197,7 @@ function pace.OnPartSelected(part, is_selecting) if not is_selecting then pace.StopSelect() end + end function pace.OnVariableChanged(obj, key, val, not_from_editor) @@ -239,7 +305,12 @@ function pace.GetRegisteredParts() end do -- menu + local trap + if not pace.Active or refresh_halo_hook then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + end + function pace.AddRegisteredPartsToMenu(menu, parent) local partsToShow = {} local clicked = false @@ -509,6 +580,8 @@ do -- menu end function pace.RemovePart(obj) + if table.HasValue(BulkSelectList,obj) then table.RemoveByValue(BulkSelectList,obj) end + pace.RecordUndoHistory() obj:Remove() pace.RecordUndoHistory() @@ -520,13 +593,177 @@ do -- menu end end + function pace.ClearBulkList() + for _,v in ipairs(BulkSelectList) do + if v.pace_tree_node ~= nil then v.pace_tree_node:SetAlpha( 255 ) end + v:SetInfo() + end + BulkSelectList = {} + print("Bulk list deleted!") + --surface.PlaySound('buttons/button16.wav') + end +//@note pace.DoBulkSelect + function pace.DoBulkSelect(obj, silent) + refresh_halo_hook = false + --print(obj.pace_tree_node, "color", obj.pace_tree_node:GetFGColor().r .. " " .. obj.pace_tree_node:GetFGColor().g .. " " .. obj.pace_tree_node:GetFGColor().b) + if obj.ClassName == "timeline_dummy_bone" then return end + local selected_part_added = false --to decide the sound to play afterward + + BulkSelectList = BulkSelectList or {} + if (table.HasValue(BulkSelectList, obj)) then + pace.RemoveFromBulkSelect(obj) + selected_part_added = false + elseif (BulkSelectList[obj] == nil) then + pace.AddToBulkSelect(obj) + selected_part_added = true + for _,v in ipairs(obj:GetChildrenList()) do + pace.RemoveFromBulkSelect(v) + end + end + + --check parents and children + for _,v in ipairs(BulkSelectList) do + if table.HasValue(v:GetChildrenList(), obj) then + --print("selected part is already child to a bulk-selected part!") + pace.RemoveFromBulkSelect(obj) + selected_part_added = false + elseif table.HasValue(obj:GetChildrenList(), v) then + --print("selected part is already parent to a bulk-selected part!") + pace.RemoveFromBulkSelect(v) + selected_part_added = false + end + end + + RebuildBulkHighlight() + if not silent then + if selected_part_added then + surface.PlaySound('buttons/button1.wav') + --test print + for i,v in ipairs(BulkSelectList) do + print("["..i.."] = "..v.UniqueID) + end + print("\n") + else surface.PlaySound('buttons/button16.wav') end + end + + if table.IsEmpty(BulkSelectList) then + --remove halo hook + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + else + --start halo hook + hook.Add('PreDrawHalos', "BulkSelectHighlights", function() + local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + if mode == 0 then return + elseif mode == 1 then ThinkBulkHighlight() + elseif mode == 2 then if input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) then ThinkBulkHighlight() end + elseif mode == 3 then if input.IsControlDown() then ThinkBulkHighlight() end + elseif mode == 4 then if input.IsShiftDown() then ThinkBulkHighlight() end + end + end) + end + + for _,v in ipairs(BulkSelectList) do + --v.pace_tree_node:SetAlpha( 150 ) + end + end + + function pace.RemoveFromBulkSelect(obj) + table.RemoveByValue(BulkSelectList, obj) + obj.pace_tree_node:SetAlpha( 255 ) + obj:SetInfo() + --RebuildBulkHighlight() + end + + function pace.AddToBulkSelect(obj) + table.insert(BulkSelectList, obj) + if obj.pace_tree_node == nil then return end + obj:SetInfo("selected in bulk select") + obj.pace_tree_node:SetAlpha( 150 ) + --RebuildBulkHighlight() + end + + function pace.BulkCutPaste(obj) + pace.RecordUndoHistory() + for _,v in ipairs(BulkSelectList) do + --if a part is inserted onto itself, it should instead serve as a parent + if v ~= obj then v:SetParent(obj) end + end + pace.RecordUndoHistory() + pace.RefreshTree() + end + + function pace.BulkCopy(obj) + if #BulkSelectList == 1 then pace.Copy(obj) end --at least if there's one selected, we can take it that we want to copy that part + pace.BulkSelectClipboard = table.Copy(BulkSelectList) --if multiple parts are selected, copy it to a new bulk clipboard + print("copied: ") + TestPrintTable(pace.BulkSelectClipboard,"pace.BulkSelectClipboard") + end + + function pace.BulkPasteFromBulkClipboard(obj) --paste bulk clipboard into one part + pace.RecordUndoHistory() + if not table.IsEmpty(pace.BulkSelectClipboard) then + for _,v in ipairs(pace.BulkSelectClipboard) do + local newObj = pac.CreatePart(v.ClassName) + newObj:SetTable(v:ToTable(), true) + newObj:SetParent(obj) + end + end + pace.RecordUndoHistory() + --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end) + end + + function pace.BulkPasteFromBulkSelectToSinglePart(obj) --paste bulk selection into one part + pace.RecordUndoHistory() + if not table.IsEmpty(BulkSelectList) then + for _,v in ipairs(BulkSelectList) do + local newObj = pac.CreatePart(v.ClassName) + newObj:SetTable(v:ToTable(), true) + newObj:SetParent(obj) + end + end + pace.RecordUndoHistory() + end + + function pace.BulkPasteFromSingleClipboard() --paste the normal clipboard into each bulk select item + pace.RecordUndoHistory() + if not table.IsEmpty(BulkSelectList) then + for _,v in ipairs(BulkSelectList) do + local newObj = pac.CreatePart(pace.Clipboard.self.ClassName) + newObj:SetTable(pace.Clipboard, true) + newObj:SetParent(v) + end + end + pace.RecordUndoHistory() + --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end) + end + + function pace.BulkRemovePart() + pace.RecordUndoHistory() + if not table.IsEmpty(BulkSelectList) then + for _,v in ipairs(BulkSelectList) do + v:Remove() + + if not v:HasParent() and v.ClassName == "group" then + pace.RemovePartOnServer(v:GetUniqueID(), false, true) + end + end + end + pace.RefreshTree() + pace.RecordUndoHistory() + pace.ClearBulkList() + --timer.Simple(0.1, function BulkSelectRefreshFadedNodes() end) + end +//@note part menu function pace.OnPartMenu(obj) local menu = DermaMenu() menu:SetPos(input.GetCursorPos()) if obj then if not obj:HasParent() then - menu:AddOption(L"wear", function() pace.SendPartToServer(obj) end):SetImage(pace.MiscIcons.wear) + menu:AddOption(L"wear", function() + pace.SendPartToServer(obj) + BulkSelectList = {} + end):SetImage(pace.MiscIcons.wear) end menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy) @@ -535,6 +772,77 @@ do -- menu menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) + --multi select + bulk_menu, icon = menu:AddSubMenu(L"bulk select ("..#BulkSelectList..")", function() pace.DoBulkSelect(obj) end) + icon:SetImage('icon16/table_multiple.png') + bulk_menu.GetDeleteSelf = function() return false end + + local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + local info + if mode == 0 then info = "not halo-highlighted" + elseif mode == 1 then info = "automatically halo-highlighted" + elseif mode == 2 then info = "halo-highlighted on custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString() + elseif mode == 3 then info = "halo-highlighted on preset keypress: control" + elseif mode == 4 then info = "halo-highlighted on preset keypress: shift" end + + bulk_menu:AddOption(L"Bulk select info: "..info, function() end):SetImage(pace.MiscIcons.info) + bulk_menu:AddOption(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts", function() end):SetImage(pace.MiscIcons.info) + + bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() + pace.BulkCutPaste(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Copy to Bulk Clipboard", function() + pace.BulkCopy(obj) + end):SetImage(pace.MiscIcons.copy) + + bulk_menu:AddSpacer() + + --bulk paste modes + bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function() + pace.BulkPasteFromBulkSelectToSinglePart(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function() + if not pace.Clipboard then pace.Copy(obj) end + pace.BulkPasteFromSingleClipboard() + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function() + pace.BulkPasteFromBulkClipboard(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function() + for _,v in ipairs(BulkSelectList) do + pace.BulkPasteFromBulkClipboard(v) + end + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk paste properties from selected part", function() + pace.Copy(obj) + for _,v in ipairs(BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddOption(L"Bulk paste properties from clipboard", function() + for _,v in ipairs(BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk Delete", function() + pace.BulkRemovePart() + end):SetImage(pace.MiscIcons.clear) + + bulk_menu:AddOption(L"Clear Bulk List", function() + pace.ClearBulkList() + end):SetImage('icon16/table_delete.png') + menu:AddSpacer() end @@ -575,36 +883,198 @@ do -- menu local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) pnl:SetImage(pace.MiscIcons.load) add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) - menu:AddOption(L"clear", function() + pace.ClearBulkList() pace.ClearParts() end):SetImage(pace.MiscIcons.clear) end end -do + +do --hover highlight halo pac.haloex = include("pac3/libraries/haloex.lua") + local hover_halo_limit + local warn = false + local post_warn_next_allowed_check_time = 0 + local post_warn_next_allowed_warn_time = 0 + local last_culprit_UID = 0 + local last_checked_partUID = 0 + local last_tbl = {} + local last_bulk_select_tbl = nil + local last_root_ent = {} + local last_time_checked = 0 + function pace.OnHoverPart(self) + local skip = false + if GetConVar("pac_hover_color"):GetString() == "none" then return end + hover_halo_limit = GetConVar("pac_hover_halo_limit"):GetInt() + local tbl = {} local ent = self:GetOwner() - - if ent:IsValid() then - table.insert(tbl, ent) + local is_root = ent == self:GetRootPart():GetOwner() + + --decide whether to skip + --it will skip the part-search loop if we already checked the part recently + if self.UniqueID == last_checked_partUID then + skip = true + if is_root and last_root_ent ~= self:GetRootPart():GetOwner() then + table.RemoveByValue(last_tbl, last_root_ent) + table.insert(last_tbl, self:GetRootPart():GetOwner()) + end + tbl = last_tbl end - for _, child in ipairs(self:GetChildrenList()) do - local ent = self:GetOwner() - if ent:IsValid() then + --operations : search the part and look for entity-candidates to halo + if not skip then + --start with entity, which could be part or entity + if (is_root and ent:IsValid()) then table.insert(tbl, ent) + else + if not ((self.ClassName == "group" or self.ClassName == "jiggle") or (self.Hide == true) or (self.Size == 0) or (self.Alpha == 0)) then + table.insert(tbl, ent) + end + end + + --get the children if any + if self:HasChildren() then + for _,v in ipairs(self:GetChildrenList()) do + local can_add = false + local ent = v:GetOwner() + + --we're not gonna add parts that don't have a specific reason to be haloed or that don't at least group up some haloable models + --because the table.insert function has a processing load on the memory, and so is halo-drawing + if (v.ClassName == "model" or v.ClassName == "model2" or v.ClassName == "jiggle") then + can_add = true + else can_add = false end + if (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then + can_add = false + end + if can_add then table.insert(tbl, ent) end + end end end - if #tbl > 0 then - local pulse = math.sin(pac.RealTime * 20) * 0.5 + 0.5 - pulse = pulse * 255 - pac.haloex.Add(tbl, Color(pulse, pulse, pulse, 255), 1, 1, 1, true, true, 5, 1, 1) + last_tbl = tbl + last_root_ent = self:GetRootPart():GetOwner() + last_checked_partUID = self.UniqueID + + DrawHaloHighlight(tbl) + + --also refresh the bulk-selected nodes' labels because pace.RefreshTree() resets their alphas, but I want to keep the fade because it indicates what's being bulk-selected + if not skip then timer.Simple(0.3, function() BulkSelectRefreshFadedNodes(self) end) end + end + + function BulkSelectRefreshFadedNodes(part_trace) + if refresh_halo_hook then return end + if part_trace then + for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do + v.pace_tree_node:SetAlpha( 255 ) + end + end + + for _,v in ipairs(BulkSelectList) do + v.pace_tree_node:SetAlpha( 150 ) + end + end + + function RebuildBulkHighlight() + local parts_tbl = {} + local ents_tbl = {} + local hover_tbl = {} + local ent = {} + + --get potential entities and part-children from each parent in the bulk list + for _,v in pairs(BulkSelectList) do --this will get parts + + if (v == v:GetRootPart()) then --if this is the root part, send the entity + table.insert(ents_tbl,v:GetRootPart():GetOwner()) + table.insert(parts_tbl,v) + else + table.insert(parts_tbl,v) + end + + for _,child in ipairs(v:GetChildrenList()) do --now do its children + table.insert(parts_tbl,child) + end + end + + --check what parts are candidates we can give to halo + for _,v in ipairs(parts_tbl) do + local can_add = false + if (v.ClassName == "model" or v.ClassName == "model2") then + can_add = true + end + if (v.ClassName == "group") or (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then + can_add = false + end + if can_add then + table.insert(hover_tbl, v:GetOwner()) + end + end + + table.Add(hover_tbl,ents_tbl) + --TestPrintTable(hover_tbl, "hover_tbl") + + last_bulk_select_tbl = hover_tbl + end + + function TestPrintTable(tbl, tbl_name) + MsgC(Color(200,255,200), "TABLE CONTENTS:" .. tbl_name .. " = {\n") + for _,v in pairs(tbl) do + MsgC(Color(200,255,200), "\t", tostring(v), ", \n") + end + MsgC(Color(200,255,200), "}\n") + end + + function ThinkBulkHighlight() + if table.IsEmpty(BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + return end + DrawHaloHighlight(last_bulk_select_tbl) + end + + function DrawHaloHighlight(tbl) + if (type(tbl) ~= "table") then return end + if not pace.Active then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + end + + --Find out the color and apply the halo + local color_string = GetConVar("pac_hover_color"):GetString() + local pulse_rate = math.min(math.abs(GetConVar("pac_hover_pulserate"):GetFloat()), 100) + local pulse = math.sin(SysTime() * pulse_rate) * 0.5 + 0.5 + if pulse_rate == 0 then pulse = 1 end + local pulseamount + + local halo_color + + if color_string == "rave" then + halo_color = Color(255*((0.33 + SysTime() * pulse_rate/20)%1), 255*((0.66 + SysTime() * pulse_rate/20)%1), 255*((SysTime() * pulse_rate/20)%1), 255) + pulseamount = 8 + elseif color_string == "ocean" then + halo_color = Color(0, 80 + 30*(pulse), 200 + 50*(pulse) * 0.5 + 0.5, 255) + pulseamount = 4 + elseif color_string == "rainbow" then + --halo_color = Color(255*(0.5 + 0.5*math.sin(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*-math.cos(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*math.sin(1 + pac.RealTime * pulse_rate/20)), 255) + halo_color = HSVToColor(SysTime() * 360 * pulse_rate/20, 1, 1) + pulseamount = 4 + elseif #string.Split(color_string, " ") == 3 then + halo_color_tbl = string.Split( color_string, " " ) + for i,v in ipairs(halo_color_tbl) do + if not isnumber(tonumber(halo_color_tbl[i])) then halo_color_tbl[i] = 0 end + end + halo_color = Color(pulse*halo_color_tbl[1],pulse*halo_color_tbl[2],pulse*halo_color_tbl[3],255) + pulseamount = 4 + else + halo_color = Color(255,255,255,255) + pulseamount = 2 + end + --print("using", halo_color, "blurs=" .. 2, "amount=" .. pulseamount) + + pac.haloex.Add(tbl, halo_color, 2, 2, pulseamount, true, true, pulseamount, 1, 1) + --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) end end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 574c99876..f8646236f 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -5,10 +5,13 @@ function PANEL:Init() local pnl = vgui.Create("DPropertySheet", self) pnl:Dock(FILL) - local props = pace.FillWearSettings(pnl) - - pnl:AddSheet("Wear / Ignore", props) + local properties_filter = pace.FillWearSettings(pnl) + + pnl:AddSheet("Wear / Ignore", properties_filter) self.sheet = pnl + + --local properties_shortcuts = pace.FillShortcutSettings(pnl) + --pnl:AddSheet("Editor Shortcuts", properties_shortcuts) end vgui.Register( "pace_settings", PANEL, "DPanel" ) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 40efcff14..3f805bcc9 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -1,3 +1,123 @@ +CreateClientConVar( "pac_focus_input1", "", true, false, "Set the first key for the custom shorctut of pac3 editor focus. Format according to internal names of the keys as console binds e.g. e or ctrl" ) +CreateClientConVar( "pac_focus_input2", "", true, false, "Set the second key for the custom shorctut of pac3 editor focus. Format according to internal names of the keys as console binds e.g. e or ctrl" ) +concommand.Add( "pac_toggle_focus", function() pace.Call("ToggleFocus") end) +concommand.Add( "pac_focus", function() pace.Call("ToggleFocus") end) + +local last_recorded_combination + +local focusKeyPrimary +local focusKeySecondary + +local ShortcutActions + +ShortcutActions = {} +ShortcutActions["wear"] = {0} +ShortcutActions["save"] = {1,2,3} +ShortcutActions["focus"] = {67,83} +ShortcutActions["copy"] = {0} +ShortcutActions["paste"] = {0} +ShortcutActions["cut"] = {0} +ShortcutActions["delete"] = {0} +ShortcutActions["expand_all"] = {0} +ShortcutActions["collapse_all"] = {0} +ShortcutActions["undo"] = {0} +ShortcutActions["redo"] = {0} + +concommand.Add( "pac_echo_shortcut", function() + timer.Simple( 3, function() + surface.PlaySound("buttons/button1.wav") + inputs = get_all_inputs() + print("inputs:") + printed_list = "" + input_list = {} + for k,v in pairs(inputs) do + printed_list = printed_list .. "key" .. k .. ", (code " .. v .. ", named " .. input.GetKeyName(v) .. ")\n" + input_list[k] = v + end + print(printed_list) + print(unpack(input_list)) + last_recorded_combination = input_list + end + ) +end) +--[[ +concommand.Add( "pac_assign_shortcut", function() + local action_name = "focus" + ShortcutActions[action_name] = last_recorded_combination + print("assigned "..action_name.." for ") + print(unpack(ShortcutActions[action_name])) +end) + + + + +concommand.Add( "pac_echo_shortcut_megatable", function() + for k,v in ipairs(ShortcutActions) do + unpacked_combo_string = "" + for k2,v2 in ipairs(ShortcutActions[k][2]) do + if (ShortcutActions[k][2][k2] ~= nil) then + unpacked_combo_string = unpacked_combo_string .. ShortcutActions[k][2][k2] .. "," + end + end + print(ShortcutActions[k][1] .. " " .. unpacked_combo_string) + end + + test_combos = { + {1,2}, + {1,3,2}, + {1,2,3}, + {0}, + {4}, + {54,57}, + } + + print("the match between " .. "{1,2}" .. " and {\"save\",{1,2,3}} is ", matches_input(test_combos[1], "save")) + +end) + +function get_all_inputs() + list = {} + count = 1 + for key=1,BUTTON_CODE_LAST do + if input.IsKeyDown(key) then + list[count] = key + count = count + 1 + end + end + return list +end + + +function matches_input(combo_in, action_name) + local target = ShortcutActions[action_name][2] + print("combo_in length is ", #combo_in, ", target length is ", #target) + if #combo_in ~= #target.length then return false end + full_match = true + print("trying to match") + for k,v in pairs(combo_in) do + if ((combo_in[v] == nil) or (target[v] == nil)) then + full_match = false + else + if (combo_in[v] == target[v]) then + print("matched " .. target[v]) + else + full_match = false + end + end + end + return full_match +end +]]-- + + + --[[thinkUndo() + thinkCopy() + thinkPaste() + thinkCut() + thinkDelete() + thinkExpandAll() + thinkCollapseAll()]]-- + function pace.OnShortcutSave() if not IsValid(pace.current_part) then return end @@ -17,8 +137,23 @@ function pace.OnShortcutWear() end local last = 0 +local last_print_time = CurTime() function pace.CheckShortcuts() + --[[ + if input.IsKeyDown(KEY_H) then + if last_print_time + 1 < CurTime() then + surface.PlaySound("buttons/button9.wav") + print("input report!") + print(unpack(get_all_inputs())) + print("done at time of "..last_print_time.."\n") + --chat.print("input report!\n"..unpack(get_all_inputs()).."\ndone at time of "..last_print_time) + last_print_time = CurTime() + end + end]]-- + focusKeyPrimary = input.GetKeyCode(GetConVar("pac_focus_input1"):GetString()) + focusKeySecondary = input.GetKeyCode(GetConVar("pac_focus_input2"):GetString()) + if gui.IsConsoleVisible() then return end if not pace.Editor or not pace.Editor:IsValid() then return end if last > RealTime() or input.IsMouseDown(MOUSE_LEFT) then return end @@ -32,6 +167,18 @@ function pace.CheckShortcuts() pace.Call("ToggleFocus") last = RealTime() + 0.2 end + -- can make new hardcoded custom shortcuts for focus + --[[if input.IsKeyDown(KEY_LSHIFT) and input.IsKeyDown(KEY_R) then + pace.Call("ToggleFocus") + last = RealTime() + 0.2 + end]]-- + --convar custom inputs + --if not ((focusKeyPrimary == -1) and (focusKeySecondary == -1)) then + if input.IsKeyDown(focusKeyPrimary) and input.IsKeyDown(focusKeySecondary) then + pace.Call("ToggleFocus") + last = RealTime() + 0.2 + end + --end if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then RunConsoleCommand("pac_restart") @@ -67,6 +214,31 @@ function pace.CheckShortcuts() end end +--[[ +function pace.FillShortcutSettings(pnl) + local list = vgui.Create("DCategoryList", pnl) + list:Dock(FILL) + do + local cat = list:Add(L"Wear") + cat.Header:SetSize(40,40) + cat.Header:SetFont("DermaLarge") + local list = vgui.Create("DListLayout") + list:DockPadding(20,20,20,20) + cat:SetContents(list) + + local mode = vgui.Create("DComboBox", list) + + mode.OnSelect = function(_, _, value) + if IsValid(mode.form) then + mode.form:Remove() + end + mode.form:SetParent(list) + end + end + return list +end +]]-- + pac.AddHook("Think", "pace_shortcuts", pace.CheckShortcuts) do diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index f772ebc9a..6c415af9c 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -1,4 +1,4 @@ - +include("pac3/editor/client/parts.lua") local L = pace.LanguageString function pace.IsPartSendable(part) @@ -34,6 +34,12 @@ function pace.ClearParts() pace.ClearUndo() pac.RemoveAllParts(true, true) pace.RefreshTree() + + --clean up the bulk selection + pace.ClearBulkList() + BulkSelectList = nil + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + refresh_halo_hook = true timer.Simple(0.1, function() if not pace.Editor:IsValid() then return end diff --git a/lua/pac3/editor/server/wear.lua b/lua/pac3/editor/server/wear.lua index f1a21e19e..3b1bd343f 100644 --- a/lua/pac3/editor/server/wear.lua +++ b/lua/pac3/editor/server/wear.lua @@ -334,8 +334,6 @@ function pace.HandleReceivedData(ply, data) data.owner = ply data.uid = pac.Hash(ply) - pac.Message("Received pac data from ", ply, " with ", data.totalParts or 0, " parts") - if data.wear_filter and #data.wear_filter > game.MaxPlayers() then pac.Message("Player ", ply, " tried to submit extraordinary wear filter size of ", #data.wear_filter, ", dropping.") data.wear_filter = nil diff --git a/lua/pac3/extra/shared/init.lua b/lua/pac3/extra/shared/init.lua index 78de1c3e9..4728c341f 100644 --- a/lua/pac3/extra/shared/init.lua +++ b/lua/pac3/extra/shared/init.lua @@ -2,6 +2,7 @@ include("hands.lua") include("pac_weapon.lua") include("projectiles.lua") +include("net_combat.lua") local cvar = CreateConVar("pac_restrictions", "0", FCVAR_REPLICATED) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua new file mode 100644 index 000000000..6c538c102 --- /dev/null +++ b/lua/pac3/extra/shared/net_combat.lua @@ -0,0 +1,368 @@ +local grab_consents = {} +local damage_zone_consents = {} + +local damage_types = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamageInfo + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, +} + +if SERVER then + util.AddNetworkString("pac_hitscan") + util.AddNetworkString("pac_request_position_override_on_entity") + util.AddNetworkString("pac_request_angle_reset_on_entity") + util.AddNetworkString("pac_request_velocity_force_on_entity") + util.AddNetworkString("pac_request_zone_damage") + util.AddNetworkString("pac_signal_player_combat_consent") + util.AddNetworkString("pac_request_player_combat_consent_update") + + net.Receive("pac_hitscan", function(len,ply) + print("WE SHOULD DO A BULLET IN THE SERVER!") + ent = net.ReadEntity() + bulletinfo = net.ReadTable() + print("hitscan!", ent) + PrintTable(bulletinfo) + ent:FireBullets(bulletinfo) + end) + + net.Receive("pac_request_zone_damage", function(len,ply) + --print("message from ",ply) + local pos = net.ReadVector() + local ang = net.ReadAngle() + local tbl = net.ReadTable() + local ply_ent = net.ReadEntity() + local dmg_info = DamageInfo() + dmg_info:SetDamage(tbl.Damage) + dmg_info:IsBulletDamage(tbl.Bullet) + dmg_info:SetDamageForce(Vector(0,0,0)) + dmg_info:SetAttacker(ply_ent) + dmg_info:SetInflictor(ply_ent) + --print("entity: ",ply_ent) + if tbl.OverrideKnockback then print("should override") end + + dmg_info:SetDamageType(damage_types[tbl.DamageType]) --print(tbl.DamageType .. " resolves to " .. damage_types[tbl.DamageType]) + + local ratio + if tbl.Radius == 0 then ratio = tbl.Length + else ratio = math.abs(tbl.Length / tbl.Radius) end + + if tbl.HitboxMode == "Sphere" then + local ents_hits = ents.FindInSphere(pos, tbl.Radius) + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "Box" then + local mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Radius) + local maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius) + local ents_hits = ents.FindInBox(mins, maxs) + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then + local ents_hits = {} + if tbl.Radius ~= 0 then + local sides = tbl.Detail + if tbl.Detail < 1 then sides = 1 end + local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + end + steps = math.max(steps + math.abs(tbl.ExtraSteps),1) + + --print("steps",steps, "total casts will be "..steps*self.Detail) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + local startpos = pos + x + y + local endpos = pos + ang:Forward()*tbl.Length + x + y + table.Merge(ents_hits, ents.FindAlongRay(startpos, endpos)) + end + end + if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then + --fast sphere check on the wide end + if tbl.Length/tbl.Radius >= 2 then + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius), tbl.Radius)) + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Radius, tbl.Radius)) + if tbl.Radius ~= 0 then + local counter = 0 + for i=math.floor(tbl.Length / tbl.Radius) - 1,1,-1 do + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Radius*i), tbl.Radius)) + if counter == 100 then break end + counter = counter + 1 + end + end + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + end + elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "CylinderSpheres" then + local ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + table.Inherit(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then + local ents_hits = {} + if tbl.Radius ~= 0 then + local sides = tbl.Detail + if tbl.Detail < 1 then sides = 1 end + local startpos = pos-- + Vector(0, self.Radius,self.Radius) + local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + end + steps = math.max(steps + math.abs(tbl.ExtraSteps),1) + --print("steps",steps, "total casts will be "..steps*self.Detail) + local timestart = SysTime() + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + + phase = math.random() + --print("ring " .. ringnumber .. " phase " .. phase) + for i=1,0,-1/sides do + --print("radius " .. tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize)) + if ringnumber == 0 then i = 0 end + x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + local endpos = pos + ang:Forward()*tbl.Length + x + y + table.Inherit(ents_hits,ents.FindAlongRay(startpos, endpos)) + end + end + if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then + --fast sphere check on the wide end + local radius_multiplier = math.atan(math.abs(ratio)) / (1.5 + 0.1*math.sqrt(ratio)) + if ratio > 0.5 then + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius * radius_multiplier), tbl.Radius * radius_multiplier)) + end + end + elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "ConeSpheres" then + local ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + --PrintTable(ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + for i = 0,1/8,1/128 do + --PrintTable(ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + if tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + + if tbl.Bullet then + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + dmg_info:GetInflictor():FireBullets(bullet) + end + end + + end) + + function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + if #ents_hits == 0 then + if tbl.Bullet then + dmg_info:GetInflictor():FireBullets(bullet) + end + return + end + + for _,ent in pairs(ents_hits) do + if IsEntity(ent) then + if (not tbl.AffectSelf) and ent == dmg_info:GetInflictor() then --nothing + elseif (ent:IsPlayer() and tbl.Players) or (ent:IsNPC() and tbl.NPC) or (ent:GetClass() == "prop_physics") then + --local oldvel = ent:GetVelocity() + local ents2 = {dmg_info:GetInflictor()} + if tbl.Bullet then + for _,v in ipairs(ents_hits) do + if v ~= ent then table.insert(ents2,v) end + end + end + + if tbl.DamageType == "heal" then + ent:SetHealth(math.min(ent:Health() + tbl.Damage, ent:GetMaxHealth())) + elseif tbl.DamageType == "armor" then + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, ent:GetMaxArmor())) + else + if tbl.Bullet then + traceresult = util.TraceLine({filter = ents2, start = pos, endpos = pos + 50000*(ent:WorldSpaceCenter() - dmg_info:GetAttacker():WorldSpaceCenter())}) + --print(traceresult.Fraction) + bullet.Dir = traceresult.Normal + bullet.Src = traceresult.HitPos + traceresult.HitNormal*5 + dmg_info:GetInflictor():FireBullets(bullet) + end + if ent:IsPlayer() then + if (ent == dmg_info:GetInflictor() and tbl.AffectSelf) then + ent:TakeDamageInfo(dmg_info) + print(ent, "hurt themself") + elseif damage_zone_consents[ent] == true then + ent:TakeDamageInfo(dmg_info) + print(dmg_info:GetAttacker(), "hurt", ent) + end + else + ent:TakeDamageInfo(dmg_info) + print(dmg_info:GetAttacker(), "hurt", ent) + end + end + --local newvel = ent:GetVelocity() + --ent:SetVelocity( oldvel - newvel) + end + end + end + end + + net.Receive("pac_request_position_override_on_entity", function(len, ply) + + local pos = net.ReadVector() + local ang = net.ReadAngle() + local override_ang = net.ReadBool() + local targ_ent = net.ReadEntity() + local auth_ent = net.ReadEntity() + + if targ_ent:EntIndex() == 0 then return end + if targ_ent ~= auth_ent and grab_consents[targ_ent] == false then return end + + if override_ang and not targ_ent:IsPlayer() then + targ_ent:SetAngles(ang) + elseif override_ang and (auth_ent == targ_ent) and targ_ent:IsPlayer() then + targ_ent:SetEyeAngles(ang) + end + targ_ent:SetPos(pos) + + if targ_ent:IsPlayer() then targ_ent:SetVelocity(-targ_ent:GetVelocity()) end + end) + + net.Receive("pac_request_angle_reset_on_entity", function(len, ply) + local ang = net.ReadAngle() + local delay = net.ReadFloat() + local targ_ent = net.ReadEntity() + local auth_ent = net.ReadEntity() + + targ_ent:SetAngles(ang) + end) + + net.Receive("pac_request_velocity_force_on_entity", function(len,ply) + + end) + + net.Receive("pac_signal_player_combat_consent", function(len,ply) + mode = net.ReadString() + b = net.ReadBool() + --print("message from", ply, "consent for",mode,"is",b) + if mode == "grab" then + grab_consents[ply] = b + --PrintTable(grab_consents) + elseif mode == "damage_zone" then + damage_zone_consents[ply] = b + --PrintTable(grab_consents) + elseif mode == "all" then + grab_consents[ply] = b + damage_zone_consents[ply] = b + end + end) + + concommand.Add("pac_refresh_consents", function() + pac_combat_RefreshConsents() + end) + + function pac_combat_RefreshConsents() + for _,ent in pairs(ents.GetAll()) do + if ent:IsPlayer() then + net.Start("pac_request_player_combat_consent_update") + net.Send(ent) + print(ent, "does that player consent grabs?", grab_consents[ent], "and damage zone?", damage_zone_consents[ent]) + end + end + PrintTable(grab_consents) + end + + +end + +if CLIENT then + CreateConVar("pac_client_grab_consent", "0", true, true) + CreateConVar("pac_client_damage_zone_consent", "0", true, true) + net.Receive("pac_request_player_combat_consent_update", function() + print("player receives request to update consents") + net.Start("pac_signal_player_combat_consent") + net.WriteString("grab") + net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) + net.SendToServer() + + net.Start("pac_signal_player_combat_consent") + net.WriteString("damage_zone") + net.WriteBool(GetConVar("pac_client_damage_zone_consent"):GetBool()) + net.SendToServer() + end) +end + diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index 7189ad6f1..e87752bbf 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -417,6 +417,7 @@ if SERVER then end pace.suppress_prop_spawn = nil + local multi_projectile_count = net.ReadUInt(7) local pos = net.ReadVector() local ang = net.ReadAngle() local part = net.ReadTable() @@ -453,7 +454,7 @@ if SERVER then end if projectile_count > 50 then - pac.Message("Player ", ply, " has more than 50 projectiles spawned!") + pac.Message("Player ", ply, " has more than 50 projectiles spawned! No more will be spawned until some expire.") return end @@ -494,10 +495,57 @@ if SERVER then ent.pac_projectile_owner = ply end - if part.Delay == 0 then - spawn() + local function multispawn() + if not ply:IsValid() then return end + + ply.pac_projectiles = ply.pac_projectiles or {} + + local projectile_count = 0 + for ent in pairs(ply.pac_projectiles) do + if ent:IsValid() then + projectile_count = projectile_count + 1 + else + ply.pac_projectiles[ent] = nil + end + end + + local remaining_projectile_slots = math.max(50 - projectile_count,0) + + if (multi_projectile_count > remaining_projectile_slots) then + if remaining_projectile_slots == 0 then + --block the spawns + pac.Message("Player ", ply, " has 50 projectiles spawned! No more will be spawned until some expire.") + goto CONTINUE + else + --adjust the spawn to just the limit + pac.Message("Player ", ply, " will spawn only ",remaining_projectile_slots," projectiles to prevent going over-limit") + multi_projectile_count = remaining_projectile_slots + end + + end + if part.Maximum > 0 and projectile_count >= part.Maximum then + return + end + + for i = multi_projectile_count - 1, 0, -1 do + spawn() + end + + ::CONTINUE:: + end + + if multi_projectile_count == 1 then + if part.Delay == 0 then + spawn() + else + timer.Simple(part.Delay, spawn) + end else - timer.Simple(part.Delay, spawn) + if part.Delay == 0 then + multispawn() + else + timer.Simple(part.Delay, multispawn) + end end end) end From 7f4b0ddcadd939b72918e1a3bd55bd7e15e695b8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 9 Apr 2023 22:32:14 -0400 Subject: [PATCH 002/300] update branch --- lua/pac3/core/client/parts/command.lua | 6 + lua/pac3/core/client/parts/damage_zone.lua | 389 +++++++++ lua/pac3/core/client/parts/event.lua | 73 +- lua/pac3/core/client/parts/hitscan.lua | 139 +++ .../client/parts/interpolated_multibone.lua | 167 ++++ lua/pac3/core/client/parts/legacy/entity.lua | 2 +- lua/pac3/core/client/parts/lock.lua | 233 +++++ lua/pac3/core/client/parts/particles.lua | 29 +- lua/pac3/core/client/parts/player_config.lua | 2 +- lua/pac3/core/client/parts/projectile.lua | 18 +- lua/pac3/core/client/parts/proxy.lua | 116 ++- lua/pac3/core/client/parts/text.lua | 84 +- lua/pac3/core/server/init.lua | 1 + lua/pac3/core/shared/util.lua | 22 +- lua/pac3/editor/client/examples.lua | 805 +++++------------- lua/pac3/editor/client/panels/editor.lua | 2 +- .../editor/client/panels/extra_properties.lua | 29 +- lua/pac3/editor/client/parts.lua | 500 ++++++++++- lua/pac3/editor/client/settings.lua | 9 +- lua/pac3/editor/client/shortcuts.lua | 172 ++++ lua/pac3/editor/client/wear.lua | 8 +- lua/pac3/extra/shared/init.lua | 1 + lua/pac3/extra/shared/net_combat.lua | 368 ++++++++ lua/pac3/extra/shared/projectiles.lua | 56 +- 24 files changed, 2520 insertions(+), 711 deletions(-) create mode 100644 lua/pac3/core/client/parts/damage_zone.lua create mode 100644 lua/pac3/core/client/parts/hitscan.lua create mode 100644 lua/pac3/core/client/parts/interpolated_multibone.lua create mode 100644 lua/pac3/core/client/parts/lock.lua create mode 100644 lua/pac3/extra/shared/net_combat.lua diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index f62eff8dd..83f60c02c 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -51,6 +51,12 @@ function PART:ShouldHighlight(str) return _G[str] ~= nil end +function PART:GetNiceName() + if self.UseLua then return ("lua: " .. self.String) end + return "command: " .. self.String + +end + local sv_allowcslua = GetConVar("sv_allowcslua") function PART:Execute() diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua new file mode 100644 index 000000000..83a6a69d0 --- /dev/null +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -0,0 +1,389 @@ +local BUILDER, PART = pac.PartTemplate("base_movable") + +PART.ClassName = "damage_zone" +PART.Group = 'advanced' +PART.Icon = 'icon16/package.png' + +local renderhooks = { + "PostDraw2DSkyBox", + "PostDrawOpaqueRenderables", + "PostDrawSkyBox", + "PostDrawTranslucentRenderables", + "PostDrawViewModel", + "PostPlayerDraw", + "PreDrawEffects", + "PreDrawHalos", + "PreDrawOpaqueRenderables", + "PreDrawSkyBox", + "PreDrawTranslucentRenderables", + "PreDrawViewModel" +} + + +BUILDER:StartStorableVars() + :SetPropertyGroup("Targets") + :GetSet("AffectSelf",false) + :GetSet("Players",true) + :GetSet("NPC",true) + :SetPropertyGroup("Shape and Sampling") + :GetSet("Radius", 20) + :GetSet("Length", 50) + :GetSet("HitboxMode", "Box", {enums = { + ["Box"] = "Box", + ["Sphere"] = "Sphere", + ["Cylinder (Raycasts Only)"] = "Cylinder", + ["Cylinder (Hybrid)"] = "CylinderHybrid", + ["Cylinder (From Spheres)"] = "CylinderSpheres", + ["Cone (Raycasts Only)"] = "Cone", + ["Cone (Hybrid)"] = "ConeHybrid", + ["Cone (From Spheres)"] = "ConeSpheres", + ["Ray"] = "Ray" + }}) + :GetSet("Detail", 20) + :GetSet("ExtraSteps",0) + :GetSet("RadialRandomize", 1) + :GetSet("PhaseRandomize", 1) + :SetPropertyGroup("Behaviour") + :GetSet("Delay", 0) + :GetSet("OverrideKnockback", true) + :GetSet("KnockbackAmount", Vector(0,0,0)) + :SetPropertyGroup("Preview Rendering") + :GetSet("NoPreview", false) + :GetSet("RenderingHook", "PostDrawOpaqueRenderables", {enums = { + ["PostDraw2DSkyBox"] = "PostDraw2DSkyBox", + ["PostDrawOpaqueRenderables"] = "PostDrawOpaqueRenderables", + ["PostDrawSkyBox"] = "PostDrawSkyBox", + ["PostDrawTranslucentRenderables"] = "PostDrawTranslucentRenderables", + ["PostDrawViewModel"] = "PostDrawViewModel", + ["PostPlayerDraw"] = "PostPlayerDraw", + ["PreDrawEffects"] = "PreDrawEffects", + ["PreDrawHalos"] = "PreDrawHalos", + ["PreDrawOpaqueRenderables"] = "PreDrawOpaqueRenderables", + ["PreDrawSkyBox"] = "PreDrawSkyBox", + ["PreDrawTranslucentRenderables"] = "PreDrawTranslucentRenderables", + ["PreDrawViewModel"] = "PreDrawViewModel" + }}) + :SetPropertyGroup("DamageInfo") + :GetSet("Bullet", true) + :GetSet("Damage", 0) + :GetSet("DamageType", "generic", {enums = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamage + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + }}) +BUILDER:EndStorableVars() + +function PART:OnShow() + if not self.NoPreview then + self:PreviewHitbox() + end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + local tbl = {} + for key in pairs(self:GetStorableVars()) do + tbl[key] = self[key] + end + net.Start("pac_request_zone_damage") + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + net.WriteTable(tbl) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() +end + +function PART:OnHide() + --self:BuildCylinder() + hook.Remove(self.RenderingHook, "pace_draw_hitbox") + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox") + end +end + +function PART:OnRemove() + --self:BuildCylinder() + hook.Remove(self.RenderingHook, "pace_draw_hitbox") + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox") + end +end + +local previousRenderingHook + +function PART:PreviewHitbox() + if previousRenderingHook ~= self.RenderingHook then + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox") + end + previousRenderingHook = self.RenderingHook + end + + hook.Add(self.RenderingHook, "pace_draw_hitbox", function() + self:GetWorldPosition() + if self.HitboxMode == "Box" then + local mat = Matrix() + mat:Rotate(self:GetWorldAngles()) + --mat:Rotate(Angle(SysTime()*100,0,0)) + local mins = Vector(-self.Radius, -self.Radius, -self.Radius) + local maxs = Vector(self.Radius, self.Radius, self.Radius) + render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0) --[[mat:GetAngles()]], mins, maxs, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Sphere" then + render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Cylinder" or self.HitboxMode == "CylinderHybrid" then + local obj = Mesh() + self:BuildCylinder(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if LocalPlayer() == self:GetPlayerOwner() then + if self.Radius ~= 0 then + local sides = self.Detail + if self.Detail < 1 then sides = 1 end + + local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + end + steps = math.max(steps + math.abs(self.ExtraSteps),1) + + --print("steps",steps, "total casts will be "..steps*self.Detail) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + y = self:GetWorldAngles():Up() *math.sin(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + local startpos = self:GetWorldPosition() + x + y + local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y + render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false ) + end + end + if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then + --fast sphere check on the wide end + if self.Length/self.Radius >= 2 then + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - self.Radius), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Radius), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if self.Radius ~= 0 then + local counter = 0 + for i=math.floor(self.Length / self.Radius) - 1,1,-1 do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Radius*i), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if counter == 100 then break end + counter = counter + 1 + end + end + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + end + elseif self.Radius == 0 then render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) end + end + elseif self.HitboxMode == "CylinderSpheres" then + local obj = Mesh() + self:BuildCylinder(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Length ~= 0 and self.Radius ~= 0 then + local counter = 0 + --render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + for i=0,1,1/(math.abs(self.Length/self.Radius)) do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if counter == 200 then break end + counter = counter + 1 + end + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Cone" or self.HitboxMode == "ConeHybrid" then + local obj = Mesh() + self:BuildCone(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if LocalPlayer() == self:GetPlayerOwner() then + if self.Radius ~= 0 then + local sides = self.Detail + if self.Detail < 1 then sides = 1 end + local startpos = self:GetWorldPosition() + local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + end + steps = math.max(steps + math.abs(self.ExtraSteps),1) + + --print("steps",steps, "total casts will be "..steps*self.Detail) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + y = self:GetWorldAngles():Up() *math.sin(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y + render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false ) + end + end + if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then + --fast sphere check on the wide end + local radius_multiplier = math.atan(math.abs(self.Length/self.Radius)) / (1.5 + 0.1*math.sqrt(self.Length/self.Radius)) + if self.Length/self.Radius > 0.5 then + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - self.Radius * radius_multiplier), self.Radius * radius_multiplier, 10, 10, Color( 255, 255, 255 ) ) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + end + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + end + elseif self.HitboxMode == "ConeSpheres" then + local obj = Mesh() + self:BuildCone(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Radius ~= 0 then + local steps + steps = math.Clamp(4*math.ceil(self.Length / (self.Radius or 1)),1,50) + for i = 1,0,-1/steps do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + + steps = math.Clamp(math.ceil(self.Length / (self.Radius or 1)),1,4) + for i = 0,1/8,1/128 do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Ray" then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + end) + timer.Simple(5, function() if self.NoPreview then hook.Remove(self.RenderingHook, "pace_draw_hitbox") end end) +end + +function PART:BuildCylinder(obj) + local sides = 30 + local circle_tris = {} + for i=1,sides,1 do + local vert1 = {pos = Vector(0, self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(0, self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + local vert3 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert4 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + --print(vert1.pos,vert3.pos,vert2.pos,vert4.pos) + --{vert1,vert2,vert3} + --{vert4,vert3,vert2} + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + + table.insert(circle_tris, vert4) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert4) + + --circle_tris[8*(i-1) + 1] = vert1 + --circle_tris[8*(i-1) + 2] = vert2 + --circle_tris[8*(i-1) + 3] = vert3 + --circle_tris[8*(i-1) + 4] = vert4 + --circle_tris[8*(i-1) + 5] = vert3 + --circle_tris[8*(i-1) + 6] = vert2 + end + obj:BuildFromTriangles( circle_tris ) +end + +function PART:BuildCone(obj) + local sides = 30 + local circle_tris = {} + local verttip = {pos = Vector(0,0,0), u = 0, v = 0 } + for i=1,sides,1 do + local vert1 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + --print(vert1.pos,vert3.pos,vert2.pos,vert4.pos) + --{vert1,vert2,vert3} + --{vert4,vert3,vert2} + table.insert(circle_tris, verttip) + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + table.insert(circle_tris, verttip) + + --circle_tris[8*(i-1) + 1] = vert1 + --circle_tris[8*(i-1) + 2] = vert2 + --circle_tris[8*(i-1) + 3] = vert3 + --circle_tris[8*(i-1) + 4] = vert4 + --circle_tris[8*(i-1) + 5] = vert3 + --circle_tris[8*(i-1) + 6] = vert2 + end + obj:BuildFromTriangles( circle_tris ) +end + + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index ae785353d..4ba8f4f1c 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -26,7 +26,7 @@ BUILDER:StartStorableVars() return output end}) BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end}) - BUILDER:GetSet("Arguments", "", {hidden = true}) + BUILDER:GetSet("Arguments", "", {hidden = false}) BUILDER:GetSet("Invert", true) BUILDER:GetSet("RootOwner", true) BUILDER:GetSet("AffectChildrenOnly", false) @@ -35,12 +35,12 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:SetEvent(event) - local reset = (self.Arguments == "") or + local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) self.Event = event self:SetWarning() - self:GetDynamicProperties(reset) + self:GetDynamicProperties(reset) end local function get_default(typ) @@ -95,8 +95,8 @@ function PART:GetDynamicProperties(reset_to_default) } local arg = tbl[key] - if arg.get() == nil or reset_to_default then - if udata.default then + if arg.get() == nil or reset_to_default then + if udata.default then arg.set(udata.default) else arg.set(nil) @@ -204,6 +204,10 @@ PART.OldEvents = { timerx = { arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}}, + userdata = { + {default = 0, timerx_property = "seconds"}, + {default = true, timerx_property = "reset_on_hide"} + }, nice = function(self, ent, seconds) return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?" end, @@ -490,8 +494,16 @@ PART.OldEvents = { ranger = { arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}}, - userdata = {{editor_panel = "ranger", ranger_property = "distance"}, {editor_panel = "ranger", ranger_property = "compare"}}, + userdata = { + {default = 15, ranger_property = "distance"}, + {default = 5, ranger_property = "compare"}, + {default = false, ranger_property = "npcs_and_players_only"} + }, callback = function(self, ent, distance, compare, npcs_and_players_only) + if npcs_and_players_only == nil then + self.Arguments = self.Arguments .. "@@0" + self.npcs_and_players_only = false + end local parent = self:GetParentEx() if parent:IsValid() and parent.GetWorldPosition then @@ -572,12 +584,45 @@ PART.OldEvents = { endpos = startpos, maxs = maxs, mins = mins, - filter = ent + filter = {self:GetRootPart():GetOwner(),ent} + } ) + return tr.Hit + end, + }, + + is_touching_scalable = { + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}}, + userdata = {{editor_panel = "is_touching"}, {x = "x_stretch"}, {y = "y_stretch"}, {z = "z_stretch"}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch) + extra_radius = extra_radius or 0 + x_stretch = x_stretch or 1 + y_stretch = y_stretch or 1 + z_stretch = z_stretch or 1 + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + local startpos = ent:WorldSpaceCenter() + + radius = math.max(extra_radius, 1) + mins = mins * radius + maxs = maxs * radius + + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {self:GetRootPart():GetOwner(),ent} } ) return tr.Hit end, }, + is_explicit = { + callback = function(self, ent) + return GetConVar("pac_hide_disturbing"):GetBool() + end + }, + is_in_noclip = { callback = function(self, ent) ent = try_viewmodel(ent) @@ -824,8 +869,8 @@ PART.OldEvents = { command = { arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}}, userdata = { - {default = "change_me", editor_friendly = "CommandName"}, - {default = 0.1, editor_friendly = "EventDuration"}, + {default = "change_me", editor_friendly = "CommandName"}, + {default = 0.1, editor_friendly = "EventDuration"}, {default = false, group = "event wheel", editor_friendly = "HideInEventWheel"} }, nice = function(self, ent, find, time) @@ -834,6 +879,14 @@ PART.OldEvents = { return "command: " .. find .. " | " .. "duration: " .. time end, callback = function(self, ent, find, time) + if time == nil then + self.Arguments = self.Arguments .. "@@0" + time = 0 + end + if hide_in_eventwheel == nil then + self.Arguments = self.Arguments .. "@@0" + hide_in_eventwheel = false + end time = time or 0.1 local ply = self:GetPlayerOwner() @@ -2207,4 +2260,4 @@ do concommand.Add("+pac_events", pac.openEventSelectionWheel) concommand.Add("-pac_events", pac.closeEventSelectionWheel) -end +end \ No newline at end of file diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua new file mode 100644 index 000000000..c201509b8 --- /dev/null +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -0,0 +1,139 @@ +language.Add("pac_hitscan", "Hitscan") +local ent +local bulletinfo = {} +--local vector_origin = vector_origin +--local angle_origin = Angle(0,0,0) +--local WorldToLocal = WorldToLocal + +local BUILDER, PART = pac.PartTemplate("base_drawable") + +PART.ClassName = "hitscan" +PART.Group = 'advanced' +PART.Icon = 'icon16/user_gray.png' + +BUILDER:StartStorableVars() + :GetSet("ServerBullets",true) + :SetPropertyGroup("bullet properties") + :GetSet("BulletImpact", false) + :GetSet("Damage", 0) + :GetSet("Force",1000) + :GetSet("DamageType", "generic", {enums = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamage + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + } + }) + :GetSet("Spread", 0) + :GetSet("SpreadX", 1) + :GetSet("SpreadY", 1) + :GetSet("NumberBullets", 1) + :GetSet("TracerSparseness", 1) + :GetSet("MaxDistance", 10000) + :GetSet("TracerName", "Tracer", {enums = { + ["Default bullet tracer"] = "Tracer", + ["AR2 pulse-rifle tracer"] = "AR2Tracer", + ["Helicopter tracer"] = "HelicopterTracer", + ["Airboat gun tracer"] = "AirboatGunTracer", + ["Airboat gun heavy tracer"] = "AirboatGunHeavyTracer", + ["Gauss tracer"] = "GaussTracer", + ["Hunter tracer"] = "HunterTracer", + ["Strider tracer"] = "StriderTracer", + ["Gunship tracer"] = "GunshipTracer", + ["Toolgun tracer"] = "ToolTracer", + ["Laser tracer"] = "LaserTracer" + }}) +BUILDER:EndStorableVars() + +function PART:OnShow() + self:UpdateBulletInfo() + self:GetWorldPosition() + self:GetWorldAngles() + self:Shoot(self:GetDrawPosition()) +end + +function PART:OnDraw() + self:GetWorldPosition() + self:GetWorldAngles() +end + +function PART:Shoot(pos, ang) + bulletinfo.Tracer = self.TracerSparseness + bulletinfo.Src = pos + bulletinfo.Dir = ang:Forward() + bulletinfo.Spread = Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0) + + ent = self:GetOwner() + if self.ServerBullets then + print("WE NEED A BULLET IN THE SERVER!") + net.Start("pac_hitscan") + net.WriteEntity(ent) + net.WriteTable(bulletinfo) + net.SendToServer() + else + ent:FireBullets(bulletinfo) + end +end + +function PART:UpdateBulletInfo() + bulletinfo.Attacker = ent + if self.Damage == 0 then + else bulletinfo.Damage = self.Damage end + + bulletinfo.Force = self.Force + bulletinfo.Distance = self.MaxDistance + bulletinfo.Num = self.NumberBullets + bulletinfo.Tracer = self.TracerSparseness --tracer every x bullets + bulletinfo.TracerName = self.TracerName + + --[[bulletinfo.ammodata = { + name = "" + dmgtype = self.DamageType + tracer = TRACER_LINE_AND_WHIZ + maxcarry = -2 + + }]]-- +end + + +BUILDER:Register() + diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua new file mode 100644 index 000000000..29849056c --- /dev/null +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -0,0 +1,167 @@ +nodes = {} + +local cam_PushModelMatrix = cam.PushModelMatrix +local cam_PopModelMatrix = cam.PopModelMatrix +local Vector = Vector +local Angle = Angle +local EF_BONEMERGE = EF_BONEMERGE +local NULL = NULL +local Color = Color +local Matrix = Matrix +local vector_origin = vector_origin + +local BUILDER, PART = pac.PartTemplate("base_movable") + +PART.ClassName = "interpolated_multibone" +PART.Group = 'advanced' +PART.Icon = 'icon16/table_multiple.png' +PART.is_model_part = false + +PART.ManualDraw = true +PART.HandleModifiersManually = true + +BUILDER:StartStorableVars() + :SetPropertyGroup("test") + :GetSet("Test1", false) + :GetSet("Test2", false) + :GetSet("Force000", false) + :SetPropertyGroup("Interpolation") + :GetSet("LerpValue",0) + :GetSet("InterpolatePosition", true) + :GetSet("InterpolateAngles", true) + :SetPropertyGroup("Nodes") + :GetSetPart("Node1") + :GetSetPart("Node2") + :GetSetPart("Node3") + :GetSetPart("Node4") + :GetSetPart("Node5") +:EndStorableVars() + +--PART:GetWorldPosition() +--PART:GetWorldAngles() +function PART:Initialize() + print("a multiboner is born") + + self.pos = Vector() + self.vel = Vector() + + self.ang = Angle() + self.angvel = Angle() + --[[] + self:SetLerpValue(self.LerpValue or 0) + self:SetInterpolatePosition(self.InterpolatePosition or true) + self:SetInterpolateAngles(self.InterpolateAngles or true)]] +end + +function PART:OnDraw() + self:GetWorldPosition() + self:GetWorldAngles() + --self:ModifiersPreEvent("OnDraw") + --self:ModifiersPostEvent("OnDraw") +end + +function PART:OnThink() + self:OnDraw() + + if self.Force000 then + print("forcing 0 0 0 world position") + self:SetWorldPos(0,0,0) + end + --self:GetWorldPosition() + --self:GetWorldAngles() + --self:GetDrawPosition() + --print(self.pos.x, self.pos.y, self.pos.z) + --print(self:GetDrawPosition().x, self:GetDrawPosition().y, self:GetDrawPosition().z) +end + +function PART:SetWorldPos(x,y,z) + self.pos.x = x + self.pos.y = y + self.pos.z = z +end + +function PART:Interpolate(stage, proportion) +end + +--we need to know the stage and proportion (progress) +--e.g. lerp 0.5 is stage 0, proportion 0.5 because it's 50% toward Node 1 +--e.g. lerp 2.2 is stage 2, proportion 0.2 because it's 20% toward Node 3 +function PART:GetInterpolationParameters() + --[[stage = math.max(0,math.floor(self.LerpValue)) + proportion = math.max(0,self.LerpValue) % 1 + print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1)) + print("proportion is " .. proportion) + return stage, proportion]]-- +end + +function PART:GetNodeAngle(nodenumber) + --print("node" .. nodenumber .. " angle " .. self.__['Node'..nodenumber].Angles) + --print("node" .. nodenumber .. " world angle " .. self.__['Node'..nodenumber]:GetWorldAngles()) + + --return self.Node1:GetWorldAngles() +end + +function PART:GetNodePosition(nodenumber) + --print("node" .. nodenumber .. " position " .. self.__['Node'..nodenumber].Position) + --print("node" .. nodenumber .. " world position " .. self.__['Node'..nodenumber]:GetWorldPosition()) + --return self.Node1:GetWorldPosition() +end + +function PART:InterpolateFromLerp(lerp) +end + +function PART:InterpolateFromNodes(firstnode, secondnode, proportion) + --position_interpolated = InterpolateFromStage("position", stage, self.Lerp) +end + +function PART:InterpolateFromStage(stage, proportion) + self:InterpolateFromNodes(stage, stage + 1) +end + +function PART:InterpolateAngle() + +end + + + +--[[function PART:ApplyMatrix() + print("MATRIX???") + local ent = self:GetOwner() + if not ent:IsValid() then return end + + local mat = Matrix() + + if self.ClassName ~= "model2" then + mat:Translate(self.Position + self.PositionOffset) + mat:Rotate(self.Angles + self.AngleOffset) + end + + if mat:IsIdentity() then + ent:DisableMatrix("RenderMultiply") + else + ent:EnableMatrix("RenderMultiply", mat) + end +end--]] + +function PART:SetLerpValue(var) + --[[print("adjusted lerp value. "..type(var).." "..var) + self.LerpValue = var + assert(self.LerpValue == var) + assert(self.LerpValue ~= nil) + self:Interpolate(self:GetInterpolationParameters())]]-- +end + +function PART:SetInterpolatePosition(b) + --print(type(b).." "..b) + self.InterpolatePosition = b +end + +function PART:SetInterpolateAngles(b) + --print(type(b).." "..b) + self.InterpolateAngles = b +end + + + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/legacy/entity.lua b/lua/pac3/core/client/parts/legacy/entity.lua index 8c3166643..3490e1302 100644 --- a/lua/pac3/core/client/parts/legacy/entity.lua +++ b/lua/pac3/core/client/parts/legacy/entity.lua @@ -89,7 +89,7 @@ function BUILDER:EntityField(name, field) end BUILDER:EntityField("InverseKinematics", "enable_ik") -BUILDER:EntityField("MuteFootsteps", "hide_weapon") +BUILDER:EntityField("MuteFootsteps", "pac_mute_footsteps") BUILDER:EntityField("AnimationRate", "global_animation_rate") BUILDER:EntityField("RunSpeed", "run_speed") diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua new file mode 100644 index 000000000..b0949f2ec --- /dev/null +++ b/lua/pac3/core/client/parts/lock.lua @@ -0,0 +1,233 @@ +include("pac3/extra/shared/net_combat.lua") +--pac3/extra/shared/net_combat.lua + + + +local target_ent = nil +local pac = pac +local Vector = Vector +local Angle = Angle +local NULL = NULL +local Matrix = Matrix + +local BUILDER, PART = pac.PartTemplate("base_movable") + +PART.ClassName = "lock" +PART.Group = 'advanced' +PART.Icon = 'icon16/lock.png' + + +BUILDER:StartStorableVars() + :SetPropertyGroup("Conditions") + :GetSet("Players", false) + :GetSet("PhysicsProps", false) + :GetSet("NPC", false) + :SetPropertyGroup("DetectionOrigin") + :GetSet("Radius", 20) + :GetSet("RadiusOffsetDown", false, {description = "Lowers the detect origin by the radius distance"}) + :GetSetPart("TargetPart") + :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."}) + :SetPropertyGroup("Behaviour") + :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"}) + :GetSet("RelativeGrab", false) + :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before grabbing are re-applied"}) + :GetSet("Mode", "None", {enums = {["None"] = "None", ["Grab"] = "Grab", ["Teleport"] = "Teleport"}}) +BUILDER:EndStorableVars() + +local valid_ent = false +local grabbing = false +local last_request_time = SysTime() +local last_entsearch = SysTime() +local default_ang = Angle(0,0,0) + +function PART:OnThink() + + self:GetWorldPosition() + self:GetWorldAngles() + if self.Mode == "Grab" then + if not valid_ent and self.ContinuousSearch then --no hit and can search = search more and try the move later + self:DecideTarget() + self:CheckEntValidity() + return + elseif not valid_ent and not self.ContinuousSearch then --if initial think failed to find and can't search = stop + --print("end of the line. ", not valid_ent, not self.ContinuousSearch, not valid_ent and not self.ContinuousSearch) + return + end + end + --good hit and can search = search more and try the move later + self:CheckEntValidity() + + --self:DecideTarget() + if self.Mode == "Grab" then + if not grabbing and not self.OverrideAngles then default_ang = self.target_ent:GetAngles() end + + relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity() + if not self.RelativeGrab then + relative_transform_matrix = Matrix() + relative_transform_matrix:Identity() + end + + offset_matrix = Matrix() + offset_matrix:Translate(self:GetWorldPosition()) + offset_matrix:Rotate(self:GetWorldAngles()) + offset_matrix:Mul(relative_transform_matrix) + + local relative_offset_pos = offset_matrix:GetTranslation() + local relative_offset_ang = offset_matrix:GetAngles() + + net.Start("pac_request_position_override_on_entity") + net.WriteVector(relative_offset_pos) + net.WriteAngle(relative_offset_ang) + local can_rotate = self.OverrideAngles + if self.target_ent:IsPlayer() then can_rotate = false end + net.WriteBool(can_rotate) + net.WriteEntity(self.target_ent) + net.WriteEntity(self:GetRootPart():GetOwner()) + --print(self:GetRootPart():GetOwner()) + net.SendToServer() + if self.Players and self.target_ent:IsPlayer() then + local mat = Matrix() + mat:Identity() + mat:Rotate(Angle(0,270,0)) + mat:Rotate(self:GetWorldAngles()) + self.target_ent:EnableMatrix("RenderMultiply", mat) + end + last_request_time = SysTime() + grabbing = true + teleported = false + elseif self.Mode == "Teleport" and not teleported then + + self.target_ent = nil + net.Start("pac_request_position_override_on_entity") + net.WriteVector(self:GetWorldPosition()) + local ang_yaw_only = self:GetWorldAngles() + ang_yaw_only.p = 0 + ang_yaw_only.r = 0 + net.WriteAngle(ang_yaw_only) + net.WriteBool(self.OverrideAngles) + net.WriteEntity(self:GetPlayerOwner()) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() + self:GetPlayerOwner():SetAngles( ang_yaw_only ) + + teleported = true + grabbing = false + end +end + +--continuous : keep trying until hit +--not : try only once + + +function PART:OnShow() + self.target_ent = nil + self.relative_transform_matrix = Matrix():Identity() + self:DecideTarget() + self:CheckEntValidity() + self:CalculateRelativeOffset() +end + +function PART:OnHide() + teleported = false + grabbing = false + if self.target_ent == nil then return end + timer.Simple(math.min(self.RestoreDelay,5), function() + if self.target_ent == nil then return end + if self.target_ent:IsValid() then + net.Start("pac_request_angle_reset_on_entity") + net.WriteAngle(default_ang) + net.WriteFloat(self.RestoreDelay) + net.WriteEntity(self.target_ent) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() + if self.Players then + self.target_ent:DisableMatrix("RenderMultiply") + end + end + end) +end + +function PART:OnRemove() +end + +function PART:DecideTarget() + --print("search") + local ents = ents.GetAll() + local ents_candidates = {} + local chosen_ent = nil + --filter entities + for i, ent_candidate in ipairs(ents) do + --print(ent_candidate:GetClass()) + if ent_candidate:IsValid() then + local origin + + if self.TargetPart and self.TargetPart:IsValid() then + origin = self.TargetPart:GetWorldPosition() + else + origin = self:GetWorldPosition() + end + + if self.RadiusOffsetDown then origin:Add(Vector(0,0,-self.Radius)) end + + if ent_candidate:GetPos():Distance( origin ) < self.Radius then + if self.Players and ent_candidate:IsPlayer() then + --we don't want to grab ourselves + if (self:GetPlayerOwner() ~= self:GetRootPart():GetOwner()) then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + elseif (self:GetPlayerOwner() ~= ent_candidate) then --if it's another player, good + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + end + elseif self.PhysicsProps and ent_candidate:GetClass() == "prop_physics" then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + elseif self.NPC and ent_candidate:IsNPC() then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + end + end + end + end + local closest_distance = math.huge + + --sort for the closest + for i,ent_candidate in ipairs(ents_candidates) do + --print("trying", ent_candidate, ent_candidate:GetClass(), (ent_candidate:GetPos()):Distance( self:GetWorldPosition()), " from part") + local test_distance = (ent_candidate:GetPos()):Distance( self:GetWorldPosition()) + if (test_distance < closest_distance) then + closest_distance = test_distance + chosen_ent = ent_candidate + end + end + + if chosen_ent ~= nil then + self.target_ent = chosen_ent + print("selected ", chosen_ent, "dist ", (chosen_ent:GetPos()):Distance( self:GetWorldPosition() )) + valid_ent = true + else + self.target_ent = nil + valid_ent = false + end +end + +function PART:CheckEntValidity() + + if self.target_ent == nil then + valid_ent = false + elseif self.target_ent:EntIndex() == 0 then + valid_ent = false + elseif self.target_ent:IsValid() then + valid_ent = true + end +end + +function PART:CalculateRelativeOffset() + if self.target_ent == nil then self.relative_transform_matrix = Matrix() return end + self.relative_transform_matrix = Matrix() + self.relative_transform_matrix:Rotate(self.target_ent:GetAngles() - self:GetWorldAngles()) + self.relative_transform_matrix:Translate(self.target_ent:GetPos() - self:GetWorldPosition()) + print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles()) +end + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/particles.lua b/lua/pac3/core/client/parts/particles.lua index 38c166445..828e910c0 100644 --- a/lua/pac3/core/client/parts/particles.lua +++ b/lua/pac3/core/client/parts/particles.lua @@ -17,6 +17,7 @@ BUILDER:StartStorableVars() BUILDER:PropertyOrder("ParentName") BUILDER:GetSet("Follow", false) BUILDER:GetSet("Additive", false) + BUILDER:GetSet("FireOnce", false) BUILDER:GetSet("FireDelay", 0.2) BUILDER:GetSet("NumberParticles", 1) BUILDER:GetSet("PositionSpread", 0) @@ -36,6 +37,9 @@ BUILDER:StartStorableVars() BUILDER:GetSet("StickEndSize", 0) BUILDER:GetSet("StickStartAlpha", 255) BUILDER:GetSet("StickEndAlpha", 0) + BUILDER:SetPropertyGroup("attract") + BUILDER:GetSet("AttractPart") + BUILDER:GetSet("AttractForce", 0) BUILDER:SetPropertyGroup("appearance") BUILDER:GetSet("Material", "effects/slime1") BUILDER:GetSet("StartAlpha", 255) @@ -49,6 +53,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("DoubleSided", true) BUILDER:GetSet("DrawManual", false) BUILDER:SetPropertyGroup("rotation") + BUILDER:GetSet("ZeroAngle",true) BUILDER:GetSet("RandomRollSpeed", 0) BUILDER:GetSet("RollDelta", 0) BUILDER:GetSet("ParticleAngleVelocity", Vector(50, 50, 50)) @@ -141,6 +146,7 @@ function PART:Set3D(b) end function PART:OnShow(from_rendering) + self.CanKeepFiring = true if not from_rendering then self.NextShot = 0 local pos, ang = self:GetDrawPosition() @@ -206,10 +212,11 @@ function PART:SetMaterial(var) end function PART:EmitParticles(pos, ang, real_ang) + if not self.FireOnce then self.CanKeepFiring = true end local emt = self:GetEmitter() if not emt then return end - if self.NextShot < pac.RealTime then + if self.NextShot < pac.RealTime and self.CanKeepFiring then if self.Material == "" then return end if self.Velocity == 500.01 then return end @@ -222,7 +229,11 @@ function PART:EmitParticles(pos, ang, real_ang) end for _ = 1, self.NumberParticles do - + local mats = self.Material:Split(";") + if #mats > 1 then + self.Materialm = pac.Material(table.Random(mats), self) + self:CallRecursive("OnMaterialChanged") + end local vec = Vector() if self.Spread ~= 0 then @@ -306,14 +317,15 @@ function PART:EmitParticles(pos, ang, real_ang) particle:SetRoll(self.RandomRollSpeed * 36) end - if self.RollDelta ~= 0 then + if self.RollDelta ~= 0 then particle:SetRollDelta(self.RollDelta + roll) end - + particle:SetAirResistance(self.AirResistance) particle:SetBounce(self.Bounce) particle:SetGravity(self.Gravity) - particle:SetAngles(particle:GetAngles() + self.ParticleAngle) + if self.ZeroAngle then particle:SetAngles(Angle(0,0,0)) + else particle:SetAngles(particle:GetAngles() + self.ParticleAngle) end particle:SetLighting(self.Lighting) if not self.Follow then @@ -346,7 +358,12 @@ function PART:EmitParticles(pos, ang, real_ang) end end - self.NextShot = pac.RealTime + self.FireDelay + if self.FireDelay == 0 then + self.CanKeepFiring = false + else + self.NextShot = pac.RealTime + self.FireDelay + self.CanKeepFiring = true + end end end diff --git a/lua/pac3/core/client/parts/player_config.lua b/lua/pac3/core/client/parts/player_config.lua index ccfde805f..77468adc6 100644 --- a/lua/pac3/core/client/parts/player_config.lua +++ b/lua/pac3/core/client/parts/player_config.lua @@ -56,7 +56,7 @@ function BUILDER:EntityField(name, field) end BUILDER:EntityField("InverseKinematics", "enable_ik") -BUILDER:EntityField("MuteFootsteps", "hide_weapon") +BUILDER:EntityField("MuteFootsteps", "pac_mute_footsteps") BUILDER:EntityField("AnimationRate", "global_animation_rate") BUILDER:EntityField("FallApartOnDeath", "death_physics_parts") BUILDER:EntityField("DeathRagdollizeParent", "death_ragdollize") diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index b5b35b4d8..073b0c458 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -68,11 +68,12 @@ BUILDER:StartStorableVars() } }) BUILDER:GetSet("Spread", 0) + BUILDER:GetSet("NumberProjectiles", 1) BUILDER:GetSet("Delay", 0) BUILDER:GetSet("Maximum", 0) BUILDER:GetSet("Mass", 100) BUILDER:GetSet("Attract", 0) - BUILDER:GetSet("AttractMode", "projectile_nearest", {enums = { + BUILDER:GetSet("AttractMode", "closest_to_projectile", {enums = { hitpos = "hitpos", hitpos_radius = "hitpos_radius", closest_to_projectile = "closest_to_projectile", @@ -84,6 +85,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("CollideWithOwner", false) BUILDER:GetSet("CollideWithSelf", false) BUILDER:GetSet("RemoveOnCollide", false) + BUILDER:EndStorableVars() PART.Translucent = false @@ -103,7 +105,11 @@ function PART:OnShow(from_rendering) part:Draw("opaque") end end - self:Shoot(self:GetDrawPosition()) + if self.NumberProjectiles <= 0 then self.NumberProjectiles = 0 end + if self.NumberProjectiles <= 50 then + local pos,ang = self:GetDrawPosition() + self:Shoot(pos,ang,self.NumberProjectiles) + else chat.AddText(Color(255,0,0),"[PAC3] Trying to spawn too many projectiles! The limit is " .. 50) end end end @@ -147,8 +153,9 @@ end local enable = CreateClientConVar("pac_sv_projectiles", 0, true) -function PART:Shoot(pos, ang) +function PART:Shoot(pos, ang, multi_projectile_count) local physics = self.Physical + local multi_projectile_count = multi_projectile_count or 1 if physics then if pac.LocalPlayer ~= self:GetPlayerOwner() then return end @@ -159,6 +166,7 @@ function PART:Shoot(pos, ang) end net.Start("pac_projectile") + net.WriteUInt(multi_projectile_count,7) net.WriteVector(pos) net.WriteAngle(ang) net.WriteTable(tbl) @@ -284,7 +292,9 @@ function PART:Shoot(pos, ang) end if self.Delay == 0 then - spawn() + for i = multi_projectile_count,1,-1 do + spawn() + end else timer.Simple(self.Delay, spawn) end diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 014fbb7d4..3f826cdc8 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -458,6 +458,77 @@ do -- end end + + + +--[[ +self.truevel_ent = nil +self.truevel_last_ent = nil +self.truevel_next_log = 0 +do --true velocity (tm) + function seek_past_neighboring_timestamp() + local offset = math.floor(self.VelocityRoughness) + local begin_seek_position = math.floor(SysTime() * 100) + if self.truevel_ent.position_timestamps[begin_seek_position - offset] ~= nil then + return self.truevel_ent.position_timestamps[begin_seek_position - offset] + else + local latest_past_timestamp + for stamp,logged_pos in ipairs(self.truevel_ent.position_timestamps) do + if self.truevel_ent.position_timestamps[stamp] ~= nil then + if stamp < begin_seek_position - offset then + + end + end + end + end + end + + function PART:GetTrueVelocity() + self.true_vel = self.true_vel or Vector() + local systime = SysTime() + local logtime = math.floor(systime * 100) + self.truevel_next_log = self.truevel_next_log or systime + if systime < self.truevel_next_log then return self.true_vel + else self.truevel_next_log = systime + 0.05 end + print("log time ", logtime) + if self.truevel_ent then self.truevel_ent.position_timestamps = ent.position_timestamps or {} end + + self.last_pos = seek_past_neighboring_timestamp() + + local pos + + if self.RootOwner then + self.truevel_ent = self:GetRootPart():GetOwner() + pos = self:GetRootPart():GetOwner():GetPos() + elseif self.truevel_ent ~= nil then + self.truevel_ent = self:GetOwner() + pos = self:GetOwner():GetWorldPosition() or self:GetOwner():GetPos() + end + + self.truevel_last_ent = self.truevel_ent + self.truevel_ent.position_timestamps[logtime] = pos + + self.true_vel = pos - self.truevel_ent.position_timestamps[seek_past_neighboring_timestamp()] + + return self.true_vel or Vector() + end + + PART.Inputs.owner_true_velocity_length = function(self) + + if self:GetPhysicalTarget():IsValid() then + ent = self:GetPhysicalTarget() + end + if self.RootOwner then + ent = self:GetRootPart():GetOwner() + end + return self:GetTrueVelocity():Length() + end +end +]] + + + + do -- velocity PART.Inputs.parent_velocity_length = function(self) return self:GetVelocity(self:GetPhysicalTarget()):Length() @@ -510,12 +581,23 @@ end PART.Inputs.pose_parameter = function(self, name) if not name then return 0 end - local owner = get_owner(self) + local owner = self:GetPlayerOwner() if owner:IsValid() and owner.GetPoseParameter then return owner:GetPoseParameter(name) end return 0 end +PART.Inputs.pose_parameter_true = function(self, name) + if not name then return 0 end + local owner = self:GetPlayerOwner() + if owner:IsValid() then + mini,maxi = owner:GetPoseParameterRange(owner:LookupPoseParameter(name)) + actual_value = mini + (maxi - mini)*(owner:GetPoseParameter(name)) + return actual_value + else end + return 0 +end + PART.Inputs.command = function(self) local ply = self:GetPlayerOwner() if ply.pac_proxy_events then @@ -755,38 +837,6 @@ do end end -PART.Inputs.flat_dot_forward = function(self) - local part = get_owner(self) - - if part:IsValid() then - local ang = part:IsPlayer() and part:EyeAngles() or part:GetAngles() - ang.p = 0 - ang.r = 0 - local dir = pac.EyePos - part:EyePos() - dir[3] = 0 - dir:Normalize() - return dir:Dot(ang:Forward()) - end - - return 0 -end - -PART.Inputs.flat_dot_right = function(self) - local part = get_owner(self) - - if part:IsValid() then - local ang = part:IsPlayer() and part:EyeAngles() or part:GetAngles() - ang.p = 0 - ang.r = 0 - local dir = pac.EyePos - part:EyePos() - dir[3] = 0 - dir:Normalize() - return dir:Dot(ang:Right()) - end - - return 0 -end - net.Receive("pac_proxy", function() local ply = net.ReadEntity() local str = net.ReadString() diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index ce5116ae9..258bcfdcd 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -7,7 +7,7 @@ local DisableClipping = DisableClipping local render_CullMode = render.CullMode local cam_End3D2D = cam.End3D2D local cam_End3D = cam.End3D -local TEXT_ALIGN_CENTER = TEXT_ALIGN_CENTER +--local Text_Align = TEXT_ALIGN_CENTER local surface_SetFont = surface.SetFont local Color = Color @@ -18,9 +18,36 @@ PART.Group = 'effects' PART.Icon = 'icon16/text_align_center.png' BUILDER:StartStorableVars() - BUILDER:GetSet("Text", "") - BUILDER:GetSet("Font", "default") - BUILDER:GetSet("Size", 1, {editor_sensitivity = 0.25}) + :SetPropertyGroup("generic") + :PropertyOrder("Name") + :PropertyOrder("Hide") + :GetSet("Text", "") + :GetSet("Font", "default") + :GetSet("Size", 1, {editor_sensitivity = 0.25}) + + + :SetPropertyGroup("text layout") + :GetSet("HorizontalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Left"] = "0", ["Center"] = "1", ["Right"] = "2"}}) + :GetSet("VerticalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Center"] = "1", ["Top"] = "3", ["Bottom"] = "4"}}) + :GetSet("ConcatenateTextAndOverrideValue",false,{editor_friendly = "CombinedText"}) + + :SetPropertyGroup("data source") + :GetSet("TextOverride", "Text", {enums = { + ["Text"] = "Text", + ["Health"] = "Health", + ["Maximum Health"] = "MaxHealth", + ["Armor"] = "Armor", + ["Maximum Armor"] = "MaxArmor", + ["Timerx"] = "Timerx", + ["CurTime"] = "CurTime", + ["RealTime"] = "RealTime", + ["Clip current Ammo"] = "Ammo", + ["Clip Size"] = "ClipSize", + ["Ammo Reserve"] = "AmmoReserve", + ["Proxy value (Using DynamicTextValue)"] = "Proxy"}}) + :GetSet("DynamicTextValue", 0) + + :SetPropertyGroup("appearance") BUILDER:GetSet("Outline", 0) BUILDER:GetSet("Color", Vector(255, 255, 255), {editor_panel = "color"}) BUILDER:GetSet("Alpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}}) @@ -34,7 +61,9 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:GetNiceName() - return '"' .. self:GetText() .. '"' + if self.TextOverride ~= "Text" then return self.TextOverride end + + return 'Text: "' .. self:GetText() .. '"' end function PART:SetColor(v) @@ -73,6 +102,7 @@ end function PART:SetFont(str) if not pcall(surface_SetFont, str) then + pac.Message(Color(255,150,0),"[PAC3] "..str.." Font not found! Reverting to DermaDefault!") str = "DermaDefault" end @@ -81,16 +111,45 @@ end function PART:OnDraw() local pos, ang = self:GetDrawPosition() + local DisplayText = self.Text or "" + if self.TextOverride == "Text" then goto DRAW end + + if self.TextOverride == "Health"then DisplayText = self:GetPlayerOwner():Health() + elseif self.TextOverride == "MaxHealth" then + DisplayText = self:GetPlayerOwner():GetMaxHealth() + elseif self.TextOverride == "Ammo" then + DisplayText = self:GetPlayerOwner():GetActiveWeapon():Clip1() + elseif self.TextOverride == "ClipSize" then + DisplayText = self:GetPlayerOwner():GetActiveWeapon():GetMaxClip1() + elseif self.TextOverride == "AmmoReserve" then + DisplayText = self:GetPlayerOwner():GetAmmoCount(self:GetPlayerOwner():GetActiveWeapon():GetPrimaryAmmoType()) + elseif self.TextOverride == "Armor" then + DisplayText = self:GetPlayerOwner():Armor() + elseif self.TextOverride == "MaxArmor" then + DisplayText = self:GetPlayerOwner():GetMaxArmor() + elseif self.TextOverride == "Timerx" then + DisplayText = ""..math.Round(CurTime() - self.time,2) + elseif self.TextOverride == "CurTime" then + DisplayText = ""..math.Round(CurTime(),2) + elseif self.TextOverride == "RealTime" then + DisplayText = ""..math.Round(RealTime(),2) + elseif self.TextOverride == "Proxy" then + --print(type(self.DynamicTextValue)) + DisplayText = ""..math.Round(self.DynamicTextValue,2) + end - if self.Text ~= "" then + if self.ConcatenateTextAndOverrideValue then DisplayText = ""..self.Text..DisplayText end + + ::DRAW:: + if DisplayText ~= "" then cam_Start3D(EyePos(), EyeAngles()) cam_Start3D2D(pos, ang, self.Size) local oldState = DisableClipping(true) - draw_SimpleTextOutlined(self.Text, self.Font, 0,0, self.ColorC, TEXT_ALIGN_CENTER,TEXT_ALIGN_CENTER, self.Outline, self.OutlineColorC) + draw_SimpleTextOutlined(DisplayText, self.Font, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) render_CullMode(1) -- MATERIAL_CULLMODE_CW - draw_SimpleTextOutlined(self.Text, self.Font, 0,0, self.ColorC, TEXT_ALIGN_CENTER,TEXT_ALIGN_CENTER, self.Outline, self.OutlineColorC) + draw_SimpleTextOutlined(DisplayText, self.Font, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) render_CullMode(0) -- MATERIAL_CULLMODE_CCW DisableClipping(oldState) @@ -99,6 +158,15 @@ function PART:OnDraw() end end +function PART:OnThink() + self:OnDraw() +end + +function PART:OnShow() + self.time = CurTime() + self:OnDraw() +end + function PART:SetText(str) self.Text = str end diff --git a/lua/pac3/core/server/init.lua b/lua/pac3/core/server/init.lua index 4e60b8688..7abebcfee 100644 --- a/lua/pac3/core/server/init.lua +++ b/lua/pac3/core/server/init.lua @@ -17,6 +17,7 @@ include("pac3/core/shared/init.lua") include("effects.lua") include("event.lua") include("net_messages.lua") +include("net_combat.lua") include("test_suite_backdoor.lua") include("in_skybox.lua") diff --git a/lua/pac3/core/shared/util.lua b/lua/pac3/core/shared/util.lua index 37e4d0cce..6e8e7d0a9 100644 --- a/lua/pac3/core/shared/util.lua +++ b/lua/pac3/core/shared/util.lua @@ -135,8 +135,6 @@ for _, params in pairs(shader_params.base) do end end -texture_keys["include"] = "include" - -- for pac_restart PAC_MDL_SALT = PAC_MDL_SALT or 0 @@ -465,7 +463,6 @@ function pac.DownloadMDL(url, callback, onfail, ply) table.insert(found_vmt_directories, {dir = dir}) f:seek(old_pos) end - table.sort(found_vmt_directories, function(a,b) return #a.dir>#b.dir end) f:seek(old_pos) end @@ -604,7 +601,7 @@ function pac.DownloadMDL(url, callback, onfail, ply) end for shader_param in pairs(texture_keys) do - data.buffer = data.buffer:gsub('("?%$?%f[%w_]' .. shader_param .. '%f[^%w_]"?%s+"?)([^"%c]+)("?%s?)', function(l, vtf_path, r) + data.buffer = data.buffer:gsub('("?%$' .. shader_param .. '"?%s+")(.-)(")', function(l, vtf_path, r) if vtf_path == "env_cubemap" then return end @@ -621,19 +618,12 @@ function pac.DownloadMDL(url, callback, onfail, ply) end end - if not new_path then - for _, info in ipairs(files) do + for _, info in ipairs(files) do + if info.file_name:EndsWith(".vtf") then local vtf_name = (vtf_path:match(".+/(.+)") or vtf_path) - if info.file_name:EndsWith(".vtf") then - if info.file_name == vtf_name .. ".vtf" or info.file_name == vtf_name then - new_path = dir .. vtf_name - break - end - elseif (info.file_name:EndsWith(".vmt") and l:StartWith("include")) then - if info.file_name == vtf_name then - new_path = "materials/" .. dir .. vtf_name - break - end + if info.file_name == vtf_name .. ".vtf" then + new_path = dir .. vtf_name + break end end end diff --git a/lua/pac3/editor/client/examples.lua b/lua/pac3/editor/client/examples.lua index d6b399c25..c940787f1 100644 --- a/lua/pac3/editor/client/examples.lua +++ b/lua/pac3/editor/client/examples.lua @@ -1931,631 +1931,238 @@ pace.example_outfits["skis"] = { } pace.example_outfits["southpark"] = { - [1] = { - ["children"] = { - [1] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["Jiggle"] = false, - ["DrawOrder"] = 0, - ["UniqueID"] = "3513732839", - ["TargetEntityUID"] = "", - ["AimPartName"] = "", - ["FollowPartUID"] = "", - ["Bone"] = "head", - ["ScaleChildren"] = false, - ["AngleOffset"] = Angle(0, 0, 0), - ["MoveChildrenToOrigin"] = false, - ["Position"] = Vector(0, 0, 0), - ["AimPartUID"] = "", - ["Angles"] = Angle(0, 0, 0), - ["Hide"] = false, - ["Name"] = "", - ["Scale"] = Vector(1, 1, 1), - ["EditorExpand"] = false, - ["ClassName"] = "bone", - ["Size"] = 0, - ["PositionOffset"] = Vector(0, 0, 0), - ["IsDisturbing"] = false, - ["AlternativeBones"] = false, - ["EyeAngles"] = false, - ["FollowAnglesOnly"] = false, - }, +[1] = { + ["children"] = { + [1] = { + ["children"] = { + [1] = { + ["children"] = { + }, + ["self"] = { + ["ClassName"] = "bone", + ["UniqueID"] = "3513732839", + ["Size"] = 0, }, - }, - ["self"] = { - ["HidePhysgunBeam"] = false, - ["Skin"] = 0, - ["UniqueID"] = "2782871480", - ["HideBullets"] = false, - ["FallApartOnDeath"] = false, - ["DeathRagdollizeParent"] = false, - ["WalkSpeed"] = 0, - ["BlendMode"] = "", - ["EyeAngles"] = false, - ["HideEntity"] = false, - ["AimPartUID"] = "", - ["Model"] = "", - ["LodOverride"] = -1, - ["Name"] = "", - ["MuteSounds"] = false, - ["AllowOggWhenMuted"] = false, - ["AngleOffset"] = Angle(0, 0, 0), - ["InverseKinematics"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["Brightness"] = 1, - ["DoubleFace"] = false, - ["IgnoreZ"] = false, - ["DrawOrder"] = 0, - ["EditorExpand"] = true, - ["HideRagdollOnDeath"] = false, - ["IsDisturbing"] = false, - ["RelativeBones"] = true, - ["TargetEntityUID"] = "", - ["DrawShadow"] = true, - ["Alpha"] = 0, - ["Material"] = "", - ["Translucent"] = false, - ["SuppressFrames"] = false, - ["CrouchSpeed"] = 0, - ["Bone"] = "head", - ["NoTextureFiltering"] = false, - ["MuteFootsteps"] = false, - ["Invert"] = false, - ["DrawWeapon"] = true, - ["Position"] = Vector(0, 0, 0), - ["DrawPlayerOnDeath"] = false, - ["Weapon"] = false, - ["Hide"] = false, - ["Angles"] = Angle(0, 0, 0), - ["Scale"] = Vector(1, 1, 1), - ["AimPartName"] = "", - ["RunSpeed"] = 0, - ["Size"] = 0.44, - ["UseLegacyScale"] = false, - ["ClassName"] = "entity", - ["AnimationRate"] = 1, - ["EyeTargetUID"] = "", - ["SprintSpeed"] = 0, }, }, - [2] = { - ["children"] = { - [1] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "3210564156", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = true, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = false, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "AngleOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - }, + ["self"] = { + ["Alpha"] = 0, + ["EditorExpand"] = true, + ["UniqueID"] = "2782871480", + ["Fullbright"] = true, + ["Size"] = 0.44, + ["ClassName"] = "entity", + }, + }, + [2] = { + ["children"] = { + [1] = { + ["children"] = { + [1] = { + ["children"] = { }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["AffectChildrenOnly"] = false, - ["DrawOrder"] = 0, - ["TargetPartUID"] = "", - ["Name"] = "", - ["Event"] = "flat_dot_right", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["RootOwner"] = true, - ["EditorExpand"] = true, - ["ClassName"] = "event", - ["Arguments"] = "-0.707", - ["Invert"] = true, - ["IsDisturbing"] = false, - ["Operator"] = "below", - ["UniqueID"] = "1222338801", - ["ZeroEyePitch"] = false, - }, + ["self"] = { + ["RootOwner"] = true, + ["UniqueID"] = "3210564156", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + ["ClassName"] = "proxy", + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "AngleOffset", }, }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = true, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "LOCALEYES_YAW", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "left", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 0.999, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 0), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = true, - ["Scale"] = Vector(1, 1, -0.0099999997764826), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 1, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(90, 90, 90), - ["TextureFilter"] = 3, - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["UniqueID"] = "537182332", + [2] = { + ["children"] = { + }, + ["self"] = { + ["Arguments"] = "-0.7", + ["UniqueID"] = "1222338801", + ["Event"] = "dot_right", + ["Operator"] = "above", + ["ClassName"] = "event", + ["EditorExpand"] = true, + }, }, }, - [2] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["AffectChildrenOnly"] = false, - ["DrawOrder"] = 0, - ["TargetPartUID"] = "", - ["Name"] = "", - ["Event"] = "flat_dot_forward", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["RootOwner"] = true, - ["EditorExpand"] = true, - ["ClassName"] = "event", - ["Arguments"] = "-0.7071067812", - ["Invert"] = true, - ["IsDisturbing"] = false, - ["Operator"] = "equal or below", - ["UniqueID"] = "34fd29c1896140b8e5b639659a278ff810f4db34e73de20b372f6d0d0f0b8bad", - ["ZeroEyePitch"] = false, - }, + ["self"] = { + ["Invert"] = true, + ["UniqueID"] = "537182332", + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["EditorExpand"] = true, + ["Name"] = "left", + ["Scale"] = Vector(1, 1, -0.0099999997764826), + ["Alpha"] = 0.999, + ["ClassName"] = "model", + ["AngleOffset"] = Angle(90, 90, 90), + ["AimPartName"] = "LOCALEYES_YAW", + ["Bone"] = "none", + ["Fullbright"] = true, + ["Translucent"] = true, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", + }, + }, + [2] = { + ["children"] = { + }, + ["self"] = { + ["EditorExpand"] = true, + ["UniqueID"] = "2385081529", + ["Expression"] = "0,0,owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or 0) or 0", + ["ClassName"] = "proxy", + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "PositionOffset", + }, + }, + [3] = { + ["children"] = { + [1] = { + ["children"] = { }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "75353876", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = true, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = true, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "AngleOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - }, + ["self"] = { + ["EditorExpand"] = true, + ["UniqueID"] = "75353876", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + ["ClassName"] = "proxy", + ["RootOwner"] = true, + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "AngleOffset", }, }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = false, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "LOCALEYES_YAW", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "back", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 0.99, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/back.png", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 0), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = true, - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 1, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(90, 90, 90), - ["TextureFilter"] = 3, - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["UniqueID"] = "2325223398", + [2] = { + ["children"] = { + }, + ["self"] = { + ["Arguments"] = "0.7", + ["UniqueID"] = "3855963026", + ["Event"] = "dot_forward", + ["Operator"] = "below", + ["ClassName"] = "event", + ["EditorExpand"] = true, + }, }, }, - [3] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["AffectChildrenOnly"] = false, - ["DrawOrder"] = 0, - ["TargetPartUID"] = "", - ["Name"] = "", - ["Event"] = "flat_dot_right", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["RootOwner"] = true, - ["EditorExpand"] = true, - ["ClassName"] = "event", - ["Arguments"] = "0.707", - ["Invert"] = true, - ["IsDisturbing"] = false, - ["Operator"] = "above", - ["UniqueID"] = "2514734784", - ["ZeroEyePitch"] = false, - }, + ["self"] = { + ["UniqueID"] = "2325223398", + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["EditorExpand"] = true, + ["Name"] = "back", + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["Alpha"] = 0.99, + ["AngleOffset"] = Angle(90, 90, 90), + ["Fullbright"] = true, + ["AimPartName"] = "LOCALEYES_YAW", + ["ClassName"] = "model", + ["Bone"] = "none", + ["Translucent"] = true, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/back.png", + }, + }, + [4] = { + ["children"] = { + [1] = { + ["children"] = { }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "2809374115", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = true, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = false, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "AngleOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - }, + ["self"] = { + ["Arguments"] = "0.7", + ["UniqueID"] = "2514734784", + ["Event"] = "dot_right", + ["Operator"] = "below", + ["ClassName"] = "event", + ["EditorExpand"] = true, }, }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = false, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "LOCALEYES_YAW", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "right", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 0.999, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 0), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = true, - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 1, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(90, 90, 90), - ["TextureFilter"] = 3, - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["UniqueID"] = "1916911126", + [2] = { + ["children"] = { + }, + ["self"] = { + ["RootOwner"] = true, + ["UniqueID"] = "2809374115", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + ["ClassName"] = "proxy", + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "AngleOffset", + }, }, }, - [4] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "2385081529", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = false, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = true, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "PositionOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "0,0,owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or 0) or 0", - }, - }, - [5] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["AffectChildrenOnly"] = false, - ["DrawOrder"] = 0, - ["TargetPartUID"] = "", - ["Name"] = "", - ["Event"] = "flat_dot_forward", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["RootOwner"] = true, - ["EditorExpand"] = true, - ["ClassName"] = "event", - ["Arguments"] = "0.7071067812", - ["Invert"] = true, - ["IsDisturbing"] = false, - ["Operator"] = "equal or above", - ["UniqueID"] = "2334772782", - ["ZeroEyePitch"] = false, - }, + ["self"] = { + ["UniqueID"] = "1916911126", + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["EditorExpand"] = true, + ["Name"] = "right", + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["Alpha"] = 0.999, + ["AngleOffset"] = Angle(90, 90, 90), + ["Fullbright"] = true, + ["AimPartName"] = "LOCALEYES_YAW", + ["ClassName"] = "model", + ["Bone"] = "none", + ["Translucent"] = true, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", + }, + }, + [5] = { + ["children"] = { + [1] = { + ["children"] = { }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "4261893074", - ["Axis"] = "", - ["Input"] = "owner_velocity_length_increase", - ["TargetPartUID"] = "", - ["InputMultiplier"] = 1, - ["RootOwner"] = false, - ["TargetEntityUID"] = "", - ["ZeroEyePitch"] = false, - ["ClassName"] = "proxy", - ["ResetVelocitiesOnHide"] = true, - ["VelocityRoughness"] = 10, - ["Max"] = 1, - ["Pow"] = 1, - ["EditorExpand"] = true, - ["AffectChildren"] = false, - ["Min"] = 0, - ["Hide"] = false, - ["Name"] = "", - ["VariableName"] = "AngleOffset", - ["Offset"] = 0, - ["PlayerAngles"] = false, - ["Additive"] = false, - ["InputDivider"] = 1, - ["IsDisturbing"] = false, - ["OutputTargetPartUID"] = "", - ["Function"] = "sin", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - }, + ["self"] = { + ["Arguments"] = "-0.7", + ["UniqueID"] = "2334772782", + ["Event"] = "dot_forward", + ["Operator"] = "above", + ["ClassName"] = "event", + ["EditorExpand"] = true, }, }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = false, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "LOCALEYES_YAW", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "front", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 0.999, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/front.png", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = true, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 0), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = true, - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 1, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(90, 90, 90), - ["TextureFilter"] = 3, - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["UniqueID"] = "2579545900", + [2] = { + ["children"] = { + }, + ["self"] = { + ["EditorExpand"] = true, + ["UniqueID"] = "4261893074", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + ["ClassName"] = "proxy", + ["Input"] = "owner_velocity_length_increase", + ["VariableName"] = "AngleOffset", + }, }, }, - }, - ["self"] = { - ["Skin"] = 0, - ["Invert"] = false, - ["LightBlend"] = 1, - ["CellShade"] = 0, - ["AimPartName"] = "", - ["IgnoreZ"] = false, - ["AimPartUID"] = "", - ["Passes"] = 1, - ["Name"] = "body", - ["Angles"] = Angle(0, 0, 0), - ["DoubleFace"] = false, - ["PositionOffset"] = Vector(0, 0, 0), - ["BlurLength"] = 0, - ["OwnerEntity"] = false, - ["Brightness"] = 1, - ["DrawOrder"] = 0, - ["BlendMode"] = "", - ["TintColor"] = Vector(0, 0, 0), - ["Alpha"] = 1, - ["LodOverride"] = -1, - ["TargetEntityUID"] = "", - ["BlurSpacing"] = 0, - ["UsePlayerColor"] = false, - ["Material"] = "", - ["UseWeaponColor"] = false, - ["EyeAngles"] = false, - ["UseLegacyScale"] = false, - ["Bone"] = "none", - ["Color"] = Vector(255, 255, 255), - ["Fullbright"] = false, - ["BoneMerge"] = false, - ["IsDisturbing"] = false, - ["Position"] = Vector(0, 0, 24), - ["NoTextureFiltering"] = false, - ["AlternativeScaling"] = false, - ["Hide"] = false, - ["Translucent"] = false, - ["Scale"] = Vector(1, 1, 1), - ["ClassName"] = "model", - ["EditorExpand"] = true, - ["Size"] = 0, - ["ModelFallback"] = "", - ["AngleOffset"] = Angle(0, 0, 0), - ["TextureFilter"] = 3, - ["Model"] = "models/pac/default.mdl", - ["UniqueID"] = "49506760", + ["self"] = { + ["UniqueID"] = "2579545900", + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["EditorExpand"] = true, + ["Name"] = "front", + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["Alpha"] = 0.999, + ["AngleOffset"] = Angle(90, 90, 90), + ["Fullbright"] = true, + ["AimPartName"] = "LOCALEYES_YAW", + ["ClassName"] = "model", + ["Bone"] = "none", + ["Translucent"] = true, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/front.png", + }, }, }, + ["self"] = { + ["UniqueID"] = "49506760", + ["ClassName"] = "model", + ["Position"] = Vector(0, 0, 24), + ["Model"] = "models/pac/default.mdl", + ["Size"] = 0, + ["Bone"] = "none", + ["Name"] = "body", + ["EditorExpand"] = true, + }, }, - ["self"] = { - ["DrawOrder"] = 0, - ["UniqueID"] = "743553614", - ["Hide"] = false, - ["TargetEntityUID"] = "", - ["EditorExpand"] = true, - ["OwnerName"] = "self", - ["IsDisturbing"] = false, - ["Name"] = "my outfit", - ["Duplicate"] = false, - ["ClassName"] = "group", - }, - }, + }, + ["self"] = { + ["EditorExpand"] = true, + ["UniqueID"] = "743553614", + ["ClassName"] = "group", + ["Name"] = "my outfit", + ["Description"] = "add parts to me!", + }, +}, } diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index dab095bdb..81a0018cd 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -481,4 +481,4 @@ function PANEL:Paint(w,h) --DFrame.Paint(self, w,h) end -pace.RegisterPanel(PANEL) +pace.RegisterPanel(PANEL) \ No newline at end of file diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 48e9dceca..690511b1f 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -714,9 +714,12 @@ do -- event is_touching if part ~= last_part then stop() return end if not part:IsValid() then stop() return end if part.ClassName ~= "event" then stop() return end - if part:GetEvent() ~= "is_touching" then stop() return end + if not (part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable") then stop() return end local extra_radius = part:GetProperty("extra_radius") or 0 + local x_stretch = part:GetProperty("x_stretch") or 1 + local y_stretch = part:GetProperty("y_stretch") or 1 + local z_stretch = part:GetProperty("z_stretch") or 1 local ent if part.RootOwner then ent = part:GetRootPart():GetOwner() @@ -725,28 +728,36 @@ do -- event is_touching end if not IsValid(ent) then stop() return end - local radius = ent:BoundingRadius() + local radius if radius == 0 and IsValid(ent.pac_projectile) then radius = ent.pac_projectile:GetRadius() end - radius = math.max(radius + extra_radius + 1, 1) + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + + if part:GetEvent() == "is_touching" then + radius = math.max(ent:BoundingRadius() + extra_radius + 1, 1) + mins = mins * radius + maxs = maxs * radius + end + if part:GetEvent() == "is_touching_scalable" then + radius = math.max(extra_radius, 1) + mins = mins * radius + maxs = maxs * radius + end - local mins = Vector(-1,-1,-1) - local maxs = Vector(1,1,1) local startpos = ent:WorldSpaceCenter() - mins = mins * radius - maxs = maxs * radius local tr = util.TraceHull( { start = startpos, endpos = startpos, maxs = maxs, mins = mins, - filter = ent + filter = {part:GetRootPart():GetOwner(),ent} } ) - + if self.udata then render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, tr.Hit and Color(255,0,0) or Color(255,255,255), true ) end diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 845c058b5..6a24acf0d 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1,4 +1,16 @@ local L = pace.LanguageString +local BulkSelectList = {} +local BulkSelectUIDs = {} +pace.BulkSelectClipboard = {} +refresh_halo_hook = true + + +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") + +CreateConVar( "pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") +CreateConVar( "pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") -- load only when hovered above local function add_expensive_submenu_load(pnl, callback) @@ -11,6 +23,7 @@ local function add_expensive_submenu_load(pnl, callback) end function pace.WearParts(temp_wear_filter) + local allowed, reason = pac.CallHook("CanWearParts", pac.LocalPlayer) if allowed == false then @@ -98,13 +111,65 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) end pace.RefreshTree() - + timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) return part end +local last_span_select_part +local last_select_was_span = false +local last_direction + function pace.OnPartSelected(part, is_selecting) - local parent = part:GetRootPart() + if input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then + --jumping multi-select if holding shift + ctrl + if input.IsControlDown() and input.IsShiftDown() and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then + --ripped some local functions from tree.lua + local added_nodes = {} + for i,v in ipairs(pace.tree.added_nodes) do + if v.part and v:IsVisible() and v:IsExpanded() then + table.insert(added_nodes, v) + end + end + + local startnodenumber = table.KeyFromValue( added_nodes, pace.current_part.pace_tree_node) + local endnodenumber = table.KeyFromValue( added_nodes, part.pace_tree_node) + + table.sort(added_nodes, function(a, b) return select(2, a:LocalToScreen()) < select(2, b:LocalToScreen()) end) + + local i = startnodenumber + + direction = math.Clamp(endnodenumber - startnodenumber,-1,1) + if direction == 0 then last_direction = direction return end + last_direction = last_direction or 0 + if last_span_select_part == nil then last_span_select_part = part end + + if last_select_was_span then + if last_direction == -direction then + pace.DoBulkSelect(pace.current_part, true) + end + if last_span_select_part == pace.current_part then + pace.DoBulkSelect(pace.current_part, true) + end + end + while (i ~= endnodenumber) do + pace.DoBulkSelect(added_nodes[i].part, true) + i = i + direction + end + pace.DoBulkSelect(part) + last_direction = direction + last_select_was_span = true + else + pace.DoBulkSelect(part) + last_select_was_span = false + end + + else last_select_was_span = false end + last_span_select_part = part + + + + local parent = part:GetRootPart() if parent:IsValid() and (parent.OwnerName == "viewmodel" or parent.OwnerName == "hands") then pace.editing_viewmodel = parent.OwnerName == "viewmodel" pace.editing_hands = parent.OwnerName == "hands" @@ -132,6 +197,7 @@ function pace.OnPartSelected(part, is_selecting) if not is_selecting then pace.StopSelect() end + end function pace.OnVariableChanged(obj, key, val, not_from_editor) @@ -239,7 +305,12 @@ function pace.GetRegisteredParts() end do -- menu + local trap + if not pace.Active or refresh_halo_hook then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + end + function pace.AddRegisteredPartsToMenu(menu, parent) local partsToShow = {} local clicked = false @@ -509,6 +580,8 @@ do -- menu end function pace.RemovePart(obj) + if table.HasValue(BulkSelectList,obj) then table.RemoveByValue(BulkSelectList,obj) end + pace.RecordUndoHistory() obj:Remove() pace.RecordUndoHistory() @@ -520,13 +593,177 @@ do -- menu end end + function pace.ClearBulkList() + for _,v in ipairs(BulkSelectList) do + if v.pace_tree_node ~= nil then v.pace_tree_node:SetAlpha( 255 ) end + v:SetInfo() + end + BulkSelectList = {} + print("Bulk list deleted!") + --surface.PlaySound('buttons/button16.wav') + end +//@note pace.DoBulkSelect + function pace.DoBulkSelect(obj, silent) + refresh_halo_hook = false + --print(obj.pace_tree_node, "color", obj.pace_tree_node:GetFGColor().r .. " " .. obj.pace_tree_node:GetFGColor().g .. " " .. obj.pace_tree_node:GetFGColor().b) + if obj.ClassName == "timeline_dummy_bone" then return end + local selected_part_added = false --to decide the sound to play afterward + + BulkSelectList = BulkSelectList or {} + if (table.HasValue(BulkSelectList, obj)) then + pace.RemoveFromBulkSelect(obj) + selected_part_added = false + elseif (BulkSelectList[obj] == nil) then + pace.AddToBulkSelect(obj) + selected_part_added = true + for _,v in ipairs(obj:GetChildrenList()) do + pace.RemoveFromBulkSelect(v) + end + end + + --check parents and children + for _,v in ipairs(BulkSelectList) do + if table.HasValue(v:GetChildrenList(), obj) then + --print("selected part is already child to a bulk-selected part!") + pace.RemoveFromBulkSelect(obj) + selected_part_added = false + elseif table.HasValue(obj:GetChildrenList(), v) then + --print("selected part is already parent to a bulk-selected part!") + pace.RemoveFromBulkSelect(v) + selected_part_added = false + end + end + + RebuildBulkHighlight() + if not silent then + if selected_part_added then + surface.PlaySound('buttons/button1.wav') + --test print + for i,v in ipairs(BulkSelectList) do + print("["..i.."] = "..v.UniqueID) + end + print("\n") + else surface.PlaySound('buttons/button16.wav') end + end + + if table.IsEmpty(BulkSelectList) then + --remove halo hook + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + else + --start halo hook + hook.Add('PreDrawHalos', "BulkSelectHighlights", function() + local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + if mode == 0 then return + elseif mode == 1 then ThinkBulkHighlight() + elseif mode == 2 then if input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) then ThinkBulkHighlight() end + elseif mode == 3 then if input.IsControlDown() then ThinkBulkHighlight() end + elseif mode == 4 then if input.IsShiftDown() then ThinkBulkHighlight() end + end + end) + end + + for _,v in ipairs(BulkSelectList) do + --v.pace_tree_node:SetAlpha( 150 ) + end + end + + function pace.RemoveFromBulkSelect(obj) + table.RemoveByValue(BulkSelectList, obj) + obj.pace_tree_node:SetAlpha( 255 ) + obj:SetInfo() + --RebuildBulkHighlight() + end + + function pace.AddToBulkSelect(obj) + table.insert(BulkSelectList, obj) + if obj.pace_tree_node == nil then return end + obj:SetInfo("selected in bulk select") + obj.pace_tree_node:SetAlpha( 150 ) + --RebuildBulkHighlight() + end + + function pace.BulkCutPaste(obj) + pace.RecordUndoHistory() + for _,v in ipairs(BulkSelectList) do + --if a part is inserted onto itself, it should instead serve as a parent + if v ~= obj then v:SetParent(obj) end + end + pace.RecordUndoHistory() + pace.RefreshTree() + end + + function pace.BulkCopy(obj) + if #BulkSelectList == 1 then pace.Copy(obj) end --at least if there's one selected, we can take it that we want to copy that part + pace.BulkSelectClipboard = table.Copy(BulkSelectList) --if multiple parts are selected, copy it to a new bulk clipboard + print("copied: ") + TestPrintTable(pace.BulkSelectClipboard,"pace.BulkSelectClipboard") + end + + function pace.BulkPasteFromBulkClipboard(obj) --paste bulk clipboard into one part + pace.RecordUndoHistory() + if not table.IsEmpty(pace.BulkSelectClipboard) then + for _,v in ipairs(pace.BulkSelectClipboard) do + local newObj = pac.CreatePart(v.ClassName) + newObj:SetTable(v:ToTable(), true) + newObj:SetParent(obj) + end + end + pace.RecordUndoHistory() + --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end) + end + + function pace.BulkPasteFromBulkSelectToSinglePart(obj) --paste bulk selection into one part + pace.RecordUndoHistory() + if not table.IsEmpty(BulkSelectList) then + for _,v in ipairs(BulkSelectList) do + local newObj = pac.CreatePart(v.ClassName) + newObj:SetTable(v:ToTable(), true) + newObj:SetParent(obj) + end + end + pace.RecordUndoHistory() + end + + function pace.BulkPasteFromSingleClipboard() --paste the normal clipboard into each bulk select item + pace.RecordUndoHistory() + if not table.IsEmpty(BulkSelectList) then + for _,v in ipairs(BulkSelectList) do + local newObj = pac.CreatePart(pace.Clipboard.self.ClassName) + newObj:SetTable(pace.Clipboard, true) + newObj:SetParent(v) + end + end + pace.RecordUndoHistory() + --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end) + end + + function pace.BulkRemovePart() + pace.RecordUndoHistory() + if not table.IsEmpty(BulkSelectList) then + for _,v in ipairs(BulkSelectList) do + v:Remove() + + if not v:HasParent() and v.ClassName == "group" then + pace.RemovePartOnServer(v:GetUniqueID(), false, true) + end + end + end + pace.RefreshTree() + pace.RecordUndoHistory() + pace.ClearBulkList() + --timer.Simple(0.1, function BulkSelectRefreshFadedNodes() end) + end +//@note part menu function pace.OnPartMenu(obj) local menu = DermaMenu() menu:SetPos(input.GetCursorPos()) if obj then if not obj:HasParent() then - menu:AddOption(L"wear", function() pace.SendPartToServer(obj) end):SetImage(pace.MiscIcons.wear) + menu:AddOption(L"wear", function() + pace.SendPartToServer(obj) + BulkSelectList = {} + end):SetImage(pace.MiscIcons.wear) end menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy) @@ -535,6 +772,77 @@ do -- menu menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) + --multi select + bulk_menu, icon = menu:AddSubMenu(L"bulk select ("..#BulkSelectList..")", function() pace.DoBulkSelect(obj) end) + icon:SetImage('icon16/table_multiple.png') + bulk_menu.GetDeleteSelf = function() return false end + + local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + local info + if mode == 0 then info = "not halo-highlighted" + elseif mode == 1 then info = "automatically halo-highlighted" + elseif mode == 2 then info = "halo-highlighted on custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString() + elseif mode == 3 then info = "halo-highlighted on preset keypress: control" + elseif mode == 4 then info = "halo-highlighted on preset keypress: shift" end + + bulk_menu:AddOption(L"Bulk select info: "..info, function() end):SetImage(pace.MiscIcons.info) + bulk_menu:AddOption(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts", function() end):SetImage(pace.MiscIcons.info) + + bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() + pace.BulkCutPaste(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Copy to Bulk Clipboard", function() + pace.BulkCopy(obj) + end):SetImage(pace.MiscIcons.copy) + + bulk_menu:AddSpacer() + + --bulk paste modes + bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function() + pace.BulkPasteFromBulkSelectToSinglePart(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function() + if not pace.Clipboard then pace.Copy(obj) end + pace.BulkPasteFromSingleClipboard() + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function() + pace.BulkPasteFromBulkClipboard(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function() + for _,v in ipairs(BulkSelectList) do + pace.BulkPasteFromBulkClipboard(v) + end + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk paste properties from selected part", function() + pace.Copy(obj) + for _,v in ipairs(BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddOption(L"Bulk paste properties from clipboard", function() + for _,v in ipairs(BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk Delete", function() + pace.BulkRemovePart() + end):SetImage(pace.MiscIcons.clear) + + bulk_menu:AddOption(L"Clear Bulk List", function() + pace.ClearBulkList() + end):SetImage('icon16/table_delete.png') + menu:AddSpacer() end @@ -575,36 +883,198 @@ do -- menu local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) pnl:SetImage(pace.MiscIcons.load) add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) - menu:AddOption(L"clear", function() + pace.ClearBulkList() pace.ClearParts() end):SetImage(pace.MiscIcons.clear) end end -do + +do --hover highlight halo pac.haloex = include("pac3/libraries/haloex.lua") + local hover_halo_limit + local warn = false + local post_warn_next_allowed_check_time = 0 + local post_warn_next_allowed_warn_time = 0 + local last_culprit_UID = 0 + local last_checked_partUID = 0 + local last_tbl = {} + local last_bulk_select_tbl = nil + local last_root_ent = {} + local last_time_checked = 0 + function pace.OnHoverPart(self) + local skip = false + if GetConVar("pac_hover_color"):GetString() == "none" then return end + hover_halo_limit = GetConVar("pac_hover_halo_limit"):GetInt() + local tbl = {} local ent = self:GetOwner() - - if ent:IsValid() then - table.insert(tbl, ent) + local is_root = ent == self:GetRootPart():GetOwner() + + --decide whether to skip + --it will skip the part-search loop if we already checked the part recently + if self.UniqueID == last_checked_partUID then + skip = true + if is_root and last_root_ent ~= self:GetRootPart():GetOwner() then + table.RemoveByValue(last_tbl, last_root_ent) + table.insert(last_tbl, self:GetRootPart():GetOwner()) + end + tbl = last_tbl end - for _, child in ipairs(self:GetChildrenList()) do - local ent = self:GetOwner() - if ent:IsValid() then + --operations : search the part and look for entity-candidates to halo + if not skip then + --start with entity, which could be part or entity + if (is_root and ent:IsValid()) then table.insert(tbl, ent) + else + if not ((self.ClassName == "group" or self.ClassName == "jiggle") or (self.Hide == true) or (self.Size == 0) or (self.Alpha == 0)) then + table.insert(tbl, ent) + end + end + + --get the children if any + if self:HasChildren() then + for _,v in ipairs(self:GetChildrenList()) do + local can_add = false + local ent = v:GetOwner() + + --we're not gonna add parts that don't have a specific reason to be haloed or that don't at least group up some haloable models + --because the table.insert function has a processing load on the memory, and so is halo-drawing + if (v.ClassName == "model" or v.ClassName == "model2" or v.ClassName == "jiggle") then + can_add = true + else can_add = false end + if (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then + can_add = false + end + if can_add then table.insert(tbl, ent) end + end end end - if #tbl > 0 then - local pulse = math.sin(pac.RealTime * 20) * 0.5 + 0.5 - pulse = pulse * 255 - pac.haloex.Add(tbl, Color(pulse, pulse, pulse, 255), 1, 1, 1, true, true, 5, 1, 1) + last_tbl = tbl + last_root_ent = self:GetRootPart():GetOwner() + last_checked_partUID = self.UniqueID + + DrawHaloHighlight(tbl) + + --also refresh the bulk-selected nodes' labels because pace.RefreshTree() resets their alphas, but I want to keep the fade because it indicates what's being bulk-selected + if not skip then timer.Simple(0.3, function() BulkSelectRefreshFadedNodes(self) end) end + end + + function BulkSelectRefreshFadedNodes(part_trace) + if refresh_halo_hook then return end + if part_trace then + for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do + v.pace_tree_node:SetAlpha( 255 ) + end + end + + for _,v in ipairs(BulkSelectList) do + v.pace_tree_node:SetAlpha( 150 ) + end + end + + function RebuildBulkHighlight() + local parts_tbl = {} + local ents_tbl = {} + local hover_tbl = {} + local ent = {} + + --get potential entities and part-children from each parent in the bulk list + for _,v in pairs(BulkSelectList) do --this will get parts + + if (v == v:GetRootPart()) then --if this is the root part, send the entity + table.insert(ents_tbl,v:GetRootPart():GetOwner()) + table.insert(parts_tbl,v) + else + table.insert(parts_tbl,v) + end + + for _,child in ipairs(v:GetChildrenList()) do --now do its children + table.insert(parts_tbl,child) + end + end + + --check what parts are candidates we can give to halo + for _,v in ipairs(parts_tbl) do + local can_add = false + if (v.ClassName == "model" or v.ClassName == "model2") then + can_add = true + end + if (v.ClassName == "group") or (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then + can_add = false + end + if can_add then + table.insert(hover_tbl, v:GetOwner()) + end + end + + table.Add(hover_tbl,ents_tbl) + --TestPrintTable(hover_tbl, "hover_tbl") + + last_bulk_select_tbl = hover_tbl + end + + function TestPrintTable(tbl, tbl_name) + MsgC(Color(200,255,200), "TABLE CONTENTS:" .. tbl_name .. " = {\n") + for _,v in pairs(tbl) do + MsgC(Color(200,255,200), "\t", tostring(v), ", \n") + end + MsgC(Color(200,255,200), "}\n") + end + + function ThinkBulkHighlight() + if table.IsEmpty(BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + return end + DrawHaloHighlight(last_bulk_select_tbl) + end + + function DrawHaloHighlight(tbl) + if (type(tbl) ~= "table") then return end + if not pace.Active then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + end + + --Find out the color and apply the halo + local color_string = GetConVar("pac_hover_color"):GetString() + local pulse_rate = math.min(math.abs(GetConVar("pac_hover_pulserate"):GetFloat()), 100) + local pulse = math.sin(SysTime() * pulse_rate) * 0.5 + 0.5 + if pulse_rate == 0 then pulse = 1 end + local pulseamount + + local halo_color + + if color_string == "rave" then + halo_color = Color(255*((0.33 + SysTime() * pulse_rate/20)%1), 255*((0.66 + SysTime() * pulse_rate/20)%1), 255*((SysTime() * pulse_rate/20)%1), 255) + pulseamount = 8 + elseif color_string == "ocean" then + halo_color = Color(0, 80 + 30*(pulse), 200 + 50*(pulse) * 0.5 + 0.5, 255) + pulseamount = 4 + elseif color_string == "rainbow" then + --halo_color = Color(255*(0.5 + 0.5*math.sin(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*-math.cos(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*math.sin(1 + pac.RealTime * pulse_rate/20)), 255) + halo_color = HSVToColor(SysTime() * 360 * pulse_rate/20, 1, 1) + pulseamount = 4 + elseif #string.Split(color_string, " ") == 3 then + halo_color_tbl = string.Split( color_string, " " ) + for i,v in ipairs(halo_color_tbl) do + if not isnumber(tonumber(halo_color_tbl[i])) then halo_color_tbl[i] = 0 end + end + halo_color = Color(pulse*halo_color_tbl[1],pulse*halo_color_tbl[2],pulse*halo_color_tbl[3],255) + pulseamount = 4 + else + halo_color = Color(255,255,255,255) + pulseamount = 2 + end + --print("using", halo_color, "blurs=" .. 2, "amount=" .. pulseamount) + + pac.haloex.Add(tbl, halo_color, 2, 2, pulseamount, true, true, pulseamount, 1, 1) + --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) end end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 574c99876..f8646236f 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -5,10 +5,13 @@ function PANEL:Init() local pnl = vgui.Create("DPropertySheet", self) pnl:Dock(FILL) - local props = pace.FillWearSettings(pnl) - - pnl:AddSheet("Wear / Ignore", props) + local properties_filter = pace.FillWearSettings(pnl) + + pnl:AddSheet("Wear / Ignore", properties_filter) self.sheet = pnl + + --local properties_shortcuts = pace.FillShortcutSettings(pnl) + --pnl:AddSheet("Editor Shortcuts", properties_shortcuts) end vgui.Register( "pace_settings", PANEL, "DPanel" ) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 40efcff14..3f805bcc9 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -1,3 +1,123 @@ +CreateClientConVar( "pac_focus_input1", "", true, false, "Set the first key for the custom shorctut of pac3 editor focus. Format according to internal names of the keys as console binds e.g. e or ctrl" ) +CreateClientConVar( "pac_focus_input2", "", true, false, "Set the second key for the custom shorctut of pac3 editor focus. Format according to internal names of the keys as console binds e.g. e or ctrl" ) +concommand.Add( "pac_toggle_focus", function() pace.Call("ToggleFocus") end) +concommand.Add( "pac_focus", function() pace.Call("ToggleFocus") end) + +local last_recorded_combination + +local focusKeyPrimary +local focusKeySecondary + +local ShortcutActions + +ShortcutActions = {} +ShortcutActions["wear"] = {0} +ShortcutActions["save"] = {1,2,3} +ShortcutActions["focus"] = {67,83} +ShortcutActions["copy"] = {0} +ShortcutActions["paste"] = {0} +ShortcutActions["cut"] = {0} +ShortcutActions["delete"] = {0} +ShortcutActions["expand_all"] = {0} +ShortcutActions["collapse_all"] = {0} +ShortcutActions["undo"] = {0} +ShortcutActions["redo"] = {0} + +concommand.Add( "pac_echo_shortcut", function() + timer.Simple( 3, function() + surface.PlaySound("buttons/button1.wav") + inputs = get_all_inputs() + print("inputs:") + printed_list = "" + input_list = {} + for k,v in pairs(inputs) do + printed_list = printed_list .. "key" .. k .. ", (code " .. v .. ", named " .. input.GetKeyName(v) .. ")\n" + input_list[k] = v + end + print(printed_list) + print(unpack(input_list)) + last_recorded_combination = input_list + end + ) +end) +--[[ +concommand.Add( "pac_assign_shortcut", function() + local action_name = "focus" + ShortcutActions[action_name] = last_recorded_combination + print("assigned "..action_name.." for ") + print(unpack(ShortcutActions[action_name])) +end) + + + + +concommand.Add( "pac_echo_shortcut_megatable", function() + for k,v in ipairs(ShortcutActions) do + unpacked_combo_string = "" + for k2,v2 in ipairs(ShortcutActions[k][2]) do + if (ShortcutActions[k][2][k2] ~= nil) then + unpacked_combo_string = unpacked_combo_string .. ShortcutActions[k][2][k2] .. "," + end + end + print(ShortcutActions[k][1] .. " " .. unpacked_combo_string) + end + + test_combos = { + {1,2}, + {1,3,2}, + {1,2,3}, + {0}, + {4}, + {54,57}, + } + + print("the match between " .. "{1,2}" .. " and {\"save\",{1,2,3}} is ", matches_input(test_combos[1], "save")) + +end) + +function get_all_inputs() + list = {} + count = 1 + for key=1,BUTTON_CODE_LAST do + if input.IsKeyDown(key) then + list[count] = key + count = count + 1 + end + end + return list +end + + +function matches_input(combo_in, action_name) + local target = ShortcutActions[action_name][2] + print("combo_in length is ", #combo_in, ", target length is ", #target) + if #combo_in ~= #target.length then return false end + full_match = true + print("trying to match") + for k,v in pairs(combo_in) do + if ((combo_in[v] == nil) or (target[v] == nil)) then + full_match = false + else + if (combo_in[v] == target[v]) then + print("matched " .. target[v]) + else + full_match = false + end + end + end + return full_match +end +]]-- + + + --[[thinkUndo() + thinkCopy() + thinkPaste() + thinkCut() + thinkDelete() + thinkExpandAll() + thinkCollapseAll()]]-- + function pace.OnShortcutSave() if not IsValid(pace.current_part) then return end @@ -17,8 +137,23 @@ function pace.OnShortcutWear() end local last = 0 +local last_print_time = CurTime() function pace.CheckShortcuts() + --[[ + if input.IsKeyDown(KEY_H) then + if last_print_time + 1 < CurTime() then + surface.PlaySound("buttons/button9.wav") + print("input report!") + print(unpack(get_all_inputs())) + print("done at time of "..last_print_time.."\n") + --chat.print("input report!\n"..unpack(get_all_inputs()).."\ndone at time of "..last_print_time) + last_print_time = CurTime() + end + end]]-- + focusKeyPrimary = input.GetKeyCode(GetConVar("pac_focus_input1"):GetString()) + focusKeySecondary = input.GetKeyCode(GetConVar("pac_focus_input2"):GetString()) + if gui.IsConsoleVisible() then return end if not pace.Editor or not pace.Editor:IsValid() then return end if last > RealTime() or input.IsMouseDown(MOUSE_LEFT) then return end @@ -32,6 +167,18 @@ function pace.CheckShortcuts() pace.Call("ToggleFocus") last = RealTime() + 0.2 end + -- can make new hardcoded custom shortcuts for focus + --[[if input.IsKeyDown(KEY_LSHIFT) and input.IsKeyDown(KEY_R) then + pace.Call("ToggleFocus") + last = RealTime() + 0.2 + end]]-- + --convar custom inputs + --if not ((focusKeyPrimary == -1) and (focusKeySecondary == -1)) then + if input.IsKeyDown(focusKeyPrimary) and input.IsKeyDown(focusKeySecondary) then + pace.Call("ToggleFocus") + last = RealTime() + 0.2 + end + --end if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then RunConsoleCommand("pac_restart") @@ -67,6 +214,31 @@ function pace.CheckShortcuts() end end +--[[ +function pace.FillShortcutSettings(pnl) + local list = vgui.Create("DCategoryList", pnl) + list:Dock(FILL) + do + local cat = list:Add(L"Wear") + cat.Header:SetSize(40,40) + cat.Header:SetFont("DermaLarge") + local list = vgui.Create("DListLayout") + list:DockPadding(20,20,20,20) + cat:SetContents(list) + + local mode = vgui.Create("DComboBox", list) + + mode.OnSelect = function(_, _, value) + if IsValid(mode.form) then + mode.form:Remove() + end + mode.form:SetParent(list) + end + end + return list +end +]]-- + pac.AddHook("Think", "pace_shortcuts", pace.CheckShortcuts) do diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index f772ebc9a..6c415af9c 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -1,4 +1,4 @@ - +include("pac3/editor/client/parts.lua") local L = pace.LanguageString function pace.IsPartSendable(part) @@ -34,6 +34,12 @@ function pace.ClearParts() pace.ClearUndo() pac.RemoveAllParts(true, true) pace.RefreshTree() + + --clean up the bulk selection + pace.ClearBulkList() + BulkSelectList = nil + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + refresh_halo_hook = true timer.Simple(0.1, function() if not pace.Editor:IsValid() then return end diff --git a/lua/pac3/extra/shared/init.lua b/lua/pac3/extra/shared/init.lua index 78de1c3e9..4728c341f 100644 --- a/lua/pac3/extra/shared/init.lua +++ b/lua/pac3/extra/shared/init.lua @@ -2,6 +2,7 @@ include("hands.lua") include("pac_weapon.lua") include("projectiles.lua") +include("net_combat.lua") local cvar = CreateConVar("pac_restrictions", "0", FCVAR_REPLICATED) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua new file mode 100644 index 000000000..6c538c102 --- /dev/null +++ b/lua/pac3/extra/shared/net_combat.lua @@ -0,0 +1,368 @@ +local grab_consents = {} +local damage_zone_consents = {} + +local damage_types = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamageInfo + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, +} + +if SERVER then + util.AddNetworkString("pac_hitscan") + util.AddNetworkString("pac_request_position_override_on_entity") + util.AddNetworkString("pac_request_angle_reset_on_entity") + util.AddNetworkString("pac_request_velocity_force_on_entity") + util.AddNetworkString("pac_request_zone_damage") + util.AddNetworkString("pac_signal_player_combat_consent") + util.AddNetworkString("pac_request_player_combat_consent_update") + + net.Receive("pac_hitscan", function(len,ply) + print("WE SHOULD DO A BULLET IN THE SERVER!") + ent = net.ReadEntity() + bulletinfo = net.ReadTable() + print("hitscan!", ent) + PrintTable(bulletinfo) + ent:FireBullets(bulletinfo) + end) + + net.Receive("pac_request_zone_damage", function(len,ply) + --print("message from ",ply) + local pos = net.ReadVector() + local ang = net.ReadAngle() + local tbl = net.ReadTable() + local ply_ent = net.ReadEntity() + local dmg_info = DamageInfo() + dmg_info:SetDamage(tbl.Damage) + dmg_info:IsBulletDamage(tbl.Bullet) + dmg_info:SetDamageForce(Vector(0,0,0)) + dmg_info:SetAttacker(ply_ent) + dmg_info:SetInflictor(ply_ent) + --print("entity: ",ply_ent) + if tbl.OverrideKnockback then print("should override") end + + dmg_info:SetDamageType(damage_types[tbl.DamageType]) --print(tbl.DamageType .. " resolves to " .. damage_types[tbl.DamageType]) + + local ratio + if tbl.Radius == 0 then ratio = tbl.Length + else ratio = math.abs(tbl.Length / tbl.Radius) end + + if tbl.HitboxMode == "Sphere" then + local ents_hits = ents.FindInSphere(pos, tbl.Radius) + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "Box" then + local mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Radius) + local maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius) + local ents_hits = ents.FindInBox(mins, maxs) + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then + local ents_hits = {} + if tbl.Radius ~= 0 then + local sides = tbl.Detail + if tbl.Detail < 1 then sides = 1 end + local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + end + steps = math.max(steps + math.abs(tbl.ExtraSteps),1) + + --print("steps",steps, "total casts will be "..steps*self.Detail) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + local startpos = pos + x + y + local endpos = pos + ang:Forward()*tbl.Length + x + y + table.Merge(ents_hits, ents.FindAlongRay(startpos, endpos)) + end + end + if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then + --fast sphere check on the wide end + if tbl.Length/tbl.Radius >= 2 then + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius), tbl.Radius)) + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Radius, tbl.Radius)) + if tbl.Radius ~= 0 then + local counter = 0 + for i=math.floor(tbl.Length / tbl.Radius) - 1,1,-1 do + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Radius*i), tbl.Radius)) + if counter == 100 then break end + counter = counter + 1 + end + end + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + end + elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "CylinderSpheres" then + local ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + table.Inherit(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then + local ents_hits = {} + if tbl.Radius ~= 0 then + local sides = tbl.Detail + if tbl.Detail < 1 then sides = 1 end + local startpos = pos-- + Vector(0, self.Radius,self.Radius) + local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + end + steps = math.max(steps + math.abs(tbl.ExtraSteps),1) + --print("steps",steps, "total casts will be "..steps*self.Detail) + local timestart = SysTime() + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + + phase = math.random() + --print("ring " .. ringnumber .. " phase " .. phase) + for i=1,0,-1/sides do + --print("radius " .. tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize)) + if ringnumber == 0 then i = 0 end + x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + local endpos = pos + ang:Forward()*tbl.Length + x + y + table.Inherit(ents_hits,ents.FindAlongRay(startpos, endpos)) + end + end + if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then + --fast sphere check on the wide end + local radius_multiplier = math.atan(math.abs(ratio)) / (1.5 + 0.1*math.sqrt(ratio)) + if ratio > 0.5 then + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius * radius_multiplier), tbl.Radius * radius_multiplier)) + end + end + elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "ConeSpheres" then + local ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + --PrintTable(ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + for i = 0,1/8,1/128 do + --PrintTable(ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + if tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + + if tbl.Bullet then + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + dmg_info:GetInflictor():FireBullets(bullet) + end + end + + end) + + function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + if #ents_hits == 0 then + if tbl.Bullet then + dmg_info:GetInflictor():FireBullets(bullet) + end + return + end + + for _,ent in pairs(ents_hits) do + if IsEntity(ent) then + if (not tbl.AffectSelf) and ent == dmg_info:GetInflictor() then --nothing + elseif (ent:IsPlayer() and tbl.Players) or (ent:IsNPC() and tbl.NPC) or (ent:GetClass() == "prop_physics") then + --local oldvel = ent:GetVelocity() + local ents2 = {dmg_info:GetInflictor()} + if tbl.Bullet then + for _,v in ipairs(ents_hits) do + if v ~= ent then table.insert(ents2,v) end + end + end + + if tbl.DamageType == "heal" then + ent:SetHealth(math.min(ent:Health() + tbl.Damage, ent:GetMaxHealth())) + elseif tbl.DamageType == "armor" then + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, ent:GetMaxArmor())) + else + if tbl.Bullet then + traceresult = util.TraceLine({filter = ents2, start = pos, endpos = pos + 50000*(ent:WorldSpaceCenter() - dmg_info:GetAttacker():WorldSpaceCenter())}) + --print(traceresult.Fraction) + bullet.Dir = traceresult.Normal + bullet.Src = traceresult.HitPos + traceresult.HitNormal*5 + dmg_info:GetInflictor():FireBullets(bullet) + end + if ent:IsPlayer() then + if (ent == dmg_info:GetInflictor() and tbl.AffectSelf) then + ent:TakeDamageInfo(dmg_info) + print(ent, "hurt themself") + elseif damage_zone_consents[ent] == true then + ent:TakeDamageInfo(dmg_info) + print(dmg_info:GetAttacker(), "hurt", ent) + end + else + ent:TakeDamageInfo(dmg_info) + print(dmg_info:GetAttacker(), "hurt", ent) + end + end + --local newvel = ent:GetVelocity() + --ent:SetVelocity( oldvel - newvel) + end + end + end + end + + net.Receive("pac_request_position_override_on_entity", function(len, ply) + + local pos = net.ReadVector() + local ang = net.ReadAngle() + local override_ang = net.ReadBool() + local targ_ent = net.ReadEntity() + local auth_ent = net.ReadEntity() + + if targ_ent:EntIndex() == 0 then return end + if targ_ent ~= auth_ent and grab_consents[targ_ent] == false then return end + + if override_ang and not targ_ent:IsPlayer() then + targ_ent:SetAngles(ang) + elseif override_ang and (auth_ent == targ_ent) and targ_ent:IsPlayer() then + targ_ent:SetEyeAngles(ang) + end + targ_ent:SetPos(pos) + + if targ_ent:IsPlayer() then targ_ent:SetVelocity(-targ_ent:GetVelocity()) end + end) + + net.Receive("pac_request_angle_reset_on_entity", function(len, ply) + local ang = net.ReadAngle() + local delay = net.ReadFloat() + local targ_ent = net.ReadEntity() + local auth_ent = net.ReadEntity() + + targ_ent:SetAngles(ang) + end) + + net.Receive("pac_request_velocity_force_on_entity", function(len,ply) + + end) + + net.Receive("pac_signal_player_combat_consent", function(len,ply) + mode = net.ReadString() + b = net.ReadBool() + --print("message from", ply, "consent for",mode,"is",b) + if mode == "grab" then + grab_consents[ply] = b + --PrintTable(grab_consents) + elseif mode == "damage_zone" then + damage_zone_consents[ply] = b + --PrintTable(grab_consents) + elseif mode == "all" then + grab_consents[ply] = b + damage_zone_consents[ply] = b + end + end) + + concommand.Add("pac_refresh_consents", function() + pac_combat_RefreshConsents() + end) + + function pac_combat_RefreshConsents() + for _,ent in pairs(ents.GetAll()) do + if ent:IsPlayer() then + net.Start("pac_request_player_combat_consent_update") + net.Send(ent) + print(ent, "does that player consent grabs?", grab_consents[ent], "and damage zone?", damage_zone_consents[ent]) + end + end + PrintTable(grab_consents) + end + + +end + +if CLIENT then + CreateConVar("pac_client_grab_consent", "0", true, true) + CreateConVar("pac_client_damage_zone_consent", "0", true, true) + net.Receive("pac_request_player_combat_consent_update", function() + print("player receives request to update consents") + net.Start("pac_signal_player_combat_consent") + net.WriteString("grab") + net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) + net.SendToServer() + + net.Start("pac_signal_player_combat_consent") + net.WriteString("damage_zone") + net.WriteBool(GetConVar("pac_client_damage_zone_consent"):GetBool()) + net.SendToServer() + end) +end + diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index 7189ad6f1..e87752bbf 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -417,6 +417,7 @@ if SERVER then end pace.suppress_prop_spawn = nil + local multi_projectile_count = net.ReadUInt(7) local pos = net.ReadVector() local ang = net.ReadAngle() local part = net.ReadTable() @@ -453,7 +454,7 @@ if SERVER then end if projectile_count > 50 then - pac.Message("Player ", ply, " has more than 50 projectiles spawned!") + pac.Message("Player ", ply, " has more than 50 projectiles spawned! No more will be spawned until some expire.") return end @@ -494,10 +495,57 @@ if SERVER then ent.pac_projectile_owner = ply end - if part.Delay == 0 then - spawn() + local function multispawn() + if not ply:IsValid() then return end + + ply.pac_projectiles = ply.pac_projectiles or {} + + local projectile_count = 0 + for ent in pairs(ply.pac_projectiles) do + if ent:IsValid() then + projectile_count = projectile_count + 1 + else + ply.pac_projectiles[ent] = nil + end + end + + local remaining_projectile_slots = math.max(50 - projectile_count,0) + + if (multi_projectile_count > remaining_projectile_slots) then + if remaining_projectile_slots == 0 then + --block the spawns + pac.Message("Player ", ply, " has 50 projectiles spawned! No more will be spawned until some expire.") + goto CONTINUE + else + --adjust the spawn to just the limit + pac.Message("Player ", ply, " will spawn only ",remaining_projectile_slots," projectiles to prevent going over-limit") + multi_projectile_count = remaining_projectile_slots + end + + end + if part.Maximum > 0 and projectile_count >= part.Maximum then + return + end + + for i = multi_projectile_count - 1, 0, -1 do + spawn() + end + + ::CONTINUE:: + end + + if multi_projectile_count == 1 then + if part.Delay == 0 then + spawn() + else + timer.Simple(part.Delay, spawn) + end else - timer.Simple(part.Delay, spawn) + if part.Delay == 0 then + multispawn() + else + timer.Simple(part.Delay, multispawn) + end end end) end From 3eb1662127a17e68ae39de54ee6a3a4948643d46 Mon Sep 17 00:00:00 2001 From: Redox Date: Mon, 10 Apr 2023 04:08:29 +0200 Subject: [PATCH 003/300] Add simple logging print --- lua/pac3/editor/server/wear.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/pac3/editor/server/wear.lua b/lua/pac3/editor/server/wear.lua index 3b1bd343f..f1a21e19e 100644 --- a/lua/pac3/editor/server/wear.lua +++ b/lua/pac3/editor/server/wear.lua @@ -334,6 +334,8 @@ function pace.HandleReceivedData(ply, data) data.owner = ply data.uid = pac.Hash(ply) + pac.Message("Received pac data from ", ply, " with ", data.totalParts or 0, " parts") + if data.wear_filter and #data.wear_filter > game.MaxPlayers() then pac.Message("Player ", ply, " tried to submit extraordinary wear filter size of ", #data.wear_filter, ", dropping.") data.wear_filter = nil From c9a8ca63244921bd2e169c69b274f9700ce7ba6f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 10 Apr 2023 16:26:51 -0400 Subject: [PATCH 004/300] update branch again renamed "develop" branch to "develop-cedricversion" new "develop" branch is fresh from CapsAdmin repository --- .../client/parts/interpolated_multibone.lua | 112 ++++++++++++------ lua/pac3/core/client/parts/legacy/entity.lua | 2 +- lua/pac3/core/client/parts/particles.lua | 17 +-- lua/pac3/core/client/parts/player_config.lua | 2 +- .../shared/entity_mutators/draw_shadow.lua | 23 ++++ lua/pac3/core/shared/footsteps_fix.lua | 23 ++++ lua/pac3/editor/client/wear.lua | 8 +- lua/pac3/editor/server/wear.lua | 2 - 8 files changed, 131 insertions(+), 58 deletions(-) create mode 100644 lua/pac3/core/shared/entity_mutators/draw_shadow.lua diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index 29849056c..2d6628745 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -10,7 +10,10 @@ local Color = Color local Matrix = Matrix local vector_origin = vector_origin -local BUILDER, PART = pac.PartTemplate("base_movable") +local pos +local ang + +local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "interpolated_multibone" PART.Group = 'advanced' @@ -41,37 +44,47 @@ BUILDER:StartStorableVars() --PART:GetWorldAngles() function PART:Initialize() print("a multiboner is born") - - self.pos = Vector() - self.vel = Vector() - - self.ang = Angle() - self.angvel = Angle() - --[[] - self:SetLerpValue(self.LerpValue or 0) - self:SetInterpolatePosition(self.InterpolatePosition or true) - self:SetInterpolateAngles(self.InterpolateAngles or true)]] end +function PART:OnShow() + +end +--NODES self 1 2 3 +--STAGE 0 1 2 3 +--PROPORTION 0 0.5 0 0.5 0 0.5 3 function PART:OnDraw() - self:GetWorldPosition() - self:GetWorldAngles() - --self:ModifiersPreEvent("OnDraw") - --self:ModifiersPostEvent("OnDraw") + local ent = self:GetOwner() + self.pos,self.ang = self:GetDrawPosition() + if not self.Test1 then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw") end + + local stage = math.max(0,math.floor(self.LerpValue)) + local proportion = math.max(0,self.LerpValue) % 1 + + if self.Test1 then + hook.Add("PostDrawOpaqueRenderables", "Multibone_draw", function() + render.DrawLine(self.pos,self.pos + self.ang:Forward()*150, Color(255,0,0)) + render.DrawLine(self.pos,self.pos - self.ang:Right()*150, Color(0,255,0)) + render.DrawLine(self.pos,self.pos + self.ang:Up()*150, Color(0,0,255)) + render.DrawWireframeSphere(self.pos, 20 + 5*math.sin(5*RealTime()), 15, 15, Color(255,255,255), true) + end) + end + self:Interpolate(stage,proportion) + + --[[self:PreEntityDraw(ent, pos, ang) + self:DrawModel(ent, pos, ang) + self:PostEntityDraw(ent, pos, ang)]] + ent:SetPos(self.pos) + ent:SetAngles(self.ang) + pac.ResetBones(ent) + --ent:DrawModel() end function PART:OnThink() - self:OnDraw() - - if self.Force000 then - print("forcing 0 0 0 world position") - self:SetWorldPos(0,0,0) - end - --self:GetWorldPosition() - --self:GetWorldAngles() - --self:GetDrawPosition() - --print(self.pos.x, self.pos.y, self.pos.z) - --print(self:GetDrawPosition().x, self:GetDrawPosition().y, self:GetDrawPosition().z) + if self.Node1 ~= nil then nodes["Node1"] = self.Node1 end + if self.Node2 ~= nil then nodes["Node2"] = self.Node2 end + if self.Node3 ~= nil then nodes["Node3"] = self.Node3 end + if self.Node4 ~= nil then nodes["Node4"] = self.Node4 end + if self.Node5 ~= nil then nodes["Node5"] = self.Node5 end end function PART:SetWorldPos(x,y,z) @@ -81,17 +94,46 @@ function PART:SetWorldPos(x,y,z) end function PART:Interpolate(stage, proportion) + --print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1)) + local firstnode + if stage <= 0 then + firstnode = self + else + firstnode = nodes["Node"..stage] or self + end + + + local secondnode = nodes["Node"..stage+1] + if firstnode == nil or firstnode == NULL then firstnode = self end + if secondnode == nil or secondnode == NULL then secondnode = self end + + if secondnode ~= nil and secondnode ~= NULL then + self.pos = (1-proportion)*firstnode:GetWorldPosition() + (secondnode:GetWorldPosition())*proportion + self.ang = (1-proportion)*firstnode:GetWorldAngles() + (secondnode:GetWorldAngles())*proportion + elseif proportion == 0 then + self.pos = firstnode:GetWorldPosition() + self.ang = firstnode:GetWorldAngles() + else + self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion + self.ang = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion + end + +end + +function PART:GoTo(part) + self.pos = part:GetWorldPosition() or self:GetWorldPosition() + self.ang = part:GetWorldAngles() or self:GetWorldAngles() end --we need to know the stage and proportion (progress) --e.g. lerp 0.5 is stage 0, proportion 0.5 because it's 50% toward Node 1 --e.g. lerp 2.2 is stage 2, proportion 0.2 because it's 20% toward Node 3 function PART:GetInterpolationParameters() - --[[stage = math.max(0,math.floor(self.LerpValue)) + stage = math.max(0,math.floor(self.LerpValue)) proportion = math.max(0,self.LerpValue) % 1 - print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1)) - print("proportion is " .. proportion) - return stage, proportion]]-- + --print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1)) + --print("proportion is " .. proportion) + return stage, proportion end function PART:GetNodeAngle(nodenumber) @@ -122,8 +164,6 @@ function PART:InterpolateAngle() end - - --[[function PART:ApplyMatrix() print("MATRIX???") local ent = self:GetOwner() @@ -143,13 +183,13 @@ end end end--]] -function PART:SetLerpValue(var) - --[[print("adjusted lerp value. "..type(var).." "..var) +--[[function PART:SetLerpValue(var) + print("adjusted lerp value. "..type(var).." "..var) self.LerpValue = var assert(self.LerpValue == var) assert(self.LerpValue ~= nil) - self:Interpolate(self:GetInterpolationParameters())]]-- -end + self:Interpolate(self:GetInterpolationParameters()) +end]] function PART:SetInterpolatePosition(b) --print(type(b).." "..b) diff --git a/lua/pac3/core/client/parts/legacy/entity.lua b/lua/pac3/core/client/parts/legacy/entity.lua index 3490e1302..5d3d240fc 100644 --- a/lua/pac3/core/client/parts/legacy/entity.lua +++ b/lua/pac3/core/client/parts/legacy/entity.lua @@ -89,7 +89,7 @@ function BUILDER:EntityField(name, field) end BUILDER:EntityField("InverseKinematics", "enable_ik") -BUILDER:EntityField("MuteFootsteps", "pac_mute_footsteps") +BUILDER:EntityField("MuteFootsteps", "mute_footsteps") BUILDER:EntityField("AnimationRate", "global_animation_rate") BUILDER:EntityField("RunSpeed", "run_speed") diff --git a/lua/pac3/core/client/parts/particles.lua b/lua/pac3/core/client/parts/particles.lua index 828e910c0..81c0f1e4b 100644 --- a/lua/pac3/core/client/parts/particles.lua +++ b/lua/pac3/core/client/parts/particles.lua @@ -37,9 +37,6 @@ BUILDER:StartStorableVars() BUILDER:GetSet("StickEndSize", 0) BUILDER:GetSet("StickStartAlpha", 255) BUILDER:GetSet("StickEndAlpha", 0) - BUILDER:SetPropertyGroup("attract") - BUILDER:GetSet("AttractPart") - BUILDER:GetSet("AttractForce", 0) BUILDER:SetPropertyGroup("appearance") BUILDER:GetSet("Material", "effects/slime1") BUILDER:GetSet("StartAlpha", 255) @@ -147,6 +144,7 @@ end function PART:OnShow(from_rendering) self.CanKeepFiring = true + self.FirstShot = true if not from_rendering then self.NextShot = 0 local pos, ang = self:GetDrawPosition() @@ -155,7 +153,7 @@ function PART:OnShow(from_rendering) end function PART:OnDraw() - + if not self.FireOnce then self.CanKeepFiring = true end local pos, ang = self:GetDrawPosition() local emitter = self:GetEmitter() @@ -212,7 +210,7 @@ function PART:SetMaterial(var) end function PART:EmitParticles(pos, ang, real_ang) - if not self.FireOnce then self.CanKeepFiring = true end + if self.FireOnce and not self.FirstShot then self.CanKeepFiring = false end local emt = self:GetEmitter() if not emt then return end @@ -358,13 +356,10 @@ function PART:EmitParticles(pos, ang, real_ang) end end - if self.FireDelay == 0 then - self.CanKeepFiring = false - else - self.NextShot = pac.RealTime + self.FireDelay - self.CanKeepFiring = true - end + + self.NextShot = pac.RealTime + self.FireDelay end + self.FirstShot = false end BUILDER:Register() diff --git a/lua/pac3/core/client/parts/player_config.lua b/lua/pac3/core/client/parts/player_config.lua index 77468adc6..7fc447de3 100644 --- a/lua/pac3/core/client/parts/player_config.lua +++ b/lua/pac3/core/client/parts/player_config.lua @@ -56,7 +56,7 @@ function BUILDER:EntityField(name, field) end BUILDER:EntityField("InverseKinematics", "enable_ik") -BUILDER:EntityField("MuteFootsteps", "pac_mute_footsteps") +BUILDER:EntityField("MuteFootsteps", "mute_footsteps") BUILDER:EntityField("AnimationRate", "global_animation_rate") BUILDER:EntityField("FallApartOnDeath", "death_physics_parts") BUILDER:EntityField("DeathRagdollizeParent", "death_ragdollize") diff --git a/lua/pac3/core/shared/entity_mutators/draw_shadow.lua b/lua/pac3/core/shared/entity_mutators/draw_shadow.lua new file mode 100644 index 000000000..331bcde73 --- /dev/null +++ b/lua/pac3/core/shared/entity_mutators/draw_shadow.lua @@ -0,0 +1,23 @@ +local MUTATOR = {} + +MUTATOR.ClassName = "draw_shadow" + +function MUTATOR:WriteArguments(enum) + net.WriteBool(enum, 8) +end + +function MUTATOR:ReadArguments() + return net.ReadBool() +end + +if SERVER then + function MUTATOR:StoreState() + return self.Entity:GetBloodColor() + end + + function MUTATOR:Mutate(enum) + self.Entity:SetBloodColor(enum) + end +end + +pac.emut.Register(MUTATOR) \ No newline at end of file diff --git a/lua/pac3/core/shared/footsteps_fix.lua b/lua/pac3/core/shared/footsteps_fix.lua index 1be8768f5..0b4b9410f 100644 --- a/lua/pac3/core/shared/footsteps_fix.lua +++ b/lua/pac3/core/shared/footsteps_fix.lua @@ -2,7 +2,13 @@ if game.SinglePlayer() then if SERVER then util.AddNetworkString('pac_footstep') + util.AddNetworkString('pac_footstep_request_state_update') + util.AddNetworkString('pac_signal_mute_footstep') + hook.Add("PlayerFootstep", "footstep_fix", function(ply, pos, _, snd, vol) + net.Start("pac_footstep_request_state_update") + net.Send(ply) + net.Start("pac_footstep") net.WriteEntity(ply) net.WriteVector(pos) @@ -10,6 +16,18 @@ if game.SinglePlayer() then net.WriteFloat(vol) net.Broadcast() end) + + net.Receive("pac_signal_mute_footstep", function(len,ply) + local b = net.ReadBool() + ply.pac_mute_footsteps = b + if ply.pac_mute_footsteps then + hook.Add("PlayerFootstep", "pac_footstep_silence", function() + return b + end) + else hook.Remove("PlayerFootstep", "pac_footstep_silence") end + end) + + end if CLIENT then @@ -23,6 +41,11 @@ if game.SinglePlayer() then hook.Run("pac_PlayerFootstep", ply, pos, snd, vol) end end) + net.Receive("pac_footstep_request_state_update", function() + net.Start("pac_signal_mute_footstep") + net.WriteBool(LocalPlayer().pac_mute_footsteps) + net.SendToServer() + end) end else hook.Add("PlayerFootstep", "footstep_fix", function(ply, pos, _, snd, vol) diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index 6c415af9c..84b852323 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -1,4 +1,4 @@ -include("pac3/editor/client/parts.lua") + local L = pace.LanguageString function pace.IsPartSendable(part) @@ -35,12 +35,6 @@ function pace.ClearParts() pac.RemoveAllParts(true, true) pace.RefreshTree() - --clean up the bulk selection - pace.ClearBulkList() - BulkSelectList = nil - hook.Remove('PreDrawHalos', "BulkSelectHighlights") - refresh_halo_hook = true - timer.Simple(0.1, function() if not pace.Editor:IsValid() then return end diff --git a/lua/pac3/editor/server/wear.lua b/lua/pac3/editor/server/wear.lua index f1a21e19e..3b1bd343f 100644 --- a/lua/pac3/editor/server/wear.lua +++ b/lua/pac3/editor/server/wear.lua @@ -334,8 +334,6 @@ function pace.HandleReceivedData(ply, data) data.owner = ply data.uid = pac.Hash(ply) - pac.Message("Received pac data from ", ply, " with ", data.totalParts or 0, " parts") - if data.wear_filter and #data.wear_filter > game.MaxPlayers() then pac.Message("Player ", ply, " tried to submit extraordinary wear filter size of ", #data.wear_filter, ", dropping.") data.wear_filter = nil From 6f33c422c07e5c9526ce9db56a2b2f7982c9fe1f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 18 Apr 2023 03:46:59 -0400 Subject: [PATCH 005/300] damage zone compatibility zeta bots lambda players drgbase etc... because they didn't get covered by ent:IsNPC(), but they have "npc_" in their name --- lua/pac3/extra/shared/net_combat.lua | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 6c538c102..eeecaa481 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1,5 +1,6 @@ local grab_consents = {} local damage_zone_consents = {} +local grab_pairs = {} local damage_types = { generic = 0, --generic damage @@ -54,7 +55,9 @@ if SERVER then util.AddNetworkString("pac_request_velocity_force_on_entity") util.AddNetworkString("pac_request_zone_damage") util.AddNetworkString("pac_signal_player_combat_consent") + util.AddNetworkString("pac_signal_stop_lock") util.AddNetworkString("pac_request_player_combat_consent_update") + net.Receive("pac_hitscan", function(len,ply) print("WE SHOULD DO A BULLET IN THE SERVER!") @@ -240,7 +243,7 @@ if SERVER then for _,ent in pairs(ents_hits) do if IsEntity(ent) then if (not tbl.AffectSelf) and ent == dmg_info:GetInflictor() then --nothing - elseif (ent:IsPlayer() and tbl.Players) or (ent:IsNPC() and tbl.NPC) or (ent:GetClass() == "prop_physics") then + elseif (ent:IsPlayer() and tbl.Players) or (ent:IsNPC() and tbl.NPC) or (string.find(ent:GetClass(), "npc") ~= nil) or (ent:GetClass() == "prop_physics") then --local oldvel = ent:GetVelocity() local ents2 = {dmg_info:GetInflictor()} if tbl.Bullet then @@ -288,6 +291,7 @@ if SERVER then local override_ang = net.ReadBool() local targ_ent = net.ReadEntity() local auth_ent = net.ReadEntity() + grab_pairs[auth_ent] = targ_ent if targ_ent:EntIndex() == 0 then return end if targ_ent ~= auth_ent and grab_consents[targ_ent] == false then return end @@ -331,6 +335,15 @@ if SERVER then end end) + net.Receive("pac_signal_stop_lock", function(len,ply) + for targ,ply in pairs(grab_pairs) do + if grab_pairs[ply] == targ then + net.Start("pac_request_lock_break") + net.Send(ply) + end + end + end) + concommand.Add("pac_refresh_consents", function() pac_combat_RefreshConsents() end) @@ -352,6 +365,10 @@ end if CLIENT then CreateConVar("pac_client_grab_consent", "0", true, true) CreateConVar("pac_client_damage_zone_consent", "0", true, true) + concommand.Add( "pac_stop_lock", function() + net.Start("pac_signal_stop_lock") + net.SendToServer() + end, "asks the server to breakup any lockpart hold on your player") net.Receive("pac_request_player_combat_consent_update", function() print("player receives request to update consents") net.Start("pac_signal_player_combat_consent") From 6c4c25a46cd57916d8a27fe0031977ed4668221d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 18 Apr 2023 03:48:46 -0400 Subject: [PATCH 006/300] quick fix also check "drg_" in the names --- lua/pac3/extra/shared/net_combat.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index eeecaa481..0b3b60aaf 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -243,7 +243,7 @@ if SERVER then for _,ent in pairs(ents_hits) do if IsEntity(ent) then if (not tbl.AffectSelf) and ent == dmg_info:GetInflictor() then --nothing - elseif (ent:IsPlayer() and tbl.Players) or (ent:IsNPC() and tbl.NPC) or (string.find(ent:GetClass(), "npc") ~= nil) or (ent:GetClass() == "prop_physics") then + elseif (ent:IsPlayer() and tbl.Players) or (ent:IsNPC() and tbl.NPC) or (string.find(ent:GetClass(), "npc") ~= nil) or (string.find(ent:GetClass(), "drg_") ~= nil) or (ent:GetClass() == "prop_physics") then --local oldvel = ent:GetVelocity() local ents2 = {dmg_info:GetInflictor()} if tbl.Bullet then From 107fd8c2dd6ffc66950dfe5b208fb6ecf58cce9f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 21 Apr 2023 00:31:29 -0400 Subject: [PATCH 007/300] general fixes pac_break_lock pac_stop_lock will ask the server to force the grabber to release the lock (locks will have a cooldown of 3 seconds and refresh the cooldown if they try again before the cooldown) consent cvars now properly saved server now automatically refreshes consents every 10 seconds reminder: multibone requires a group to carry its children SOMEHOW, temporary fix! --- .../client/parts/interpolated_multibone.lua | 42 ++++- lua/pac3/core/client/parts/lock.lua | 155 +++++++++++++----- lua/pac3/extra/shared/net_combat.lua | 50 ++++-- 3 files changed, 188 insertions(+), 59 deletions(-) diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index 2d6628745..5374ac7fa 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -30,6 +30,7 @@ BUILDER:StartStorableVars() :GetSet("Force000", false) :SetPropertyGroup("Interpolation") :GetSet("LerpValue",0) + :GetSet("Power",1) :GetSet("InterpolatePosition", true) :GetSet("InterpolateAngles", true) :SetPropertyGroup("Nodes") @@ -42,17 +43,38 @@ BUILDER:StartStorableVars() --PART:GetWorldPosition() --PART:GetWorldAngles() +function PART:OnRemove() + SafeRemoveEntityDelayed(self.Owner,0.1) +end + function PART:Initialize() print("a multiboner is born") + + self:SetOwner(pac.CreateEntity("models/pac/default.mdl")) + self.Owner:SetNoDraw(true) + pac.HookEntityRender(self.Owner, self) + pac.HookEntityRender(self.Owner, self) + self.Owner.PACPart = self end function PART:OnShow() end + +function PART:OnHide() + hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw") + +end + +function PART:OnRemove() + hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw") +end --NODES self 1 2 3 --STAGE 0 1 2 3 --PROPORTION 0 0.5 0 0.5 0 0.5 3 function PART:OnDraw() + + local ent = self:GetOwner() self.pos,self.ang = self:GetDrawPosition() if not self.Test1 then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw") end @@ -62,10 +84,10 @@ function PART:OnDraw() if self.Test1 then hook.Add("PostDrawOpaqueRenderables", "Multibone_draw", function() - render.DrawLine(self.pos,self.pos + self.ang:Forward()*150, Color(255,0,0)) - render.DrawLine(self.pos,self.pos - self.ang:Right()*150, Color(0,255,0)) - render.DrawLine(self.pos,self.pos + self.ang:Up()*150, Color(0,0,255)) - render.DrawWireframeSphere(self.pos, 20 + 5*math.sin(5*RealTime()), 15, 15, Color(255,255,255), true) + render.DrawLine(self.pos,self.pos + self.ang:Forward()*50, Color(255,0,0)) + render.DrawLine(self.pos,self.pos - self.ang:Right()*50, Color(0,255,0)) + render.DrawLine(self.pos,self.pos + self.ang:Up()*50, Color(0,0,255)) + render.DrawWireframeSphere(self.pos, 8 + 2*math.sin(5*RealTime()), 15, 15, Color(255,255,255), true) end) end self:Interpolate(stage,proportion) @@ -75,7 +97,8 @@ function PART:OnDraw() self:PostEntityDraw(ent, pos, ang)]] ent:SetPos(self.pos) ent:SetAngles(self.ang) - pac.ResetBones(ent) + --self:ShowFromRendering() + --pac.ResetBones(ent) --ent:DrawModel() end @@ -85,6 +108,8 @@ function PART:OnThink() if self.Node3 ~= nil then nodes["Node3"] = self.Node3 end if self.Node4 ~= nil then nodes["Node4"] = self.Node4 end if self.Node5 ~= nil then nodes["Node5"] = self.Node5 end + + end function PART:SetWorldPos(x,y,z) @@ -107,15 +132,16 @@ function PART:Interpolate(stage, proportion) if firstnode == nil or firstnode == NULL then firstnode = self end if secondnode == nil or secondnode == NULL then secondnode = self end + proportion = math.pow(proportion,self.Power) if secondnode ~= nil and secondnode ~= NULL then - self.pos = (1-proportion)*firstnode:GetWorldPosition() + (secondnode:GetWorldPosition())*proportion - self.ang = (1-proportion)*firstnode:GetWorldAngles() + (secondnode:GetWorldAngles())*proportion + self.pos = (1-proportion)*(firstnode:GetWorldPosition()) + (secondnode:GetWorldPosition())*proportion + self.ang = (1-proportion)*(firstnode:GetWorldAngles() + Angle(360,360,360)) + (secondnode:GetWorldAngles() + Angle(360,360,360))*proportion elseif proportion == 0 then self.pos = firstnode:GetWorldPosition() self.ang = firstnode:GetWorldAngles() else self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion - self.ang = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion + self.ang = (1-proportion)*(self:GetWorldAngles() + Angle(360,360,360)) + (self:GetWorldAngles() + Angle(360,360,360))*proportion end end diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index b0949f2ec..da004d7a1 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -18,20 +18,27 @@ PART.Icon = 'icon16/lock.png' BUILDER:StartStorableVars() - :SetPropertyGroup("Conditions") - :GetSet("Players", false) - :GetSet("PhysicsProps", false) - :GetSet("NPC", false) + :SetPropertyGroup("Behaviour") + :GetSet("Mode", "None", {enums = {["None"] = "None", ["Grab"] = "Grab", ["Teleport"] = "Teleport"}}) + :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"}) + :GetSet("RelativeGrab", false) + :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before grabbing are re-applied"}) + :SetPropertyGroup("DetectionOrigin") :GetSet("Radius", 20) :GetSet("RadiusOffsetDown", false, {description = "Lowers the detect origin by the radius distance"}) :GetSetPart("TargetPart") :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."}) - :SetPropertyGroup("Behaviour") - :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"}) - :GetSet("RelativeGrab", false) - :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before grabbing are re-applied"}) - :GetSet("Mode", "None", {enums = {["None"] = "None", ["Grab"] = "Grab", ["Teleport"] = "Teleport"}}) + + :SetPropertyGroup("PlayerCameraOverride") + :GetSet("OverrideEyeAngles", true, {description = "Whether the part will try to override players' eye angles. Requires OverrideAngles and user consent"}) + :GetSetPart("OverrideEyePositionPart") + + :SetPropertyGroup("Targets") + :GetSet("Players", false) + :GetSet("PhysicsProps", false) + :GetSet("NPC", false) + BUILDER:EndStorableVars() local valid_ent = false @@ -40,7 +47,19 @@ local last_request_time = SysTime() local last_entsearch = SysTime() local default_ang = Angle(0,0,0) +local forcebreak = false +local next_allowed_grab = SysTime() + function PART:OnThink() + + if SysTime() > next_allowed_grab then + forcebreak = false + elseif forcebreak then + valid_ent = false + self:reset_ent_ang() + target_ent = nil + return + end self:GetWorldPosition() self:GetWorldAngles() @@ -59,15 +78,19 @@ function PART:OnThink() --self:DecideTarget() if self.Mode == "Grab" then + if self.OverrideAngles then + default_ang = self.target_ent:GetAngles() + if self.OverrideEyeAngles then default_ang.y = self:GetWorldAngles().y end + end if not grabbing and not self.OverrideAngles then default_ang = self.target_ent:GetAngles() end - relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity() + local relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity() if not self.RelativeGrab then relative_transform_matrix = Matrix() relative_transform_matrix:Identity() end - offset_matrix = Matrix() + local offset_matrix = Matrix() offset_matrix:Translate(self:GetWorldPosition()) offset_matrix:Rotate(self:GetWorldAngles()) offset_matrix:Mul(relative_transform_matrix) @@ -75,76 +98,129 @@ function PART:OnThink() local relative_offset_pos = offset_matrix:GetTranslation() local relative_offset_ang = offset_matrix:GetAngles() - net.Start("pac_request_position_override_on_entity") - net.WriteVector(relative_offset_pos) - net.WriteAngle(relative_offset_ang) + if LocalPlayer() == self:GetPlayerOwner() then + net.Start("pac_request_position_override_on_entity") + if self.RelativeGrab then + net.WriteVector(relative_offset_pos) + net.WriteAngle(relative_offset_ang) + else + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + end + end + local can_rotate = self.OverrideAngles if self.target_ent:IsPlayer() then can_rotate = false end - net.WriteBool(can_rotate) - net.WriteEntity(self.target_ent) - net.WriteEntity(self:GetRootPart():GetOwner()) + if LocalPlayer() == self:GetPlayerOwner() then + net.WriteBool(can_rotate) + net.WriteEntity(self.target_ent) + net.WriteEntity(self:GetRootPart():GetOwner()) + net.SendToServer() + end --print(self:GetRootPart():GetOwner()) - net.SendToServer() - if self.Players and self.target_ent:IsPlayer() then + + if self.Players and self.target_ent:IsPlayer() and self.OverrideAngles then + local mat = Matrix() mat:Identity() - mat:Rotate(Angle(0,270,0)) + mat:Rotate(Angle(0,0,0)) mat:Rotate(self:GetWorldAngles()) + --mat:Rotate(Angle(self:GetWorldAngles().p,self:GetWorldAngles().y,self:GetWorldAngles().r)) self.target_ent:EnableMatrix("RenderMultiply", mat) + if self.OverrideEyeAngles then + --self.target_ent:SetEyeAngles(Angle(self:GetWorldAngles().p,default_ang.y,default_ang.r)) + end + if self.OverrideEyePositionPart then + --[[if self.OverrideEyePositionPart:IsValid() then + self.target_ent:SetCurrentViewOffset(self.OverrideEyePositionPart:GetWorldPosition() - self:GetWorldPosition()) + end]] + end + --if self.OverrideEyeAngles then self.target_ent:SetEyeAngles(self:GetWorldAngles()) end end + last_request_time = SysTime() grabbing = true teleported = false elseif self.Mode == "Teleport" and not teleported then self.target_ent = nil - net.Start("pac_request_position_override_on_entity") - net.WriteVector(self:GetWorldPosition()) + local ang_yaw_only = self:GetWorldAngles() ang_yaw_only.p = 0 ang_yaw_only.r = 0 - net.WriteAngle(ang_yaw_only) - net.WriteBool(self.OverrideAngles) - net.WriteEntity(self:GetPlayerOwner()) - net.WriteEntity(self:GetPlayerOwner()) - net.SendToServer() + if LocalPlayer() == self:GetPlayerOwner() then + net.Start("pac_request_position_override_on_entity") + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(ang_yaw_only) + net.WriteBool(self.OverrideAngles) + net.WriteEntity(self:GetPlayerOwner()) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() + end self:GetPlayerOwner():SetAngles( ang_yaw_only ) teleported = true grabbing = false end + + if CLIENT then + net.Receive("pac_request_lock_break", function(len) + local target_to_release = net.ReadEntity() + print("YOU'RE HOLDING " .. tostring(self.target_ent)) + pac.Message(Color(255, 50, 50), tostring(target_to_release) .. " WANTS TO BREAK FREE!!") + if self.target_ent == target_to_release then + forcebreak = true + next_allowed_grab = SysTime() + 3 + end + end) + end end ---continuous : keep trying until hit ---not : try only once function PART:OnShow() + + if self.Preview then + hook.Add("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview", function() + if self.RadiusOffsetDown then + render.DrawLine(self:GetWorldPosition(),self:GetWorldPosition() + Vector(0,0,-self.Radius),Color(255,255,255)) + render.DrawWireframeSphere(self:GetWorldPosition() + Vector(0,0,-self.Radius), self.Radius, 30, 30, Color(255,255,255),true) + else render.DrawWireframeSphere(self:GetWorldPosition(), self.Radius, 30, 30, Color(255,255,255),true) end + end) + end self.target_ent = nil - self.relative_transform_matrix = Matrix():Identity() + --self.relative_transform_matrix = Matrix():Identity() self:DecideTarget() self:CheckEntValidity() - self:CalculateRelativeOffset() + --self:CalculateRelativeOffset() end function PART:OnHide() + hook.Remove("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview") teleported = false grabbing = false if self.target_ent == nil then return end timer.Simple(math.min(self.RestoreDelay,5), function() if self.target_ent == nil then return end - if self.target_ent:IsValid() then + self:reset_ent_ang() + end) +end + +function PART:reset_ent_ang() + if self.target_ent:IsValid() then + --if self.target_ent:GetClass() == "prop_physics" then return end + if LocalPlayer() == self:GetPlayerOwner() then net.Start("pac_request_angle_reset_on_entity") net.WriteAngle(default_ang) net.WriteFloat(self.RestoreDelay) net.WriteEntity(self.target_ent) net.WriteEntity(self:GetPlayerOwner()) net.SendToServer() - if self.Players then - self.target_ent:DisableMatrix("RenderMultiply") - end end - end) + if self.Players then + self.target_ent:DisableMatrix("RenderMultiply") + end + end end function PART:OnRemove() @@ -155,14 +231,15 @@ function PART:DecideTarget() local ents = ents.GetAll() local ents_candidates = {} local chosen_ent = nil + local target_part = self.TargetPart --filter entities for i, ent_candidate in ipairs(ents) do --print(ent_candidate:GetClass()) if ent_candidate:IsValid() then local origin - if self.TargetPart and self.TargetPart:IsValid() then - origin = self.TargetPart:GetWorldPosition() + if self.TargetPart and (self.TargetPart):IsValid() then + origin = (self.TargetPart):GetWorldPosition() else origin = self:GetWorldPosition() end @@ -179,7 +256,7 @@ function PART:DecideTarget() chosen_ent = ent_candidate table.insert(ents_candidates, ent_candidate) end - elseif self.PhysicsProps and ent_candidate:GetClass() == "prop_physics" then + elseif self.PhysicsProps and (ent_candidate:GetClass() == "prop_physics" or ent_candidate:GetClass() == "prop_ragdoll") then chosen_ent = ent_candidate table.insert(ents_candidates, ent_candidate) elseif self.NPC and ent_candidate:IsNPC() then diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 0b3b60aaf..e3cc2c0c7 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1,6 +1,7 @@ local grab_consents = {} local damage_zone_consents = {} local grab_pairs = {} +local pairs_grabbed_by = {} local damage_types = { generic = 0, --generic damage @@ -56,6 +57,7 @@ if SERVER then util.AddNetworkString("pac_request_zone_damage") util.AddNetworkString("pac_signal_player_combat_consent") util.AddNetworkString("pac_signal_stop_lock") + util.AddNetworkString("pac_request_lock_break") util.AddNetworkString("pac_request_player_combat_consent_update") @@ -81,7 +83,6 @@ if SERVER then dmg_info:SetAttacker(ply_ent) dmg_info:SetInflictor(ply_ent) --print("entity: ",ply_ent) - if tbl.OverrideKnockback then print("should override") end dmg_info:SetDamageType(damage_types[tbl.DamageType]) --print(tbl.DamageType .. " resolves to " .. damage_types[tbl.DamageType]) @@ -291,7 +292,11 @@ if SERVER then local override_ang = net.ReadBool() local targ_ent = net.ReadEntity() local auth_ent = net.ReadEntity() - grab_pairs[auth_ent] = targ_ent + if not grab_pairs[auth_ent] then + grab_pairs[auth_ent] = targ_ent + pairs_grabbed_by[targ_ent] = auth_ent + print(auth_ent, "grabs", targ_ent) + end if targ_ent:EntIndex() == 0 then return end if targ_ent ~= auth_ent and grab_consents[targ_ent] == false then return end @@ -304,6 +309,8 @@ if SERVER then targ_ent:SetPos(pos) if targ_ent:IsPlayer() then targ_ent:SetVelocity(-targ_ent:GetVelocity()) end + + if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end end) net.Receive("pac_request_angle_reset_on_entity", function(len, ply) @@ -313,6 +320,8 @@ if SERVER then local auth_ent = net.ReadEntity() targ_ent:SetAngles(ang) + targ_ent:PhysWake() + table.RemoveByValue(grab_pairs,auth_ent) end) net.Receive("pac_request_velocity_force_on_entity", function(len,ply) @@ -336,10 +345,21 @@ if SERVER then end) net.Receive("pac_signal_stop_lock", function(len,ply) - for targ,ply in pairs(grab_pairs) do - if grab_pairs[ply] == targ then - net.Start("pac_request_lock_break") - net.Send(ply) + print("Requesting lock break") + PrintTable(grab_pairs) + for p,targ in pairs(grab_pairs) do + if grab_pairs[p] == targ and ply == targ then + print("from",p) + if p:IsPlayer() then + net.Start("pac_request_lock_break") + net.WriteEntity(ply) + net.Send(p) + elseif targ:IsPlayer() then + net.Start("pac_request_lock_break") + net.WriteEntity(ply) + net.Send(targ) + end + end end end) @@ -353,24 +373,30 @@ if SERVER then if ent:IsPlayer() then net.Start("pac_request_player_combat_consent_update") net.Send(ent) - print(ent, "does that player consent grabs?", grab_consents[ent], "and damage zone?", damage_zone_consents[ent]) + --print(ent, "does that player consent grabs?", grab_consents[ent], "and damage zone?", damage_zone_consents[ent]) end end - PrintTable(grab_consents) end - + timer.Create("pac_timer_refresh_client_consents", 10, 0, function() pac_combat_RefreshConsents() end) end if CLIENT then - CreateConVar("pac_client_grab_consent", "0", true, true) - CreateConVar("pac_client_damage_zone_consent", "0", true, true) + CreateConVar("pac_client_grab_consent", "0", FCVAR_ARCHIVE, true) + CreateConVar("pac_client_damage_zone_consent", "0", FCVAR_ARCHIVE, true) + concommand.Add( "pac_stop_lock", function() net.Start("pac_signal_stop_lock") net.SendToServer() end, "asks the server to breakup any lockpart hold on your player") + + concommand.Add( "pac_break_lock", function() + net.Start("pac_signal_stop_lock") + net.SendToServer() + end, "asks the server to breakup any lockpart hold on your player") + net.Receive("pac_request_player_combat_consent_update", function() - print("player receives request to update consents") + --print("player receives request to update consents") net.Start("pac_signal_player_combat_consent") net.WriteString("grab") net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) From 6c3cc38ecd86aa16a83c183e852b9745251a0c63 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 21 Apr 2023 23:48:48 -0400 Subject: [PATCH 008/300] merge from upstream and current changes --- lua/pac3/core/client/parts/beam.lua | 14 +- lua/pac3/core/client/parts/damage_zone.lua | 2 - lua/pac3/core/client/parts/event.lua | 73 +- lua/pac3/core/client/parts/lock.lua | 11 +- lua/pac3/core/client/parts/model.lua | 17 +- lua/pac3/core/client/parts/proxy.lua | 116 +-- lua/pac3/core/client/parts/text.lua | 7 +- lua/pac3/core/shared/util.lua | 22 +- lua/pac3/editor/client/examples.lua | 805 +++++++++++++++------ lua/pac3/editor/client/panels/editor.lua | 2 +- lua/pac3/editor/client/parts.lua | 405 ++++++++++- lua/pac3/editor/client/wear.lua | 2 +- lua/pac3/editor/server/wear.lua | 2 + 13 files changed, 1095 insertions(+), 383 deletions(-) diff --git a/lua/pac3/core/client/parts/beam.lua b/lua/pac3/core/client/parts/beam.lua index 3793347bf..6ca97ca93 100644 --- a/lua/pac3/core/client/parts/beam.lua +++ b/lua/pac3/core/client/parts/beam.lua @@ -24,7 +24,7 @@ do local vector = Vector() local color = Color(255, 255, 255, 255) - function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size) + function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size, width_start_mul, width_end_mul) if not veca or not vecb or not dira or not dirb then return end @@ -44,6 +44,8 @@ do width_bend = width_bend or 0 width_bend_size = width_bend_size or 1 tex_scroll = tex_scroll or 0 + width_start_mul = width_start_mul or 1 + width_end_mul = width_end_mul or 1 render_StartBeam(res + 1) @@ -64,7 +66,7 @@ do render_AddBeam( vector, - width + ((math_sin(wave) ^ width_bend_size) * width_bend), + (width + ((math_sin(wave) ^ width_bend_size) * width_bend)) * Lerp(frac, width_start_mul, width_end_mul), (i / tex_stretch) + tex_scroll, color ) @@ -80,6 +82,7 @@ local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "beam" PART.Group = 'effects' PART.Icon = 'icon16/vector.png' +PART.HandleModifiersManually = true BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") @@ -94,6 +97,8 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Width", 1) BUILDER:GetSet("WidthBend", 0) BUILDER:GetSet("WidthBendSize", 1) + BUILDER:GetSet("StartWidthMultiplier", 1) + BUILDER:GetSet("EndWidthMultiplier", 1) BUILDER:GetSet("TextureStretch", 1) BUILDER:GetSet("TextureScroll", 0) BUILDER:SetPropertyGroup("orientation") @@ -196,6 +201,7 @@ end function PART:OnDraw() local part = self.EndPoint + if self.Materialm and self.StartColorC and self.EndColorC and part:IsValid() and part.GetWorldPosition then local pos, ang = self:GetDrawPosition() render.SetMaterial(self.Materialm) @@ -215,7 +221,9 @@ function PART:OnDraw() self.TextureStretch, self.TextureScroll, self.WidthBend, - self.WidthBendSize + self.WidthBendSize, + self.StartWidthMultiplier, + self.EndWidthMultiplier ) end end diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 83a6a69d0..bcb9e66e9 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -45,8 +45,6 @@ BUILDER:StartStorableVars() :GetSet("PhaseRandomize", 1) :SetPropertyGroup("Behaviour") :GetSet("Delay", 0) - :GetSet("OverrideKnockback", true) - :GetSet("KnockbackAmount", Vector(0,0,0)) :SetPropertyGroup("Preview Rendering") :GetSet("NoPreview", false) :GetSet("RenderingHook", "PostDrawOpaqueRenderables", {enums = { diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 4ba8f4f1c..ae785353d 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -26,7 +26,7 @@ BUILDER:StartStorableVars() return output end}) BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end}) - BUILDER:GetSet("Arguments", "", {hidden = false}) + BUILDER:GetSet("Arguments", "", {hidden = true}) BUILDER:GetSet("Invert", true) BUILDER:GetSet("RootOwner", true) BUILDER:GetSet("AffectChildrenOnly", false) @@ -35,12 +35,12 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:SetEvent(event) - local reset = (self.Arguments == "") or + local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) self.Event = event self:SetWarning() - self:GetDynamicProperties(reset) + self:GetDynamicProperties(reset) end local function get_default(typ) @@ -95,8 +95,8 @@ function PART:GetDynamicProperties(reset_to_default) } local arg = tbl[key] - if arg.get() == nil or reset_to_default then - if udata.default then + if arg.get() == nil or reset_to_default then + if udata.default then arg.set(udata.default) else arg.set(nil) @@ -204,10 +204,6 @@ PART.OldEvents = { timerx = { arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}}, - userdata = { - {default = 0, timerx_property = "seconds"}, - {default = true, timerx_property = "reset_on_hide"} - }, nice = function(self, ent, seconds) return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?" end, @@ -494,16 +490,8 @@ PART.OldEvents = { ranger = { arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}}, - userdata = { - {default = 15, ranger_property = "distance"}, - {default = 5, ranger_property = "compare"}, - {default = false, ranger_property = "npcs_and_players_only"} - }, + userdata = {{editor_panel = "ranger", ranger_property = "distance"}, {editor_panel = "ranger", ranger_property = "compare"}}, callback = function(self, ent, distance, compare, npcs_and_players_only) - if npcs_and_players_only == nil then - self.Arguments = self.Arguments .. "@@0" - self.npcs_and_players_only = false - end local parent = self:GetParentEx() if parent:IsValid() and parent.GetWorldPosition then @@ -584,45 +572,12 @@ PART.OldEvents = { endpos = startpos, maxs = maxs, mins = mins, - filter = {self:GetRootPart():GetOwner(),ent} - } ) - return tr.Hit - end, - }, - - is_touching_scalable = { - arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}}, - userdata = {{editor_panel = "is_touching"}, {x = "x_stretch"}, {y = "y_stretch"}, {z = "z_stretch"}}, - callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch) - extra_radius = extra_radius or 0 - x_stretch = x_stretch or 1 - y_stretch = y_stretch or 1 - z_stretch = z_stretch or 1 - local mins = Vector(-x_stretch,-y_stretch,-z_stretch) - local maxs = Vector(x_stretch,y_stretch,z_stretch) - local startpos = ent:WorldSpaceCenter() - - radius = math.max(extra_radius, 1) - mins = mins * radius - maxs = maxs * radius - - local tr = util.TraceHull( { - start = startpos, - endpos = startpos, - maxs = maxs, - mins = mins, - filter = {self:GetRootPart():GetOwner(),ent} + filter = ent } ) return tr.Hit end, }, - is_explicit = { - callback = function(self, ent) - return GetConVar("pac_hide_disturbing"):GetBool() - end - }, - is_in_noclip = { callback = function(self, ent) ent = try_viewmodel(ent) @@ -869,8 +824,8 @@ PART.OldEvents = { command = { arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}}, userdata = { - {default = "change_me", editor_friendly = "CommandName"}, - {default = 0.1, editor_friendly = "EventDuration"}, + {default = "change_me", editor_friendly = "CommandName"}, + {default = 0.1, editor_friendly = "EventDuration"}, {default = false, group = "event wheel", editor_friendly = "HideInEventWheel"} }, nice = function(self, ent, find, time) @@ -879,14 +834,6 @@ PART.OldEvents = { return "command: " .. find .. " | " .. "duration: " .. time end, callback = function(self, ent, find, time) - if time == nil then - self.Arguments = self.Arguments .. "@@0" - time = 0 - end - if hide_in_eventwheel == nil then - self.Arguments = self.Arguments .. "@@0" - hide_in_eventwheel = false - end time = time or 0.1 local ply = self:GetPlayerOwner() @@ -2260,4 +2207,4 @@ do concommand.Add("+pac_events", pac.openEventSelectionWheel) concommand.Add("-pac_events", pac.closeEventSelectionWheel) -end \ No newline at end of file +end diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index da004d7a1..927fda426 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -29,6 +29,7 @@ BUILDER:StartStorableVars() :GetSet("RadiusOffsetDown", false, {description = "Lowers the detect origin by the radius distance"}) :GetSetPart("TargetPart") :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."}) + :GetSet("Preview", false) :SetPropertyGroup("PlayerCameraOverride") :GetSet("OverrideEyeAngles", true, {description = "Whether the part will try to override players' eye angles. Requires OverrideAngles and user consent"}) @@ -179,13 +180,13 @@ end function PART:OnShow() - + local targ = self.TargetPart or self if self.Preview then hook.Add("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview", function() if self.RadiusOffsetDown then - render.DrawLine(self:GetWorldPosition(),self:GetWorldPosition() + Vector(0,0,-self.Radius),Color(255,255,255)) - render.DrawWireframeSphere(self:GetWorldPosition() + Vector(0,0,-self.Radius), self.Radius, 30, 30, Color(255,255,255),true) - else render.DrawWireframeSphere(self:GetWorldPosition(), self.Radius, 30, 30, Color(255,255,255),true) end + render.DrawLine(targ:GetWorldPosition(),targ:GetWorldPosition() + Vector(0,0,-self.Radius),Color(255,255,255)) + render.DrawWireframeSphere(targ:GetWorldPosition() + Vector(0,0,-self.Radius), self.Radius, 30, 30, Color(255,255,255),true) + else render.DrawWireframeSphere(targ:GetWorldPosition(), self.Radius, 30, 30, Color(255,255,255),true) end end) end self.target_ent = nil @@ -211,7 +212,7 @@ function PART:reset_ent_ang() --if self.target_ent:GetClass() == "prop_physics" then return end if LocalPlayer() == self:GetPlayerOwner() then net.Start("pac_request_angle_reset_on_entity") - net.WriteAngle(default_ang) + net.WriteAngle(Angle(0,0,0)) net.WriteFloat(self.RestoreDelay) net.WriteEntity(self.target_ent) net.WriteEntity(self:GetPlayerOwner()) diff --git a/lua/pac3/core/client/parts/model.lua b/lua/pac3/core/client/parts/model.lua index 92b28825f..318729295 100644 --- a/lua/pac3/core/client/parts/model.lua +++ b/lua/pac3/core/client/parts/model.lua @@ -1,3 +1,8 @@ +CreateConVar( "pac_model_max_scales", "10000", FCVAR_ARCHIVE, "Maximum scales model can have") + + + + local pac = pac local render_SetColorModulation = render.SetColorModulation @@ -742,9 +747,15 @@ function PART:SetAlternativeScaling(b) end function PART:SetScale(vec) - if vec then - vec = Vector(math.Clamp(vec.x, -100, 100), math.Clamp(vec.y, -100, 100), math.Clamp(vec.z, -100, 100)) - end + max_scale = GetConVar("pac_model_max_scales"):GetFloat() + largest_scale = math.max(math.abs(vec.x), math.abs(vec.y), math.abs(vec.z)) + if vec and not LocalPlayer() == self:GetPlayerOwner() and max_scale > 0 then --clamp for other players if they have pac_model_max_scales convar more than 0 + vec = Vector(math.Clamp(vec.x, -max_scale, max_scale), math.Clamp(vec.y, -max_scale, max_scale), math.Clamp(vec.z, -max_scale, max_scale)) + elseif largest_scale > 10000 then --warn about the default max scale + pac.Message("Your model ", self, " scale is beyond the default limit! It will be limited on other clients' rendering. But then again you probably shouldn't have gigantic scales to begin with.") + self:SetError("Scale is being limited due to having an excessive component") + else self:SetError() end --if ok, clear the warning + vec = vec or Vector(1,1,1) self.Scale = vec diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 3f826cdc8..014fbb7d4 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -458,77 +458,6 @@ do -- end end - - - ---[[ -self.truevel_ent = nil -self.truevel_last_ent = nil -self.truevel_next_log = 0 -do --true velocity (tm) - function seek_past_neighboring_timestamp() - local offset = math.floor(self.VelocityRoughness) - local begin_seek_position = math.floor(SysTime() * 100) - if self.truevel_ent.position_timestamps[begin_seek_position - offset] ~= nil then - return self.truevel_ent.position_timestamps[begin_seek_position - offset] - else - local latest_past_timestamp - for stamp,logged_pos in ipairs(self.truevel_ent.position_timestamps) do - if self.truevel_ent.position_timestamps[stamp] ~= nil then - if stamp < begin_seek_position - offset then - - end - end - end - end - end - - function PART:GetTrueVelocity() - self.true_vel = self.true_vel or Vector() - local systime = SysTime() - local logtime = math.floor(systime * 100) - self.truevel_next_log = self.truevel_next_log or systime - if systime < self.truevel_next_log then return self.true_vel - else self.truevel_next_log = systime + 0.05 end - print("log time ", logtime) - if self.truevel_ent then self.truevel_ent.position_timestamps = ent.position_timestamps or {} end - - self.last_pos = seek_past_neighboring_timestamp() - - local pos - - if self.RootOwner then - self.truevel_ent = self:GetRootPart():GetOwner() - pos = self:GetRootPart():GetOwner():GetPos() - elseif self.truevel_ent ~= nil then - self.truevel_ent = self:GetOwner() - pos = self:GetOwner():GetWorldPosition() or self:GetOwner():GetPos() - end - - self.truevel_last_ent = self.truevel_ent - self.truevel_ent.position_timestamps[logtime] = pos - - self.true_vel = pos - self.truevel_ent.position_timestamps[seek_past_neighboring_timestamp()] - - return self.true_vel or Vector() - end - - PART.Inputs.owner_true_velocity_length = function(self) - - if self:GetPhysicalTarget():IsValid() then - ent = self:GetPhysicalTarget() - end - if self.RootOwner then - ent = self:GetRootPart():GetOwner() - end - return self:GetTrueVelocity():Length() - end -end -]] - - - - do -- velocity PART.Inputs.parent_velocity_length = function(self) return self:GetVelocity(self:GetPhysicalTarget()):Length() @@ -581,23 +510,12 @@ end PART.Inputs.pose_parameter = function(self, name) if not name then return 0 end - local owner = self:GetPlayerOwner() + local owner = get_owner(self) if owner:IsValid() and owner.GetPoseParameter then return owner:GetPoseParameter(name) end return 0 end -PART.Inputs.pose_parameter_true = function(self, name) - if not name then return 0 end - local owner = self:GetPlayerOwner() - if owner:IsValid() then - mini,maxi = owner:GetPoseParameterRange(owner:LookupPoseParameter(name)) - actual_value = mini + (maxi - mini)*(owner:GetPoseParameter(name)) - return actual_value - else end - return 0 -end - PART.Inputs.command = function(self) local ply = self:GetPlayerOwner() if ply.pac_proxy_events then @@ -837,6 +755,38 @@ do end end +PART.Inputs.flat_dot_forward = function(self) + local part = get_owner(self) + + if part:IsValid() then + local ang = part:IsPlayer() and part:EyeAngles() or part:GetAngles() + ang.p = 0 + ang.r = 0 + local dir = pac.EyePos - part:EyePos() + dir[3] = 0 + dir:Normalize() + return dir:Dot(ang:Forward()) + end + + return 0 +end + +PART.Inputs.flat_dot_right = function(self) + local part = get_owner(self) + + if part:IsValid() then + local ang = part:IsPlayer() and part:EyeAngles() or part:GetAngles() + ang.p = 0 + ang.r = 0 + local dir = pac.EyePos - part:EyePos() + dir[3] = 0 + dir:Normalize() + return dir:Dot(ang:Right()) + end + + return 0 +end + net.Receive("pac_proxy", function() local ply = net.ReadEntity() local str = net.ReadString() diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index 258bcfdcd..2bfeb7389 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -102,7 +102,7 @@ end function PART:SetFont(str) if not pcall(surface_SetFont, str) then - pac.Message(Color(255,150,0),"[PAC3] "..str.." Font not found! Reverting to DermaDefault!") + pac.Message(Color(255,150,0),str.." Font not found! Reverting to DermaDefault!") str = "DermaDefault" end @@ -158,13 +158,8 @@ function PART:OnDraw() end end -function PART:OnThink() - self:OnDraw() -end - function PART:OnShow() self.time = CurTime() - self:OnDraw() end function PART:SetText(str) diff --git a/lua/pac3/core/shared/util.lua b/lua/pac3/core/shared/util.lua index 6e8e7d0a9..37e4d0cce 100644 --- a/lua/pac3/core/shared/util.lua +++ b/lua/pac3/core/shared/util.lua @@ -135,6 +135,8 @@ for _, params in pairs(shader_params.base) do end end +texture_keys["include"] = "include" + -- for pac_restart PAC_MDL_SALT = PAC_MDL_SALT or 0 @@ -463,6 +465,7 @@ function pac.DownloadMDL(url, callback, onfail, ply) table.insert(found_vmt_directories, {dir = dir}) f:seek(old_pos) end + table.sort(found_vmt_directories, function(a,b) return #a.dir>#b.dir end) f:seek(old_pos) end @@ -601,7 +604,7 @@ function pac.DownloadMDL(url, callback, onfail, ply) end for shader_param in pairs(texture_keys) do - data.buffer = data.buffer:gsub('("?%$' .. shader_param .. '"?%s+")(.-)(")', function(l, vtf_path, r) + data.buffer = data.buffer:gsub('("?%$?%f[%w_]' .. shader_param .. '%f[^%w_]"?%s+"?)([^"%c]+)("?%s?)', function(l, vtf_path, r) if vtf_path == "env_cubemap" then return end @@ -618,12 +621,19 @@ function pac.DownloadMDL(url, callback, onfail, ply) end end - for _, info in ipairs(files) do - if info.file_name:EndsWith(".vtf") then + if not new_path then + for _, info in ipairs(files) do local vtf_name = (vtf_path:match(".+/(.+)") or vtf_path) - if info.file_name == vtf_name .. ".vtf" then - new_path = dir .. vtf_name - break + if info.file_name:EndsWith(".vtf") then + if info.file_name == vtf_name .. ".vtf" or info.file_name == vtf_name then + new_path = dir .. vtf_name + break + end + elseif (info.file_name:EndsWith(".vmt") and l:StartWith("include")) then + if info.file_name == vtf_name then + new_path = "materials/" .. dir .. vtf_name + break + end end end end diff --git a/lua/pac3/editor/client/examples.lua b/lua/pac3/editor/client/examples.lua index c940787f1..d6b399c25 100644 --- a/lua/pac3/editor/client/examples.lua +++ b/lua/pac3/editor/client/examples.lua @@ -1931,238 +1931,631 @@ pace.example_outfits["skis"] = { } pace.example_outfits["southpark"] = { -[1] = { - ["children"] = { - [1] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["ClassName"] = "bone", - ["UniqueID"] = "3513732839", - ["Size"] = 0, + [1] = { + ["children"] = { + [1] = { + ["children"] = { + [1] = { + ["children"] = { + }, + ["self"] = { + ["Jiggle"] = false, + ["DrawOrder"] = 0, + ["UniqueID"] = "3513732839", + ["TargetEntityUID"] = "", + ["AimPartName"] = "", + ["FollowPartUID"] = "", + ["Bone"] = "head", + ["ScaleChildren"] = false, + ["AngleOffset"] = Angle(0, 0, 0), + ["MoveChildrenToOrigin"] = false, + ["Position"] = Vector(0, 0, 0), + ["AimPartUID"] = "", + ["Angles"] = Angle(0, 0, 0), + ["Hide"] = false, + ["Name"] = "", + ["Scale"] = Vector(1, 1, 1), + ["EditorExpand"] = false, + ["ClassName"] = "bone", + ["Size"] = 0, + ["PositionOffset"] = Vector(0, 0, 0), + ["IsDisturbing"] = false, + ["AlternativeBones"] = false, + ["EyeAngles"] = false, + ["FollowAnglesOnly"] = false, + }, }, }, + ["self"] = { + ["HidePhysgunBeam"] = false, + ["Skin"] = 0, + ["UniqueID"] = "2782871480", + ["HideBullets"] = false, + ["FallApartOnDeath"] = false, + ["DeathRagdollizeParent"] = false, + ["WalkSpeed"] = 0, + ["BlendMode"] = "", + ["EyeAngles"] = false, + ["HideEntity"] = false, + ["AimPartUID"] = "", + ["Model"] = "", + ["LodOverride"] = -1, + ["Name"] = "", + ["MuteSounds"] = false, + ["AllowOggWhenMuted"] = false, + ["AngleOffset"] = Angle(0, 0, 0), + ["InverseKinematics"] = false, + ["PositionOffset"] = Vector(0, 0, 0), + ["Color"] = Vector(255, 255, 255), + ["Fullbright"] = true, + ["Brightness"] = 1, + ["DoubleFace"] = false, + ["IgnoreZ"] = false, + ["DrawOrder"] = 0, + ["EditorExpand"] = true, + ["HideRagdollOnDeath"] = false, + ["IsDisturbing"] = false, + ["RelativeBones"] = true, + ["TargetEntityUID"] = "", + ["DrawShadow"] = true, + ["Alpha"] = 0, + ["Material"] = "", + ["Translucent"] = false, + ["SuppressFrames"] = false, + ["CrouchSpeed"] = 0, + ["Bone"] = "head", + ["NoTextureFiltering"] = false, + ["MuteFootsteps"] = false, + ["Invert"] = false, + ["DrawWeapon"] = true, + ["Position"] = Vector(0, 0, 0), + ["DrawPlayerOnDeath"] = false, + ["Weapon"] = false, + ["Hide"] = false, + ["Angles"] = Angle(0, 0, 0), + ["Scale"] = Vector(1, 1, 1), + ["AimPartName"] = "", + ["RunSpeed"] = 0, + ["Size"] = 0.44, + ["UseLegacyScale"] = false, + ["ClassName"] = "entity", + ["AnimationRate"] = 1, + ["EyeTargetUID"] = "", + ["SprintSpeed"] = 0, + }, }, - ["self"] = { - ["Alpha"] = 0, - ["EditorExpand"] = true, - ["UniqueID"] = "2782871480", - ["Fullbright"] = true, - ["Size"] = 0.44, - ["ClassName"] = "entity", - }, - }, - [2] = { - ["children"] = { - [1] = { - ["children"] = { - [1] = { - ["children"] = { + [2] = { + ["children"] = { + [1] = { + ["children"] = { + [1] = { + ["children"] = { + }, + ["self"] = { + ["DrawOrder"] = 0, + ["UniqueID"] = "3210564156", + ["Axis"] = "", + ["Input"] = "owner_velocity_length_increase", + ["TargetPartUID"] = "", + ["InputMultiplier"] = 1, + ["RootOwner"] = true, + ["TargetEntityUID"] = "", + ["ZeroEyePitch"] = false, + ["ClassName"] = "proxy", + ["ResetVelocitiesOnHide"] = true, + ["VelocityRoughness"] = 10, + ["Max"] = 1, + ["Pow"] = 1, + ["EditorExpand"] = false, + ["AffectChildren"] = false, + ["Min"] = 0, + ["Hide"] = false, + ["Name"] = "", + ["VariableName"] = "AngleOffset", + ["Offset"] = 0, + ["PlayerAngles"] = false, + ["Additive"] = false, + ["InputDivider"] = 1, + ["IsDisturbing"] = false, + ["OutputTargetPartUID"] = "", + ["Function"] = "sin", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + }, }, - ["self"] = { - ["RootOwner"] = true, - ["UniqueID"] = "3210564156", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - ["ClassName"] = "proxy", - ["Input"] = "owner_velocity_length_increase", - ["VariableName"] = "AngleOffset", + [2] = { + ["children"] = { + }, + ["self"] = { + ["AffectChildrenOnly"] = false, + ["DrawOrder"] = 0, + ["TargetPartUID"] = "", + ["Name"] = "", + ["Event"] = "flat_dot_right", + ["Hide"] = false, + ["TargetEntityUID"] = "", + ["RootOwner"] = true, + ["EditorExpand"] = true, + ["ClassName"] = "event", + ["Arguments"] = "-0.707", + ["Invert"] = true, + ["IsDisturbing"] = false, + ["Operator"] = "below", + ["UniqueID"] = "1222338801", + ["ZeroEyePitch"] = false, + }, }, }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["Arguments"] = "-0.7", - ["UniqueID"] = "1222338801", - ["Event"] = "dot_right", - ["Operator"] = "above", - ["ClassName"] = "event", - ["EditorExpand"] = true, - }, + ["self"] = { + ["Skin"] = 0, + ["Invert"] = true, + ["LightBlend"] = 1, + ["CellShade"] = 0, + ["AimPartName"] = "LOCALEYES_YAW", + ["IgnoreZ"] = false, + ["AimPartUID"] = "", + ["Passes"] = 1, + ["Name"] = "left", + ["Angles"] = Angle(0, 0, 0), + ["DoubleFace"] = false, + ["PositionOffset"] = Vector(0, 0, 0), + ["BlurLength"] = 0, + ["OwnerEntity"] = false, + ["Brightness"] = 1, + ["DrawOrder"] = 0, + ["BlendMode"] = "", + ["TintColor"] = Vector(0, 0, 0), + ["Alpha"] = 0.999, + ["LodOverride"] = -1, + ["TargetEntityUID"] = "", + ["BlurSpacing"] = 0, + ["UsePlayerColor"] = false, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", + ["UseWeaponColor"] = false, + ["EyeAngles"] = false, + ["UseLegacyScale"] = false, + ["Bone"] = "none", + ["Color"] = Vector(255, 255, 255), + ["Fullbright"] = true, + ["BoneMerge"] = false, + ["IsDisturbing"] = false, + ["Position"] = Vector(0, 0, 0), + ["NoTextureFiltering"] = false, + ["AlternativeScaling"] = false, + ["Hide"] = false, + ["Translucent"] = true, + ["Scale"] = Vector(1, 1, -0.0099999997764826), + ["ClassName"] = "model", + ["EditorExpand"] = true, + ["Size"] = 1, + ["ModelFallback"] = "", + ["AngleOffset"] = Angle(90, 90, 90), + ["TextureFilter"] = 3, + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["UniqueID"] = "537182332", }, }, - ["self"] = { - ["Invert"] = true, - ["UniqueID"] = "537182332", - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["EditorExpand"] = true, - ["Name"] = "left", - ["Scale"] = Vector(1, 1, -0.0099999997764826), - ["Alpha"] = 0.999, - ["ClassName"] = "model", - ["AngleOffset"] = Angle(90, 90, 90), - ["AimPartName"] = "LOCALEYES_YAW", - ["Bone"] = "none", - ["Fullbright"] = true, - ["Translucent"] = true, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", - }, - }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["EditorExpand"] = true, - ["UniqueID"] = "2385081529", - ["Expression"] = "0,0,owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or 0) or 0", - ["ClassName"] = "proxy", - ["Input"] = "owner_velocity_length_increase", - ["VariableName"] = "PositionOffset", - }, - }, - [3] = { - ["children"] = { - [1] = { - ["children"] = { + [2] = { + ["children"] = { + [1] = { + ["children"] = { + }, + ["self"] = { + ["AffectChildrenOnly"] = false, + ["DrawOrder"] = 0, + ["TargetPartUID"] = "", + ["Name"] = "", + ["Event"] = "flat_dot_forward", + ["Hide"] = false, + ["TargetEntityUID"] = "", + ["RootOwner"] = true, + ["EditorExpand"] = true, + ["ClassName"] = "event", + ["Arguments"] = "-0.7071067812", + ["Invert"] = true, + ["IsDisturbing"] = false, + ["Operator"] = "equal or below", + ["UniqueID"] = "34fd29c1896140b8e5b639659a278ff810f4db34e73de20b372f6d0d0f0b8bad", + ["ZeroEyePitch"] = false, + }, }, - ["self"] = { - ["EditorExpand"] = true, - ["UniqueID"] = "75353876", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - ["ClassName"] = "proxy", - ["RootOwner"] = true, - ["Input"] = "owner_velocity_length_increase", - ["VariableName"] = "AngleOffset", + [2] = { + ["children"] = { + }, + ["self"] = { + ["DrawOrder"] = 0, + ["UniqueID"] = "75353876", + ["Axis"] = "", + ["Input"] = "owner_velocity_length_increase", + ["TargetPartUID"] = "", + ["InputMultiplier"] = 1, + ["RootOwner"] = true, + ["TargetEntityUID"] = "", + ["ZeroEyePitch"] = false, + ["ClassName"] = "proxy", + ["ResetVelocitiesOnHide"] = true, + ["VelocityRoughness"] = 10, + ["Max"] = 1, + ["Pow"] = 1, + ["EditorExpand"] = true, + ["AffectChildren"] = false, + ["Min"] = 0, + ["Hide"] = false, + ["Name"] = "", + ["VariableName"] = "AngleOffset", + ["Offset"] = 0, + ["PlayerAngles"] = false, + ["Additive"] = false, + ["InputDivider"] = 1, + ["IsDisturbing"] = false, + ["OutputTargetPartUID"] = "", + ["Function"] = "sin", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + }, }, }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["Arguments"] = "0.7", - ["UniqueID"] = "3855963026", - ["Event"] = "dot_forward", - ["Operator"] = "below", - ["ClassName"] = "event", - ["EditorExpand"] = true, - }, + ["self"] = { + ["Skin"] = 0, + ["Invert"] = false, + ["LightBlend"] = 1, + ["CellShade"] = 0, + ["AimPartName"] = "LOCALEYES_YAW", + ["IgnoreZ"] = false, + ["AimPartUID"] = "", + ["Passes"] = 1, + ["Name"] = "back", + ["Angles"] = Angle(0, 0, 0), + ["DoubleFace"] = false, + ["PositionOffset"] = Vector(0, 0, 0), + ["BlurLength"] = 0, + ["OwnerEntity"] = false, + ["Brightness"] = 1, + ["DrawOrder"] = 0, + ["BlendMode"] = "", + ["TintColor"] = Vector(0, 0, 0), + ["Alpha"] = 0.99, + ["LodOverride"] = -1, + ["TargetEntityUID"] = "", + ["BlurSpacing"] = 0, + ["UsePlayerColor"] = false, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/back.png", + ["UseWeaponColor"] = false, + ["EyeAngles"] = false, + ["UseLegacyScale"] = false, + ["Bone"] = "none", + ["Color"] = Vector(255, 255, 255), + ["Fullbright"] = true, + ["BoneMerge"] = false, + ["IsDisturbing"] = false, + ["Position"] = Vector(0, 0, 0), + ["NoTextureFiltering"] = false, + ["AlternativeScaling"] = false, + ["Hide"] = false, + ["Translucent"] = true, + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["ClassName"] = "model", + ["EditorExpand"] = true, + ["Size"] = 1, + ["ModelFallback"] = "", + ["AngleOffset"] = Angle(90, 90, 90), + ["TextureFilter"] = 3, + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["UniqueID"] = "2325223398", }, }, - ["self"] = { - ["UniqueID"] = "2325223398", - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["EditorExpand"] = true, - ["Name"] = "back", - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["Alpha"] = 0.99, - ["AngleOffset"] = Angle(90, 90, 90), - ["Fullbright"] = true, - ["AimPartName"] = "LOCALEYES_YAW", - ["ClassName"] = "model", - ["Bone"] = "none", - ["Translucent"] = true, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/back.png", - }, - }, - [4] = { - ["children"] = { - [1] = { - ["children"] = { + [3] = { + ["children"] = { + [1] = { + ["children"] = { + }, + ["self"] = { + ["AffectChildrenOnly"] = false, + ["DrawOrder"] = 0, + ["TargetPartUID"] = "", + ["Name"] = "", + ["Event"] = "flat_dot_right", + ["Hide"] = false, + ["TargetEntityUID"] = "", + ["RootOwner"] = true, + ["EditorExpand"] = true, + ["ClassName"] = "event", + ["Arguments"] = "0.707", + ["Invert"] = true, + ["IsDisturbing"] = false, + ["Operator"] = "above", + ["UniqueID"] = "2514734784", + ["ZeroEyePitch"] = false, + }, }, - ["self"] = { - ["Arguments"] = "0.7", - ["UniqueID"] = "2514734784", - ["Event"] = "dot_right", - ["Operator"] = "below", - ["ClassName"] = "event", - ["EditorExpand"] = true, + [2] = { + ["children"] = { + }, + ["self"] = { + ["DrawOrder"] = 0, + ["UniqueID"] = "2809374115", + ["Axis"] = "", + ["Input"] = "owner_velocity_length_increase", + ["TargetPartUID"] = "", + ["InputMultiplier"] = 1, + ["RootOwner"] = true, + ["TargetEntityUID"] = "", + ["ZeroEyePitch"] = false, + ["ClassName"] = "proxy", + ["ResetVelocitiesOnHide"] = true, + ["VelocityRoughness"] = 10, + ["Max"] = 1, + ["Pow"] = 1, + ["EditorExpand"] = false, + ["AffectChildren"] = false, + ["Min"] = 0, + ["Hide"] = false, + ["Name"] = "", + ["VariableName"] = "AngleOffset", + ["Offset"] = 0, + ["PlayerAngles"] = false, + ["Additive"] = false, + ["InputDivider"] = 1, + ["IsDisturbing"] = false, + ["OutputTargetPartUID"] = "", + ["Function"] = "sin", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + }, }, }, - [2] = { - ["children"] = { - }, - ["self"] = { - ["RootOwner"] = true, - ["UniqueID"] = "2809374115", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - ["ClassName"] = "proxy", - ["Input"] = "owner_velocity_length_increase", - ["VariableName"] = "AngleOffset", - }, + ["self"] = { + ["Skin"] = 0, + ["Invert"] = false, + ["LightBlend"] = 1, + ["CellShade"] = 0, + ["AimPartName"] = "LOCALEYES_YAW", + ["IgnoreZ"] = false, + ["AimPartUID"] = "", + ["Passes"] = 1, + ["Name"] = "right", + ["Angles"] = Angle(0, 0, 0), + ["DoubleFace"] = false, + ["PositionOffset"] = Vector(0, 0, 0), + ["BlurLength"] = 0, + ["OwnerEntity"] = false, + ["Brightness"] = 1, + ["DrawOrder"] = 0, + ["BlendMode"] = "", + ["TintColor"] = Vector(0, 0, 0), + ["Alpha"] = 0.999, + ["LodOverride"] = -1, + ["TargetEntityUID"] = "", + ["BlurSpacing"] = 0, + ["UsePlayerColor"] = false, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", + ["UseWeaponColor"] = false, + ["EyeAngles"] = false, + ["UseLegacyScale"] = false, + ["Bone"] = "none", + ["Color"] = Vector(255, 255, 255), + ["Fullbright"] = true, + ["BoneMerge"] = false, + ["IsDisturbing"] = false, + ["Position"] = Vector(0, 0, 0), + ["NoTextureFiltering"] = false, + ["AlternativeScaling"] = false, + ["Hide"] = false, + ["Translucent"] = true, + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["ClassName"] = "model", + ["EditorExpand"] = true, + ["Size"] = 1, + ["ModelFallback"] = "", + ["AngleOffset"] = Angle(90, 90, 90), + ["TextureFilter"] = 3, + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["UniqueID"] = "1916911126", }, }, - ["self"] = { - ["UniqueID"] = "1916911126", - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["EditorExpand"] = true, - ["Name"] = "right", - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["Alpha"] = 0.999, - ["AngleOffset"] = Angle(90, 90, 90), - ["Fullbright"] = true, - ["AimPartName"] = "LOCALEYES_YAW", - ["ClassName"] = "model", - ["Bone"] = "none", - ["Translucent"] = true, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/side.png", - }, - }, - [5] = { - ["children"] = { - [1] = { - ["children"] = { - }, - ["self"] = { - ["Arguments"] = "-0.7", - ["UniqueID"] = "2334772782", - ["Event"] = "dot_forward", - ["Operator"] = "above", - ["ClassName"] = "event", - ["EditorExpand"] = true, - }, + [4] = { + ["children"] = { }, - [2] = { - ["children"] = { + ["self"] = { + ["DrawOrder"] = 0, + ["UniqueID"] = "2385081529", + ["Axis"] = "", + ["Input"] = "owner_velocity_length_increase", + ["TargetPartUID"] = "", + ["InputMultiplier"] = 1, + ["RootOwner"] = false, + ["TargetEntityUID"] = "", + ["ZeroEyePitch"] = false, + ["ClassName"] = "proxy", + ["ResetVelocitiesOnHide"] = true, + ["VelocityRoughness"] = 10, + ["Max"] = 1, + ["Pow"] = 1, + ["EditorExpand"] = true, + ["AffectChildren"] = false, + ["Min"] = 0, + ["Hide"] = false, + ["Name"] = "", + ["VariableName"] = "PositionOffset", + ["Offset"] = 0, + ["PlayerAngles"] = false, + ["Additive"] = false, + ["InputDivider"] = 1, + ["IsDisturbing"] = false, + ["OutputTargetPartUID"] = "", + ["Function"] = "sin", + ["Expression"] = "0,0,owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or 0) or 0", + }, + }, + [5] = { + ["children"] = { + [1] = { + ["children"] = { + }, + ["self"] = { + ["AffectChildrenOnly"] = false, + ["DrawOrder"] = 0, + ["TargetPartUID"] = "", + ["Name"] = "", + ["Event"] = "flat_dot_forward", + ["Hide"] = false, + ["TargetEntityUID"] = "", + ["RootOwner"] = true, + ["EditorExpand"] = true, + ["ClassName"] = "event", + ["Arguments"] = "0.7071067812", + ["Invert"] = true, + ["IsDisturbing"] = false, + ["Operator"] = "equal or above", + ["UniqueID"] = "2334772782", + ["ZeroEyePitch"] = false, + }, }, - ["self"] = { - ["EditorExpand"] = true, - ["UniqueID"] = "4261893074", - ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", - ["ClassName"] = "proxy", - ["Input"] = "owner_velocity_length_increase", - ["VariableName"] = "AngleOffset", + [2] = { + ["children"] = { + }, + ["self"] = { + ["DrawOrder"] = 0, + ["UniqueID"] = "4261893074", + ["Axis"] = "", + ["Input"] = "owner_velocity_length_increase", + ["TargetPartUID"] = "", + ["InputMultiplier"] = 1, + ["RootOwner"] = false, + ["TargetEntityUID"] = "", + ["ZeroEyePitch"] = false, + ["ClassName"] = "proxy", + ["ResetVelocitiesOnHide"] = true, + ["VelocityRoughness"] = 10, + ["Max"] = 1, + ["Pow"] = 1, + ["EditorExpand"] = true, + ["AffectChildren"] = false, + ["Min"] = 0, + ["Hide"] = false, + ["Name"] = "", + ["VariableName"] = "AngleOffset", + ["Offset"] = 0, + ["PlayerAngles"] = false, + ["Additive"] = false, + ["InputDivider"] = 1, + ["IsDisturbing"] = false, + ["OutputTargetPartUID"] = "", + ["Function"] = "sin", + ["Expression"] = "90 + (owner_velocity_length() > 2 and (sin(owner_velocity_length_increase()*10 + random()) > 0 and 2 or -2) or 0),90,90", + }, }, }, - }, - ["self"] = { - ["UniqueID"] = "2579545900", - ["Model"] = "models/hunter/plates/plate1x1.mdl", - ["EditorExpand"] = true, - ["Name"] = "front", - ["Scale"] = Vector(1, 1, 0.0099999997764826), - ["Alpha"] = 0.999, - ["AngleOffset"] = Angle(90, 90, 90), - ["Fullbright"] = true, - ["AimPartName"] = "LOCALEYES_YAW", - ["ClassName"] = "model", - ["Bone"] = "none", - ["Translucent"] = true, - ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/front.png", + ["self"] = { + ["Skin"] = 0, + ["Invert"] = false, + ["LightBlend"] = 1, + ["CellShade"] = 0, + ["AimPartName"] = "LOCALEYES_YAW", + ["IgnoreZ"] = false, + ["AimPartUID"] = "", + ["Passes"] = 1, + ["Name"] = "front", + ["Angles"] = Angle(0, 0, 0), + ["DoubleFace"] = false, + ["PositionOffset"] = Vector(0, 0, 0), + ["BlurLength"] = 0, + ["OwnerEntity"] = false, + ["Brightness"] = 1, + ["DrawOrder"] = 0, + ["BlendMode"] = "", + ["TintColor"] = Vector(0, 0, 0), + ["Alpha"] = 0.999, + ["LodOverride"] = -1, + ["TargetEntityUID"] = "", + ["BlurSpacing"] = 0, + ["UsePlayerColor"] = false, + ["Material"] = "https://raw.githubusercontent.com/CapsAdmin/pac3_assets/master/organic/human/male/kyle/front.png", + ["UseWeaponColor"] = false, + ["EyeAngles"] = false, + ["UseLegacyScale"] = false, + ["Bone"] = "none", + ["Color"] = Vector(255, 255, 255), + ["Fullbright"] = true, + ["BoneMerge"] = false, + ["IsDisturbing"] = false, + ["Position"] = Vector(0, 0, 0), + ["NoTextureFiltering"] = false, + ["AlternativeScaling"] = false, + ["Hide"] = false, + ["Translucent"] = true, + ["Scale"] = Vector(1, 1, 0.0099999997764826), + ["ClassName"] = "model", + ["EditorExpand"] = true, + ["Size"] = 1, + ["ModelFallback"] = "", + ["AngleOffset"] = Angle(90, 90, 90), + ["TextureFilter"] = 3, + ["Model"] = "models/hunter/plates/plate1x1.mdl", + ["UniqueID"] = "2579545900", + }, }, }, - }, - ["self"] = { - ["UniqueID"] = "49506760", - ["ClassName"] = "model", - ["Position"] = Vector(0, 0, 24), - ["Model"] = "models/pac/default.mdl", - ["Size"] = 0, - ["Bone"] = "none", - ["Name"] = "body", - ["EditorExpand"] = true, + ["self"] = { + ["Skin"] = 0, + ["Invert"] = false, + ["LightBlend"] = 1, + ["CellShade"] = 0, + ["AimPartName"] = "", + ["IgnoreZ"] = false, + ["AimPartUID"] = "", + ["Passes"] = 1, + ["Name"] = "body", + ["Angles"] = Angle(0, 0, 0), + ["DoubleFace"] = false, + ["PositionOffset"] = Vector(0, 0, 0), + ["BlurLength"] = 0, + ["OwnerEntity"] = false, + ["Brightness"] = 1, + ["DrawOrder"] = 0, + ["BlendMode"] = "", + ["TintColor"] = Vector(0, 0, 0), + ["Alpha"] = 1, + ["LodOverride"] = -1, + ["TargetEntityUID"] = "", + ["BlurSpacing"] = 0, + ["UsePlayerColor"] = false, + ["Material"] = "", + ["UseWeaponColor"] = false, + ["EyeAngles"] = false, + ["UseLegacyScale"] = false, + ["Bone"] = "none", + ["Color"] = Vector(255, 255, 255), + ["Fullbright"] = false, + ["BoneMerge"] = false, + ["IsDisturbing"] = false, + ["Position"] = Vector(0, 0, 24), + ["NoTextureFiltering"] = false, + ["AlternativeScaling"] = false, + ["Hide"] = false, + ["Translucent"] = false, + ["Scale"] = Vector(1, 1, 1), + ["ClassName"] = "model", + ["EditorExpand"] = true, + ["Size"] = 0, + ["ModelFallback"] = "", + ["AngleOffset"] = Angle(0, 0, 0), + ["TextureFilter"] = 3, + ["Model"] = "models/pac/default.mdl", + ["UniqueID"] = "49506760", + }, }, }, - }, - ["self"] = { - ["EditorExpand"] = true, - ["UniqueID"] = "743553614", - ["ClassName"] = "group", - ["Name"] = "my outfit", - ["Description"] = "add parts to me!", - }, -}, + ["self"] = { + ["DrawOrder"] = 0, + ["UniqueID"] = "743553614", + ["Hide"] = false, + ["TargetEntityUID"] = "", + ["EditorExpand"] = true, + ["OwnerName"] = "self", + ["IsDisturbing"] = false, + ["Name"] = "my outfit", + ["Duplicate"] = false, + ["ClassName"] = "group", + }, + }, } diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index 81a0018cd..dab095bdb 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -481,4 +481,4 @@ function PANEL:Paint(w,h) --DFrame.Paint(self, w,h) end -pace.RegisterPanel(PANEL) \ No newline at end of file +pace.RegisterPanel(PANEL) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 6a24acf0d..95453a210 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1,3 +1,4 @@ +include("pac3/editor/client/panels/properties.lua") local L = pace.LanguageString local BulkSelectList = {} local BulkSelectUIDs = {} @@ -681,6 +682,362 @@ do -- menu obj.pace_tree_node:SetAlpha( 150 ) --RebuildBulkHighlight() end +//@note apply properties + function pace.BulkApplyProperties(obj, policy) + local basepart = obj + --[[if not table.HasValue(BulkSelectList,obj) then + basepart = BulkSelectList[1] + end]] + + local Panel = vgui.Create( "DFrame" ) + Panel:SetSize( 500, 600 ) + Panel:Center() + Panel:SetTitle("BULK SELECT PROPERTY EDIT - WARNING! EXPERIMENTAL FEATURE!") + + Panel:MakePopup() + surface.CreateFont("Font", { + font = "Arial", + extended = true, + weight = 700, + size = 15 + }) + + local scroll_panel = vgui.Create("DScrollPanel", Panel) + scroll_panel:SetSize( 500, 540 ) + scroll_panel:SetPos( 0, 60 ) + local thoroughness_tickbox = vgui.Create("DCheckBox", Panel) + thoroughness_tickbox:SetSize(20,20) + thoroughness_tickbox:SetPos( 5, 30 ) + local thoroughness_tickbox_label = vgui.Create("DLabel", Panel) + thoroughness_tickbox_label:SetSize(150,30) + thoroughness_tickbox_label:SetPos( 30, 25 ) + thoroughness_tickbox_label:SetText("Affect children?") + thoroughness_tickbox_label:SetFont("Font") + local basepart_label = vgui.Create("DLabel", Panel) + basepart_label:SetSize(340,30) + basepart_label:SetPos( 160, 25 ) + local partinfo = basepart.ClassName + if basepart.ClassName == "event" then partinfo = basepart.Event .. " " .. partinfo end + local partinfo_icon = vgui.Create("DImage",basepart_label) + partinfo_icon:SetSize(30,30) + partinfo_icon:SetPos( 300, 0 ) + partinfo_icon:SetImage(basepart.Icon) + + basepart_label:SetText("base part: "..partinfo) + basepart_label:SetFont("Font") + + local excluded_vars = {"Duplicate","OwnerName","ParentUID","UniqueID","TargetEntityUID"} + local var_candidates = {} + + local shared_properties = {} + local shared_udata_properties = {} + + for _,prop in pairs(basepart:GetProperties()) do + + local shared = true + for _,part2 in pairs(BulkSelectList) do + if basepart ~= part2 and basepart.ClassName ~= part2.ClassName then + if part2["Get" .. prop["key"]] == nil then + if policy == "harsh" then shared = false end + end + end + end + if shared and not prop.udata.editor_friendly and basepart["Get" .. prop["key"]] ~= nil then + shared_properties[#shared_properties + 1] = prop["key"] + elseif shared and prop.udata.editor_friendly and basepart["Get" .. prop["key"]] == nil then + shared_udata_properties[#shared_udata_properties + 1] = "event_udata_"..prop["key"] + end + end + + if policy == "lenient" then + local initial_shared_properties = table.Copy(shared_properties) + local initial_shared_udata_properties = table.Copy(shared_udata_properties) + for _,part2 in pairs(BulkSelectList) do + for _,prop in ipairs(part2:GetProperties()) do + if not (table.HasValue(shared_properties, prop["key"]) or table.HasValue(shared_udata_properties, "event_udata_"..prop["key"])) then + if part2["Get" .. prop["key"]] ~= nil then + initial_shared_properties[#initial_shared_properties + 1] = prop["key"] + elseif part2["Get" .. prop["key"]] == nil then + initial_shared_udata_properties[#initial_shared_udata_properties + 1] = "event_udata_"..prop["key"] + end + end + end + end + shared_properties = initial_shared_properties + shared_udata_properties = initial_shared_udata_properties + end + --populate panels for standard GetSet part properties + for i,v in pairs(shared_properties) do + local VAR_PANEL = vgui.Create("DFrame") + VAR_PANEL:SetSize(500,30) + VAR_PANEL:SetPos(0,0) + VAR_PANEL:ShowCloseButton( false ) + local VAR_PANEL_BUTTON = VAR_PANEL:Add("DButton") + VAR_PANEL_BUTTON:SetSize(80,30) + VAR_PANEL_BUTTON:SetPos(400,0) + local VAR_PANEL_EDITZONE + local var_type + for _,testpart in ipairs(BulkSelectList) do + if + testpart["Get" .. v] ~= nil + then + var_type = type(testpart["Get" .. v](testpart)) + end + end + if basepart["Get" .. v] ~= nil then var_type = type(basepart["Get" .. v](basepart)) end + + if var_type == "number" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "boolean" then + VAR_PANEL_EDITZONE = vgui.Create("DCheckBox", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(30,30) + elseif var_type == "string" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "Vector" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "Angle" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + else + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + end + VAR_PANEL_EDITZONE:SetPos(200,0) + + VAR_PANEL_BUTTON:SetText("APPLY") + + VAR_PANEL:SetTitle("[" .. i .. "] "..v.." "..var_type) + + VAR_PANEL:Dock( TOP ) + VAR_PANEL:DockMargin( 5, 0, 0, 5 ) + VAR_PANEL_BUTTON.DoClick = function() + for i,part in pairs(BulkSelectList) do + local sent_var + if var_type == "number" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if not tonumber(sent_var) then + local ok, res = pac.CompileExpression(sent_var) + if ok then + sent_var = res() or 0 + end + end + elseif var_type == "boolean" then + sent_var = VAR_PANEL_EDITZONE:GetChecked() + elseif var_type == "string" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if v == "Name" and sent_var ~= "" then + sent_var = sent_var..i + end + elseif var_type == "Vector" then + local str = string.Split(VAR_PANEL_EDITZONE:GetValue(), ",") + sent_var = Vector() + sent_var.x = tonumber(str[1]) or 1 + sent_var.y = tonumber(str[2]) or 1 + sent_var.z = tonumber(str[3]) or 1 + if v and not part.ProperColorRange then sent_var = sent_var*255 end + elseif var_type == "Angle" then + local str = string.Split(VAR_PANEL_EDITZONE:GetValue(), ",") + sent_var = Angle() + sent_var.r = tonumber(str[1]) or 1 + sent_var.g = tonumber(str[2]) or 1 + sent_var.b = tonumber(str[3]) or 1 + else sent_var = VAR_PANEL_EDITZONE:GetValue() end + + + if policy == "harsh" then part["Set" .. v](part, sent_var) + elseif policy == "lenient" then + if part["Get" .. v] ~= nil then part["Set" .. v](part, sent_var) end + end + if thoroughness_tickbox:GetChecked() then + for _,child in pairs(part:GetChildrenList()) do + if part["Get" .. v] ~= nil then child["Set" .. v](child, sent_var) end + end + end + end + + pace.RefreshTree(true) + timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) + end + scroll_panel:AddItem( VAR_PANEL ) + end + + --populate panels for event "userdata" packaged into arguments + if #shared_udata_properties > 1 then + local udata_types = { + hide_in_eventwheel = "boolean", + find = "string", + time = "number", + distance = "number", + compare = "number", + min = "number", + max = "number", + primary = "boolean", + amount = "number", + ammo_id = "number", + find_ammo = "string", + hide = "boolean", + interval = "number", + offset = "number", + find_sound = "string", + mute = "boolean", + all_players = "boolean"} + local udata_orders = { + ["command"] = {[1] = "find", [2] = "time", [3] = "hide_in_eventwheel"}, + ["timerx"] = {[1] = "seconds", [2] = "reset_on_hide", [3] = "synced_time"}, + ["ranger"] = {[1] = "distance", [2] = "compare", [3] = "npcs_and_players_only"}, + ["randint"] = {[1] = "compare", [2] = "min", [3] = "max"}, + ["random_timer"] = {[1] = "min", [2] = "max", [3] = "compare"}, + ["timersys"] = {[1] = "seconds", [2] = "reset_on_hide"}, + ["pose_parameter"] = {[1] = "name", [2] = "num"}, + ["ammo"] = {[1] = "primary", [2] = "amount"}, + ["total_ammo"] = {[1] = "ammo_id", [2] = "amount"}, + ["clipsize"] = {[1] = "primary", [2] = "amount"}, + ["weapon_class"] = {[1] = "find", [2] = "hide"}, + ["timer"] = {[1] = "interval", [2] = "offset"}, + ["animation_event"] = {[1] = "find", [2] = "time"}, + ["fire_bullets"] = {[1] = "find_ammo", [2] = "time"}, + ["emit_sound"] = {[1] = "find_sound", [2] = "time", [3] = "mute"}, + ["say"] = {[1] = "find", [2] = "time", [3] = "all_players"}} + for i,v in pairs(shared_udata_properties) do + local udata_val_name = string.gsub(v, "event_udata_", "") + + local var_type = udata_types[udata_val_name] + + local VAR_PANEL = vgui.Create("DFrame") + + VAR_PANEL:SetSize(500,30) + VAR_PANEL:SetPos(0,0) + VAR_PANEL:ShowCloseButton( false ) + local VAR_PANEL_BUTTON = VAR_PANEL:Add("DButton") + VAR_PANEL_BUTTON:SetSize(80,30) + VAR_PANEL_BUTTON:SetPos(400,0) + local VAR_PANEL_EDITZONE + if var_type == "number" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "boolean" then + VAR_PANEL_EDITZONE = vgui.Create("DCheckBox", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(30,30) + elseif var_type == "string" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "Vector" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "Angle" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + else + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + end + + VAR_PANEL_EDITZONE:SetPos(200,0) + VAR_PANEL:SetTitle("[" .. i .. "] "..string.gsub(v, "event_udata_", "").." "..var_type) + VAR_PANEL_BUTTON:SetText("APPLY") + + + VAR_PANEL:Dock( TOP ) + VAR_PANEL:DockMargin( 5, 0, 0, 5 ) + VAR_PANEL_BUTTON.DoClick = function() + + for i,part in pairs(BulkSelectList) do + if part.ClassName == "event" and part.Event == basepart.Event then + local sent_var + if var_type == "number" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if not tonumber(sent_var) then + local ok, res = pac.CompileExpression(sent_var) + if ok then + sent_var = res() or 0 + end + end + elseif var_type == "boolean" then + sent_var = VAR_PANEL_EDITZONE:GetChecked() + if sent_var == true then sent_var = "1" + else sent_var = "0" end + elseif var_type == "string" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if v == "Name" and sent_var ~= "" then + sent_var = sent_var..i + end + else sent_var = VAR_PANEL_EDITZONE:GetValue() end + + local arg_split = string.Split(part:GetArguments(), "@@") + if #udata_orders[part.Event] ~= #string.Split(part:GetArguments(), "@@") then arg_split[#arg_split + 1] = "0" end + + local sent_var_final = "" + -- PRESCRIBED X @@ Y @@ Z ... + -- EVERY STEP ADD + -- EVERY STEP EXCEPT LAST..@@ + for n,arg in ipairs(arg_split) do + if udata_orders[part.Event][n] == udata_val_name then + sent_var_final = sent_var_final .. sent_var + else + sent_var_final = sent_var_final .. arg_split[n] + end + if n ~= #arg_split then + sent_var_final = sent_var_final .. "@@" + end + end + + part:SetArguments(sent_var_final) + end + + if thoroughness_tickbox:GetChecked() then + for _,child in pairs(part:GetChildrenList()) do + if child.ClassName == "event" and child.Event == basepart.Event then + local sent_var + if var_type == "number" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if not tonumber(sent_var) then + local ok, res = pac.CompileExpression(sent_var) + if ok then + sent_var = res() or 0 + end + end + elseif var_type == "boolean" then + sent_var = VAR_PANEL_EDITZONE:GetChecked() + if sent_var == true then sent_var = "1" + else sent_var = "0" end + elseif var_type == "string" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if v == "Name" and sent_var ~= "" then + sent_var = sent_var..i + end + else sent_var = VAR_PANEL_EDITZONE:GetValue() end + + local arg_split = string.Split(child:GetArguments(), "@@") + if #udata_orders[basepart.Event] ~= #string.Split(child:GetArguments(), "@@") then arg_split[#arg_split + 1] = "0" end + local sent_var_final = "" + + for n,arg in ipairs(arg_split) do + if udata_orders[child.Event][n] == udata_val_name then + sent_var_final = sent_var_final .. sent_var + else + sent_var_final = sent_var_final .. arg_split[n] + end + if n ~= #arg_split then + sent_var_final = sent_var_final .. "@@" + end + end + + child:SetArguments(sent_var_final) + end + end + end + end + + pace.RefreshTree(true) + timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) + end + scroll_panel:AddItem( VAR_PANEL ) + end + end + end function pace.BulkCutPaste(obj) pace.RecordUndoHistory() @@ -771,10 +1128,15 @@ do -- menu menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png') menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) - - --multi select - bulk_menu, icon = menu:AddSubMenu(L"bulk select ("..#BulkSelectList..")", function() pace.DoBulkSelect(obj) end) - icon:SetImage('icon16/table_multiple.png') + + local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end) + bap_icon:SetImage('icon16/table_multiple.png') + bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end) + bulk_apply_properties:AddOption("Policy: lenient filtering", function() pace.BulkApplyProperties(obj, "lenient") end) + + --bulk select + bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#BulkSelectList..")", function() pace.DoBulkSelect(obj) end) + bs_icon:SetImage('icon16/table_multiple.png') bulk_menu.GetDeleteSelf = function() return false end local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() @@ -1078,3 +1440,38 @@ do --hover highlight halo --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) end end + + +--[[ test visualise part +local cachedmodel = ClientsideModel( obj:GetRootPart():GetOwner():GetModel(), RENDERGROUP_BOTH ) + +print(tbl ~= nil) +previewmdl_tbl = previewmdl_tbl or obj:ToTable() + +timer.Simple(8,function() Panel:Remove() end) + +ENT = cachedmodel +--ENT = icon:GetEntity() +pac.SetupENT(ENT) +ENT:AttachPACPart(tbl, ENT:GetOwner(), false) + + +function Panel:Paint( w, h ) + + local x, y = self:GetPos() + local vec = LocalPlayer():GetPos() + + cachedmodel:SetPos(vec + Vector(100,0,50)) + cachedmodel:SetAngles(Angle(0,180 + SysTime()*60,0)) + render.RenderView( { + origin = vec + Vector( 0, 0, 90 ), + angles = Angle(0,0,0), + x = x, y = y, + w = w, h = h, + fov = 50, aspect = w/h + } ) + +end +timer.Simple(8, function() + SafeRemoveEntity(cachedmodel) +end)]] \ No newline at end of file diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index 84b852323..f772ebc9a 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -34,7 +34,7 @@ function pace.ClearParts() pace.ClearUndo() pac.RemoveAllParts(true, true) pace.RefreshTree() - + timer.Simple(0.1, function() if not pace.Editor:IsValid() then return end diff --git a/lua/pac3/editor/server/wear.lua b/lua/pac3/editor/server/wear.lua index 3b1bd343f..f1a21e19e 100644 --- a/lua/pac3/editor/server/wear.lua +++ b/lua/pac3/editor/server/wear.lua @@ -334,6 +334,8 @@ function pace.HandleReceivedData(ply, data) data.owner = ply data.uid = pac.Hash(ply) + pac.Message("Received pac data from ", ply, " with ", data.totalParts or 0, " parts") + if data.wear_filter and #data.wear_filter > game.MaxPlayers() then pac.Message("Player ", ply, " tried to submit extraordinary wear filter size of ", #data.wear_filter, ", dropping.") data.wear_filter = nil From 4cff07bb5400468e355945496addec1e2d4eaf43 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 22 Apr 2023 00:09:03 -0400 Subject: [PATCH 009/300] more events and proxies is_touching_scalable is_explicit pose_parameter_true (adjusted for their full range instead of 0-1) command() string argument instead of proxy's name max armor armor and health fractions concatenate missing arguments for some events for a backward compatibility fix --- lua/pac3/core/client/parts/event.lua | 88 ++++++++++++++++++++++++---- lua/pac3/core/client/parts/proxy.lua | 37 +++++++++++- 2 files changed, 112 insertions(+), 13 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index ae785353d..1739befaa 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -26,7 +26,7 @@ BUILDER:StartStorableVars() return output end}) BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end}) - BUILDER:GetSet("Arguments", "", {hidden = true}) + BUILDER:GetSet("Arguments", "", {hidden = false}) BUILDER:GetSet("Invert", true) BUILDER:GetSet("RootOwner", true) BUILDER:GetSet("AffectChildrenOnly", false) @@ -35,12 +35,12 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:SetEvent(event) - local reset = (self.Arguments == "") or + local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) self.Event = event self:SetWarning() - self:GetDynamicProperties(reset) + self:GetDynamicProperties(reset) end local function get_default(typ) @@ -95,8 +95,8 @@ function PART:GetDynamicProperties(reset_to_default) } local arg = tbl[key] - if arg.get() == nil or reset_to_default then - if udata.default then + if arg.get() == nil or reset_to_default then + if udata.default then arg.set(udata.default) else arg.set(nil) @@ -204,10 +204,17 @@ PART.OldEvents = { timerx = { arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}}, + userdata = { + {default = 0, timerx_property = "seconds"}, + {default = true, timerx_property = "reset_on_hide"} + }, nice = function(self, ent, seconds) return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?" end, callback = function(self, ent, seconds, reset_on_hide, synced_time) + if #string.Split(self.Arguments, "@@") < 3 then + self.Arguments = self.Arguments .. "@@0" + end local time = synced_time and CurTime() or RealTime() self.time = self.time or time @@ -424,6 +431,18 @@ PART.OldEvents = { end, }, + pose_parameter_true = { + arguments = {{name = "string"}}, + callback = function(self, ent, name, num) + ent = try_viewmodel(ent) + if owner:IsValid() then + min,max = owner:GetPoseParameterRange(owner:LookupPoseParameter(name)) + actual_value = min + (max - min)*(owner:GetPoseParameter(name)) + return actual_value + else return 0 end + end, + }, + speed = { arguments = {{speed = "number"}}, callback = function(self, ent, num) @@ -490,8 +509,14 @@ PART.OldEvents = { ranger = { arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}}, - userdata = {{editor_panel = "ranger", ranger_property = "distance"}, {editor_panel = "ranger", ranger_property = "compare"}}, + userdata = { + {default = 15, editor_panel = "ranger", ranger_property = "distance"}, + {default = 5, editor_panel = "ranger", ranger_property = "compare"} + }, callback = function(self, ent, distance, compare, npcs_and_players_only) + if #string.Split(self.Arguments, "@@") < 3 then + self.Arguments = self.Arguments .. "@@0" + end local parent = self:GetParentEx() if parent:IsValid() and parent.GetWorldPosition then @@ -572,12 +597,51 @@ PART.OldEvents = { endpos = startpos, maxs = maxs, mins = mins, - filter = ent + filter = {self:GetRootPart():GetOwner(),ent} } ) return tr.Hit end, }, + is_touching_scalable = { + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}}, + userdata = {{editor_panel = "is_touching", default = 15}, {x = "x_stretch"}, {y = "y_stretch"}, {z = "z_stretch"}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch) + extra_radius = extra_radius or 15 + x_stretch = x_stretch or 1 + y_stretch = y_stretch or 1 + z_stretch = z_stretch or 1 + if #string.Split(self.Arguments, "@@") == 1 then + self.Arguments = self.Arguments .. "@@1" + end + if #string.Split(self.Arguments, "@@") < 4 then + self.Arguments = self.Arguments .. "@@1" + end + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + local startpos = ent:WorldSpaceCenter() + + radius = math.max(extra_radius, 1) + mins = mins * radius + maxs = maxs * radius + + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {self:GetRootPart():GetOwner(),ent} + } ) + return tr.Hit + end, + }, + + is_explicit = { + callback = function(self, ent) + return GetConVar("pac_hide_disturbing"):GetBool() + end + }, + is_in_noclip = { callback = function(self, ent) ent = try_viewmodel(ent) @@ -824,8 +888,8 @@ PART.OldEvents = { command = { arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}}, userdata = { - {default = "change_me", editor_friendly = "CommandName"}, - {default = 0.1, editor_friendly = "EventDuration"}, + {default = "change_me", editor_friendly = "CommandName"}, + {default = 0.1, editor_friendly = "EventDuration"}, {default = false, group = "event wheel", editor_friendly = "HideInEventWheel"} }, nice = function(self, ent, find, time) @@ -834,6 +898,10 @@ PART.OldEvents = { return "command: " .. find .. " | " .. "duration: " .. time end, callback = function(self, ent, find, time) + if #string.Split(self.Arguments, "@@") < 3 then + self.Arguments = self.Arguments .. "@@0" + end + time = time or 0.1 local ply = self:GetPlayerOwner() @@ -2207,4 +2275,4 @@ do concommand.Add("+pac_events", pac.openEventSelectionWheel) concommand.Add("-pac_events", pac.closeEventSelectionWheel) -end +end \ No newline at end of file diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 014fbb7d4..ecb5db882 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -516,15 +516,28 @@ PART.Inputs.pose_parameter = function(self, name) return 0 end -PART.Inputs.command = function(self) +PART.Inputs.pose_parameter_true = function(self, name) + if not name then return 0 end + local owner = get_owner(self) + if owner:IsValid() then + min,max = owner:GetPoseParameterRange(owner:LookupPoseParameter(name)) + actual_value = min + (max - min)*(owner:GetPoseParameter(name)) + return actual_value + else end + return 0 +end + +PART.Inputs.command = function(self, name) local ply = self:GetPlayerOwner() if ply.pac_proxy_events then - local data = ply.pac_proxy_events[self.Name] + local data + if not name then data = ply.pac_proxy_events[self.Name] + else data = ply.pac_proxy_events[name] end + if data then data.x = data.x or 0 data.y = data.y or 0 data.z = data.z or 0 - return data.x, data.y, data.z end end @@ -607,6 +620,12 @@ do -- health and armor return owner:GetMaxHealth() end + PART.Inputs.owner_health_fraction = function(self) + local owner = self:GetPlayerOwner() + if not owner:IsValid() then return 0 end + + return owner:Health() / owner:GetMaxHealth() + end PART.Inputs.owner_armor = function(self) local owner = self:GetPlayerOwner() @@ -614,6 +633,18 @@ do -- health and armor return owner:Armor() end + PART.Inputs.owner_max_armor = function(self) + local owner = self:GetPlayerOwner() + if not owner:IsValid() then return 0 end + + return owner:GetMaxArmor() + end + PART.Inputs.owner_armor_fraction = function(self) + local owner = self:GetPlayerOwner() + if not owner:IsValid() then return 0 end + + return owner:Armor() / owner:GetMaxArmor() + end end do -- weapon and player color From 00cc8c89cc97cdaf75ce38fa527718cb6d4e3328 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 22 Apr 2023 05:14:04 -0400 Subject: [PATCH 010/300] Update beam.lua --- lua/pac3/core/client/parts/beam.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/pac3/core/client/parts/beam.lua b/lua/pac3/core/client/parts/beam.lua index 6ca97ca93..835854a51 100644 --- a/lua/pac3/core/client/parts/beam.lua +++ b/lua/pac3/core/client/parts/beam.lua @@ -82,7 +82,6 @@ local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "beam" PART.Group = 'effects' PART.Icon = 'icon16/vector.png' -PART.HandleModifiersManually = true BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") From fbbf202ef4681c67a3a4426f4444c42193a61e6d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 22 Apr 2023 17:53:37 -0400 Subject: [PATCH 011/300] proper cleanup of BulkSelectList before editing labels --- lua/pac3/editor/client/parts.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 95453a210..7fc7eaed7 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1337,7 +1337,10 @@ do --hover highlight halo end for _,v in ipairs(BulkSelectList) do - v.pace_tree_node:SetAlpha( 150 ) + if not v:IsValid() then table.RemoveByValue(BulkSelectList, v) + else + v.pace_tree_node:SetAlpha( 150 ) + end end end @@ -1474,4 +1477,4 @@ function Panel:Paint( w, h ) end timer.Simple(8, function() SafeRemoveEntity(cachedmodel) -end)]] \ No newline at end of file +end)]] From a15c5af9a85bb669ffa856a472414ac15812d0ef Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 2 May 2023 15:24:04 -0400 Subject: [PATCH 012/300] better checks for missing references and append uniqueID to the render preview hook to allow multiple previews --- lua/pac3/core/client/parts/lock.lua | 74 +++++++++++++++-------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 927fda426..08223bd90 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -52,7 +52,8 @@ local forcebreak = false local next_allowed_grab = SysTime() function PART:OnThink() - + self:CheckEntValidity() + if SysTime() > next_allowed_grab then forcebreak = false elseif forcebreak then @@ -75,11 +76,10 @@ function PART:OnThink() end end --good hit and can search = search more and try the move later - self:CheckEntValidity() - + --self:DecideTarget() - if self.Mode == "Grab" then - if self.OverrideAngles then + if self.Mode == "Grab" and valid_ent then + if self.OverrideAngles and valid_ent and self.target_ent:IsValid() then default_ang = self.target_ent:GetAngles() if self.OverrideEyeAngles then default_ang.y = self:GetWorldAngles().y end end @@ -177,18 +177,22 @@ function PART:OnThink() end end - - function PART:OnShow() - local targ = self.TargetPart or self - if self.Preview then - hook.Add("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview", function() - if self.RadiusOffsetDown then - render.DrawLine(targ:GetWorldPosition(),targ:GetWorldPosition() + Vector(0,0,-self.Radius),Color(255,255,255)) - render.DrawWireframeSphere(targ:GetWorldPosition() + Vector(0,0,-self.Radius), self.Radius, 30, 30, Color(255,255,255),true) - else render.DrawWireframeSphere(targ:GetWorldPosition(), self.Radius, 30, 30, Color(255,255,255),true) end - end) - end + local origin_part + hook.Add("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID, function() + if self.TargetPart:IsValid() then + origin_part = self.TargetPart + else + origin_part = self + end + if origin_part == nil or not self.Preview or LocalPlayer() ~= self:GetPlayerOwner() then return end + if self.RadiusOffsetDown then + render.DrawLine(origin_part:GetWorldPosition(),origin_part:GetWorldPosition() + Vector(0,0,-self.Radius),Color(255,255,255)) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.Radius), self.Radius, 30, 30, Color(255,255,255),true) + else + render.DrawWireframeSphere(origin_part:GetWorldPosition(), self.Radius, 30, 30, Color(255,255,255),true) + end + end) self.target_ent = nil --self.relative_transform_matrix = Matrix():Identity() self:DecideTarget() @@ -197,30 +201,30 @@ function PART:OnShow() end function PART:OnHide() - hook.Remove("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview") + hook.Remove("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID) teleported = false grabbing = false if self.target_ent == nil then return end - timer.Simple(math.min(self.RestoreDelay,5), function() - if self.target_ent == nil then return end - self:reset_ent_ang() - end) + self:reset_ent_ang() end function PART:reset_ent_ang() - if self.target_ent:IsValid() then - --if self.target_ent:GetClass() == "prop_physics" then return end - if LocalPlayer() == self:GetPlayerOwner() then - net.Start("pac_request_angle_reset_on_entity") - net.WriteAngle(Angle(0,0,0)) - net.WriteFloat(self.RestoreDelay) - net.WriteEntity(self.target_ent) - net.WriteEntity(self:GetPlayerOwner()) - net.SendToServer() - end - if self.Players then - self.target_ent:DisableMatrix("RenderMultiply") - end + if self.target_ent == nil then return end + local reset_ent = self.target_ent + if reset_ent:IsValid() then + timer.Simple(math.min(self.RestoreDelay,5), function() + if LocalPlayer() == self:GetPlayerOwner() then + net.Start("pac_request_angle_reset_on_entity") + net.WriteAngle(Angle(0,0,0)) + net.WriteFloat(self.RestoreDelay) + net.WriteEntity(reset_ent) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() + end + if self.Players then + self.target_ent:DisableMatrix("RenderMultiply") + end + end) end end @@ -308,4 +312,4 @@ function PART:CalculateRelativeOffset() print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles()) end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From 8f0a2031d83820fa7e941baabb7d059d266987b5 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 15 May 2023 17:01:25 -0400 Subject: [PATCH 013/300] update fixed interpolated multibone global angles to get proper midpoints teleport safeties for lock part different table merging function for damage zone and some changes from main repo --- lua/pac3/core/client/bones.lua | 18 ++++ lua/pac3/core/client/parts/damage_zone.lua | 14 +++- .../client/parts/interpolated_multibone.lua | 79 ++++++++++++------ lua/pac3/core/client/parts/lock.lua | 22 ++++- lua/pac3/core/client/parts/model.lua | 9 +- lua/pac3/editor/client/menu_bar.lua | 3 +- lua/pac3/editor/client/parts.lua | 74 +++++------------ lua/pac3/editor/client/view.lua | 38 +++++++++ lua/pac3/extra/shared/net_combat.lua | 82 ++++++++++++++----- 9 files changed, 229 insertions(+), 110 deletions(-) diff --git a/lua/pac3/core/client/bones.lua b/lua/pac3/core/client/bones.lua index ac99fd001..477c506dd 100644 --- a/lua/pac3/core/client/bones.lua +++ b/lua/pac3/core/client/bones.lua @@ -107,6 +107,9 @@ function pac.GetAllBones(ent) tbl.footstep = {friendly = "footsteps", is_special = true} tbl.skirt = {friendly = "skirt", is_special = true} tbl.skirt2 = {friendly = "skirt2", is_special = true} + tbl.hitpos_world_props = {friendly = "hitpos_world_props", is_special = true} + tbl.hitpos_world = {friendly = "hitpos_world", is_special = true} + tbl.hitpos_world_noang = {friendly = "hitpos_world_noang", is_special = true} tbl.hitpos_ent_ang = {friendly = "hitpos_ent_ang", is_special = true} tbl.hitpos_ent_ang_zero_pitch = {friendly = "hitpos_ent_ang_zero_pitch", is_special = true} tbl.pos_ang = {friendly = "pos_ang", is_special = true} @@ -261,6 +264,21 @@ function pac.GetBonePosAng(ent, id, parent) return ent:EyePos(), ent:EyeAngles() elseif id == "eyepos_ang" then return ent:EyePos(), ent:GetAngles() + elseif id == "hitpos_world_props" then + local res = util_QuickTrace(ent:EyePos(), ent:EyeAngles():Forward() * 16000, function(ent) + return ent:GetClass() == "prop_physics" + end) + return res.HitPos, res.HitNormal:Angle() + elseif id == "hitpos_world" then + local res = util_QuickTrace(ent:EyePos(), ent:EyeAngles():Forward() * 16000, function(ent) + return ent:IsWorld() + end) + return res.HitPos, res.HitNormal:Angle() + elseif id == "hitpos_world_noang" then + local res = util_QuickTrace(ent:EyePos(), ent:EyeAngles():Forward() * 16000, function(ent) + return ent:IsWorld() + end) + return res.HitPos, angle_origin elseif id == "hitpos" or id == "hit position" then if ent.pac_traceres then return ent.pac_traceres.HitPos, ent.pac_traceres.HitNormal:Angle() diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index bcb9e66e9..998f0b020 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -30,6 +30,7 @@ BUILDER:StartStorableVars() :GetSet("Length", 50) :GetSet("HitboxMode", "Box", {enums = { ["Box"] = "Box", + ["Cube"] = "Cube", ["Sphere"] = "Sphere", ["Cylinder (Raycasts Only)"] = "Cylinder", ["Cylinder (Hybrid)"] = "CylinderHybrid", @@ -157,12 +158,14 @@ function PART:PreviewHitbox() hook.Add(self.RenderingHook, "pace_draw_hitbox", function() self:GetWorldPosition() if self.HitboxMode == "Box" then - local mat = Matrix() - mat:Rotate(self:GetWorldAngles()) + local mins = Vector(-self.Radius, -self.Radius, -self.Length) + local maxs = Vector(self.Radius, self.Radius, self.Length) + render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Cube" then --mat:Rotate(Angle(SysTime()*100,0,0)) local mins = Vector(-self.Radius, -self.Radius, -self.Radius) local maxs = Vector(self.Radius, self.Radius, self.Radius) - render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0) --[[mat:GetAngles()]], mins, maxs, Color( 255, 255, 255 ) ) + render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) ) elseif self.HitboxMode == "Sphere" then render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) elseif self.HitboxMode == "Cylinder" or self.HitboxMode == "CylinderHybrid" then @@ -266,6 +269,7 @@ function PART:PreviewHitbox() --print("steps",steps, "total casts will be "..steps*self.Detail) for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier phase = math.random() + local ray_thickness = math.Clamp(0.5*math.log(self.Radius) + 0.05*self.Radius,0,10)*(1.5 - 0.7*ringnumber) for i=1,0,-1/sides do if ringnumber == 0 then i = 0 end x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) @@ -273,6 +277,10 @@ function PART:PreviewHitbox() local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false ) end + --[[render.DrawWireframeBox(self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + self:GetWorldAngles():Right() * self.Radius * ringnumber, Angle(0,0,0), + Vector(ray_thickness,ray_thickness,ray_thickness), + Vector(-ray_thickness,-ray_thickness,-ray_thickness), + Color(255,255,255))]] end if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then --fast sphere check on the wide end diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index 5374ac7fa..1d8b62169 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -39,6 +39,11 @@ BUILDER:StartStorableVars() :GetSetPart("Node3") :GetSetPart("Node4") :GetSetPart("Node5") + :GetSetPart("Node6") + :GetSetPart("Node7") + :GetSetPart("Node8") + :GetSetPart("Node9") + :GetSetPart("Node10") :EndStorableVars() --PART:GetWorldPosition() @@ -49,41 +54,41 @@ end function PART:Initialize() print("a multiboner is born") - - self:SetOwner(pac.CreateEntity("models/pac/default.mdl")) + self.Owner = pac.CreateEntity("models/pac/default.mdl") self.Owner:SetNoDraw(true) - pac.HookEntityRender(self.Owner, self) - pac.HookEntityRender(self.Owner, self) - self.Owner.PACPart = self + --self:SetOwner(pac.CreateEntity("models/pac/default.mdl")) + + --pac.HookEntityRender(self.Owner, self) + --self.Owner.PACPart = self + end function PART:OnShow() - + end function PART:OnHide() - hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw") + hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end function PART:OnRemove() - hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw") + hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end --NODES self 1 2 3 --STAGE 0 1 2 3 --PROPORTION 0 0.5 0 0.5 0 0.5 3 function PART:OnDraw() - - local ent = self:GetOwner() + --local ent = self:GetOwner() self.pos,self.ang = self:GetDrawPosition() - if not self.Test1 then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw") end + if not self.Test1 then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end local stage = math.max(0,math.floor(self.LerpValue)) local proportion = math.max(0,self.LerpValue) % 1 if self.Test1 then - hook.Add("PostDrawOpaqueRenderables", "Multibone_draw", function() + hook.Add("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID, function() render.DrawLine(self.pos,self.pos + self.ang:Forward()*50, Color(255,0,0)) render.DrawLine(self.pos,self.pos - self.ang:Right()*50, Color(0,255,0)) render.DrawLine(self.pos,self.pos + self.ang:Up()*50, Color(0,0,255)) @@ -91,15 +96,9 @@ function PART:OnDraw() end) end self:Interpolate(stage,proportion) - - --[[self:PreEntityDraw(ent, pos, ang) - self:DrawModel(ent, pos, ang) - self:PostEntityDraw(ent, pos, ang)]] - ent:SetPos(self.pos) - ent:SetAngles(self.ang) - --self:ShowFromRendering() - --pac.ResetBones(ent) - --ent:DrawModel() + --ent:SetPos(self.pos) + --ent:SetAngles(self.ang) + --self.pos = Vector(0,0,0) end function PART:OnThink() @@ -109,7 +108,6 @@ function PART:OnThink() if self.Node4 ~= nil then nodes["Node4"] = self.Node4 end if self.Node5 ~= nil then nodes["Node5"] = self.Node5 end - end function PART:SetWorldPos(x,y,z) @@ -135,15 +133,48 @@ function PART:Interpolate(stage, proportion) proportion = math.pow(proportion,self.Power) if secondnode ~= nil and secondnode ~= NULL then self.pos = (1-proportion)*(firstnode:GetWorldPosition()) + (secondnode:GetWorldPosition())*proportion - self.ang = (1-proportion)*(firstnode:GetWorldAngles() + Angle(360,360,360)) + (secondnode:GetWorldAngles() + Angle(360,360,360))*proportion + self.ang = GetClosestAngleMidpoint(firstnode:GetWorldAngles(), secondnode:GetWorldAngles(), proportion) + --self.ang = (1-proportion)*(firstnode:GetWorldAngles() + Angle(360,360,360)) + (secondnode:GetWorldAngles() + Angle(360,360,360))*proportion elseif proportion == 0 then self.pos = firstnode:GetWorldPosition() self.ang = firstnode:GetWorldAngles() else self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion - self.ang = (1-proportion)*(self:GetWorldAngles() + Angle(360,360,360)) + (self:GetWorldAngles() + Angle(360,360,360))*proportion + self.ang = GetClosestAngleMidpoint(self:GetWorldAngles(), self:GetWorldAngles(), proportion) + --self.ang = (1-proportion)*(self:GetWorldAngles() + Angle(360,360,360)) + (self:GetWorldAngles() + Angle(360,360,360))*proportion + end + self.Owner:SetPos(self.pos) + self.Owner:SetAngles(self.ang) +end + +function GetClosestAngleMidpoint(a1, a2, proportion) + --print(a1) + --print(a2) + local axes = {"p","y","r"} + local ang_delta_candidate1 + local ang_delta_candidate2 + local ang_delta_candidate3 + local ang_delta_final + local final_ang = Angle() + for _,ax in pairs(axes) do + ang_delta_candidate1 = a2[ax] - a1[ax] + ang_delta_candidate2 = (a2[ax] + 360) - a1[ax] + ang_delta_candidate3 = (a2[ax] - 360) - a1[ax] + ang_delta_final = 180 + if math.abs(ang_delta_candidate1) < math.abs(ang_delta_final) then + ang_delta_final = ang_delta_candidate1 + end + if math.abs(ang_delta_candidate2) < math.abs(ang_delta_final) then + ang_delta_final = ang_delta_candidate2 + end + if math.abs(ang_delta_candidate3) < math.abs(ang_delta_final) then + ang_delta_final = ang_delta_candidate3 + end + --print("at "..ax.." 1:"..ang_delta_candidate1.." 2:"..ang_delta_candidate2.." 3:"..ang_delta_candidate3.." pick "..ang_delta_final) + final_ang[ax] = a1[ax] + proportion * ang_delta_final end + return final_ang end function PART:GoTo(part) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 08223bd90..be34ef648 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -31,6 +31,10 @@ BUILDER:StartStorableVars() :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."}) :GetSet("Preview", false) + :SetPropertyGroup("TeleportSafety") + :GetSet("ClampDistance", false, {description = "Prevents the teleport from going too far (By Radius amount). For example, if you use hitpos bone on a pac model, it can act as a safety in case the raycast falls out of bounds."}) + :GetSet("SlopeSafety", false, {description = "Teleports a bit up in case you end up on a slope and get stuck."}) + :SetPropertyGroup("PlayerCameraOverride") :GetSet("OverrideEyeAngles", true, {description = "Whether the part will try to override players' eye angles. Requires OverrideAngles and user consent"}) :GetSetPart("OverrideEyePositionPart") @@ -150,8 +154,22 @@ function PART:OnThink() ang_yaw_only.p = 0 ang_yaw_only.r = 0 if LocalPlayer() == self:GetPlayerOwner() then + + local teleport_pos_final = self:GetWorldPosition() + + if self.LimitTeleportDistanceByRadius then + local ply_pos = self:GetPlayerOwner():GetPos() + local pos = self:GetWorldPosition() + + if pos:Distance(ply_pos) > self.Radius then + local clamped_pos = ply_pos + (pos - ply_pos):GetNormalized()*self.Radius + --print(clamped_pos:Length()) + teleport_pos_final = clamped_pos + end + end + if self.SlopeSafety then teleport_pos_final = teleport_pos_final + Vector(0,0,30) end net.Start("pac_request_position_override_on_entity") - net.WriteVector(self:GetWorldPosition()) + net.WriteVector(teleport_pos_final) net.WriteAngle(ang_yaw_only) net.WriteBool(self.OverrideAngles) net.WriteEntity(self:GetPlayerOwner()) @@ -312,4 +330,4 @@ function PART:CalculateRelativeOffset() print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles()) end -BUILDER:Register() +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/model.lua b/lua/pac3/core/client/parts/model.lua index 318729295..ad8e952d7 100644 --- a/lua/pac3/core/client/parts/model.lua +++ b/lua/pac3/core/client/parts/model.lua @@ -749,13 +749,12 @@ end function PART:SetScale(vec) max_scale = GetConVar("pac_model_max_scales"):GetFloat() largest_scale = math.max(math.abs(vec.x), math.abs(vec.y), math.abs(vec.z)) - if vec and not LocalPlayer() == self:GetPlayerOwner() and max_scale > 0 then --clamp for other players if they have pac_model_max_scales convar more than 0 + if vec and max_scale > 0 and (LocalPlayer() ~= self:GetPlayerOwner()) then --clamp for other players if they have pac_model_max_scales convar more than 0 vec = Vector(math.Clamp(vec.x, -max_scale, max_scale), math.Clamp(vec.y, -max_scale, max_scale), math.Clamp(vec.z, -max_scale, max_scale)) - elseif largest_scale > 10000 then --warn about the default max scale - pac.Message("Your model ", self, " scale is beyond the default limit! It will be limited on other clients' rendering. But then again you probably shouldn't have gigantic scales to begin with.") - self:SetError("Scale is being limited due to having an excessive component") + end + if largest_scale > 10000 then --warn about the default max scale + self:SetError("Scale is being limited due to having an excessive component. Default maximum values are 10000") else self:SetError() end --if ok, clear the warning - vec = vec or Vector(1,1,1) self.Scale = vec diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 1a55fe64d..15f8f5588 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -93,7 +93,8 @@ local function populate_view(menu) function() pace.Call("ToggleFocus") chat.AddText("[PAC3] \"ctrl + e\" to get the editor back") end):SetImage("icon16/application_delete.png") - menu:AddCVar(L"camera follow", "pac_camera_follow_entity", "1", "0"):SetImage("icon16/camera_go.png") + menu:AddCVar(L"camera follow: "..GetConVar("pac_camera_follow_entity"):GetInt(), "pac_camera_follow_entity", "1", "0"):SetImage("icon16/camera_go.png") + menu:AddCVar(L"enable editor camera: "..GetConVar("pac_enable_editor_view"):GetInt(), "pac_enable_editor_view", "1", "0"):SetImage("icon16/camera.png") menu:AddOption(L"reset view position", function() pace.ResetView() end):SetImage("icon16/camera_link.png") menu:AddOption(L"reset zoom", function() pace.ResetZoom() end):SetImage("icon16/magnifier.png") end diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 7fc7eaed7..e1a189c4a 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -837,7 +837,7 @@ do -- menu sent_var.x = tonumber(str[1]) or 1 sent_var.y = tonumber(str[2]) or 1 sent_var.z = tonumber(str[3]) or 1 - if v and not part.ProperColorRange then sent_var = sent_var*255 end + if v == "Color" and not part.ProperColorRange then sent_var = sent_var*255 end elseif var_type == "Angle" then local str = string.Split(VAR_PANEL_EDITZONE:GetValue(), ",") sent_var = Angle() @@ -905,6 +905,7 @@ do -- menu local udata_val_name = string.gsub(v, "event_udata_", "") local var_type = udata_types[udata_val_name] + if var_type == nil then var_type = "string" end local VAR_PANEL = vgui.Create("DFrame") @@ -967,23 +968,25 @@ do -- menu else sent_var = VAR_PANEL_EDITZONE:GetValue() end local arg_split = string.Split(part:GetArguments(), "@@") - if #udata_orders[part.Event] ~= #string.Split(part:GetArguments(), "@@") then arg_split[#arg_split + 1] = "0" end - - local sent_var_final = "" - -- PRESCRIBED X @@ Y @@ Z ... - -- EVERY STEP ADD - -- EVERY STEP EXCEPT LAST..@@ - for n,arg in ipairs(arg_split) do - if udata_orders[part.Event][n] == udata_val_name then - sent_var_final = sent_var_final .. sent_var - else - sent_var_final = sent_var_final .. arg_split[n] - end - if n ~= #arg_split then - sent_var_final = sent_var_final .. "@@" - end - end + if #arg_split > 1 then + if #udata_orders[part.Event] ~= #string.Split(part:GetArguments(), "@@") then arg_split[#arg_split + 1] = "0" end + + local sent_var_final = "" + -- PRESCRIBED X @@ Y @@ Z ... + -- EVERY STEP ADD + -- EVERY STEP EXCEPT LAST..@@ + for n,arg in ipairs(arg_split) do + if udata_orders[part.Event][n] == udata_val_name then + sent_var_final = sent_var_final .. sent_var + else + sent_var_final = sent_var_final .. arg_split[n] + end + if n ~= #arg_split then + sent_var_final = sent_var_final .. "@@" + end + end + else sent_var_final = sent_var end part:SetArguments(sent_var_final) end @@ -1442,39 +1445,4 @@ do --hover highlight halo pac.haloex.Add(tbl, halo_color, 2, 2, pulseamount, true, true, pulseamount, 1, 1) --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) end -end - - ---[[ test visualise part -local cachedmodel = ClientsideModel( obj:GetRootPart():GetOwner():GetModel(), RENDERGROUP_BOTH ) - -print(tbl ~= nil) -previewmdl_tbl = previewmdl_tbl or obj:ToTable() - -timer.Simple(8,function() Panel:Remove() end) - -ENT = cachedmodel ---ENT = icon:GetEntity() -pac.SetupENT(ENT) -ENT:AttachPACPart(tbl, ENT:GetOwner(), false) - - -function Panel:Paint( w, h ) - - local x, y = self:GetPos() - local vec = LocalPlayer():GetPos() - - cachedmodel:SetPos(vec + Vector(100,0,50)) - cachedmodel:SetAngles(Angle(0,180 + SysTime()*60,0)) - render.RenderView( { - origin = vec + Vector( 0, 0, 90 ), - angles = Angle(0,0,0), - x = x, y = y, - w = w, h = h, - fov = 50, aspect = w/h - } ) - -end -timer.Simple(8, function() - SafeRemoveEntity(cachedmodel) -end)]] +end \ No newline at end of file diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 1a8a9adfe..8f04a0905 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -119,6 +119,17 @@ function pace.GUIMouseReleased(mc) if pace.editing_viewmodel or pace.editing_hands then return end mcode = nil + if not GetConVar("pac_enable_editor_view"):GetBool() then pace.EnableView(true) + else + pac.RemoveHook("CalcView", "camera_part") + pac.AddHook("GUIMousePressed", "editor", pace.GUIMousePressed) + pac.AddHook("GUIMouseReleased", "editor", pace.GUIMouseReleased) + pac.AddHook("ShouldDrawLocalPlayer", "editor", pace.ShouldDrawLocalPlayer, DLib and -4 or ULib and -1 or nil) + pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil) + pac.AddHook("HUDPaint", "editor", pace.HUDPaint) + pac.AddHook("HUDShouldDraw", "editor", pace.HUDShouldDraw) + pac.AddHook("PostRenderVGUI", "editor", pace.PostRenderVGUI) + end end local function set_mouse_pos(x, y) @@ -238,6 +249,7 @@ local function CalcDrag() end local follow_entity = CreateClientConVar("pac_camera_follow_entity", "0", true) +local enable_editor_view = CreateClientConVar("pac_enable_editor_view", "1", true) local lastEntityPos function pace.CalcView(ply, pos, ang, fov) @@ -331,6 +343,7 @@ function pace.EnableView(b) pac.AddHook("GUIMouseReleased", "editor", pace.GUIMouseReleased) pac.AddHook("ShouldDrawLocalPlayer", "editor", pace.ShouldDrawLocalPlayer, DLib and -4 or ULib and -1 or nil) pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil) + pac.RemoveHook("CalcView", "camera_part") pac.AddHook("HUDPaint", "editor", pace.HUDPaint) pac.AddHook("HUDShouldDraw", "editor", pace.HUDShouldDraw) pac.AddHook("PostRenderVGUI", "editor", pace.PostRenderVGUI) @@ -342,11 +355,36 @@ function pace.EnableView(b) pac.RemoveHook("GUIMouseReleased", "editor") pac.RemoveHook("ShouldDrawLocalPlayer", "editor") pac.RemoveHook("CalcView", "editor") + pac.RemoveHook("CalcView", "camera_part") pac.RemoveHook("HUDPaint", "editor") pac.RemoveHook("HUDShouldDraw", "editor") pac.RemoveHook("PostRenderVGUI", "editor") pace.SetTPose(false) end + + if not enable_editor_view:GetBool() then + local ply = LocalPlayer() + pac.RemoveHook("CalcView", "editor") + pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) + for _, part in pairs(pac.GetLocalParts()) do + if part:IsValid() and part.ClassName == "camera" then + part:CalcShowHide() + local temp = {} + if not part:IsHidden() then + pos, ang, fov, nearz, farz = part:CalcView(_,_,ply:EyeAngles()) + temp.origin = pos + temp.angles = ang + temp.fov = fov + temp.znear = nearz + temp.zfar = farz + temp.drawviewer = not part.DrawViewModel + return temp + end + end + end + end) + --pac.RemoveHook("ShouldDrawLocalPlayer", "editor") + end end local function CalcAnimationFix(ent) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index e3cc2c0c7..965b06b89 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -50,6 +50,22 @@ local damage_types = { } if SERVER then + local function maximized_ray_mins_maxs(startpos,endpos,padding) + local maxsx,maxsy,maxsz + local highest_sq_distance = 0 + for xsign = -1, 1, 2 do + for ysign = -1, 1, 2 do + for zsign = -1, 1, 2 do + local distance_tried = (startpos + Vector(padding*xsign,padding*ysign,padding*zsign)):DistToSqr(endpos - Vector(padding*xsign,padding*ysign,padding*zsign)) + if distance_tried > highest_sq_distance then + highest_sq_distance = distance_tried + maxsx,maxsy,maxsz = xsign,ysign,zsign + end + end + end + end + return Vector(padding*maxsx,padding*maxsy,padding*maxsz),Vector(padding*-maxsx,padding*-maxsy,padding*-maxsz) + end util.AddNetworkString("pac_hitscan") util.AddNetworkString("pac_request_position_override_on_entity") util.AddNetworkString("pac_request_angle_reset_on_entity") @@ -93,9 +109,17 @@ if SERVER then if tbl.HitboxMode == "Sphere" then local ents_hits = ents.FindInSphere(pos, tbl.Radius) ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) - elseif tbl.HitboxMode == "Box" then - local mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Radius) - local maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius) + elseif tbl.HitboxMode == "Box" or tbl.HitboxMode == "Cube" then + local mins + local maxs + if tbl.HitboxMode == "Box" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) + elseif tbl.HitboxMode == "Cube" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Radius) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius) + end + local ents_hits = ents.FindInBox(mins, maxs) ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then @@ -114,24 +138,25 @@ if SERVER then --print("steps",steps, "total casts will be "..steps*self.Detail) for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier phase = math.random() + local ray_thickness = math.Clamp(0.5*math.log(tbl.Radius) + 0.05*tbl.Radius,0,10)*(1 - 0.7*ringnumber) for i=1,0,-1/sides do if ringnumber == 0 then i = 0 end x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) local startpos = pos + x + y local endpos = pos + ang:Forward()*tbl.Length + x + y - table.Merge(ents_hits, ents.FindAlongRay(startpos, endpos)) + MergeTargetsByID(ents_hits, ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness))) end end if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then --fast sphere check on the wide end if tbl.Length/tbl.Radius >= 2 then - table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius), tbl.Radius)) - table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Radius, tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius), tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Radius, tbl.Radius)) if tbl.Radius ~= 0 then local counter = 0 for i=math.floor(tbl.Length / tbl.Radius) - 1,1,-1 do - table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Radius*i), tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Radius*i), tbl.Radius)) if counter == 100 then break end counter = counter + 1 end @@ -139,21 +164,21 @@ if SERVER then --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) end end - elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) elseif tbl.HitboxMode == "CylinderSpheres" then local ents_hits = {} if tbl.Length ~= 0 and tbl.Radius ~= 0 then local counter = 0 - table.Inherit(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do - table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) if counter == 200 then break end counter = counter + 1 end - table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) - elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then local ents_hits = {} @@ -170,9 +195,11 @@ if SERVER then steps = math.max(steps + math.abs(tbl.ExtraSteps),1) --print("steps",steps, "total casts will be "..steps*self.Detail) local timestart = SysTime() - for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier - + local casts = 0 + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the ringnumber multiplier phase = math.random() + local ray_thickness = 5 * (2 - ringnumber) + --print("ring " .. ringnumber .. " phase " .. phase) for i=1,0,-1/sides do --print("radius " .. tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize)) @@ -180,17 +207,19 @@ if SERVER then x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) local endpos = pos + ang:Forward()*tbl.Length + x + y - table.Inherit(ents_hits,ents.FindAlongRay(startpos, endpos)) + MergeTargetsByID(ents_hits,ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness))) + casts = casts + 1 end end + print(casts .. " casts") if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then --fast sphere check on the wide end local radius_multiplier = math.atan(math.abs(ratio)) / (1.5 + 0.1*math.sqrt(ratio)) if ratio > 0.5 then - table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius * radius_multiplier), tbl.Radius * radius_multiplier)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius * radius_multiplier), tbl.Radius * radius_multiplier)) end end - elseif tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) elseif tbl.HitboxMode == "ConeSpheres" then local ents_hits = {} @@ -198,15 +227,15 @@ if SERVER then steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) for i = 1,0,-1/steps do --PrintTable(ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) - table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) end steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) for i = 0,1/8,1/128 do --PrintTable(ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) - table.Inherit(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + --MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) end - if tbl.Radius == 0 then table.Inherit(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) elseif tbl.HitboxMode =="Ray" then local startpos = pos + Vector(0,0,0) @@ -228,6 +257,9 @@ if SERVER then end) function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + print("received", #ents_hits, "potential targets") + --print("process hurt ", #ents_hits) + --PrintTable(ents_hits) local bullet = {} bullet.Src = pos + ang:Forward() bullet.Dir = ang:Forward()*50000 @@ -269,10 +301,10 @@ if SERVER then if (ent == dmg_info:GetInflictor() and tbl.AffectSelf) then ent:TakeDamageInfo(dmg_info) print(ent, "hurt themself") - elseif damage_zone_consents[ent] == true then + elseif damage_zone_consents[ent] == true or ent:IsBot() then ent:TakeDamageInfo(dmg_info) print(dmg_info:GetAttacker(), "hurt", ent) - end + else print("can't do that because",ent,damage_zone_consents[ent]) end else ent:TakeDamageInfo(dmg_info) print(dmg_info:GetAttacker(), "hurt", ent) @@ -285,6 +317,12 @@ if SERVER then end end + function MergeTargetsByID(tbl1, tbl2) + for i,v in pairs(tbl2) do + tbl1[v:EntIndex()] = v + end + end + net.Receive("pac_request_position_override_on_entity", function(len, ply) local pos = net.ReadVector() From 4e4e65c0d77282751b01267821c74a2d22845d9f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 7 Sep 2023 21:34:34 -0400 Subject: [PATCH 014/300] Update README.md announcement / information about an upcoming major update --- README.md | 142 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index beece8d00..13280a380 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,146 @@ --- -PAC3 gives you the ability to personalize your player model's look by placing objects and effects on yourself. You can go from putting just a hat on your head to creating an entire new player model. PAC works on any entity and can also be used as a way to make custom weapons and npcs for your gamemode easily. +Welcome to my experimental combat update for PAC3. Here's the overview of the important bits to expect. -You can wear your outfit on any server with PAC3 and everyone should be able to see it on you as long as they have the content you used. If you're using content from Episode 2 (which very few people have mounted) they might see your models as errors. +I have a major update coming soon, when I finish wrapping some things up, so some of these aren't in yet. -You can also load custom models (.obj), textures (.jpg and .png) and sounds (.ogg and .wav) from URL. -By default, PAC3 is in basic mode. You can change this by going to options enable advanced mode. +# New combat-related parts: ---- + damage_zone: deals damage (a more direct and controllable alternative to projectiles) + + hitscan: shoots bullets + + lock: teleport/grab + + force: does physics forces + + health_modifier: changes your health, armor etc + + interpolated_multibone: morphs position / angles between different base_movables nodes, like a path + + +The combat features work with the principle of consent. The lock part especially is severely restricted for grabbing players, for what should be obvious reasons. You can only damage or grab players who have opted in for the corresponding consent. + + pac_client_damage_zone_consent 0 + pac_client_hitscan_consent 0 + pac_client_force_consent 0 + pac_client_grab_consent 0 + pac_client_lock_camera_consent 1 + +There are also commands for clients to free themselves if they're being grabbed. + + pac_break_lock + pac_stop_lock + +Multiple options exist for servers to prevent mass abuse. Although I might've had things to say about server owners being resistant to new disruptive features, I've come to a compromise in the form of cvars. size limits, damage limits, which combat parts are allowed... + + pac_sv_combat_whitelisting 0 + pac_sv_damage_zone 1 + pac_sv_lock 1 + pac_sv_lock_grab 1 + pac_sv_lock_teleport 1 + pac_sv_lock_max_grab_radius 200 + ... + + +# Editor features: + +## Bulk Select + +Select multiple parts and do some basic operations repeatedly. By default it's CTRL + click to select/unselect a part. + +Along with it, bulk apply properties is a new menu to change multiple parts' properties at once. + + +## Extensive customizability (user configs will be saved in data/pac3_config) + +Customizable shortcuts for almost every action (in the pac settings menu). + +Reordering the part menu actions layout (in the pac settings menu). + +Changing your part categories, with possible custom icons. (no menu, you'll have to edit the pac_part_categories.txt file directly) + + +## Expanded settings menu + +Clients can configure their editor experience, and owners with server access can configure serverwide combat-related limits and policies. + +## Favorite assets for quick access (user configs will be saved in data/pac3_config) + +right click on assets in the pac asset browser to save it to your favorites. it can also try to do series if they end in a number, but it might fail. right clicking on the related field will bring it up in your list + +## Popup system + +select a part and press F1 to open information about it. limited support but it will be useful later on. It can be configured to be on a part in your viewport, on your cursor, next to the part's tree label ... + +## Editor autopilot + +An idea: correct common mistakes automatically or inform the user about it. -The Wiki for PAC3 can be found -[here](https://github.com/CapsAdmin/pac3/wiki "PAC3 Wiki") +For now, it's only two things: selecting an event will pick an appropriate operator, and clicking away from a proxy without a variable name will notify you about how it won't work, telling you to go back and change it -This addon is also on the [workshop](http://steamcommunity.com/sharedfiles/filedetails/?id=104691717 "Workshop Version") -If you're completely new to PAC check out the [Beginners FAQ](https://github.com/CapsAdmin/pac3/wiki/Beginners-FAQ "Beginners FAQ") +# Reference and help features -If you know the basics check out the [Intermediate FAQ](https://github.com/CapsAdmin/pac3/wiki/Intermediate-FAQ "Intermediate FAQ") +proxy bank: some presets with tooltip explanations. right click on the expression field to look at them + +command bank: presets to use the command part. again, right click on the expression field to look at them + +built-in wiki written by me, for every part and most event types: short tooltips to tell you what a part does when you hover over the label when choosing which part to create, longer tutorials opened with F1 when you select an existing part. -You can join the official PAC3 Discord Guild [here](https://discord.gg/utpR3gJ "Join PAC3 Discord Guild") + +# Miscellaneous features + +## Part notes + +a text field for the base_part, so you can write notes on any part. + +## pac_event_sequenced + +pac_event but with more options to control series of numbered events. + +pac will try to register the max number when you create a command event with the relevant number e.g. to reach command10 you need to have a command event with the name command10. rewear for best results. + +examples: + +this increments by 1 (and loops back if necessary) + + pac_event_sequenced hat_style + + +this sets the series to 3 + + pac_event_sequenced hat_style set 3 + +keywords for going forward: +, add, backward, advance, sequence+ + +keywords for going backward: -, sub, backward, sequence- + +keyword to set: set + + +## Improvements to physics and projectile parts + +Set the surface properties, preview the sizes and some more. + +For projectiles to change the physics mesh, it might have some issues. + +## Bigger fonts for the editor + pac_editor_scale for the tree's scale + +just a quick edit for people with higher resolution screens + +## New tools + +-destroy hidden parts, proxies and events. I also call it Ultra cleanup. This is a quick but destructive optimization tool to improve framerate by only keeping visible parts and obliterating non-static elements. You can mark parts to keep by writing "important" in their notes field. + +-Engrave targets: assign proxies and events' target part to quickly allow you to reorganize them in a separate group in the editor. + +-dump model submaterials: same as dump player submaterials (prints the submaterials in the console) but for a pac3 model you select in the tree --- +### Thank you for reading. Now go make something cool! + +### Yours truly, +### Cédric. From a0c8c81055106e2c34bf72c02146cd088d9e125f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 7 Sep 2023 22:26:25 -0400 Subject: [PATCH 015/300] Update README.md fix a consistency error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13280a380..a4ec110d6 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ this sets the series to 3 pac_event_sequenced hat_style set 3 -keywords for going forward: +, add, backward, advance, sequence+ +keywords for going forward: +, add, forward, advance, sequence+ keywords for going backward: -, sub, backward, sequence- From 89c680d674f0e427b4c0a56bd19cbeb4d3dac037 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 12 Sep 2023 17:20:49 -0400 Subject: [PATCH 016/300] mega update read the readme --- lua/pac3/core/client/base_movable.lua | 67 +- lua/pac3/core/client/base_part.lua | 75 + lua/pac3/core/client/part_pool.lua | 4 + lua/pac3/core/client/parts/camera.lua | 132 +- lua/pac3/core/client/parts/damage_zone.lua | 496 +++- lua/pac3/core/client/parts/event.lua | 2013 ++++++++++++++++- lua/pac3/core/client/parts/force.lua | 233 ++ lua/pac3/core/client/parts/group.lua | 4 + .../core/client/parts/health_modifier.lua | 155 ++ lua/pac3/core/client/parts/hitscan.lua | 70 +- .../client/parts/interpolated_multibone.lua | 114 +- lua/pac3/core/client/parts/light.lua | 20 + lua/pac3/core/client/parts/lock.lua | 488 ++-- lua/pac3/core/client/parts/movement.lua | 33 +- lua/pac3/core/client/parts/physics.lua | 214 +- lua/pac3/core/client/parts/projectile.lua | 258 ++- lua/pac3/core/client/parts/proxy.lua | 35 +- lua/pac3/core/client/parts/sound.lua | 22 +- lua/pac3/core/client/parts/text.lua | 421 +++- lua/pac3/core/server/event.lua | 56 +- lua/pac3/core/server/net_messages.lua | 136 ++ lua/pac3/core/shared/movement.lua | 137 +- lua/pac3/core/shared/util.lua | 2 +- lua/pac3/editor/client/asset_browser.lua | 211 +- lua/pac3/editor/client/fonts.lua | 4 +- lua/pac3/editor/client/logic.lua | 1 + lua/pac3/editor/client/menu_bar.lua | 55 + lua/pac3/editor/client/panels/editor.lua | 11 +- .../editor/client/panels/extra_properties.lua | 102 +- lua/pac3/editor/client/panels/pac_tree.lua | 2 +- lua/pac3/editor/client/panels/properties.lua | 586 ++++- lua/pac3/editor/client/panels/tree.lua | 189 +- lua/pac3/editor/client/parts.lua | 1114 ++++++++- .../editor/client/popups_part_tutorials.lua | 1170 ++++++++++ lua/pac3/editor/client/saved_parts.lua | 133 +- lua/pac3/editor/client/settings.lua | 1567 ++++++++++++- lua/pac3/editor/client/shortcuts.lua | 896 ++++++-- lua/pac3/editor/client/tools.lua | 53 + lua/pac3/editor/client/view.lua | 116 +- lua/pac3/editor/client/wear.lua | 63 +- lua/pac3/editor/server/bans.lua | 40 + lua/pac3/editor/server/combat_bans.lua | 98 + lua/pac3/editor/server/init.lua | 11 +- .../editor/server/pac_settings_manager.lua | 13 + lua/pac3/extra/shared/net_combat.lua | 1494 +++++++++++- lua/pac3/extra/shared/projectiles.lua | 76 +- models/pac/circle.dx80.vtx | Bin 0 -> 1089 bytes models/pac/circle.dx90.vtx | Bin 0 -> 1089 bytes models/pac/circle.mdl | Bin 0 -> 1692 bytes models/pac/circle.sw.vtx | Bin 0 -> 1081 bytes models/pac/circle.vvd | Bin 0 -> 4160 bytes models/pac/plane.dx80.vtx | Bin 0 -> 189 bytes models/pac/plane.dx90.vtx | Bin 0 -> 189 bytes models/pac/plane.mdl | Bin 0 -> 1692 bytes models/pac/plane.sw.vtx | Bin 0 -> 181 bytes models/pac/plane.vvd | Bin 0 -> 320 bytes 56 files changed, 12018 insertions(+), 1172 deletions(-) create mode 100644 lua/pac3/core/client/parts/force.lua create mode 100644 lua/pac3/core/client/parts/health_modifier.lua create mode 100644 lua/pac3/editor/client/popups_part_tutorials.lua create mode 100644 lua/pac3/editor/server/combat_bans.lua create mode 100644 lua/pac3/editor/server/pac_settings_manager.lua create mode 100644 models/pac/circle.dx80.vtx create mode 100644 models/pac/circle.dx90.vtx create mode 100644 models/pac/circle.mdl create mode 100644 models/pac/circle.sw.vtx create mode 100644 models/pac/circle.vvd create mode 100644 models/pac/plane.dx80.vtx create mode 100644 models/pac/plane.dx90.vtx create mode 100644 models/pac/plane.mdl create mode 100644 models/pac/plane.sw.vtx create mode 100644 models/pac/plane.vvd diff --git a/lua/pac3/core/client/base_movable.lua b/lua/pac3/core/client/base_movable.lua index f9886c30d..4175fe7aa 100644 --- a/lua/pac3/core/client/base_movable.lua +++ b/lua/pac3/core/client/base_movable.lua @@ -24,6 +24,9 @@ BUILDER ["player eyes"] = "PLAYEREYES", ["local eyes yaw"] = "LOCALEYES_YAW", ["local eyes pitch"] = "LOCALEYES_PITCH", + ["nearest npc or player (torso-level)"] = "NEAREST_LIFE", + ["nearest npc or player (entity position)"] = "NEAREST_LIFE_POS", + ["nearest npc or player (yaw only)"] = "NEAREST_LIFE_YAW" }}) :GetSetPart("Parent") :EndStorableVars() @@ -37,7 +40,7 @@ do -- bones function PART:GetBonePosition() local parent = self:GetParent() if parent:IsValid() then - if parent.ClassName == "jiggle" then + if parent.ClassName == "jiggle" or parent.ClassName == "interpolated_multibone" then return parent.pos, parent.ang elseif not parent.is_model_part and @@ -61,11 +64,13 @@ do -- bones function PART:GetBoneMatrix() local parent = self:GetParent() - if parent:IsValid() then - if parent.ClassName == "jiggle" then + if parent:IsValid() or IsValid(parent) then + if parent.ClassName == "jiggle" or parent.ClassName == "interpolated_multibone" then local bone_matrix = Matrix() - bone_matrix:SetTranslation(parent.pos) - bone_matrix:SetAngles(parent.ang) + if parent.pos then + bone_matrix:SetTranslation(parent.pos) + bone_matrix:SetAngles(parent.ang) + end return bone_matrix elseif not parent.is_model_part and @@ -184,6 +189,58 @@ function PART:CalcAngles(ang, wpos) return self.Angles + (pac.EyePos - wpos):Angle() end + + if pac.StringFind(self.AimPartName, "NEAREST_LIFE_YAW", true, true) then + local nearest_ent = self:GetRootPart():GetOwner() + local nearest_dist = math.huge + local owner_ent = self:GetRootPart():GetOwner() + local part_ent = self:GetOwner() + for _,ent in pairs(ents.GetAll()) do + if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then + local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() + if dist < nearest_dist then + nearest_ent = ent + nearest_dist = dist + end + end + end + local ang = (nearest_ent:GetPos() - part_ent:GetPos()):Angle() + return Angle(0,ang.y,0) + self.Angles + end + + if pac.StringFind(self.AimPartName, "NEAREST_LIFE_POS", true, true) then + local nearest_ent = self:GetRootPart():GetOwner() + local nearest_dist = math.huge + local owner_ent = self:GetRootPart():GetOwner() + local part_ent = self:GetOwner() + for _,ent in pairs(ents.GetAll()) do + if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then + local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() + if dist < nearest_dist then + nearest_ent = ent + nearest_dist = dist + end + end + end + return self.Angles + (nearest_ent:GetPos() - part_ent:GetPos()):Angle() + end + + if pac.StringFind(self.AimPartName, "NEAREST_LIFE", true, true) then + local nearest_ent = self:GetRootPart():GetOwner() + local nearest_dist = math.huge + local owner_ent = self:GetRootPart():GetOwner() + local part_ent = self:GetOwner() + for _,ent in pairs(ents.GetAll()) do + if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then + local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() + if dist < nearest_dist then + nearest_ent = ent + nearest_dist = dist + end + end + end + return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - part_ent:GetPos()):Angle() + end if self.AimPart:IsValid() and self.AimPart.GetWorldPosition then return self.Angles + (self.AimPart:GetWorldPosition() - wpos):Angle() diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 778678008..0218a5a81 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -31,6 +31,7 @@ BUILDER :StartStorableVars() :SetPropertyGroup("generic") :GetSet("Name", "") + :GetSet("Notes", "") :GetSet("Hide", false) :GetSet("EditorExpand", false, {hidden = true}) :GetSet("UniqueID", "", {hidden = true}) @@ -200,10 +201,12 @@ do -- owner end function PART:GetParentOwner() + if self.TargetEntity:IsValid() and self.TargetEntity ~= self then return self.TargetEntity:GetOwner() end + for _, parent in ipairs(self:GetParentList()) do -- legacy behavior @@ -1147,4 +1150,76 @@ do function PART:AlwaysOnThink() end end +--the popup system +function PART:SetupEditorPopup(str, force_open, tbl) + + if not IsValid(self) then return end + + local popup_config_table = tbl or { + pac_part = self, obj_type = GetConVar("pac_popups_preferred_location"):GetString(), + hoverfunc = function() end, + doclickfunc = function() end, + panel_exp_width = 900, panel_exp_height = 400 + } + + local default_state = str == nil or str == "" + local info_string = str or self.ClassName .. "\nno special information available" + + if default_state and pace then + local partsize_tbl = pace.GetPartSizeInformation(self) + info_string = info_string .. "\n" .. partsize_tbl.info .. ", " .. partsize_tbl.all_share_percent .. "% of all parts" + end + + if self.Notes and self.Notes ~= "" then + info_string = info_string .. "\n\nNotes:\n\n" .. self.Notes + end + + local tree_node = self.pace_tree_node + local part = self + self.killpopup = false + local pnl + + --local pace = pace or {} + if tree_node then + tree_node.Label:SetTooltip(self.ClassName) + local part = self + + function tree_node:Think() + --if not part.killpopup and ((self.Label:IsHovered() and GetConVar("pac_popups_preferred_location"):GetString() == "pac tree label") or input.IsButtonDown(KEY_F1) or force_open) then + if not part.killpopup and ((self.Label:IsHovered() and GetConVar("pac_popups_preferred_location"):GetString() == "pac tree label") or force_open) then + if not self.popuppnl_is_up and not IsValid(self.popupinfopnl) and not part.killpopup then + self.popupinfopnl = pac.InfoPopup( + info_string, + popup_config_table + ) + self.popuppnl_is_up = true + end + + --if IsValid(self.popupinfopnl) then self.popupinfopnl:MakePopup() end + pnl = self.popupinfopnl + + end + if not IsValid(self.popupinfopnl) then self.popupinfopnl = nil self.popuppnl_is_up = false end + end + end + return pnl +end + +function PART:AttachEditorPopup(str, flash, tbl) + local pnl = self:SetupEditorPopup(str, flash, tbl) + if flash and pnl then + pnl:MakePopup() + end +end + +function PART:DetachEditorPopup() + local tree_node = self.pace_tree_node + if tree_node then + if tree_node.popupinfopnl then + tree_node.popupinfopnl:Remove() + end + if not IsValid(tree_node.popupinfopnl) then tree_node.popupinfopnl = nil end + end +end + BUILDER:Register() diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 8bf2da5a9..d8da6b2f1 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -44,6 +44,10 @@ local ent_parts = _G.pac_local_parts or {} local all_parts = _G.pac_all_parts or {} local uid_parts = _G.pac_uid_parts or {} +function pac.getentparts() return ent_parts or _G.pac_ent_parts or {} end +function pac.getallparts() return all_parts or _G.pac_all_parts or {} end +function pac.getuidparts() return uid_parts or _G.pac_uid_parts or {} end + if game.SinglePlayer() or (player.GetCount() == 1 and LocalPlayer():IsSuperAdmin()) then _G.pac_local_parts = ent_parts _G.pac_all_parts = all_parts diff --git a/lua/pac3/core/client/parts/camera.lua b/lua/pac3/core/client/parts/camera.lua index 01319bf1b..fbd40609f 100644 --- a/lua/pac3/core/client/parts/camera.lua +++ b/lua/pac3/core/client/parts/camera.lua @@ -20,13 +20,47 @@ for i, ply in ipairs(player.GetAll()) do end function PART:OnShow() - local owner = self:GetRootPart():GetOwner() + local owner = self:GetPlayerOwner() if not owner:IsValid() then return end + self.inactive = false owner.pac_cameras = owner.pac_cameras or {} owner.pac_cameras[self] = self + + --the policy is that a shown camera takes priority over all others + for _, part in pairs(owner.pac_cameras) do + if part ~= self then + part.priority = false + end + end + self.priority = true + timer.Simple(0.02, function() + self.priority = true + end) end +function PART:OnHide() + local owner = self:GetPlayerOwner() + if not owner:IsValid() then return end + + owner.pac_cameras = owner.pac_cameras or {} + + --this camera cedes priority to others that may be active + for _, part in pairs(owner.pac_cameras) do + if part ~= self and not part:IsHidden() then + part.priority = true + end + end + self.inactive = true + self.priority = false + owner.pac_cameras[self] = nil +end + +--[[function PART:OnHide() + --only stop the part if explicitly set to hidden. + if not self.Hide and not self:IsHidden() then return end +end]] + function PART:CalcView(_, _, eyeang, fov, nearz, farz) local pos, ang = self:GetDrawPosition(nil, true) @@ -49,28 +83,92 @@ end BUILDER:Register() + local temp = {} +local remaining_camera = false +local remaining_camera_time_buffer = CurTime() + +local function CheckCamerasAgain(ply) + local cams = ply.pac_cameras or {} + local fpos, fang, ffov, fnearz, ffarz + + for _, part in pairs(cams) do + if (not part.inactive or part.priority) and not part:IsHidden() then + return true + end + end +end + +local function RebuildCameras(ply) + ply.pac_cameras = {} + for _,part in pairs(pac.GetLocalParts()) do + if part:IsValid() then + if part.ClassName == "camera" and (not part.inactive or not part:IsHidden() or part.priority) then + if part:GetPlayerOwner() == ply then + ply.pac_cameras[part] = part + end + end + end + end +end pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) - if not ply.pac_cameras then return end + + local fpos, fang, ffov, fnearz, ffarz + local warning_state = not ply.pac_cameras + if not warning_state then warning_state = table.IsEmpty(ply.pac_cameras) end if ply:GetViewEntity() ~= ply then return end - for _, part in pairs(ply.pac_cameras) do - if part:IsValid() then - part:CalcShowHide() - - if not part:IsHidden() then - pos, ang, fov, nearz, farz = part:CalcView(ply, pos, ang, fov, nearz, farz) - temp.origin = pos - temp.angles = ang - temp.fov = fov - temp.znear = nearz - temp.zfar = farz - temp.drawviewer = not part.DrawViewModel - return temp + remaining_camera = false + remaining_camera_time_buffer = remaining_camera_time_buffer or CurTime() + if warning_state then + RebuildCameras(ply) + else + for _, part in pairs(ply.pac_cameras) do + if part.ClassName ~= "camera" then + ply.pac_cameras[part] = nil + end + + if part.ClassName == "camera" and part:IsValid() then + if not part:IsHidden() then + remaining_camera = true + remaining_camera_time_buffer = CurTime() + 0.1 + end + + part:CalcShowHide() + if not part.inactive then + --calculate values ahead of the return, used as a fallback just in case + fpos, fang, ffov, fnearz, ffarz = part:CalcView(ply, pos, ang, fov, nearz, farz) + temp.origin = fpos + temp.angles = fang + temp.fov = ffov + temp.znear = fnearz + temp.zfar = ffarz + temp.drawviewer = false + + if not part:IsHidden() and not part.inactive and part.priority then + temp.drawviewer = not part.DrawViewModel + return temp + end + end + else + ply.pac_cameras[part] = nil end - else - ply.pac_cameras[part] = nil end end + + if remaining_camera or CurTime() < remaining_camera_time_buffer then + return temp + end + + --final fallback, just give us any valid pac camera to preserve the view! priority will be handled elsewhere + if CheckCamerasAgain(ply) then + return temp + else + return + end + + return + --only time to return to first person is if all camera parts are hidden AFTER we pass the buffer time filter + --until we make reversible first person a thing, letting some non-drawable parts think, this is the best solution I could come up with end) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 998f0b020..7ee2c4755 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -1,5 +1,7 @@ local BUILDER, PART = pac.PartTemplate("base_movable") +--ultrakill parryables: club, slash, buckshot + PART.ClassName = "damage_zone" PART.Group = 'advanced' PART.Icon = 'icon16/package.png' @@ -25,6 +27,7 @@ BUILDER:StartStorableVars() :GetSet("AffectSelf",false) :GetSet("Players",true) :GetSet("NPC",true) + :GetSet("PointEntities",true, {description = "Other source engine entities such as item_item_crate and prop_physics"}) :SetPropertyGroup("Shape and Sampling") :GetSet("Radius", 20) :GetSet("Length", 50) @@ -44,10 +47,11 @@ BUILDER:StartStorableVars() :GetSet("ExtraSteps",0) :GetSet("RadialRandomize", 1) :GetSet("PhaseRandomize", 1) - :SetPropertyGroup("Behaviour") - :GetSet("Delay", 0) + :SetPropertyGroup("Falloff") + :GetSet("DamageFalloff", false) + :GetSet("DamageFalloffPower", 1) :SetPropertyGroup("Preview Rendering") - :GetSet("NoPreview", false) + :GetSet("Preview", false) :GetSet("RenderingHook", "PostDrawOpaqueRenderables", {enums = { ["PostDraw2DSkyBox"] = "PostDraw2DSkyBox", ["PostDrawOpaqueRenderables"] = "PostDrawOpaqueRenderables", @@ -63,7 +67,7 @@ BUILDER:StartStorableVars() ["PreDrawViewModel"] = "PreDrawViewModel" }}) :SetPropertyGroup("DamageInfo") - :GetSet("Bullet", true) + :GetSet("Bullet", false, {description = "Fires a bullet on each target for the added hit decal"}) :GetSet("Damage", 0) :GetSet("DamageType", "generic", {enums = { generic = 0, --generic damage @@ -98,7 +102,6 @@ BUILDER:StartStorableVars() removenoragdoll = 4194304, --don't create a ragdoll on death slowburn = 2097152, -- - explosion = -1, -- util.BlastDamage fire = -1, -- ent:Ignite(5) -- env_entity_dissolver @@ -110,35 +113,471 @@ BUILDER:StartStorableVars() heal = -1, armor = -1, }}) + :GetSet("DoNotKill",false, {description = "Will only damage to as low as critical health"}) + :GetSet("CriticalHealth",1) + :SetPropertyGroup("HitOutcome") + :GetSetPart("HitSoundPart") + :GetSetPart("KillSoundPart") + :GetSetPart("HitMarkerPart") + :GetSet("HitMarkerLifetime", 1) + :GetSetPart("KillMarkerPart") + :GetSet("KillMarkerLifetime", 1) + :GetSet("AllowOverlappingHitSounds", false, {description = "If false, then when there are entities killed, do not play the hit sound part at the same time, since the kill sound takes priority"}) + :GetSet("AllowOverlappingHitMarkers", false, {description = "If false, then for entities killed, do not spawn the hit marker part, since the kill marker takes priority and we don't want an overlap"}) + :GetSet("RemoveDuplicateHitMarkers", true, {description = "If true, hit markers on an entity will be removed before creating a new one.\nBE WARNED. You still have a limited budget to create hit markers. It will be enforced."}) + :GetSet("RemoveNPCWeaponsOnKill",false) BUILDER:EndStorableVars() + + + + +--a budget system to prevent mass abuse of hit marker parts +function CalculateHitMarkerPrice(part) + if not part then return end + + if not part.known_hitmarker_size then part.known_hitmarker_size = 2*#util.TableToJSON(part:ToTable()) end + return part.known_hitmarker_size +end + +function HasBudget(owner, part) + if not owner.pac_dmgzone_hitmarker_budget then + owner.pac_dmgzone_hitmarker_budget = 50000 --50kB's worth of pac parts + end + + if part then --calculate based on an additional part added + --print("budget:" .. string.NiceSize(owner.pac_dmgzone_hitmarker_budget) .. ", cost: " .. string.NiceSize(CalculateHitMarkerPrice(part))) + return owner.pac_dmgzone_hitmarker_budget - CalculateHitMarkerPrice(part) > 0 + else --get result from current state + --print("budget:" .. string.NiceSize(owner.pac_dmgzone_hitmarker_budget)) + return owner.pac_dmgzone_hitmarker_budget > 0 + end +end + +function PART:LaunchAuditAndEnforceSoftBan(amount, reason) + if reason == "recursive loop" then + self.stop_until = CurTime() + 3600 + owner.stop_hit_markers_until = CurTime() + 3600 + Derma_Message("HEY! You know infinite recursive loops are super duper dangerous?") + surface.PlaySound("garrysmod/ui_return.wav") + return + end + local owner = self:GetPlayerOwner() + if owner ~= LocalPlayer() then return end + owner.stop_hit_markers_admonishment_count = owner.stop_hit_markers_admonishment_count or 1 + owner.stop_hit_markers_admonishment_message_up = false + local str_admonishment = "WARNING.\n" + str_admonishment = str_admonishment .. "One of your hit marker parts is way too big. It went ".. string.NiceSize(amount) .. " overbudget at ONCE.\n" + if self.HitBoxMode ~= "Ray" then + if self.Radius > 300 or self.Length > 300 then + str_admonishment = str_admonishment .. "Your damage zone is oversized too. Are you purposefully trying to target large numbers of targets?\n" + end + end + str_admonishment = str_admonishment .. owner.stop_hit_markers_admonishment_count .. " warnings so far\n" + if owner.stop_hit_markers_admonishment_count > 5 then + self.stop_until = CurTime() + 2 + owner.stop_hit_markers_until = CurTime() + 180 --that's rough but necessary + str_admonishment = str_admonishment .. "FIVE TIMES REPEAT OFFENDER. ENJOY YOUR BAN.\n" + end + + self:SetWarning("One of your hit marker parts is way too big. It went ".. string.NiceSize(amount) .. " overbudget at ONCE.") + timer.Simple(0.5, function() --don't open duplicate windows + if not owner.stop_hit_markers_admonishment_message_up then + surface.PlaySound("garrysmod/ui_return.wav") + Derma_Message(str_admonishment) + self:SetError(str_admonishment.."This part will be limited for 3 minutes") + owner.stop_hit_markers_admonishment_message_up = true + owner.stop_hit_markers_admonishment_count = owner.stop_hit_markers_admonishment_count + 1 + print(str_admonishment) + end + end) + +end + +function PART:ClearBudgetAdmonishmentWarning() + self:SetError() self:SetWarning() + owner = self:GetPlayerOwner() + owner.stop_hit_markers_admonishment_message_up = false + owner.stop_hit_markers_until = 0 +end + +local global_hitmarker_CSEnt_seed = 0 + +local spawn_queue = {} +local tick = 0 + +--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. + +--next solution: +--a table of 20 hit part slots to unhide instead of creating parts every time +--each player commands a table of hitmarker slots +--each slot has an entry for a hitpart which will be like a pigeon-hole for clientside hitmarker ents to share +--hitmarker removal will free up the slot +--[[ + owner.hitparts[free] = { + active = true, + specimen_part = FindOrCreateFloatingPart(ent, part_uid), + hitmarker_id = ent_id, + template_uid = part_uid + } +]] + +--add : go up until we find a free spot, register it in the table until the marker is removed and the entry is marked as inactive +--remove: go up until we find the spot with the same ent id and part uid + + +--hook.Add("Tick", "pac_spawn_hit") + +local part_setup_runtimes = 0 + +--the floating part pool is player-owned +--uid-indexed for every individual part instance +--each entry is a table +--[[ + { + active + template_uid --to identify from which part it's derived + hitmarker_id --to identify what entity it's attached to + + } +]] + +local function FindOrCreateFloatingPart(owner, part_uid) + owner.hitmarker_partpool = owner.hitmarker_partpool or {} + + for spec_uid,tbl in pairs(owner.hitmarker_partpool) do + if tbl.template_uid == part_uid then + if not tbl.active then + return part --whoowee we found an already existing part + end + end + end + + return part + +end + +local function FreeSpotInStack(owner) + owner.hitparts = owner.hitparts or {} + for i=1,20,1 do + if owner.hitparts[i] then + if not owner.hitparts[i].active then + return i + end + else + return i + end + end + return nil +end + +local function MatchInStack(owner, ent) + owner.hitparts = owner.hitparts or {} + for i=1,20,1 do + if owner.hitparts[i] then + --match: entry's template uid is the same as entity's template uid + --if there's more, still match entry's specimen ID with specimen ID + if owner.hitparts[i].pac_hitmarker_template_id == ent.pac_hitmarker_template_id and owner.hitparts[i].hitmarker_id == ent.marker_id then + return i + end + end + end + return nil +end + +local function AddHitMarkerToStack(owner, ent, part_uid, ent_id) + owner.hitparts = owner.hitparts or {} + local free = FreeSpotInStack() + if free then + owner.hitparts[free] = {active = true, specimen_part = FindOrCreateFloatingPart(ent, part_uid), hitmarker_id = ent_id, template_uid = part_uid} + end +end + +local function RemoveHitMarker(owner, ent) + owner.hitparts = owner.hitparts or {} + local match = MatchInStack(owner, ent) + if match then + owner.hitparts[match].active = false + end + SafeRemoveEntity(ent) +end + +--grabbed the function from projectile.lua +function PART:AttachToEntity(part, ent, parent_ent, marker_id) + if not part:IsValid() then return false end + + ent.pac_draw_distance = 0 + + local tbl = part:ToTable() + + local group = pac.CreatePart("group", self:GetPlayerOwner()) + group:SetShowInEditor(false) + + local part = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl)) + group:AddChild(part) + + group:SetOwner(ent) + group.SetOwner = function(s) s.Owner = ent end + part:SetHide(false) + + local id = group.Id + local owner_id = self:GetPlayerOwnerId() + if owner_id then + id = id .. owner_id + end + + ent:CallOnRemove("pac_dmgzone_hitmarker_" .. id, function() + group:Remove() + end) + group:CallRecursive("Think") + + parent_ent.pac_dmgzone_hitmarker_ents = parent_ent.pac_dmgzone_hitmarker_ents or {} + ent.part = group + ent.pac_hitmarker_template_id = part.UniqueID + parent_ent.pac_dmgzone_hitmarker_ents[marker_id] = ent + ent.marker_id = marker_id + + return true +end + +local function RecursedHitmarker(part) + if part.HitMarkerPart == part or part.KillMarkerPart == part then + return true + end + if IsValid(part.HitMarkerPart) then + for i,child in pairs(part.HitMarkerPart:GetChildrenList()) do + if child.ClassName == "damage_zone" then + if child.HitMarkerPart == part or child.KillMarkerPart == part then + return true + end + end + end + end + if IsValid(part.KillMarkerPart) then + for i,child in pairs(part.KillMarkerPart:GetChildrenList()) do + if child.ClassName == "damage_zone" then + if child.HitMarkerPart == part or child.KillMarkerPart == part then + return true + end + end + end + end + +end + function PART:OnShow() - if not self.NoPreview then + if self.Preview then self:PreviewHitbox() end - if pac.LocalPlayer ~= self:GetPlayerOwner() then return end - local tbl = {} - for key in pairs(self:GetStorableVars()) do - tbl[key] = self[key] + self.stop_until = self.stop_until or 0 + 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: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 + if self.Preview then + self:PreviewHitbox() + end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + local tbl = {} + for key in pairs(self:GetStorableVars()) do + tbl[key] = self[key] + end + net.Start("pac_request_zone_damage") + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + net.WriteTable(tbl) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() + end) + elseif (self.validTime > SysTime()) then + return + else + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + local tbl = {} + for key in pairs(self:GetStorableVars()) do + tbl[key] = self[key] + end + net.Start("pac_request_zone_damage") + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + net.WriteTable(tbl) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() end - net.Start("pac_request_zone_damage") - net.WriteVector(self:GetWorldPosition()) - net.WriteAngle(self:GetWorldAngles()) - net.WriteTable(tbl) - net.WriteEntity(self:GetPlayerOwner()) - net.SendToServer() + + net.Receive("pac_hit_results", function() + + local hit = net.ReadBool() + local kill = net.ReadBool() + local highest_dmg = net.ReadFloat() + local ents_hit = net.ReadTable() + local ents_kill = net.ReadTable() + part_setup_runtimes = 0 + + if RecursedHitmarker(self) then + self:LaunchAuditAndEnforceSoftBan(nil,"recursive loop") + end + + local pos = self:GetWorldPosition() + local owner = self:GetPlayerOwner() + + self.lag_risk = table.Count(ents_hit) > 15 + + local function ValidSound(part) + if part ~= nil then + if part.ClassName == "sound" or part.ClassName == "sound2" then + return true + end + end + return false + end + --grabbed the function from projectile.lua + --here, we spawn a static hitmarker and the max delay is 8 seconds + local function spawn(part, pos, ang, parent_ent, duration, owner) + if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker + --what if people employ a more roundabout method? CRACKDOWN! + + + if not owner.hitparts then owner.hitparts = {} end + if not owner.hitparts_floatingparts then owner.hitparts_floatingparts = {} end + + if owner.stop_hit_markers_until then + if owner.stop_hit_markers_until > CurTime() then return end + end + if self.lag_risk and math.random() > 0.5 then return end + if not self:IsValid() then return end + if not part:IsValid() then return end + + if not HasBudget(owner, part) then + local overbudget = math.abs(owner.pac_dmgzone_hitmarker_budget - CalculateHitMarkerPrice(part)) + if overbudget > 10000 then + self:LaunchAuditAndEnforceSoftBan(overbudget) + end + --print(owner, "is trying to go over budget by " .. string.NiceSize(math.abs(overbudget))) + return + else + timer.Simple(1, function() self:ClearBudgetAdmonishmentWarning() end) + local budget_add = CalculateHitMarkerPrice(part) + --print(owner, "is within budget with " .. owner.pac_dmgzone_hitmarker_budget .. " left. can add " .. string.NiceSize(budget_add)) + owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget - CalculateHitMarkerPrice(part) + end + + local start = SysTime() + local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") + if not ent:IsValid() then return end + ent:SetNoDraw(true) + ent:SetOwner(self:GetPlayerOwner(true)) + ent:SetPos(pos) + ent:SetAngles(ang) + global_hitmarker_CSEnt_seed = global_hitmarker_CSEnt_seed + 1 + + --the spawn order needs to decide whether it can or can't create an ent or part + + local flush = self.RemoveDuplicateHitMarkers + if flush then + --go through the entity and remove the clientside hitmarkers entities + if parent_ent.pac_dmgzone_hitmarker_ents then + for id,ent in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do + if IsValid(ent) then + if ent.part:IsValid() then + owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget + CalculateHitMarkerPrice(part) + ent.part:Remove() + end + RemoveHitMarker(ent, part.UniqueID, id) + end + end + end + end + if FreeSpotInStack(owner) then + if self:AttachToEntity(part, ent, parent_ent, global_hitmarker_CSEnt_seed) then + timer.Simple(math.Clamp(duration, 0, 8), function() + if ent:IsValid() then + if parent_ent.pac_dmgzone_hitmarker_ents then + for id,ent in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do + if IsValid(ent) then + if ent.part:IsValid() then + owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget + CalculateHitMarkerPrice(part) + ent.part:Remove() + end + RemoveHitMarker(ent, part.UniqueID, id) + end + end + end + + timer.Simple(0.5, function() + RemoveHitMarker(ent, part.UniqueID, id) + end) + end + end) + end + end + + local creation_delta = SysTime() - start + --print(string.NiceTime(creation_delta)) + --print(100000 * creation_delta / CalculateHitMarkerPrice(part) .. "budget score") + return creation_delta + end + + if IsValid(self.HitMarkerPart) then + --(self.HitMarkerPart, "price is", string.NiceSize(CalculateHitMarkerPrice(self.HitMarkerPart))) + end + + if hit then + self.dmgzone_hit_done = CurTime() + --try not to play both sounds at once + if ValidSound(self.HitSoundPart) then + --if can overlap, always play + if self.AllowOverlappingHitSounds then + self.HitSoundPart:PlaySound() + --if cannot overlap, only play if there's only one entity or if we didn't kill + elseif (table.Count(ents_kill) == 1) or not (kill and ValidSound(self.KillSoundPart)) then + self.HitSoundPart:PlaySound() + end + end + for ent,_ in pairs(ents_hit) do + if IsValid(ent) then + local ang = (ent:GetPos() - pos):Angle() + if ents_kill[ent] then + if self.AllowOverlappingHitMarkers then + part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) + end + else + part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) + end + end + end + end + if kill then + self.dmgzone_kill_done = CurTime() + if ValidSound(self.KillSoundPart) then + self.KillSoundPart:PlaySound() + end + + for ent,_ in pairs(ents_kill) do + if IsValid(ent) then + local ang = (ent:GetPos() - pos):Angle() + part_setup_runtimes = part_setup_runtimes + (spawn(self.KillMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.KillMarkerLifetime, owner) or 0) + end + end + end + if IsValid(self.HitMarkerPart) then + --print("runtimes were " .. string.FormattedTime( part_setup_runtimes).ms .. "ms.\n\t" .. 1000*part_setup_runtimes .. " impact.\n\t" .. table.Count(self.HitMarkerPart:GetChildrenList()) .. " children, " .. 1000*part_setup_runtimes / table.Count(self.HitMarkerPart:GetChildrenList()) .. " impact per children") + end + end) + end function PART:OnHide() - --self:BuildCylinder() - hook.Remove(self.RenderingHook, "pace_draw_hitbox") + hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) for _,v in pairs(renderhooks) do - hook.Remove(v, "pace_draw_hitbox") + hook.Remove(v, "pace_draw_hitbox"..self.UniqueID) end end function PART:OnRemove() - --self:BuildCylinder() hook.Remove(self.RenderingHook, "pace_draw_hitbox") for _,v in pairs(renderhooks) do hook.Remove(v, "pace_draw_hitbox") @@ -148,14 +587,18 @@ end local previousRenderingHook function PART:PreviewHitbox() + if previousRenderingHook ~= self.RenderingHook then for _,v in pairs(renderhooks) do - hook.Remove(v, "pace_draw_hitbox") + hook.Remove(v, "pace_draw_hitbox"..self.UniqueID) end previousRenderingHook = self.RenderingHook end - hook.Add(self.RenderingHook, "pace_draw_hitbox", function() + if not self.Preview then return end + + hook.Add(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID, function() + if not self.Preview then hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end self:GetWorldPosition() if self.HitboxMode == "Box" then local mins = Vector(-self.Radius, -self.Radius, -self.Length) @@ -322,7 +765,10 @@ function PART:PreviewHitbox() render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) end end) - timer.Simple(5, function() if self.NoPreview then hook.Remove(self.RenderingHook, "pace_draw_hitbox") end end) +end + +function PART:OnThink() + if self.Preview then self:PreviewHitbox() end end function PART:BuildCylinder(obj) @@ -390,6 +836,8 @@ function PART:BuildCone(obj) obj:BuildFromTriangles( circle_tris ) end - +function PART:Initialize() + self.validTime = SysTime() + 2 +end BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 1739befaa..9d6d08438 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1,3 +1,6 @@ +include("pac3/core/client/part_pool.lua") +--include("pac3/editor/client/parts.lua") + local FrameTime = FrameTime local CurTime = CurTime local NULL = NULL @@ -5,6 +8,7 @@ local Vector = Vector local util = util local SysTime = SysTime + local BUILDER, PART = pac.PartTemplate("base") PART.ClassName = "event" @@ -31,16 +35,112 @@ BUILDER:StartStorableVars() BUILDER:GetSet("RootOwner", true) BUILDER:GetSet("AffectChildrenOnly", false) BUILDER:GetSet("ZeroEyePitch", false) - BUILDER:GetSetPart("TargetPart") + BUILDER:GetSetPart("TargetPart", {editor_friendly = "ExternalOriginPart"}) + BUILDER:GetSetPart("DestinationPart", {editor_friendly = "TargetedPart"}) BUILDER:EndStorableVars() +local registered_command_event_series = {} + +function PART:register_command_event(str,b) + local ply = self:GetPlayerOwner() + + local event = str + local flush = b + + local num = tonumber(string.sub(event, string.find(event,"[%d]+$") or 0)) or 0 + + if string.find(event,"[%d]+$") then + event = string.gsub(event,"[%d]+$","") + end + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} + + if flush then + ply.pac_command_event_sequencebases[event] = nil + return + end + + if ply.pac_command_event_sequencebases[event] and string.find(str,"[%d]+$") then + ply.pac_command_event_sequencebases[event].max = math.max(ply.pac_command_event_sequencebases[event].max,num) + else + ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = num} + end + +end + +function PART:fix_event_operator() + --check if exists + --check class + --check current operator + --PrintTable(PART.Events[self.Event]) + if PART.Events[self.Event] then + local event_type = PART.Events[self.Event].operator_type + if event_type == "number" then + if self.Operator == "find" or self.Operator == "find simple" then + self.Operator = PART.Events[self.Event].preferred_operator --which depends, it's usually above but we'll have cases where it's best to have below, or equal + self:SetInfo("The operator was automatically changed to work with this event type, which handles numbers") + end + + elseif event_type == "string" then + if self.Operator ~= "find" and self.Operator ~= "find simple" and self.Operator ~= "equal" then + self.Operator = PART.Events[self.Event].preferred_operator --find simple + self:SetInfo("The operator was automatically changed to work with this event type, which handles strings (text)") + end + elseif event_type == "mixed" then + self:SetInfo("This event is mixed, which means it might have different behaviour with numeric operators or string operators. Some of these are that way because they're using different sources of data at once (e.g. addons' weapons can use different formats for fire modes), and we want to catch the most valid uses possible to fit what the event says") + --do nothing but still warn about it being a special complex event + elseif event_type == "none" then + --do nothing + end + end +end + +function PART:AttachEditorPopup(str) + + local info_string = str or "no information available" + local verbosity = "" + + --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + info_string = PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory" + --end + + str = info_string or str + self:SetupEditorPopup(info_string, true) +end + function PART:SetEvent(event) local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) - + self.Event = event self:SetWarning() + self:SetInfo() + + --foolproofing: fix the operator to match the event's type, and fix arguments as needed + self:fix_event_operator() + self:fix_args() + + pace.changed_event = self --a reference to make it refresh the popup label panel + pace.changed_event_time = CurTime() + + if self == pace.current_part then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit + self:GetDynamicProperties(reset) + +end + +function PART:Initialize() + if self:GetPlayerOwner() == LocalPlayer() then + timer.Simple(0.2, function() + if self.Event == "command" then + local cmd, time, hide = self:GetParsedArgumentsForObject(self.Events.command) + self:register_command_event(cmd, true) + timer.Simple(0.2, function() + self:register_command_event(cmd, false) + end) + end + end) + end + end local function get_default(typ) @@ -149,9 +249,92 @@ for k,v in pairs(_G) do end end +local grounds_enums = { + ["MAT_ANTLION"] = "65", + ["MAT_BLOODYFLESH"] = "66", + ["MAT_CONCRETE"] = "67", + ["MAT_DIRT"] = "68", + ["MAT_EGGSHELL"] = "69", + ["MAT_FLESH"] = "70", + ["MAT_GRATE"] = "71", + ["MAT_ALIENFLESH"] = "72", + ["MAT_CLIP"] = "73", + ["MAT_SNOW"] = "74", + ["MAT_PLASTIC"] = "76", + ["MAT_METAL"] = "77", + ["MAT_SAND"] = "78", + ["MAT_FOLIAGE"] = "79", + ["MAT_COMPUTER"] = "80", + ["MAT_SLOSH"] = "83", + ["MAT_TILE"] = "84", + ["MAT_GRASS"] = "85", + ["MAT_VENT"] = "86", + ["MAT_WOOD"] = "87", + ["MAT_DEFAULT"] = "88", + ["MAT_GLASS"] = "89", + ["MAT_WARPSHIELD"] = "90" +} + +local grounds_enums_reverse = { + ["65"] = "antlion", + ["66"] = "bloody flesh", + ["67"] = "concrete", + ["68"] = "dirt", + ["69"] = "egg shell", + ["70"] = "flesh", + ["71"] = "grate", + ["72"] = "alien flesh", + ["73"] = "clip", + ["74"] = "snow", + ["76"] = "plastic", + ["77"] = "metal", + ["78"] = "sand", + ["79"] = "foliage", + ["80"] = "computer", + ["83"] = "slosh", + ["84"] = "tile", + ["85"] = "grass", + ["86"] = "vent", + ["87"] = "wood", + ["88"] = "default", + ["89"] = "glass", + ["90"] = "warp shield" +} + +local animation_event_enums = { + "attack primary", + "swim", + "flinch rightleg", + "flinch leftarm", + "flinch head", + "cancel", + "attack secondary", + "flinch rightarm", + "jump", + "snap yaw", + "attack grenade", + "custom", + "cancel reload", + "reload loop", + "custom gesture sequence", + "custom sequence", + "spawn", + "doublejump", + "flinch leftleg", + "flinch chest", + "die", + "reload end", + "reload", + "custom gesture" +} + + + PART.Events = {} PART.OldEvents = { + random = { + operator_type = "number", preferred_operator = "above", arguments = {{compare = "number"}}, callback = function(self, ent, compare) return self:NumberOperator(math.random(), compare) @@ -159,6 +342,7 @@ PART.OldEvents = { }, randint = { + operator_type = "number", preferred_operator = "above", arguments = {{compare = "number"}, {min = "number"}, {max = "number"}}, callback = function(self, ent, compare, min, max) min = min or 0 @@ -169,6 +353,8 @@ PART.OldEvents = { }, random_timer = { + operator_type = "none", + tutorial_explanation = "random_timer picks a number between min and max, waits this amount of seconds,\nthen activates for the amount of seconds from holdtime.\nafter this is over, it picks a new random number and starts waiting again", arguments = {{min = "number"}, {max = "number"}, {holdtime = "number"}}, callback = function(self, ent, min, max, holdtime) @@ -203,6 +389,8 @@ PART.OldEvents = { }, timerx = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "timerx is a stopwatch that counts time since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes", arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}}, userdata = { {default = 0, timerx_property = "seconds"}, @@ -212,9 +400,7 @@ PART.OldEvents = { return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?" end, callback = function(self, ent, seconds, reset_on_hide, synced_time) - if #string.Split(self.Arguments, "@@") < 3 then - self.Arguments = self.Arguments .. "@@0" - end + local time = synced_time and CurTime() or RealTime() self.time = self.time or time @@ -224,15 +410,18 @@ PART.OldEvents = { return false end self.number = time - self.time - + return self:NumberOperator(self.number, seconds) end, }, timersys = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "like timerx, timersys is a stopwatch that counts time (it uses SysTime()) since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes", arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}}, callback = function(self, ent, seconds, reset_on_hide) + local time = SysTime() self.time = self.time or time @@ -246,6 +435,7 @@ PART.OldEvents = { }, map_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(game.GetMap(), find) @@ -253,6 +443,7 @@ PART.OldEvents = { }, fov = { + operator_type = "number", preferred_operator = "above", arguments = {{fov = "number"}}, callback = function(self, ent, fov) ent = try_viewmodel(ent) @@ -265,6 +456,7 @@ PART.OldEvents = { end, }, health_lost = { + operator_type = "number", preferred_operator = "above", arguments = {{amount = "number"}}, callback = function(self, ent, amount) @@ -306,6 +498,7 @@ PART.OldEvents = { }, holdtype = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -314,9 +507,18 @@ PART.OldEvents = { return true end end, + nice = function(self, ent, find) + local str = "holdtype ["..self.Operator.. " " .. find .. "] | " + local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL + if wep:IsValid() then + str = str .. wep:GetHoldType() + end + return str + end }, is_crouching = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.Crouching and ent:Crouching() @@ -324,6 +526,7 @@ PART.OldEvents = { }, is_typing = { + operator_type = "none", callback = function(self, ent) ent = self:GetPlayerOwner() return ent.IsTyping and ent:IsTyping() @@ -331,6 +534,7 @@ PART.OldEvents = { }, using_physgun = { + operator_type = "none", callback = function(self, ent) ent = self:GetPlayerOwner() local pac_drawphysgun_event_part = ent.pac_drawphysgun_event_part @@ -343,11 +547,42 @@ PART.OldEvents = { end, }, + is_using_entity = { + operator_type = "none", + tutorial_explanation = "For when you're picking up props, clicking buttons etc.\nAlthough not all entities will do things if you +use them, the event tries to take the class of the entity you used,\nand compares it with the one written in class", + arguments = {{class = "string"}}, + callback = function(self, ent, class) + ent = self:GetPlayerOwner() + local b = false + + if not ent:IsPlayer() then return false + elseif ent == LocalPlayer() and self.singleactivatestate then + net.Start("pac.RequestPlayerObjUsed") + net.SendToServer() + self.singleactivatestate = false + self.nextactivationrefresh = CurTime() + 0.05 + end + if ent.entity_inuse or ent.entity_inuse_classname then + if ent.entity_inuse_classname == "player_pickup" then + b = true + end + if IsValid(ent.entity_inuse) and string.find(ent.entity_inuse_classname, class, 1, true) and string.find(ent.entity_inuse:GetClass(), class, 1, true) then + b = true + end + end + + return b + end + }, + eyetrace_entity_class = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "this compares the class of the entity you point to with the one(s) written in class", arguments = {{class = "string"}}, callback = function(self, ent, find) if ent.GetEyeTrace then ent = ent:GetEyeTrace().Entity + if not IsValid(ent) then return false end if self:StringOperator(ent:GetClass(), find) then return true end @@ -356,8 +591,10 @@ PART.OldEvents = { }, owner_health = { + operator_type = "number", preferred_operator = "above", arguments = {{health = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.Health then return self:NumberOperator(ent:Health(), num) @@ -367,8 +604,10 @@ PART.OldEvents = { end, }, owner_max_health = { + operator_type = "number", preferred_operator = "above", arguments = {{health = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.GetMaxHealth then return self:NumberOperator(ent:GetMaxHealth(), num) @@ -378,6 +617,7 @@ PART.OldEvents = { end, }, owner_alive = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) if ent.Alive then @@ -387,8 +627,10 @@ PART.OldEvents = { end, }, owner_armor = { + operator_type = "number", preferred_operator = "above", arguments = {{armor = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.Armor then return self:NumberOperator(ent:Armor(), num) @@ -399,31 +641,36 @@ PART.OldEvents = { }, owner_scale_x = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, owner_scale_y = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, owner_scale_z = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, pose_parameter = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "pose parameters are values used in models for body movement and animation.\nthis event searches a pose parameter and compares its normalized (0-1 range) value with the number defined in num", arguments = {{name = "string"}, {num = "number"}}, callback = function(self, ent, name, num) ent = try_viewmodel(ent) @@ -432,18 +679,21 @@ PART.OldEvents = { }, pose_parameter_true = { - arguments = {{name = "string"}}, + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "pose parameters are values used in models for body movement and animation.\nthis event searches a pose parameter and compares its true (as opposed to normalized into the 0-1 range) value with number defined in num", + arguments = {{name = "string"}, {num = "number"}}, callback = function(self, ent, name, num) ent = try_viewmodel(ent) - if owner:IsValid() then - min,max = owner:GetPoseParameterRange(owner:LookupPoseParameter(name)) - actual_value = min + (max - min)*(owner:GetPoseParameter(name)) - return actual_value - else return 0 end + if ent:IsValid() then + min,max = ent:GetPoseParameterRange(ent:LookupPoseParameter(name)) + actual_value = min + (max - min)*(ent:GetPoseParameter(name)) + return self:NumberOperator(actual_value, num) + end end, }, speed = { + operator_type = "number", preferred_operator = "equal", arguments = {{speed = "number"}}, callback = function(self, ent, num) ent = try_viewmodel(ent) @@ -452,6 +702,8 @@ PART.OldEvents = { }, 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", arguments = {{level = "number"}}, callback = function(self, ent, num) ent = try_viewmodel(ent) @@ -460,6 +712,7 @@ PART.OldEvents = { }, is_on_fire = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent:IsOnFire() @@ -467,17 +720,23 @@ PART.OldEvents = { }, client_spawned = { + operator_type = "number", preferred_operator = "below", + tutorial_explanation = "client_spawned supposedly activates for some time after you spawn", + arguments = {{time = "number"}}, callback = function(self, ent, time) time = time or 0.1 ent = try_viewmodel(ent) - if ent.pac_playerspawn and ent.pac_playerspawn + time > pac.RealTime then - return true + if ent.pac_playerspawn then + return self:NumberOperator(pac.RealTime, ent.pac_playerspawn + time) end + return false end, }, is_client = { + operator_type = "none", + tutorial_explanation = "is_client makes something visible only for you, or others (uninverted)", callback = function(self, ent) ent = try_viewmodel(ent) return self:GetPlayerOwner() == ent @@ -485,6 +744,7 @@ PART.OldEvents = { }, is_flashlight_on = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.FlashlightIsOn and ent:FlashlightIsOn() @@ -492,6 +752,7 @@ PART.OldEvents = { }, collide = { + operator_type = "none", callback = function(self, ent) ent.pac_event_collide_callback = ent.pac_event_collide_callback or ent:AddCallback("PhysicsCollide", function(ent, data) ent.pac_event_collision_data = data @@ -508,15 +769,15 @@ PART.OldEvents = { }, ranger = { + operator_type = "number", preferred_operator = "below", + tutorial_explanation = "ranger looks in a line to see if something is in front (red arrow) of its host's (parent) model;\ndetected things could be found before(below) or beyond(above) the distance defined in compare;\nthe event will only look as far as the distance defined in distance", arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}}, userdata = { {default = 15, editor_panel = "ranger", ranger_property = "distance"}, {default = 5, editor_panel = "ranger", ranger_property = "compare"} }, callback = function(self, ent, distance, compare, npcs_and_players_only) - if #string.Split(self.Arguments, "@@") < 3 then - self.Arguments = self.Arguments .. "@@0" - end + local parent = self:GetParentEx() if parent:IsValid() and parent.GetWorldPosition then @@ -533,7 +794,7 @@ PART.OldEvents = { if npcs_and_players_only and (not res.Entity:IsPlayer() and not res.Entity:IsNPC()) then return false end - + return self:NumberOperator(res.Fraction * distance, compare) else local classname = parent:GetNiceName() @@ -541,6 +802,100 @@ PART.OldEvents = { self:SetWarning(("ranger doesn't work on [%s] %s"):format(classname, classname ~= name and "(" .. name .. ")" or "")) end end, + nice = function(self, ent, distance, compare, npcs_and_players_only) + local str = "ranger: [" .. self.Operator .. " " .. compare .. "]" + return str + end + }, + + ground_surface = { + operator_type = "mixed", + tutorial_explanation = "ground_surface checks what ground you're standing on, and activates if it matches among the IDs written in surfaces.\nMatch multiple with ;", + arguments = {{exclude_noclip = "boolean"}, {surfaces = "string"}}, + userdata = {{}, {enums = function() + --grounds_enums = + return + { + ["MAT_ANTLION"] = "65", + ["MAT_BLOODYFLESH"] = "66", + ["MAT_CONCRETE"] = "67", + ["MAT_DIRT"] = "68", + ["MAT_EGGSHELL"] = "69", + ["MAT_FLESH"] = "70", + ["MAT_GRATE"] = "71", + ["MAT_ALIENFLESH"] = "72", + ["MAT_CLIP"] = "73", + ["MAT_SNOW"] = "74", + ["MAT_PLASTIC"] = "76", + ["MAT_METAL"] = "77", + ["MAT_SAND"] = "78", + ["MAT_FOLIAGE"] = "79", + ["MAT_COMPUTER"] = "80", + ["MAT_SLOSH"] = "83", + ["MAT_TILE"] = "84", + ["MAT_GRASS"] = "85", + ["MAT_VENT"] = "86", + ["MAT_WOOD"] = "87", + ["MAT_DEFAULT"] = "88", + ["MAT_GLASS"] = "89", + ["MAT_WARPSHIELD"] = "90" + } end}}, + nice = function(self, ent, exclude_noclip, surfaces) + local grounds_enums_reverse = { + ["65"] = "antlion", + ["66"] = "bloody flesh", + ["67"] = "concrete", + ["68"] = "dirt", + ["69"] = "egg shell", + ["70"] = "flesh", + ["71"] = "grate", + ["72"] = "alien flesh", + ["73"] = "clip", + ["74"] = "snow", + ["76"] = "plastic", + ["77"] = "metal", + ["78"] = "sand", + ["79"] = "foliage", + ["80"] = "computer", + ["83"] = "slosh", + ["84"] = "tile", + ["85"] = "grass", + ["86"] = "vent", + ["87"] = "wood", + ["88"] = "default", + ["89"] = "glass", + ["90"] = "warp shield" + } + surfaces = surfaces or "" + local str = "ground surface: " + for i,v in ipairs(string.Split(surfaces,";")) do + local element = grounds_enums_reverse[v] or "" + str = str .. element + if i ~= #string.Split(surfaces,";") then + str = str .. ", " + end + end + return str + end, + callback = function(self, ent, exclude_noclip, surfaces, down) + surfaces = surfaces or "" + if exclude_noclip and ent:GetMoveType() == MOVETYPE_NOCLIP then return false end + local trace = util.TraceLine( { + start = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, 10), + endpos = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, -30 ), + filter = function(ent) + if ent == self:GetRootPart():GetOwner() or ent == self:GetPlayerOwner() then return false end + end + }) + local found = false + if trace.Hit then + local surfs = string.Split(surfaces,";") + for _,surf in pairs(surfs) do + if surf == tostring(trace.MatType) then found = true end + end + end + return found + end }, is_on_ground = { @@ -571,11 +926,15 @@ PART.OldEvents = { return false end, }, - + + --this one uses util.TraceHull is_touching = { - arguments = {{extra_radius = "number"}}, - userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius"}}, - callback = function(self, ent, extra_radius) + operator_type = "none", + tutorial_explanation = "is_touching checks in a box (util.TraceHull) around the host model to see if there's something inside it.\nusually it's the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner,to override the old root owner setting,\nin case of issues when stacking this event inside other events", + arguments = {{extra_radius = "number"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = 0}}, + callback = function(self, ent, extra_radius, nearest_model) + if nearest_model then ent = self:GetOwner() end extra_radius = extra_radius or 0 local radius = ent:BoundingRadius() @@ -592,31 +951,170 @@ PART.OldEvents = { mins = mins * radius maxs = maxs * radius + local tr = util.TraceHull( { start = startpos, endpos = startpos, maxs = maxs, mins = mins, - filter = {self:GetRootPart():GetOwner(),ent} - } ) + filter = {ent, self:GetRootPart():GetOwner()} + }) + return tr.Hit end, + nice = function(self, ent, extra_radius, nearest_model) + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. "]" + return str + end, + }, + --this one uses ents.FindInBox + is_touching_filter = { + operator_type = "none", + tutorial_explanation = "is_touching_filter checks in a box (ents.FindInBox) around the host model to see if there's something inside it, but you can selectively exclude living things (NPCs or players) from being detected.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + arguments = {{extra_radius = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = false}, {default = false}, {default = false}}, + callback = function(self, ent, extra_radius, no_npc, no_players, nearest_model) + if nearest_model then ent = self:GetOwner() end + extra_radius = extra_radius or 0 + no_npc = no_npc or false + no_players = no_players or false + nearest_model = nearest_model or false + + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.max(radius + extra_radius + 1, 1) + + local mins = Vector(-1,-1,-1) + local maxs = Vector(1,1,1) + local startpos = ent:WorldSpaceCenter() + mins = startpos + mins * radius + maxs = startpos + maxs * radius + + local b = false + local ents_hits = ents.FindInBox(mins, maxs) + for _,ent2 in pairs(ents_hits) do + if (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) and + not ( (no_npc and ent2:IsNPC()) or (no_players and ent2:IsPlayer()) ) + then b = true end + end + + return b + end, + nice = function(self, ent, extra_radius, no_npc, no_players, nearest_model) + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. "]" + if no_npc or no_players then str = str .. " | " end + if no_npc then str = str .. "no_npc " end + if no_players then str = str .. "no_players " end + return str + end }, + --this one uses ents.FindInBox + is_touching_life = { + operator_type = "none", + tutorial_explanation = "is_touching_life checks in a stretchable box (ents.FindInBox) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}, {default = false}, {default = false}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) + + if nearest_model then ent = self:GetOwner() end + extra_radius = extra_radius or 0 + no_npc = no_npc or false + no_players = no_players or false + x_stretch = x_stretch or 1 + y_stretch = y_stretch or 1 + z_stretch = z_stretch or 1 + nearest_model = nearest_model or false + + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.max(radius + extra_radius + 1, 1) + + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + local startpos = ent:WorldSpaceCenter() + mins = startpos + mins * radius + maxs = startpos + maxs * radius + + local ents_hits = ents.FindInBox(mins, maxs) + local b = false + for _,ent2 in pairs(ents_hits) do + if IsValid(ent2) and (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) + + then + b = true + if ent2:IsNPC() and no_npc then + b = false + elseif ent2:IsPlayer() and no_players then + b = false + end + if b then return b end + end + end + + return b + end, + nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) + + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]" + if no_npc or no_players then str = str .. " | " end + if no_npc then str = str .. "no_npc " end + if no_players then str = str .. "no_players " end + return str + end, + }, + --this one uses util.TraceHull is_touching_scalable = { - arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}}, - userdata = {{editor_panel = "is_touching", default = 15}, {x = "x_stretch"}, {y = "y_stretch"}, {z = "z_stretch"}}, - callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch) + operator_type = "none", + tutorial_explanation = "is_touching_life checks in a stretchable box (util.TraceHull) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) + if nearest_model then ent = self:GetOwner() end extra_radius = extra_radius or 15 x_stretch = x_stretch or 1 y_stretch = y_stretch or 1 z_stretch = z_stretch or 1 - if #string.Split(self.Arguments, "@@") == 1 then - self.Arguments = self.Arguments .. "@@1" - end - if #string.Split(self.Arguments, "@@") < 4 then - self.Arguments = self.Arguments .. "@@1" - end + nearest_model = nearest_model or false + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) local maxs = Vector(x_stretch,y_stretch,z_stretch) local startpos = ent:WorldSpaceCenter() @@ -634,15 +1132,32 @@ PART.OldEvents = { } ) return tr.Hit end, + nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]" + return str + end, }, is_explicit = { + operator_type = "none", + tutorial_explanation = "is_explicit activates for viewers who want to hide explicit content with pac_hide_disturbing.\nyou can make special censoring effects for them, for example", + callback = function(self, ent) return GetConVar("pac_hide_disturbing"):GetBool() end }, is_in_noclip = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent:GetMoveType() == MOVETYPE_NOCLIP and (not ent.GetVehicle or not ent:GetVehicle():IsValid()) @@ -650,6 +1165,7 @@ PART.OldEvents = { }, is_voice_chatting = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.IsSpeaking and ent:IsSpeaking() @@ -657,6 +1173,7 @@ PART.OldEvents = { }, ammo = { + operator_type = "number", preferred_operator = "above", arguments = {{primary = "boolean"}, {amount = "number"}}, userdata = {{editor_onchange = function(part, num) return math.Round(num) end}}, callback = function(self, ent, primary, amount) @@ -669,6 +1186,7 @@ PART.OldEvents = { end, }, total_ammo = { + operator_type = "number", preferred_operator = "above", arguments = {{ammo_id = "string"}, {amount = "number"}}, callback = function(self, ent, ammo_id, amount) if ent.GetAmmoCount then @@ -687,6 +1205,7 @@ PART.OldEvents = { }, clipsize = { + operator_type = "number", preferred_operator = "above", arguments = {{primary = "boolean"}, {amount = "number"}}, callback = function(self, ent, primary, amount) ent = try_viewmodel(ent) @@ -699,6 +1218,7 @@ PART.OldEvents = { }, vehicle_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -711,6 +1231,7 @@ PART.OldEvents = { }, vehicle_model = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -723,6 +1244,7 @@ PART.OldEvents = { }, driver_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = ent.GetDriver and ent:GetDriver() or NULL @@ -734,6 +1256,7 @@ PART.OldEvents = { }, entity_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(ent:GetClass(), find) @@ -741,6 +1264,7 @@ PART.OldEvents = { }, weapon_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}, {hide = "boolean"}}, callback = function(self, ent, find, hide) ent = try_viewmodel(ent) @@ -765,6 +1289,7 @@ PART.OldEvents = { }, has_weapon = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -781,6 +1306,7 @@ PART.OldEvents = { }, model_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(ent:GetModel(), find) @@ -788,9 +1314,14 @@ PART.OldEvents = { }, sequence_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, - nice = function(self, ent) - return self.sequence_name or "invalid sequence" + nice = function(self, ent, find) + local anim = find + if find == "" then anim = "" end + local str = self.Event .. " ["..self.Operator.. " " .. anim .. "] | " + local seq = self.sequence_name or "invalid sequence" + return str .. seq end, callback = function(self, ent, find) ent = get_owner(self) @@ -802,6 +1333,7 @@ PART.OldEvents = { }, timer = { + operator_type = "none", arguments = {{interval = "number"}, {offset = "number"}}, callback = function(self, ent, interval, offset) interval = interval or 1 @@ -817,21 +1349,42 @@ PART.OldEvents = { }, animation_event = { - arguments = {{find = "string"}, {time = "number"}}, - nice = function(self) - return self.anim_name or "" + operator_type = "string", preferred_operator = "find simple", + arguments = {{find = "string"}, {time = "number"}, {try_stop_gesture = "boolean"}}, + userdata = {{default = "attack primary", enums = function() + local tbl = {} + for i,v in pairs(animation_event_enums) do + tbl[i] = v + end + return tbl + end}, {default = 0.5}}, + nice = function(self, ent, find, time) + find = find or "" + time = time or 0 + local anim = self.anim_name or "" + local str = self.Event .. " ["..self.Operator.. " \"" .. find .. "\" : " .. time .. " seconds] | " + return str .. anim end, - callback = function(self, ent, find, time) + callback = function(self, ent, find, time, try_stop_gesture) time = time or 0.1 ent = get_owner(self) local data = ent.pac_anim_event local b = false - + if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then data.reset = false b = true + if try_stop_gesture then + if string.find(find, "attack grenade") then + ent:AnimResetGestureSlot( GESTURE_SLOT_GRENADE ) + elseif string.find(find, "attack") or string.find(find, "reload") then + ent:AnimResetGestureSlot( GESTURE_SLOT_ATTACK_AND_RELOAD ) + elseif string.find(find, "flinch") then + ent:AnimResetGestureSlot( GESTURE_SLOT_FLINCH ) + end + end end if b then @@ -845,6 +1398,8 @@ PART.OldEvents = { }, fire_bullets = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "fire_bullets supposedly checks what types of bullets you're firing", arguments = {{find_ammo = "string"}, {time = "number"}}, callback = function(self, ent, find, time) time = time or 0.1 @@ -854,7 +1409,7 @@ PART.OldEvents = { local data = ent.pac_fire_bullets local b = false - if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then + if data and (self:StringOperator(data.name, find_ammo) and (time == 0 or data.time + time > pac.RealTime)) then data.reset = false b = true end @@ -864,6 +1419,7 @@ PART.OldEvents = { }, emit_sound = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find_sound = "string"}, {time = "number"}, {mute = "boolean"}}, callback = function(self, ent, find, time, mute) time = time or 0.1 @@ -886,23 +1442,22 @@ PART.OldEvents = { }, command = { + operator_type = "string", preferred_operator = "equal", + tutorial_explanation = "the command event reads your pac_event states.\nthe pac_event command can turn on (1), off (0) or toggle (2) a state that has a name.\nfor example, \"pac_event myhat 2\" can be used with a myhat command event to put the hat on or off\n\nwith this event, you read the states that contain this find name\n(equal being an exact match; find and find simple allowing to detect from different states having a part of the name)\n\nthe final result is to activate if:\n\tA) there's one active, or \n\tB) there's one recently turned off not too long ago", arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}}, userdata = { {default = "change_me", editor_friendly = "CommandName"}, - {default = 0.1, editor_friendly = "EventDuration"}, + {default = 0, editor_friendly = "EventDuration"}, {default = false, group = "event wheel", editor_friendly = "HideInEventWheel"} }, nice = function(self, ent, find, time) find = find or "?" time = time or "?" - return "command: " .. find .. " | " .. "duration: " .. time + return "command: [" .. self.Operator .. " " .. find .."] | " .. "duration: " .. time end, callback = function(self, ent, find, time) - if #string.Split(self.Arguments, "@@") < 3 then - self.Arguments = self.Arguments .. "@@0" - end - time = time or 0.1 + time = time or 0 local ply = self:GetPlayerOwner() @@ -925,6 +1480,8 @@ PART.OldEvents = { }, say = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "say looks at the chat to find if a certain thing has been said some time ago", arguments = {{find = "string"}, {time = "number"}, {all_players = "boolean"}}, callback = function(self, ent, find, time, all_players) time = time or 0.1 @@ -954,6 +1511,7 @@ PART.OldEvents = { -- outfit owner owner_velocity_length = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -967,6 +1525,7 @@ PART.OldEvents = { end, }, owner_velocity_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -979,6 +1538,7 @@ PART.OldEvents = { end, }, owner_velocity_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -991,6 +1551,7 @@ PART.OldEvents = { end, }, owner_velocity_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -1003,6 +1564,7 @@ PART.OldEvents = { end, }, owner_velocity_world_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -1015,6 +1577,7 @@ PART.OldEvents = { end, }, owner_velocity_world_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -1027,6 +1590,7 @@ PART.OldEvents = { end, }, owner_velocity_world_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -1041,6 +1605,7 @@ PART.OldEvents = { -- parent part parent_velocity_length = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1057,6 +1622,7 @@ PART.OldEvents = { end, }, parent_velocity_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1073,6 +1639,7 @@ PART.OldEvents = { end, }, parent_velocity_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1089,6 +1656,7 @@ PART.OldEvents = { end, }, parent_velocity_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1106,6 +1674,7 @@ PART.OldEvents = { }, parent_scale_x = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1122,6 +1691,7 @@ PART.OldEvents = { end, }, parent_scale_y = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1138,6 +1708,7 @@ PART.OldEvents = { end, }, parent_scale_z = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1155,6 +1726,7 @@ PART.OldEvents = { }, gravitygun_punt = { + operator_type = "number", preferred_operator = "above", arguments = {{time = "number"}}, callback = function(self, ent, time) time = time or 0.1 @@ -1163,13 +1735,14 @@ PART.OldEvents = { local punted = ent.pac_gravgun_punt - if punted and punted + time > pac.RealTime then - return true + if punted then + return self:NumberOperator(pac.RealTime, punted + time) end end, }, movetype = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) local mt = ent:GetMoveType() @@ -1180,6 +1753,8 @@ PART.OldEvents = { }, dot_forward = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles", arguments = {{normal = "number"}}, callback = function(self, ent, normal) @@ -1196,6 +1771,9 @@ PART.OldEvents = { }, dot_right = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) @@ -1212,6 +1790,9 @@ PART.OldEvents = { }, flat_dot_forward = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) local owner = self:GetRootPart():GetOwner() @@ -1231,6 +1812,9 @@ PART.OldEvents = { }, flat_dot_right = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) local owner = self:GetRootPart():GetOwner() @@ -1247,72 +1831,785 @@ PART.OldEvents = { return 0 end - } -} + }, -do - local enums = {} - local enums2 = {} - for key, val in pairs(_G) do - if isstring(key) and isnumber(val) then - if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then - enums[val] = key:sub(5):lower() - enums2[enums[val]] = val - elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then - enums[val] = key:lower() - enums2[enums[val]] = val - end + is_sitting = { + operator_type = "none", + callback = function(self, ent) + if not ent:GetVehicle() then return false end + if ent.GetSitting then return (IsValid(ent:GetVehicle()) or ent:GetSitting()) and ent:GetVehicle():GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" end --sit anywhere script + return IsValid(ent:GetVehicle()) and ent:GetVehicle():GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" --no prison pod! end - end - - pac.key_enums = enums - - --TODO: Rate limit!!! - net.Receive("pac.BroadcastPlayerButton", function() - local ply = net.ReadEntity() - - if not ply:IsValid() then return end - - if ply == pac.LocalPlayer and (pace and pace.IsFocused() or gui.IsConsoleVisible()) then return end - - local key = net.ReadUInt(8) - local down = net.ReadBool() - - key = pac.key_enums[key] or key - - ply.pac_buttons = ply.pac_buttons or {} - ply.pac_buttons[key] = down - end) + }, - PART.OldEvents.button = { - arguments = {{button = "string"}}, - userdata = {{enums = function() - return enums - end}}, - nice = function(self, ent, button) - local ply = self:GetPlayerOwner() + is_driving = { + operator_type = "none", + callback = function(self, ent) + if not ent:IsPlayer() then return false end + local vehicle = ent:GetVehicle() + + if IsValid(vehicle) then --vehicle entity exists + if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent + --print(vehicle:GetParent().PassengerSeats) + + if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys + return ent:IsDrivingSimfphys() and ent:GetVehicle() == ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat + elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too + --print(vehicle:GetParent().BaseClass.ClassName, #vehicle:GetParent().Seats) + --PrintTable(vehicle:GetParent().Seats[1]) + return vehicle == vehicle.wac_seatswitcher.seats[1] --first seat + end + elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we don't want bare seats or prisoner pod + if vehicle.HandleAnimation == true and not isfunction(vehicle.HandleAnimation) and vehicle:GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" then --exclude prisoner pod and narrow down to SCars + return true + end + return false + else --assume that most other classes than prop_vehicle_prisoner_pod are drivable vehicles + return true + end + end + return false + end + }, - local active = {} - if ply.pac_buttons then - for k,v in pairs(ply.pac_buttons) do - if v then - table.insert(active, "\"" .. tostring(k) .. "\"") + is_passenger = { + operator_type = "none", + callback = function(self, ent) + if not ent:IsPlayer() then return false end + local vehicle = ent:GetVehicle() + + if IsValid(vehicle) then --vehicle entity exists + if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent + if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys + return ent:IsDrivingSimfphys() and ent:GetVehicle() ~= ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat + elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too + return vehicle ~= vehicle.wac_seatswitcher.seats[1] --first seat end + elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we can count bare seats and prisoner pods as passengers + return true + else --assume that most other classes than prop_vehicle_prisoner_pod are drivable vehicles, but they're also probably single seaters so... + return false end end - active = table.concat(active, " or ") + return false + end + }, - if active == "" then - active = "-" + weapon_iron_sight = { + operator_type = "none", + callback = function(self, ent) + if not IsValid(ent) or ent:Health() < 1 then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + if wep.IsFAS2Weapon then + return wep.dt.Status == FAS_STAT_ADS end - return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")" - end, - callback = function(self, ent, button) - local ply = self:GetPlayerOwner() + if wep.GetIronSights then return wep:GetIronSights() end + if wep.Sighted then return wep:GetActiveSights() end --arccw + return false + end + }, + + weapon_firemode = { + operator_type = "mixed", + arguments = {{name_or_id = "string"}}, + callback = function(self, ent, name_or_id) + name_or_id = string.lower(name_or_id) + if not IsValid(ent) or ent:Health() < 1 then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + + if wep.ArcCW then + if wep.Firemodes[wep:GetFireMode()] then --some use a Firemodes table + if wep.Firemodes[wep:GetFireMode()].PrintName then + return + self:StringOperator(name_or_id, wep.Firemodes[wep:GetFireMode()].PrintName) + or self:StringOperator(name_or_id, wep:GetFiremodeName()) + or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end + elseif wep.Primary then + if wep.Primary.Automatic ~= nil then + if wep.Primary.Automatic == true then + return name_or_id == "automatic" or name_or_id == "auto" + else + return name_or_id == "semi-automatic" or name_or_id == "semi-auto" or name_or_id == "single" + end + end + self:StringOperator(name_or_id, wep:GetFiremodeName()) + end + return self:StringOperator(name_or_id, wep:GetFiremodeName()) or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end + + if wep.IsFAS2Weapon then + if not wep.FireMode then return name_or_id == "" or name_or_id == "nil" or name_or_id == "null" or name_or_id == "none" + else return self:StringOperator(wep.FireMode, name_or_id) end + end + + if wep.GetFireModeName then --TFA base is an arbitrary number and name (language-specific) + return self:StringOperator(string.lower(wep:GetFireModeName()), name_or_id) or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end + + if wep.Primary then + if wep.Primary.Automatic ~= nil then --M9K is a boolean + if wep.Primary.Automatic == true then + return name_or_id == "automatic" or name_or_id == "auto" or name_or_id == "1" + else + return name_or_id == "semi-automatic" or name_or_id == "semi-auto" or name_or_id == "single" or name_or_id == "0" + end + end + end + + + return false + end, + nice = function(self, ent, name_or_id) + if not IsValid(ent) then return end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return "invalid weapon" end + wep = ent:GetActiveWeapon() + local str = "weapon_firemode ["..self.Operator.. " " .. name_or_id .. "] | " + + if wep.IsFAS2Weapon then + + if wep.FireMode then + str = str .. wep.FireMode .. " | options : " + for i,v in ipairs(wep.FireModes) do + str = str .. "(" .. v .. " = " .. i.. "), " + end + else str = str .. "" end + return str + end + + if wep.ArcCW then + if not IsValid(wep) then return "no active weapon" end + if wep.GetFiremodeName then + str = str .. wep:GetFiremodeName() .. " | options : " + for i,v in ipairs(wep.Firemodes) do + if v.PrintName then + str = str .. "(" .. i .. " = " .. v.PrintName .. "), " + end + end + if wep.Primary.Automatic then + str = str .. "(" .. "Automatic" .. "), " + end + end + return str + end + + if wep.GetFireModeName then --TFA base or arccw + if not IsValid(wep) then return "no active weapon" end + if wep.GetFireModeName then + str = str .. wep:GetFireModeName() .. " | options : " + for i,v in ipairs(wep:GetStatL("FireModes")) do + str = str .. "(" .. v .. " = " .. i.. "), " + end + end + return str + end + + if wep.Primary then --M9K + if wep.Primary.Automatic ~= nil then + if wep.Primary.Automatic then + str = str .. "automatic" + else + str = str .. "semi-auto" + end + end + str = str .. " | options : 1/auto/automatic, 0/single/semi-auto/semi-automatic" + return str + end + + + return str + end + }, + + weapon_safety = { + operator_type = "none", + callback = function(self, ent) + if not ent or not IsValid(ent) then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + if wep.IsSafety then + return wep:IsSafety() + end + if wep.ArcCW then + return wep:GetFiremodeName() == "Safety" + end + + return false + end + }, +--@note take damage is like health_lost but 400% better + + take_damage = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {damage = "number"}, {attackers = "string"}, {inflictors = "string"}, {damage_type = "number"}}, + userdata = {{default = 1}, {default = 10}, + {default = "any", + enums = function() + local players = {} + for i,v in ipairs(player.GetAll()) do + players[v:Nick()] = v:SteamID() + end + return players + end + }, {default = "any"}, + {default = -1, + enums = function() + local damage_enums = {} + for k,v in pairs(_G) do + if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then + damage_enums[k] = tonumber(v) + end + end + return damage_enums + end + }}, + callback = function(self, ent, time, damage, attackers, inflictors, damage_type) + local time = time or 0 + local ent = self:GetRootPart():GetOwner() + if not IsValid(ent) then return false end + local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" + local found_attacker = attackers == "" or attackers == "anyone" or attackers == "any" or attackers == "all" + local unspec_inflictor = found_inflictor + local unspec_attacker = found_attacker + local unspec_dmg = damage_type == -1 + + local lastest_attacker = nil + local latest_hit_time = 0 + + if not ent.pac_damage_attributions then + ent.pac_damage_attributions = {} + return false + elseif (table.Count(ent.pac_damage_attributions) < 1) then + return false + end + + ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 + + if CurTime() < ent.pac_damage_attributions.IngoingGraceTime + time then return true end + if not ent.pac_damage_attributions then return false end --the entity is a damage virgin, it's not hurt + + + for attacker,tbl in pairs(ent.pac_damage_attributions) do + if IsValid(attacker) and IsValid(tbl.inflictor) then + local found_inflictor_class = false + for i,v in ipairs(string.Split(inflictors, ";")) do + if v == tbl.inflictor:GetClass() then + found_inflictor = true + end + end + for i,v in ipairs(string.Split(attackers, ";")) do + if v == tbl.attacker:GetClass() then + found_attacker = true + elseif tbl.attacker:IsPlayer() then + if tbl.attacker:SteamID() == v or tbl.attacker:Nick() == v then + found_attacker = true + end + --print("tested attacker ", tbl.attacker:GetClass(), "it aint it.", v) + end + end + if tbl.hit_time > latest_hit_time and (bit.bor(tbl.dmg_type, damage_type) or damage_type == -1) then + latest_hit_time = tbl.hit_time + lastest_attacker = attacker + end + else --lost entity! i.e. grenades get removed so we can't use direct entity reference anymore + --so we give it a grace period for next time + if not type(attacker) == "table" then --exclude the grace fields... + ent.pac_damage_attributions.IngoingGraceTime = CurTime() + ent.pac_damage_attributions[attacker] = nil + end + end + + end + + --print(ent.pac_damage_attributions.IngoingGraceTime) + ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 + + --print("CurTime:"..CurTime(), "Grace:" .. ent.pac_damage_attributions.IngoingGraceTime) + + if found_attacker and found_inflictor then + if ent.pac_damage_attributions[lastest_attacker] then + if CurTime() < ent.pac_damage_attributions[lastest_attacker].hit_time + time then + return self:NumberOperator(ent.pac_damage_attributions[lastest_attacker].dmg_amount, damage) + end + elseif CurTime() < ent.pac_damage_attributions.IngoingGraceTime + time then + return true + end + + end + + if unspec_attacker and unspec_inflictor then + + if ent.pac_damage_attributions.latest then + + if (CurTime() < ent.pac_damage_attributions.latest.hit_time + time) then + return self:NumberOperator(ent.pac_damage_attributions.latest.dmg_amount, damage) + end + end + end + + return false + end, + nice = function(self, ent, time, damage, attackers, inflictors, damage_type) + time = time or 0 + damage = damage or 0 + attackers = attackers or "" + inflictors = inflictors or "" + damage_type = damage_type or -1 + local str = "take_damage : [" .. self.Operator .. " " .. damage .. "]" + if attackers == "" or attackers == "any" or attackers == "anyone" or attackers == "all" then + str = str .. " | from any attacker " + else + str = str .. " | from attackers: " + for i,v in ipairs(string.Split(attackers, ";")) do + str = str .. v .. " " + end + end + for i,v in ipairs(string.Split(attackers, ";")) do + str = str .. v .. " " + end + if inflictors == "" or inflictors == "any" or inflictors == "all" then + str = str .. " | from any inflictor" + else + str = str .. " | from inflictors: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + str = str .. " | with damage types : " .. damage_type + return str + end + }, + + inflicting_damage = { + operator_type = "mixed", preferred_operator = "above", + tutorial_explanation = "", + arguments = {{time = "number"}, {damage = "number"}, {targets = "string"}, {inflictors = "string"}, {damage_type = "number"}}, + userdata = {{default = 1}, {default = 10}, + {default = "any", + enums = function() + local players = {} + for i,v in ipairs(player.GetAll()) do + players[v:Nick()] = v:SteamID() + end + return players + end + }, {default = "any"}, + {default = -1, + enums = function() + local damage_enums = {} + for k,v in pairs(_G) do + if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then + damage_enums[k] = tonumber(v) + end + end + return damage_enums + end + }}, + callback = function(self, ent, time, damage, targets, inflictors, damage_type) + local time = time or 0 + local ent = self:GetRootPart():GetOwner() + if not IsValid(ent) then return false end + local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" + local found_target = targets == "" or targets == "anyone" or targets == "any" or targets == "all" + local unspec_dmg = damage_type == -1 + + ent.pac_damage_attributions = ent.pac_damage_attributions or {} + ent.pac_damage_attributions.OutgoingGraceTime = ent.pac_damage_attributions.OutgoingGraceTime or 0 + ent.pac_damage_attributions.OutgoingGraceTimeDMG = ent.pac_damage_attributions.OutgoingGraceTimeDMG or 0 + local latest_hit_time = ent.pac_damage_attributions.OutgoingGraceTime or 0 + + for _,target in pairs(ents.GetAll()) do --check ents we could hurt + if target.pac_damage_attributions then --skip the virgins + if target.pac_damage_attributions[ent] then --we're in. + + tbl = target.pac_damage_attributions[ent] + if not found_inflictor then + for i,v in ipairs(string.Split(inflictors, ";")) do + if v == tbl.inflictor:GetClass() then + found_inflictor = true + end + end + end + if not found_target then + for i,v in ipairs(string.Split(targets, ";")) do + if v == target:GetClass() then + found_target = true + elseif target:IsPlayer() then + if target:SteamID() == v or target:Nick() == v then + found_target = true + end + end + end + end + if tbl.hit_time > latest_hit_time and (bit.bor(tbl.dmg_type, damage_type) or damage_type == -1) then + latest_hit_time = CurTime() + ent.pac_damage_attributions.OutgoingGraceTime = CurTime() + ent.pac_damage_attributions.OutgoingGraceTimeDMG = tbl.dmg_amount + end + end + end + end + --WHAT ABOUT KILLS?? DONT WORRY ABOUT IT (TM) + --print("CurTime:" .. CurTime(), "out grace"..ent.pac_damage_attributions.OutgoingGraceTime) + if found_target and found_inflictor then + if CurTime() < ent.pac_damage_attributions.OutgoingGraceTime + time then + return self:NumberOperator(ent.pac_damage_attributions.OutgoingGraceTimeDMG, damage) + end + end + + return false + end, + nice = function(self, ent, time, damage, targets, inflictors, damage_type) + time = time or 0 + damage = damage or 0 + attackers = attackers or "" + targets = targets or "" + damage_type = damage_type or -1 + local str = "inflicting_damage : [" .. self.Operator .. " " .. damage .. "]" + if targets == "" or targets == "anyone" or targets == "any" or targets == "all" then + str = str .. " | to any target" + else + str = str .. " | to targets: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + if inflictors == "" or inflictors == "any" or inflictors == "all" then + str = str .. " | from any inflictor " + else + str = str .. " | from inflictors: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + str = str .. " | with damage types : " .. damage_type + return str + end + }, + + damage_zone_hit = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {damage = "number"}, {uid = "string"}}, + userdata = {{default = 1}, {default = 0}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "damage_zone" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, time, damage, uid) + uid = uid or "" + local valid_uid, err = xpcall(pac.GetPartFromUniqueID, uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "damage_zone" then + if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then + if part.dmgzone_hit_done + time > CurTime() then + return true + end + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(uid) + if part.ClassName == "damage_zone" then + if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then + if part.dmgzone_hit_done + time > CurTime() then + return true + end + end + else + self:SetError("You set a UID that's not a damage zone!") + end + end + return false + end, + }, + + damage_zone_kill = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {uid = "string"}}, + userdata = {{default = 1}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "damage_zone" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, time, uid) + uid = uid or "" + local valid_uid, err = xpcall(pac.GetPartFromUniqueID, uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "damage_zone" then + if part.dmgzone_kill_done then + if part.dmgzone_kill_done + time > CurTime() then + return true + end + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(uid) + if part.ClassName == "damage_zone" then + if part.dmgzone_kill_done then + if part.dmgzone_kill_done + time > CurTime() then + return true + end + end + else + self:SetError("You set a UID that's not a damage zone!") + end + end + return false + end, + }, + + lockpart_grabbed = { + operator_type = "none", + callback = function(self, ent) + return ent.IsGrabbed and ent.IsGrabbedByUID + end + }, + + lockpart_grabbing = { + operator_type = "none", + arguments = {{uid = "string"}}, + userdata = {{enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "lock" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, uid) + uid = uid or "" + local valid_uid, err = xpcall(pac.GetPartFromUniqueID, uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "lock" then + if part.grabbing then + return IsValid(part.target_ent) + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(uid) + if part.ClassName == "lock" then + if part.grabbing then + return IsValid(part.target_ent) + end + else + self:SetError("You set a UID that's not a lock part!") + end + end + return false + end + } +} + + +do + + local base_input_enums_names = { + ["IN_ATTACK"] = 1, + ["IN_JUMP"] = 2, + ["IN_DUCK"] = 4, + ["IN_FORWARD"] = 8, + ["IN_BACK"] = 16, + ["IN_USE"] = 32, + ["IN_CANCEL"] = 64, + ["IN_LEFT"] = 128, + ["IN_RIGHT"] = 256, + ["IN_MOVELEFT"] = 512, + ["IN_MOVERIGHT"] = 1024, + ["IN_ATTACK2"] = 2048, + ["IN_RUN"] = 4096, + ["IN_RELOAD"] = 8192, + ["IN_ALT1"] = 16384, + ["IN_ALT2"] = 32768, + ["IN_SCORE"] = 65536, + ["IN_SPEED"] = 131072, + ["IN_WALK"] = 262144, + ["IN_ZOOM"] = 524288, + ["IN_WEAPON1"] = 1048576, + ["IN_WEAPON2"] = 2097152, + ["IN_BULLRUSH"] = 4194304, + ["IN_GRENADE1"] = 8388608, + ["IN_GRENADE2"] = 16777216 + } + local input_aliases = {} + + for name,value in pairs(base_input_enums_names) do + local alternative0 = string.lower(name) + local alternative1 = string.Replace(string.lower(name),"in_","") + local alternative2 = "+"..alternative1 + input_aliases[name] = value + input_aliases[alternative0] = value + input_aliases[alternative1] = value + input_aliases[alternative2] = value + end + + + local enums = {} + local enums2 = {} + for key, val in pairs(_G) do + if isstring(key) and isnumber(val) then + if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:sub(5):lower() + enums2[enums[val]] = val + elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:lower() + enums2[enums[val]] = val + end + end + end + + pac.key_enums = enums + +--@note button broadcast + + --TODO: Rate limit!!! + net.Receive("pac.BroadcastPlayerButton", function() + local ply = net.ReadEntity() + + if not ply:IsValid() then return end + + if ply == pac.LocalPlayer and (pace and pace.IsFocused() or gui.IsConsoleVisible()) then return end + + local key = net.ReadUInt(8) + local down = net.ReadBool() + + if not pac.key_enums then --rebuild the enums + local enums = {} + local enums2 = {} + for key, val in pairs(_G) do + if isstring(key) and isnumber(val) then + if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:sub(5):lower() + enums2[enums[val]] = val + elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:lower() + enums2[enums[val]] = val + end + end + end + + pac.key_enums = enums + end + + key = pac.key_enums[key] or key + + ply.pac_buttons = ply.pac_buttons or {} + ply.pac_buttons[key] = down + + --print("button update from" , ply, key, down) + --PrintTable(ply.pac_buttons) + + if down then + ply.pac_broadcasted_buttons_lastpressed = ply.pac_broadcasted_buttons_lastpressed or {} + ply.pac_broadcasted_buttons_lastpressed[key] = SysTime() + end + + for _,part in pairs(pac.getallparts()) do --locate the corresponding parts among the part pool + if part:GetPlayerOwner() == ply and part.ClassName == "event" and part.Event == "button" then + part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {} + part.holdtime = part.holdtime or 0 + part.toggleimpulsekey = part.toggleimpulsekey or {} + part.toggleimpulsekey[key] = down + part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime + end + end + + end) + + pac.player_inputs = {} + pac.player_inputs_update_times = {} + + net.Receive("pac.BroadcastPlayerInputs", function() + pac.player_inputs = net.ReadTable() + pac.player_inputs_update_times = net.ReadTable() + end) + + + PART.OldEvents.button = { + operator_type = "none", + arguments = {{button = "string"}, {holdtime = "number"}, {toggle = "boolean"}}, + userdata = {{enums = function() + return enums + end, default = "mouse_left"}, {default = 0}, {default = false}}, + nice = function(self, ent, button) + local ply = self:GetPlayerOwner() + + local active = {} + if ply.pac_buttons then + for k,v in pairs(ply.pac_buttons) do + if v then + table.insert(active, "\"" .. tostring(k) .. "\"") + end + end + end + active = table.concat(active, " or ") + + if active == "" then + active = "-" + end + + return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")" + end, + callback = function(self, ent, button, holdtime, toggle) + + local holdtime = holdtime or 0 + local toggle = toggle or false + + self.togglestate = self.togglestate or false + self.holdtime = holdtime + self.toggle = toggle + + self.toggleimpulsekey = self.toggleimpulsekey or {} + + if self.toggleimpulsekey[button] then + self.togglestate = not self.togglestate + self.toggleimpulsekey[button] = false + end + + --print(button, "hold" ,self.holdtime) + local ply = self:GetPlayerOwner() + self.pac_broadcasted_buttons_holduntil = self.pac_broadcasted_buttons_holduntil or {} + if ply == pac.LocalPlayer then + ply.pac_broadcast_buttons = ply.pac_broadcast_buttons or {} + if not ply.pac_broadcast_buttons[button] then local val = enums2[button:lower()] if val then @@ -1322,15 +2619,103 @@ do end ply.pac_broadcast_buttons[button] = true end + + --print(button, ply.pac_broadcasted_buttons_holduntil[button], ply.pac_broadcast_buttons[button]) + --PrintTable(ply.pac_broadcast_buttons) + --PrintTable(self.pac_broadcasted_buttons_holduntil) end local buttons = ply.pac_buttons + self.pac_broadcasted_buttons_holduntil[button] = self.pac_broadcasted_buttons_holduntil[button] or SysTime() + --print(button, self.toggle, self.togglestate) + --print(button,"until",self.pac_broadcasted_buttons_holduntil[button]) if buttons then - return buttons[button] + --print("trying to compare " .. SysTime() .. " > " .. self.pac_broadcasted_buttons_holduntil[button] - 0.05) + if self.toggle then + return self.togglestate + elseif self.holdtime > 0 then + return SysTime() < self.pac_broadcasted_buttons_holduntil[button] + else + return buttons[button] + end + end end, } + + PART.OldEvents.input = { + operator_type = "none", + arguments = {{UserInputs = "string"}, {RequireAllInputs = "boolean"}}, + userdata = {{enums = function() + return base_input_enums_names + end}}, + callback = function(self, ent, UserInputs, RequireAllInputs) + local ply = self:GetPlayerOwner() + UserInputs = UserInputs or "" + pac.player_inputs[ply] = pac.player_inputs[ply] or {} + local detect = false + local fulldetect = true + local input_list = string.Split(UserInputs, ";") + + for i,v in pairs(pac.player_inputs[ply]) do + for _,v2 in pairs(input_list) do + if pac.player_inputs[ply][input_aliases[v2]] then detect = true + else fulldetect = false end + end + end + if RequireAllInputs then return fulldetect + else return detect end + end + } + + PART.OldEvents.is_moving = { + operator_type = "none", + callback = function(self) + local ply = self:GetPlayerOwner() + pac.player_inputs = pac.player_inputs or {} + pac.player_inputs[ply] = pac.player_inputs[ply] or {} + return pac.player_inputs[ply][IN_FORWARD] or + pac.player_inputs[ply][IN_BACK] or + pac.player_inputs[ply][IN_MOVELEFT] or + pac.player_inputs[ply][IN_MOVERIGHT] or + pac.player_inputs[ply][IN_JUMP] + end + } + + PART.OldEvents.afk = { + operator_type = "none", + arguments = {{time = "number"}, {IncludeEyeAngles = "boolean"}}, + callback = function(self, ent, time, IncludeEyeAngles) + local time = time or 0 + local IncludeEyeAngles = IncludeEyeAngles + local ply = self:GetPlayerOwner() + local time_bool = false + local eyes_bool = false + + if pac.player_inputs_update_times then + pac.player_inputs_update_times[ply] = pac.player_inputs_update_times[ply] or 0 + time_bool = pac.player_inputs_update_times[ply] + time > CurTime() + end + + ply.last_eyeang = ply.last_eyeang or ply:EyeAngles() + ply.eyeang_update_time = ply.eyeang_update_time or CurTime() + + if ply.last_eyeang ~= ply:EyeAngles() then + ply.eyeang_update_time = CurTime() + end + + eyes_bool = (ply.last_eyeang ~= ply:EyeAngles()) or (ply.eyeang_update_time + time > CurTime()) + ply.last_eyeang = ply:EyeAngles() + if IncludeEyeAngles then + return not (time_bool or eyes_bool) + else + return not time_bool + end + return true + end + } + end do @@ -1485,7 +2870,7 @@ do local arguments = data.arguments local think = data.callback local eventObject = pac.CreateEvent(classname) - + if arguments then for i, data2 in ipairs(arguments) do local key, Type = next(data2) @@ -1495,6 +2880,13 @@ do eventObject.extra_nice_name = data.nice + local operator_type = data.operator_type + local preferred_operator = data.preferred_operator + local tutorial_explanation = data.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + function eventObject:Think(event, ent, ...) return think(event, ent, ...) end @@ -1511,6 +2903,8 @@ end do local animations = pac.animations local event = { + operator_type = "none", + tutorial_explanation = "selecting a custom animation part via UID,\nthis event activates whenever the linked custom animation is currently playing somewhere between the frames specified", name = "custom_animation_frame", nice = function(self, ent, animation) if animation == "" then self:SetWarning("no animation selected") return "no animation" end @@ -1585,6 +2979,15 @@ do eventObject.IsAvailable = event.available eventObject.extra_nice_name = event.nice + data = event + + local operator_type = data.operator_type + local preferred_operator = data.preferred_operator + local tutorial_explanation = data.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + pac.RegisterEvent(eventObject) end @@ -1685,6 +3088,13 @@ do return isDarkRP() and available() end + local operator_type = v.operator_type + local preferred_operator = v.preferred_operator + local tutorial_explanation = v.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + pac.RegisterEvent(eventObject) end end @@ -1711,7 +3121,7 @@ local function is_hidden_by_something_else(part, ignored_part) if part.active_events_ref_count > 0 and not part.active_events[ignored_part] then return true end - + return part.Hide end @@ -1758,13 +3168,50 @@ end PART.last_event_triggered = false +function PART:fix_args() + local args = string.Split(self.Arguments, "@@") + if self.Events[self.Event] then + if self.Events[self.Event].__registeredArguments then + --PrintTable(self.Events[self.Event].__registeredArguments) + if #self.Events[self.Event].__registeredArguments ~= #args then + for argn,arg in ipairs(self.Events[self.Event].__registeredArguments) do + if not args[argn] or args[argn] == "" then + local added_arg = "0" + if arg[2] == "boolean" then + if arg[3] then + if arg[3].default then added_arg = "1" + else added_arg = "0" end + end + else + if arg[3] then + if arg[3].default then + added_arg = tostring(arg[3].default) + end + end + end + args[argn] = added_arg + end + end + self.Arguments = table.concat(args, "@@") + end + end + end +end + function PART:OnThink() + self.nextactivationrefresh = self.nextactivationrefresh or CurTime() + if not self.singleactivatestate and self.nextactivationrefresh < CurTime() then + self.singleactivatestate = true + end + local ent = get_owner(self) if not ent:IsValid() then return end local data = PART.Events[self.Event] + if not data then return end + self:fix_args() self:TriggerEvent(should_trigger(self, ent, data)) if pace and pace.IsActive() and self.Name == "" then @@ -1775,6 +3222,51 @@ function PART:OnThink() end +function PART:SetAffectChildrenOnly(b) + if b == nil then return end + + if self.AffectChildrenOnly ~= nil and self.AffectChildrenOnly ~= b then + --print("changing") + local ent = get_owner(self) + local data = PART.Events[self.Event] + + if ent:IsValid() and data then + local b = should_trigger(self, ent, data) + if self.AffectChildrenOnly then + local parent = self:GetParent() + if parent:IsValid() then + parent:SetEventTrigger(self, b) + + for _, child in ipairs(self:GetChildren()) do + if child.active_events[self] then + child.active_events[self] = nil + child.active_events_ref_count = child.active_events_ref_count - 1 + child:CallRecursive("CalcShowHide", false) + end + end + end + + else + for _, child in ipairs(self:GetChildren()) do + child:SetEventTrigger(self, b) + end + if self:GetParent():IsValid() then + local parent = self:GetParent() + if parent.active_events[self] then + parent.active_events[self] = nil + parent.active_events_ref_count = parent.active_events_ref_count - 1 + parent:CallRecursive("CalcShowHide", false) + end + end + + end + end + end + self.AffectChildrenOnly = b + +end + + function PART:TriggerEvent(b) self.event_triggered = b -- event_triggered is just used for the editor @@ -1788,6 +3280,16 @@ function PART:TriggerEvent(b) parent:SetEventTrigger(self, b) end end + if IsValid(self.DestinationPart) then --target part. the proper one. + if IsValid(self.previousdestinationpart) then + if self.DestinationPart ~= self.previousdestinationpart then --once we change the destination part we need to reset the old one + self.previousdestinationpart:SetEventTrigger(self, false) + end + end + + (self.DestinationPart):SetEventTrigger(self, b) + self.previousdestinationpart = (self.DestinationPart) + end end PART.Operators = { @@ -1934,6 +3436,8 @@ function PART:NumberOperator(a, b) end end + + function PART:OnHide() if self.timerx_reset then self.time = nil @@ -1957,6 +3461,8 @@ function PART:OnShow() self.time = nil self.number = 0 end + self.showtime = CurTime() + self.singleactivatestate = true end function PART:OnAnimationEvent(ent) @@ -2031,6 +3537,7 @@ pac.AddHook("EntityFireBullets", "firebullets", function(ent, data) end end) + net.Receive("pac_event", function(umr) local ply = net.ReadEntity() local str = net.ReadString() @@ -2047,6 +3554,95 @@ net.Receive("pac_event", function(umr) end end) +concommand.Add("pac_wipe_events", function(ply) + ply.pac_command_events = nil + ply.pac_command_event_sequencebases = nil +end) +concommand.Add("pac_print_events", function(ply) + ply.pac_command_events = ply.pac_command_events or {} + PrintTable(ply.pac_command_events) +end) + +concommand.Add("pac_event_sequenced", function(ply, cmd, args) + + if not args[1] then return end + + local event = args[1] + local action = args[2] or "+" + local sequence_number = 0 + local set_target = args[3] or 1 + local found = false + + ply.pac_command_events = ply.pac_command_events or {} + ply.pac_command_events[event..1] = ply.pac_command_events[event..1] or {name = event..1, time = 0, on = 1} + + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} + + if not ply.pac_command_event_sequencebases[event] then + ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = 1} + end + + local target_number = 1 + local min = 1 + local max = ply.pac_command_event_sequencebases[event].max + + for i=1,100,1 do + if ply.pac_command_events[event..i] then + if ply.pac_command_events[event..i].on == 1 then + if sequence_number == 0 then sequence_number = i end + found = true + end + elseif ply.pac_command_events[event..i] == nil then + ply.pac_command_events[event..i] = {name = event..i, time = 0, on = 0} + end + end + + if found then + if action == "+" or action == "forward" or action == "add" or action == "sequence+" or action == "advance" then + + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 0} + if sequence_number == max then + target_number = min + else target_number = sequence_number + 1 end + + pac.Message("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) + ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} + + RunConsoleCommand("pac_event", event..sequence_number, "0") + RunConsoleCommand("pac_event", event..target_number, "1") + + + elseif action == "-" or action == "backward" or action == "sub" or action == "sequence-" then + + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 0} + if sequence_number == min then + target_number = max + else target_number = sequence_number - 1 end + + print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) + ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} + + RunConsoleCommand("pac_event", event..sequence_number, "0") + RunConsoleCommand("pac_event", event..target_number, "1") + + elseif action == "set" then + print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. set_target .. " / " .. max) + + sequence_number = set_target or 1 + for i=1,100,1 do + ply.pac_command_events[event..i] = nil + end + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 1} + target_number = set_target + net.Start("pac_event_set_sequence") + net.WriteString(event) + net.WriteUInt(sequence_number,8) + net.SendToServer() + end + end +end) + + pac.AddHook("OnPlayerChat", "say_event", function(ply, str) if ply:IsValid() then ply.pac_say_event = {str = str, time = pac.RealTime} @@ -2100,28 +3696,121 @@ reload custom gesture --]] - +local eventwheel_visibility_rule = CreateConVar("pac_eventwheel_visibility_rule" , "00", FCVAR_ARCHIVE, +"Different ways to filter your command events for the wheel.\n".. +"-1 ignores hide flags completely\n".. +"0 will hide a command if at least one event of one name has the \"hide in event wheel\" flag\n".. +"00 will hide a command only if ALL events of one name have the \"hide in event wheel\" flag\n".. +"1 will hide a command as soon as one event of a name is being hidden\n".. +"11 will hide a command only if ALL events of a name are being hidden\n".. +"2 will only show commands containing the following substrings, separated by spaces\n".. +"-2 will hide commands containing the following substrings, separated by spaces") -- Custom event selector wheel do local function get_events() local available = {} + local names = {} + local args = string.Split(eventwheel_visibility_rule:GetString(), " ") for k,v in pairs(pac.GetLocalParts()) do if v.ClassName == "event" then local e = v:GetEvent() if e == "command" then local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) - if hide then continue end + local this_event_hidden = v:IsHiddenBySomethingElse(false) + + if not names[cmd] then + --wheel_hidden is the hide_in_eventwheel box + --possible_hidden is part hidden + names[cmd] = { + name = cmd, event = v, + + wheel_hidden = hide, + all_wheel_hidden = hide, + + possible_hidden = this_event_hidden, + all_possible_hidden = this_event_hidden, + } + else + --if already exists, we need to check counter examples for whether all members are hidden or hide_in_eventwheel + + if not hide then + names[cmd].all_wheel_hidden = false + end + + if not this_event_hidden then + names[cmd].all_possible_hidden = false + end + + if not names[cmd].wheel_hidden and hide then + names[cmd].wheel_hidden = true + end + if not names[cmd].possible_hidden and this_event_hidden then + names[cmd].possible_hidden = true + end + + + end + available[cmd] = {type = e, time = time} end end end + for cmd,v in pairs(names) do + local remove = false + + if args[1] == "-1" then --skip + remove = false + elseif args[1] == "0" then --one hide_in_eventwheel + if v.wheel_hidden then + remove = true + end + elseif args[1] == "00" then --all hide_in_eventwheel + if v.all_wheel_hidden then + remove = true + end + elseif args[1] == "1" then --one hidden + if v.possible_hidden then + remove = true + end + elseif args[1] == "11" then --all hidden + if v.all_possible_hidden then + remove = true + end + elseif args[2] then + if #args > 1 then --args contains many strings + local match = false + + for i=2, #args, 1 do + local str = args[i] + if string.find(cmd, str) then + match = true + end + end + + if args[1] == "2" and not match then + remove = true + elseif args[1] == "-2" and match then + remove = true + end + + else --why would you use the 2 or -2 mode if you didn't set keywords?? + remove = false + end + end + + if remove then + available[cmd] = nil + end + end local list = {} for k,v in pairs(available) do - v.trigger = k - table.insert(list, v) + if k == names[k].name then + v.trigger = k + table.insert(list, v) + end end table.sort(list, function(a, b) return a.trigger > b.trigger end) @@ -2216,6 +3905,7 @@ do surface.SetDrawColor(HSVToColor(210,0,0.15)) end + --surface.DrawTexturedRect(x-48, y-48, 96, 96) surface.DrawTexturedRect(x-48, y-48, 96, 96) draw.SimpleText(self.name, "DermaDefault", x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) @@ -2273,6 +3963,97 @@ do selected = nil end + function pac.openEventSelectionList() + local selections = {} + local events = get_events() + for k, v in ipairs(events) do + + selections[k] = { + grow = 0, + name = v.trigger, + event = v + } + end + + gui.EnableScreenClicker(true) + + pac.AddHook("HUDPaint","custom_event_selector_list",function() + -- Right clicking cancels + if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionWheel(true) return end + + DisableClipping(true) + render.PushFilterMag(TEXFILTER.ANISOTROPIC) + render.PushFilterMin(TEXFILTER.ANISOTROPIC) + draw.SimpleText("Right click to cancel", "DermaDefault", ScrW()/2, ScrH()/2, color_red, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) + for _, v in ipairs(selections) do + draw_circle(v, x, y) + end + + render.PopFilterMag() + render.PopFilterMin() + DisableClipping(false) + end) + end + + function pac.closeEventSelectionList(cancel) + gui.EnableScreenClicker(false) + pac.RemoveHook("HUDPaint","custom_event_selector_list") + + if selected and cancel ~= true then + if not selected.event.time then + RunConsoleCommand("pac_event", selected.event.trigger, "toggle") + elseif selected.event.time > 0 then + RunConsoleCommand("pac_event", selected.event.trigger) + else + local ply = pac.LocalPlayer + + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then + RunConsoleCommand("pac_event", selected.event.trigger, "0") + else + RunConsoleCommand("pac_event", selected.event.trigger, "1") + end + end + end + selected = nil + end + concommand.Add("+pac_events", pac.openEventSelectionWheel) concommand.Add("-pac_events", pac.closeEventSelectionWheel) -end \ No newline at end of file + + concommand.Add("+pac_events_list", pac.openEventSelectionList) + concommand.Add("-pac_events_list", pac.closeEventSelectionList) + + + +end + +net.Receive("pac.SendPlayerObjUsed", function() + + local ply = net.ReadEntity() + local ent = net.ReadEntity() + local class = net.ReadString() + local override = net.ReadBool() + + if ply then + if ply:IsPlayer() and override then + ply.entity_inuse = ent + ply.entity_inuse_classname = class + end + end +end) + +net.Receive("pac.BroadcastDamageAttributions", function() + local ent = net.ReadEntity() + local tbl = net.ReadTable() + local kill = net.ReadBool() + if IsValid(ent) and tbl and IsValid(tbl.inflictor) then + ent.pac_damage_attributions = ent.pac_damage_attributions or {} + ent.pac_damage_attributions[tbl.attacker] = tbl + ent.pac_damage_attributions.latest = tbl + ent.pac_damage_attributions.is_kill = kill + if tbl.inflictor:GetClass() == "npc_grenade_frag" then + ent.pac_damage_attributions.IngoingGraceTime = CurTime() + end + end + +end) \ No newline at end of file diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua new file mode 100644 index 000000000..5341399a9 --- /dev/null +++ b/lua/pac3/core/client/parts/force.lua @@ -0,0 +1,233 @@ +local BUILDER, PART = pac.PartTemplate("base_drawable") + +PART.ClassName = "force" +PART.Group = 'advanced' +PART.Icon = 'icon16/database_go.png' + +PART.ManualDraw = true +PART.HandleModifiersManually = true + +BUILDER:StartStorableVars() + :SetPropertyGroup("AreaShape") + :GetSet("HitboxMode", "Box", {enums = { + ["Box"] = "Box", + ["Cube"] = "Cube", + ["Sphere"] = "Sphere", + ["Cylinder"] = "Cylinder", + ["Cone"] = "Cone", + ["Ray"] = "Ray" + }}) + :GetSet("Length",50) + :GetSet("Radius",50) + :GetSet("Preview",false) + :SetPropertyGroup("Forces") + :GetSet("BaseForce", 0) + :GetSet("AddedVectorForce", Vector(0,0,0)) + :GetSet("Torque", Vector(0,0,0)) + :GetSet("BaseForceAngleMode","Radial",{enums = {["Radial"] = "Radial", ["Locus"] = "Locus", ["Local"] = "Local"}}) + :GetSet("VectorForceAngleMode", "Global", {enums = {["Global"] = "Global", ["Local"] = "Local", ["Radial"] = "Radial", ["RadialNoPitch"] = "RadialNoPitch"}}) + :GetSet("TorqueMode", "TargetLocal", {enums = {["Global"] = "Global", ["TargetLocal"] = "TargetLocal", ["Local"] = "Local", ["Radial"] = "Radial"}}) + :GetSetPart("Locus", nil) + :GetSet("Continuous", true, {description = "If set to false, the force will be a single, stronger impulse"}) + :GetSet("AccountMass", false, {description = "Apply acceleration according to mass."}) + :GetSet("Falloff", false, {description = "Whether the force to apply should fade with distance"}) + :SetPropertyGroup("Targets") + :GetSet("AffectSelf",false) + :GetSet("Players",true) + :GetSet("PhysicsProps", true) + :GetSet("NPC",false) +:EndStorableVars() + + +function PART:OnRemove() +end + +function PART:Initialize() + self.next_impulse = CurTime() + 0.05 +end + +function PART:OnShow() + self.next_impulse = CurTime() + 0.05 + self:Impulse(true) +end + +function PART:OnHide() + hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) + self:Impulse(false) +end + +function PART:OnRemove() + hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) + self:Impulse(false) +end + + +function PART:OnDraw() + self.pos,self.ang = self:GetDrawPosition() + if not self.Preview then hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end + + if self.Preview then + hook.Add("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID, function() + if self.HitboxMode == "Box" then + local mins = Vector(-self.Radius, -self.Radius, -self.Length) + local maxs = Vector(self.Radius, self.Radius, self.Length) + render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Sphere" then + render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Cylinder" then + local obj = Mesh() + self:BuildCylinder(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Length ~= 0 and self.Radius ~= 0 then + local counter = 0 + --render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + for i=0,1,1/(math.abs(self.Length/self.Radius)) do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if counter == 200 then break end + counter = counter + 1 + end + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Cone" then + local obj = Mesh() + self:BuildCone(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Radius ~= 0 then + local steps + steps = math.Clamp(4*math.ceil(self.Length / (self.Radius or 1)),1,50) + for i = 1,0,-1/steps do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + + steps = math.Clamp(math.ceil(self.Length / (self.Radius or 1)),1,4) + for i = 0,1/8,1/128 do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Ray" then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + end) + end +end + + + +function PART:OnThink() + if self.Continuous and self.next_impulse < CurTime() then + self:Impulse(true) + end +end + +function PART:Impulse(on) + self.next_impulse = CurTime() + 0.05 + local tbl = {} + tbl.HitboxMode = self.HitboxMode + tbl.Length = self.Length + tbl.Radius = self.Radius + tbl.BaseForce = self.BaseForce + tbl.BaseForceAngleMode = self.BaseForceAngleMode + tbl.AddedVectorForce = self.AddedVectorForce + tbl.VectorForceAngleMode = self.VectorForceAngleMode + tbl.Torque = self.Torque + tbl.TorqueMode = self.TorqueMode + tbl.Continuous = self.Continuous + tbl.AccountMass = self.AccountMass + tbl.Falloff = self.Falloff + tbl.AffectSelf = self.AffectSelf + tbl.Players = self.Players + tbl.PhysicsProps = self.PhysicsProps + tbl.NPC = self.NPC + + tbl.UniqueID = self.UniqueID + tbl.PlayerOwner = self:GetPlayerOwner() + tbl.RootPartOwner = self:GetRootPart():GetOwner() + + if self.Locus ~= nil then + if self.Locus:IsValid() then + tbl.Locus_pos = self.Locus:GetWorldPosition() + end + else tbl.Locus_pos = self:GetWorldPosition() end + net.Start("pac_request_force") + net.WriteTable(tbl) + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + net.WriteBool(on) + net.SendToServer() +end + + + +function PART:BuildCylinder(obj) + local sides = 30 + local circle_tris = {} + for i=1,sides,1 do + local vert1 = {pos = Vector(0, self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(0, self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + local vert3 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert4 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + + table.insert(circle_tris, vert4) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert4) + + end + obj:BuildFromTriangles( circle_tris ) +end + +function PART:BuildCone(obj) + local sides = 30 + local circle_tris = {} + local verttip = {pos = Vector(0,0,0), u = 0, v = 0 } + for i=1,sides,1 do + local vert1 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + + table.insert(circle_tris, verttip) + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + table.insert(circle_tris, verttip) + + --circle_tris[8*(i-1) + 1] = vert1 + --circle_tris[8*(i-1) + 2] = vert2 + --circle_tris[8*(i-1) + 3] = vert3 + --circle_tris[8*(i-1) + 4] = vert4 + --circle_tris[8*(i-1) + 5] = vert3 + --circle_tris[8*(i-1) + 6] = vert2 + end + obj:BuildFromTriangles( circle_tris ) +end + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/group.lua b/lua/pac3/core/client/parts/group.lua index 2c0d2170a..ba6b30070 100644 --- a/lua/pac3/core/client/parts/group.lua +++ b/lua/pac3/core/client/parts/group.lua @@ -8,6 +8,8 @@ PART.Description = "right click to add parts" BUILDER:StartStorableVars() BUILDER:GetSet("Duplicate", false) BUILDER:GetSet("OwnerName", "self") + BUILDER:GetSet("ModelTracker", "", {hide_in_editor = true}) + BUILDER:GetSet("ClassTracker", "", {hide_in_editor = true}) BUILDER:EndStorableVars() local init_list = {} @@ -50,6 +52,8 @@ function PART:SetOwner(ent) if not pac.HookEntityRender(owner, self) then self:ShowFromRendering() end + self.ModelTracker = owner:GetModel() + self.ClassTracker = owner:GetClass() end end end diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua new file mode 100644 index 000000000..4d899ea43 --- /dev/null +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -0,0 +1,155 @@ +local BUILDER, PART = pac.PartTemplate("base") + +PART.ClassName = "health_modifier" + +PART.Group = 'advanced' +PART.Icon = 'icon16/heart.png' + +BUILDER:StartStorableVars() + + BUILDER:SetPropertyGroup("Health") + BUILDER:GetSet("ChangeHealth", false) + BUILDER:GetSet("FollowHealth", true, {description = "whether changing the max health should try to set your health at the same time"}) + BUILDER:GetSet("MaxHealth", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) + + BUILDER:SetPropertyGroup("ExtraHpBars") + BUILDER:GetSet("FollowHealthBars", true, {description = "whether changing the extra health bars should try to set your health at the same time"}) + BUILDER:GetSet("HealthBars", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,100)) end}) + BUILDER:GetSet("BarsAmount", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) + BUILDER:GetSet("BarsLayer", 1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,15)) end}) + BUILDER:GetSet("AbsorbFactor", 0, {editor_onchange = function(self,num) return math.Clamp(num,-1,1) end}) + + BUILDER:SetPropertyGroup("Armor") + BUILDER:GetSet("ChangeArmor", false) + BUILDER:GetSet("FollowArmor", true, {description = "whether changing the max armor should try to set your armor at the same time"}) + BUILDER:GetSet("MaxArmor", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) + + BUILDER:SetPropertyGroup("DamageMultipliers") + BUILDER:GetSet("DamageMultiplier", 1) + BUILDER:GetSet("ModifierId", "") + +BUILDER:EndStorableVars() + + +function PART:SendModifier(str) + if self:IsHidden() then return end + if str == "MaxHealth" and self.ChangeHealth then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("MaxHealth") + net.WriteUInt(self.MaxHealth, 32) + net.WriteBool(self.FollowHealth) + net.SendToServer() + elseif str == "MaxArmor" and self.ChangeArmor then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("MaxArmor") + net.WriteUInt(self.MaxArmor, 32) + net.WriteBool(self.FollowArmor) + net.SendToServer() + elseif str == "DamageMultiplier" then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("DamageMultiplier") + net.WriteFloat(self.DamageMultiplier) + net.WriteBool(true) + net.SendToServer() + elseif str == "HealthBars" then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("HealthBars") + net.WriteUInt(self.HealthBars, 32) + net.WriteUInt(self.BarsAmount, 32) + net.WriteUInt(self.BarsLayer, 4) + net.WriteFloat(self.AbsorbFactor) + net.WriteBool(self.FollowHealthBars) + net.SendToServer() + + elseif str == "all" then + self:SendModifier("MaxHealth") + self:SendModifier("MaxArmor") + self:SendModifier("DamageMultiplier") + self:SendModifier("HealthBars") + end +end + +function PART:SetHealthBars(val) + self.HealthBars = val + if LocalPlayer() ~= self:GetPlayerOwner() then return end + self:SendModifier("HealthBars") +end + +function PART:SetBarsAmount(val) + self.BarsAmount = val + if LocalPlayer() ~= self:GetPlayerOwner() then return end + self:SendModifier("HealthBars") +end + +function PART:SetBarsLayer(val) + self.BarsLayer = val + if LocalPlayer() ~= self:GetPlayerOwner() then return end + self:SendModifier("HealthBars") +end + +function PART:SetMaxHealth(val) + self.MaxHealth = val + if LocalPlayer() ~= self:GetPlayerOwner() then return end + self:SendModifier("MaxHealth") +end + +function PART:SetMaxArmor(val) + self.MaxArmor = val + if LocalPlayer() ~= self:GetPlayerOwner() then return end + self:SendModifier("MaxArmor") +end + +function PART:SetDamageMultiplier(val) + self.DamageMultiplier = val + if LocalPlayer() ~= self:GetPlayerOwner() then return end + self:SendModifier("DamageMultiplier") +end + +function PART:OnRemove() + if LocalPlayer() ~= self:GetPlayerOwner() then return end + local found_remaining_healthmod = false + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "health_modifier" and part ~= self then + found_remaining_healthmod = true + end + end + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("OnRemove") + net.WriteFloat(0) + net.WriteBool(true) + net.SendToServer() + + if not found_remaining_healthmod then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("MaxHealth") + net.WriteUInt(100,32) + net.WriteBool(true) + net.SendToServer() + + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("MaxArmor") + net.WriteUInt(100,32) + net.WriteBool(false) + net.SendToServer() + end +end + +function PART:OnShow() + self:SendModifier("all") +end + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index c201509b8..5c712df14 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -1,6 +1,4 @@ language.Add("pac_hitscan", "Hitscan") -local ent -local bulletinfo = {} --local vector_origin = vector_origin --local angle_origin = Angle(0,0,0) --local WorldToLocal = WorldToLocal @@ -12,11 +10,15 @@ PART.Group = 'advanced' PART.Icon = 'icon16/user_gray.png' BUILDER:StartStorableVars() - :GetSet("ServerBullets",true) + :GetSet("ServerBullets",true, {description = "serverside bullets can do damage and exert a physical impact force"}) :SetPropertyGroup("bullet properties") :GetSet("BulletImpact", false) :GetSet("Damage", 0) :GetSet("Force",1000) + :GetSet("DamageFalloff", false, {description = "enable damage falloff. The lowest damage is not a fixed damage number, but a fraction of the total initial damage.\nThe server can still restrict the maximum distance of all bullets"}) + :GetSet("DamageFalloffDistance", 5000) + :GetSet("DamageFalloffFraction", 0.5) + :GetSet("DamageType", "generic", {enums = { generic = 0, --generic damage crush = 1, --caused by physics interaction @@ -67,6 +69,7 @@ BUILDER:StartStorableVars() :GetSet("SpreadX", 1) :GetSet("SpreadY", 1) :GetSet("NumberBullets", 1) + :GetSet("DistributeDamage", false, {description = "whether or not the damage should be divided equally to all bullets in NumberBullets.\nThe server can still force multi-shots to do that"}) :GetSet("TracerSparseness", 1) :GetSet("MaxDistance", 10000) :GetSet("TracerName", "Tracer", {enums = { @@ -82,12 +85,19 @@ BUILDER:StartStorableVars() ["Toolgun tracer"] = "ToolTracer", ["Laser tracer"] = "LaserTracer" }}) + BUILDER:EndStorableVars() +function PART:Initialize() + self.bulletinfo = {} + self.ent = self:GetRootPart():GetOwner() + self:UpdateBulletInfo() +end + function PART:OnShow() self:UpdateBulletInfo() - self:GetWorldPosition() - self:GetWorldAngles() + --self:GetWorldPosition() + --self:GetWorldAngles() self:Shoot(self:GetDrawPosition()) end @@ -97,33 +107,47 @@ function PART:OnDraw() end function PART:Shoot(pos, ang) - bulletinfo.Tracer = self.TracerSparseness - bulletinfo.Src = pos - bulletinfo.Dir = ang:Forward() - bulletinfo.Spread = Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0) - - ent = self:GetOwner() - if self.ServerBullets then - print("WE NEED A BULLET IN THE SERVER!") + if not self.ent then self:UpdateBulletInfo() end + if not IsValid(self.ent) then return end + + self.bulletinfo.Src = pos + self.bulletinfo.Dir = ang:Forward() + self.bulletinfo.Spread = Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0) + + if self.NumberBullets == 0 then return end + if self.ServerBullets and self.Damage ~= 0 then + --print("WE NEED A BULLET IN THE SERVER!") + --PrintTable(self.bulletinfo) + net.Start("pac_hitscan") - net.WriteEntity(ent) - net.WriteTable(bulletinfo) + net.WriteEntity(self:GetRootPart():GetOwner()) + net.WriteTable(self.bulletinfo) + net.WriteAngle(ang) + net.WriteString(self.UniqueID) net.SendToServer() else - ent:FireBullets(bulletinfo) + self.ent:FireBullets(self.bulletinfo) end end function PART:UpdateBulletInfo() - bulletinfo.Attacker = ent + self.bulletinfo.Attacker = self:GetRootPart():GetOwner() + self.ent = self:GetRootPart():GetOwner() if self.Damage == 0 then - else bulletinfo.Damage = self.Damage end + else self.bulletinfo.Damage = self.Damage end + + self.bulletinfo.Tracer = self.TracerSparseness + + self.bulletinfo.Force = self.Force + self.bulletinfo.Distance = self.MaxDistance + self.bulletinfo.Num = self.NumberBullets + self.bulletinfo.Tracer = self.TracerSparseness --tracer every x bullets + self.bulletinfo.TracerName = self.TracerName + self.bulletinfo.DistributeDamage = self.DistributeDamage - bulletinfo.Force = self.Force - bulletinfo.Distance = self.MaxDistance - bulletinfo.Num = self.NumberBullets - bulletinfo.Tracer = self.TracerSparseness --tracer every x bullets - bulletinfo.TracerName = self.TracerName + self.bulletinfo.DamageFalloff = self.DamageFalloff + self.bulletinfo.DamageFalloffDistance = self.DamageFalloffDistance + self.bulletinfo.DamageFalloffFraction = self.DamageFalloffFraction --[[bulletinfo.ammodata = { name = "" diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index 1d8b62169..4b6c54321 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -1,18 +1,3 @@ -nodes = {} - -local cam_PushModelMatrix = cam.PushModelMatrix -local cam_PopModelMatrix = cam.PopModelMatrix -local Vector = Vector -local Angle = Angle -local EF_BONEMERGE = EF_BONEMERGE -local NULL = NULL -local Color = Color -local Matrix = Matrix -local vector_origin = vector_origin - -local pos -local ang - local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "interpolated_multibone" @@ -21,13 +6,11 @@ PART.Icon = 'icon16/table_multiple.png' PART.is_model_part = false PART.ManualDraw = true -PART.HandleModifiersManually = true +PART.HandleModifiersManually = false BUILDER:StartStorableVars() :SetPropertyGroup("test") - :GetSet("Test1", false) - :GetSet("Test2", false) - :GetSet("Force000", false) + :GetSet("Preview", false) :SetPropertyGroup("Interpolation") :GetSet("LerpValue",0) :GetSet("Power",1) @@ -44,27 +27,31 @@ BUILDER:StartStorableVars() :GetSetPart("Node8") :GetSetPart("Node9") :GetSetPart("Node10") + :GetSetPart("Node11") + :GetSetPart("Node12") + :GetSetPart("Node13") + :GetSetPart("Node14") + :GetSetPart("Node15") + :GetSetPart("Node16") + :GetSetPart("Node17") + :GetSetPart("Node18") + :GetSetPart("Node19") + :GetSetPart("Node20") :EndStorableVars() ---PART:GetWorldPosition() ---PART:GetWorldAngles() function PART:OnRemove() SafeRemoveEntityDelayed(self.Owner,0.1) end function PART:Initialize() - print("a multiboner is born") + self.nodes = {} self.Owner = pac.CreateEntity("models/pac/default.mdl") self.Owner:SetNoDraw(true) - --self:SetOwner(pac.CreateEntity("models/pac/default.mdl")) - - --pac.HookEntityRender(self.Owner, self) - --self.Owner.PACPart = self - + self.valid_time = CurTime() + 1 end function PART:OnShow() - + self.valid_time = CurTime() end function PART:OnHide() @@ -79,15 +66,18 @@ end --STAGE 0 1 2 3 --PROPORTION 0 0.5 0 0.5 0 0.5 3 function PART:OnDraw() + self:UpdateNodes() + if self.valid_time > CurTime() then print("returned") return end + + self.pos = self.pos or self:GetWorldPosition() + self.ang = self.ang or self:GetWorldAngles() - --local ent = self:GetOwner() - self.pos,self.ang = self:GetDrawPosition() - if not self.Test1 then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end + if not self.Preview then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end local stage = math.max(0,math.floor(self.LerpValue)) local proportion = math.max(0,self.LerpValue) % 1 - if self.Test1 then + if self.Preview then hook.Add("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID, function() render.DrawLine(self.pos,self.pos + self.ang:Forward()*50, Color(255,0,0)) render.DrawLine(self.pos,self.pos - self.ang:Right()*50, Color(0,255,0)) @@ -96,18 +86,13 @@ function PART:OnDraw() end) end self:Interpolate(stage,proportion) - --ent:SetPos(self.pos) - --ent:SetAngles(self.ang) - --self.pos = Vector(0,0,0) + end -function PART:OnThink() - if self.Node1 ~= nil then nodes["Node1"] = self.Node1 end - if self.Node2 ~= nil then nodes["Node2"] = self.Node2 end - if self.Node3 ~= nil then nodes["Node3"] = self.Node3 end - if self.Node4 ~= nil then nodes["Node4"] = self.Node4 end - if self.Node5 ~= nil then nodes["Node5"] = self.Node5 end - +function PART:UpdateNodes() + for i=1,10,1 do + self.nodes["Node"..i] = self["Node"..i] + end end function PART:SetWorldPos(x,y,z) @@ -122,13 +107,13 @@ function PART:Interpolate(stage, proportion) if stage <= 0 then firstnode = self else - firstnode = nodes["Node"..stage] or self + firstnode = self.nodes["Node"..stage] or self end - local secondnode = nodes["Node"..stage+1] - if firstnode == nil or firstnode == NULL then firstnode = self end - if secondnode == nil or secondnode == NULL then secondnode = self end + local secondnode = self.nodes["Node"..stage+1] + if firstnode == nil or firstnode == NULL or not firstnode.GetWorldPosition then firstnode = self end + if secondnode == nil or secondnode == NULL or not secondnode.GetWorldPosition then secondnode = self end proportion = math.pow(proportion,self.Power) if secondnode ~= nil and secondnode ~= NULL then @@ -139,10 +124,17 @@ function PART:Interpolate(stage, proportion) self.pos = firstnode:GetWorldPosition() self.ang = firstnode:GetWorldAngles() else - self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion - self.ang = GetClosestAngleMidpoint(self:GetWorldAngles(), self:GetWorldAngles(), proportion) + if self.InterpolatePosition then self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion end + if self.InterpolateAngles then self.ang = GetClosestAngleMidpoint(self:GetWorldAngles(), self:GetWorldAngles(), proportion) end --self.ang = (1-proportion)*(self:GetWorldAngles() + Angle(360,360,360)) + (self:GetWorldAngles() + Angle(360,360,360))*proportion end + + if not self.InterpolatePosition then + self.pos = self:GetWorldPosition() + end + if not self.InterpolateAngles then + self.ang = self:GetWorldAngles() + end self.Owner:SetPos(self.pos) self.Owner:SetAngles(self.ang) end @@ -221,32 +213,6 @@ function PART:InterpolateAngle() end ---[[function PART:ApplyMatrix() - print("MATRIX???") - local ent = self:GetOwner() - if not ent:IsValid() then return end - - local mat = Matrix() - - if self.ClassName ~= "model2" then - mat:Translate(self.Position + self.PositionOffset) - mat:Rotate(self.Angles + self.AngleOffset) - end - - if mat:IsIdentity() then - ent:DisableMatrix("RenderMultiply") - else - ent:EnableMatrix("RenderMultiply", mat) - end -end--]] - ---[[function PART:SetLerpValue(var) - print("adjusted lerp value. "..type(var).." "..var) - self.LerpValue = var - assert(self.LerpValue == var) - assert(self.LerpValue ~= nil) - self:Interpolate(self:GetInterpolationParameters()) -end]] function PART:SetInterpolatePosition(b) --print(type(b).." "..b) diff --git a/lua/pac3/core/client/parts/light.lua b/lua/pac3/core/client/parts/light.lua index 68d651e0b..decfa0f93 100644 --- a/lua/pac3/core/client/parts/light.lua +++ b/lua/pac3/core/client/parts/light.lua @@ -16,6 +16,21 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Brightness", 8) BUILDER:GetSet("Size", 100, {editor_sensitivity = 0.25}) BUILDER:GetSet("Color", Vector(1, 1, 1), {editor_panel = "color2"}) + BUILDER:GetSet("Style", 0, {editor_clamp = {0, 12}, enums = { + ["Normal"] = "0", + ["Flicker A"] = "1", + ["Slow, strong pulse"] = "2", + ["Candle A"] = "3", + ["Fast strobe"] = "4", + ["Gentle pulse"] = "5", + ["Flicker B"] = "6", + ["Candle B"] = "7", + ["Candle C"] = "8", + ["Slow strobe"] = "9", + ["Fluorescent flicker"] = "10", + ["Slow pulse, noblack"] = "11", + ["Underwater light mutation"] = "12" + }}) BUILDER:EndStorableVars() function PART:GetLight() @@ -67,6 +82,11 @@ function PART:OnDraw() self:GetLight().pos = pos self:GetLight().dir = ang:Forward() end +function PART:SetStyle(val) + print(val, type(val)) + self.Style = val + self:GetLight().Style = self.Style +end function PART:SetSize(val) self.Size = val diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index be34ef648..84e5c78f5 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -2,14 +2,24 @@ include("pac3/extra/shared/net_combat.lua") --pac3/extra/shared/net_combat.lua - -local target_ent = nil local pac = pac local Vector = Vector local Angle = Angle local NULL = NULL local Matrix = Matrix +local physics_point_ent_classes = { + ["prop_physics"] = true, + ["prop_physics_multiplayer"] = true, + ["prop_ragdoll"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true +} + + local BUILDER, PART = pac.PartTemplate("base_movable") PART.ClassName = "lock" @@ -22,11 +32,11 @@ BUILDER:StartStorableVars() :GetSet("Mode", "None", {enums = {["None"] = "None", ["Grab"] = "Grab", ["Teleport"] = "Teleport"}}) :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"}) :GetSet("RelativeGrab", false) - :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before grabbing are re-applied"}) + :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before self.grabbing are re-applied"}) :SetPropertyGroup("DetectionOrigin") :GetSet("Radius", 20) - :GetSet("RadiusOffsetDown", false, {description = "Lowers the detect origin by the radius distance"}) + :GetSet("OffsetDownAmount", 0, {description = "Lowers the detect origin by some amount"}) :GetSetPart("TargetPart") :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."}) :GetSet("Preview", false) @@ -36,118 +46,272 @@ BUILDER:StartStorableVars() :GetSet("SlopeSafety", false, {description = "Teleports a bit up in case you end up on a slope and get stuck."}) :SetPropertyGroup("PlayerCameraOverride") - :GetSet("OverrideEyeAngles", true, {description = "Whether the part will try to override players' eye angles. Requires OverrideAngles and user consent"}) + :GetSet("OverrideEyeAngles", false, {description = "Whether the part will try to override players' eye angles. Requires OverrideAngles and user consent"}) + :GetSet("OverrideEyePosition", false, {description = "Whether the part will try to override players' view position to a selected base_movable part with a CalcView hook as well. Requires OverrideEyeAngles, OverrideAngles, a valid base_movable OverrideEyePositionPart and user consent"}) :GetSetPart("OverrideEyePositionPart") + :GetSet("DrawLocalPlayer", true, {description = "Whether the resulting calcview will draw the target player as in third person, otherwise hide the player"}) :SetPropertyGroup("Targets") + :GetSet("AffectPlayerOwner", false) :GetSet("Players", false) :GetSet("PhysicsProps", false) :GetSet("NPC", false) -BUILDER:EndStorableVars() - -local valid_ent = false -local grabbing = false -local last_request_time = SysTime() -local last_entsearch = SysTime() -local default_ang = Angle(0,0,0) -local forcebreak = false -local next_allowed_grab = SysTime() +BUILDER:EndStorableVars() function PART:OnThink() - self:CheckEntValidity() - - if SysTime() > next_allowed_grab then - forcebreak = false - elseif forcebreak then - valid_ent = false - self:reset_ent_ang() - target_ent = nil - return + if self.forcebreak then + if self.next_allowed_grab < CurTime() then --we're able to resume + if self.ContinuousSearch then + self.forcebreak = false + else + --wait for the next showing to reset the search because we have self.resetcondition + end + else + return + end end - self:GetWorldPosition() - self:GetWorldAngles() if self.Mode == "Grab" then - if not valid_ent and self.ContinuousSearch then --no hit and can search = search more and try the move later + + if self.ContinuousSearch then self:DecideTarget() - self:CheckEntValidity() - return - elseif not valid_ent and not self.ContinuousSearch then --if initial think failed to find and can't search = stop - --print("end of the line. ", not valid_ent, not self.ContinuousSearch, not valid_ent and not self.ContinuousSearch) - return - end - end - --good hit and can search = search more and try the move later - - --self:DecideTarget() - if self.Mode == "Grab" and valid_ent then - if self.OverrideAngles and valid_ent and self.target_ent:IsValid() then - default_ang = self.target_ent:GetAngles() - if self.OverrideEyeAngles then default_ang.y = self:GetWorldAngles().y end end - if not grabbing and not self.OverrideAngles then default_ang = self.target_ent:GetAngles() end - - local relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity() - if not self.RelativeGrab then - relative_transform_matrix = Matrix() + self:CheckEntValidity() + if self.valid_ent then + local final_ang = Angle(0,0,0) + if self.OverrideAngles then --if overriding angles + if self.is_first_time then + self.default_ang = self.target_ent:GetAngles() --record the initial ent angles + end + if self.OverrideEyeAngles then self.default_ang.y = self:GetWorldAngles().y end --if we want to override players eye angles we will keep recording the yaw + + elseif not self.grabbing then + self.default_ang = self.target_ent:GetAngles() --record the initial ent angles anyway + end + + local relative_transform_matrix = Matrix() relative_transform_matrix:Identity() - end - - local offset_matrix = Matrix() - offset_matrix:Translate(self:GetWorldPosition()) - offset_matrix:Rotate(self:GetWorldAngles()) - offset_matrix:Mul(relative_transform_matrix) - - local relative_offset_pos = offset_matrix:GetTranslation() - local relative_offset_ang = offset_matrix:GetAngles() - - if LocalPlayer() == self:GetPlayerOwner() then - net.Start("pac_request_position_override_on_entity") + if self.RelativeGrab then - net.WriteVector(relative_offset_pos) - net.WriteAngle(relative_offset_ang) + if self.is_first_time then self:CalculateRelativeOffset() end + relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity() else - net.WriteVector(self:GetWorldPosition()) - net.WriteAngle(self:GetWorldAngles()) + relative_transform_matrix = Matrix() + relative_transform_matrix:Identity() + end + + local offset_matrix = Matrix() + offset_matrix:Translate(self:GetWorldPosition()) + offset_matrix:Rotate(self:GetWorldAngles()) + offset_matrix:Mul(relative_transform_matrix) + + local relative_offset_pos = offset_matrix:GetTranslation() + local relative_offset_ang = offset_matrix:GetAngles() + + if LocalPlayer() == self:GetPlayerOwner() then + net.Start("pac_request_position_override_on_entity_grab") + net.WriteBool(self.is_first_time) + net.WriteString(self.UniqueID) + if self.RelativeGrab then + net.WriteVector(relative_offset_pos) + net.WriteAngle(relative_offset_ang) + else + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + end + end + + local try_override_eyeang = false + if self.target_ent:IsPlayer() then + if self.OverrideEyeAngles then try_override_eyeang = true end + end + if LocalPlayer() == self:GetPlayerOwner() then + net.WriteBool(self.OverrideAngles) + net.WriteBool(try_override_eyeang) + net.WriteEntity(self.target_ent) + net.WriteEntity(self:GetRootPart():GetOwner()) + local can_calcview = false + if self.OverrideEyePosition and IsValid(self.OverrideEyePositionPart) then + if self.OverrideEyePositionPart.GetWorldAngles then + can_calcview = true + end + end + net.WriteBool(can_calcview) + --print(IsValid(self.OverrideEyePositionPart), self.OverrideEyeAngles) + if can_calcview then + net.WriteVector(self.OverrideEyePositionPart:GetWorldPosition()) + net.WriteAngle(self.OverrideEyePositionPart:GetWorldAngles()) + else + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + end + net.WriteBool(self.DrawLocalPlayer) + net.SendToServer() + end + --print(self:GetRootPart():GetOwner()) + if self.Players and self.target_ent:IsPlayer() and self.OverrideAngles then + local mat = Matrix() + mat:Identity() + + if self.OverrideAngles then + final_ang = self:GetWorldAngles() + end + if self.OverrideEyeAngles then + final_ang = self:GetWorldAngles() + --final_ang = Angle(0,180,0) + --print("chose part ang") + end + if self.OverrideEyePosition and can_calcview then + final_ang = self.OverrideEyePositionPart:GetWorldAngles() + --print("chose alt part ang") + end + + local eyeang = self.target_ent:EyeAngles() + --print("eyeang", eyeang) + eyeang.p = 0*eyeang.p + eyeang.y = eyeang.y + eyeang.r = 0*eyeang.r + mat:Rotate(final_ang - eyeang) --this works + --mat:Rotate(eyeang) + --print("transform ang", final_ang) + --print("part's angles", self:GetWorldAngles()) + --mat:Rotate(self:GetWorldAngles()) + + + self.target_ent:EnableMatrix("RenderMultiply", mat) + end + + self.grabbing = true + self.teleported = false end + end + --if self.is_first_time then print("lock " .. self.UniqueID .. "did its first clock") end + self.is_first_time = false +end - local can_rotate = self.OverrideAngles - if self.target_ent:IsPlayer() then can_rotate = false end - if LocalPlayer() == self:GetPlayerOwner() then - net.WriteBool(can_rotate) - net.WriteEntity(self.target_ent) - net.WriteEntity(self:GetRootPart():GetOwner()) - net.SendToServer() +do + function PART:BreakLock(ent) + self.forcebreak = true + self.next_allowed_grab = CurTime() + 3 + self.target_ent.IsGrabbedID = nil + self.target_ent = nil + self.grabbing = false + pac.Message(Color(255, 50, 50), "lock break result:") + MsgC(Color(0,255,255), "\t", self) MsgC(Color(200, 200, 200), " in your group ") MsgC(Color(0,255,255), self:GetRootPart(),"\n") + MsgC(Color(200, 200, 200), "\tIt will now be in the forcebreak state until the next allowed grab, 3 seconds from now.\n") + if not self.ContinuousSearch then + self.resetcondition = true end - --print(self:GetRootPart():GetOwner()) - if self.Players and self.target_ent:IsPlayer() and self.OverrideAngles then - - local mat = Matrix() - mat:Identity() - mat:Rotate(Angle(0,0,0)) - mat:Rotate(self:GetWorldAngles()) - --mat:Rotate(Angle(self:GetWorldAngles().p,self:GetWorldAngles().y,self:GetWorldAngles().r)) - self.target_ent:EnableMatrix("RenderMultiply", mat) - if self.OverrideEyeAngles then - --self.target_ent:SetEyeAngles(Angle(self:GetWorldAngles().p,default_ang.y,default_ang.r)) + ent:SetGravity(1) + + ent.pac_recently_broken_free_from_lock = CurTime() + ent:DisableMatrix("RenderMultiply") + end + net.Receive("pac_request_lock_break", function(len) + --[[format: + net.Start("pac_request_lock_break") + net.WriteEntity(ply) --the breaker + net.WriteString("") --the uid if applicable + net.Send(ent) --that's us! the locker + ]] + local target_to_release = net.ReadEntity() + local uid = net.ReadString() + local reason = net.ReadString() + pac.Message(Color(255, 255, 255), "------------CEASE AND DESIST!------------") + MsgC(Color(0,255,255), tostring(target_to_release)) MsgC(Color(255,50,50), " WANTS TO BREAK FREE!!\n") + MsgC(Color(255,50,50), "reason:") MsgC(Color(0,255,255), reason .."\n") + + if uid ~= "" then --if a uid is provided + MsgC(Color(255, 50, 50), "AND IT KNOWS YOUR UID! " .. uid .. "\n") + local part = pac.GetPartFromUniqueID(found_uid) + if part then + if part.ClassName == "lock" then + part:BreakLock(target_to_release) + + end end - if self.OverrideEyePositionPart then - --[[if self.OverrideEyePositionPart:IsValid() then - self.target_ent:SetCurrentViewOffset(self.OverrideEyePositionPart:GetWorldPosition() - self:GetWorldPosition()) - end]] + + else + MsgC(Color(200, 200, 200), "NOW! WE SEARCH YOUR LOCAL PARTS!\n") + for i,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "lock" then + if part.grabbing then + if IsValid(part.target_ent) and part.target_ent == target_to_release then + part:BreakLock(target_to_release) + end + end + end end - --if self.OverrideEyeAngles then self.target_ent:SetEyeAngles(self:GetWorldAngles()) end end - last_request_time = SysTime() - grabbing = true - teleported = false - elseif self.Mode == "Teleport" and not teleported then + end) + + net.Receive("pac_mark_grabbed_ent", function(len) + + local target_to_mark = net.ReadEntity() + local successful_grab = net.ReadBool() + local uid = net.ReadString() + local part = pac.GetPartFromUniqueID(uid) + --print(target_to_mark,"is grabbed by",uid) + + if not successful_grab then + part:BreakLock(target_to_mark) --yes we will employ the aggressive lock break here + else + target_to_mark.IsGrabbed = successful_grab + target_to_mark.IsGrabbedID = uid + target_to_mark:SetGravity(0) + end + + end) +end + +function PART:SetRadius(val) + self.Radius = val + local sv_dist = GetConVar("pac_sv_lock_max_grab_radius"):GetInt() + if self.Radius > sv_dist then + self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end + +end + +function PART:OnShow() + local origin_part + self.is_first_time = true + if self.resetting_condition or self.forcebreak then + if self.next_allowed_grab < CurTime() then + self.forcebreak = false + self.resetting_condition = false + end + end + hook.Add("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID, function() + if self.TargetPart:IsValid() then + origin_part = self.TargetPart + else + origin_part = self + end + if origin_part == nil or not self.Preview or LocalPlayer() ~= self:GetPlayerOwner() then return end + local sv_dist = GetConVar("pac_sv_lock_max_grab_radius"):GetInt() + + render.DrawLine(origin_part:GetWorldPosition(),origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount),Color(255,255,255)) + + if self.Radius < sv_dist then + self:SetInfo(nil) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), sv_dist, 30, 30, Color(50,50,150),true) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), self.Radius, 30, 30, Color(255,255,255),true) + else + self:SetInfo("Your radius is beyond the server max! Active max is " .. sv_dist) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), sv_dist, 30, 30, Color(0,255,255),true) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), self.Radius, 30, 30, Color(100,100,100),true) + end + end) + if self.Mode == "Teleport" then self.target_ent = nil local ang_yaw_only = self:GetWorldAngles() @@ -157,78 +321,43 @@ function PART:OnThink() local teleport_pos_final = self:GetWorldPosition() - if self.LimitTeleportDistanceByRadius then + if self.ClampDistance then local ply_pos = self:GetPlayerOwner():GetPos() local pos = self:GetWorldPosition() if pos:Distance(ply_pos) > self.Radius then local clamped_pos = ply_pos + (pos - ply_pos):GetNormalized()*self.Radius - --print(clamped_pos:Length()) teleport_pos_final = clamped_pos end end if self.SlopeSafety then teleport_pos_final = teleport_pos_final + Vector(0,0,30) end - net.Start("pac_request_position_override_on_entity") + net.Start("pac_request_position_override_on_entity_teleport") + net.WriteString(self.UniqueID) net.WriteVector(teleport_pos_final) net.WriteAngle(ang_yaw_only) net.WriteBool(self.OverrideAngles) - net.WriteEntity(self:GetPlayerOwner()) - net.WriteEntity(self:GetPlayerOwner()) net.SendToServer() end - self:GetPlayerOwner():SetAngles( ang_yaw_only ) - - teleported = true - grabbing = false - end - - if CLIENT then - net.Receive("pac_request_lock_break", function(len) - local target_to_release = net.ReadEntity() - print("YOU'RE HOLDING " .. tostring(self.target_ent)) - pac.Message(Color(255, 50, 50), tostring(target_to_release) .. " WANTS TO BREAK FREE!!") - if self.target_ent == target_to_release then - forcebreak = true - next_allowed_grab = SysTime() + 3 - end - end) + self.grabbing = false + elseif self.Mode == "Grab" then + self:DecideTarget() + self:CheckEntValidity() end end -function PART:OnShow() - local origin_part - hook.Add("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID, function() - if self.TargetPart:IsValid() then - origin_part = self.TargetPart - else - origin_part = self - end - if origin_part == nil or not self.Preview or LocalPlayer() ~= self:GetPlayerOwner() then return end - if self.RadiusOffsetDown then - render.DrawLine(origin_part:GetWorldPosition(),origin_part:GetWorldPosition() + Vector(0,0,-self.Radius),Color(255,255,255)) - render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.Radius), self.Radius, 30, 30, Color(255,255,255),true) - else - render.DrawWireframeSphere(origin_part:GetWorldPosition(), self.Radius, 30, 30, Color(255,255,255),true) - end - end) - self.target_ent = nil - --self.relative_transform_matrix = Matrix():Identity() - self:DecideTarget() - self:CheckEntValidity() - --self:CalculateRelativeOffset() -end - function PART:OnHide() hook.Remove("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID) - teleported = false - grabbing = false - if self.target_ent == nil then return end + self.teleported = false + self.grabbing = false + if self.target_ent == nil then return + else self.target_ent.IsGrabbed = false self.target_ent.IsGrabbedID = nil end self:reset_ent_ang() end function PART:reset_ent_ang() if self.target_ent == nil then return end local reset_ent = self.target_ent + if reset_ent:IsValid() then timer.Simple(math.min(self.RestoreDelay,5), function() if LocalPlayer() == self:GetPlayerOwner() then @@ -239,8 +368,8 @@ function PART:reset_ent_ang() net.WriteEntity(self:GetPlayerOwner()) net.SendToServer() end - if self.Players then - self.target_ent:DisableMatrix("RenderMultiply") + if self.Players and reset_ent:IsPlayer() then + reset_ent:DisableMatrix("RenderMultiply") end end) end @@ -250,41 +379,48 @@ function PART:OnRemove() end function PART:DecideTarget() - --print("search") - local ents = ents.GetAll() + + local RADIUS = math.Clamp(self.Radius,0,GetConVar("pac_sv_lock_max_grab_radius"):GetInt()) local ents_candidates = {} local chosen_ent = nil local target_part = self.TargetPart - --filter entities - for i, ent_candidate in ipairs(ents) do - --print(ent_candidate:GetClass()) - if ent_candidate:IsValid() then - local origin - - if self.TargetPart and (self.TargetPart):IsValid() then - origin = (self.TargetPart):GetWorldPosition() - else - origin = self:GetWorldPosition() - end + local origin - if self.RadiusOffsetDown then origin:Add(Vector(0,0,-self.Radius)) end + if self.TargetPart and (self.TargetPart):IsValid() then + origin = (self.TargetPart):GetWorldPosition() + else + origin = self:GetWorldPosition() + end + origin:Add(Vector(0,0,-self.OffsetDownAmount)) - if ent_candidate:GetPos():Distance( origin ) < self.Radius then - if self.Players and ent_candidate:IsPlayer() then - --we don't want to grab ourselves - if (self:GetPlayerOwner() ~= self:GetRootPart():GetOwner()) then + for i, ent_candidate in ipairs(ents.GetAll()) do + + if IsValid(ent_candidate) then + local check_further = true + if ent_candidate.pac_recently_broken_free_from_lock then + if ent_candidate.pac_recently_broken_free_from_lock + 10 > CurTime() then + check_further = false + end + else check_further = true end + + if check_further then + if ent_candidate:GetPos():Distance( origin ) < RADIUS then + if self.Players and ent_candidate:IsPlayer() then + --we don't want to grab ourselves + if (ent_candidate ~= self:GetRootPart():GetOwner()) or (self.AffectPlayerOwner and ent_candidate == self:GetPlayerOwner()) then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + elseif (self:GetPlayerOwner() ~= ent_candidate) then --if it's another player, good + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + end + elseif self.PhysicsProps and (physics_point_ent_classes[ent_candidate:GetClass()] or string.find(ent_candidate:GetClass(),"item_") or string.find(ent_candidate:GetClass(),"ammo_")) then chosen_ent = ent_candidate table.insert(ents_candidates, ent_candidate) - elseif (self:GetPlayerOwner() ~= ent_candidate) then --if it's another player, good + elseif self.NPC and (ent_candidate:IsNPC() or ent_candidate.IsDrGEntity or ent_candidate.IsVJBaseSNPC) then chosen_ent = ent_candidate table.insert(ents_candidates, ent_candidate) end - elseif self.PhysicsProps and (ent_candidate:GetClass() == "prop_physics" or ent_candidate:GetClass() == "prop_ragdoll") then - chosen_ent = ent_candidate - table.insert(ents_candidates, ent_candidate) - elseif self.NPC and ent_candidate:IsNPC() then - chosen_ent = ent_candidate - table.insert(ents_candidates, ent_candidate) end end end @@ -293,7 +429,6 @@ function PART:DecideTarget() --sort for the closest for i,ent_candidate in ipairs(ents_candidates) do - --print("trying", ent_candidate, ent_candidate:GetClass(), (ent_candidate:GetPos()):Distance( self:GetWorldPosition()), " from part") local test_distance = (ent_candidate:GetPos()):Distance( self:GetWorldPosition()) if (test_distance < closest_distance) then closest_distance = test_distance @@ -303,31 +438,40 @@ function PART:DecideTarget() if chosen_ent ~= nil then self.target_ent = chosen_ent - print("selected ", chosen_ent, "dist ", (chosen_ent:GetPos()):Distance( self:GetWorldPosition() )) - valid_ent = true + if LocalPlayer() == self:GetPlayerOwner() then + print("selected ", chosen_ent, "dist ", (chosen_ent:GetPos()):Distance( self:GetWorldPosition() )) + end + self.valid_ent = true else self.target_ent = nil - valid_ent = false + self.valid_ent = false end end + + function PART:CheckEntValidity() if self.target_ent == nil then - valid_ent = false + self.valid_ent = false elseif self.target_ent:EntIndex() == 0 then - valid_ent = false - elseif self.target_ent:IsValid() then - valid_ent = true + self.valid_ent = false + elseif IsValid(self.target_ent) then + self.valid_ent = true + end + if self.target_ent ~= nil then + if self.target_ent.IsGrabbedID and self.target_ent.IsGrabbedID ~= self.UniqueID then self.valid_ent = false end end + if not self.valid_ent then self.target_ent = nil end + --print("ent check:",self.valid_ent) end function PART:CalculateRelativeOffset() - if self.target_ent == nil then self.relative_transform_matrix = Matrix() return end + if self.target_ent == nil or not IsValid(self.target_ent) then self.relative_transform_matrix = Matrix() return end self.relative_transform_matrix = Matrix() self.relative_transform_matrix:Rotate(self.target_ent:GetAngles() - self:GetWorldAngles()) self.relative_transform_matrix:Translate(self.target_ent:GetPos() - self:GetWorldPosition()) - print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles()) + --print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles()) end BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/movement.lua b/lua/pac3/core/client/parts/movement.lua index e5fb4de5e..94273c9d5 100644 --- a/lua/pac3/core/client/parts/movement.lua +++ b/lua/pac3/core/client/parts/movement.lua @@ -16,7 +16,8 @@ local function ADD(PART, name, default, ...) PART["Set" .. name] = function(self, val) self[name] = val - local ply = self:GetRootPart():GetOwner() + local ply = self:GetPlayerOwner() + --if ply:GetClass() == "viewmodel" then ply = self:GetRootPart():GetOwner() end if ply == pac.LocalPlayer then @@ -44,6 +45,8 @@ BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") ADD(PART, "Noclip", false) ADD(PART, "Gravity", Vector(0, 0, -600)) + ADD(PART, "Mass", 85) + BUILDER:GetSet("PreserveInFirstPerson", false, {description = "keeps the movement modification active in first person"}) BUILDER:SetPropertyGroup("movement") ADD(PART, "SprintSpeed", 400) @@ -60,7 +63,10 @@ BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("air") ADD(PART, "AllowZVelocity", false) ADD(PART, "AirFriction", 0.01, {editor_clamp = {0, 1}, editor_sensitivity = 0.1}) - ADD(PART, "MaxAirSpeed", 1) + ADD(PART, "HorizontalAirFrictionMultiplier", 1, {editor_clamp = {0, 1}, editor_sensitivity = 0.1}) + ADD(PART, "MaxAirSpeed", 750) + ADD(PART, "StrafingStrengthMultiplier", 1) + BUILDER:SetPropertyGroup("view angles") ADD(PART, "ReversePitch", false) @@ -94,9 +100,10 @@ function PART:GetNiceName() end function PART:OnShow() - local ent = self:GetRootPart():GetOwner() - - if ent:IsValid() then + local ent = self:GetPlayerOwner() + + if ent:IsValid() then + if ent:GetClass() == "viewmodel" then ent = self:GetPlayerOwner() end ent.last_movement_part = self:GetUniqueID() for i,v in ipairs(update_these) do v(self) @@ -105,12 +112,24 @@ function PART:OnShow() end function PART:OnHide() - local ent = self:GetRootPart():GetOwner() - + local ent = self:GetRootPart():GetOwner() or self:GetOwner() or self:GetPlayerOwner() + if not IsValid(ent) then return end + if ent:GetClass() == "viewmodel" then ent = self:GetPlayerOwner() end + if not self:IsHidden() and self.PreserveInFirstPerson then return end if ent == pac.LocalPlayer and ent.last_movement_part == self:GetUniqueID() then net.Start("pac_modify_movement", true) net.WriteString("disable") net.SendToServer() + ent.pac_movement = nil + end +end + +function PART:OnRemove() + local ent = self:GetRootPart():GetOwner() + if ent == pac.LocalPlayer then + net.Start("pac_modify_movement", true) + net.WriteString("disable") + net.SendToServer() ent.pac_movement = nil end diff --git a/lua/pac3/core/client/parts/physics.lua b/lua/pac3/core/client/parts/physics.lua index cc616301e..3cc8fb9ab 100644 --- a/lua/pac3/core/client/parts/physics.lua +++ b/lua/pac3/core/client/parts/physics.lua @@ -1,3 +1,15 @@ +local physprop_enums = {} +local physprop_indices = {} +for i=0,500,1 do + local name = util.GetSurfacePropName(i) + if name ~= "" then + physprop_enums[name] = name + physprop_indices[name] = i + end +end + + + local BUILDER, PART = pac.PartTemplate("base") PART.ThinkTime = 0 @@ -7,25 +19,44 @@ PART.Group = 'model' PART.Icon = 'icon16/shape_handles.png' BUILDER:StartStorableVars() - BUILDER:GetSet("Box", true) - BUILDER:GetSet("Radius", 1) - BUILDER:GetSet("SelfCollision", false) - BUILDER:GetSet("Gravity", true) - BUILDER:GetSet("Collisions", true) - BUILDER:GetSet("Mass", 100) - - BUILDER:GetSet("Follow", false) - BUILDER:GetSet("SecondsToArrive", 0.1) - - BUILDER:GetSet("MaxSpeed", 10000) - BUILDER:GetSet("MaxAngular", 3600) - - BUILDER:GetSet("MaxSpeedDamp", 1000) - BUILDER:GetSet("MaxAngularDamp", 1000) - BUILDER:GetSet("DampFactor", 1) - - BUILDER:GetSet("ConstrainSphere", 0) + BUILDER:SetPropertyGroup("Behavior") + :GetSet("SelfCollision", false) + :GetSet("Gravity", true) + :GetSet("Collisions", true) + :GetSet("ConstrainSphere", 0) + :GetSet("Pushable", false, {description = "Whether the physics object should be pushed back by nearby players and props within its radius."}) + :GetSet("ThinkDelay", 1) + + BUILDER:SetPropertyGroup("Follow") + :GetSet("Follow", false, {description = "Whether the physics object should follow via SetPos. But it might clip in the world! seconds to arrive will be used for deciding the speed"}) + :GetSet("PushFollow", false, {description = "Whether the physics object should try to follow via AddVelocity, to prevent phasing through walls. But it might get stuck in a corner!\n".. + "seconds to arrive, along with the extra distance if it's beyond the constrain sphere, will be used for deciding the speed"}) + :GetSet("SecondsToArrive", 0.1) + :GetSet("MaxSpeed", 10000) + :GetSet("MaxAngular", 3600) + :GetSet("MaxSpeedDamp", 1000) + :GetSet("MaxAngularDamp", 1000) + :GetSet("DampFactor", 1) + BUILDER:SetPropertyGroup("Speeds") + + :GetSet("ConstantVelocity", Vector(0,0,0)) + + BUILDER:SetPropertyGroup("Shape") + :GetSet("BoxScale",Vector(1,1,1)) + :GetSet("Box", true) + :GetSet("Radius", 1) + :GetSet("SurfaceProperties", "default", {enums = physprop_enums}) + :GetSet("Preview", false) + :GetSet("Mass", 100) + + BUILDER:SetPropertyGroup("InitialVelocity") + :GetSet("AddOwnerSpeed", false) + :GetSet("InitialVelocityVector", Vector(0,0,0)) + :GetSetPart("InitialVelocityPart") + :GetSet("OverrideInitialPosition", false, {description = "Whether the initial velocity part should be used as an initial position, otherwise it'll just be for the initial velocity's angle"}) + + BUILDER:EndStorableVars() local function IsInvalidParent(self) @@ -55,6 +86,38 @@ function PART:SetMass(n) end end +function PART:MeshDraw() + if not IsValid(self.phys) then return end + + local mesh = (self.phys):GetMesh() + local drawmesh = Mesh() + + if mesh == nil or not self.Box then + render.DrawWireframeSphere( self.phys:GetPos(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + else + drawmesh:BuildFromTriangles(mesh) + + render.SetMaterial( Material( "models/wireframe" ) ) + local mat = Matrix() + mat:Translate(self.phys:GetPos()) + mat:Rotate(self.phys:GetAngles()) + cam.PushModelMatrix( mat ) + drawmesh:Draw() + cam.PopModelMatrix() + end +end + +function PART:SetPreview(b) + self.Preview = b + if self.Preview then + hook.Add("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID, function() + self:MeshDraw() + end) + else + hook.Remove("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID) + end +end + function PART:SetRadius(n) self.Radius = n @@ -67,9 +130,9 @@ function PART:SetRadius(n) ent:SetNoDraw(false) if self.Box then - ent:PhysicsInitBox(Vector(1,1,1) * -n, Vector(1,1,1) * n) + ent:PhysicsInitBox(self.BoxScale * -n, self.BoxScale * n, self.SurfaceProperties) else - ent:PhysicsInitSphere(n) + ent:PhysicsInitSphere(n, self.SurfaceProperties) end self.phys = ent:GetPhysicsObject() @@ -79,6 +142,20 @@ function PART:SetRadius(n) end end +function PART:SetSurfaceProperties(str) + self.SurfaceProperties = str + self:SetRadius(self.Radius) --refresh the physics +end + +function PART:GetSurfacePropsTable() --to view info over in the properties + return util.GetSurfaceData(physprop_indices[self.SurfaceProperties]) +end + +function PART:SetBoxScale(vec) + self.BoxScale = vec + self:SetRadius(self.Radius) --refresh the physics +end + function PART:SetGravity(b) self.Gravity = b @@ -121,25 +198,34 @@ function PART:OnThink() if phys:IsValid() then phys:Wake() - if self.Follow then - params.pos = self.Parent:GetWorldPosition() - params.angle = self.Parent:GetWorldAngles() - - params.secondstoarrive = math.max(self.SecondsToArrive, 0.0001) - params.maxangular = self.MaxAngular - params.maxangulardamp = self.MaxAngularDamp - params.maxspeed = self.MaxSpeed - params.maxspeeddamp = self.MaxSpeedDamp - params.dampfactor = self.DampFactor + if self.Follow or self.PushFollow then + if not self.PushFollow then + params.pos = self.Parent:GetWorldPosition() + params.angle = self.Parent:GetWorldAngles() - params.teleportdistance = 0 - - phys:ComputeShadowControl(params) + params.secondstoarrive = math.max(self.SecondsToArrive, 0.0001) + params.maxangular = self.MaxAngular + params.maxangulardamp = self.MaxAngularDamp + params.maxspeed = self.MaxSpeed + params.maxspeeddamp = self.MaxSpeedDamp + params.dampfactor = self.DampFactor + params.teleportdistance = 0 + phys:ComputeShadowControl(params) + end + -- this is nicer i think if self.ConstrainSphere ~= 0 and phys:GetPos():Distance(self.Parent:GetWorldPosition()) > self.ConstrainSphere then - phys:SetPos(self.Parent:GetWorldPosition() + (self.Parent:GetWorldPosition() - phys:GetPos()):GetNormalized() * -self.ConstrainSphere) + if not self.PushFollow then + phys:SetPos(self.Parent:GetWorldPosition() + (self.Parent:GetWorldPosition() - phys:GetPos()):GetNormalized() * -self.ConstrainSphere) + --new push mode + else + local vec = (self.Parent:GetWorldPosition() - phys:GetPos()) + local current_dist = vec:Length() + local extra_dist = current_dist - self.ConstrainSphere + phys:AddVelocity(0.5 * vec:GetNormalized() * extra_dist / math.Clamp(self.SecondsToArrive,0.05,10)) + end end else if self.ConstrainSphere ~= 0 then @@ -162,13 +248,25 @@ function PART:OnUnParent(part) timer.Simple(0, function() self:Disable() end) end - function PART:OnShow() timer.Simple(0, function() self:Enable() end) end function PART:OnHide() + if not self.Hide and not self:IsHidden() then + timer.Simple(0.4, function() + self:GetRootPart():OnShow() + self.Parent:OnShow() + for _,part in pairs(self:GetParent():GetChildrenList()) do + part:OnShow() + + end + end) + return + end timer.Simple(0, function() self:Disable() end) + + hook.Remove("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID) end function PART:Enable() @@ -190,14 +288,60 @@ function PART:Enable() end self.disabled = false + + if IsValid(self.InitialVelocityPart) then + if self.InitialVelocityPart.GetWorldPosition then + local local_vec, local_ang = self.InitialVelocityPart:GetDrawPosition() + local local_vec2 = self.InitialVelocityVector.x * local_ang:Forward() + + self.InitialVelocityVector.y * local_ang:Right() + + self.InitialVelocityVector.z * local_ang:Up() + self.phys:AddVelocity(local_vec2) + if self.OverrideInitialPosition then + self.phys:SetPos(local_vec) + end + else + self.phys:AddVelocity(self.InitialVelocityVector) + end + else + self.phys:AddVelocity(self.InitialVelocityVector) + end + + if self.AddOwnerSpeed then + self.phys:AddVelocity(self:GetRootPart():GetOwner():GetVelocity()) + end + + timer.Simple(self.ThinkDelay, function() hook.Add("Tick", "pac_phys_repulsionthink"..self.UniqueID, function() + if not IsValid(self.phys) then hook.Remove("Tick", "pac_phys_repulsionthink"..self.UniqueID) return end + self.phys:AddVelocity(self.ConstantVelocity * RealFrameTime()) + + if self.Pushable then + local pushvec = Vector(0,0,0) + local pos = self.phys:GetPos() + local ents_tbl = ents.FindInSphere(pos, self.Radius) + local valid_phys_pushers = 0 + for i,ent in pairs(ents_tbl) do + if ent.GetPhysicsObject or ent:IsPlayer() then + if ent:IsPlayer() or ent:GetClass() == "prop_physics" or ent:GetClass() == "prop_ragdoll" then + valid_phys_pushers = valid_phys_pushers + 1 + pushvec = pushvec + (pos - ent:GetPos()):GetNormalized() * 20 + end + end + end + if valid_phys_pushers > 0 then self.phys:AddVelocity(pushvec / valid_phys_pushers) end + end + + + end) end) end function PART:Disable() + hook.Remove("Tick", "pac_phys_repulsionthink"..self.UniqueID) if IsInvalidParent(self) then return end local part = self:GetParent() local ent = part:GetOwner() + if ent:IsValid() then -- SetNoDraw does not care of validity but PhysicsInit does? ent:SetNoDraw(true) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 073b0c458..3c6601339 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -1,5 +1,17 @@ +local physprop_enums = {} +local physprop_indices = {} +for i=0,500,1 do + local name = util.GetSurfacePropName(i) + if name ~= "" then + physprop_enums[name] = name + physprop_indices[name] = i + end +end + language.Add("pac_projectile", "Projectile") + + local BUILDER, PART = pac.PartTemplate("base_movable") PART.ClassName = "projectile" @@ -7,89 +19,102 @@ PART.Group = 'advanced' PART.Icon = 'icon16/bomb.png' BUILDER:StartStorableVars() - BUILDER:GetSet("Speed", 1) - BUILDER:GetSet("AddOwnerSpeed", false) - BUILDER:GetSet("Damping", 0) - BUILDER:GetSet("Gravity", true) - BUILDER:GetSet("Collisions", true) - BUILDER:GetSet("Sphere", false) - BUILDER:GetSet("Radius", 1) - BUILDER:GetSet("DamageRadius", 50) - BUILDER:GetSet("LifeTime", 5) - BUILDER:GetSet("AimDir", false) - BUILDER:GetSet("Sticky", false) - BUILDER:GetSet("Bounce", 0) - BUILDER:GetSet("BulletImpact", false) - BUILDER:GetSet("Damage", 0) - BUILDER:GetSet("DamageType", "generic", {enums = { - generic = 0, --generic damage - crush = 1, --caused by physics interaction - bullet = 2, --bullet damage - slash = 4, --sharp objects, such as manhacks or other npcs attacks - burn = 8, --damage from fire - vehicle = 16, --hit by a vehicle - fall = 32, --fall damage - blast = 64, --explosion damage - club = 128, --crowbar damage - shock = 256, --electrical damage, shows smoke at the damage position - sonic = 512, --sonic damage,used by the gargantua and houndeye npcs - energybeam = 1024, --laser - nevergib = 4096, --don't create gibs - alwaysgib = 8192, --always create gibs - drown = 16384, --drown damage - paralyze = 32768, --same as dmg_poison - nervegas = 65536, --neurotoxin damage - poison = 131072, --poison damage - acid = 1048576, -- - airboat = 33554432, --airboat gun damage - blast_surface = 134217728, --this won't hurt the player underwater - buckshot = 536870912, --the pellets fired from a shotgun - direct = 268435456, -- - dissolve = 67108864, --forces the entity to dissolve on death - drownrecover = 524288, --damage applied to the player to restore health after drowning - physgun = 8388608, --damage done by the gravity gun - plasma = 16777216, -- - prevent_physics_force = 2048, -- - radiation = 262144, --radiation - removenoragdoll = 4194304, --don't create a ragdoll on death - slowburn = 2097152, -- - - explosion = -1, -- util.BlastDamage - fire = -1, -- ent:Ignite(5) - - -- env_entity_dissolver - dissolve_energy = 0, - dissolve_heavy_electrical = 1, - dissolve_light_electrical = 2, - dissolve_core_effect = 3, - - heal = -1, - armor = -1, - } - }) - BUILDER:GetSet("Spread", 0) - BUILDER:GetSet("NumberProjectiles", 1) - BUILDER:GetSet("Delay", 0) - BUILDER:GetSet("Maximum", 0) - BUILDER:GetSet("Mass", 100) - BUILDER:GetSet("Attract", 0) - BUILDER:GetSet("AttractMode", "closest_to_projectile", {enums = { - hitpos = "hitpos", - hitpos_radius = "hitpos_radius", - closest_to_projectile = "closest_to_projectile", - closest_to_hitpos = "closest_to_hitpos", - }}) - BUILDER:GetSet("AttractRadius", 200) - BUILDER:GetSetPart("OutfitPart") - BUILDER:GetSet("Physical", false) - BUILDER:GetSet("CollideWithOwner", false) - BUILDER:GetSet("CollideWithSelf", false) - BUILDER:GetSet("RemoveOnCollide", false) + BUILDER:SetPropertyGroup("Firing") + BUILDER:GetSet("Speed", 1) + BUILDER:GetSet("AddOwnerSpeed", false) + BUILDER:GetSet("Spread", 0) + BUILDER:GetSet("NumberProjectiles", 1) + BUILDER:GetSet("Delay", 0) + BUILDER:GetSet("Maximum", 0) + BUILDER:GetSet("RandomAngleVelocity", Vector(0,0,0)) + BUILDER:GetSet("LocalAngleVelocity", Vector(0,0,0)) + BUILDER:SetPropertyGroup("Physics") + BUILDER:GetSet("Mass", 100) + BUILDER:GetSet("SurfaceProperties", "default", {enums = physprop_enums}) + BUILDER:GetSet("RescalePhysMesh", false, {description = "experimental! tries to scale the collide mesh by the radius! Stay within small numbers! 1 radius should be associated with a full-size model"}) + BUILDER:GetSet("OverridePhysMesh", false, {description = "experimental! tries to redefine the projectile's model to change the physics mesh"}) + BUILDER:GetSet("FallbackSurfpropModel", "models/props_junk/PopCan01a.mdl") + BUILDER:GetSet("Damping", 0) + BUILDER:GetSet("Gravity", true) + BUILDER:GetSet("Collisions", true) + BUILDER:GetSet("Sphere", false) + BUILDER:GetSet("Radius", 1, {editor_panel = "projectile_radii"}) + BUILDER:GetSet("Bounce", 0) + BUILDER:GetSet("Sticky", false) + BUILDER:GetSet("CollideWithOwner", false) + BUILDER:GetSet("CollideWithSelf", false) + BUILDER:SetPropertyGroup("Appearance") + BUILDER:GetSetPart("OutfitPart") + BUILDER:GetSet("RemoveOnHide", false) + BUILDER:GetSet("AimDir", false) + BUILDER:GetSet("DrawShadow", true) + BUILDER:SetPropertyGroup("ActiveBehavior") + BUILDER:GetSet("Physical", false) + BUILDER:GetSet("DamageRadius", 50, {editor_panel = "projectile_radii"}) + BUILDER:GetSet("LifeTime", 5) + BUILDER:GetSet("RemoveOnCollide", false) + BUILDER:GetSet("BulletImpact", false) + BUILDER:GetSet("Damage", 0) + BUILDER:GetSet("DamageType", "generic", {enums = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamage + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + } + }) + BUILDER:GetSet("Attract", 0, {editor_friendly = "attract force"}) + BUILDER:GetSet("AttractMode", "closest_to_projectile", {enums = { + hitpos = "hitpos", + hitpos_radius = "hitpos_radius", + closest_to_projectile = "closest_to_projectile", + closest_to_hitpos = "closest_to_hitpos", + }}) + BUILDER:GetSet("AttractRadius", 200) BUILDER:EndStorableVars() PART.Translucent = false + function PART:OnShow(from_rendering) if not from_rendering then -- TODO: @@ -113,7 +138,12 @@ function PART:OnShow(from_rendering) end end -function PART:AttachToEntity(ent) +function PART:GetSurfacePropsTable() --to view info over in the properties + return util.GetSurfaceData(physprop_indices[self.SurfaceProperties]) +end + +local_projectiles = {} +function PART:AttachToEntity(ent, physical) if not self.OutfitPart:IsValid() then return false end ent.pac_draw_distance = 0 @@ -146,7 +176,8 @@ function PART:AttachToEntity(ent) end ent.pac_projectile_part = group - ent.pac_projectile = self + ent.pac_projectile = self --that's just the launcher though + if not physical then local_projectiles[group] = ent end return true end @@ -198,7 +229,7 @@ function PART:Shoot(pos, ang, multi_projectile_count) if not self:IsValid() then return end - local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") + local ent = pac.CreateEntity(self.FallbackSurfpropModel) if not ent:IsValid() then return end local idx = table.insert(self.projectiles, ent) @@ -240,9 +271,9 @@ function PART:Shoot(pos, ang, multi_projectile_count) ent:SetCollisionGroup(COLLISION_GROUP_PROJECTILE) if self.Sphere then - ent:PhysicsInitSphere(math.Clamp(self.Radius, 1, 30)) + ent:PhysicsInitSphere(math.Clamp(self.Radius, 1, 500), self.SurfaceProperties) else - ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 30), Vector(1,1,1) * math.Clamp(self.Radius, 1, 30)) + ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 500), Vector(1,1,1) * math.Clamp(self.Radius, 1, 500), self.SurfaceProperties) end ent.RenderOverride = function() @@ -276,7 +307,8 @@ function PART:Shoot(pos, ang, multi_projectile_count) ent:SetCollisionGroup(COLLISION_GROUP_PROJECTILE) - if self:AttachToEntity(ent) then + if self:AttachToEntity(ent, false) then + timer.Simple(math.Clamp(self.LifeTime, 0, 10), function() if ent:IsValid() then if ent.pac_projectile_part and ent.pac_projectile_part:IsValid() then @@ -288,7 +320,10 @@ function PART:Shoot(pos, ang, multi_projectile_count) end) end end) + end + + end if self.Delay == 0 then @@ -310,6 +345,23 @@ function PART:OnRemove() self.projectiles = {} end end + +pac.AddHook("Think", "pac_cleanup_CS_projectiles", function() + for rootpart,ent in pairs(local_projectiles) do + if ent.pac_projectile_part == rootpart then + local tbl = ent.pac_projectile_part:GetChildren() + local partchild = tbl[next(tbl)] --ent.pac_projectile_part is the root group, but outfit part is the first child + + if IsValid(partchild) then + if partchild:IsHidden() then + SafeRemoveEntity(ent) + end + end + end + end + +end) + --[[ function PART:OnHide() if self.RemoveOnHide then @@ -317,10 +369,25 @@ function PART:OnHide() end end ]] + +--[[if ent.pac_projectile_part then + local partchild = next(ent.pac_projectile_part:GetChildren()) --ent.pac_projectile_part is the root group, but outfit part is the first child + if IsValid(part) then + if partchild:IsHidden() then + if ent.pac_projectile.RemoveOnHide then + net.Start("pac_projectile_remove") + net.WriteInt(data.ent_id) + net.SendToServer() + end + end + end +end]] + do -- physical local Entity = Entity local projectiles = {} pac.AddHook("Think", "pac_projectile", function() + for key, data in pairs(projectiles) do if not data.ply:IsValid() then projectiles[key] = nil @@ -332,10 +399,11 @@ do -- physical if ent:IsValid() and ent:GetClass() == "pac_projectile" then local part = pac.GetPartFromUniqueID(pac.Hash(data.ply), data.partuid) if part:IsValid() and part:GetPlayerOwner() == data.ply then - part:AttachToEntity(ent) + part:AttachToEntity(ent, true) end projectiles[key] = nil end + ::CONTINUE:: end end) @@ -344,10 +412,30 @@ do -- physical local ply = net.ReadEntity() local ent_id = net.ReadInt(16) local partuid = net.ReadString() + local surfprop = net.ReadString() + if ply:IsValid() then table.insert(projectiles, {ply = ply, ent_id = ent_id, partuid = partuid}) end + + local ent = Entity(ent_id) + + ent.Think = function() + if ent.pac_projectile_part then + local tbl = ent.pac_projectile_part:GetChildren() + local partchild = tbl[next(tbl)] --ent.pac_projectile_part is the root group, but outfit part is the first child + if partchild:IsHidden() then + if ent.pac_projectile.RemoveOnHide and not ent.markedforremove then + ent.markedforremove = true + net.Start("pac_projectile_remove") + net.WriteInt(ent_id, 16) + net.SendToServer() + + end + end + end + end end) end diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 778435c18..2a3bc1b83 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -219,6 +219,20 @@ PART.Inputs.property = function(self, property_name, field) return 0 end +PART.Inputs.polynomial = function(self, x, ...) + x = x or 1 + local total = 0 + local args = { ... } + + pow = 0 + for _, coefficient in ipairs(args) do + total = total + coefficient*math.pow(x, pow) + pow = pow + 1 + end + return total + +end + PART.Inputs.owner_position = function(self) local owner = get_owner(self) @@ -307,7 +321,7 @@ PART.Inputs.random_once = function(self, seed, min, max) self.rand_id = self.rand_id or {} if seed then self.rand_id[seed] = self.rand_id[seed] or min + math.random()*(max-min) - else + else self.rand = self.rand or min + math.random()*(max-min) end @@ -941,6 +955,9 @@ net.Receive("pac_proxy", function() if ply:IsValid() then ply.pac_proxy_events = ply.pac_proxy_events or {} ply.pac_proxy_events[str] = {name = str, x = x, y = y, z = z} + if LocalPlayer() == ply then + pac.Message("pac_proxy -> command(\""..str.."\") is " .. x .. "," .. y .. "," .. z) + end end end) @@ -1094,7 +1111,21 @@ end function PART:OnThink() local part = self:GetTarget() if not part:IsValid() then return end - if part.ClassName == 'woohoo' then return end + if part.ClassName == 'woohoo' then --why a part hardcode exclusion?? + --ok fine I guess it's because it's super expensive, but at least we can be selective about it, the other parameters are safe + if self.VariableName == "Resolution" or self.VariableName == "BlurFiltering" and self.touched then + return + end + end + + --foolproofing: scream at the user if they didn't set a variable name + if self == pace.current_part then self.touched = true end + if self ~= pace.current_part and self.VariableName == "" and self.touched then + self:AttachEditorPopup("You forgot to set a variable name! The proxy won't work until it knows where to send the math!", true) + pace.FlashNotification("An edited proxy still has no variable name! The proxy won't work until it knows where to send the math!") + self:SetWarning("You forgot to set a variable name! The proxy won't work until it knows where to send the math!") + self.touched = false + elseif self.VariableName ~= "" then self:SetWarning() end self:CalcVelocity() diff --git a/lua/pac3/core/client/parts/sound.lua b/lua/pac3/core/client/parts/sound.lua index da1f90d84..51afcb415 100644 --- a/lua/pac3/core/client/parts/sound.lua +++ b/lua/pac3/core/client/parts/sound.lua @@ -11,6 +11,7 @@ PART.Group = 'effects' BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") BUILDER:GetSet("Path", "", {editor_panel = "sound"}) + BUILDER:GetSet("AllPaths", "", {hide_in_editor = true}) BUILDER:GetSet("Volume", 1, {editor_sensitivity = 0.25}) BUILDER:GetSet("Pitch", 1, {editor_sensitivity = 0.125}) BUILDER:GetSet("Radius", 1500) @@ -166,6 +167,15 @@ function PART:OnThink() end function PART:SetPath(path) + if #path > 1024 then + self:AttachEditorPopup("This part has more sounds than the 1024-letter limit! Please do not touch the path field now!") + self:SetInfo("This part has more sounds than the 1024-letter limit! Please do not touch the path field now!") + if self.Name == "" then + self:SetName("big sound list") + pace.RefreshTree() + end + end + self.seq_index = 1 self.Path = path @@ -180,10 +190,13 @@ function PART:SetPath(path) if min and max then for i = min, max do table.insert(paths, (path:gsub("%[.-%]", i))) + self.AllPaths = self.AllPaths .. ";" .. path end else table.insert(paths, path) + self.AllPaths = self.AllPaths .. ";" .. path end + end for _, stream in pairs(self.streams) do @@ -245,10 +258,15 @@ function PART:SetPath(path) end end self.paths = paths + end PART.last_stream = NULL +function PART:UpdateSoundsFromAll() + self:SetPath(self.AllPaths) +end + function PART:PlaySound(_, additiveVolumeFraction) --PrintTable(self.streams) additiveVolumeFraction = additiveVolumeFraction or 0 @@ -271,7 +289,7 @@ function PART:PlaySound(_, additiveVolumeFraction) if self.streams[snd]:IsValid() then stream = self.streams[snd] - print(snd,self.seq_index) + --print(snd,self.seq_index) end self.seq_index = self.seq_index + self.SequentialStep if self.seq_index > #self.paths then @@ -333,4 +351,4 @@ function PART:OnRemove() end end -BUILDER:Register() +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index 2bfeb7389..e03c0da58 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -13,6 +13,73 @@ local Color = Color local BUILDER, PART = pac.PartTemplate("base_drawable") +local default_fonts = { + "BudgetLabel", + "CenterPrintText", + "ChatFont", + "ClientTitleFont", + "CloseCaption_Bold", + "CloseCaption_BoldItalic", + "CloseCaption_Italic", + "CloseCaption_Normal", + "CreditsLogo", + "CreditsOutroLogos", + "CreditsOutroText", + "CreditsText", + "Crosshairs", + "DebugFixed", + "DebugFixedSmall", + "DebugOverlay", + "Default", + "DefaultFixed", + "DefaultFixedDropShadow", + "DefaultSmall", + "DefaultUnderline", + "DefaultVerySmall", + "HDRDemoText", + "HL2MPTypeDeath", + "HudDefault", + "HudHintTextLarge", + "HudHintTextSmall", + "HudNumbers", + "HudNumbersGlow", + "HudNumbersSmall", + "HudSelectionNumbers", + "HudSelectionText", + "Marlett", + "QuickInfo", + "TargetID", + "TargetIDSmall", + "Trebuchet18", + "Trebuchet24", + "WeaponIcons", + "WeaponIconsSelected", + "WeaponIconsSmall", + "DermaDefault", + "DermaDefaultBold", + "DermaLarge", + "GModNotify", + "ScoreboardDefault", + "ScoreboardDefaultTitle", + "GModToolName", + "GModToolSubtitle", + "GModToolHelp", + "GModToolScreen", + "ContentHeader", + "GModWorldtip", + "ContentHeader", + "DefaultBold", + "TabLarge", + "Trebuchet22", + "TraitorState", + "TimeLeft", + "HealthAmmo", + "cool_small", + "cool_large", + "treb_small" +} + + PART.ClassName = "text" PART.Group = 'effects' PART.Icon = 'icon16/text_align_center.png' @@ -22,17 +89,22 @@ BUILDER:StartStorableVars() :PropertyOrder("Name") :PropertyOrder("Hide") :GetSet("Text", "") - :GetSet("Font", "default") + :GetSet("Font", "default", {enums = default_fonts}) :GetSet("Size", 1, {editor_sensitivity = 0.25}) - - + :GetSet("DrawMode", "DrawTextOutlined", {enums = { + ["draw.SimpleTextOutlined"] = "DrawTextOutlined", + ["surface.DrawText"] = "SurfaceText" + }}) + :SetPropertyGroup("text layout") :GetSet("HorizontalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Left"] = "0", ["Center"] = "1", ["Right"] = "2"}}) :GetSet("VerticalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Center"] = "1", ["Top"] = "3", ["Bottom"] = "4"}}) :GetSet("ConcatenateTextAndOverrideValue",false,{editor_friendly = "CombinedText"}) + :GetSet("TextPosition","Prefix", {enums = {["Prefix"] = "Prefix", ["Postfix"] = "Postfix"}},{editor_friendly = "ConcatenateMode"}) :SetPropertyGroup("data source") :GetSet("TextOverride", "Text", {enums = { + ["Proxy value (DynamicTextValue)"] = "Proxy", ["Text"] = "Text", ["Health"] = "Health", ["Maximum Health"] = "MaxHealth", @@ -41,13 +113,38 @@ BUILDER:StartStorableVars() ["Timerx"] = "Timerx", ["CurTime"] = "CurTime", ["RealTime"] = "RealTime", + ["Velocity"] = "Velocity", + ["Velocity Vector"] = "VelocityVector", + ["Position Vector"] = "PositionVector", + ["Owner Position Vector"] = "OwnerPositionVector", ["Clip current Ammo"] = "Ammo", ["Clip Size"] = "ClipSize", ["Ammo Reserve"] = "AmmoReserve", - ["Proxy value (Using DynamicTextValue)"] = "Proxy"}}) + ["Sequence Name"] = "SequenceName", + ["Weapon Name"] = "Weapon", + ["Vehicle Class"] = "VehicleClass", + ["Model Name"] = "ModelName", + ["Model Path"] = "ModelPath", + ["Player Name"] = "PlayerName", + ["Player SteamID"] = "SteamID", + ["Map"] = "Map", + ["Ground Surface"] = "GroundSurface", + ["Ground Entity Class"] = "GroundEntityClass", + ["Players"] = "Players", + ["Max Players"] = "MaxPlayers", + ["Difficulty"] = "GameDifficulty" + }}) :GetSet("DynamicTextValue", 0) + :GetSet("RoundingPosition", 2, {editor_onchange = function(self, num) + return math.Round(num,0) + end}) + :SetPropertyGroup("orientation") + :PropertyOrder("AimPartName") + :PropertyOrder("Bone") + :PropertyOrder("Position") :SetPropertyGroup("appearance") + BUILDER:GetSet("ForceAdditive",false, {description = "additive rendering for the surface.DrawText mode"}) BUILDER:GetSet("Outline", 0) BUILDER:GetSet("Color", Vector(255, 255, 255), {editor_panel = "color"}) BUILDER:GetSet("Alpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}}) @@ -58,6 +155,22 @@ BUILDER:StartStorableVars() return math.Clamp(num, 0, 1) end}) BUILDER:GetSet("Translucent", true) + :SetPropertyGroup("CustomFont") + :GetSet("CreateCustomFont",false, {description = "Tries to create a custom font.\nHeavily throttled as creating fonts is an expensive process.\nSupport is limited because of the fonts' supported features and the limits of Lua strings.\nFont names include those stored in your operating system. for example: Comic Sans MS, Ink Free"}) + :GetSet("CustomFont", "DermaDefault") + :GetSet("FontSize", 13) + :GetSet("FontWeight",500) + :GetSet("FontBlurSize",0) + :GetSet("FontScanLines",0) + :GetSet("FontAntialias",true) + :GetSet("FontUnderline",false) + :GetSet("FontItalic",false) + :GetSet("FontStrikeout",false) + :GetSet("FontSymbol",false) + :GetSet("FontRotary",false) + :GetSet("Shadow",false) + :GetSet("FontAdditive",false) + :GetSet("FontOutline",false) BUILDER:EndStorableVars() function PART:GetNiceName() @@ -101,22 +214,49 @@ function PART:SetOutlineAlpha(n) end function PART:SetFont(str) - if not pcall(surface_SetFont, str) then - pac.Message(Color(255,150,0),str.." Font not found! Reverting to DermaDefault!") - str = "DermaDefault" + self.UsedFont = str + if not self.CreateCustomFont then + if not pcall(surface_SetFont, str) then + if #self.Font > 20 then + + self.lastwarn = self.lastwarn or CurTime() + if self.lastwarn > CurTime() + 1 then + pac.Message(Color(255,150,0),str.." Font not found! Could be custom font, trying again in 4 seconds!") + self.lastwarn = CurTime() + end + timer.Simple(4, function() + if not pcall(surface_SetFont, str) then + pac.Message(Color(255,150,0),str.." Font still not found! Reverting to DermaDefault!") + str = "DermaDefault" + self.UsedFont = str + end + end) + else + timer.Simple(5, function() + if not pcall(surface_SetFont, str) then + pac.Message(Color(255,150,0),str.." Font still not found! Reverting to DermaDefault!") + str = "DermaDefault" + self.UsedFont = str + end + end) + end + end end - - self.Font = str + self.Font = self.UsedFont end - +local lastfontcreationtime = 0 function PART:OnDraw() local pos, ang = self:GetDrawPosition() + self:CheckFont() + if not pcall(surface_SetFont, self.UsedFont) then return end + + local DisplayText = self.Text or "" if self.TextOverride == "Text" then goto DRAW end - - if self.TextOverride == "Health"then DisplayText = self:GetPlayerOwner():Health() + DisplayText = "" + if self.TextOverride == "Health" then DisplayText = self:GetRootPart():GetOwner():Health() elseif self.TextOverride == "MaxHealth" then - DisplayText = self:GetPlayerOwner():GetMaxHealth() + DisplayText = self:GetRootPart():GetOwner():GetMaxHealth() elseif self.TextOverride == "Ammo" then DisplayText = self:GetPlayerOwner():GetActiveWeapon():Clip1() elseif self.TextOverride == "ClipSize" then @@ -128,33 +268,246 @@ function PART:OnDraw() elseif self.TextOverride == "MaxArmor" then DisplayText = self:GetPlayerOwner():GetMaxArmor() elseif self.TextOverride == "Timerx" then - DisplayText = ""..math.Round(CurTime() - self.time,2) + DisplayText = ""..math.Round(CurTime() - self.time,self.RoundingPosition) elseif self.TextOverride == "CurTime" then - DisplayText = ""..math.Round(CurTime(),2) + DisplayText = ""..math.Round(CurTime(),self.RoundingPosition) elseif self.TextOverride == "RealTime" then - DisplayText = ""..math.Round(RealTime(),2) + DisplayText = ""..math.Round(RealTime(),self.RoundingPosition) + elseif self.TextOverride == "Velocity" then + local ent = self:GetRootPart():GetOwner() + DisplayText = math.Round(ent:GetVelocity():Length(),2) + elseif self.TextOverride == "VelocityVector" then + local ent = self:GetOwner() or self:GetRootPart():GetOwner() + local vec = ent:GetVelocity() + DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")" + elseif self.TextOverride == "PositionVector" then + local vec = self:GetDrawPosition() + DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")" + elseif self.TextOverride == "OwnerPositionVector" then + local ent = self:GetRootPart():GetOwner() + local vec = ent:GetPos() + DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")" + elseif self.TextOverride == "SequenceName" then + DisplayText = self:GetRootPart():GetOwner():GetSequenceName(self:GetPlayerOwner():GetSequence()) + elseif self.TextOverride == "PlayerName" then + DisplayText = self:GetPlayerOwner():GetName() + elseif self.TextOverride == "SteamID" then + DisplayText = self:GetPlayerOwner():SteamID() + elseif self.TextOverride == "ModelName" then + local path = self:GetRootPart():GetOwner():GetModel() or "null" + path = string.Split(path, "/")[#string.Split(path, "/")] + path = string.gsub(path,".mdl","") + DisplayText = path + elseif self.TextOverride == "ModelPath" then + DisplayText = self:GetPlayerOwner():GetModel() + elseif self.TextOverride == "Map" then + DisplayText = game.GetMap() + elseif self.TextOverride == "GroundSurface" then + local trace = util.TraceLine( { + start = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, 30), + endpos = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, -60 ), + filter = function(ent) + if ent == self:GetRootPart():GetOwner() or ent == self:GetPlayerOwner() then return false else return true end + end + }) + if trace.Hit then + if trace.MatType == MAT_ANTLION then DisplayText = "Antlion" + elseif trace.MatType == MAT_BLOODYFLESH then DisplayText = "Bloody Flesh" + elseif trace.MatType == MAT_CONCRETE then DisplayText = "Concrete" + elseif trace.MatType == MAT_DIRT then DisplayText = "Dirt" + elseif trace.MatType == MAT_EGGSHELL then DisplayText = "Egg Shell" + elseif trace.MatType == MAT_FLESH then DisplayText = "Flesh" + elseif trace.MatType == MAT_GRATE then DisplayText = "Grate" + elseif trace.MatType == MAT_ALIENFLESH then DisplayText = "Alien Flesh" + elseif trace.MatType == MAT_CLIP then DisplayText = "Clip" + elseif trace.MatType == MAT_SNOW then DisplayText = "Snow" + elseif trace.MatType == MAT_PLASTIC then DisplayText = "Plastic" + elseif trace.MatType == MAT_METAL then DisplayText = "Metal" + elseif trace.MatType == MAT_SAND then DisplayText = "Sand" + elseif trace.MatType == MAT_FOLIAGE then DisplayText = "Foliage" + elseif trace.MatType == MAT_COMPUTER then DisplayText = "Computer" + elseif trace.MatType == MAT_SLOSH then DisplayText = "Slime" + elseif trace.MatType == MAT_TILE then DisplayText = "Tile" + elseif trace.MatType == MAT_GRASS then DisplayText = "Grass" + elseif trace.MatType == MAT_VENT then DisplayText = "Grass" + elseif trace.MatType == MAT_WOOD then DisplayText = "Wood" + elseif trace.MatType == MAT_DEFAULT then DisplayText = "Default" + elseif trace.MatType == MAT_GLASS then DisplayText = "Glass" + elseif trace.MatType == MAT_WARPSHIELD then DisplayText = "Warp Shield" + else DisplayText = "Other Surface" end + else DisplayText = "Air" end + elseif self.TextOverride == "GroundEntityClass" then + local trace = util.TraceLine( { + start = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, 30), + endpos = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, -60 ), + filter = function(ent) + if ent == self:GetRootPart():GetOwner() or ent == self:GetPlayerOwner() then return false else return true end + end + }) + if trace.Hit then + DisplayText = trace.Entity:GetClass() + end + elseif self.TextOverride == "GameDifficulty" then + local diff = game.GetSkillLevel() + if diff == 1 then DisplayText = "Easy" + elseif diff == 2 then DisplayText = "Normal" + elseif diff == 3 then DisplayText = "Hard" end + elseif self.TextOverride == "Players" then + DisplayText = #player.GetAll() + elseif self.TextOverride == "MaxPlayers" then + DisplayText = game.MaxPlayers() + elseif self.TextOverride == "Weapon" then + if IsValid(self:GetRootPart():GetOwner():GetActiveWeapon()) then + DisplayText = self:GetRootPart():GetOwner():GetActiveWeapon():GetClass() + else DisplayText = "unarmed" end + elseif self.TextOverride == "VehicleClass" then + if IsValid(self:GetPlayerOwner():GetVehicle()) then + DisplayText = self:GetPlayerOwner():GetVehicle():GetClass() + else DisplayText = "not driving" end elseif self.TextOverride == "Proxy" then - --print(type(self.DynamicTextValue)) - DisplayText = ""..math.Round(self.DynamicTextValue,2) + DisplayText = ""..math.Round(self.DynamicTextValue,self.RoundingPosition) end - - if self.ConcatenateTextAndOverrideValue then DisplayText = ""..self.Text..DisplayText end - + + if self.ConcatenateTextAndOverrideValue then + if self.TextPosition == "Prefix" then + DisplayText = ""..self.Text..DisplayText + elseif self.TextPosition == "Postfix" then + DisplayText = ""..DisplayText..self.Text + end + end + ::DRAW:: + if DisplayText ~= "" then - cam_Start3D(EyePos(), EyeAngles()) - cam_Start3D2D(pos, ang, self.Size) - local oldState = DisableClipping(true) + 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_SimpleTextOutlined(DisplayText, self.Font, 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(1) -- MATERIAL_CULLMODE_CW - draw_SimpleTextOutlined(DisplayText, self.Font, 0,0, self.ColorC, self.HorizontalTextAlign,self.VerticalTextAlign, self.Outline, self.OutlineColorC) - render_CullMode(0) -- MATERIAL_CULLMODE_CCW + draw.SimpleText(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() + DisableClipping(oldState) + cam_End3D2D() + cam_End3D() + elseif self.DrawMode == "SurfaceText" then + hook.Add("HUDPaint", "pac.DrawText"..self.UniqueID, function() + if not pcall(surface_SetFont, self.UsedFont) then return end + self:SetFont(self.UsedFont) + + surface.SetTextColor(self.Color.r, self.Color.g, self.Color.b) + + surface.SetFont(self.UsedFont) + local pos2d = self:GetDrawPosition():ToScreen() + local w, h = surface.GetTextSize(DisplayText) + + 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 + + surface.SetTextPos(pos2d.x, pos2d.y) + local dist = (EyePos() - self:GetWorldPosition()):Length() + local fadestartdist = 200 + local fadeenddist = 1000 + if fadestartdist == 0 then fadestartdist = 0.1 end + if fadeenddist == 0 then fadeenddist = 0.1 end + + if fadestartdist > fadeenddist then + local temp = fadestartdist + fadestartdist = fadeenddist + fadeenddist = temp + end + + --print("dist:",dist,"start",fadestartdist,"end",fadeenddist, "factor = ", math.pow(math.Clamp((fadeenddist - dist)/fadestartdist,0,1),1)) + + if dist < fadeenddist then + if dist < fadestartdist then + surface.DrawText(DisplayText, self.ForceAdditive) + else + local fade = math.pow(math.Clamp(1 - (dist-fadestartdist)/fadeenddist,0,1),3) + surface.SetTextColor(self.Color.r * fade, self.Color.g * fade, self.Color.b * fade) + surface.DrawText(DisplayText, true) + end + end + end) + end + if self.DrawMode ~= "SurfaceText" then + hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) + end + else hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) end +end + +function PART:Initialize() + self:TryCreateFont() +end + +function PART:CheckFont() + if self.CreateCustomFont then + lastfontcreationtime = lastfontcreationtime or 0 + if lastfontcreationtime + 3 <= CurTime() then + self:TryCreateFont() + end + else + self:SetFont(self.Font) + end + +end + +function PART:TryCreateFont() + if "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID == self.lastcustomfont then + self.UsedFont = "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID + return + end + if self.CreateCustomFont then + local newfont = "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID + surface.CreateFont( newfont, { + font = self.CustomFont, -- Use the font-name which is shown to you by your operating system Font Viewer, not the file name + extended = self.Extended, + size = self.FontSize, + weight = self.Weight, + blursize = self.BlurSize, + scanlines = self.ScanLines, + antialias = self.Antialias, + underline = self.Underline, + italic = self.Italic, + strikeout = self.Strikeout, + symbol = self.Symbol, + rotary = self.Rotary, + shadow = self.Shadow, + additive = self.Additive, + outline = self.Outline, + } ) + self:SetFont(newfont) + self.lastcustomfont = newfont + lastfontcreationtime = CurTime() end end @@ -162,6 +515,12 @@ function PART:OnShow() self.time = CurTime() end +function PART:OnHide() + hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) +end +function PART:OnRemove() + hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) +end function PART:SetText(str) self.Text = str end diff --git a/lua/pac3/core/server/event.lua b/lua/pac3/core/server/event.lua index 84cc0db15..252d9a843 100644 --- a/lua/pac3/core/server/event.lua +++ b/lua/pac3/core/server/event.lua @@ -1,6 +1,16 @@ util.AddNetworkString("pac_proxy") util.AddNetworkString("pac_event") +util.AddNetworkString("pac_event_set_sequence") + +net.Receive("pac_event_set_sequence", function(len, ply) + local event = net.ReadString() + local num = net.ReadUInt(8) + for i=1,100,1 do + ply.pac_command_events[event..i] = nil + end + ply.pac_command_events[event..num] = {name = event, time = pac.RealTime, on = 1} +end) -- event concommand.Add("pac_event", function(ply, _, args) @@ -21,6 +31,9 @@ concommand.Add("pac_event", function(ply, _, args) net.WriteString(event) net.WriteInt(extra, 8) net.Broadcast() + ply.pac_command_events = ply.pac_command_events or {} + ply.pac_command_events[event] = ply.pac_command_events[event] or {} + ply.pac_command_events[event] = {name = event, time = pac.RealTime, on = extra} end) concommand.Add("+pac_event", function(ply, _, args) @@ -66,12 +79,49 @@ end) -- proxy concommand.Add("pac_proxy", function(ply, _, args) + str = args[1] + + if ply:IsValid() then + ply.pac_proxy_events = ply.pac_proxy_events or {} + end + local x + local y + local z + if ply.pac_proxy_events[str] ~= nil then + if args[2] then + if string.sub(args[2],1,2) == "++" or string.sub(args[2],1,2) == "--" then + x = ply.pac_proxy_events[str].x + tonumber(string.sub(args[2],2,#args[2])) + else x = tonumber(args[2]) or ply.pac_proxy_events[str].x or 0 end + end + + if args[3] then + if string.sub(args[3],1,2) == "++" or string.sub(args[3],1,2) == "--" then + y = ply.pac_proxy_events[str].y + tonumber(string.sub(args[3],2,#args[3])) + else y = tonumber(args[3]) or ply.pac_proxy_events[str].y or 0 end + end + if not args[3] then y = 0 end + + if args[4] then + if string.sub(args[4],1,2) == "++" or string.sub(args[4],1,2) == "--" then + z = ply.pac_proxy_events[str].z + tonumber(string.sub(args[4],2,#args[4])) + else z = tonumber(args[4]) or ply.pac_proxy_events[str].z or 0 end + end + if not args[4] then z = 0 end + else + x = tonumber(args[2]) or 0 + y = tonumber(args[3]) or 0 + z = tonumber(args[4]) or 0 + end + ply.pac_proxy_events[str] = {name = str, x = x, y = y, z = z} + net.Start("pac_proxy", true) net.WriteEntity(ply) net.WriteString(args[1]) - net.WriteFloat(tonumber(args[2]) or 0) - net.WriteFloat(tonumber(args[3]) or 0) - net.WriteFloat(tonumber(args[4]) or 0) + net.WriteFloat(x or 0) + net.WriteFloat(y or 0) + net.WriteFloat(z or 0) net.Broadcast() + + --PrintTable(ply.pac_proxy_events[str]) end) diff --git a/lua/pac3/core/server/net_messages.lua b/lua/pac3/core/server/net_messages.lua index b0bfd379e..856473ef5 100644 --- a/lua/pac3/core/server/net_messages.lua +++ b/lua/pac3/core/server/net_messages.lua @@ -2,6 +2,13 @@ util.AddNetworkString("pac.AllowPlayerButtons") util.AddNetworkString("pac.BroadcastPlayerButton") +util.AddNetworkString("pac.BroadcastPlayerInputs") + +util.AddNetworkString("pac.RequestPlayerObjUsed") +util.AddNetworkString("pac.SendPlayerObjUsed") + +util.AddNetworkString("pac.BroadcastDamageAttributions") + do -- button event net.Receive("pac.AllowPlayerButtons", function(length, client) local key = net.ReadUInt(8) @@ -28,4 +35,133 @@ do -- button event pac.AddHook("PlayerButtonUp", "event", function(ply, key) broadcast_key(ply, key, false) end) +end + +do -- input event + local input_enums = { + IN_ATTACK, --1 + IN_JUMP, --2 + IN_DUCK, --4 + IN_FORWARD, --8 + IN_BACK, --16 + IN_USE, --32 + IN_CANCEL, --64 + IN_LEFT, --128 + IN_RIGHT, --256 + IN_MOVELEFT, --512 + IN_MOVERIGHT, --1024 + IN_ATTACK2, --2048 + IN_RUN, --4096 + IN_RELOAD, --8192 + IN_ALT1, --16384 + IN_ALT2, --32768 + IN_SCORE, --65536 + IN_SPEED, --131072 + IN_WALK, --262144 + IN_ZOOM, --524288 + IN_WEAPON1, --1048576 + IN_WEAPON2, --2097152 + IN_BULLRUSH, --4194304 + IN_GRENADE1, --8388608 + IN_GRENADE2 --16777216 + } + + local pac_broadcast_inputs = {} + local last_input_broadcast = 0 + local player_last_input_broadcast_times = {} + + local function broadcast_inputs(ply, update) + + if not update and not (last_input_broadcast + 0.05 < CurTime()) then return + else + net.Start("pac.BroadcastPlayerInputs") + net.WriteTable(pac_broadcast_inputs) + net.WriteTable(player_last_input_broadcast_times) + net.Broadcast() + last_input_broadcast = CurTime() + end + end + + pac.AddHook("Tick", "PACBroadcastPlayerInputs", function() + + local update = false + local updated_players = {} + pac_broadcast_inputs = pac_broadcast_inputs or {} + local last_broadcast_inputs = table.Copy(pac_broadcast_inputs) + local time = CurTime() + for _,ply in pairs(player.GetAll()) do + if not pac_broadcast_inputs[ply] then pac_broadcast_inputs[ply] = {} end + for _,v in pairs(input_enums) do + + if ply:KeyDown( v ) then + pac_broadcast_inputs[ply][v] = true + elseif ply:KeyDownLast( v ) then + pac_broadcast_inputs[ply][v] = false + end + + if last_broadcast_inputs[ply][v] ~= pac_broadcast_inputs[ply][v] then + update = true + player_last_input_broadcast_times[ply] = CurTime() + end + end + end + broadcast_inputs(update) + + end) +end + +do --is_using_entity + local function send_player_used_object(client, ent, class, b, from_client) + net.Start("pac.SendPlayerObjUsed") + net.WriteEntity(client) + net.WriteEntity(ent) + net.WriteString(class) + net.WriteBool(b) + + --print("BROADCAST", client, ent, class, b) + net.Send(player.GetAll()) + end + + net.Receive("pac.RequestPlayerObjUsed", function(length, client) + local from_client = true + local override = true + local ent_used = client:GetEntityInUse() + local class = "nil" + if ent_used and IsValid(ent_used) then + if ent_used.GetClass ~= nil then + class = ent_used:GetClass() + end + end + if class == "player_pickup" then + override = false + end + + send_player_used_object(client, ent_used, class, override, from_client) + end) + + hook.Add("PlayerUse", "pac.PlayerUse", function( client, ent ) + local class = ent:GetClass() + --print("USE", client,ent, class) + if ent:GetClass() ~= "player_pickup" then + send_player_used_object(client, ent, class, true, false) + end + end) +end + +do --damage attribution + timer.Simple(1, --call it regularly in case a new hook overrides the damage, we want the final one + pac.AddHook("EntityTakeDamage", "pac.AttributeDamage", function(ent, dmg) + local time = CurTime() + if IsValid(dmg:GetAttacker()) then + local tbl = {hit_time = time, attacker = dmg:GetAttacker(), dmg_amount = dmg:GetDamage(), dmg_type = dmg:GetDamageType(), inflictor = dmg:GetInflictor()} + ent[dmg:GetInflictor()] = tbl + net.Start("pac.BroadcastDamageAttributions") + net.WriteEntity(ent) + net.WriteTable(tbl) + net.WriteBool(ent:Health() < dmg:GetDamage()) + net.Broadcast() + end + + end) + ) end \ No newline at end of file diff --git a/lua/pac3/core/shared/movement.lua b/lua/pac3/core/shared/movement.lua index f0052f22a..a6b13caf3 100644 --- a/lua/pac3/core/shared/movement.lua +++ b/lua/pac3/core/shared/movement.lua @@ -1,24 +1,31 @@ local movementConvar = CreateConVar("pac_free_movement", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow players to modify movement. -1 apply only allow when noclip is allowed, 1 allow for all gamemodes, 0 to disable") +local allowMass = CreateConVar("pac_player_movement_allow_mass", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "enables changing player mass in player movement. 1 to enable, 0 to disable", 0, 1) +local massUpperLimit = CreateConVar("pac_player_movement_max_mass", 50000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the maximum mass that players can use with player movement", 85, 50000) +local massLowerLimit = CreateConVar("pac_player_movement_min_mass", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the minimum mass that players can use with player movement", 0, 85) +local massDamageScale = CreateConVar("pac_player_movement_physics_damage_scaling", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the damage scaling applied to players by modified mass values. 1 to enable, 0 to disable", 0, 1) local default = { JumpHeight = 200, StickToGround = true, GroundFriction = 0.12, AirFriction = 0.01, + HorizontalAirFrictionMultiplier = 1, + StrafingStrengthMultiplier = 1, Gravity = Vector(0,0,-600), + Mass = 85, Noclip = false, MaxGroundSpeed = 750, - MaxAirSpeed = 1, + MaxAirSpeed = 750, AllowZVelocity = false, ReversePitch = false, UnlockPitch = false, VelocityToViewAngles = 0, RollAmount = 0, - SprintSpeed = 750, - RunSpeed = 300, + SprintSpeed = 400, + RunSpeed = 200, WalkSpeed = 100, - DuckSpeed = 25, + DuckSpeed = 50, FinEfficiency = 0, FinLiftMode = "normal", @@ -35,6 +42,7 @@ if SERVER then local str = net.ReadString() if str == "disable" then ply.pac_movement = nil + ply:GetPhysicsObject():SetMass(default.Mass) else if default[str] ~= nil then local val = net.ReadType() @@ -113,7 +121,9 @@ local function badMovetype(ply) end local frictionConvar = GetConVar("sv_friction") +local lasttime = 0 pac.AddHook("Move", "custom_movement", function(ply, mv) + lasttime = SysTime() local self = ply.pac_movement if not self then @@ -145,6 +155,25 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) ply:SetJumpPower(self.JumpHeight) + if SERVER then + if allowMass:GetInt() == 1 then + ply:GetPhysicsObject():SetMass(math.Clamp(self.Mass, massLowerLimit:GetFloat(), massUpperLimit:GetFloat())) + end + end + + if (movementConvar:GetInt() == 1 or (movementConvar:GetInt() == -1 and hook.Run("PlayerNoClip", ply, true) == true)) and massDamageScale:GetInt() == 1 then + scale_mass = 85/math.Clamp(self.Mass, math.max(massLowerLimit:GetFloat(), 0.01), massUpperLimit:GetFloat()) + else + scale_mass = 1 + end + + pac.AddHook("EntityTakeDamage", "PAC3MassDamageScale", function(target, dmginfo) + if (target:IsPlayer() and dmginfo:IsDamageType(DMG_CRUSH or DMG_VEHICLE)) then + dmginfo:ScaleDamage(scale_mass) + end + end) + + if self.Noclip then ply:SetMoveType(MOVETYPE_NONE) else @@ -173,7 +202,11 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) speed = self.DuckSpeed end --- speed = speed * FrameTime() + if not on_ground and not self.AllowZVelocity then + speed = speed * self.StrafingStrengthMultiplier + end + + --speed = speed * FrameTime() local ang = mv:GetAngles() local vel = Vector() @@ -193,7 +226,8 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) elseif mv:KeyDown(IN_MOVELEFT) then vel = vel - ang:Right() end - + + vel = vel:GetNormalized() * speed if self.AllowZVelocity then @@ -208,15 +242,19 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) vel.z = 0 end - local speed = vel + local speed = vel --That makes speed the driver (added velocity) + if not on_ground and not self.AllowZVelocity then + speed = speed * self.StrafingStrengthMultiplier + end local vel = mv:GetVelocity() + --@note ground friction if on_ground and not self.Noclip and self.StickToGround then -- work against ground friction local sv_friction = frictionConvar:GetInt() - + --ice and glass go too fast? what do? if sv_friction > 0 then - sv_friction = 1 - (sv_friction * 15) / 1000 + sv_friction = 1 - (sv_friction * 15) / 1000 --default is 8, and the formula ends up being equivalent to 0.12 groundfriction variable multiplying vel by 0.88 vel = vel / sv_friction end end @@ -226,14 +264,64 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) -- todo: don't allow adding more velocity to existing velocity if it exceeds -- but allow decreasing if not on_ground then - local friction = self.AirFriction - friction = -(friction) + 1 + + if ply:WaterLevel() >= 2 then + local ground_speed = self.RunSpeed - vel = vel * friction + if mv:KeyDown(IN_SPEED) then + ground_speed = self.SprintSpeed + end - vel = vel + self.Gravity * 0.015 - speed = speed:GetNormalized() * math.Clamp(speed:Length(), 0, self.MaxAirSpeed) - vel = vel + (speed * FrameTime()*(66.666*(-friction+1))) + if mv:KeyDown(IN_WALK) then + ground_speed = self.WalkSpeed + end + + if mv:KeyDown(IN_DUCK) then + ground_speed = self.DuckSpeed + end + if self.MaxGroundSpeed == 0 then self.MaxGroundSpeed = 400 end + if self.MaxAirSpeed == 0 then self.MaxAirSpeed = 400 end + local water_speed = math.min(ground_speed, self.MaxAirSpeed, self.MaxGroundSpeed) + print("water speed " .. water_speed) + + ang = ply:EyeAngles() + local vel2 = Vector() + + if mv:KeyDown(IN_FORWARD) then + vel2 = water_speed*ang:Forward() + elseif mv:KeyDown(IN_BACK) then + vel2 = -water_speed*ang:Forward() + end + + if mv:KeyDown(IN_MOVERIGHT) then + vel2 = vel2 + ang:Right() + elseif mv:KeyDown(IN_MOVELEFT) then + vel2 = vel2 - ang:Right() + end + + vel = vel + vel2 * math.min(FrameTime(),0.3) * 2 + + else + local friction = self.AirFriction + local friction_mult = -(friction) + 1 + + local hfric = friction * self.HorizontalAirFrictionMultiplier + local hfric_mult = -(hfric) + 1 + + vel.x = vel.x * hfric_mult + vel.y = vel.y * hfric_mult + vel.z = vel.z * friction_mult + vel = vel + self.Gravity * 0.015 + + speed = speed:GetNormalized() * math.Clamp(speed:Length(), 0, self.MaxAirSpeed) --base driver speed but not beyond max? + --why should the base driver speed depend on friction? + + --reminder: vel is the existing speed, speed is the driver (added velocity) + --vel = vel + (speed * FrameTime()*(66.666*friction)) + vel.x = vel.x + (speed.x * math.min(FrameTime(),0.3)*(66.666*hfric)) + vel.y = vel.y + (speed.y * math.min(FrameTime(),0.3)*(66.666*hfric)) + vel.z = vel.z + (speed.z * math.min(FrameTime(),0.3)*(66.666*friction)) + end else local friction = self.GroundFriction friction = -(friction) + 1 @@ -241,7 +329,24 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) vel = vel * friction speed = speed:GetNormalized() * math.min(speed:Length(), self.MaxGroundSpeed) - vel = vel + (speed * FrameTime()*(75.77*(-friction+1))) + + local trace = { + start = mv:GetOrigin(), + endpos = mv:GetOrigin() + Vector(0, 0, -20), + mask = MASK_SOLID_BRUSHONLY + } + local trc = util.TraceLine(trace) + local special_surf_fric = 1 + --print(trc.MatType) + if trc.MatType == MAT_GLASS then + special_surf_fric = 0.6 + elseif trc.MatType == MAT_SNOW then + special_surf_fric = 0.4 + end + + --vel = vel + (special_surf_fric * speed * FrameTime()*(75.77*(-friction+1))) + vel = vel + (special_surf_fric * speed * math.min(FrameTime(),0.3)*(75.77*(-friction+1))) + vel = vel + self.Gravity * 0.015 end diff --git a/lua/pac3/core/shared/util.lua b/lua/pac3/core/shared/util.lua index 37e4d0cce..41c93817c 100644 --- a/lua/pac3/core/shared/util.lua +++ b/lua/pac3/core/shared/util.lua @@ -421,7 +421,7 @@ function pac.DownloadMDL(url, callback, onfail, ply) if not found then for i, v in pairs(files) do if string.find(v.file_path, material_name, 1, true) or string.find(material_name, v.file_name, 1, true) then - v.file_name = material_name + table.insert(files, {file_name = material_name, buffer = v.buffer, crc = v.crc, file_path = v.file_path}) found = v.file_path break end diff --git a/lua/pac3/editor/client/asset_browser.lua b/lua/pac3/editor/client/asset_browser.lua index c4ea9969c..57e3a784d 100644 --- a/lua/pac3/editor/client/asset_browser.lua +++ b/lua/pac3/editor/client/asset_browser.lua @@ -1,6 +1,87 @@ -- based on starfall CreateClientConVar("pac_asset_browser_close_on_select", "1") CreateClientConVar("pac_asset_browser_remember_layout", "1") +CreateClientConVar("pac_asset_browser_extra_options", "1") +CreateClientConVar("pac_favorites_try_to_get_asset_series", "1") +CreateClientConVar("pac_favorites_try_to_build_asset_series", "0") + +local function rebuild_bookmarks() + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + + --here's some default favorites + if not pace.bookmarked_ressources["models"] or table.IsEmpty(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 + + if not pace.bookmarked_ressources["sound"] or table.IsEmpty(pace.bookmarked_ressources["sound"]) then + pace.bookmarked_ressources["sound"] = { + "music/hl1_song11.mp3", + "npc/combine_gunship/dropship_engine_near_loop1.wav", + "ambient/alarms/warningbell1.wav", + "phx/epicmetal_hard7.wav", + "phx/explode02.wav" + } + end + + if not pace.bookmarked_ressources["materials"] or table.IsEmpty(pace.bookmarked_ressources["materials"]) then + pace.bookmarked_ressources["materials"] = { + "models/debug/debugwhite", + "vgui/null", + "debug/env_cubemap_model", + "models/wireframe", + "cable/physbeam", + "cable/cable2", + "effects/tool_tracer", + "effects/flashlight/logo", + "particles/flamelet[1,5]", + "sprites/key_[0,9]", + "vgui/spawnmenu/generating", + "vgui/spawnmenu/hover" + } + end +end + +local function encode_table_to_file(str) + local data = {} + if not file.Exists("pac3_config", "DATA") then + file.CreateDir("pac3_config") + + end + + + if str == "pac_editor_shortcuts" then + data = pace.PACActionShortcut + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "pac_editor_partmenu_layouts" then + data = pace.operations_order + file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data)) + elseif str == "pac_part_categories" then + data = pace.partgroups + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "bookmarked_ressources" then + rebuild_bookmarks() + for category, tbl in pairs(pace.bookmarked_ressources) do + data = tbl + str = category + file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data)) + end + + end + +end + +function pace.SaveRessourceBookmarks() + encode_table_to_file("bookmarked_ressources") +end + + local function table_tolist(tbl, sort) local list = {} @@ -51,6 +132,7 @@ end local L = pace.LanguageString +--icon is the item's panel, path is the item's full file path, on_menu is a function that can extend the function local function install_click(icon, path, pattern, on_menu, pathid) local old = icon.OnMouseReleased icon.OnMouseReleased = function(_, code) @@ -70,6 +152,42 @@ local function install_click(icon, path, pattern, on_menu, pathid) end SetClipboardText(path) end) + + if string.match(path, "^materials/(.+)%.vmt$") or string.match(path, "^materials/(.+%.png)$") then resource_type = "materials" + elseif string.match(path, "^models/") then resource_type = "models" end + + if not pace.bookmarked_ressources then + pace.SaveRessourceBookmarks() + elseif not pace.bookmarked_ressources[resource_type] then + pace.SaveRessourceBookmarks() + end + if GetConVar("pac_asset_browser_extra_options"):GetBool() then + if GetConVar("pac_favorites_try_to_get_asset_series"):GetBool() then + if not table.HasValue(pace.bookmarked_ressources[resource_type], path) then + menu:AddOption(L"add series to favorites", function() + table.insert(pace.bookmarked_ressources[resource_type], path) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove series from favorites", function() + table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], path )) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/cross.png") + end + end + if not table.HasValue(pace.bookmarked_ressources[resource_type], path) then + menu:AddOption(L"add to favorites", function() + table.insert(pace.bookmarked_ressources[resource_type], path) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove from favorites", function() + table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], path )) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/cross.png") + end + end + if on_menu then on_menu(menu) end menu:Open() end @@ -93,6 +211,10 @@ local function get_unlit_mat(path) return CreateMaterial(path .. "_pac_asset_browser", "UnlitGeneric", {["$basetexture"] = path:match("materials/(.+)%.vtf")}) end +function pace.get_unlit_mat(path) + return get_unlit_mat(path) +end + local next_generate_icon = 0 local max_generating = 5 @@ -215,25 +337,25 @@ local function create_material_icon(path, grid_panel) --[[ - local old = icon.OnCursorEntered - function icon:OnCursorEntered(...) - if pace.current_part:IsValid() and pace.current_part.Materialm then - pace.asset_browser_old_mat = pace.asset_browser_old_mat or pace.current_part.Materialm - pace.current_part.Materialm = mat - end + local old = icon.OnCursorEntered + function icon:OnCursorEntered(...) + if pace.current_part:IsValid() and pace.current_part.Materialm then + pace.asset_browser_old_mat = pace.asset_browser_old_mat or pace.current_part.Materialm + pace.current_part.Materialm = mat + end - old(self, ...) - end + old(self, ...) + end - local old = icon.OnCursorExited - function icon:OnCursorExited(...) - if pace.current_part:IsValid() and pace.current_part.Materialm then - pace.current_part.Materialm = pace.asset_browser_old_mat + local old = icon.OnCursorExited + function icon:OnCursorExited(...) + if pace.current_part:IsValid() and pace.current_part.Materialm then + pace.current_part.Materialm = pace.asset_browser_old_mat + end + old(self, ...) end - old(self, ...) - end -]] + ]] local unlit_mat = get_unlit_mat(path) @@ -343,6 +465,7 @@ local function create_material_icon(path, grid_panel) create_text_view(str) end) + end) grid_panel:Add(icon) @@ -606,6 +729,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) local frame = vgui.Create("DFrame") frame.title = L"asset browser" .. " - " .. (browse_types_str:gsub(";", " ")) + if GetConVar("pac_asset_browser_remember_layout"):GetBool() then frame:SetCookieName("pac_asset_browser") end @@ -681,6 +805,9 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) options_menu:SetDeleteSelf(false) options_menu:AddCVar(L"close browser on select", "pac_asset_browser_close_on_select", "1", "0") options_menu:AddCVar(L"remember layout", "pac_asset_browser_remember_layout", "1", "0") + options_menu:AddCVar(L"additional right click options", "pac_asset_browser_extra_options", "1", "0") + options_menu:AddCVar(L"try to find asset series for saving favorites", "pac_favorites_try_to_get_asset_series", "1", "0") + options_menu:AddCVar(L"try to build asset series in the editor", "pac_favorites_try_to_build_asset_series", "1", "0") local zoom_controls = vgui.Create("pac_AssetBrowser_ZoomControls", menu_bar) @@ -813,6 +940,60 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) if code == MOUSE_RIGHT then play:Start() + local menu = DermaMenu() + menu:SetPos(input.GetCursorPos()) + menu:AddOption(L"copy path", function() + SetClipboardText(sound) + end) + if GetConVar("pac_asset_browser_extra_options"):GetBool() then + pace.bookmarked_ressources["sound"] = pace.bookmarked_ressources["sound"] or {} + local resource_type = "sound" + if GetConVar("pac_favorites_try_to_get_asset_series"):GetBool() then + --print(sound) + local extension = string.GetExtensionFromFilename(sound) + local base_name = string.gsub(sound, "%d+."..extension.."$", "") + base_name = string.gsub(base_name, "^sound/", "") + --print(resource_type, base_name, extension) + + local series_results = pace.FindAssetSeriesBounds(resource_type, base_name, extension) + PrintTable(series_results) + if not series_results.start_index then + goto CONTINUE + end + + local series_str = base_name .. "[" .. series_results.start_index .. "," .. series_results.end_index .. "]." .. extension + + if not table.HasValue(pace.bookmarked_ressources[resource_type], series_str) then + + menu:AddOption(L"add series to favorites", function() + table.insert(pace.bookmarked_ressources[resource_type], series_str) + pace.SaveRessourceBookmarks() + + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove series from favorites", function() + table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], series_str )) + pace.SaveRessourceBookmarks() + + end):SetImage("icon16/cross.png") + end + + ::CONTINUE:: + end + + if not table.HasValue(pace.bookmarked_ressources["sound"], sound) then + menu:AddOption(L"add to favorites", function() + table.insert(pace.bookmarked_ressources["sound"], sound) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove from favorites", function() + table.remove(pace.bookmarked_ressources["sound"], table.KeyFromValue( pace.bookmarked_ressources["sound"], sound )) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/cross.png") + end + end + menu:MakePopup() menu:RequestFocus() else pace.model_browser_callback(sound, "GAME") end diff --git a/lua/pac3/editor/client/fonts.lua b/lua/pac3/editor/client/fonts.lua index 4a70c106c..59b75c50f 100644 --- a/lua/pac3/editor/client/fonts.lua +++ b/lua/pac3/editor/client/fonts.lua @@ -2,7 +2,7 @@ local L = pace.LanguageString pace.Fonts = {} -for i = 1, 5 do +for i = 1, 34 do surface.CreateFont("pac_font_"..i, { font = "Arial", @@ -14,7 +14,7 @@ for i = 1, 5 do table.insert(pace.Fonts, "pac_font_"..i) end -for i = 1, 5 do +for i = 1, 34 do surface.CreateFont("pac_font_bold"..i, { font = "Arial", diff --git a/lua/pac3/editor/client/logic.lua b/lua/pac3/editor/client/logic.lua index 329928faa..92026aca8 100644 --- a/lua/pac3/editor/client/logic.lua +++ b/lua/pac3/editor/client/logic.lua @@ -70,6 +70,7 @@ function pace.OnOpenEditor() end function pace.OnCloseEditor() + pace.FlushInfoPopups() pace.EnableView(false) pace.StopSelect() pace.SafeRemoveSpecialPanel() diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 15f8f5588..aefcfe5eb 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -55,6 +55,11 @@ local function populate_pac(menu) function() pace.ShowWiki(pace.WikiURL .. "Beginners-FAQ") end ):SetImage(pace.MiscIcons.info) + help:AddOption( + L"PAC3 Wiki", + function() pace.ShowWiki("https://wiki.pac3.info/start") end + ):SetImage(pace.MiscIcons.info) + do local chat_pnl = help:AddOption( L"Discord / PAC3 Chat", @@ -75,8 +80,12 @@ local function populate_pac(menu) version_pnl:SetImage(pace.MiscIcons.info) version:AddOption(version_string) + + version:AddOption("update news", function() pac.OpenMOTD(false) end) end + + help:AddOption( L"about", function() pace.ShowAbout() end @@ -101,10 +110,39 @@ end local function populate_options(menu) menu:AddOption(L"settings", function() pace.OpenSettings() end) + menu:AddCVar(L"Keyboard shortcuts: Legacy mode", "pac_editor_shortcuts_legacy_mode", "1", "0") menu:AddCVar(L"inverse collapse/expand controls", "pac_reverse_collapse", "1", "0") menu:AddCVar(L"enable shift+move/rotate clone", "pac_grab_clone", "1", "0") menu:AddCVar(L"remember editor position", "pac_editor_remember_position", "1", "0") + menu:AddCVar(L"ask before loading autoload", "pac_prompt_for_autoload", "1", "0") + if game.SinglePlayer() then menu:AddCVar(L"queue prop / npc outfits for next spawned entity", "pac_prompt_for_autoload", "2", "0") end menu:AddCVar(L"show parts IDs", "pac_show_uniqueid", "1", "0") + local popups, pnlp = menu:AddSubMenu("configure editor popups", function() end) + popups.GetDeleteSelf = function() return false end + pnlp:SetImage("icon16/comment.png") + popups:AddCVar(L"enable editor popups", "pac_popups_enable", "1", "0") + popups:AddCVar(L"don't kill popups on autofade", "pac_popups_preserve_on_autofade", "1", "0") + popups:AddOption("Configure popups appearance", function() pace.OpenPopupConfig() end):SetImage('icon16/color_wheel.png') + local popup_pref_mode, pnlppm = popups:AddSubMenu("prefered location", function() end) + pnlppm:SetImage("icon16/layout_header.png") + popup_pref_mode.GetDeleteSelf = function() return false end + popup_pref_mode:AddOption(L"parts on viewport", function() RunConsoleCommand("pac_popups_preferred_location", "part world") end):SetImage('icon16/camera.png') + popup_pref_mode:AddOption(L"part label on tree", function() RunConsoleCommand("pac_popups_preferred_location", "pac tree label") end):SetImage('icon16/layout_content.png') + popup_pref_mode:AddOption(L"menu bar", function() RunConsoleCommand("pac_popups_preferred_location", "menu bar") end):SetImage('icon16/layout_header.png') + popup_pref_mode:AddOption(L"cursor", function() RunConsoleCommand("pac_popups_preferred_location", "cursor") end):SetImage('icon16/mouse.png') + popup_pref_mode:AddOption(L"screen", function() RunConsoleCommand("pac_popups_preferred_location", "screen") end):SetImage('icon16/monitor.png') + + local combat_consents, pnlcc = menu:AddSubMenu("pac combat consents", function() end) + combat_consents.GetDeleteSelf = function() return false end + pnlcc:SetImage("icon16/joystick.png") + + combat_consents:AddCVar(L"damage_zone part (area damage)", "pac_client_damage_zone_consent", "1", "0") + combat_consents:AddCVar(L"hitscan part (bullets)", "pac_client_hitscan_consent", "1", "0") + combat_consents:AddCVar(L"force part (physics forces)", "pac_client_force_consent", "1", "0") + combat_consents:AddCVar(L"lock part's grab (can take control of your position)", "pac_client_grab_consent", "1", "0") + combat_consents:AddCVar(L"lock part's grab calcview (can take control of your view)", "pac_client_lock_camera_consent", "1", "0") + + menu:AddSpacer() menu:AddOption(L"position grid size", function() Derma_StringRequest(L"position grid size", L"size in units:", GetConVarNumber("pac_grid_pos_size"), function(val) @@ -149,6 +187,18 @@ local function populate_player(menu) end end +function pace.PopulateMenuBarTab(menu, tab) + if tab == "pac" then + populate_pac(menu) + elseif tab == "player" then + populate_player(menu) + elseif tab == "options" then + populate_options(menu) + elseif tab == "view" then + populate_view(menu) + end +end + function pace.OnMenuBarPopulate(bar) for k,v in pairs(bar.Menus) do v:Remove() @@ -161,6 +211,11 @@ function pace.OnMenuBarPopulate(bar) pace.AddToolsToMenu(bar:AddMenu(L"tools")) bar:RequestFocus(true) + timer.Simple(0.2, function() + if IsValid(bar) then + bar:RequestFocus(true) + end + end) end function pace.OnOpenMenu() diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index dab095bdb..f4ff29d86 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -185,6 +185,10 @@ function PANEL:OnRemove() end end +function PANEL:IsLeft() --which side the editor is on. + return self:GetPos() + self:GetWide() / 2 < ScrW() / 2 +end + function PANEL:Think(...) if not self.okay then return end DFrame.Think(self, ...) @@ -212,7 +216,7 @@ function PANEL:Think(...) if self.exit_button:IsValid() then - if self:GetPos() + self:GetWide() / 2 < ScrW() / 2 then + if self:IsLeft() then self.exit_button:SetPos(ScrW() - self.exit_button:GetWide() + 4, -4) else self.exit_button:SetPos(-4, -4) @@ -244,6 +248,9 @@ function PANEL:Think(...) self.zoomslider:SetValue(75) pace.zoom_reset = nil end + if pace.OverridingFOVSlider then + self.zoomslider:SetValue(pace.ViewFOV) + end if zoom_smooth:GetInt() == 1 then pace.SetZoom(self.zoomslider:GetValue(),true) @@ -261,6 +268,8 @@ function PANEL:Think(...) else self.zoomsettings:SetVisible(false) end + + end end diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 690511b1f..f2eefa773 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -714,12 +714,16 @@ do -- event is_touching if part ~= last_part then stop() return end if not part:IsValid() then stop() return end if part.ClassName ~= "event" then stop() return end - if not (part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable") then stop() return end + if not (part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable" or part:GetEvent() == "is_touching_filter" or part:GetEvent() == "is_touching_life") then stop() return end local extra_radius = part:GetProperty("extra_radius") or 0 + local nearest_model = part:GetProperty("nearest_model") or false + local no_npc = part:GetProperty("no_npc") or false + local no_players = part:GetProperty("no_players") or false local x_stretch = part:GetProperty("x_stretch") or 1 local y_stretch = part:GetProperty("y_stretch") or 1 local z_stretch = part:GetProperty("z_stretch") or 1 + local ent if part.RootOwner then ent = part:GetRootPart():GetOwner() @@ -727,6 +731,8 @@ do -- event is_touching ent = part:GetOwner() end + if nearest_model then ent = part:GetOwner() end + if not IsValid(ent) then stop() return end local radius @@ -737,32 +743,90 @@ do -- event is_touching local mins = Vector(-x_stretch,-y_stretch,-z_stretch) local maxs = Vector(x_stretch,y_stretch,z_stretch) - if part:GetEvent() == "is_touching" then - radius = math.max(ent:BoundingRadius() + extra_radius + 1, 1) - mins = mins * radius - maxs = maxs * radius + radius = math.max(ent:BoundingRadius() + extra_radius + 1, 1) + mins = mins * radius + maxs = maxs * radius + + local startpos = ent:WorldSpaceCenter() + local b = false + if part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable" then + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {part:GetRootPart():GetOwner(),ent} + } ) + b = tr.Hit + elseif part:GetEvent() == "is_touching_life" then + local found = false + local ents_hits = ents.FindInBox(startpos + mins, startpos + maxs) + for _,ent2 in pairs(ents_hits) do + + if IsValid(ent2) and (ent2 ~= ent and ent2 ~= part:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) + then + found = true + if ent2:IsNPC() and no_npc then + found = false + elseif ent2:IsPlayer() and no_players then + found = false + end + if found then b = true end + end + end + elseif part:GetEvent() == "is_touching_filter" then + local ents_hits = ents.FindInBox(startpos + mins, startpos + maxs) + for _,ent2 in pairs(ents_hits) do + if (ent2 ~= ent and ent2 ~= part:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) and + not ( (no_npc and ent2:IsNPC()) or (no_players and ent2:IsPlayer()) ) + then b = true end + end end - if part:GetEvent() == "is_touching_scalable" then - radius = math.max(extra_radius, 1) - mins = mins * radius - maxs = maxs * radius + + if self.udata then + render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, b and Color(255,0,0) or Color(255,255,255), true ) end + end) + end - local startpos = ent:WorldSpaceCenter() + pace.RegisterPanel(PANEL) +end - local tr = util.TraceHull( { - start = startpos, - endpos = startpos, - maxs = maxs, - mins = mins, - filter = {part:GetRootPart():GetOwner(),ent} - } ) +do --projectile radius + local PANEL = {} + PANEL.ClassName = "properties_projectile_radii" + PANEL.Base = "pace_properties_number" + + function PANEL:OnValueSet() + time = os.clock() + 6 + local function stop() + hook.Remove("PostDrawOpaqueRenderables", "pace_draw_projectile_radii") + end + local last_part = pace.current_part + + hook.Add("PostDrawOpaqueRenderables", "pace_draw_projectile_radii", function() + if time < os.clock() then + stop() + end + if not pace.current_part:IsValid() then stop() return end + if pace.current_part.ClassName ~= "projectile" then stop() return end if self.udata then - render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, tr.Hit and Color(255,0,0) or Color(255,255,255), true ) + if last_part.Sphere then + render.DrawWireframeSphere( last_part:GetWorldPosition(), last_part.Radius, 10, 10, Color(255,255,255), true ) + render.DrawWireframeSphere( last_part:GetWorldPosition(), last_part.DamageRadius, 10, 10, Color(255,0,0), true ) + else + local mins_ph = Vector(last_part.Radius,last_part.Radius,last_part.Radius) + local mins_dm = Vector(last_part.DamageRadius,last_part.DamageRadius,last_part.DamageRadius) + render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_ph, mins_ph, Color(255,255,255), true ) + render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_dm, mins_dm, Color(255,0,0), true ) + end + end end) end pace.RegisterPanel(PANEL) -end +end \ No newline at end of file diff --git a/lua/pac3/editor/client/panels/pac_tree.lua b/lua/pac3/editor/client/panels/pac_tree.lua index 5389e0756..994497cc0 100644 --- a/lua/pac3/editor/client/panels/pac_tree.lua +++ b/lua/pac3/editor/client/panels/pac_tree.lua @@ -30,7 +30,7 @@ AccessorFunc(PANEL, "m_bClickOnDragHover", "ClickOnDragHover") function PANEL:Init() self:SetShowIcons(true) self:SetIndentSize(14) - self:SetLineHeight(17) + self:SetLineHeight(17 * GetConVar("pac_editor_scale"):GetFloat()) self.RootNode = self:GetCanvas():Add("pac_dtree_node") self.RootNode:SetRoot(self) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index b062fdd96..7d969a9ad 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -1,6 +1,9 @@ local L = pace.LanguageString local languageID = CreateClientConVar("pac_editor_languageid", 1, true, false, "Whether we should show the language indicator inside of editable text entries.") +local favorites_menu_expansion = CreateClientConVar("pac_favorites_try_to_build_asset_series", "0", true, false) + +local searched_cache_series_results = {} function pace.ShowSpecial(pnl, parent, size) size = size or 150 @@ -16,6 +19,157 @@ function pace.FixMenu(menu) menu:SetPos(pace.Editor:GetPos() + pace.Editor:GetWide(), gui.MouseY() - (menu:GetTall() * 0.5)) end +---returns table +--start_index is the first known index +--continuous is whether it's continuous (some series have holes) +--end_index is the last known +function pace.FindAssetSeriesBounds(base_directory, base_file, extension) + + --LEADING ZEROES FIX NOT YET IMPLEMENTED + local function leading_zeros(str) + str = string.StripExtension(str) + + local untilzero_pattern = "%f[1-9][0-9]+$" + local afterzero_pattern = "0+%f[1-9+]" + local beforenumbers_pattern = "%f[%f[1-9][0-9]+$]" + --string.gsub(str, "%f[1-9][0-9]+$", "") --get the start until the zeros stop + + --string.gsub(str, "0+%f[1-9+]", "") --leave start + + if string.find(str, afterzero_pattern) then + return string.gsub(str, untilzero_pattern, string.match(str, afterzero_pattern)) + end + end + --print(base_file .. "leading zeros?" , leading_zeros(base_file)) + if searched_cache_series_results[base_directory .. "/" .. base_file] then return searched_cache_series_results[base_directory .. "/" .. base_file] end + local tbl = {} + local i = 0 --try with 0 at first + local keep_looking = true + local file_n + local lookaheads_left = 15 + local next_exists + tbl.start_index = nil + tbl.all_paths = {} + local index_compressed = 1 --increasing ID number of valid files + + while keep_looking do + + file_n = base_directory .. "/" .. base_file .. i .. "." .. extension + --print(file_n , "file" , file.Exists(file_n, "GAME") and "exists" or "doesn't exist") + --print("checking" , file_n) print("\tThe file" , file.Exists(file_n, "GAME") and "exists" or "doesn't exist") + if file.Exists(file_n, "GAME") then + if not tbl.start_index then tbl.start_index = i end + tbl.end_index = i + tbl.all_paths[index_compressed] = file_n + index_compressed = index_compressed + 1 + end + + + i = i + 1 + file_n = base_directory .. "/" .. base_file .. i .. "." .. extension + next_exists = file.Exists(file_n, "GAME") + if not next_exists then + if tbl.start_index then tbl.continuous = false end + lookaheads_left = lookaheads_left - 1 + else + lookaheads_left = 15 + end + keep_looking = next_exists or lookaheads_left > 0 + end + if not tbl.start_index then tbl.continuous = false end + --print("result of search:") + --PrintTable(tbl) + searched_cache_series_results[base_directory .. "/" .. base_file] = tbl + return tbl +end + + +function pace.AddSubmenuWithBracketExpansion(pnl, func, base_file, extension, base_directory) + if extension == "vmt" then base_directory = "materials" end --prescribed format: short + if extension == "mdl" then base_directory = "models" end --prescribed format: full + if extension == "wav" or extension == "mp3" or extension == "ogg" then base_directory = "sound" end --prescribed format: no trunk + + local base_file_original = base_file + if string.find(base_file, "%[%d+,%d+%]") then --find the bracket notation + base_file = string.gsub(base_file, "%[%d+,%d+%]$", "") + elseif string.find(base_file, "%d+") then + base_file = string.gsub(base_file, "%d+$", "") + end + + local tbl = pace.FindAssetSeriesBounds(base_directory, base_file, extension) + + local icon = "icon16/sound.png" + + if string.find(base_file, "music") or string.find(base_file, "theme") then + icon = "icon16/music.png" + elseif string.find(base_file, "loop") then + icon = "icon16/arrow_rotate_clockwise.png" + end + + if base_directory == "materials" then + icon = "icon16/paint_can.png" + elseif base_directory == "models" then + icon = "materials/spawnicons/"..string.gsub(base_file, ".mdl", "")..".png" + end + + local pnl2 + local menu2 + --print(base_file , #tbl.all_paths) + if #tbl.all_paths > 1 then + pnl2, menu2 = pnl:AddSubMenu(base_file .. " series", function() + func(base_file_original .. "." .. extension) + end) + + if base_directory == "materials" then + menu2:SetImage("icon16/table_multiple.png") + --local mat = string.gsub(base_file_original, "." .. string.GetExtensionFromFilename(base_file_original), "") + --pnl2:AddOption(mat, function() func(base_file_original) end):SetImage("icon16/paint_can.png") + elseif base_directory == "models" then + menu2:SetImage(icon) + elseif base_directory == "sound" then + --print("\t" .. icon) + menu2:SetImage(icon) + end + + else + if base_directory == "materials" then + --local mat = string.gsub(base_file_original, "." .. string.GetExtensionFromFilename(base_file_original), "") + --pnl2:AddOption(mat, function() func(base_file_original) end):SetImage("icon16/paint_can.png") + elseif base_directory == "models" then + + elseif base_directory == "sound" then + local snd = base_file_original + menu2 = pnl:AddOption(snd, function() func(snd) end):SetImage(icon) + end + end + + + --print(tbl) + --PrintTable(tbl.all_paths) + if not tbl then return end + if #tbl.all_paths > 1 then + for _,path in ipairs(tbl.all_paths) do + path_no_trunk = string.gsub(path, base_directory .. "/", "") + if base_directory == "materials" then + local mat = string.gsub(path_no_trunk, "." .. string.GetExtensionFromFilename(path_no_trunk), "") + pnl2:AddOption(mat, function() func(mat) end):SetMaterial(pace.get_unlit_mat(path)) + + elseif base_directory == "models" then + local mdl = path + pnl2:AddOption(string.GetFileFromFilename(mdl), function() func(mdl) end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") + + elseif base_directory == "sound" then + local snd = path_no_trunk + pnl2:AddOption(snd, function() func(snd) end):SetImage(icon) + end + end + end + + + +end + + local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight) local btn = vgui.Create("DButton", self) btn:SetSize(16, 16) @@ -833,6 +987,436 @@ do -- base editable self.OnValueChanged(self:GetValue()) end):SetImage(pace.MiscIcons.paste) + --command's String variable + if self.CurrentKey == "String" then + + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + pace.bookmarked_ressources["command"] = + { + --[[["user"] = { + + },]] + ["basic lua"] = { + { + lua = true, + nicename = "if alive then say I\'m alive", + expression = "if LocalPlayer():Health() > 0 then print(\"I\'m alive\") RunConsoleCommand(\"say\", \"I\'m alive\") else RunConsoleCommand(\"say\", \"I\'m DEAD\") end", + explanation = "To showcase a basic if/else statement, this will make you say \"I'm alive\" or \"I\'m DEAD\" depending on whether you have more than 0 health." + }, + { + lua = true, + nicename = "print 100 first numbers", + expression = "for i=0,100,1 do print(\"number\" .. i) end", + explanation = "To showcase a basic for loop (with the number setup), this will print the first 100 numbers in the console." + }, + { + lua = true, + nicename = "print all entities' health", + expression = "for _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end", + explanation = "To showcase a basic for loop (using a table iterator), this will print the list of all entities\' health" + }, + { + lua = true, + nicename = "print all entities' health", + expression = "local random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)", + explanation = "To showcase basic number handling and variables, this will run a pac_event command for \"event_1\" to \"event_5\"" + } + }, + ["movement"] ={ + { + lua = false, + nicename = "dash", + expression = "+forward;+speed", + explanation = "go forward. WARNING. It holds forever until you release it with -forward;-speed" + }, + }, + ["weapons"] = { + { + lua = false, + nicename = "go unarmed (using console)", + expression = "give none; use none", + explanation = "use the hands swep (\"none\"). In truth, we need to split the command and run the second one after a delay, or run the full thing twice. the console doesn't let us switch to a weapon we don't yet have" + }, + { + lua = true, + nicename = "go unarmed (using lua)", + expression = "RunConsoleCommand(\"give\", \"none\") timer.Simple(0.1, function() RunConsoleCommand(\"use\", \"none\") end)", + explanation = "use the hands swep (\"none\"). we need lua because the console doesn't let us switch to a weapon we don't yet have" + } + }, + ["events logic"] = { + { + lua = true, + nicename = "random command event activation", + expression = "RunConsoleCommand(\"pac_event\", \"COMMAND\" .. math.ceil(math.random()*4))", + explanation = "randomly pick between commands COMMAND1 to COMMAND4.\nReplace 4 to another whole number if you need more or less" + }, + { + lua = true, + nicename = "command series (held down)", + expression = "local i = LocalPlayer()[\"COMMAND\"] RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i, \"1\") RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i-1, \"0\") if i > 5 then i = 0 end LocalPlayer()[\"COMMAND\"] = i + 1", + explanation = "goes in the series of COMMAND1 to COMMAND5 activating the current number and deactivating the previous.\nYou can replace COMMAND for another name, and replace the i > 5 for another limit to loop back around\nAlthough now you can use pac_event_sequenced to control event series" + }, + { + lua = true, + nicename = "command series (impulse)", + expression = "local i = LocalPlayer()[\"COMMAND\"] RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i) if i >= 5 then i = 0 end LocalPlayer()[\"COMMAND\"] = i + 1", + explanation = "goes in the series of COMMAND1 to COMMAND5 activating one command instantaneously.\nYou can replace COMMAND for another name, and replace the i >= 5 for another limit to loop back around" + }, + { + lua = nil, + nicename = "save current events to a single command", + explanation = "this hardcoded preset should build a list of all your active command events and save it as a single command string for you" + } + }, + --[[["experimental things"] = { + { + nicename = "", + expression = "", + explanation = "" + }, + }]] + } + + local menu1, pnl1 = menu:AddSubMenu(L"example commands", function() + end) + pnl1:SetIcon("icon16/cart_go.png") + for group, tbl in pairs(pace.bookmarked_ressources["command"]) do + local icon = "icon16/bullet_white.png" + if group == "user" then icon = "icon16/user.png" + elseif group == "movement" then icon = "icon16/user_go.png" + elseif group == "weapons" then icon = "icon16/bomb.png" + elseif group == "events logic" then icon = "icon16/clock.png" + elseif group == "spatial" then icon = "icon16/world.png" + elseif group == "experimental things" then icon = "icon16/ruby.png" + end + local menu2, pnl2 = menu1:AddSubMenu(group) + pnl2:SetIcon(icon) + + if not table.IsEmpty(tbl) then + for i,tbl2 in pairs(tbl) do + --print(tbl2.nicename) + local str = tbl2.nicename or "invalid name" + local pnl3 = menu2:AddOption(str, function() + if pace.current_part.ClassName == "command" then + local expression = pace.current_part.String + local hardcode = tbl2.lua == nil + local new_expression = "" + if hardcode then + + if tbl2.nicename == "save current events to a single command" then + local tbl3 = {} + for i,v in pairs(LocalPlayer().pac_command_events) do tbl3[i] = v.on end + for i,v in pairs(LocalPlayer().pac_command_events) do RunConsoleCommand("pac_event", i, "0") end + new_expression = "" + + for i,v in pairs(tbl3) do new_expression = new_expression .. "pac_event " .. i .. " " .. v .. ";" end + pace.current_part:SetUseLua(false) + end + + end + if expression == "" then --blank: bare insert + expression = tbl2.expression + pace.current_part:SetUseLua(tbl2.lua) + elseif pace.current_part.UseLua == tbl2.lua then --something present: concatenate the existing bit but only if we're on the same mode + expression = expression .. ";" .. tbl2.expression + pace.current_part:SetUseLua(tbl2.lua) + end + + if not hardcode then + pace.current_part:SetString(expression) + self:SetValue(expression) + else + pace.current_part:SetString(new_expression) + self:SetValue(new_expression) + end + end + + end) + pnl3:SetIcon(icon) + pnl3:SetTooltip(tbl2.explanation) + end + + end + end + end + + --proxy expression + if self.CurrentKey == "Expression" then + + + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + pace.bookmarked_ressources["proxy"] = pace.bookmarked_ressources["proxy"] + local menu1, pnl1 = menu:AddSubMenu(L"Proxy template bits", function() + end) + pnl1:SetIcon("icon16/cart_go.png") + for group, tbl in pairs(pace.bookmarked_ressources["proxy"]) do + local icon = "icon16/bullet_white.png" + if group == "user" then icon = "icon16/user.png" + elseif group == "fades and transitions" then icon = "icon16/shading.png" + elseif group == "pulses" then icon = "icon16/transmit_blue.png" + elseif group == "facial expressions" then icon = "icon16/emoticon_smile.png" + elseif group == "spatial" then icon = "icon16/world.png" + elseif group == "experimental things" then icon = "icon16/ruby.png" + end + local menu2, pnl2 = menu1:AddSubMenu(group) + pnl2:SetIcon(icon) + + if not table.IsEmpty(tbl) then + for i,tbl2 in pairs(tbl) do + --print(tbl2.nicename) + local str = tbl2.nicename or "invalid name" + local pnl3 = menu2:AddOption(str, function() + if pace.current_part.ClassName == "proxy" then + local expression = pace.current_part.Expression + if expression == "" then --blank: bare insert + expression = tbl2.expression + elseif true then --something present: multiply the existing bit? + expression = expression .. " * " .. tbl2.expression + end + + pace.current_part:SetExpression(expression) + self:SetValue(expression) + end + + end) + pnl3:SetIcon(icon) + pnl3:SetTooltip(tbl2.explanation) + end + + end + end + end + + if self.CurrentKey == "LoadVmt" then + local owner = pace.current_part:GetOwner() + local name = string.GetFileFromFilename( owner:GetModel() ) + local mats = owner:GetMaterials() + + local pnl, menu2 = menu:AddSubMenu(L"Load " .. name .. "'s material", function() + end) + menu2:SetImage("icon16/paintcan.png") + + for id,mat in ipairs(mats) do + pnl:AddOption(string.GetFileFromFilename(mat), function() + pace.current_part:SetLoadVmt(mat) + end) + end + end + + if self.CurrentKey == "SurfaceProperties" and pace.current_part.GetSurfacePropsTable then + local tbl = pace.current_part:GetSurfacePropsTable() + menu:AddOption(L"See physics info", function() + local pnl2 = vgui.Create("DFrame") + local txt_zone = vgui.Create("DTextEntry", pnl2) + local str = "" + for i,v in pairs(tbl) do + str = str .. i .. " = " .. v .."\n" + end + txt_zone:SetMultiline(true) + txt_zone:SetText(str) + txt_zone:Dock(FILL) + pnl2:SetTitle("SurfaceProp info : " .. pace.current_part.SurfaceProperties) + pnl2:SetSize(500, 500) + pnl2:SetPos(ScrW()/2, ScrH()/2) + pnl2:MakePopup() + + end):SetImage("icon16/table.png") + + 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/cedrics_models/basic_shapes/plane.mdl", + "models/cedrics_models/basic_shapes/circle.mdl", + "models/hunter/blocks/cube025x025x025.mdl", + "models/editor/axis_helper.mdl", + "models/editor/axis_helper_thick.mdl" + } + end + + local pnl, menu2 = menu:AddSubMenu(L"Load favourite models", function() + end) + menu2:SetImage("icon16/cart_go.png") + + local pm = pace.current_part:GetPlayerOwner():GetModel() + + pnl:AddOption("Current playermodel - " .. string.gsub(string.GetFileFromFilename(pm), ".mdl", ""), function() + pace.current_part:SetModel(pm) + end):SetImage("materials/spawnicons/"..string.gsub(pm, ".mdl", "")..".png") + + for id,mdl in ipairs(pace.bookmarked_ressources["models"]) do + pnl:AddOption(string.GetFileFromFilename(mdl), function() + self:SetValue(mdl) + pace.current_part:SetModel(mdl) + end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") + end + 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 pnl, menu2 = menu:AddSubMenu(L"Load favourite materials", function() + end) + menu2: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.find(mat, "%[%d+,%d+%]") then --find the bracket notation + mat_no_ext = string.gsub(mat_no_ext, "%[%d+,%d+%]", "") + pace.AddSubmenuWithBracketExpansion(pnl, 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 + pnl: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 + 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 pnl, menu2 = menu:AddSubMenu(L"Load favourite sounds", function() + end) + menu2: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.find(snd_no_ext, "%[%d+,%d+%]") then --find the bracket notation + pace.AddSubmenuWithBracketExpansion(pnl, 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(pnl, 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 + + pnl: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 + + --long string menu to bypass the DLabel's limits, only applicable for sound2 for urls and base part's notes + if (pace.current_part.ClassName == "sound2" and self.CurrentKey == "Path") or self.CurrentKey == "Notes" then + + menu:AddOption(L"Insert long text", function() + local pnl = vgui.Create("DFrame") + local DText = vgui.Create("DTextEntry", pnl) + local DButtonOK = vgui.Create("DButton", pnl) + DText:SetMaximumCharCount(50000) + + pnl:SetSize(1200,800) + pnl:SetTitle("Long text 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) + DText:SetPos(5,25) + DText:SetSize(1190,700) + DText:SetMultiline(true) + DText:SetContentAlignment(7) + pnl:MakePopup() + DText:RequestFocus() + + DButtonOK.DoClick = function() + local str = DText:GetText() + if self.CurrentKey == "Notes" then + pace.current_part.Notes = str + elseif pace.current_part.ClassName == "sound2" then + pace.current_part.AllPaths = str + pace.current_part:UpdateSoundsFromAll() + end + pnl:Remove() + end + end):SetImage('icon16/text_letter_omega.png') + end + --left right swap available on strings (and parts) if type(self:GetValue()) == 'string' then menu:AddSpacer() @@ -1178,7 +1762,7 @@ do -- vector val = ctor(val.p, val.y, val.r) end - if _G.type(val):lower() == type or type == "color" then + if _G.type(val):lower() == type or type == "color" or type == "color2" then self:SetValue(val) self.OnValueChanged(self.vector * 1) diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index eb3d91eab..44925fc84 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -1,3 +1,4 @@ +CreateClientConVar("pac_editor_scale","1", true, false) local L = pace.LanguageString local PANEL = {} @@ -8,7 +9,7 @@ PANEL.Base = "pac_dtree" function PANEL:Init() pace.pac_dtree.Init(self) - self:SetLineHeight(18) + self:SetLineHeight(18 * GetConVar("pac_editor_scale"):GetFloat()) self:SetIndentSize(10) self.parts = {} @@ -19,6 +20,7 @@ function PANEL:Init() end do + local function get_added_nodes(self) local added_nodes = {} for i,v in ipairs(self.added_nodes) do @@ -55,67 +57,6 @@ do function PANEL:Think(...) if not pace.current_part:IsValid() then return end - if - pace.current_part.pace_tree_node and - pace.current_part.pace_tree_node:IsValid() and not - ( - pace.BusyWithProperties:IsValid() or - pace.ActiveSpecialPanel:IsValid() or - pace.editing_viewmodel or - pace.editing_hands or - pace.properties.search:HasFocus() - ) and - not gui.IsConsoleVisible() - then - if input.IsKeyDown(KEY_LEFT) then - pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) - elseif input.IsKeyDown(KEY_RIGHT) then - pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) - end - - if input.IsKeyDown(KEY_UP) or input.IsKeyDown(KEY_PAGEUP) then - local added_nodes = get_added_nodes(self) - local offset = input.IsKeyDown(KEY_PAGEUP) and 10 or 1 - if not self.scrolled_up or self.scrolled_up < os.clock() then - for i,v in ipairs(added_nodes) do - if v == pace.current_part.pace_tree_node then - local node = added_nodes[i - offset] or added_nodes[1] - if node then - node:DoClick() - scroll_to_node(self, node) - break - end - end - end - - self.scrolled_up = self.scrolled_up or os.clock() + 0.4 - end - else - self.scrolled_up = nil - end - - if input.IsKeyDown(KEY_DOWN) or input.IsKeyDown(KEY_PAGEDOWN) then - local added_nodes = get_added_nodes(self) - local offset = input.IsKeyDown(KEY_PAGEDOWN) and 10 or 1 - if not self.scrolled_down or self.scrolled_down < os.clock() then - for i,v in ipairs(added_nodes) do - if v == pace.current_part.pace_tree_node then - local node = added_nodes[i + offset] or added_nodes[#added_nodes] - if node then - node:DoClick() - --scroll_to_node(self, node) - break - end - end - end - - self.scrolled_down = self.scrolled_down or os.clock() + 0.4 - end - else - self.scrolled_down = nil - end - end - for _, part in pairs(pac.GetLocalParts()) do local node = part.pace_tree_node if not node or not node:IsValid() then continue end @@ -156,8 +97,8 @@ do if not node.Icon.event_icon then local pnl = vgui.Create("DImage", node.Icon) pnl:SetImage("icon16/clock_red.png") - pnl:SetSize(8, 8) - pnl:SetPos(8, 8) + pnl:SetSize(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1))) + pnl:SetPos(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1))) pnl:SetVisible(false) node.Icon.event_icon = pnl end @@ -183,8 +124,83 @@ do end end end + + function DoScrollControl(self, action) + pace.BulkSelectKey = input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString()) + if + pace.current_part.pace_tree_node and + pace.current_part.pace_tree_node:IsValid() and not + ( + pace.BusyWithProperties:IsValid() or + pace.ActiveSpecialPanel:IsValid() or + pace.editing_viewmodel or + pace.editing_hands or + pace.properties.search:HasFocus() + ) and + not gui.IsConsoleVisible() + then + + if action == "editor_node_collapse" then + pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) + elseif action == "editor_node_expand" then + pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) + end + + if action == "editor_up" or action == "editor_pageup" then + local added_nodes = get_added_nodes(self) + local offset = action == "editor_pageup" and 10 or 1 + if not self.scrolled_up or self.scrolled_up < os.clock() then + for i,v in ipairs(added_nodes) do + if v == pace.current_part.pace_tree_node then + local node = added_nodes[i - offset] or added_nodes[1] + if node then + node:DoClick() + scroll_to_node(self, node) + if input.IsKeyDown(pace.BulkSelectKey) then pace.DoBulkSelect(node.part, true) end + break + end + end + end + + self.scrolled_up = self.scrolled_up or os.clock() + 0.4 + end + else + self.scrolled_up = nil + end + + if action == "editor_down" or action == "editor_pagedown" then + local added_nodes = get_added_nodes(self) + local offset = action == "editor_pagedown" and 10 or 1 + if not self.scrolled_down or self.scrolled_down < os.clock() then + for i,v in ipairs(added_nodes) do + if v == pace.current_part.pace_tree_node then + local node = added_nodes[i + offset] or added_nodes[#added_nodes] + if node then + node:DoClick() + if input.IsKeyDown(pace.BulkSelectKey) then pace.DoBulkSelect(node.part, true) end + --scroll_to_node(self, node) + break + end + end + end + + self.scrolled_down = self.scrolled_down or os.clock() + 0.4 + end + else + self.scrolled_down = nil + end + end + end + + function pace.DoScrollControls(action) + DoScrollControl(pace.tree, action) + end + end + + + function PANEL:OnMouseReleased(mc) if mc == MOUSE_RIGHT then pace.Call("PartMenu") @@ -352,7 +368,7 @@ function PANEL:AddNode(...) local add_button = node:Add("DImageButton") add_button:SetImage(pace.MiscIcons.new) - add_button:SetSize(16, 16) + add_button:SetSize(16*GetConVar("pac_editor_scale"):GetFloat(), 16*GetConVar("pac_editor_scale"):GetFloat()) add_button:SetVisible(false) add_button.DoClick = function() add_parts_menu(node) pace.Call("PartSelected", node.part) end add_button.DoRightClick = function() node:DoRightClick() end @@ -454,6 +470,7 @@ function PANEL:PopulateParts(node, parts, children) elseif isstring(part.Icon) then part_node.Icon:SetImage(part.Icon) end + part_node.Icon:SetSize(16 * GetConVar("pac_editor_scale"):GetFloat(),16 * GetConVar("pac_editor_scale"):GetFloat()) self:PopulateParts(part_node, part:GetChildren(), true) @@ -499,8 +516,8 @@ end function PANEL:Populate(reset) - self:SetLineHeight(18) - self:SetIndentSize(2) + self:SetLineHeight(18 * (1 + (GetConVar("pac_editor_scale"):GetFloat()-1))) + self:SetIndentSize(10) for key, node in pairs(self.parts) do if reset or (not node.part or not node.part:IsValid()) then @@ -530,6 +547,7 @@ local function remove_node(part) part.pace_tree_node:GetRoot().m_pSelectedItem = nil part.pace_tree_node:Remove() pace.RefreshTree() + end end @@ -564,7 +582,42 @@ pac.AddHook("pace_OnVariableChanged", "pace_create_tree_nodes", function(part, k end end) +local function refresh_events_gated() + pace.final_scheduled_event_refresh = pace.final_scheduled_event_refresh or CurTime() + 0.2 + pace.event_refresh_spam_time = CurTime() + hook.Add("Tick", "pace_refresh_events", function() + if CurTime() < pace.event_refresh_spam_time + 0.2 then return end + if CurTime() > pace.final_scheduled_event_refresh then + pace.RefreshEvents() + pace.final_scheduled_event_refresh = nil + hook.Remove("Tick", "pace_refresh_events") + end + end) +end + +function pace.RefreshEvents() + local events = {} + for _, part in pairs(pac.GetLocalParts()) do + if part.ClassName == "event" then + events[part] = part + end + end + local no_events = table.Count(events) == 0 + + for _, child in pairs(pac.GetLocalParts()) do + child.active_events = {} + child.active_events_ref_count = 0 + if not no_events then + for _,event in pairs(events) do + event:OnThink() + end + end + child:CallRecursive("CalcShowHide", false) + end +end + function pace.RefreshTree(reset) + --print("pace.RefreshTree("..tostring(reset)..")") if pace.tree:IsValid() then timer.Create("pace_refresh_tree", 0.01, 1, function() if pace.tree:IsValid() then @@ -573,8 +626,10 @@ function pace.RefreshTree(reset) pace.TrySelectPart() end + refresh_events_gated() end) end + end if Entity(1):IsPlayer() and not PAC_RESTART and not VLL2_FILEDEF then diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index e1a189c4a..567a0dcac 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1,10 +1,19 @@ -include("pac3/editor/client/panels/properties.lua") +--include("pac3/editor/client/panels/properties.lua") +include("popups_part_tutorials.lua") + local L = pace.LanguageString -local BulkSelectList = {} -local BulkSelectUIDs = {} +pace.BulkSelectList = {} +pace.BulkSelectUIDs = {} pace.BulkSelectClipboard = {} 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"} + +pace.operations_default = {"wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "spacer", "save", "load", "spacer", "remove"} + +pace.operations_experimental = {"help_part_info", "wear", "copy", "paste", "cut", "paste_properties", "clone", "bulk_select", "spacer", "registered_parts", "spacer", "bulk_apply_properties", "partsize_info", "copy_uid", "spacer", "save", "load", "spacer", "remove"} +pace.operations_bulk_poweruser = {"bulk_select","clone", "registered_parts", "spacer", "copy", "paste", "cut", "spacer", "wear", "save", "load", "partsize_info"} +pace.operations_order = pace.operations_experimental 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") @@ -13,6 +22,7 @@ CreateConVar( "pac_hover_halo_limit", 100, FCVAR_ARCHIVE, "max number of parts b CreateConVar( "pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") CreateConVar( "pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") + -- load only when hovered above local function add_expensive_submenu_load(pnl, callback) local old = pnl.OnCursorEntered @@ -24,6 +34,7 @@ local function add_expensive_submenu_load(pnl, callback) end function pace.WearParts(temp_wear_filter) + pace.still_loading_wearing = true local allowed, reason = pac.CallHook("CanWearParts", pac.LocalPlayer) @@ -31,6 +42,8 @@ function pace.WearParts(temp_wear_filter) pac.Message(reason or "the server doesn't want you to wear parts for some reason") return end + + pace.still_loading_wearing = false return pace.WearOnServer(temp_wear_filter) end @@ -121,10 +134,11 @@ local last_select_was_span = false local last_direction function pace.OnPartSelected(part, is_selecting) - - if input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then + pace.delaybulkselect = pace.delaybulkselect or 0 --a time updated in shortcuts.lua to prevent common pac operations from triggering bulk selection + local bulk_key_pressed = input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) + if RealTime() > pace.delaybulkselect and bulk_key_pressed and not input.IsKeyDown(input.GetKeyCode("v")) and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then --jumping multi-select if holding shift + ctrl - if input.IsControlDown() and input.IsShiftDown() and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then + if bulk_key_pressed and input.IsShiftDown() then --ripped some local functions from tree.lua local added_nodes = {} for i,v in ipairs(pace.tree.added_nodes) do @@ -311,7 +325,7 @@ do -- menu if not pace.Active or refresh_halo_hook then hook.Remove('PreDrawHalos', "BulkSelectHighlights") end - +//@note registered parts function pace.AddRegisteredPartsToMenu(menu, parent) local partsToShow = {} local clicked = false @@ -356,16 +370,39 @@ do -- menu end end end + return newMenuEntry end local sortedTree = {} - + local PartStructure = {} + local Groups = {} + local Parts = pac.GetRegisteredParts() for _, part in pairs(pace.GetRegisteredParts()) do + local class = part.ClassName + local groupname = "other" + local group = part.Group or part.Groups or "other" - + --print(group) if isstring(group) then + --MsgC(Color(0,255,0), "\t" .. group .. "\n") + groupname = group group = {group} + else + --PrintTable(group) + Groups[groupname] = Groups[groupname] or {} + for i,v in ipairs(group) do + Groups[v] = Groups[v] or {} + Groups[v][class] = Groups[v][class] or class + end end + + Groups[groupname] = Groups[groupname] or group + + Groups[groupname][class] = Groups[groupname][class] or class + + --[[if isstring(group) then + group = {group} + end]] for i, name in ipairs(group) do if not sortedTree[name] then @@ -384,52 +421,118 @@ do -- menu end end end - + + --file.Write("pac_partgroups.txt", util.TableToKeyValues(Groups)) + local other = sortedTree.other sortedTree.other = nil - for group, groupData in pairs(sortedTree) do - local sub, pnl = menu:AddSubMenu(groupData.name, function() - if groupData.group_class_name then - pace.RecordUndoHistory() - pace.Call("CreatePart", groupData.group_class_name, nil, nil, parent) - pace.RecordUndoHistory() + if not file.Exists("pac3_config/pac_part_categories.txt", "DATA") then + for group, groupData in pairs(sortedTree) do + local sub, pnl = menu:AddSubMenu(groupData.name, function() + if groupData.group_class_name then + pace.RecordUndoHistory() + pace.Call("CreatePart", groupData.group_class_name, nil, nil, parent) + pace.RecordUndoHistory() + end + end) + + sub.GetDeleteSelf = function() return false end + + if groupData.icon then + pnl:SetImage(groupData.icon) end - end) - sub.GetDeleteSelf = function() return false end + trap = false + table.sort(groupData.parts, function(a, b) return a.ClassName < b.ClassName end) + for i, part in ipairs(groupData.parts) do + add_part(sub, part) + end - if groupData.icon then - pnl:SetImage(groupData.icon) - end + hook.Add('Think', sub, function() + local ctrl = input.IsControlDown() - trap = false - table.sort(groupData.parts, function(a, b) return a.ClassName < b.ClassName end) - for i, part in ipairs(groupData.parts) do - add_part(sub, part) - end + if clicked and not ctrl then + sub:SetDeleteSelf(true) + RegisterDermaMenuForClose(sub) + CloseDermaMenus() + return + end - hook.Add('Think', sub, function() - local ctrl = input.IsControlDown() + sub:SetDeleteSelf(not ctrl) + end) - if clicked and not ctrl then - sub:SetDeleteSelf(true) - RegisterDermaMenuForClose(sub) - CloseDermaMenus() - return - end + hook.Add('CloseDermaMenus', sub, function() + if input.IsControlDown() and trap then + trap = false + sub:SetVisible(true) + end - sub:SetDeleteSelf(not ctrl) - end) + RegisterDermaMenuForClose(sub) + end) + end + else --custom part categories + pace.partgroups = pace.partgroups or util.KeyValuesToTable(file.Read("pac3_config/pac_part_categories.txt", "DATA")) + Groups = pace.partgroups + --group is the group name + --tbl is a shallow table with part class names + --PrintTable(Groups) + for group, tbl in pairs(Groups) do + + local sub, pnl = menu:AddSubMenu(group, function() + if Parts[group] then + if group == "entity" then + pace.RecordUndoHistory() + pace.Call("CreatePart", "entity2", nil, nil, parent) + pace.RecordUndoHistory() + elseif group == "model" then + pace.RecordUndoHistory() + pace.Call("CreatePart", "model2", nil, nil, parent) + pace.RecordUndoHistory() + else + pace.RecordUndoHistory() + pace.Call("CreatePart", group, nil, nil, parent) + pace.RecordUndoHistory() + end + end + end) - hook.Add('CloseDermaMenus', sub, function() - if input.IsControlDown() and trap then - trap = false - sub:SetVisible(true) + +--@note partmenu definer + sub.GetDeleteSelf = function() return false end + + if tbl["icon"] then + --print(tbl["icon"]) + if pace.MiscIcons[string.gsub(tbl["icon"], "pace.MiscIcons.", "")] then + pnl:SetImage(pace.MiscIcons[string.gsub(tbl["icon"], "pace.MiscIcons.", "")]) + else + local img = string.gsub(tbl["icon"], ".png", "") --remove the png extension + img = string.gsub(img, "icon16/", "") --remove the icon16 base path + img = "icon16/" .. img .. ".png" --why do this? to be able to write any form and let the program fix the form + pnl:SetImage(img) + end + elseif Parts[group] then + pnl:SetImage(Parts[group].Icon) + else + pnl:SetImage("icon16/page_white.png") end - - RegisterDermaMenuForClose(sub) - end) + if tbl["tooltip"] then + pnl:SetTooltip(tbl["tooltip"]) + end + --trap = false + table.sort(tbl, function(a, b) return a < b end) + for i, class in pairs(tbl) do + if isstring(i) and Parts[class] then + local tooltip = pace.TUTORIALS.PartInfos[class].tooltip + + if not tooltip or tooltip == "" then tooltip = "no information available" end + if #i > 2 then + local part_submenu = add_part(sub, Parts[class]) + part_submenu:SetTooltip(tooltip) + end + end + end + end end for i,v in ipairs(other.parts) do @@ -453,6 +556,7 @@ do -- menu local base = vgui.Create("EditablePanel") base:SetPos(input.GetCursorPos()) base:SetSize(200, 300) + base:MakePopup() function base:OnRemove() @@ -465,7 +569,7 @@ do -- menu edit:RequestFocus() edit:SetUpdateOnType(true) - local result = base:Add("DPanel") + local result = base:Add("DScrollPanel") result:Dock(FILL) function edit:OnEnter() @@ -528,7 +632,9 @@ do -- menu line:Dock(TOP) end - base:SetHeight(20 * #result.found + edit:GetTall()) + --base:SetHeight(20 * #result.found + edit:GetTall()) + base:SetHeight(600 + edit:GetTall()) + end edit:OnValueChange("") @@ -581,7 +687,7 @@ do -- menu end function pace.RemovePart(obj) - if table.HasValue(BulkSelectList,obj) then table.RemoveByValue(BulkSelectList,obj) end + if table.HasValue(pace.BulkSelectList,obj) then table.RemoveByValue(pace.BulkSelectList,obj) end pace.RecordUndoHistory() obj:Remove() @@ -594,13 +700,116 @@ do -- menu end end + function pace.SwapBaseMovables(obj1, obj2, promote) + if not obj1 or not obj2 then return end + if not obj1.Position or not obj1.Angles or not obj2.Position or not obj2.Angles then return end + local base_movable_fields = { + "Position", "PositionOffset", "Angles", "AngleOffset", "EyeAngles", "AimPart", "AimPartName" + } + local a_part = obj2 + local b_part = obj1 + + if promote then --obj1 takes place of obj2 up or down the hierarchy + if obj1.Parent == obj2 then + a_part = obj2 + b_part = obj1 + elseif obj2.Parent == obj1 then + a_part = obj1 + b_part = obj2 + end + end + + for i,field in ipairs(base_movable_fields) do + local a_val = a_part["Get"..field](a_part) + local b_val = b_part["Get"..field](b_part) + a_part["Set"..field](a_part, b_val) + b_part["Set"..field](b_part, a_val) + end + + if promote then + b_part:SetParent(a_part.Parent) b_part:SetEditorExpand(true) + a_part:SetParent(b_part) a_part:SetEditorExpand(true) + else + a_part:SetParent(b_part.Parent) + b_part:SetParent(a_part.Parent) + end + pace.RefreshTree() + end + + function pace.SubstituteBaseMovable(obj,action) + if action == "create_parent" then + Derma_StringRequest("Create substitute parent", "Select a class name to create a parent", "model2", + function(str) + if str == "model" then str = "model2" end --I don't care, stop using legacy + local newObj = pac.CreatePart(str) + if not IsValid(newObj) then return end + + newObj:SetParent(obj.Parent) + obj:SetParent(newObj) + + for i,v in pairs(obj:GetChildren()) do + v:SetParent(newObj) + end + + newObj:SetPosition(obj.Position) + newObj:SetPositionOffset(obj.PositionOffset) + newObj:SetAngles(obj.Angles) + newObj:SetAngleOffset(obj.AngleOffset) + newObj:SetEyeAngles(obj.EyeAngles) + newObj:SetAimPart(obj.AimPart) + newObj:SetAimPartName(obj.AimPartName) + newObj:SetBone(obj.Bone) + newObj:SetEditorExpand(true) + + obj:SetPosition(Vector(0,0,0)) + obj:SetPositionOffset(Vector(0,0,0)) + obj:SetAngles(Angle(0,0,0)) + obj:SetAngleOffset(Angle(0,0,0)) + obj:SetEyeAngles(false) + obj:SetAimPart(nil) + obj:SetAimPartName("") + obj:SetBone("head") + + pace.RefreshTree() + end) + elseif action == "reorder_child" then + if obj.Parent then + if obj.Parent.Position and obj.Parent.Angles then + pace.SwapBaseMovables(obj, obj.Parent, true) + end + end + pace.RefreshTree() + elseif action == "cast" then + Derma_StringRequest("Cast", "Select a class name to convert to. Make sure you know what you\'re doing! It will do a pac_restart after!", "model2", + function(str) + if str == obj.ClassName then return end + if str == "model" then str = "model2" end --I don't care, stop using legacy + local uid = obj.UniqueID + pace.RefreshTree() + pace.Editor:InvalidateLayout() + pace.RefreshTree() + + obj.ClassName = str + + timer.Simple(0, function() + _G.pac_Restart() + if str == "model2" then + obj = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid) + obj:SetModel("models/pac/default.mdl") + end + + end) + end) + end + end + function pace.ClearBulkList() - for _,v in ipairs(BulkSelectList) do + for _,v in ipairs(pace.BulkSelectList) do if v.pace_tree_node ~= nil then v.pace_tree_node:SetAlpha( 255 ) end v:SetInfo() end - BulkSelectList = {} - print("Bulk list deleted!") + pace.BulkSelectList = {} + pac.Message("Bulk list deleted!") --surface.PlaySound('buttons/button16.wav') end //@note pace.DoBulkSelect @@ -610,11 +819,11 @@ do -- menu if obj.ClassName == "timeline_dummy_bone" then return end local selected_part_added = false --to decide the sound to play afterward - BulkSelectList = BulkSelectList or {} - if (table.HasValue(BulkSelectList, obj)) then + pace.BulkSelectList = pace.BulkSelectList or {} + if (table.HasValue(pace.BulkSelectList, obj)) then pace.RemoveFromBulkSelect(obj) selected_part_added = false - elseif (BulkSelectList[obj] == nil) then + elseif (pace.BulkSelectList[obj] == nil) then pace.AddToBulkSelect(obj) selected_part_added = true for _,v in ipairs(obj:GetChildrenList()) do @@ -623,7 +832,7 @@ do -- menu end --check parents and children - for _,v in ipairs(BulkSelectList) do + for _,v in ipairs(pace.BulkSelectList) do if table.HasValue(v:GetChildrenList(), obj) then --print("selected part is already child to a bulk-selected part!") pace.RemoveFromBulkSelect(obj) @@ -639,15 +848,11 @@ do -- menu if not silent then if selected_part_added then surface.PlaySound('buttons/button1.wav') - --test print - for i,v in ipairs(BulkSelectList) do - print("["..i.."] = "..v.UniqueID) - end - print("\n") + else surface.PlaySound('buttons/button16.wav') end end - if table.IsEmpty(BulkSelectList) then + if table.IsEmpty(pace.BulkSelectList) then --remove halo hook hook.Remove('PreDrawHalos', "BulkSelectHighlights") else @@ -663,30 +868,37 @@ do -- menu end) end - for _,v in ipairs(BulkSelectList) do + for _,v in ipairs(pace.BulkSelectList) do --v.pace_tree_node:SetAlpha( 150 ) end end function pace.RemoveFromBulkSelect(obj) - table.RemoveByValue(BulkSelectList, obj) + table.RemoveByValue(pace.BulkSelectList, obj) obj.pace_tree_node:SetAlpha( 255 ) obj:SetInfo() --RebuildBulkHighlight() end function pace.AddToBulkSelect(obj) - table.insert(BulkSelectList, obj) + table.insert(pace.BulkSelectList, obj) if obj.pace_tree_node == nil then return end obj:SetInfo("selected in bulk select") obj.pace_tree_node:SetAlpha( 150 ) --RebuildBulkHighlight() end + function pace.BulkHide() + if #pace.BulkSelectList == 0 then return end + local first_bool = pace.BulkSelectList[1]:GetHide() + for _,v in ipairs(pace.BulkSelectList) do + v:SetHide(not first_bool) + end + end //@note apply properties function pace.BulkApplyProperties(obj, policy) local basepart = obj - --[[if not table.HasValue(BulkSelectList,obj) then - basepart = BulkSelectList[1] + --[[if not table.HasValue(pace.BulkSelectList,obj) then + basepart = pace.BulkSelectList[1] end]] local Panel = vgui.Create( "DFrame" ) @@ -735,7 +947,7 @@ do -- menu for _,prop in pairs(basepart:GetProperties()) do local shared = true - for _,part2 in pairs(BulkSelectList) do + for _,part2 in pairs(pace.BulkSelectList) do if basepart ~= part2 and basepart.ClassName ~= part2.ClassName then if part2["Get" .. prop["key"]] == nil then if policy == "harsh" then shared = false end @@ -752,7 +964,7 @@ do -- menu if policy == "lenient" then local initial_shared_properties = table.Copy(shared_properties) local initial_shared_udata_properties = table.Copy(shared_udata_properties) - for _,part2 in pairs(BulkSelectList) do + for _,part2 in pairs(pace.BulkSelectList) do for _,prop in ipairs(part2:GetProperties()) do if not (table.HasValue(shared_properties, prop["key"]) or table.HasValue(shared_udata_properties, "event_udata_"..prop["key"])) then if part2["Get" .. prop["key"]] ~= nil then @@ -777,7 +989,7 @@ do -- menu VAR_PANEL_BUTTON:SetPos(400,0) local VAR_PANEL_EDITZONE local var_type - for _,testpart in ipairs(BulkSelectList) do + for _,testpart in ipairs(pace.BulkSelectList) do if testpart["Get" .. v] ~= nil then @@ -814,7 +1026,7 @@ do -- menu VAR_PANEL:Dock( TOP ) VAR_PANEL:DockMargin( 5, 0, 0, 5 ) VAR_PANEL_BUTTON.DoClick = function() - for i,part in pairs(BulkSelectList) do + for i,part in pairs(pace.BulkSelectList) do local sent_var if var_type == "number" then sent_var = VAR_PANEL_EDITZONE:GetValue() @@ -945,7 +1157,7 @@ do -- menu VAR_PANEL:DockMargin( 5, 0, 0, 5 ) VAR_PANEL_BUTTON.DoClick = function() - for i,part in pairs(BulkSelectList) do + for i,part in pairs(pace.BulkSelectList) do if part.ClassName == "event" and part.Event == basepart.Event then local sent_var if var_type == "number" then @@ -1044,7 +1256,7 @@ do -- menu function pace.BulkCutPaste(obj) pace.RecordUndoHistory() - for _,v in ipairs(BulkSelectList) do + for _,v in ipairs(pace.BulkSelectList) do --if a part is inserted onto itself, it should instead serve as a parent if v ~= obj then v:SetParent(obj) end end @@ -1052,9 +1264,35 @@ do -- menu pace.RefreshTree() end + function pace.BulkCutPasteOrdered() --two-state operation + --first to define an ordered list of parts to move, from bulk select + --second to transfer these parts to bulk select list + if not pace.ordered_operation_readystate then + pace.temp_bulkselect_orderedlist = {} + for i,v in ipairs(pace.BulkSelectList) do + pace.temp_bulkselect_orderedlist[i] = v + end + pace.ordered_operation_readystate = true + pace.ClearBulkList() + pace.FlashNotification("Selected " .. #pace.temp_bulkselect_orderedlist .. " parts for Ordered Insert. Now select " .. #pace.temp_bulkselect_orderedlist .. " parts destinations.") + surface.PlaySound("buttons/button4.wav") + else + if #pace.temp_bulkselect_orderedlist == #pace.BulkSelectList then + pace.RecordUndoHistory() + for i,v in ipairs(pace.BulkSelectList) do + pace.temp_bulkselect_orderedlist[i]:SetParent(v) + end + pace.RecordUndoHistory() + pace.RefreshTree() + surface.PlaySound("buttons/button6.wav") + end + pace.ordered_operation_readystate = false + end + end + function pace.BulkCopy(obj) - if #BulkSelectList == 1 then pace.Copy(obj) end --at least if there's one selected, we can take it that we want to copy that part - pace.BulkSelectClipboard = table.Copy(BulkSelectList) --if multiple parts are selected, copy it to a new bulk clipboard + if #pace.BulkSelectList == 1 then pace.Copy(obj) end --at least if there's one selected, we can take it that we want to copy that part + pace.BulkSelectClipboard = table.Copy(pace.BulkSelectList) --if multiple parts are selected, copy it to a new bulk clipboard print("copied: ") TestPrintTable(pace.BulkSelectClipboard,"pace.BulkSelectClipboard") end @@ -1074,8 +1312,8 @@ do -- menu function pace.BulkPasteFromBulkSelectToSinglePart(obj) --paste bulk selection into one part pace.RecordUndoHistory() - if not table.IsEmpty(BulkSelectList) then - for _,v in ipairs(BulkSelectList) do + if not table.IsEmpty(pace.BulkSelectList) then + for _,v in ipairs(pace.BulkSelectList) do local newObj = pac.CreatePart(v.ClassName) newObj:SetTable(v:ToTable(), true) newObj:SetParent(obj) @@ -1086,8 +1324,8 @@ do -- menu function pace.BulkPasteFromSingleClipboard() --paste the normal clipboard into each bulk select item pace.RecordUndoHistory() - if not table.IsEmpty(BulkSelectList) then - for _,v in ipairs(BulkSelectList) do + if not table.IsEmpty(pace.BulkSelectList) then + for _,v in ipairs(pace.BulkSelectList) do local newObj = pac.CreatePart(pace.Clipboard.self.ClassName) newObj:SetTable(pace.Clipboard, true) newObj:SetParent(v) @@ -1097,10 +1335,16 @@ do -- menu --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end) end + function pace.BulkPasteFromBulkClipboardToBulkSelect() + for _,v in ipairs(pace.BulkSelectList) do + pace.BulkPasteFromBulkClipboard(v) + end + end + function pace.BulkRemovePart() pace.RecordUndoHistory() - if not table.IsEmpty(BulkSelectList) then - for _,v in ipairs(BulkSelectList) do + if not table.IsEmpty(pace.BulkSelectList) then + for _,v in ipairs(pace.BulkSelectList) do v:Remove() if not v:HasParent() and v.ClassName == "group" then @@ -1113,16 +1357,28 @@ do -- menu pace.ClearBulkList() --timer.Simple(0.1, function BulkSelectRefreshFadedNodes() end) end + + function pace.CopyUID(obj) + pace.Clipboard = obj.UniqueID + SetClipboardText("\"" .. obj.UniqueID .. "\"") + pace.FlashNotification(tostring(obj) .. " UID " .. obj.UniqueID .. " has been copied") + end //@note part menu function pace.OnPartMenu(obj) local menu = DermaMenu() menu:SetPos(input.GetCursorPos()) - - if obj then + --new_operations_order + --default_operations_order + + for _,option_name in ipairs(pace.operations_order) do + pace.addPartMenuComponent(menu, obj, option_name) + end + + --[[if obj then if not obj:HasParent() then menu:AddOption(L"wear", function() pace.SendPartToServer(obj) - BulkSelectList = {} + pace.BulkSelectList = {} end):SetImage(pace.MiscIcons.wear) end @@ -1132,13 +1388,95 @@ do -- menu menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) + local part_size_info, psi_icon = menu:AddSubMenu(L"get part size information", function() + local function GetTableSizeInfo(obj_arg) + if not IsValid(obj_arg) then return { + raw_bytes = 0, + info = "" + } end + local charsize = #util.TableToJSON(obj_arg:ToTable()) + + local kilo_range = -1 + local remainder = charsize*2 + while remainder / 1000 > 1 do + kilo_range = kilo_range + 1 + remainder = remainder / 1000 + end + local unit = "" + if kilo_range == -1 then + unit = "B" + elseif kilo_range == 0 then + unit = "KB" + elseif (kilo_range == 1) then + unit = "MB" + elseif (kilo_range == 2) then + unit = "GB" + end + return { + raw_bytes = charsize*2, + info = "raw JSON table size: " .. charsize*2 .. " bytes (" .. remainder .. " " .. unit .. ")" + } + end + + local part_size_info = GetTableSizeInfo(obj) + local part_size_info_root = GetTableSizeInfo(obj:GetRootPart()) + + local part_size_info_root_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_root.raw_bytes,1) .. "% share of root " + + local part_size_info_parent + local part_size_info_parent_processed + if IsValid(obj.Parent) then + part_size_info_parent = GetTableSizeInfo(obj.Parent) + part_size_info_parent_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_parent.raw_bytes,1) .. "% share of parent " + pac.Message( + obj, " " .. + part_size_info.info.."\n".. + part_size_info_parent_processed,obj.Parent,"\n".. + part_size_info_root_processed,obj:GetRootPart() + ) + else + pac.Message( + obj, " " .. + part_size_info.info.."\n".. + part_size_info_root_processed,obj:GetRootPart() + ) + end + + end) + psi_icon:SetImage('icon16/drive.png') + + part_size_info:AddOption(L"from bulk select", function() + local cumulative_bytes = 0 + for _,v in pairs(pace.BulkSelectList) do + cumulative_bytes = cumulative_bytes + 2*#util.TableToJSON(v:ToTable()) + end + local kilo_range = -1 + local remainder = cumulative_bytes + while remainder / 1000 > 1 do + kilo_range = kilo_range + 1 + remainder = remainder / 1000 + end + local unit = "" + if kilo_range == -1 then + unit = "B" + elseif kilo_range == 0 then + unit = "KB" + elseif (kilo_range == 1) then + unit = "MB" + elseif (kilo_range == 2) then + unit = "GB" + end + pac.Message("Bulk selected parts total " .. remainder .. unit) + end + ) + local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end) bap_icon:SetImage('icon16/table_multiple.png') bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end) bulk_apply_properties:AddOption("Policy: lenient filtering", function() pace.BulkApplyProperties(obj, "lenient") end) --bulk select - bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#BulkSelectList..")", function() pace.DoBulkSelect(obj) end) + bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#pace.BulkSelectList..")", function() pace.DoBulkSelect(obj) end) bs_icon:SetImage('icon16/table_multiple.png') bulk_menu.GetDeleteSelf = function() return false end @@ -1178,7 +1516,7 @@ do -- menu end):SetImage('icon16/arrow_join.png') bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function() - for _,v in ipairs(BulkSelectList) do + for _,v in ipairs(pace.BulkSelectList) do pace.BulkPasteFromBulkClipboard(v) end end):SetImage('icon16/arrow_divide.png') @@ -1187,13 +1525,13 @@ do -- menu bulk_menu:AddOption(L"Bulk paste properties from selected part", function() pace.Copy(obj) - for _,v in ipairs(BulkSelectList) do + for _,v in ipairs(pace.BulkSelectList) do pace.PasteProperties(v) end end):SetImage(pace.MiscIcons.replace) bulk_menu:AddOption(L"Bulk paste properties from clipboard", function() - for _,v in ipairs(BulkSelectList) do + for _,v in ipairs(pace.BulkSelectList) do pace.PasteProperties(v) end end):SetImage(pace.MiscIcons.replace) @@ -1209,27 +1547,26 @@ do -- menu end):SetImage('icon16/table_delete.png') menu:AddSpacer() - end - - pace.AddRegisteredPartsToMenu(menu, not obj) + end]] + + --pace.AddRegisteredPartsToMenu(menu, not obj) - menu:AddSpacer() + --menu:AddSpacer() - if obj then + --[[if obj then local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end) pnl:SetImage(pace.MiscIcons.save) add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end) - end + end]] - local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) + --[[local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) + pnl:SetImage(pace.MiscIcons.load)]] - pnl:SetImage(pace.MiscIcons.load) - - if obj then + --[[if obj then menu:AddSpacer() menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear) - end + end]] menu:Open() menu:MakePopup() @@ -1238,6 +1575,7 @@ do -- menu function pace.OnNewPartMenu() pace.current_part = NULL local menu = DermaMenu() + menu:MakePopup() menu:SetPos(input.GetCursorPos()) @@ -1254,8 +1592,556 @@ do -- menu end):SetImage(pace.MiscIcons.clear) end + + end +function pace.GetPartSizeInformation(obj) + if not IsValid(obj) then return { raw_bytes = 0, info = "" } end + local charsize = #util.TableToJSON(obj:ToTable()) + local root_charsize = #util.TableToJSON(obj:GetRootPart():ToTable()) + + local roots = {} + local all_charsize = 0 + for i,v in pairs(pac.GetLocalParts()) do + roots[v:GetRootPart()] = v:GetRootPart() + end + for i,v in pairs(roots) do + all_charsize = all_charsize + #util.TableToJSON(v:ToTable()) + end + + return { + raw_bytes = charsize*2, + info = "raw JSON table size: " .. charsize*2 .. " bytes (" .. string.NiceSize(charsize*2) .. ")", + root_share_fraction = math.Round(charsize / root_charsize, 1), + root_share_percent = math.Round(100 * charsize / root_charsize, 1), + all_share_fraction = math.Round(charsize / all_charsize, 1), + all_share_percent = math.Round(100 * charsize / all_charsize, 1), + all_size_raw_bytes = all_charsize*2, + all_size_nice = string.NiceSize(all_charsize*2) + } +end + +function pace.addPartMenuComponent(menu, obj, option_name) + if option_name == "save" then + if obj then + local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end) + pnl:SetImage(pace.MiscIcons.save) + add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end) + end + elseif option_name == "load" then + local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) + add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) + pnl:SetImage(pace.MiscIcons.load) + elseif option_name == "wear" then + if obj then + if not obj:HasParent() then + menu:AddOption(L"wear", function() + pace.SendPartToServer(obj) + pace.BulkSelectList = {} + end):SetImage(pace.MiscIcons.wear) + end + end + elseif option_name == "remove" then + if obj then + menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear) + end + elseif option_name == "copy" then + local menu2, pnl = menu:AddSubMenu(L"copy", function() pace.Copy(obj) end) + pnl:SetIcon(pace.MiscIcons.copy) + --menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy) + menu2:AddOption(L"Copy part UniqueID", function() pace.CopyUID(obj) end):SetImage(pace.MiscIcons.uniqueid) + elseif option_name == "paste" then + menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste) + elseif option_name == "cut" then + menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png') + elseif option_name == "paste_properties" then + menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) + elseif option_name == "clone" then + menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) + elseif option_name == "partsize_info" then + local function GetTableSizeInfo(obj_arg) + return pace.GetPartSizeInformation(obj_arg) + end + local part_size_info, psi_icon = menu:AddSubMenu(L"get part size information", function() + local part_size_info = GetTableSizeInfo(obj) + local part_size_info_root = GetTableSizeInfo(obj:GetRootPart()) + + local part_size_info_root_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_root.raw_bytes,1) .. "% share of root " + + local part_size_info_parent + local part_size_info_parent_processed + if IsValid(obj.Parent) then + part_size_info_parent = GetTableSizeInfo(obj.Parent) + part_size_info_parent_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_parent.raw_bytes,1) .. "% share of parent " + pac.Message( + obj, " " .. + part_size_info.info.."\n".. + part_size_info_parent_processed,obj.Parent,"\n".. + part_size_info_root_processed,obj:GetRootPart() + ) + else + pac.Message( + obj, " " .. + part_size_info.info.."\n".. + part_size_info_root_processed,obj:GetRootPart() + ) + end + + end) + psi_icon:SetImage('icon16/drive.png') + part_size_info:AddOption(L"from bulk select", function() + local cumulative_bytes = 0 + for _,v in pairs(pace.BulkSelectList) do + v.partsize_info = pace.GetPartSizeInformation(v) + cumulative_bytes = cumulative_bytes + 2*#util.TableToJSON(v:ToTable()) + end + + pac.Message("Bulk selected parts total " .. string.NiceSize(cumulative_bytes) .. "\nhere's the breakdown:") + for _,v in pairs(pace.BulkSelectList) do + local partsize_info = pace.GetPartSizeInformation(v) + MsgC(Color(100,255,100), string.NiceSize(partsize_info.raw_bytes)) MsgC(Color(200,200,200), " - ", v, "\n\t ") + MsgC(Color(0,255,255), math.Round(100 * partsize_info.raw_bytes/cumulative_bytes,1) .. "%") + MsgC(Color(200,200,200), " of bulk select total\n\t ") + MsgC(Color(0,255,255), math.Round(100 * partsize_info.raw_bytes/partsize_info.all_size_raw_bytes,1) .. "%") + MsgC(Color(200,200,200), " of total local parts)\n") + end + end) + elseif option_name == "bulk_apply_properties" then + local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end) + bap_icon:SetImage('icon16/application_form.png') + bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end) + bulk_apply_properties:AddOption("Policy: lenient filtering", function() pace.BulkApplyProperties(obj, "lenient") end) + elseif option_name == "bulk_select" then + bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#pace.BulkSelectList..")", function() pace.DoBulkSelect(obj) end) + bs_icon:SetImage('icon16/table_multiple.png') + bulk_menu.GetDeleteSelf = function() return false end + + local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + local info + if mode == 0 then info = "not halo-highlighted" + elseif mode == 1 then info = "automatically halo-highlighted" + elseif mode == 2 then info = "halo-highlighted on custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString() + elseif mode == 3 then info = "halo-highlighted on preset keypress: control" + elseif mode == 4 then info = "halo-highlighted on preset keypress: shift" end + + bulk_menu:AddOption(L"Bulk select info: "..info, function() end):SetImage(pace.MiscIcons.info) + bulk_menu:AddOption(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts", function() end):SetImage(pace.MiscIcons.info) + + bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() + pace.BulkCutPaste(obj) + end):SetImage('icon16/arrow_join.png') + + if not pace.ordered_operation_readystate then + bulk_menu:AddOption(L"prepare Ordered Insert (please select parts in order beforehand)", function() + pace.BulkCutPasteOrdered() + end):SetImage('icon16/text_list_numbers.png') + else + bulk_menu:AddOption(L"do Ordered Insert (select destinations in order)", function() + pace.BulkCutPasteOrdered() + end):SetImage('icon16/arrow_switch.png') + end + + + bulk_menu:AddOption(L"Copy to Bulk Clipboard", function() + pace.BulkCopy(obj) + end):SetImage(pace.MiscIcons.copy) + + bulk_menu:AddSpacer() + + --bulk paste modes + bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function() + pace.BulkPasteFromBulkSelectToSinglePart(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function() + if not pace.Clipboard then pace.Copy(obj) end + pace.BulkPasteFromSingleClipboard() + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function() + pace.BulkPasteFromBulkClipboard(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function() + pace.BulkPasteFromBulkClipboardToBulkSelect() + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk paste properties from selected part", function() + pace.Copy(obj) + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddOption(L"Bulk paste properties from clipboard", function() + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddOption(L"Deploy a numbered command event series ("..#pace.BulkSelectList..")", function() + Derma_StringRequest(L"command series", L"input the base name", "", function(str) + str = string.gsub(str, " ", "") + for i,v in ipairs(pace.BulkSelectList) do + part = pac.CreatePart("event") + part:SetParent(v) + part.Event = "command" + part.Arguments = str..i.."@@0@@0" + end + end) + end):SetImage('icon16/clock.png') + + bulk_menu:AddOption(L"Pack into a new root group", function() + root = pac.CreatePart("group") + for i,v in ipairs(pace.BulkSelectList) do + v:SetParent(root) + end + end):SetImage('icon16/world.png') + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk Delete", function() + pace.BulkRemovePart() + end):SetImage(pace.MiscIcons.clear) + + bulk_menu:AddOption(L"Clear Bulk List", function() + pace.ClearBulkList() + end):SetImage('icon16/table_delete.png') + elseif option_name == "spacer" then + menu:AddSpacer() + elseif option_name == "registered_parts" then + pace.AddRegisteredPartsToMenu(menu, not obj) + elseif option_name == "hide_editor" then + menu:AddOption(L"hide editor / toggle focus", function() pace.Call("ToggleFocus") end):SetImage('icon16/zoom.png') + elseif option_name == "expand_all" then + menu:AddOption(L"expand all", function() + obj:CallRecursive('SetEditorExpand', true) + pace.RefreshTree(true) end):SetImage('icon16/arrow_down.png') + elseif option_name == "collapse_all" then + menu:AddOption(L"collapse all", function() + obj:CallRecursive('SetEditorExpand', false) + pace.RefreshTree(true) end):SetImage('icon16/arrow_in.png') + elseif option_name == "copy_uid" then + local menu2, pnl = menu:AddSubMenu(L"Copy part UniqueID", function() pace.CopyUID(obj) end) + pnl:SetIcon(pace.MiscIcons.uniqueid) + elseif option_name == "help_part_info" then + menu:AddOption(L"View help or info about this part", function() pac.AttachInfoPopupToPart(obj, nil, { + obj_type = GetConVar("pac_popups_preferred_location"):GetString(), + hoverfunc = "open", + pac_part = pace.current_part, + panel_exp_width = 900, panel_exp_height = 400 + }) end):SetImage('icon16/information.png') + elseif option_name == "reorder_movables" then + if obj then + if obj.Position and obj.Angles and obj.PositionOffset then + local substitute, pnl = menu:AddSubMenu("Reorder / replace base movable") + pnl:SetImage('icon16/application_double.png') + substitute:AddOption("Create a parent for position substitution", function() pace.SubstituteBaseMovable(obj, "create_parent") end) + if obj.Parent then + if obj.Parent.Position and obj.Parent.Angles then + substitute:AddOption("Switch with parent", function() pace.SubstituteBaseMovable(obj, "reorder_child") end) + end + end + substitute:AddOption("Switch with another (select two parts with bulk select)", function() pace.SwapBaseMovables(pace.BulkSelectList[1], pace.BulkSelectList[2], false) end) + substitute:AddOption("Recast into new class (warning!)", function() pace.SubstituteBaseMovable(obj, "cast") end) + end + end + end + +end + +function pace.addPartGroupMenuComponent(menu, obj, group_name) + +end + +--destructive tool +function pace.UltraCleanup(obj) + if not obj then return end + + local root = obj:GetRootPart() + local safe_parts = {} + local parts_have_saved_parts = {} + local marked_for_deletion = {} + + local function IsImportantMarked(part) + if part.Notes and part.Notes == "important" then return true end + return false + end + + local function FoundImportantMarkedParent(part) + if IsImportantMarked(part) then return true end + local root = part:GetRootPart() + local parent = part + while parent ~= root do + if parent.Notes and parent.Notes == "important" then return true end + parent = parent:GetParent() + end + return false + end + + local function Important(part) + return IsImportantMarked(part) or FoundImportantMarkedParent(part) + end + + local function CheckPartWithLinkedParts(part) + local found_parts = false + local part_roles = { + ["AimPart"] = nil, --base_movable + ["OutfitPart"] = nil, --projectile bullets + ["EndPoint"] = nil --beams + } + + if part.ClassName == "projectile" then + if part.OutfitPart then + if part.OutfitPart:IsValid() then + part_roles["OutfitPart"] = part.OutfitPart + found_parts = true + end + end + end + + if part.AimPart then + if part.AimPart:IsValid() then + part_roles["AimPart"] = part.AimPart + found_parts = true + end + end + + if part.ClassName == "beam" then + if part.EndPoint then + if part.EndPoint:IsValid() then + part_roles["EndPoint"] = part.EndPoint + found_parts = true + end + end + end + + parts_have_saved_parts[part] = found_parts + if found_parts then + safe_parts[part] = part + for i2,v2 in pairs(part_roles) do + if v2 then + safe_parts[v2] = v2 + end + end + end + end + + local function IsSafe(part) + local safe = true + + if part.Notes then + if #(part.Notes) > 20 then return true end --assume if we write 20 characters in the notes then it's fine to keep it... + end + + if part.ClassName == "event" or part.ClassName == "proxy" or part.ClassName == "command" then + return false + end + + if not part:IsHidden() and not part.Hide then + else + safe = false + if string.find(part.ClassName,"material") then + safe = true + end + end + + return safe + end + + local function IsMildlyRisky(part) + if part.ClassName == "event" or part.ClassName == "proxy" or part.ClassName == "command" then + if not part:IsHidden() and not part.Hide then + return true + end + return false + end + return false + end + + local function IsHangingPart(part) + if IsImportantMarked(part) then return false end + local c = part.ClassName + + --unlooped sounds or 0 volume should be wiped + if c == "sound" or c == "ogg" or c == "webaudio" or c == "sound2" then + if part.Volume == 0 then return true end + if part.Loop ~= nil then + if not part.Loop then return true end + end + if part.PlayCount then + if part.PlayCount == 0 then return true end + end + end + + --fireonce particles should be wiped + if c == "particle" then + if part.NumberParticles == 0 or part.FireOnce then return true end + end + + --0 weight flexes have to be removed + if c == "flex" then + if part.Weight < 0.1 then return true end + end + + if c == "sunbeams" then + if math.abs(part.Multiplier) == 0 then return true end + end + + --other parts to leave forever + if c == "shake" or c == "gesture" then + return true + end + end + + local function FindNearestSafeParent(part) + if not part then return end + local root = part:GetRootPart() + local parent = part:GetParent() + local child = part + local i = 0 + while parent ~= root do + if i > 10 then return parent end + i = i + 1 + if IsSafe(parent) then + return parent + elseif not IsMildlyRisky(parent) then + return parent + elseif not parent:IsHidden() and parent.Hide then + return parent + end + child = parent + parent = parent:GetParent() + end + return parent + end + + + local function SafeRemove(part) + if IsValid(part) then + if IsSafe(part) or Important(part) then + return + elseif IsMildlyRisky(part) then + if table.Count(part:GetChildren()) == 0 then + part:Remove() + end + end + end + end + + --does algorithm needs to be recursive? + --delete absolute unsafes: hiddens. + --now there are safe events. + --extract children into nearest safe parent BUT ONLY DO IT VIA ITS DOMINANT and IF IT'S SAFE + --delete remaining unsafes: events, hiddens, commands ... + --but we still need to check for children to extract! + + local function Move_contents_up(part) --this will be the powerhouse recursor + local parent = FindNearestSafeParent(part) + --print(part, "nearest parent is", parent) + for _,child in pairs(part:GetChildren()) do + if child:IsHidden() or child.Hide then --hidden = delete + marked_for_deletion[child] = child + else --visible = possible container = check + if table.Count(child:GetChildren()) == 0 then --dead end = immediate action + if IsSafe(child) then --safe = keep but now extract it + child:SetParent(parent) + --print(child, "moved to", parent) + safe_parts[child] = child + elseif child:IsHidden() or child.Hide then --hidden = delete + marked_for_deletion[child] = child + end + else --parent = process the children? done by the recursion + --the parent still needs to be moved up + child:SetParent(parent) + + safe_parts[child] = child + Move_contents_up(child) --recurse + end + + end + + end + end + + --find parts to delete + --first pass: absolute unsafes: hidden parts + for i,v in pairs(root:GetChildrenList()) do + if v:IsHidden() or v.Hide then + v:Remove() + end + end + + --second pass: + --A: mark safe parts + --B: extract children in remaining unsafes (i.e. break the chain of an event) + for i,v in pairs(root:GetChildrenList()) do + if IsSafe(v) then + safe_parts[v] = v + CheckPartWithLinkedParts(v) + if IsMildlyRisky(v:GetParent()) then + v:SetParent(v:GetParent():GetParent()) + end + elseif IsMildlyRisky(v) then + Move_contents_up(v) + marked_for_deletion[v] = v + end + + end + --after that, the remaining events etc are marked + for i,v in pairs(root:GetChildrenList()) do + if IsMildlyRisky(v) then + marked_for_deletion[v] = v + end + end + + pace.RefreshTree() + --go through delete tables except when marked as important or those protected by these + for i,v in pairs(marked_for_deletion) do + + local delete = false + + if not safe_parts[v] then + + if v:IsValid() then + delete = true + end + if FoundImportantMarkedParent(v) then + delete = false + end + end + + if delete then SafeRemove(v) end + end + + --third pass: cleanup the last remaining unwanted parts + for i,v in pairs(root:GetChildrenList()) do + --remove remaining events after their children have been freed, and delete parts that don't have durable use, like sounds that aren't looping + if IsMildlyRisky(v) or IsHangingPart(v) then + if not Important(v) then + v:Remove() + end + end + end + + --fourth pass: delete bare containing nothing left + for i,v in pairs(root:GetChildrenList()) do + if v.ClassName == "group" then + local bare = true + for i2,v2 in pairs(v:GetChildrenList()) do + if v2.ClassName ~= "group" then + bare = false + end + end + if bare then v:Remove() end + end + end + pace.RefreshTree() + +end do --hover highlight halo pac.haloex = include("pac3/libraries/haloex.lua") @@ -1335,13 +2221,16 @@ do --hover highlight halo if refresh_halo_hook then return end if part_trace then for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do - v.pace_tree_node:SetAlpha( 255 ) + if v.pace_tree_node then + v.pace_tree_node:SetAlpha( 255 ) + end + end end - for _,v in ipairs(BulkSelectList) do - if not v:IsValid() then table.RemoveByValue(BulkSelectList, v) - else + for _,v in ipairs(pace.BulkSelectList) do + if not v:IsValid() then table.RemoveByValue(pace.BulkSelectList, v) + elseif v.pace_tree_node then v.pace_tree_node:SetAlpha( 150 ) end end @@ -1354,7 +2243,7 @@ do --hover highlight halo local ent = {} --get potential entities and part-children from each parent in the bulk list - for _,v in pairs(BulkSelectList) do --this will get parts + for _,v in pairs(pace.BulkSelectList) do --this will get parts if (v == v:GetRootPart()) then --if this is the root part, send the entity table.insert(ents_tbl,v:GetRootPart():GetOwner()) @@ -1397,7 +2286,7 @@ do --hover highlight halo end function ThinkBulkHighlight() - if table.IsEmpty(BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then + if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then hook.Remove('PreDrawHalos', "BulkSelectHighlights") return end @@ -1417,11 +2306,14 @@ do --hover highlight halo if pulse_rate == 0 then pulse = 1 end local pulseamount - local halo_color + local halo_color = Color(255,255,255) if color_string == "rave" then halo_color = Color(255*((0.33 + SysTime() * pulse_rate/20)%1), 255*((0.66 + SysTime() * pulse_rate/20)%1), 255*((SysTime() * pulse_rate/20)%1), 255) pulseamount = 8 + elseif color_string == "funky" then + halo_color = Color(255*((0.33 + SysTime() * pulse_rate/10)%1), 255*((0.2 + SysTime() * pulse_rate/15)%1), 255*((SysTime() * pulse_rate/15)%1), 255) + pulseamount = 5 elseif color_string == "ocean" then halo_color = Color(0, 80 + 30*(pulse), 200 + 50*(pulse) * 0.5 + 0.5, 255) pulseamount = 4 @@ -1445,4 +2337,18 @@ do --hover highlight halo pac.haloex.Add(tbl, halo_color, 2, 2, pulseamount, true, true, pulseamount, 1, 1) --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) end -end \ No newline at end of file +end + +--custom info panel +--[[args +tbl = { + obj = part.Label, --the associated object, could be a tree label, mouse, part etc. + pac_part = part --a pac part reference, if applicable + obj_type = "pac tree label", + hoverfunc = function() end, + doclickfunc = function() end, + panel_exp_width = 300, panel_exp_height = 200 +} + +]] + diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua new file mode 100644 index 000000000..b0eadfe6a --- /dev/null +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -0,0 +1,1170 @@ +--[[ + This is the framework for popups. This should be expandable for various use cases. + It uses DFrame as a base, overrides the Paint function for a basic fade effect. + + Tutorials will be written here +]] + + +CreateConVar("pac_popups_enable", 1, FCVAR_ARCHIVE, "Enables PAC editor popups. They provide some information but can be annoying") +CreateConVar("pac_popups_preserve_on_autofade", 1, FCVAR_ARCHIVE, "If set to 0, PAC editor popups appear only once and don't reappear when hovering over the part label or pressing F1") +CreateConVar("pac_popups_base_color", "215 230 255", FCVAR_ARCHIVE, "The color of the base filler rectangle for editor popups") +CreateConVar("pac_popups_base_color_pulse", "0", FCVAR_ARCHIVE, "Amount of pulse of the base filler rectangle for editor popups") + +CreateConVar("pac_popups_base_alpha", "0.5", FCVAR_ARCHIVE, "The alpha opacity of the base filler rectangle for editor popups") +CreateConVar("pac_popups_fade_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") +CreateConVar("pac_popups_fade_alpha", "1", FCVAR_ARCHIVE, "The alpha opacity of the fading effect for editor popups") +CreateConVar("pac_popups_text_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") +CreateConVar("pac_popups_verbosity", "beginner tutorial", FCVAR_ARCHIVE, "Sets the amount of information added to PAC editor popups. While in development, there will be limited contextual support. If no special information is defined, it will indicate the part size information. Here are the planned modes: \nbeginner tutorial : Basic tutorials about pac parts, for beginners or casual users looking for a quick reference for what a part does\nReference tutorial : doesn't give part tutorials, but still keeps events' tutorial explanations.\n") +CreateConVar("pac_popups_preferred_location", "pac tree label", FCVAR_ARCHIVE, "Sets the preferred method of PAC editor popups.\n".. + "pac tree label : the part label on the pac tree\n".. + "part world : if part is base_movable, place it next to the part in the viewport\n".. + "screen : static x,y on screen no matter what. That would be at the center\n".. + "cursor : right on the cursor\n".. + "editor bar : next to the toolbar") + + +function pace.OpenPopupConfig() + local master_pnl = vgui.Create("DFrame") + master_pnl:SetTitle("Configure PAC3 popups appearance") + master_pnl:SetSize(400,800) + master_pnl:Center() + + local list_pnl = vgui.Create("DListLayout", master_pnl) + list_pnl:Dock(FILL) + + local basecolor = vgui.Create("DColorMixer") + basecolor:SetSize(400,150) + local col_args = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + basecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + function basecolor:ValueChanged(col) + GetConVar("pac_popups_base_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_popups_base_alpha"):SetString(col.a) + end + local basecolor_pulse = vgui.Create("DNumSlider") + basecolor_pulse:SetMax(255) + basecolor_pulse:SetMin(0) + + if isnumber(GetConVar("pac_popups_base_color_pulse"):GetInt()) then + basecolor_pulse:SetValue(GetConVar("pac_popups_base_color_pulse"):GetInt()) + else + basecolor_pulse:SetValue(0) + end + + basecolor_pulse:SetText("base pulse") + function basecolor_pulse:OnValueChanged(val) + val = math.Round(tonumber(val),0) + GetConVar("pac_popups_base_color_pulse"):SetInt(val) + end + + local fadecolor = vgui.Create("DColorMixer") + fadecolor:SetSize(400,150) + col_args = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + fadecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + function fadecolor:ValueChanged(col) + GetConVar("pac_popups_fade_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_popups_fade_alpha"):SetString(col.a) + end + + local textcolor = vgui.Create("DColorMixer") + textcolor:SetSize(400,150) + col_args = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + + if isnumber(col_args[1]) then + textcolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + end + + textcolor:SetAlphaBar(false) + function textcolor:ValueChanged(col) + GetConVar("pac_popups_text_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + end + + local invertcolor_btn = vgui.Create("DButton") + invertcolor_btn:SetSize(400,30) + invertcolor_btn:SetText("Use text invert color (experimental)") + function invertcolor_btn:DoClick() + GetConVar("pac_popups_text_color"):SetString("invert") + end + + local preview_pnl = vgui.Create("DLabel") + preview_pnl:SetSize(400,170) + preview_pnl:SetText("") + local label_text = "Popup preview! The text will look like this." + + local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + local a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + + local preview_refresh_btn = vgui.Create("DButton") + preview_refresh_btn:SetSize(400,30) + preview_refresh_btn:SetText("Refresh") + local oldpaintfunc = master_pnl.Paint + local invis_frame = false + function preview_refresh_btn:DoClick() + invis_frame = not invis_frame + if invis_frame then master_pnl.Paint = nil else master_pnl.Paint = oldpaintfunc end + rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + end + + function preview_pnl:Paint( w, h ) + --base layer + local sine = 0.5 + 0.5*math.sin(CurTime()*2) + draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) ) + for band=0,w,1 do + --per-pixel fade + fade = 1 - (1/w * band * 1) + fade = math.pow(fade,2) + draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) + end + draw.DrawText(label_text, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,255)) + end + + list_pnl:Add(Label("Base color")) + list_pnl:Add(basecolor) + list_pnl:Add(basecolor_pulse) + list_pnl:Add(Label("Gradient color")) + list_pnl:Add(fadecolor) + list_pnl:Add(Label("Text color")) + list_pnl:Add(textcolor) + list_pnl:Add(invertcolor_btn) + list_pnl:Add(preview_refresh_btn) + list_pnl:Add(preview_pnl) + master_pnl:MakePopup() + + +end + +concommand.Add("pac_popups_settings", function() pace.OpenPopupConfig() end) + +--[[ + info_string, main string + { info about where to position the label + pac_part = part, that would be the pac part if applicable + obj = self.Label, that would be the positioning target + obj_type = "pac tree label", what type of thing is the target, for positioning + pac tree label = on the editor, needs to realign when scrolling + part world = if base_movable, place it next to the part in the view, if not, owner entity + screen = static x,y on screen no matter what, needs the further x,y args specified outside + cursor = right on the cursor + editor bar = next to the toolbar + hoverfunc = function() end, a function to run when hovering. + doclickfunc = function() end, a function to run when clicking + panel_exp_width = 900, panel_exp_height = 200 prescribed dimensions to expand to + }, + self:LocalToScreen() x,y +]] + + + +--[[ +we generally have two routes to create a popup: part and direct +at the part level we can tell pac to try to create a popup +pac.AttachInfoPopupToPart(part : obj, string : str, table : tbl) --naming scheme close to a general pac function + PART:AttachEditorPopup(string : str, bool : flash, table : tbl) --calls the generic base setup in base_part, shouldn't be overridden + PART:SetupEditorPopup(str, force_open, tbl) --calls the specific setup, can be overridden for different classes + pac.InfoPopup(str, tbl, x, y) --creates the vgui element + +we can directly create an independent editor popup +pac.InfoPopup(str, tbl, x, y) +]] + + +function pac.InfoPopup(str, tbl, x, y) + local x = x + local y = y + if not x or not y then + x = ScrW()/2 + math.Rand(-300,300) + y = ScrH()/2 + math.Rand(-300,0) + end + tbl = tbl or {} + if not tbl.obj then + if tbl.obj_type == "pac tree label" then + tbl.obj = tbl.pac_part.pace_tree_node + elseif tbl.obj_type == "part world" then + tbl.obj = tbl.pac_part + end + end + + str = str or "" + local verbosity = GetConVar("pac_popups_verbosity"):GetString() + + local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + local a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + + local pnl = vgui.Create("DFrame") + local txt_zone = vgui.Create("RichText", pnl) + + --function pnl:PerformLayout() end + pnl:SetTitle("") pnl:SetText("") pnl:ShowCloseButton( false ) + txt_zone:SetPos(5,25) + txt_zone:SetContentAlignment( 7 ) --top left + + if tbl.pac_part then + if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + if pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName] then + str = str .. "\n\n" .. pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName].popup_tutorial .. "\n" + end + end + end + + + pnl.hoverfunc = function() end + pnl.doclickfunc = function() end + pnl.titletext = "Click for more information! (or F1)" + pnl.alternativetitle = "Right click to kill the popup. \"pac_popups_preserve_on_autofade\" is set to " .. GetConVar("pac_popups_preserve_on_autofade"):GetInt() .. ", " .. (GetConVar("pac_popups_preserve_on_autofade"):GetBool() and "If it fades away, the popup is allowed to reappear on hover or F1" or "If it fades away, the popup will not reappear") + + --pnl:SetPos(ScrW()/2 + math.Rand(-100,100), ScrH()/2 + math.Rand(-100,100)) + + function pnl:FixPartReference(tbl) + if not tbl or table.IsEmpty(tbl) then self:Remove() end + if tbl.pac_part then tbl.obj = tbl.pac_part.pace_tree_node end + + end + + function pnl:MoveToObj(tbl) + --self:MakePopup() + if tbl.obj_type == "pac tree label" then + if not IsValid(tbl.obj) then + self:FixPartReference(tbl) + self:SetPos(x,y) + else + local x,y = tbl.obj:LocalToScreen() + x = pace.Editor:GetWide() + --print(pace.Editor:GetWide(), input.GetCursorPos()) + self:SetPos(x,y) + end + if pace then + if pace.Editor then + if pace.Editor.IsLeft then + if not pace.Editor:IsLeft() then + self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) + else + self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) + end + end + end + end + + elseif tbl.obj_type == "part world" then + if tbl.pac_part then + local global_position = tbl.pac_part:GetRootPart():GetOwner():GetPos() + tbl.pac_part:GetRootPart():GetOwner():OBBCenter()*1.5 + if tbl.pac_part.GetWorldPosition then + global_position = tbl.pac_part:GetWorldPosition() --if part is a base_movable, we'll get its position right away + elseif tbl.pac_part:GetParent().GetWorldPosition then + global_position = tbl.pac_part:GetParent():GetWorldPosition() --if part isn't but has a base_movable parent, get that + end + local scr_tbl = global_position:ToScreen() + self:SetPos(scr_tbl.x, scr_tbl.y) + end + + elseif tbl.obj_type == "screen" then + self:SetPos(x,y) + + elseif tbl.obj_type == "cursor" then + self:SetPos(input.GetCursorPos()) + + elseif tbl.obj_type == "editor bar" then + if not pace.Editor:IsLeft() then + self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) + else + self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) + end + end + + end + + + if tbl then + pnl.tbl = tbl + pnl:MoveToObj(tbl) + if tbl.hoverfunc then + if tbl.hoverfunc == "open" then + pnl.hoverfunc = function() + pnl.hovering = true + pnl:keep_alive(3) + if not pnl.hovering and not pnl.expand then + pnl.resizing = true + pnl.expand = true + + pnl.ResizeStartTime = CurTime() + pnl.ResizeEndTime = CurTime() + 0.3 + end + end + else + pnl.hoverfunc = tbl.hoverfunc + end + end + pnl.exp_height = tbl.panel_exp_height or 400 + pnl.exp_width = tbl.panel_exp_width or 800 + end + + pnl.exp_height = pnl.exp_height or 400 + pnl.exp_width = pnl.exp_width or 800 + pnl:SetSize(200,20) + + pnl.DeathTimeAdd = 0 + if GetConVar("pac_popups_preserve_on_autofade"):GetBool() then + pnl.DeathTimeAdd = 240 + end + pnl.DeathTime = CurTime() + 13 + pnl.FadeTime = CurTime() + 10 + pnl.FadeDuration = pnl.DeathTime - pnl.FadeTime + pnl.ResizeEndTime = 0 + pnl.ResizeStartTime = 0 + pnl.resizing = false + + function pnl:keep_alive(extra_time) + pnl.DeathTime = math.max(pnl.DeathTime, CurTime() + extra_time + pnl.FadeDuration) + pnl.FadeTime = math.max(pnl.FadeTime, CurTime() + extra_time) + pnl:SetAlpha(255) + end + + --the header needs a label to click on to open the popup + function pnl:DoClick() + + if input.IsKeyDown(KEY_F1) or (self:IsHovered() and not txt_zone:IsHovered()) then + pnl.expand = not pnl.expand + pnl.ResizeStartTime = CurTime() + pnl.ResizeEndTime = CurTime() + 0.3 + pnl.resizing = true + end + + pnl:keep_alive(3) + pnl.doclickfunc() + end + + --handle positioning, expanding and termination + function pnl:Think() + self:MoveToObj(tbl) + if input.IsButtonDown(KEY_P) and input.IsButtonDown(KEY_LALT) then --auto-kill if alt-p + self:Remove() + end + + if input.IsMouseDown(MOUSE_RIGHT) then + if self:IsHovered() and not txt_zone:IsHovered() then + self:Remove() + end + end + + self.F1_doclick_possible_at = self.F1_doclick_possible_at or 0 + self.mouse_doclick_possible_at = self.mouse_doclick_possible_at or 0 + + if input.IsButtonDown(KEY_F1) then --expand if press F1, but only after a delay + if self.F1_doclick_possible_at == 0 then + self.F1_doclick_possible_at = CurTime() + 0.3 + end + if CurTime() > self.F1_doclick_possible_at then + self.F1_doclick_possible_at = 0 + self:DoClick() + end + end + if input.IsMouseDown(MOUSE_LEFT) and self:IsHovered() or self:IsChildHovered() then --expand if press mouse left + if self.mouse_doclick_possible_at == 0 then + self.mouse_doclick_possible_at = CurTime() + 1 + end + if CurTime() > self.mouse_doclick_possible_at then + self.mouse_doclick_possible_at = 0 + self:DoClick() + end + end + if not input.IsMouseDown(MOUSE_LEFT) then + self.mouse_doclick_possible_at = CurTime() + end + + if not IsValid(tbl.pac_part) and tbl.pac_part ~= false then self:Remove() end + self.exp_width = self.exp_width or 800 + self.exp_height = self.exp_height or 500 + --resizing code, initially the label should start small + if self.resizing then + local expand_frac_w = math.Clamp((self.ResizeEndTime - CurTime()) / 0.3,0,1) + local expand_frac_h = math.Clamp((self.ResizeEndTime - (CurTime() - 0.5)) / 0.5,0,1) + local width,height + if not self.expand then + width = 200 + (self.exp_width - 200)*(expand_frac_w) + height = 20 + (self.exp_height - 20)*(expand_frac_h) + if self.hovering and not self:IsHovered() then self.hovering = false end + else + width = 200 + (self.exp_width - 200)*(1 - expand_frac_h) + height = 20 + (self.exp_height - 20)*(1 - expand_frac_w) + + end + self:SetSize(width,height) + txt_zone:SetSize(width-10,height-30) + end + + + self.fade_factor = math.Clamp((self.DeathTime - CurTime()) / self.FadeDuration,0,1) + self.fade_factor = math.pow(self.fade_factor, 3) + + if CurTime() > self.DeathTime + self.DeathTimeAdd then + self:Remove() + end + if pace.Focused then self:SetAlpha(255*self.fade_factor) end + if self:IsHovered() then + self:keep_alive(1) + self.hoverfunc() + if input.IsMouseDown(MOUSE_RIGHT) then + if tbl.pac_part then + tbl.pac_part.killpopup = true + end + self:Remove() + end + + end + + if not pace.Focused then + self:AlphaTo(0, 0.1, 0) + self:KillFocus() + self:SetMouseInputEnabled(false) + self:SetKeyBoardInputEnabled(false) + gui.EnableScreenClicker(false) + end + + function pnl:OnRemove() + if not GetConVar("pac_popups_preserve_on_autofade"):GetBool() then + tbl.pac_part.killpopup = true + end + end + end + + pnl.doclickfunc = tbl.doclickfunc or function() end + + + + pnl.exp_height = tbl.panel_exp_height + pnl.exp_width = tbl.panel_exp_width + + --cast the convars values + r1 = tonumber(r1) + g1 = tonumber(g1) + b1 = tonumber(b1) + a1 = tonumber(a1) + r2 = tonumber(r2) + g2 = tonumber(g2) + b2 = tonumber(b2) + a2 = tonumber(a2) + r3 = tonumber(r3) + g3 = tonumber(g3) + b3 = tonumber(b3) + + local col = Color(r3,g3,b3,255) + + --txt_zone:SetFont("DermaDefaultBold") + function txt_zone:PerformLayout() + txt_zone:SetBGColor(0,0,0,0) + txt_zone:SetFGColor(col) + end + + function txt_zone:Think() + if self:IsHovered() then + pnl:keep_alive(3) + end + end + + txt_zone:SetText("") + txt_zone:AppendText(str) + + txt_zone:SetVerticalScrollbarEnabled(true) + + function pnl:Paint( w, h ) + + + self.fade_factor = self.fade_factor or 1 + --base layer + local sine = 0.5 + 0.5*math.sin(CurTime()*2) + draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) ) + --draw.RoundedBox( 0, 0, 0, 1, h, Color( 88, 179, 255, 255)) + for band=0,w,1 do + --per-pixel fade + fade = 1 - (1/w * band * self.fade_factor) + fade2 = math.pow(fade,3) + fade = math.pow(fade,2) + --draw.RoundedBox( c, x, y, w, h, color ) + draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) + --draw.RoundedBox( 0, band, 0, 1, 1, Color( 88, 179, 255, 255)) + --draw.RoundedBox( 0, band, h-1, 1, 1, Color( 0, 0, 0, 255)) + end + + if self.expand then + draw.DrawText(self.alternativetitle, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) + else + draw.DrawText(self.titletext, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) + end + end + + pnl:MakePopup() + return pnl +end + +function pac.AttachInfoPopupToPart(obj, str, tbl) + obj:AttachEditorPopup(str, true, tbl) +end + +function pace.FlushInfoPopups() + for _,part in pairs(pac.GetLocalParts()) do + local node = part.pace_tree_node + if not node or not node:IsValid() then continue end + if node.popupinfopnl then + node.popupinfopnl:Remove() + node.popupinfopnl = nil + end + end + +end + +--[[ + part classes info + +ideally we should have: +1-a tooltip form (7 words max) + e.g. projectile: throws missiles into the world +2-a fuller form for the popups (4-5 sentences or more if needed) + e.g. projectile: the projectile part creates physical entities and launches them forward.\n + the entity has physics but it can be clientside (visual) or serverside (physical)\n + by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n + the entity can do damage but servers can restrict that. + +but then again we should probably look for better ways for the full-length explanations, + maybe grab some of them from the wiki or have a web browser for the wiki +]] + +do + + pace.TUTORIALS = pace.TUTORIALS or {} + pace.TUTORIALS.PartInfos = { + + ["trail"] = { + tooltip = "leaves a trail behind", + popup_tutorial = + "the trail part creates beams along its path to make a trail\n".. + "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. + "you can set how it looks, how big it becomes etc." + }, + + ["trail2"] = { + tooltip = "leaves a trail behind", + popup_tutorial = + "the trail part creates beams along its path to make a trail\n".. + "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. + "you can set how it looks, how big it becomes etc." + }, + + ["sound"] = { + tooltip = "plays sounds", + popup_tutorial = "plays sounds in wav, mp3, ogg formats.\n".. + "for random sounds, paste each path separated by semicolons e.g. sound1.wav;sound3.wav;sound8.wav\n".. + "we have a special bracket notation for sound lists: sound[1,50].wav\n\n".. + "some of the parameters to know:\n".. + "sound level affects the falloff along with volume; a good starting point is 70 level, 0.6 volume\n".. + "overlapping means it doesn't get cut off if hidden\n".. + "sequential plays sounds in a list in order once you have the semicolon or bracket notation;\n".. + "\tthe steps is how much you progress by each activation. it can go one by one (1), every other sound (2+), stay there (0) or go back (negative values)" + }, + + ["sound2"] = { + tooltip = "plays web sounds", + popup_tutorial = "plays sounds in wav, mp3, ogg formats, with the option to download sound files from the internet\n".. + "people usually use dropbox, google drive, other cloud hosts or their own server host to store and distribute their files. each has its limitations.\n\n".. + "WARNING! Downloading and using these sounds is only possible in the chromium branch of garry's mod!\n\n".. + "to randomize sounds, we still have the same notations as legacy sound:\n".. + "\tsemicolon notation e.g. path1.wav;https://url1.wav;https://url2.wav\n".. + "\tbracket notation e.g. sound[1,50].wav\n\n".. + "some of the parameters to know, you'll already know some of them from legacy sound:\n".. + "-radius affects the falloff distance\n".. + "-overlapping means it doesn't get cut off if hidden\n".. + "-sequential plays sounds in a list in order" + }, + + ["ogg"] = { + tooltip = "plays ogg sounds (broken)", + popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." + }, + + ["webaudio"] = { + tooltip = "plays web sounds (legacy)", + popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." + }, + + + ["halo"] = { + tooltip = "makes models glow", + popup_tutorial = + "This part creates a halo around a model entity.\n".. + "That could be your playermodel or a pac3 model, but for some reason it doesn't work on your player if you have an entity part.\n".. + "passes is the thickness of the halo, amount is the brightness, blur x and y spread out the shape" + }, + + ["bodygroup"] = { + tooltip = "changes body parts on supported models", + popup_tutorial = + "Bodygroups are a Source engine model feature which allows to easily show or hide different pieces of a model\n".. + "those are often used for accessories and styles. But it won't work unless your model has bodygroups.\n".. + "this part does exactly that. but you might do that directly with the model part or entity part" + }, + + ["holdtype"] = { + tooltip = "changes your animation set", + popup_tutorial = + "this part allows you to change animations played in individual movement slots, so you can mix and match from the available animations in your playermodel\n".. + "a holdtype is a set of animations for holding one kind of weapon, such as one-handed pistols vs two-handed revolvers, rifles, melee weapons etc.\n".. + "The option is also in the normal animation part, but this part goes in more detail in choosing different animations" + }, + + ["clip"] = { + tooltip = "cuts a model in a plane (legacy)", + popup_tutorial = + "This part cuts off one side of the model in rendering.\n".. + "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." + }, + + ["clip2"] = { + tooltip = "cuts a model in a plane", + popup_tutorial = + "This part cuts off one side of the model in rendering.\n".. + "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." + }, + + ["model"] = { + tooltip = "places a model (legacy)", + popup_tutorial = "The old model part still does the basic things you need a model to do" + }, + + ["model2"] = { + tooltip = "places a model", + popup_tutorial = + "The model part creates a clientside entity to draw a model locally.\n".. + "Being a base_movable, parts inside it will be physically arented to it.\n".. + "therefore, it can act as a regrouper, rail or anchoring point for your pac structuring needs, although you probably shouldn't abuse it.\n".. + "It can accept most modifiers and play animations, if present or referenced in the model.\n\n".. + "It can load MDL zips or OBJ files from a direct link to a server host or cloud provider, allowing you to use pretty much any model as long as it's the right format for Source. And on that subject, you would do well to install Crowbar, as well as Blender with Blender Source Tools, if you want to extract and edit models. Consult the valve developer community wiki for more information about QC; I view this as common knowledge rather than the purview of pac3 so you have to do some research." + }, + + ["material"] = { + tooltip = "defines a material (legacy)", + popup_tutorial = + "the old material still works as it says. it lets you define some VMT parameters for a material" + }, + + ["material_3d"] = { + tooltip = "defines a material for models", + popup_tutorial = + "This part creates a VMT material of the shader type VertexLitGeneric.\n".. + "If you have experience in Source engine things, you probably should know what some of these do, I won't expound fully but here's the essential summary anyway:\n\n".. + "\tbase texture is the base image. It's basically just color pixels.\n".. + "\tbump map / normal map is a relief that gives a texture on the surface. It uses a distinctly purple pixel format; it's not color but directional information\n".. + "\tdetail is a second base image added on top to modify the pixels. It's usually grayscale because we don't need to add color to give more grit to an image\n".. + "\tself illumination and emissive blend are glowing layers. emissive blend is more complex and needs three necessary components before it starts to work properly.\n".. + "\tenvironment map is a layer for pre-baked room reflection, by default env_cubemap tries to get the nearest cubemap but you can choose another texture, although cubemaps are a very specific format\n".. + "\tphong is a layer of dynamic light reflections\n\n".. + "If you want to edit a material, you can load its VMT with a right click on \"load vmt\", then select the right material override\n".. + "Reminder that transparent textures may need additive or some form of translucent setting on the model and on the material.\n\n".. + "For more information, search the Valve developer community site or elsewhere. Many material features are standard, and if you want to push this part to the limit, the extra research will be worth it." + }, + + ["material_2d"] = { + tooltip = "defines a material for sprites", + popup_tutorial = + "This part creates a VMT material of the shader type UnlitGeneric. This is used by particles and sprites.\n".. + "For transparent textures, use additive or vertex alpha/vertex color (for particles and decals). Some VTF or PNG textures have an alpha channel, but many just have a black background meant for additive rendering.\n\n".. + "For more information, search the Valve developer community site" + }, + + ["material_refract"] = { + tooltip = "defines a refracting material", + popup_tutorial = + "This part creates a VMT material of the shader type Refract. As with other material parts, you would find it useful to name the material to use that in multiple models' \"material\" fields\n".. + "In a way, it doesn't work by surface, but by silhouette. But the surface does determine how the refraction occurs. Setting a base texture creates a flat wall behind it that can distort in interesting ways but it'll replace the view behind.\n".. + "The normal section does most of the heavy lifting. This is where the image behind the material gets refracted according to the surface. You can blend between two normal maps in greater detail.\n".. + "Your model needs to be set to \"translucent\" rendering mode for this to work because the shader is in a multi-step rendering process.\n\n".. + "For more information, search the Valve developer community site" + }, + + ["material_eye refract"] = { + tooltip = "defines a refracting eye material", + popup_tutorial = + "This part creates a VMT material of the shader type EyeRefract.\n".. + "It's tricky to use because of how it involves projections and entity eye position, but you can more easily get something working on premade HL2 or other Source games' characters with QC eyes." + }, + + ["submaterial"] = { + tooltip = "applies a material on a submaterial zone", + popup_tutorial = + "Models can be comprised of multiple materials in different areas. This part can replace the material applied to one of these zones.\n".. + "Depending on how the model was made, it might correspond to what you want, or it might not.\n".. + "As usual, as with other model modifiers your expectations should always line up with the quality of the model you're using." + }, + + ["bone"] = { + tooltip = "changes a bone (legacy)", + popup_tutorial = + "The legacy bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." + }, + + ["bone2"] = { + tooltip = "changes a bone (legacy)", + popup_tutorial = + "The legacy experimental bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." + }, + + ["bone3"] = { + tooltip = "changes a bone", + popup_tutorial = + "This part modifies a model's bone. It can move relative to the parent bone, scale, and rotate.\n".. + "Follow part forces the bone to relocate to a base_movable part. Might have issues if you successively follow part multiple related bones. You could try to fix that by changing draw orders of the follow parts and bones." + }, + + ["player_config"] = { + tooltip = "sets your player entity's behaviour", + popup_tutorial = + "This part has access to some of your player's behavior, like whether you will play footsteps, the chat animation etc.\n".. + "Some of these may or may not work as intended..." + }, + + ["light"] = { + tooltip = "lights up the world (legacy)", + popup_tutorial = + "This legacy part still does the basic thing you want from a light, but the new light part is more fully-featured, for the most part.\n".. + "There is one thing it does that the new part doesn't, and that's styles." + }, + + ["light2"] = { + tooltip = "lights up models or the world", + popup_tutorial = + "This part creates a dynamic light that can illuminate models or the world independently.\n".. + "There are some options for the light's falloff shape (inner and outer angles).\n".. + "Its brightness works by magnitude and size, not multiplication. Which means you can still have light at 0 or lower brightness." + }, + + ["event"] = { + tooltip = "activates or deactivates other parts", + popup_tutorial = + "This part hides or shows connected parts when certain conditions are met. We won't describe them in this tutorial, you'll have to read them individually. The essential behaviour remains common accross events.\n\n".. + "Domain, in other words, which parts get affected:\n".. + "\t1-Default: The event will command its direct parent. Because parts can contain other parts, this includes the event itself, and parts beside the event too. While this is not usually a problem, you have to be aware of that.\n".. + "\t2-ACO: Affect Children Only. The event will command parts inside it, not beside, not above. This is the first step to isolate your setup and have clean logic in your pac.\n".. + "\t3-Targeted: The event gets wired to a part directly, including its children of course. This is accessed when you select a part in the \"targeted part\" field, which has an unfortunate name because there's still the old \"target part\" parameter\n\n".. + "Some events, like is_touching, can select an external \"target\" to use as a point to gather information.\n\n".. + "Operators:\n".. + "Operators are just how the event asks the question to determine when to activate or deactivate. Just read the event the same way as it asks the question: is my source equal to the value? can I find this text in my source?\n".. + "\tnumber-related operators: equal, above, below, equal or above, equal or below\n".. + "\tstring-related operators: equal, find, find simple\n".. + "There's still a caveat. If you use the wrong type of operator for your event, it will NOT work. Please trust the editor autopilot when it automatically changes your operator to a good one. Do not change it unless you know what you're doing." + }, + + ["sprite"] = { + tooltip = "draws a 2D texture", + popup_tutorial = + "Sprites are another Source engine thing which are useful for some point effects. Most textures being for model surfaces will look like squares if drawn flat, but sprite and particle textures are made specially for this purpose.\n".. + "They should have a transparent background or black background. The difference is because of rendering modes or blend modes.\n".. + "Additive rendering adds pixels' values. So, bright pixels will be more visible, but dark pixels end up being faded or invisible because their amounts are low." + }, + + ["fog"] = { + tooltip = "colors a model with fog", + popup_tutorial = + "This strange modifier renders a fog-like color over a model. Not in the world, not inside the model, but over its surface.\n".. + "For that reason, you might do well to change rendering-related values like blend mode on the host model's side\n".. + "It requires to be attached to a base_drawable part, keep in mind the start and end values are multiplied by 100 in post for some reason.".. + "start is the distance where the fog starts to appear outside, end is where the fog is thickest." + }, + + ["force"] = { + tooltip = "provides physical force", + popup_tutorial = + "This part tries to tell the server to do a force impulse, or continually request small impulses for a continuous force. It should work for most physics props, some item and ammo entities, players and NPCs. But it may or may not be allowed on the server due to server settings: pac_sv_force.\n\n".. + "There's a base force and an added xyz vector force. You have options to choose how they're applied. Aside from that, the part's area is mainly for detection.\n\n".. + "For the Base force, Radial is from to self to each entity, Locus is from locus to each entity, Local is forward of self\n\n".. + "For the Vector force, Global is on world coordinates, Local is on self's coordinates, Radial is relative to the line from the self or locus toward the entity (Used in orbits/vortex/circular motion with centrifugal force)\n\n".. + "NPCs might have weird movement so don't expect much from pushing them." + }, + + ["faceposer"] = { + tooltip = "Adjusts multiple facial expression slots", + popup_tutorial = + "This part gives access to multiple facial expressions defined by your model's shape keys in one part.\n".. + "The flex multiplier affects the whole model, so you should avoid stacking faceposers if they have different multipliers." + }, + + ["command"] = { + tooltip = "Runs a console command or lua code", + popup_tutorial = "This part attempts to run a command or Lua code on your client. It may or may not work depending on the command and some servers don't allow you to run clientside lua, because of sv_allowcslua 0.\n\n".. + "Some example lua bits:\n".. + "\tif LocalPlayer():Health() > 0 then print(\"I'm alive\") RunConsoleCommand(\"say\", \"I\'m alive\") end\n".. + "\tfor i=0,100,1 do print(\"number\" .. i) end\n".. + "\tfor _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end\n".. + "\tlocal random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)" + + }, + + ["weapon"] = { + tooltip = "configures your weapon entity", + popup_tutorial = "This part is like an entity part, but for weapons. It can change your weapon's position and appearance, for all or one weapon class." + }, + + ["woohoo"] = { + tooltip = "applies a censor square", + popup_tutorial = + "This part draws a pixelated square with what's behind it, with a possible blur filter and adjustable resolution.\n".. + "It requires a lot of resources to set up and needs to refresh in specific circumstances, which is why you can't change its resolution or blur filtering state with proxies." + }, + + ["flex"] = { + tooltip = "Adjusts one facial expression slot", + popup_tutorial = + "This part gives access to one facial expression defined by your model's shape keys." + }, + + ["particles"] = { + tooltip = "Emits particles", + popup_tutorial = + "Throws particles into the world. They are quite configurable, can be flat 3D or 2D sprites, can be stretched with start/end length.\n".. + "To start with, you may want to set zero angle to false and particle angle velocity to (0, 0, 0)\n".. + "You can use a web texture but you might still need to work around material limitations for transparent images\n".. + "They are not PCF effects though. But I think that with a wise choice and layered particles, you can recreate something that looks like an effect." + }, + + ["custom_animation"] = { + tooltip = "sets up an editable bone animation", + popup_tutorial = + "This part creates a custom animation with a separate editor menu. It is not a sequence, but it moves bones on top of your base animations. It morphs between keyframes which correspond to bones' positions and angles. This is what creates movement.\n\n".. + "Custom animation types:\n".. + "\tsequence: loopable. plays the A-pose animation as a base, layers bone movements on top.".. + "\tstance: loopable. layers bone movements on top.".. + "\tgesture: not loopable. layers bone movements on top. ideally you should start with duplicating your initial frame once for smoothly going back to 0.".. + "\tposture: only applies one non-moving frame. this is like a set of bones.\n\n".. + "There are interesting Easing styles available when you select the linear interpolation mode. They're useful in many ways, if you want to have more control over the dynamics and ultimately give character to your animation.\n".. + "While this is not the place to write a full tutorial for how to animate, or explaining animation principles in depth, I editorialize a bit and say those are two I try to aim for:\n".. + "\tinertia: trying to carry some movement over from a previous frame, because real physics take time to decelerate and accelerate between positions.\n".. + "\texaggeration: animations often use unnatural movement dynamics (e.g. different speeds at different times) to make movements look more pleasing by giving it more character. This goes in hand with anticipation." + }, + + ["beam"] = { + tooltip = "draws a rope or beam", + popup_tutorial = + "This part renders a rope or beam between itself and the end point. It can bend relative to the two endpoints' angles.\n".. + "frequency determines how many half-cycles it goes through. 1 is half a cycle (1 bump), 2 is one cycle(2 bumps)\n".. + "resolution is how many segments it tries to draw for that.\n\n".. + "And here's another reminder that while it can load url images, there are limitations so you may have to do something with a material part or blend mode if you want a custom transparent texture." + }, + + ["animation"] = { + tooltip = "plays a sequence animation", + popup_tutorial = + "This part plays a sequence animation defined in your model via the model's inherent animation definitions, included animations and active addons. Cannot load custom animations, not even .ani, .mdl or .smd\n".. + "If you want to import an animation from somewhere else, you need to know some decompiling/recompiling QC knowledge" + }, + + ["player_movement"] = { + tooltip = "edits your player movement", + popup_tutorial = "This part tells the server to handle your movement manually with a Move hook.\n".. + "Z-Velocity means you can move in the air relative to your eye angles, with WASD and jump almost like noclip. It is however still subject to air friction (needs friction to move, but friction also decelerates you) and uses ground friction as a driver.\n".. + "Friction generally cuts your movement as a percentage every tick. This is why it's very sensitive because its effect is exponential. Horizontal air friction tries to mitigate that a bit\n".. + "Reverse pitch is probably buggy. " + }, + + ["group"] = { + tooltip = "organizes parts", + popup_tutorial = + "This part groups parts. That's all it does. It bypasses parenting, which means it has no side effect, aside from when modifiers act on their direct parent, in which case the group can get in the way.\n".. + "But with a root group, (a group at the root/top level, \"my outfit\"), you can choose an owner name to select another entity to bear the pac outfit." + }, + + ["lock"] = { + tooltip = "grabs or teleports", + popup_tutorial = + "This part allows you to grab things or teleport yourself.\n\n".. + "Warning in advance: It has the most barriers because it probably has the most potential for abuse out of all parts.\n".. + "\tClients need to give consent explicitly (pac_client_grab_consent 1), otherwise you can't grab them.\n".. + "\tThis is doubly true for players' view position. That's another consent (pac_client_lock_camera_consent 1) layered on top of the existing grab consent.\n".. + "\tOn top of that, grabbed players will get a notification if you grab them, and they will know how to break the lock. Clients have multiple commands (pac_break_lock, pac_stop_lock) to request the server to force you to release them. It is mildly punitive.\n".. + "\tThere are multiple server-level settings to limit it. Some servers may even wholesale disable the new combat parts for all players by default until they're trusted/whitelisted.\n\n".. + "Now, here's business. How it works, and how to use this part:\n".. + "\tThe part searches for entities around a sphere, selects the closest one and locks onto it. You should plan ahead for the fact that it only picks up entities by their origin position, which for NPCs and players is between their feet. offset down amount compensates for this, but only for where the detection radius begins.\n".. + "\tIt will then start communicating with the server and the server may reposition the entity if it's allowed. If rejected, you may get a warning in the console, and the part will be stopped for a while.".. + "\tOverrideEyeAngles works for players only, and as stated previously, is subject to consent restrictions.\n" + }, + + ["physics"] = { + tooltip = "creates a clientside physics object", + popup_tutorial = + "This part creates a physics object clientside which can be a box or a sphere. It will relocate the model and pac parts contained and put them in the object.\n".. + "It's not compatible with the force part, unfortunately, because it's clientside. There are other reasonably fun things it can do though.\n".. + "It only works as a direct modifier on a model." + }, + + ["jiggle"] = { + tooltip = "wobbles around", + popup_tutorial = + "This part creates a subpoint that carries base_movables, and moves around with a certain type of dynamics that can lag behind and then catch up, or wiggle back and forth for a while. Strain is how much it will wobble. The children parts will be contained within that subpoint.\n".. + "There is immense utility to control movement and have some physicality to your parts' movement. To name a few examples:\n".. + "\tThe jiggle 0 speed trick: Having your jiggle set at 0 speed will freeze what's inside. You can easily control that with two proxies: one for moving (not 0), one for stopping (0)\n".. + "\tPets and drones: Fun things that are semi-independent. Easy to do with jiggle.\n".. + "\tSmoother transitions with multiple static proxies: If you have position proxies that snap to different positions, making a model teleport too fast, using these proxies on a jiggle instead will let the jiggle do the work of smoothing things out with the movement.\n".. + "\tForward velocity indicator via a counter-lagger: jiggle lags behind an origin, model points to origin with aim part, other model is forward relative to the pointer. Result: a model that goes in the direction of your movement.\n\n".. + "The part, however, has issues when crossing certain angles (up and down)." + }, + + ["projected_texture"] = { + tooltip = "creates a lamp", + popup_tutorial = + "This part creates a dynamic light / projected texture that can project onto models or the world. That's pretty much it. It's useful for lamps, flashlights and the like.\n".. + "But if you're expecting a proper light, its directed lighting method gives mediocre results alone. With another light, and a sprite maybe, it'll look nicer. We won't have point_spotlights though.\n".. + "Its brightness works by multiplication, not magnitude. 0 is a proper 0 amount.\n\n".. + "Because it uses ITexture / VTF, it doesn't link up with pac materials. Animated textures must be done by frames instead of proxies. Although you can still set a custom image. But it's additive so the transparency can't be done with alpha, but on a black background\t".. + "fov on one hand, and horizontal/vertical fovs on the other hand, compete; so you should touch only one and leave the other." + }, + + ["hitscan"] = { + tooltip = "fires bullets", + popup_tutorial = + "This part tries to fire bullets. There are damaging serverside bullets and non-damaging clientside bullets. Both could be useful in their own scenarios.\n".. + "For serverside bullets, the server might restrict that. For example, it can force you to spread your damage among all your bullets, to notably prevent you from stacking tons of bullets to multiply your damage beyond the limit.\n".. + "Damage falloff works with a fractional floor on individual bullets, which means each bullet is lowered to a percentage of its max damage." + }, + + ["motion_blur"] = { + tooltip = "makes a trail of after-images", + popup_tutorial = + "This part continually renders a series of ghost copies of your model along its path to simulate a motion blur-like effect.\n".. + "It has limited options because of how models' clientside entity is set up, allegedly." + }, + + ["link"] = { + tooltip = "transfers variables between parts", + popup_tutorial = + "This part tries to copy variables between two parts and update them when their values change.\n".. + "It doesn't work for all variables especially booleans! Also, \"link\" is a strong word. Whatever you think it means, it's not doing that.".. + "Might require a rewear to work properly." + }, + + ["effect"] = { + tooltip = "runs a PCF effect", + popup_tutorial = + "This part uses an existing PCF effect on your game installation, from your mounted games or addons. No importable PCFs from the web.\n".. + "It apparently can use control points and make tracers work. It may or may not be supported by different effects; start by putting the effect in a basic model to position the effect.\n".. + "And PCF effects can be a gigantic pain, with for example looping issues, permanence issues (BEWARE OF TF2 UNUSUAL EFFECTS!), wrong positions etc.".. + "You should probably look into particles and think about how to layer them if you're looking for something more configurable." + }, + + ["text"] = { + tooltip = "draws 3D2D or 2D text", + popup_tutorial = + "This part renders text on a flat surface (3D2D with the DrawTextOutlined mode) or on the screen (2D with the SurfaceText mode). Due to technical limitations, some features in one may not be present in the other, such as the outline and the size scaling\n\n".. + "You can use a combination of data and text to build up your text. Combined text tells the part to use both, and text position tells you whether the text is before or after the data.\n".. + "What's this data? text override. There are a handful of presets, like your health, name, position. If you want more control, you can use Proxy, and it will use the dynamic text value (a simple number variable) which you can control with proxies.\n\n".. + "If you want to raise the resolution of the text, you should try making a bigger font. But creating fonts is expensive so it's throttled. You can only make one every 3 seconds.\n".. + "Although you can use any of gmod's or try to use your operating system's font names, there are still limits to fonts' features, both in their definitions and in the lua code. Not everything will work. But it will create a unique ID for the font it creates, and you can reuse that font in other text parts." + }, + + ["camera"] = { + tooltip = "changes your view", + popup_tutorial = + "This part runs a CalcView hook to allow you to go into a third person mode and change your view accordingly. Some parts on your player may get in the way.\n".. + "eye angle lerp determines how much you mix the original eye angles into the view. Otherwise at 0 it will fully use the part's local angles.\n".. + "Remember a right hand rule! Point forward, thumb up, middle finger perpendicular to the palm. This is how the camera will look with 0 lerp.\n".. + "\tX = Red = index finger = forward\n".. + "\tY = Green= middle finger = left\n".. + "\tZ = Blue = thumb finger = up\n".. + "As an example, if you apply this, you will learn that, on the head, you can simply take a 0,-90,-90 angle value and be done with it.\n\n".. + "Because of how pac3 works, you should be careful when toggling between cameras. I've made some fixes to prevent part of that, but if you hide with events, and lose your final camera, you can't come back unless you go back to third person (to restart the cameras) and then back into first person (to put priority back on an active camera)." + }, + + ["decal"] = { + tooltip = "applies decals", + popup_tutorial = + "Decals are a Source engine thing for bullet holes, sprays and other such flat details as manholes and posters. This part when shown emits one by tracing a line forward and applying it at the hit surface.\n".. + "It can use web images and pac materials, but still subject to rendering quirks depending on transparency and others.\n".. + "Decals are semi-permanent, you can only remove them with r_cleardecals" + }, + + ["projectile"] = { + tooltip = "throws missiles into the world", + popup_tutorial = + "the projectile part creates physical entities and launches them forward.\n".. + "the entity has physics but it can be clientside (visual) or serverside (physical)\n".. + "by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n".. + "the entity can do damage but servers can restrict that.\n".. + "For visual reference, a 1x1x1 cubeex is around radius 24, and a default pac sphere is around 8." + }, + + ["poseparameter"] = { + tooltip = "sets a pose parameter", + popup_tutorial = + "pose parameters are a Source engine thing that helps models animate using a \"blend sequence\". For instance, this is how the body and head are rotated, and how 8-way walks are blended.\n".. + "It goes without saying that not all models have those, and some have fewer supported pose parameters because of how they were made." + }, + + ["entity"] = { + tooltip = "edits an entity (legacy)", + popup_tutorial = "The legacy entity part still does the usual things you need to edit your entity. Color, model, size, no draw etc. But better use the new entity part." + }, + + ["entity2"] = { + tooltip = "edits an entity", + popup_tutorial = + "This part can edit some properties of an entity. This can be your playermodel, a pac3 model (it's a clientside entity) or a prop (you give a prop a pac outfit by selecting it with the owner name on a root group).\n".. + "It supports web models. See the model part's tutorial or read the MDL zips page on our wiki for further info.\n\n".. + "As with other bone-related things, it might not work properly if you use it on a ragdoll or some similar entities.\n\n".. + "Another warning in advance, if you wonder why your playermodel won't change, there are some addons, such as Enhanced Playermodel Selector, known to cause issues because they override your entity, thus conflicting with pac3. This one can be fixed if you disable \"enforce playermodel\"\n".. + "Other than that, the server setting pac_modifier_model and pac_modifier_size can forbid you from changing your playermodel and size respectively." + }, + + ["interpolated_multibone"] = { + tooltip = "morphs position between nodes", + popup_tutorial = + "A node-based path/morpher. This part allows you to move its contents by blending positions and angles between different points. Obviously enough, the nodes you select need to be base_movable parts.\n".. + "The first (Zeroth) node is the interpolated_multibone itself. From then on, the next node is reached when lerp reaches the corresponding number, and when you're at the end, i.e. an invalid or missing node, it morphs back to the origin.\n".. + "For example, 0.5 lerp will be halfway between the first node and the origin.\n".. + "While this part finally breaks through one of pac3's fundamental limitations (that of base_movables being limited to specific bones as anchoring points), there are still known issues, namely because of how angles are morphed. Roll angles might break.\n\n".. + "Suggested use cases: multi-position cutscene camera, returning hitpos pseudo-projectile, joints." + }, + + ["proxy"] = { + tooltip = "applies math to parts", + popup_tutorial = + "This part computes math and applies the numbers it gives to a parameter on a part, for number (x), vector (x,y,z) or boolean(true (1) or false (0)) types. It can send to the parent, to all its children, or to an external target part.\n".. + "Easy setup can help you make a rough idea quickly, but writing math yourself in the expression gives supremely superior control over what the math does.\n\n".. + "Here's a quick crash course in the syntax with basic examples showing the rules to observe:\n\n".. + "Basic numbers /math operators : 4^0.5 - 2*(0.2 / 5) + timeex()%4\n".. + "The only basic operators are: + - * / % ^\n".. + "Functions:\n".. + "\tFunctions are like variables that gather data from the world or that process math.\n".. + "\tMost functions are nullary, which means they have no argument: timeex(), time(), owner_health(), owner_armor_fraction()\n".. + "\tOthers have arguments, which can be required or optional: clamp(x,min,max), random(), random(min,max), random_once(seed,min,max), etc.\n".. + "\tAll Lua functions are declared by a set of parentheses containing arguments, possibly separated by commas.\n".. + "Arguments and tokens:\n".. + "\tMost arguments\' type is numbers, but some might be strings with some requirements; Most of the time it\'s a name or a part UID, for example:\n".. + "\tValid number arguments are numbers, functions or well-formed expressions. It\'s the same type because at the end of the day it gives you a number.\n".. + "\t\tNeedless to say, if you compose an expression, you need a coherent link between the tokens (i.e. math operators or functions). 2 + 2 is valid, 2 2 is not.\n".. + "\tValid string arguments are text declared by double quotes. Lua\'s string concatenation operator works. command(\"name\"..2) is the same as command(\"name2\")\n".. + "\t\tWithout the string declaration, Lua tries to look for a global variable. command(\"name\") is valid, command(name) is not.\n\n".. + "Nested functions (composition) : clamp(1 - timeex()^0.5,0,1)\n".. + "XYZ / Vectors (comma notation) : 100,0,50\n".. + "nil (skipping an axis) : 100,nil,0\n\n".. + "You can write pretty much any math using the existing functions as long as you observe the syntax\'s rules: the most common ones being to close your brackets properly, don't misspell your functions\' names and give them all their necessary arguments.\n\n".. + "There are lots of technical things to learn, but you can consult my example proxy bank by right clicking the expression field, and go consult our wiki for reference. https://wiki.pac3.info/part/proxy\n\n".. + "As a conclusion, I\'m gonna editorialize and give my recommendations:\n".. + "\t-Write with purpose. Avoid unnecessary math.\n".. + "\t\t->But still, write in a way that lets you understand the concept better.\n".. + "\t-More to the point, please have patience and deliberation. Make sure every piece works BEFORE moving on and making it more complex.\n".. + "\t-The fundamental mechanism of developing and applying new ideas is composition / compounding.\n".. + "\t\t->Multiplying different expression bits together tends to combine the concepts\n".. + "\t\t->e.g. clamp(0.2*timeex(),0,1)*sin(10*time()) is a fadein and a sine wave. What do you get? sine wave fading to full power.\n".. + "\t-Please read the debug messages in the console or in chat, they help the correction process if we make mistakes." + + }, + + ["sunbeams"] = { + tooltip = "shines like rays of light", + popup_tutorial = + "This part applies a sunbeam effect centered around the part.\n".. + "Multiplier is the strength of the effect. It can also be negative for engulfing darkness.\n".. + "Darken is how much to darken or brighten the whole effect. It helps control the contrast in conjunction with multiplier. With enough darken, only the brightest rays will go through, otherwise with unchecked multipliers or negative darken there's a whole blob of white that just overpowers everything\n".. + "Size affects the after-images projected around the center, which serve as a base for the effect." + }, + + ["shake"] = { + tooltip = "shakes nearby viewers\' camera", + popup_tutorial = + "This part applies a shake that uses the camera\'s position to take effect. For that reason, it may be nullified by certain third person addons, as well as the pac3 editor camera. You can still temporarily disable it to preview your shakes." + }, + + ["gesture"] = { + tooltip = "plays a gesture", + popup_tutorial = + "Gestures are a type of animation usually added as a layer on top of other animations, this part tries to play one but not all animations listed are gestures, so it might not work for most." + }, + + ["damage_zone"] = { + tooltip = "deals damage in a zone", + popup_tutorial = + "This part tries to deal hitbox damage via the server. It may or may not be allowed because of server settings (pac_sv_damage_zone) and client consents (pac_client_damage_zone_consent), etc. Server owners can add or remove entity classes that can be damaged with pac_damage_zone_blacklist_entity_class, pac_damage_zone_whitelist_entity_class commands.\n".. + "Among NPCs it should include VJ and DRG base NPCs, but only if they have npc_ or drg_ in their name\n".. + "Most shapes should be self-explanatory but you can use the preview function to see what it should cover. There are some settings for raycasts which could come in handy for some niche use cases even if the basic ones you'll use most of the time (box, sphere, cone from spheres, ray) will not really use these.".. + "There are certain special damage types. the dissolves can disintegrate entities but can be restricted in the server, prevent_physics_force suppresses the corpse force, removenoragdoll removes the corpse.\n".. + "" + }, + + ["health_modifier"] = { + tooltip = "modifies your health, armor", + popup_tutorial = + "This part allows you to quickly change your max health, max armor, damage multiplier taken, and has the possibility to give you extra health bars that absorb damage before the main health gets damaged.\n".. + "For the extra bars, you need to set a layer priority to pick which ones get damaged first. Outer ones are higher layer values. But they're still invisible for now... events and proxies will come later...\n".. + "The part's usage may or may not be allowed by the server." + } + + } + --print("we have defined the pace.TUTORIALS.PartInfos", pace.TUTORIALS.PartInfos) + + for i,v in pairs(pace.TUTORIALS.PartInfos) do + --print(i,v) + if pace.PartTemplates then + if pace.PartTemplates[i] then + pace.PartTemplates[i].TutorialInfo = v + end + end + end +end + +local motd_cvar = CreateConVar("pac_show_message_on_startup", "2", {FCVAR_ARCHIVE}, "Whether to show the update MOTD when you load in the game") + + +function pac.OpenMOTD(from_initial_startup) + local pnl = vgui.Create("DFrame") + pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) + + local url = "https://github.com/pingu7867/pac3#readme" + + function pnl:OnClose() + if not from_initial_startup then return end + local function exit_message() + if LocalPlayer():IsAdmin() then + notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat!", NOTIFY_GENERIC, 10) + end + notification.AddLegacy("Before you go, once you're in the PAC editor, please go to the options tab and consider choosing which parts of the combat update you want to consent to.", NOTIFY_GENERIC, 10) + end + Derma_Query("Did you read the update news?", "update news", + "Yes, go away", function() motd_cvar:SetInt(0) exit_message() end, + "Bring it up next update", function() motd_cvar:SetInt(1) exit_message() end, + "No, I'll read later", function() motd_cvar:SetInt(2) exit_message() end + ) + + end + + pnl:SetTitle("Welcome to a new update!") + local html = vgui.Create("DHTML", pnl) + html:Dock(FILL) + html:OpenURL( url ) + + + pnl:Center() + pnl:MakePopup() + pace.motd_opened = true +end + + +timer.Simple(10, function() if motd_cvar:GetInt() ~= 0 and not pace.motd_opened then pac.OpenMOTD(true) end end) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 6bee23138..df13b127e 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -1,10 +1,11 @@ local L = pace.LanguageString -- load only when hovered above -local function add_expensive_submenu_load(pnl, callback) +local function add_expensive_submenu_load(pnl, callback, subdir) + local old = pnl.OnCursorEntered pnl.OnCursorEntered = function(...) - callback() + callback(subdir) pnl.OnCursorEntered = old return old(...) end @@ -95,6 +96,10 @@ end local last_backup local maxBackups = CreateConVar("pac_backup_limit", "100", {FCVAR_ARCHIVE}, "Maximal amount of backups") +local autoload_prompt = CreateConVar("pac_prompt_for_autoload", "1", {FCVAR_ARCHIVE}, "Whether to ask before loading autoload. The prompt can let you choose to not load, pick autoload or the newest backup") +local auto_spawn_prop = CreateConVar("pac_autoload_preferred_prop", "2", {FCVAR_ARCHIVE}, "When loading a pac with an owner name suggesting a prop, notify you and then wait before auto-applying the outfit next time you spawn a prop.\n".. + "0 : do not check\n1 : check if only 1 such group is present\n2 : check if multiple such groups are present and queue one group at a time") + function pace.Backup(data, name) name = name or "" @@ -138,7 +143,28 @@ function pace.Backup(data, name) end end +local latestprop +local latest_uid +if game.SinglePlayer() then + hook.Add("OnEntityCreated", "PAC_queue_proppacs", function( ent ) + if ( ent:GetClass() == "prop_physics" or ent:IsNPC()) and not ent:CreatedByMap() and LocalPlayer().pac_propload_queuedparts then + if not table.IsEmpty(LocalPlayer().pac_propload_queuedparts) then + ent:EmitSound( "buttons/button4.wav" ) + local root = LocalPlayer().pac_propload_queuedparts[next(LocalPlayer().pac_propload_queuedparts)] + root.self.OwnerName = ent:EntIndex() + latest_uid = root.self.UniqueID + pace.LoadPartsFromTable(root, false, false) + LocalPlayer().pac_propload_queuedparts[next(LocalPlayer().pac_propload_queuedparts)] = nil + latestprop = ent + end + + end + end) +end + + function pace.LoadParts(name, clear, override_part) + if not name then local frm = vgui.Create("DFrame") frm:SetTitle(L"parts") @@ -190,6 +216,7 @@ function pace.LoadParts(name, clear, override_part) local data, err = pace.luadata.Decode(str) if not data then + ErrorNoHalt(("URL fail: %s : %s\n"):format(name,err)) local message = string.format("URL fail: %s : %s\n", name, err) pace.MessagePrompt(message, "URL Failed", "OK") return @@ -206,21 +233,56 @@ function pace.LoadParts(name, clear, override_part) local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") - if name == "autoload" and (not data or not next(data)) then - local err - data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true) - if not data then - if err then - ErrorNoHalt(("Autoload failed: %s\n"):format(err)) + --queue up prop pacs for the next prop or npc you spawn when in singleplayer + if (auto_spawn_prop:GetInt() == 1 or auto_spawn_prop:GetInt() == 2) and game.SinglePlayer() then + + LocalPlayer().pac_propload_queuedparts = LocalPlayer().pac_propload_queuedparts or {} + + if auto_spawn_prop:GetInt() == 2 or (auto_spawn_prop:GetInt() == 1 and #data > 1) then + --check all root parts from data. format: each data member is a {self, children} table of the part and the list of children + for i,part in pairs(data) do + local possible_prop_pac = isnumber(tonumber(part.self.OwnerName)) + if part.self.ClassName == "group" and possible_prop_pac then + + part.self.ModelTracker = part.self.ModelTracker or "" + part.self.ClassTracker = part.self.ClassTracker or "" + local str = "" + if part.self.ClassTracker == "" or part.self.ClassTracker == "" then + str = "But the class or model is unknown" + else + str = part.self.ClassTracker .. " : " .. part.self.ModelTracker + end + --notify which model / entity should be spawned with the class tracker + notification.AddLegacy( "You have queued a pac part (" .. i .. ":" .. part.self.Name .. ") for a prop or NPC! " .. str, NOTIFY_HINT, 10 ) + LocalPlayer().pac_propload_queuedparts[i] = part + + else + pace.LoadPartsFromTable(part, false, false) + end + end + end + + else + + if name == "autoload" and (not data or not next(data)) then + local err + data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true) + if not data then + if err then + ErrorNoHalt(("Autoload failed: %s\n"):format(err)) + end + return end + elseif not data then + ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err)) return end - elseif not data then - ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err)) - return - end - pace.LoadPartsFromTable(data, clear, override_part) + + pace.LoadPartsFromTable(data, clear, override_part) + + end + end end end @@ -425,6 +487,39 @@ local function populate_parts(menu, tbl, override_part, clear) end end +function pace.AddOneDirectorySavedPartsToMenu(menu, subdir, nicename) + if not subdir then return end + local subdir_head = subdir .. "/" + + local exp_submenu, pnl = menu:AddSubMenu(L""..subdir) + pnl:SetImage(pace.MiscIcons.load) + exp_submenu.GetDeleteSelf = function() return false end + subdir = "pac3/" .. subdir + if nicename then exp_submenu:SetText(nicename) end + + add_expensive_submenu_load(pnl, function(subdir) + local files = file.Find(subdir.."/*", "DATA") + local files2 = {} + --PrintTable(files) + for i, filename in ipairs(files) do + table.insert(files2, {filename, file.Time(subdir .. filename, "DATA")}) + end + + table.sort(files2, function(a, b) + return a[2] > b[2] + end) + + for _, data in pairs(files2) do + local name = data[1] + local full_path = subdir .. "/" .. name + --print(full_path) + local friendly_name = name .. " " .. string.NiceSize(file.Size(full_path, "DATA")) + exp_submenu:AddOption(friendly_name, function() pace.LoadParts(subdir_head .. name, true) end) + :SetImage(pace.MiscIcons.outfit) + end + end, subdir) +end + function pace.AddSavedPartsToMenu(menu, clear, override_part) menu.GetDeleteSelf = function() return false end @@ -481,7 +576,10 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) pnl:SetImage(pace.MiscIcons.clone) backups.GetDeleteSelf = function() return false end - add_expensive_submenu_load(pnl, function() + local subdir = "pac3/__backup/*" + + add_expensive_submenu_load(pnl, function(subdir) + local files = file.Find("pac3/__backup/*", "DATA") local files2 = {} @@ -500,14 +598,15 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) backups:AddOption(friendly_name, function() pace.LoadParts("__backup/" .. name, true) end) :SetImage(pace.MiscIcons.outfit) end - end) + end, subdir) local backups, pnl = menu:AddSubMenu(L"outfit backups") pnl:SetImage(pace.MiscIcons.clone) backups.GetDeleteSelf = function() return false end + subdir = "pac3/__backup_save/*" add_expensive_submenu_load(pnl, function() - local files = file.Find("pac3/__backup_save/*", "DATA") + local files = file.Find(subdir, "DATA") local files2 = {} for i, filename in ipairs(files) do @@ -534,7 +633,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) end) :SetImage(pace.MiscIcons.outfit) end - end) + end, subdir) end local function populate_parts(menu, tbl, dir, override_part) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index f8646236f..6cf13bba5 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1,14 +1,354 @@ +include("parts.lua") +include("shortcuts.lua") +if SERVER then + include("pac3/editor/server/combat_bans.lua") +end + +pace = pace + + +local function rebuild_bookmarks() + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + + --here's some default favorites + if not pace.bookmarked_ressources["models"] or table.IsEmpty(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 + + if not pace.bookmarked_ressources["sound"] or table.IsEmpty(pace.bookmarked_ressources["sound"]) then + pace.bookmarked_ressources["sound"] = { + "music/hl1_song11.mp3", + "npc/combine_gunship/dropship_engine_near_loop1.wav", + "ambient/alarms/warningbell1.wav", + "phx/epicmetal_hard7.wav", + "phx/explode02.wav" + } + end + + if not pace.bookmarked_ressources["materials"] or table.IsEmpty(pace.bookmarked_ressources["materials"]) then + pace.bookmarked_ressources["materials"] = { + "models/debug/debugwhite", + "vgui/null", + "debug/env_cubemap_model", + "models/wireframe", + "cable/physbeam", + "cable/cable2", + "effects/tool_tracer", + "effects/flashlight/logo", + "particles/flamelet[1,5]", + "sprites/key_[0,9]", + "vgui/spawnmenu/generating", + "vgui/spawnmenu/hover" + } + end + + if not pace.bookmarked_ressources["proxy"] or table.IsEmpty(pace.bookmarked_ressources["proxy"]) then + pace.bookmarked_ressources["proxy"] = { + --[[["user"] = { + + },]] + ["fades and transitions"] ={ + { + nicename = "standard clamp fade (in)", + expression = "clamp(timeex(),0,1)", + explanation = "the simplest fade.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 0, moves gradually to 1 and stops progressing at 1 due to the clamp" + }, + { + nicename = "standard clamp fade (out)", + expression = "clamp(1 - timeex(),0,1)", + explanation = "the simplest fade's reverse.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 1, moves gradually to 0 and stops progressing at 0 due to the clamp" + }, + { + nicename = "standard clamp fade (delayed in)", + expression = "clamp(-1 + timeex(),0,1)", + explanation = "the basic fade is delayed by the fact that the clamp makes sure the negative values are pulled back to 0 until the first argument crosses 0 into the clamp's range." + }, + { + nicename = "standard clamp fade (delayed out)", + expression = "clamp(2 - timeex(),0,1)", + explanation = "the reverse fade is delayed by the fact that the clamp makes sure the values beyond 1 are pulled back to 1 until the first argument crosses 1 into the clamp's range." + }, + { + nicename = "standard clamp fade (in and out)", + expression = "clamp(timeex(),0,1)*clamp(3 - timeex(),0,1)", + explanation = "this is just compounding both fades. the second clamp's 3 is comprised of 1 (the clamp max) + 1 (the delay BEFORE the fade) + 1 (the delay BETWEEN the fades)" + }, + { + nicename = "quick ease setup", + expression = "easeInBack(clamp(timeex(),0,1))", + explanation = "get started quickly with the new easing functions.\nsearch \"ease\" in the proxy's input list to see how to write them in pac3, or look at the gmod wiki to see previews of each" + }, + }, + ["pulses"] = { + { + nicename = "bell pulse", + expression = "(0 + 1*sin(PI*timeex())^16)", + explanation = "a basic normalized pulse, using a sine power." + }, + { + nicename = "square-like throb", + expression = "(0 + 1 * (cos(PI*timeex())^16) ^0.3)", + explanation = "a throbbing-like pulse, made by combining a sine power with a fractionnal power.\nthis is better explained visually, so either test it right here in game or go look at a graph to see how x, and cos or sin behave with powers.\ntry x^pow and sin(x)^pow, and try different pows" + }, + { + nicename = "binary pulse", + expression = "floor(1 + sin(PI*timeex()))", + explanation = "an on-off pulse, in other words a square wave.\nthis one completes one cycle every 2 seconds.\nfloor rounds down between 1 and 0 with nothing in-between." + }, + { + nicename = "saw wave (up)", + expression = "(timeex()%1)", + explanation = "a sawtooth wave. it can repeat a 0-1 transition." + }, + { + nicename = "saw wave (down)", + expression = "(1 - timeex()%1)", + explanation = "a sawtooth wave. it can repeat a 1-0 transition." + }, + { + nicename = "triangle wave", + expression = "(clamp(-1+timeex()%2,0,1) + clamp(1 - timeex()%2,0,1))", + explanation = "a triangle wave. it goes back and forth linearly like a saw up and down." + } + }, + ["facial expressions"] = { + { + nicename = "normal slow blink", + expression = "3*clamp(sin(timeex())^100,0,1)", + explanation = "a normal slow blink.\nwhile flexes usually have a range of 0-1, the 3 outside of the clamp is there to trick the value into going faster in case they're too slow to reach their target" + }, + { + nicename = "normal fast blink", + expression = "8*clamp(sin(timeex())^600,0,1)", + explanation = "a normal slow blink.\nwhile flexes usually have a range of 0-1, the 8 outside of the clamp is there to trick the value into going faster in case they're too slow to reach their target\nif it's still not enough, use another flex with less blinking amount to provide the additionnal distance for the blink" + }, + { + nicename = "babble", + expression = "sin(12*timeex())^2", + explanation = "a basic piece to move the mouth semi-convincingly for voicelines.\nthere'll never be dynamic lipsync in pac3, but this is a start." + }, + { + nicename = "voice smoothener", + expression = "clamp(feedback() + 70*voice_volume()*ftime() - 15*ftime(),0,2)", + explanation = "uses a feedback() setup to raise the mouth's value gradually against a constantly lowering value, which should be more smoothly than a direct input" + }, + { + nicename = "look side (legacy symmetrical look)", + expression = "3*(-1 + 2*pose_parameter(\"head_yaw\"))", + explanation = "an expression to mimic the head's yaw" + }, + { + nicename = "look side (new)", + expression = "pose_parameter_true(\"head_yaw\")", + explanation = "an expression to mimic the head's yaw, but it requires your model to have this standard pose parameter" + }, + { + nicename = "look up", + expression = "(-1 + 2*owner_eye_angle_pitch())", + explanation = "an expression to mimic the head's pitch on a [-1,1] range" + }, + { + nicename = "single eyeflex direction (up)", + expression = "-0.03*pose_parameter_true(\"head_pitch\")", + explanation = "plug into an eye_look_up flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (down)", + expression = "0.03*pose_parameter_true(\"head_pitch\")", + explanation = "plug into an eye_look_down flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (left)", + expression = "0.03*pose_parameter_true(\"head_yaw\")", + explanation = "plug into an eye_look_left flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (right)", + expression = "-0.03*pose_parameter_true(\"head_yaw\")", + explanation = "plug into an eye_look_right flex or an eye bone with a higher multiplier" + }, + }, + ["spatial"] = { + { + nicename = "random position (cloud)", + expression = "150*random(-1,1),150*random(-1,1),150*random(-1,1)", + explanation = "position a part randomly across X,Y,Z\nbut constantly blinking everywhere, because random generates a new number every frame.\nyou should only use this for parts that emit things into the world" + }, + { + nicename = "random position (once)", + expression = "150*random_once(0,-1,1),150*random_once(1,-1,1),150*random_once(2,-1,1)", + explanation = "position a part randomly across X,Y,Z\nbut once, because random_once only generates a number once.\nit, however, needs distinct numbers in the first arguments to distinguish them every time you write the function." + }, + { + nicename = "distance-based fade", + expression = "clamp((250/500) + 1 - (eye_position_distance() / 500),0,1)", + explanation = "a fading based on the viewer's distance. 250 and 500 are the example distances, 250 is where the expression starts diminishing, and 750 is where we reach 0." + }, + { + nicename = "distance between two points", + expression = "part_distance(uid1,uid2)", + explanation = "Trick question! You have some homework! You need to find out your parts' UIDs first.\ntry tools -> copy global id, then paste those in place of uid1 and uid2" + }, + { + nicename = "revolution (orbit)", + expression = "150*sin(time()),150*cos(time()),0", + explanation = "Trick question! You might need to rearrange the expression depending on which coordinate system we're at. For a thing on a pos_noang bone, it works as is. But for something on your head, you would need to swap x and z\n0,150*cos(time()),150*sin(time())" + }, + { + nicename = "spin", + expression = "0,360*time(),0", + explanation = "a simple spinner on Y" + } + }, + ["experimental things"] = { + { + nicename = "control a boolean directly with an event", + expression = "event_alternative(uid1,0,1)", + explanation = "trick question! you need to find out your event's part UID first and substitute uid1\n" + }, + { + nicename = "feedback system controlled with 2 events", + expression = "feedback() + ftime()*(event_alternative(uid1,0,1) + event_alternative(uid2,0,-1))", + explanation = "trick question! you need to find out your event parts' UIDs first and substitute uid1 and uid2.\nthe new event_alternative function gets an event's state\nwe can inject that into our feedback system to act as a positive or negative speed" + }, + { + nicename = "basic if-else statement", + expression = "number_operator_alternative(1,\">\",0,100,50)", + explanation = "might be niche but here's a basic alternator thing, you can compare the 1st and 3rd args with numeric operators like \"above\", \"<\", \"=\", \"~=\" etc. to choose between the 4th and 5th args\nit goes like this\nnumber_operator_alternative(1,\">\",0,100,50)\nif 1>0, return 100, else return 50" + }, + { + nicename = "pick from 3 random colors", + expression = "number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.75)),number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 0.8, 0.65)),number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.58))", + explanation = + "using a shared random source, you can nest number_operator_alternative functions to get a 3-way branching random choice\n0.333 and 0.666 correspond to the chance slices where each choice gets decided so you can change the probabilities by editing these numbers\nBecause of the fact we're going deep, it's not easily readable so I'll lay out each component.\n\n" .. + "R: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.75))\n".. + "G: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 0.8, 0.65))\n".. + "B: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.58))\n\n".. + "The first choice is white (1,1,1), the second choice is light pink (1,0.8,1) like a strawberry milk, the third choice is light creamy brown (0.75,0.65,0.58) like chocolate milk" + }, + { + nicename = "feedback command attractor", + expression = "feedback() + ftime()*(command(\"destination\") - feedback())", + explanation = + "This thing uses a principle of iteration similar to exponential functions to attract the feedback toward any target\n".. + "The delta bit will get smaller and smaller as the gap between destination and feedback closes, stabilizing at 0, thereby stopping.\n".. + "You will utilize pac_proxy commands to set the destination target: \"pac_proxy destination 2\" will make the expression tend toward 2." + } + } + } + + end + +end local PANEL = {} +local player_ban_list = {} +local player_combat_ban_list = {} + +local function encode_table_to_file(str) + local data = {} + if not file.Exists("pac3_config", "DATA") then + file.CreateDir("pac3_config") + + end + + + if str == "pac_editor_shortcuts" then + data = pace.PACActionShortcut + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "pac_editor_partmenu_layouts" then + data = pace.operations_order + file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data)) + elseif str == "pac_part_categories" then + data = pace.partgroups + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "bookmarked_ressources" then + rebuild_bookmarks() + for category, tbl in pairs(pace.bookmarked_ressources) do + data = tbl + str = category + file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data)) + end + + end + +end + +local function decode_table_from_file(str) + if str == "bookmarked_ressources" then + rebuild_bookmarks() + local ressource_types = {"models", "sound", "materials", "sprites"} + for _, category in pairs(ressource_types) do + data = file.Read("pac3_config/bookmarked_" .. category ..".txt", "DATA") + if data then pace.bookmarked_ressources[category] = util.KeyValuesToTable(data) end + end + return + end + + local data = file.Read("pac3_config/" .. str..".txt", "DATA") + if not data then return end + + if str == "pac_editor_shortcuts" then + pace.PACActionShortcut = util.KeyValuesToTable(data) + + elseif str == "pac_editor_partmenu_layouts" then + pace.operations_order = util.JSONToTable(data) + + elseif str == "pac_part_categories" then + pace.partgroups = util.KeyValuesToTable(data) + + end + + +end + +decode_table_from_file("bookmarked_ressources") +pace.bookmarked_ressources = pace.bookmarked_ressources or {} + +function pace.SaveRessourceBookmarks() + encode_table_to_file("bookmarked_ressources") +end + function PANEL:Init() - local pnl = vgui.Create("DPropertySheet", self) - pnl:Dock(FILL) + local master_pnl = vgui.Create("DPropertySheet", self) + master_pnl:Dock(FILL) + + local properties_filter = pace.FillWearSettings(master_pnl) + master_pnl:AddSheet("Wear / Ignore", properties_filter) + + local editor_settings = pace.FillEditorSettings(master_pnl) + master_pnl:AddSheet("Editor menu Settings", editor_settings) + + local editor_settings2 = pace.FillEditorSettings2(master_pnl) + master_pnl:AddSheet("Editor menu Settings 2", editor_settings2) - local properties_filter = pace.FillWearSettings(pnl) + + if game.SinglePlayer() or LocalPlayer():IsAdmin() then + + local general_sv_settings = pace.FillServerSettings(master_pnl) + master_pnl:AddSheet("General Settings (SV)", general_sv_settings) + + local combat_sv_settings = pace.FillCombatSettings(master_pnl) + master_pnl:AddSheet("Combat Settings (SV)", combat_sv_settings) + + local ban_settings = pace.FillBanPanel(master_pnl) + master_pnl:AddSheet("Bans (SV)", ban_settings) + + local combat_ban_settings = pace.FillCombatBanPanel(master_pnl) + master_pnl:AddSheet("Combat Bans (SV)", combat_ban_settings) + + end + - pnl:AddSheet("Wear / Ignore", properties_filter) - self.sheet = pnl + self.sheet = master_pnl --local properties_shortcuts = pace.FillShortcutSettings(pnl) --pnl:AddSheet("Editor Shortcuts", properties_shortcuts) @@ -23,7 +363,7 @@ function pace.OpenSettings() local pnl = vgui.Create("DFrame") pnl:SetTitle("pac settings") pace.settings_panel = pnl - pnl:SetSize(600,600) + pnl:SetSize(800,600) pnl:MakePopup() pnl:Center() pnl:SetSizable(true) @@ -34,4 +374,1217 @@ end concommand.Add("pace_settings", function() pace.OpenSettings() -end) \ No newline at end of file +end) + + +function pace.FillBanPanel(pnl) + local pnl = pnl + local BAN = vgui.Create("DPanel", pnl) + local ply_state_list = player_ban_list or {} + + local ban_list = vgui.Create("DListView", BAN) + ban_list:SetText("ban list") + ban_list:SetSize(400,400) + ban_list:SetPos(10,10) + + ban_list:AddColumn("Player name") + ban_list:AddColumn("SteamID") + ban_list:AddColumn("State") + ban_list:SetSortable(false) + for _,ply in pairs(player.GetAll()) do + --print(ply, pace.IsBanned(ply)) + ban_list:AddLine(ply:Name(),ply:SteamID(),player_ban_list[ply] or "Allowed") + end + + function ban_list:DoDoubleClick( lineID, line ) + --MsgN( "Line " .. lineID .. " was double clicked!" ) + local state = line:GetColumnText( 3 ) + + if state == "Banned" then state = "Allowed" + elseif state == "Allowed" then state = "Banned" end + line:SetColumnText(3,state) + ply_state_list[player.GetBySteamID(line:GetColumnText( 2 ))] = state + PrintTable(ply_state_list) + end + + local ban_confirm_list_button = vgui.Create("DButton", BAN) + ban_confirm_list_button:SetText("Send ban list update to server") + + ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!") + ban_confirm_list_button:SetColor(Color(255,0,0)) + ban_confirm_list_button:SetSize(200, 40) + ban_confirm_list_button:SetPos(450, 10) + function ban_confirm_list_button:DoClick() + net.Start("pac.BanUpdate") + net.WriteTable(ply_state_list) + net.SendToServer() + end + local ban_request_list_button = vgui.Create("DButton", BAN) + ban_request_list_button:SetText("Request ban list from server") + --ban_request_list_button:SetColor(Color(255,0,0)) + ban_request_list_button:SetSize(200, 40) + ban_request_list_button:SetPos(450, 60) + + function ban_request_list_button:DoClick() + net.Start("pac.RequestBanStates") + net.SendToServer() + end + + net.Receive("pac.SendBanStates", function() + local players = net.ReadTable() + player_ban_list = players + PrintTable(players) + end) + + + return BAN +end + +function pace.FillCombatBanPanel(pnl) + local pnl = pnl + local BAN = vgui.Create("DPanel", pnl) + pac.global_combat_whitelist = pac.global_combat_whitelist or {} + + + local ban_list = vgui.Create("DListView", BAN) + ban_list:SetText("Combat ban list") + ban_list:SetSize(400,400) + ban_list:SetPos(10,10) + + ban_list:AddColumn("Player name") + ban_list:AddColumn("SteamID") + ban_list:AddColumn("State") + ban_list:SetSortable(false) + if GetConVar('pac_sv_combat_whitelisting'):GetBool() then + ban_list:SetTooltip( "Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed" ) + else + ban_list:SetTooltip( "Blacklist mode: Default players are allowed to use the combat features" ) + end + + local combat_bans_temp_merger = {} + + for _,ply in pairs(player.GetAll()) do + combat_bans_temp_merger[ply:SteamID()] = pac.global_combat_whitelist[ply:SteamID()]-- or {nick = ply:Nick(), steamid = ply:SteamID(), permission = "Default"} + end + + for id,data in pairs(pac.global_combat_whitelist) do + combat_bans_temp_merger[id] = data + end + + for id,data in pairs(combat_bans_temp_merger) do + ban_list:AddLine(data.nick,data.steamid,data.permission) + end + + function ban_list:DoDoubleClick( lineID, line ) + --MsgN( "Line " .. lineID .. " was double clicked!" ) + local state = line:GetColumnText( 3 ) + + if state == "Banned" then state = "Default" + elseif state == "Default" then state = "Allowed" + elseif state == "Allowed" then state = "Banned" end + line:SetColumnText(3,state) + pac.global_combat_whitelist[string.lower(line:GetColumnText( 2 ))].permission = state + PrintTable(pac.global_combat_whitelist) + end + + local ban_confirm_list_button = vgui.Create("DButton", BAN) + ban_confirm_list_button:SetText("Send combat ban list update to server") + + ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!") + ban_confirm_list_button:SetColor(Color(255,0,0)) + ban_confirm_list_button:SetSize(200, 40) + ban_confirm_list_button:SetPos(450, 10) + function ban_confirm_list_button:DoClick() + net.Start("pac.CombatBanUpdate") + net.WriteTable(pac.global_combat_whitelist) + net.WriteBool(true) + net.SendToServer() + end + local ban_request_list_button = vgui.Create("DButton", BAN) + ban_request_list_button:SetText("Request ban list from server") + --ban_request_list_button:SetColor(Color(255,0,0)) + ban_request_list_button:SetSize(200, 40) + ban_request_list_button:SetPos(450, 60) + + function ban_request_list_button:DoClick() + net.Start("pac.RequestCombatBanStates") + net.SendToServer() + end + + net.Receive("pac.SendCombatBanStates", function() + pac.global_combat_whitelist = net.ReadTable() + PrintTable(pac.global_combat_whitelist) + end) + + + return BAN +end + +function pace.FillCombatSettings(pnl) + local pnl = pnl + + local master_list = vgui.Create("DCategoryList", pnl) + master_list:Dock(FILL) + --general + do + local general_list = master_list:Add("General") + general_list.Header:SetSize(40,40) + general_list.Header:SetFont("DermaLarge") + local general_list_list = vgui.Create("DListLayout") + general_list_list:DockPadding(20,0,20,20) + general_list:SetContents(general_list_list) + + local sv_prop_protection_props_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_prop_protection_props_box:SetText("Enforce generic prop protection for player-owned props and physics entities") + sv_prop_protection_props_box:SetSize(400,30) + sv_prop_protection_props_box:SetConVar("pac_sv_prop_protection") + + + local sv_combat_whitelisting_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_combat_whitelisting_box:SetText("Restrict new pac3 combat (damage zone, lock, force) to only whitelisted users.") + sv_combat_whitelisting_box:SetSize(400,30) + sv_combat_whitelisting_box:SetConVar("pac_sv_combat_whitelisting") + sv_combat_whitelisting_box:SetTooltip("off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed") + + end + + do --hitscan + --[[ + pac_sv_hitscan + pac_sv_hitscan_max_bullets + pac_sv_hitscan_max_damage + pac_sv_hitscan_divide_max_damage_by_max_bullets + ]] + + local hitscans_list = master_list:Add("Hitscans") + hitscans_list.Header:SetSize(40,40) + hitscans_list.Header:SetFont("DermaLarge") + local hitscans_list_list = vgui.Create("DListLayout") + hitscans_list_list:DockPadding(20,0,20,20) + hitscans_list:SetContents(hitscans_list_list) + + local sv_hitscans_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) + sv_hitscans_box:SetText("allow serverside physical projectiles") + sv_hitscans_box:SetSize(400,30) + sv_hitscans_box:SetConVar("pac_sv_projectiles") + + local hitscans_max_dmg_numbox = vgui.Create("DNumSlider", hitscans_list_list) + hitscans_max_dmg_numbox:SetText("Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)") + hitscans_max_dmg_numbox:SetValue(GetConVar("pac_sv_hitscan_max_damage"):GetInt()) + hitscans_max_dmg_numbox:SetMin(0) hitscans_max_dmg_numbox:SetDecimals(0) hitscans_max_dmg_numbox:SetMax(1000000) + hitscans_max_dmg_numbox:SetSize(400,30) + hitscans_max_dmg_numbox:SetConVar("pac_sv_hitscan_max_damage") + + local sv_hitscans_distribute_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) + sv_hitscans_distribute_box:SetText("force hitscans to distribute their total damage accross bullets. if off, every bullet does full damage; if on, adding more bullets doesn't do more damage") + sv_hitscans_distribute_box:SetSize(400,30) + sv_hitscans_distribute_box:SetConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets") + + local hitscans_max_numbullets_numbox = vgui.Create("DNumSlider", hitscans_list_list) + hitscans_max_numbullets_numbox:SetText("Maximum number of bullets for hitscan multishots") + hitscans_max_numbullets_numbox:SetValue(GetConVar("pac_sv_hitscan_max_bullets"):GetInt()) + hitscans_max_numbullets_numbox:SetMin(1) hitscans_max_numbullets_numbox:SetDecimals(0) hitscans_max_numbullets_numbox:SetMax(500) + hitscans_max_numbullets_numbox:SetSize(400,30) + hitscans_max_numbullets_numbox:SetConVar("pac_sv_hitscan_max_bullets") + end + + do --projectiles + local projectiles_list = master_list:Add("Projectiles") + projectiles_list.Header:SetSize(40,40) + projectiles_list.Header:SetFont("DermaLarge") + local projectiles_list_list = vgui.Create("DListLayout") + projectiles_list_list:DockPadding(20,0,20,20) + projectiles_list:SetContents(projectiles_list_list) + + local sv_projectiles_box = vgui.Create("DCheckBoxLabel", projectiles_list_list) + sv_projectiles_box:SetText("allow serverside physical projectiles") + sv_projectiles_box:SetSize(400,30) + sv_projectiles_box:SetConVar("pac_sv_projectiles") + + local projectile_max_phys_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_phys_radius_numbox:SetText("Max projectile physical radius") + projectile_max_phys_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_phys_radius"):GetInt()) + projectile_max_phys_radius_numbox:SetMin(0) projectile_max_phys_radius_numbox:SetDecimals(0) projectile_max_phys_radius_numbox:SetMax(1000) + projectile_max_phys_radius_numbox:SetSize(400,30) + projectile_max_phys_radius_numbox:SetConVar("pac_sv_projectile_max_phys_radius") + + local projectile_max_dmg_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_dmg_radius_numbox:SetText("Max projectile damage radius") + projectile_max_dmg_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage_radius"):GetInt()) + projectile_max_dmg_radius_numbox:SetMin(0) projectile_max_dmg_radius_numbox:SetDecimals(0) projectile_max_dmg_radius_numbox:SetMax(5000) + projectile_max_dmg_radius_numbox:SetSize(400,30) + projectile_max_dmg_radius_numbox:SetConVar("pac_sv_projectile_max_damage_radius") + + local projectile_max_attract_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_attract_radius_numbox:SetText("Max projectile attract radius") + projectile_max_attract_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_attract_radius"):GetInt()) + projectile_max_attract_radius_numbox:SetMin(0) projectile_max_attract_radius_numbox:SetDecimals(0) projectile_max_attract_radius_numbox:SetMax(100000000) + projectile_max_attract_radius_numbox:SetSize(400,30) + projectile_max_attract_radius_numbox:SetConVar("pac_sv_projectile_max_attract_radius") + + local projectile_max_dmg_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_dmg_numbox:SetText("Max projectile damage") + projectile_max_dmg_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage"):GetInt()) + projectile_max_dmg_numbox:SetMin(0) projectile_max_dmg_numbox:SetDecimals(0) projectile_max_dmg_numbox:SetMax(100000000) + projectile_max_dmg_numbox:SetSize(400,30) + projectile_max_dmg_numbox:SetConVar("pac_sv_projectile_max_damage") + + local projectile_max_speed_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_speed_numbox:SetText("Max projectile speed") + projectile_max_speed_numbox:SetValue(GetConVar("pac_sv_projectile_max_speed"):GetInt()) + projectile_max_speed_numbox:SetMin(0) projectile_max_speed_numbox:SetDecimals(0) projectile_max_speed_numbox:SetMax(50000) + projectile_max_speed_numbox:SetSize(400,30) + projectile_max_speed_numbox:SetConVar("pac_sv_projectile_max_speed") + + local projectile_max_mass_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_mass_numbox:SetText("Max projectile mass") + projectile_max_mass_numbox:SetValue(GetConVar("pac_sv_projectile_max_mass"):GetInt()) + projectile_max_mass_numbox:SetMin(0) projectile_max_mass_numbox:SetDecimals(0) projectile_max_mass_numbox:SetMax(500000) + projectile_max_mass_numbox:SetSize(400,30) + projectile_max_mass_numbox:SetConVar("pac_sv_projectile_max_mass") + end + + do --damage zone + local damagezone_list = master_list:Add("Damage Zone") + damagezone_list.Header:SetSize(40,40) + damagezone_list.Header:SetFont("DermaLarge") + local damagezone_list_list = vgui.Create("DListLayout") + damagezone_list_list:DockPadding(20,0,20,20) + damagezone_list:SetContents(damagezone_list_list) + + local sv_dmgzone_box = vgui.Create("DCheckBoxLabel", damagezone_list_list) + sv_dmgzone_box:SetText("Allow damage zone") + sv_dmgzone_box:SetSize(400,30) + sv_dmgzone_box:SetConVar("pac_sv_damage_zone") + + local max_dmgzone_radius_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_radius_numbox:SetText("Max damage zone radius") + max_dmgzone_radius_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_radius"):GetInt()) + max_dmgzone_radius_numbox:SetMin(0) max_dmgzone_radius_numbox:SetDecimals(0) max_dmgzone_radius_numbox:SetMax(50000) + max_dmgzone_radius_numbox:SetSize(400,30) + max_dmgzone_radius_numbox:SetConVar("pac_sv_damage_zone_max_radius") + + local max_dmgzone_length_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_length_numbox:SetText("Max damage zone length") + max_dmgzone_length_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_length"):GetInt()) + max_dmgzone_length_numbox:SetMin(0) max_dmgzone_length_numbox:SetDecimals(0) max_dmgzone_length_numbox:SetMax(50000) + max_dmgzone_length_numbox:SetSize(400,30) + max_dmgzone_length_numbox:SetConVar("pac_sv_damage_zone_max_length") + + local max_dmgzone_damage_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_damage_numbox:SetText("Max damage zone damage") + max_dmgzone_damage_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_damage"):GetInt()) + max_dmgzone_damage_numbox:SetMin(0) max_dmgzone_damage_numbox:SetDecimals(0) max_dmgzone_damage_numbox:SetMax(100000000) + max_dmgzone_damage_numbox:SetSize(400,30) + max_dmgzone_damage_numbox:SetConVar("pac_sv_damage_zone_max_damage") + + local sv_dmgzone_allow_dissolve_box = vgui.Create("DCheckBoxLabel", damagezone_list_list) + sv_dmgzone_allow_dissolve_box:SetText("Allow damage entity dissolvers") + sv_dmgzone_allow_dissolve_box:SetSize(400,30) + sv_dmgzone_allow_dissolve_box:SetConVar("pac_sv_damage_zone_allow_dissolve") + + end + + do --lock part + local lock_list = master_list:Add("Lock part") + lock_list.Header:SetSize(40,40) + lock_list.Header:SetFont("DermaLarge") + local lock_list_list = vgui.Create("DListLayout") + lock_list_list:DockPadding(20,0,20,20) + lock_list:SetContents(lock_list_list) + + local sv_lock_allow_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_allow_box:SetText("Allow lock part") + sv_lock_allow_box:SetSize(400,30) + sv_lock_allow_box:SetConVar("pac_sv_lock") + + local sv_lock_grab_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_box:SetText("Allow lock part grabbing") + sv_lock_grab_box:SetSize(400,30) + sv_lock_grab_box:SetConVar("pac_sv_lock_grab") + + local sv_lock_grab_ply_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_ply_box:SetText("Allow grabbing players") + sv_lock_grab_ply_box:SetSize(400,30) + sv_lock_grab_ply_box:SetConVar("pac_sv_lock_allow_grab_ply") + + local sv_lock_grab_npc_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_npc_box:SetText("Allow grabbing NPCs") + sv_lock_grab_npc_box:SetSize(400,30) + sv_lock_grab_npc_box:SetConVar("pac_sv_lock_allow_grab_npc") + + local sv_lock_grab_ents_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_ents_box:SetText("Allow grabbing other entities") + sv_lock_grab_ents_box:SetSize(400,30) + sv_lock_grab_ents_box:SetConVar("pac_sv_lock_allow_grab_ent") + + local sv_lock_teleport_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_teleport_box:SetText("Allow lock part teleportation") + sv_lock_teleport_box:SetSize(400,30) + sv_lock_teleport_box:SetConVar("pac_sv_lock_teleport") + + local max_lock_radius_numbox = vgui.Create("DNumSlider", lock_list_list) + max_lock_radius_numbox:SetText("Max lock part grab range") + max_lock_radius_numbox:SetValue(GetConVar("pac_sv_lock_max_grab_radius"):GetInt()) + max_lock_radius_numbox:SetMin(0) max_lock_radius_numbox:SetDecimals(0) max_lock_radius_numbox:SetMax(5000) + max_lock_radius_numbox:SetSize(400,30) + max_lock_radius_numbox:SetConVar("pac_sv_lock_max_grab_radius") + end + + do --force + local force_list = master_list:Add("Force part") + force_list.Header:SetSize(40,40) + force_list.Header:SetFont("DermaLarge") + local force_list_list = vgui.Create("DListLayout") + force_list_list:DockPadding(20,0,20,20) + force_list:SetContents(force_list_list) + + local sv_force_box = vgui.Create("DCheckBoxLabel", force_list_list) + sv_force_box:SetText("Allow force part") + sv_force_box:SetSize(400,30) + sv_force_box:SetConVar("pac_sv_force") + + local max_force_radius_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_radius_numbox:SetText("Max force part radius") + max_force_radius_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + max_force_radius_numbox:SetMin(0) max_force_radius_numbox:SetDecimals(0) max_force_radius_numbox:SetMax(50000) + max_force_radius_numbox:SetSize(400,30) + max_force_radius_numbox:SetConVar("pac_sv_force_max_radius") + + local max_force_length_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_length_numbox:SetText("Max force part length") + max_force_length_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + max_force_length_numbox:SetMin(0) max_force_length_numbox:SetDecimals(0) max_force_length_numbox:SetMax(50000) + max_force_length_numbox:SetSize(400,30) + max_force_length_numbox:SetConVar("pac_sv_force_max_length") + + local max_force_amount_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_amount_numbox:SetText("Max force part amount") + max_force_amount_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + max_force_amount_numbox:SetMin(0) max_force_amount_numbox:SetDecimals(0) max_force_amount_numbox:SetMax(10000000) + max_force_amount_numbox:SetSize(400,30) + max_force_amount_numbox:SetConVar("pac_sv_force_max_amount") + end + return master_list +end + +function pace.FillServerSettings(pnl) + local pnl = pnl + + local master_list = vgui.Create("DCategoryList", pnl) + master_list:Dock(FILL) + + --models/entity + --[[ + pac_allow_blood_color + pac_allow_mdl + pac_allow_mdl_entity + pac_modifier_model + pac_modifier_size + ]] + + local model_category = master_list:Add("Allowed Playermodel Mutations") + model_category.Header:SetSize(40,40) + model_category.Header:SetFont("DermaLarge") + local model_category_list = vgui.Create("DListLayout") + model_category_list:DockPadding(20,0,20,20) + model_category:SetContents(model_category_list) + + local pac_allow_blood_color_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_blood_color_box:SetText("Blood") + pac_allow_blood_color_box:SetSize(400,30) + pac_allow_blood_color_box:SetConVar("pac_allow_blood_color") + model_category_list:Add(pac_allow_blood_color_box) + local pac_allow_mdl_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_mdl_box:SetText("MDL") + pac_allow_mdl_box:SetSize(400,30) + pac_allow_mdl_box:SetConVar("pac_allow_mdl") + model_category_list:Add(pac_allow_mdl_box) + local pac_allow_mdl_entity_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_mdl_entity_box:SetText("Entity MDL") + pac_allow_mdl_entity_box:SetSize(400,30) + pac_allow_mdl_entity_box:SetConVar("pac_allow_mdl_entity") + model_category_list:Add(pac_allow_mdl_entity_box) + local pac_modifier_model_box = vgui.Create("DCheckBoxLabel", master_list) + pac_modifier_model_box:SetText("Entity model") + pac_modifier_model_box:SetSize(400,30) + pac_modifier_model_box:SetConVar("pac_modifier_model") + model_category_list:Add(pac_modifier_model_box) + local pac_modifier_size_box = vgui.Create("DCheckBoxLabel", master_list) + pac_modifier_size_box:SetText("Entity size") + pac_modifier_size_box:SetSize(400,30) + pac_modifier_size_box:SetConVar("pac_modifier_size") + model_category_list:Add(pac_modifier_size_box) + + --movement and mass + --[[ + pac_free_movement + ]] + + local movement_category = master_list:Add("Player Movement") + movement_category.Header:SetSize(40,40) + movement_category.Header:SetFont("DermaLarge") + local movement_category_list = vgui.Create("DListLayout") + movement_category_list:DockPadding(20,20,20,20) + movement_category:SetContents(movement_category_list) + + local pac_allow_movement_form = vgui.Create("DComboBox", movement_category_list) + pac_allow_movement_form:SetText("Allow PAC player movement") + --pac_allow_movement_form:SetSize(400,20) + pac_allow_movement_form:SetSortItems(false) + + pac_allow_movement_form:AddChoice("disabled") + pac_allow_movement_form:AddChoice("disabled if noclip not allowed") + pac_allow_movement_form:AddChoice("enabled") + + pac_allow_movement_form.OnSelect = function(_, _, value) + if value == "disabled" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("0") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is disabled.") + elseif value == "disabled if noclip not allowed" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("-1") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is disabled if noclip is not allowed.") + elseif value == "enabled" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("1") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is enabled.") + end + end + + --mode:ChooseOption(mode_str) + + local pac_player_movement_allow_mass_box = vgui.Create("DCheckBoxLabel", movement_category_list) + pac_player_movement_allow_mass_box:SetText("Allow Modify Mass") + pac_player_movement_allow_mass_box:SetSize(400,30) + movement_category_list:Add(pac_player_movement_allow_mass_box) + pac_player_movement_allow_mass_box:SetConVar("pac_player_movement_allow_mass") + + local playermovement_min_mass_numbox = vgui.Create("DNumSlider", movement_category_list) + playermovement_min_mass_numbox:SetText("Mimnimum mass players can set for themselves") + playermovement_min_mass_numbox:SetValue(GetConVar("pac_player_movement_min_mass"):GetFloat()) + playermovement_min_mass_numbox:SetMin(0.01) playermovement_min_mass_numbox:SetDecimals(0) playermovement_min_mass_numbox:SetMax(1000000) + playermovement_min_mass_numbox:SetSize(400,30) + movement_category_list:Add(playermovement_min_mass_numbox) + playermovement_min_mass_numbox:SetConVar("pac_player_movement_min_mass") + + + local playermovement_max_mass_numbox = vgui.Create("DNumSlider", movement_category_list) + playermovement_max_mass_numbox:SetText("Maximum mass players can set for themselves") + playermovement_max_mass_numbox:SetValue(GetConVar("pac_player_movement_max_mass"):GetFloat()) + playermovement_max_mass_numbox:SetMin(0.01) playermovement_max_mass_numbox:SetDecimals(0) playermovement_max_mass_numbox:SetMax(1000000) + playermovement_max_mass_numbox:SetSize(400,30) + movement_category_list:Add(playermovement_max_mass_numbox) + playermovement_max_mass_numbox:SetConVar("pac_player_movement_max_mass") + + + local pac_player_movement_allow_mass_dmgscaling_box = vgui.Create("DCheckBoxLabel", movement_category_list) + pac_player_movement_allow_mass_dmgscaling_box:SetText("Allow damage scaling of physics damage based on player's mass") + pac_player_movement_allow_mass_dmgscaling_box:SetSize(400,30) + movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) + pac_player_movement_allow_mass_dmgscaling_box:SetConVar("pac_player_movement_physics_damage_scaling") + movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) + + + --wear limits and bans + --[[ + pac_sv_draw_distance + pac_sv_hide_outfit_on_death WORKSHOP DEPRECATED + pac_submit_limit + pac_submit_spam + pac_ban + pac_unban + ]] + + local wear_list = master_list:Add("Server wearing/drawing") + wear_list.Header:SetSize(40,40) + wear_list.Header:SetFont("DermaLarge") + local draw_distance_list = vgui.Create("DListLayout") + draw_distance_list:DockPadding(20,0,20,20) + wear_list:SetContents(draw_distance_list) + + local draw_dist_numbox = vgui.Create("DNumSlider", draw_distance_list) + draw_dist_numbox:SetText("Server draw distance") + draw_dist_numbox:SetValue(GetConVar("pac_sv_draw_distance"):GetInt()) + draw_dist_numbox:SetMin(0) draw_dist_numbox:SetDecimals(0) draw_dist_numbox:SetMax(50000) + draw_dist_numbox:SetSize(400,30) + draw_dist_numbox:SetConVar("pac_sv_draw_distance") + + local pac_submit_limit_numbox = vgui.Create("DNumSlider", draw_distance_list) + pac_submit_limit_numbox:SetText("pac_submit limit") + pac_submit_limit_numbox:SetValue(GetConVar("pac_submit_limit"):GetInt()) + pac_submit_limit_numbox:SetMin(0) pac_submit_limit_numbox:SetDecimals(0) pac_submit_limit_numbox:SetMax(100) + pac_submit_limit_numbox:SetSize(400,30) + pac_submit_limit_numbox:SetConVar("pac_submit_limit") + + local pac_submit_spam_box = vgui.Create("DCheckBoxLabel", draw_distance_list) + pac_submit_spam_box:SetText("prevent pac_submit spam") + pac_submit_spam_box:SetSize(400,30) + pac_submit_spam_box:SetConVar("pac_submit_spam") + + + + --misc + --[[ + sv_pac_webcontent_allow_no_content_length + sv_pac_webcontent_limit + pac_to_contraption_allow + pac_max_contraption_entities + pac_restrictions + ]] + local misc_list = master_list:Add("Misc") + misc_list.Header:SetSize(40,40) + misc_list.Header:SetFont("DermaLarge") + local misc_list_list = vgui.Create("DListLayout") + misc_list_list:DockPadding(20,0,20,20) + misc_list:SetContents(misc_list_list) + local webcontent_no_content_box = vgui.Create("DCheckBoxLabel", misc_list_list) + webcontent_no_content_box:SetText("allow downloads with no content length") + webcontent_no_content_box:SetSize(400,30) + webcontent_no_content_box:SetConVar("sv_pac_webcontent_allow_no_content_length") + + local contraption_box = vgui.Create("DCheckBoxLabel", misc_list_list) + contraption_box:SetText("allow contraptions") + contraption_box:SetSize(400,30) + contraption_box:SetConVar("pac_to_contraption_allow") + + local contraption_entities_numbox = vgui.Create("DNumSlider", misc_list_list) + contraption_entities_numbox:SetText("PAC3 contraption entities limit") + contraption_entities_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + contraption_entities_numbox:SetMin(0) contraption_entities_numbox:SetDecimals(0) contraption_entities_numbox:SetMax(200) + contraption_entities_numbox:SetSize(400,30) + contraption_entities_numbox:SetConVar("pac_max_contraption_entities") + + local cam_restrict_box = vgui.Create("DCheckBoxLabel", misc_list_list) + cam_restrict_box:SetText("restrict PAC editor camera movement") + cam_restrict_box:SetSize(400,30) + cam_restrict_box:SetConVar("pac_restrictions") + + + return master_list +end + + + +--part order, shortcuts +function pace.FillEditorSettings(pnl) + + local buildlist_partmenu = {} + local f = vgui.Create( "DPanel", pnl ) + f:SetSize(800) + f:Center() + + local LeftPanel = vgui.Create( "DPanel", f ) -- Can be any panel, it will be stretched + + local partmenu_order_presets = vgui.Create("DComboBox",LeftPanel) + partmenu_order_presets:SetText("Select a part menu preset") + partmenu_order_presets:AddChoice("factory preset") + partmenu_order_presets:AddChoice("expanded PAC4.5 preset") + partmenu_order_presets:AddChoice("custom preset") + partmenu_order_presets:AddChoice("bulk select poweruser") + partmenu_order_presets:SetX(10) partmenu_order_presets:SetY(10) + partmenu_order_presets:SetWidth(200) + partmenu_order_presets:SetHeight(20) + + local partmenu_apply_button = vgui.Create("DButton", LeftPanel) + partmenu_apply_button:SetText("Apply") + partmenu_apply_button:SetX(220) + partmenu_apply_button:SetY(10) + partmenu_apply_button:SetWidth(65) + partmenu_apply_button:SetImage('icon16/accept.png') + + local partmenu_clearlist_button = vgui.Create("DButton", LeftPanel) + partmenu_clearlist_button:SetText("Clear") + partmenu_clearlist_button:SetX(285) + partmenu_clearlist_button:SetY(10) + partmenu_clearlist_button:SetWidth(65) + partmenu_clearlist_button:SetImage('icon16/application_delete.png') + + local partmenu_savelist_button = vgui.Create("DButton", LeftPanel) + partmenu_savelist_button:SetText("Save") + partmenu_savelist_button:SetX(350) + partmenu_savelist_button:SetY(10) + partmenu_savelist_button:SetWidth(70) + partmenu_savelist_button:SetImage('icon16/disk.png') + + + + local partmenu_choices = vgui.Create("DScrollPanel", LeftPanel) + local partmenu_choices_textAdd = vgui.Create("DLabel", LeftPanel) + partmenu_choices_textAdd:SetText("ADD MENU COMPONENTS") + partmenu_choices_textAdd:SetFont("DermaDefaultBold") + partmenu_choices_textAdd:SetColor(Color(0,200,0)) + partmenu_choices_textAdd:SetWidth(200) + partmenu_choices_textAdd:SetX(10) + partmenu_choices_textAdd:SetY(30) + + local partmenu_choices_textRemove = vgui.Create("DLabel", LeftPanel) + partmenu_choices_textRemove:SetText("DOUBLE CLICK TO REMOVE") + partmenu_choices_textRemove:SetColor(Color(200,0,0)) + partmenu_choices_textRemove:SetFont("DermaDefaultBold") + partmenu_choices_textRemove:SetWidth(200) + partmenu_choices_textRemove:SetX(220) + partmenu_choices_textRemove:SetY(30) + + local partmenu_previews = vgui.Create("DListView", LeftPanel) + partmenu_previews:AddColumn("index") + partmenu_previews:AddColumn("control name") + partmenu_previews:SetSortable(false) + partmenu_previews:SetX(220) + partmenu_previews:SetY(50) + partmenu_previews:SetHeight(320) + partmenu_previews:SetWidth(200) + + + + local shortcutaction_choices = vgui.Create("DComboBox", LeftPanel) + shortcutaction_choices:SetText("Select a PAC action") + for _,name in ipairs(pace.PACActionShortcut_Dictionary) do + shortcutaction_choices:AddChoice(name) + end + shortcutaction_choices:SetX(10) shortcutaction_choices:SetY(400) + shortcutaction_choices:SetWidth(170) + shortcutaction_choices:SetHeight(20) + shortcutaction_choices:ChooseOptionID(1) + + function shortcutaction_choices:Think() + self.next = self.next or 0 + self.found = self.found or false + if self.next < RealTime() then self.found = false end + if self:IsHovered() then + if input.IsKeyDown(KEY_UP) then + if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() + 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end + elseif input.IsKeyDown(KEY_DOWN) then + if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() - 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end + else self.found = false end + else self.found = false + end + end + + local shortcuts_description_text = vgui.Create("DLabel", LeftPanel) + shortcuts_description_text:SetFont("DermaDefaultBold") + shortcuts_description_text:SetText("Edit keyboard shortcuts") + shortcuts_description_text:SetColor(Color(0,0,0)) + shortcuts_description_text:SetWidth(200) + shortcuts_description_text:SetX(10) + shortcuts_description_text:SetY(380) + + local shortcutaction_presets = vgui.Create("DComboBox", LeftPanel) + shortcutaction_presets:SetText("Select a shortcut preset") + shortcutaction_presets:AddChoice("factory preset", pace.PACActionShortcut_Default) + shortcutaction_presets:AddChoice("no CTRL preset", pace.PACActionShortcut_NoCTRL) + shortcutaction_presets:AddChoice("Cedric's preset", pace.PACActionShortcut_Cedric) + shortcutaction_presets:SetX(10) shortcutaction_presets:SetY(420) + shortcutaction_presets:SetWidth(170) + shortcutaction_presets:SetHeight(20) + function shortcutaction_presets:OnSelect(num, name, data) + pace.PACActionShortcut = data + pac.Message("Selected shortcut preset: " .. name) + for i,v in pairs(data) do + if #v > 0 then MsgC(Color(50,250,50), i .. "\n") end + for i2,v2 in pairs(v) do + MsgC(Color(0,250,250), "\t" .. table.concat(v2, "+") .. "\n") + end + end + end + + + local shortcutaction_choices_textCurrentShortcut = vgui.Create("DLabel", LeftPanel) + shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit:") + shortcutaction_choices_textCurrentShortcut:SetColor(Color(0,60,160)) + shortcutaction_choices_textCurrentShortcut:SetWidth(200) + shortcutaction_choices_textCurrentShortcut:SetX(200) + shortcutaction_choices_textCurrentShortcut:SetY(420) + + + local shortcutaction_index = vgui.Create("DNumberWang", LeftPanel) + shortcutaction_index:SetToolTip("index") + shortcutaction_index:SetValue(1) + shortcutaction_index:SetMin(1) + shortcutaction_index:SetMax(10) + shortcutaction_index:SetWidth(30) + shortcutaction_index:SetHeight(20) + shortcutaction_index:SetX(180) + shortcutaction_index:SetY(400) + + local function update_shortcutaction_choices_textCurrentShortcut(num) + shortcutaction_choices_textCurrentShortcut:SetText("") + num = tonumber(num) + local action, val = shortcutaction_choices:GetSelected() + local strs = {} + + if action and action ~= "" then + if pace.PACActionShortcut[action] and pace.PACActionShortcut[action][num] then + for i,v in ipairs(pace.PACActionShortcut[action][num]) do + strs[i] = v + end + shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit: " .. table.concat(strs, " + ")) + else + shortcutaction_choices_textCurrentShortcut:SetText("") + end + end + end + update_shortcutaction_choices_textCurrentShortcut(1) + + function shortcutaction_index:OnValueChanged(num) + update_shortcutaction_choices_textCurrentShortcut(num) + end + + function shortcutaction_choices:OnSelect(i, action) + shortcutaction_index:OnValueChanged(shortcutaction_index:GetValue()) + end + + local binder1 = vgui.Create("DBinder", LeftPanel) + binder1:SetX(10) + binder1:SetY(440) + binder1:SetHeight(30) + binder1:SetWidth(90) + function binder1:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 1: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 1: "..input.GetKeyName( num )) + end + + local binder2 = vgui.Create("DBinder", LeftPanel) + binder2:SetX(105) + binder2:SetY(440) + binder2:SetHeight(30) + binder2:SetWidth(90) + function binder2:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 2: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 2: "..input.GetKeyName( num )) + end + + local binder3 = vgui.Create("DBinder", LeftPanel) + binder3:SetX(200) + binder3:SetY(440) + binder3:SetHeight(30) + binder3:SetWidth(90) + function binder3:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 3: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 3: "..input.GetKeyName( num )) + end + + local function send_active_shortcut_to_assign(tbl) + local action = shortcutaction_choices:GetValue() + local index = shortcutaction_index:GetValue() + + if not tbl then + pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {} + pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {} + + if table.IsEmpty(pace.PACActionShortcut[action][index]) then + pace.PACActionShortcut[action][index] = nil + if table.IsEmpty(pace.PACActionShortcut[action]) then + pace.PACActionShortcut[action] = nil + end + else + pace.PACActionShortcut[action][index] = nil + end + elseif not table.IsEmpty(tbl) then + pace.AssignEditorShortcut(shortcutaction_choices:GetValue(), tbl, shortcutaction_index:GetValue()) + end + encode_table_to_file("pac_editor_shortcuts") + end + + local bindclear = vgui.Create("DButton", LeftPanel) + bindclear:SetText("clear keys") + bindclear:SetTooltip("deletes the current shortcut at the current index") + bindclear:SetX(10) + bindclear:SetY(480) + bindclear:SetHeight(30) + bindclear:SetWidth(90) + bindclear:SetColor(Color(200,0,0)) + function bindclear:DoClick() + binder1:SetSelectedNumber(0) + binder2:SetSelectedNumber(0) + binder3:SetSelectedNumber(0) + send_active_shortcut_to_assign() + update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue()) + end + + local bindoverwrite = vgui.Create("DButton", LeftPanel) + bindoverwrite:SetText("confirm") + bindoverwrite:SetTooltip("applies the current shortcut combination at the current index") + bindoverwrite:SetX(105) + bindoverwrite:SetY(480) + bindoverwrite:SetHeight(30) + bindoverwrite:SetWidth(90) + bindoverwrite:SetColor(Color(0,200,0)) + function bindoverwrite:DoClick() + local tbl = {} + local i = 1 + --print(binder1:GetValue(), binder2:GetValue(), binder3:GetValue()) + if binder1:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder1:GetValue()) i = i + 1 end + if binder2:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder2:GetValue()) i = i + 1 end + if binder3:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder3:GetValue()) end + if not table.IsEmpty(tbl) then + pace.FlashNotification("Combo " .. shortcutaction_index:GetValue() .. " committed: " .. table.concat(tbl," ")) + if not pace.PACActionShortcut[shortcutaction_choices:GetValue()] then + pace.PACActionShortcut[shortcutaction_choices:GetValue()] = {} + end + send_active_shortcut_to_assign(tbl) + update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue()) + end + encode_table_to_file("pac_editor_shortcuts") + end + + local bindcapture_text = vgui.Create("DLabel", LeftPanel) + bindcapture_text:SetFont("DermaDefaultBold") + bindcapture_text:SetText("") + bindcapture_text:SetColor(Color(0,0,0)) + bindcapture_text:SetX(300) + bindcapture_text:SetY(480) + bindcapture_text:SetSize(300, 30) + function bindcapture_text:Think() + self:SetText(pace.bindcapturelabel_text) + end + local bindcapture = vgui.Create("DButton", LeftPanel) + bindcapture:SetText("capture input") + bindcapture:SetX(200) + bindcapture:SetY(480) + bindcapture:SetHeight(30) + bindcapture:SetWidth(90) + pace.bindcapturelabel_text = "" + function bindcapture:DoClick() + pace.delayshortcuts = RealTime() + 5 + local input_active = {} + local no_input = true + local inputs_str = "" + local previous_inputs_str = "" + pace.FlashNotification("Recording input... Release one key when you're done") + + hook.Add("Tick", "pace_buttoncapture_countdown", function() + pace.delayshortcuts = RealTime() + 5 + local inputs_tbl = {} + inputs_str = "" + for i=1,172,1 do --build bool list of all current keys + if input.IsKeyDown(i) then + input_active[i] = true + inputs_tbl[i] = true + no_input = false + inputs_str = inputs_str .. input.GetKeyName(i) .. " " + else + input_active[i] = false + end + end + pace.bindcapturelabel_text = "Recording input:\n" .. inputs_str + + if previous_inputs_tbl and table.Count(previous_inputs_tbl) > 0 then + if table.Count(inputs_tbl) < table.Count(previous_inputs_tbl) then + pace.FlashNotification("ending input!" .. previous_inputs_str) + + local tbl = {} + local i = 1 + for key,bool in pairs(previous_inputs_tbl) do + tbl[i] = input.GetKeyName(key) + i = i + 1 + end + --print(shortcutaction_choices:GetValue(), shortcutaction_index:GetValue()) + pace.AssignEditorShortcut(shortcutaction_choices:GetValue(), tbl, shortcutaction_index:GetValue()) + --pace.PACActionShortcut[shortcutaction_choices:GetValue()][shortcutaction_index:GetValue()] = tbl + pace.delayshortcuts = RealTime() + 5 + pace.bindcapturelabel_text = "Recorded input:\n" .. previous_inputs_str + hook.Remove("Tick", "pace_buttoncapture_countdown") + end + end + previous_inputs_str = inputs_str + previous_inputs_tbl = inputs_tbl + end) + + end + + local bulkbinder = vgui.Create("DBinder", LeftPanel) + function bulkbinder:OnChange( num ) + GetConVar("pac_bulk_select_key"):SetString(input.GetKeyName( num )) + end + bulkbinder:SetX(210) + bulkbinder:SetY(400) + bulkbinder:SetSize(80,20) + bulkbinder:SetText("bulk select key") + + local function ClearPartMenuPreviewList() + local i = 0 + while (partmenu_previews:GetLine(i + 1) ~= nil) do + i = i+1 + end + for v=i,0,-1 do + if partmenu_previews:GetLine(v) ~= nil then partmenu_previews:RemoveLine(v) end + v = v - 1 + 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' + 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", menu) + pnl:SetText(string.Replace(string.upper(v),"_"," ")) + pnl:SetImage(FindImage(v)) + + function pnl:DoClick() + table.insert(buildlist_partmenu,v) + partmenu_previews:AddLine(#buildlist_partmenu,v) + end + partmenu_choices:AddItem(pnl) + pnl:SetHeight(18) + pnl:SetWidth(200) + pnl:SetY(20*(i-1)) + end + + partmenu_choices:SetWidth(200) + partmenu_choices:SetHeight(320) + partmenu_choices:SetVerticalScrollbarEnabled(true) + + + local RightPanel = vgui.Create( "DTree", f ) + Test_Node = RightPanel:AddNode( "Test", "icon16/world.png" ) + test_part = pac.CreatePart("base") //the menu needs a part to get its full version in preview + function RightPanel:DoRightClick() + temp_list = pace.operations_order + pace.operations_order = buildlist_partmenu + pace.OnPartMenu(test_part) + temp_list = pace.operations_order + pace.operations_order = temp_list + end + function RightPanel:DoClick() + temp_list = pace.operations_order + pace.operations_order = buildlist_partmenu + pace.OnPartMenu(test_part) + temp_list = pace.operations_order + pace.operations_order = temp_list + end + test_part:Remove() //dumb workaround but it works + + + local div = vgui.Create( "DHorizontalDivider", f ) + div:Dock( FILL ) + div:SetLeft( LeftPanel ) + div:SetRight( RightPanel ) + + div:SetDividerWidth( 8 ) + div:SetLeftMin( 50 ) + div:SetRightMin( 50 ) + div:SetLeftWidth( 450 ) + partmenu_order_presets.OnSelect = function( self, index, value ) + local temp_list = {"wear","save","load"} + if value == "factory preset" then + temp_list = table.Copy(pace.operations_default) + elseif value == "expanded PAC4.5 preset" then + temp_list = table.Copy(pace.operations_experimental) + elseif value == "bulk select poweruser" then + temp_list = table.Copy(pace.operations_bulk_poweruser) + elseif value == "custom preset" then + temp_list = {"wear","save","load"} + end + ClearPartMenuPreviewList() + for i,v in ipairs(temp_list) do + partmenu_previews:AddLine(i,v) + end + buildlist_partmenu = temp_list + end + + function partmenu_apply_button:DoClick() + pace.operations_order = buildlist_partmenu + end + + function partmenu_clearlist_button:DoClick() + ClearPartMenuPreviewList() + buildlist_partmenu = {} + end + + function partmenu_savelist_button:DoClick() + encode_table_to_file("pac_editor_partmenu_layouts") + end + + function partmenu_previews:DoDoubleClick(id, line) + table.remove(buildlist_partmenu,id) + + ClearPartMenuPreviewList() + for i,v in ipairs(buildlist_partmenu) do + partmenu_previews:AddLine(i,v) + end + + PrintTable(buildlist_partmenu) + end + + return f +end + +--camera movement +function pace.FillEditorSettings2(pnl) + local panel = vgui.Create( "DPanel", pnl ) + --[[ movement binds + CreateConVar("pac_editor_camera_forward_bind", "w") + + CreateConVar("pac_editor_camera_back_bind", "s") + + CreateConVar("pac_editor_camera_moveleft_bind", "a") + + CreateConVar("pac_editor_camera_moveright_bind", "d") + + CreateConVar("pac_editor_camera_up_bind", "space") + + CreateConVar("pac_editor_camera_down_bind", "") + + ]] + + --[[pace.camera_movement_binds = { + ["forward"] = pace.camera_forward_bind, + ["back"] = pace.camera_back_bind, + ["moveleft"] = pace.camera_moveleft_bind, + ["moveright"] = pace.camera_moveright_bind, + ["up"] = pace.camera_up_bind, + ["down"] = pace.camera_down_bind, + ["slow"] = pace.camera_slow_bind, + ["speed"] = pace.camera_speed_bind + } + ]] + + local movement_binders_label = vgui.Create("DLabel", panel) + movement_binders_label:SetText("PAC editor camera movement") + movement_binders_label:SetFont("DermaDefaultBold") + movement_binders_label:SetColor(Color(0,0,0)) + movement_binders_label:SetSize(200,40) + movement_binders_label:SetPos(30,5) + + local forward_binder = vgui.Create("DBinder", panel) + forward_binder:SetSize(40,40) + forward_binder:SetPos(100,40) + forward_binder:SetTooltip("move forward") + forward_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["forward"]:GetString())) + function forward_binder:OnChange(num) + pace.camera_movement_binds["forward"]:SetString(input.GetKeyName( num )) + end + + local back_binder = vgui.Create("DBinder", panel) + back_binder:SetSize(40,40) + back_binder:SetPos(100,80) + back_binder:SetTooltip("move back") + back_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["back"]:GetString())) + function back_binder:OnChange(num) + pace.camera_movement_binds["back"]:SetString(input.GetKeyName( num )) + end + + local moveleft_binder = vgui.Create("DBinder", panel) + moveleft_binder:SetSize(40,40) + moveleft_binder:SetPos(60,80) + moveleft_binder:SetTooltip("move left") + moveleft_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveleft"]:GetString())) + function moveleft_binder:OnChange(num) + pace.camera_movement_binds["moveleft"]:SetString(input.GetKeyName( num )) + end + + local moveright_binder = vgui.Create("DBinder", panel) + moveright_binder:SetSize(40,40) + moveright_binder:SetPos(140,80) + moveright_binder:SetTooltip("move right") + moveright_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveright"]:GetString())) + function moveright_binder:OnChange(num) + pace.camera_movement_binds["moveright"]:SetString(input.GetKeyName( num )) + end + + local up_binder = vgui.Create("DBinder", panel) + up_binder:SetSize(40,40) + up_binder:SetPos(180,40) + up_binder:SetTooltip("move up") + up_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["up"]:GetString())) + function up_binder:OnChange(num) + pace.camera_movement_binds["up"]:SetString(input.GetKeyName( num )) + end + + local down_binder = vgui.Create("DBinder", panel) + down_binder:SetSize(40,40) + down_binder:SetPos(180,80) + down_binder:SetTooltip("move down") + down_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["down"]:GetString())) + function down_binder:OnChange(num) + print(num, input.GetKeyName( num )) + pace.camera_movement_binds["down"]:SetString(input.GetKeyName( num )) + end + + local slow_binder = vgui.Create("DBinder", panel) + slow_binder:SetSize(40,40) + slow_binder:SetPos(20,80) + slow_binder:SetTooltip("go slow") + slow_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["slow"]:GetString())) + function slow_binder:OnChange(num) + pace.camera_movement_binds["slow"]:SetString(input.GetKeyName( num )) + end + + local speed_binder = vgui.Create("DBinder", panel) + speed_binder:SetSize(40,40) + speed_binder:SetPos(20,40) + speed_binder:SetTooltip("go fast") + speed_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["speed"]:GetString())) + function speed_binder:OnChange(num) + pace.camera_movement_binds["speed"]:SetString(input.GetKeyName( num )) + end + + return panel +end + +function pace.GetPartMenuComponentPreviewForMenuEdit(menu, option_name) + local pnl = vgui.Create("DButton", menu) + pnl:SetText(string.Replace(string.upper(option_name),"_"," ")) + return pnl +end + + +decode_table_from_file("pac_editor_shortcuts") +decode_table_from_file("pac_editor_partmenu_layouts") +decode_table_from_file("pac_part_categories") \ No newline at end of file diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 3f805bcc9..aa9023a9b 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -1,113 +1,243 @@ -CreateClientConVar( "pac_focus_input1", "", true, false, "Set the first key for the custom shorctut of pac3 editor focus. Format according to internal names of the keys as console binds e.g. e or ctrl" ) -CreateClientConVar( "pac_focus_input2", "", true, false, "Set the second key for the custom shorctut of pac3 editor focus. Format according to internal names of the keys as console binds e.g. e or ctrl" ) +include("parts.lua") +include("popups_part_tutorials.lua") + +local L = pace.LanguageString + concommand.Add( "pac_toggle_focus", function() pace.Call("ToggleFocus") end) concommand.Add( "pac_focus", function() pace.Call("ToggleFocus") end) +local legacy_input = CreateConVar("pac_editor_shortcuts_legacy_mode", "1", FCVAR_ARCHIVE, "Reverts the editor to hardcoded key checks ignoring customizable keys. Some keys are hidden and held down causing serious editor usage problems.") + local last_recorded_combination -local focusKeyPrimary -local focusKeySecondary - -local ShortcutActions - -ShortcutActions = {} -ShortcutActions["wear"] = {0} -ShortcutActions["save"] = {1,2,3} -ShortcutActions["focus"] = {67,83} -ShortcutActions["copy"] = {0} -ShortcutActions["paste"] = {0} -ShortcutActions["cut"] = {0} -ShortcutActions["delete"] = {0} -ShortcutActions["expand_all"] = {0} -ShortcutActions["collapse_all"] = {0} -ShortcutActions["undo"] = {0} -ShortcutActions["redo"] = {0} - -concommand.Add( "pac_echo_shortcut", function() - timer.Simple( 3, function() - surface.PlaySound("buttons/button1.wav") - inputs = get_all_inputs() - print("inputs:") - printed_list = "" - input_list = {} - for k,v in pairs(inputs) do - printed_list = printed_list .. "key" .. k .. ", (code " .. v .. ", named " .. input.GetKeyName(v) .. ")\n" - input_list[k] = v - end - print(printed_list) - print(unpack(input_list)) - last_recorded_combination = input_list - end - ) -end) ---[[ -concommand.Add( "pac_assign_shortcut", function() - local action_name = "focus" - ShortcutActions[action_name] = last_recorded_combination - print("assigned "..action_name.." for ") - print(unpack(ShortcutActions[action_name])) -end) - - - - -concommand.Add( "pac_echo_shortcut_megatable", function() - for k,v in ipairs(ShortcutActions) do - unpacked_combo_string = "" - for k2,v2 in ipairs(ShortcutActions[k][2]) do - if (ShortcutActions[k][2][k2] ~= nil) then - unpacked_combo_string = unpacked_combo_string .. ShortcutActions[k][2][k2] .. "," - end - end - print(ShortcutActions[k][1] .. " " .. unpacked_combo_string) - end - - test_combos = { - {1,2}, - {1,3,2}, - {1,2,3}, - {0}, - {4}, - {54,57}, +pace.PACActionShortcut_Dictionary = { + "wear", + "save", + "load", + "hide_editor", + "hide_editor_visible", + "copy", + "paste", + "cut", + "clone", + "delete", + "expand_all", + "collapse_all", + "editor_up", + "editor_down", + "editor_pageup", + "editor_pagedown", + "editor_node_collapse", + "editor_node_expand", + "undo", + "redo", + "hide", + "panic", + "restart", + "partmenu", + "add_part", + "toolbar_pac", + "toolbar_tools", + "toolbar_player", + "toolbar_view", + "toolbar_options", + "zoom_panel", + "T_Pose", + "clear_bulkselect", + "copy_bulkselect", + "bulk_insert", + "bulk_delete", + "bulk_pack", + "bulk_paste_1", + "bulk_paste_2", + "bulk_paste_3", + "bulk_paste_4", + "bulk_paste_properties_1", + "bulk_paste_properties_2", + "bulk_hide", + "help_info_popup", + "ultra_cleanup" +} + +pace.PACActionShortcut_Default = { + ["wear"] = { + [1] = {"CTRL", "n"} + }, + + ["save"] = { + [1] = {"CTRL", "s"} + }, + + ["hide_editor"] = { + [1] = {"CTRL", "e"} + }, + + ["hide_editor_visible"] = { + [1] = {"ALT", "e"} + }, + + ["copy"] = { + [1] = {"CTRL", "c"} + }, + + ["paste"] = { + [1] = {"CTRL", "v"} + }, + ["cut"] = { + [1] = {"CTRL", "x"} + }, + ["delete"] = { + [1] = {"DEL"} + }, + ["expand_all"] = { + + }, + ["collapse_all"] = { + + }, + ["undo"] = { + [1] = {"CTRL", "z"} + }, + ["redo"] = { + [1] = {"CTRL", "y"} + }, + ["T_Pose"] = { + [1] = {"CTRL", "t"} + }, + ["editor_up"] = { + [1] = {"UPARROW"} + }, + ["editor_down"] = { + [1] = {"DOWNARROW"} + }, + ["editor_pageup"] = { + [1] = {"PGUP"} + }, + ["editor_pagedown"] = { + [1] = {"PGDN"} + }, + ["editor_node_collapse"] = { + [1] = {"LEFTARROW"} + }, + ["editor_node_expand"] = { + [1] = {"RIGHTARROW"} } - - print("the match between " .. "{1,2}" .. " and {\"save\",{1,2,3}} is ", matches_input(test_combos[1], "save")) - -end) +} -function get_all_inputs() - list = {} - count = 1 - for key=1,BUTTON_CODE_LAST do - if input.IsKeyDown(key) then - list[count] = key - count = count + 1 - end - end - return list -end +pace.PACActionShortcut_NoCTRL = { + ["wear"] = { + [1] = {"n"} + }, + ["save"] = { + [1] = {"m"} + }, -function matches_input(combo_in, action_name) - local target = ShortcutActions[action_name][2] - print("combo_in length is ", #combo_in, ", target length is ", #target) - if #combo_in ~= #target.length then return false end - full_match = true - print("trying to match") - for k,v in pairs(combo_in) do - if ((combo_in[v] == nil) or (target[v] == nil)) then - full_match = false - else - if (combo_in[v] == target[v]) then - print("matched " .. target[v]) - else - full_match = false - end - end - end - return full_match -end -]]-- + ["hide_editor"] = { + [1] = {"q"} + }, + + ["copy"] = { + [1] = {"c"} + }, + + ["paste"] = { + [1] = {"v"} + }, + + ["cut"] = { + [1] = {"x"} + }, + + ["delete"] = { + [1] = {"DEL"} + }, + + ["undo"] = { + [1] = {"z"} + }, + + ["redo"] = { + [1] = {"y"} + }, + + ["T_Pose"] = { + [1] = {"t"} + } +} + +pace.PACActionShortcut_Cedric = { + ["wear"] = { + [1] = {"CTRL", "n"} + }, + + ["save"] = { + [1] = {"CTRL", "m"} + }, + + ["hide_editor"] = { + [1] = {"CTRL", "e"}, + [2] = {"INS"}, + [3] = {"TAB"} + }, + + ["hide_editor_visible"] = { + [1] = {"LALT", "e"} + }, + + ["copy"] = { + [1] = {"CTRL", "c"} + }, + + ["paste"] = { + [1] = {"CTRL", "v"} + }, + ["cut"] = { + [1] = {"CTRL", "x"} + }, + ["delete"] = { + [1] = {"DEL"} + }, + ["expand_all"] = { + [1] = {"x"} + }, + ["collapse_all"] = { + [1] = {"c"} + }, + ["undo"] = { + [1] = {"CTRL", "z"}, + [2] = {"u"} + }, + ["redo"] = { + [1] = {"CTRL", "y"}, + [2] = {"i"} + }, + ["tpose"] = { + [1] = {"CTRL", "t"} + }, + ["editor_up"] = { + [1] = {"UPARROW"} + }, + ["editor_down"] = { + [1] = {"DOWNARROW"} + }, + ["editor_pageup"] = { + [1] = {"PGUP"} + }, + ["editor_pagedown"] = { + [1] = {"PGDN"} + }, + ["editor_node_collapse"] = { + [1] = {"LEFTARROW"} + }, + ["editor_node_expand"] = { + [1] = {"RIGHTARROW"} + } +} + +pace.PACActionShortcut = pace.PACActionShortcut or pace.PACActionShortcut_Cedric + +--pace.PACActionShortcut = pace.PACActionShortcuts_NoCTRL --[[thinkUndo() @@ -137,26 +267,499 @@ function pace.OnShortcutWear() end local last = 0 -local last_print_time = CurTime() +pace.passthrough_keys = { + [KEY_LWIN] = true, + [KEY_RWIN] = true, + [KEY_CAPSLOCK] = true, + [KEY_CAPSLOCKTOGGLE] = true +} + + +function pace.LookupShortcutsForAction(action, provided_inputs, do_it) + pace.BulkSelectKey = input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString()) + + --combo is the table of key names for one combo slot + local function input_contains_one_match(combo, action, inputs) + --if pace.shortcut_inputs_count ~= #combo then return false end --if input has too much or too little keys, we already know it doesn't match + for _,key in ipairs(combo) do --check the combo's keys for a match + --[[if not (input.IsKeyDown(input.GetKeyCode(key)) and inputs[input.GetKeyCode(key)]) then --all keys must be there + return false + end]] + + if not input.IsKeyDown(input.GetKeyCode(key)) then --all keys must be there + return false + end + end + return true + end + + local function shortcut_contains_counterexample(combo, action, inputs) + + local counterexample = false + for key,bool in ipairs(inputs) do --check the input for counter-examples + if input.IsKeyDown(key) then + if not table.HasValue(combo, input.GetKeyName(key)) then --any keypress that is not in the combo invalidates the combo + --some keys don't count as counterexamples?? + --random windows or capslocktoggle keys being pressed screw up the input + --bulk select should allow rolling select with the scrolling options + + if key == pace.BulkSelectKey and not action == "editor_up" and not action == "editor_down" and not action == "editor_pageup" and not action == "editor_pagedown" then + counterexample = true + elseif not pace.passthrough_keys[key] and key ~= pace.BulkSelectKey then + counterexample = true + end + + if pace.passthrough_keys[key] or key == pace.BulkSelectKey then + counterexample = false + end + end + + end + end + return counterexample + end + + if not pace.PACActionShortcut[action] then return false end + local final_success = false + + local keynames_str = "" + for key,bool in ipairs(provided_inputs) do + if bool then keynames_str = keynames_str .. input.GetKeyName(key) .. "," end + end + + for i=1,10,1 do --go through each combo slot + if pace.PACActionShortcut[action][i] then --is there a combo in that slot + combo = pace.PACActionShortcut[action][i] + local keynames_str = "" + + local single_match = false + if input_contains_one_match(combo, action, provided_inputs) then + single_match = true + if shortcut_contains_counterexample(combo, action, provided_inputs) then + single_match = false + end + end + + if single_match and do_it then + pace.DoShortcutFunc(action) + final_success = true + --MsgC(Color(50,255,100),"-------------------------\n\n\n\nrun yes " .. action .. "\n" .. keynames_str .. "\n\n\n\n-------------------------") + end + end + end + + return final_success +end + +function pace.AssignEditorShortcut(action, tbl, index) + print("received a new shortcut assignation") + + pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {} + pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {} + + if table.IsEmpty(tbl) or not tbl then + pace.PACActionShortcut[action][index] = nil + print("wiped shortcut " .. action .. " off index " .. index) + return + end + --validate tbl argument + for i,key in pairs(tbl) do + print(i,key) + if not isnumber(i) then print("passed a wrong table") return end + if not isstring(key) then print("passed a wrong table") return end + end + pace.PACActionShortcut[action][index] = tbl +end + +function pace.DoShortcutFunc(action) + + pace.delaybulkselect = RealTime() + 0.5 + pace.delayshortcuts = RealTime() + 0.2 + pace.delaymovement = RealTime() + 1 + + if action == "editor_up" then pace.DoScrollControls(action) + elseif action == "editor_down" then pace.DoScrollControls(action) + elseif action == "editor_pageup" then pace.DoScrollControls(action) + elseif action == "editor_pagedown" then pace.DoScrollControls(action) + end + if action == "editor_node_expand" then pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) + elseif action == "editor_node_collapse" then pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) end + + if action == "redo" then pace.Redo(pace.current_part) pace.delayshortcuts = RealTime() end + if action == "undo" then pace.Undo(pace.current_part) pace.delayshortcuts = RealTime() end + if action == "delete" then pace.RemovePart(pace.current_part) end + if action == "hide" then pace.current_part:SetHide(not pace.current_part:GetHide()) end + + if action == "copy" then pace.Copy(pace.current_part) end + if action == "cut" then pace.Cut(pace.current_part) end + if action == "paste" then pace.Paste(pace.current_part) end + if action == "clone" then pace.Clone(pace.current_part) end + if action == "save" then pace.Call("ShortcutSave") end + if action == "load" then + local function add_expensive_submenu_load(pnl, callback) + local old = pnl.OnCursorEntered + pnl.OnCursorEntered = function(...) + callback() + pnl.OnCursorEntered = old + return old(...) + end + end + local menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + + menu.GetDeleteSelf = function() return false end + + menu:AddOption(L"load from url", function() + Derma_StringRequest( + L"load parts", + L"Some indirect urls from on pastebin, dropbox, github, etc are handled automatically. Pasting the outfit's file contents into the input field will also work.", + "", + + function(name) + pace.LoadParts(name, clear, override_part) + end + ) + end):SetImage(pace.MiscIcons.url) + + menu:AddOption(L"load from clipboard", function() + pace.MultilineStringRequest( + L"load parts from clipboard", + L"Paste the outfits content here.", + "", + + function(name) + local data,err = pace.luadata.Decode(name) + if data then + pace.LoadPartsFromTable(data, clear, override_part) + end + end + ) + end):SetImage(pace.MiscIcons.paste) + + if not override_part and pace.example_outfits then + local examples, pnl = menu:AddSubMenu(L"examples") + pnl:SetImage(pace.MiscIcons.help) + examples.GetDeleteSelf = function() return false end + + local sorted = {} + for k,v in pairs(pace.example_outfits) do sorted[#sorted + 1] = {k = k, v = v} end + table.sort(sorted, function(a, b) return a.k < b.k end) + + for _, data in pairs(sorted) do + examples:AddOption(data.k, function() pace.LoadPartsFromTable(data.v) end) + :SetImage(pace.MiscIcons.outfit) + end + end + + menu:AddSpacer() + + pace.AddOneDirectorySavedPartsToMenu(menu, "templates", "templates") + pace.AddOneDirectorySavedPartsToMenu(menu, "__backup_save", "backups") + + menu:AddSpacer() + do + local menu, icon = menu:AddSubMenu(L"load (expensive)", function() pace.LoadParts(nil, true) end) + menu:SetDeleteSelf(false) + icon:SetImage(pace.MiscIcons.load) + add_expensive_submenu_load(icon, function() pace.AddSavedPartsToMenu(menu, true) end) + end + + menu:SetMaxHeight(ScrH() - y) + menu:MakePopup() + + end + if action == "wear" then pace.Call("ShortcutWear") end + + if action == "hide_editor" then pace.Call("ToggleFocus") pace.delaymovement = RealTime() pace.delaybulkselect = RealTime() end + if action == "hide_editor_visible" then pace.Call("ToggleFocus", true) end + if action == "panic" then pac.Panic() end + if action == "restart" then RunConsoleCommand("pac_restart") end + if action == "collapse_all" then + + local part = pace.current_part + + if not part or not part:IsValid() then + pace.FlashNotification('No part to collapse') + else + + end + part:CallRecursive('SetEditorExpand', GetConVar("pac_reverse_collapse"):GetBool()) + pace.RefreshTree(true) + end + if action == "expand_all" then + + local part = pace.current_part + + if not part or not part:IsValid() then + pace.FlashNotification('No part to collapse') + else + + end + part:CallRecursive('SetEditorExpand', not GetConVar("pac_reverse_collapse"):GetBool()) + pace.RefreshTree(true) + end + + if action == "partmenu" then pace.OnPartMenu(pace.current_part) end + if action == "add_part" then pace.OnAddPartMenu(pace.current_part) end + if action == "toolbar_tools" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + pace.AddToolsToMenu(menu) + end + if action == "toolbar_pac" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:AddOption("pac") + menu:SetPos(x,y) + pace.PopulateMenuBarTab(menu, "pac") + end + if action == "toolbar_options" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + pace.PopulateMenuBarTab(menu, "options") + end + if action == "toolbar_player" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + pace.PopulateMenuBarTab(menu, "player") + + end + if action == "toolbar_view" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + pace.PopulateMenuBarTab(menu, "view") + end + + if action == "zoom_panel" then + pace.PopupMiniFOVSlider() + end + + if action == "T_Pose" then pace.SetTPose(not pace.GetTPose()) end + + if action == "clear_bulkselect" then pace.ClearBulkList() end + if action == "copy_bulkselect" then pace.BulkCopy(pace.current_part) end + if action == "bulk_insert" then pace.BulkCutPaste(pace.current_part) end + if action == "bulk_delete" then pace.BulkRemovePart() end + if action == "bulk_pack" then + root = pac.CreatePart("group") + for i,v in ipairs(pace.BulkSelectList) do + v:SetParent(root) + end + end + if action == "bulk_paste_1" then pace.BulkPasteFromBulkSelectToSinglePart(pace.current_part) end + if action == "bulk_paste_2" then pace.BulkPasteFromSingleClipboard(pace.current_part) end + if action == "bulk_paste_3" then pace.BulkPasteFromBulkClipboard(pace.current_part) end + if action == "bulk_paste_4" then pace.BulkPasteFromBulkClipboardToBulkSelect(pace.current_part) end + + if action == "bulk_paste_properties_1" then + pace.Copy(pace.current_part) + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end + if action == "bulk_paste_properties_2" then + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end + if action == "bulk_hide" then pace.BulkHide() end + + if action == "help_info_popup" then + if pace.floating_popup_reserved then + pace.floating_popup_reserved:Remove() + end + + --[[pac.InfoPopup("Looks like you don't have an active part. You should right click and go make one to get started", { + obj_type = "screen", + clickfunc = function() pace.OnAddPartMenu(pace.current_part) end, + hoverfunc = "open", + pac_part = false + }, ScrW()/2, ScrH()/2)]] + + + local popup_setup_tbl = { + obj_type = "", + clickfunc = function() pace.OnAddPartMenu(pace.current_part) end, + hoverfunc = "open", + pac_part = pace.current_part, + panel_exp_width = 900, panel_exp_height = 400 + } + + --obj_type types + local popup_prefered_type = GetConVar("pac_popups_preferred_location"):GetString() + popup_setup_tbl.obj_type = popup_prefered_type + + if popup_prefered_type == "pac tree label" then + popup_setup_tbl.obj = pace.current_part.pace_tree_node + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) + + elseif popup_prefered_type == "part world" then + popup_setup_tbl.obj = pace.current_part + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) + + elseif popup_prefered_type == "screen" then + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, ScrW()/2, ScrH()/2) + + elseif popup_prefered_type == "cursor" then + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, input.GetCursorPos()) + + elseif popup_prefered_type == "editor bar" then + popup_setup_tbl.obj = pace.Editor + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) + + end + + + + + --[[if IsValid(pace.current_part) then + pac.AttachInfoPopupToPart(pace.current_part) + else + pac.InfoPopup("Looks like you don't have an active part. You should right click and go make one to get started", { + obj_type = "screen", + --hoverfunc = function() pace.OnAddPartMenu(pace.current_part) end, + pac_part = false + }, ScrW()/2, ScrH()/2) + end]] + end + + if action == "ultra_cleanup" then + pace.UltraCleanup(pace.current_part) + end + + +end + +pace.delaybulkselect = 0 +pace.delayshortcuts = 0 +pace.delaymovement = 0 + +--only check once. what does this mean? +--if a shortcut is SUCCESSFULLY run (check_input = false), stop checking until inputs are reset (if no_input then check_input = true end) +--always refresh the inputs, but check if we stay the same before checking the shortcuts! +-- + +skip = false +no_input_override = false +has_run_something = false +previous_inputs_str = "" function pace.CheckShortcuts() - --[[ - if input.IsKeyDown(KEY_H) then - if last_print_time + 1 < CurTime() then - surface.PlaySound("buttons/button9.wav") - print("input report!") - print(unpack(get_all_inputs())) - print("done at time of "..last_print_time.."\n") - --chat.print("input report!\n"..unpack(get_all_inputs()).."\ndone at time of "..last_print_time) - last_print_time = CurTime() - end - end]]-- - focusKeyPrimary = input.GetKeyCode(GetConVar("pac_focus_input1"):GetString()) - focusKeySecondary = input.GetKeyCode(GetConVar("pac_focus_input2"):GetString()) + if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then + if gui.IsConsoleVisible() then return end + if not pace.Editor or not pace.Editor:IsValid() then return end + if last > RealTime() or input.IsMouseDown(MOUSE_LEFT) then return end + + if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_E) then + pace.Call("ToggleFocus", true) + last = RealTime() + 0.2 + end + + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_E) then + pace.Call("ToggleFocus") + last = RealTime() + 0.2 + end + + if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then + RunConsoleCommand("pac_restart") + end + + -- Only if the editor is in the foreground + if pace.IsFocused() then + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_S) then + pace.Call("ShortcutSave") + last = RealTime() + 0.2 + end + + -- CTRL + (W)ear? + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_N) then + pace.Call("ShortcutWear") + last = RealTime() + 0.2 + end + + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_T) then + pace.SetTPose(not pace.GetTPose()) + last = RealTime() + 0.2 + end + + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_F) then + pace.properties.search:SetVisible(true) + pace.properties.search:RequestFocus() + pace.properties.search:SetEnabled(true) + pace.property_searching = true + + last = RealTime() + 0.2 + end + + end + return + end + + local input_active = {} + local no_input = true + no_input_override = false + local inputs_str = "" + pace.shortcut_inputs_count = 0 + for i=1,172,1 do --build bool list of all current keys + if input.IsKeyDown(i) then + if pace.passthrough_keys[i] or i == pace.BulkSelectKey then no_input_override = true end + input_active[i] = true + pace.shortcut_inputs_count = pace.shortcut_inputs_count + 1 + no_input = false + inputs_str = inputs_str .. input.GetKeyName(i) .. " " + else input_active[i] = false end + end + if previous_inputs_str ~= inputs_str then + if last + 0.2 > RealTime() and has_run_something then + skip = true + else + has_run_something = false + end + + end + if no_input then + skip = false + end + previous_inputs_str = inputs_str + + + if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end if gui.IsConsoleVisible() then return end if not pace.Editor or not pace.Editor:IsValid() then return end - if last > RealTime() or input.IsMouseDown(MOUSE_LEFT) then return end + + + local master_bool = true + + + if skip and not no_input_override then return end + + local starttime = SysTime() + + for action,list_of_lists in pairs(pace.PACActionShortcut) do + if not has_run_something then + if action == "hide_editor" and pace.LookupShortcutsForAction(action, input_active, true) then --we can focus back if editor is not focused + --pace.DoShortcutFunc(action) + last = RealTime() + has_run_something = true + check_input = false + elseif pace.Focused and pace.LookupShortcutsForAction(action, input_active, true) then --we can't do anything else if not focused + --pace.DoShortcutFunc(action) + pace.FlashNotification(action) + last = RealTime() + has_run_something = true + check_input = false + end + end + end + + if master_bool then return end + if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_E) then pace.Call("ToggleFocus", true) @@ -167,18 +770,7 @@ function pace.CheckShortcuts() pace.Call("ToggleFocus") last = RealTime() + 0.2 end - -- can make new hardcoded custom shortcuts for focus - --[[if input.IsKeyDown(KEY_LSHIFT) and input.IsKeyDown(KEY_R) then - pace.Call("ToggleFocus") - last = RealTime() + 0.2 - end]]-- - --convar custom inputs - --if not ((focusKeyPrimary == -1) and (focusKeySecondary == -1)) then - if input.IsKeyDown(focusKeyPrimary) and input.IsKeyDown(focusKeySecondary) then - pace.Call("ToggleFocus") - last = RealTime() + 0.2 - end - --end + if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then RunConsoleCommand("pac_restart") @@ -210,34 +802,9 @@ function pace.CheckShortcuts() last = RealTime() + 0.2 end - - end -end - ---[[ -function pace.FillShortcutSettings(pnl) - local list = vgui.Create("DCategoryList", pnl) - list:Dock(FILL) - do - local cat = list:Add(L"Wear") - cat.Header:SetSize(40,40) - cat.Header:SetFont("DermaLarge") - local list = vgui.Create("DListLayout") - list:DockPadding(20,20,20,20) - cat:SetContents(list) - - local mode = vgui.Create("DComboBox", list) - - mode.OnSelect = function(_, _, value) - if IsValid(mode.form) then - mode.form:Remove() - end - mode.form:SetParent(list) - end + end - return list end -]]-- pac.AddHook("Think", "pace_shortcuts", pace.CheckShortcuts) @@ -426,16 +993,21 @@ do end pac.AddHook("Think", "pace_keyboard_shortcuts", function() + if not pace.IsActive() then return end if not pace.Focused then return end if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end if gui.IsConsoleVisible() then return end - thinkUndo() - thinkCopy() - thinkPaste() - thinkCut() - thinkDelete() - thinkExpandAll() - thinkCollapseAll() + if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then + thinkUndo() + thinkCopy() + thinkPaste() + thinkCut() + thinkDelete() + thinkExpandAll() + thinkCollapseAll() + end + end) -end \ No newline at end of file +end + diff --git a/lua/pac3/editor/client/tools.lua b/lua/pac3/editor/client/tools.lua index b94b3dceb..330eea2e3 100644 --- a/lua/pac3/editor/client/tools.lua +++ b/lua/pac3/editor/client/tools.lua @@ -1,5 +1,6 @@ local L = pace.LanguageString pace.Tools = {} +include("parts.lua") function pace.AddToolsToMenu(menu) menu.GetDeleteSelf = function() return false end @@ -782,6 +783,58 @@ pace.AddTool(L"dump player submaterials", function() end end) +pace.AddTool(L"dump model submaterials", function(part) + if part.ClassName == "model" or part.ClassName == "model2" then + for id,mat in pairs(part:GetOwner():GetMaterials()) do + chat.AddText(("%d %s"):format(id,tostring(mat))) + end + end +end) + +pace.AddTool(L"proxy/event: Engrave targets", function(part) + local function reassign(part) + if part.ClassName == "proxy" then + if not IsValid(part.TargetPart) then + if part.AffectChildren and table.Count(part:GetChildren()) == 1 then + part:SetTargetPart(part:GetChildren()[1]) + part:SetAffectChildren(nil) + else + part:SetTargetPart(part:GetParent()) + end + end + elseif part.ClassName == "event" then + if not IsValid(part.DestinationPart) then + if part.AffectChildrenOnly == true and table.Count(part:GetChildren()) == 1 then + part:SetDestinationPart(part:GetChildren()[1]) + elseif part.AffectChildrenOnly == false then + part:SetDestinationPart(part:GetParent()) + end + end + end + end + if part ~= part:GetRootPart() then + reassign(part) + else + for i,part2 in pairs(pac.GetLocalParts()) do + reassign(part2) + end + end +end) + +--aka pace.UltraCleanup +pace.AddTool(L"Destroy hidden parts, proxies and events", function(part) + + if not part then part = pace.current_part end + root = part:GetRootPart() + + pnl = Derma_Query("Only do this if you know what you're doing!\nMark parts as important in their notes to protect them.", "Warning", + "Destroy!", function() pace.UltraCleanup( root ) end, + "cancel", nil + ) + pnl:SetWidth(300) + +end) + pace.AddTool(L"stop all custom animations", function() pac.animations.StopAllEntityAnimations(pac.LocalPlayer) pac.animations.ResetEntityBoneMatrix(pac.LocalPlayer) diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 8f04a0905..bd35dcb3f 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -12,6 +12,26 @@ acsfnc("Pos", Vector(5,5,5)) acsfnc("Angles", Angle(0,0,0)) acsfnc("FOV", 75) +pace.camera_forward_bind = CreateClientConVar("pac_editor_camera_forward_bind", "w", true) +pace.camera_back_bind = CreateClientConVar("pac_editor_camera_back_bind", "s", true) +pace.camera_moveleft_bind = CreateClientConVar("pac_editor_camera_moveleft_bind", "a", true) +pace.camera_moveright_bind = CreateClientConVar("pac_editor_camera_moveright_bind", "d", true) +pace.camera_up_bind = CreateClientConVar("pac_editor_camera_up_bind", "space", true) +pace.camera_down_bind = CreateClientConVar("pac_editor_camera_down_bind", "", true) +pace.camera_slow_bind = CreateClientConVar("pac_editor_camera_slow_bind", "lctrl", true) +pace.camera_speed_bind = CreateClientConVar("pac_editor_camera_speed_bind", "lshift", true) + +pace.camera_movement_binds = { + ["forward"] = pace.camera_forward_bind, + ["back"] = pace.camera_back_bind, + ["moveleft"] = pace.camera_moveleft_bind, + ["moveright"] = pace.camera_moveright_bind, + ["up"] = pace.camera_up_bind, + ["down"] = pace.camera_down_bind, + ["slow"] = pace.camera_slow_bind, + ["speed"] = pace.camera_speed_bind +} + function pace.GetViewEntity() return pace.ViewEntity:IsValid() and pace.ViewEntity or pac.LocalPlayer end @@ -141,6 +161,10 @@ end local WORLD_ORIGIN = Vector(0, 0, 0) +local function MovementBindDown(name) + return input.IsButtonDown(input.GetKeyCode(pace.camera_movement_binds[name]:GetString())) +end + local function CalcDrag() if @@ -161,7 +185,7 @@ local function CalcDrag() local ftime = FrameTime() * 50 local mult = 5 - if input.IsKeyDown(KEY_LCONTROL) or input.IsKeyDown(KEY_RCONTROL) then + if MovementBindDown("slow") then mult = 0.1 end @@ -197,7 +221,7 @@ local function CalcDrag() mult = mult * math.min(origin:Distance(pace.ViewPos) / 200, 3) - if input.IsKeyDown(KEY_LSHIFT) then + if MovementBindDown("speed") then mult = mult + 5 end @@ -223,28 +247,30 @@ local function CalcDrag() end end - if input.IsKeyDown(KEY_W) then - pace.ViewPos = pace.ViewPos + pace.ViewAngles:Forward() * mult * ftime - elseif input.IsKeyDown(KEY_S) then - pace.ViewPos = pace.ViewPos - pace.ViewAngles:Forward() * mult * ftime - end - - if input.IsKeyDown(KEY_D) then - pace.ViewPos = pace.ViewPos + pace.ViewAngles:Right() * mult * ftime - elseif input.IsKeyDown(KEY_A) then - pace.ViewPos = pace.ViewPos - pace.ViewAngles:Right() * mult * ftime - end - if input.IsKeyDown(KEY_SPACE) then - if not IsValid(pace.timeline.frame) then - pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * mult * ftime + if pace.delaymovement < RealTime() then + if MovementBindDown("forward") then + pace.ViewPos = pace.ViewPos + pace.ViewAngles:Forward() * mult * ftime + elseif MovementBindDown("back") then + pace.ViewPos = pace.ViewPos - pace.ViewAngles:Forward() * mult * ftime end - end - --[[if input.IsKeyDown(KEY_LALT) then - pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * -mult * ftime - end]] + if MovementBindDown("moveright") then + pace.ViewPos = pace.ViewPos + pace.ViewAngles:Right() * mult * ftime + elseif MovementBindDown("moveleft") then + pace.ViewPos = pace.ViewPos - pace.ViewAngles:Right() * mult * ftime + end + if MovementBindDown("up") then + if not IsValid(pace.timeline.frame) then + pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * mult * ftime + end + elseif MovementBindDown("down") then + if not IsValid(pace.timeline.frame) then + pace.ViewPos = pace.ViewPos - pace.ViewAngles:Up() * mult * ftime + end + end + end end @@ -338,6 +364,7 @@ function pace.PostRenderVGUI() end function pace.EnableView(b) + if b then pac.AddHook("GUIMousePressed", "editor", pace.GUIMousePressed) pac.AddHook("GUIMouseReleased", "editor", pace.GUIMouseReleased) @@ -580,3 +607,52 @@ function pace.ResetEyeAngles() pac.SetupBones(ent) end end + +function pace.PopupMiniFOVSlider() + zoom_persistent = GetConVar("pac_zoom_persistent") + zoom_smooth = GetConVar("pac_zoom_smooth") + local zoomframe = vgui.Create( "DPanel" ) + local x,y = input.GetCursorPos() + zoomframe:SetPos(x - 90,y - 10) + zoomframe:SetSize( 180, 20 ) + + zoomframe.zoomslider = vgui.Create("DNumSlider", zoomframe) + zoomframe.zoomslider:DockPadding(4,0,0,0) + zoomframe.zoomslider:SetSize(200, 20) + zoomframe.zoomslider:SetMin( 0 ) + zoomframe.zoomslider:SetMax( 100 ) + zoomframe.zoomslider:SetDecimals( 0 ) + zoomframe.zoomslider:SetText("Camera FOV") + zoomframe.zoomslider:SetDark(true) + zoomframe.zoomslider:SetDefaultValue( 75 ) + + zoomframe.zoomslider:SetValue( pace.ViewFOV ) + + function zoomframe:Think(...) + pace.ViewFOV = zoomframe.zoomslider:GetValue() + if zoom_smooth:GetInt() == 1 then + pace.SetZoom(zoomframe.zoomslider:GetValue(),true) + else + pace.SetZoom(zoomframe.zoomslider:GetValue(),false) + end + end + + local hook_id = "pac_tools_menu"..util.SHA256(tostring(zoomframe)) + + pac.AddHook("VGUIMousePressed", hook_id, function(pnl, code) + pace.OverridingFOVSlider = true --to link the values with the original panel in the pac editor panel + if not IsValid(zoomframe) then + pac.RemoveHook("VGUIMousePressed", hook_id) + return + end + if code == MOUSE_LEFT or code == MOUSE_RIGHT then + if not zoomframe:IsOurChild(pnl) then + if zoomframe.zoomslider then zoomframe.zoomslider:Remove() end + zoomframe:Remove() + pac.RemoveHook("VGUIMousePressed", hook_id) + pace.OverridingFOVSlider = false + end + end + end) + +end diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index 22b0f0434..aa6512310 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -240,7 +240,7 @@ end) function pace.Notify(allowed, reason, name) name = name or "???" - if allowed == true then + if allowed == true then pac.Message(string.format('Your part %q has been applied', name)) else chat.AddText(Color(255, 255, 0), "[PAC3] ", Color(255, 0, 0), string.format('The server rejected applying your part (%q) - %s', name, reason)) @@ -253,15 +253,62 @@ end) do local function LoadUpDefault() - if next(pac.GetLocalParts()) then - pac.Message("not wearing autoload outfit, already wearing something") - elseif pace.IsActive() then - pac.Message("not wearing autoload outfit, editor is open") + if GetConVar("pac_prompt_for_autoload"):GetBool() then + local files, directories = file.Find( "pac3/__backup/*.txt", "DATA", "datedesc") + if not files then + Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)", + "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() + pac.Message("Wearing autoload...") + pace.LoadParts("autoload") + pace.WearParts() + end, + + "cancel", function() pac.Message("Not loading autoload or backups...") end + ) + else + if files[1] then + local latest_autosave = "pac3/__backup/" .. files[1] + Derma_Query("Do you want to load an outfit?", "PAC3 autoload (pac_prompt_for_autoload)", + "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() + pac.Message("Wearing autoload...") + pace.LoadParts("autoload") + pace.WearParts() + end, + + "load latest backup : " .. latest_autosave .. " " .. string.NiceSize(file.Size(latest_autosave, "DATA")), function() + pac.Message("Wearing latest backup outfit...") + pace.LoadParts("__backup/" .. files[1], true) + pace.WearParts() + end, + + "cancel", function() pac.Message("Not loading autoload or backups...") end + ) + else + Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)", + "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() + pac.Message("Wearing autoload...") + pace.LoadParts("autoload") + pace.WearParts() + end, + + "cancel", function() pac.Message("Not loading autoload or backups...") end + ) + end + + end + else - pac.Message("Wearing autoload...") - pace.LoadParts("autoload") - pace.WearParts() + if next(pac.GetLocalParts()) then + pac.Message("not wearing autoload outfit, already wearing something") + elseif pace.IsActive() then + pac.Message("not wearing autoload outfit, editor is open") + else + pac.Message("Wearing autoload...") + pace.LoadParts("autoload") + pace.WearParts() + end end + pac.RemoveHook("Think", "pac_request_outfits") pac.Message("Requesting outfits in 8 seconds...") diff --git a/lua/pac3/editor/server/bans.lua b/lua/pac3/editor/server/bans.lua index 2d3bfdb81..101be43fe 100644 --- a/lua/pac3/editor/server/bans.lua +++ b/lua/pac3/editor/server/bans.lua @@ -1,3 +1,7 @@ +util.AddNetworkString("pac.BanUpdate") +util.AddNetworkString("pac.RequestBanStates") +util.AddNetworkString("pac.SendBanStates") + local function get_bans() local str = file.Read("pac_bans.txt", "DATA") @@ -111,3 +115,39 @@ function pace.IsBanned(ply) return pace.Bans[ply:UniqueID()] ~= nil end + +net.Receive("pac.BanUpdate", function(len, ply) + pac.Message("Received ban list update operation from : ", ply) + pac.Message("Time : ", os.date( "%a %X %x", os.time() )) + local playerlist = net.ReadTable() + for i,v in pairs(playerlist) do + if playerlist[i] == "Allowed" then + pace.Unban(i) + elseif playerlist[i] == "Banned" then + pace.Ban(i) + end + print(i, "banned?", pace.IsBanned(i), "Update ->", playerlist[i]) + end +end) + +net.Receive("pac.RequestBanStates", function(len,ply) + local archive = net.ReadBool() + pac.Message("Received ban list request from : ", ply) + pac.Message("Time : ", os.date( "%a %X %x", os.time() )) + local players = {} + for _,v in pairs(player.GetAll()) do + players[v] = false + end + if not pace.Bans then + pace.Bans = get_bans() + end + for i,v in pairs(pace.Bans) do + print(player.GetBySteamID(i), player.GetBySteamID(v[1])) + local ply = player.GetBySteamID(v[1]) + players[ply] = true + end + + net.Start("pac.SendBanStates") + net.WriteTable(players) + net.Send(ply) +end) diff --git a/lua/pac3/editor/server/combat_bans.lua b/lua/pac3/editor/server/combat_bans.lua new file mode 100644 index 000000000..2d919bb24 --- /dev/null +++ b/lua/pac3/editor/server/combat_bans.lua @@ -0,0 +1,98 @@ +util.AddNetworkString("pac.BanUpdate") +util.AddNetworkString("pac.RequestBanStates") +util.AddNetworkString("pac.SendBanStates") + +if SERVER then + local function get_combat_ban_states() + local str = file.Read("pac_combat_bans.txt", "DATA") + + local banstates = {} + + if str and str ~= "" then + banstates = util.KeyValuesToTable(str) + end + + do -- check if this needs to be rebuilt + local k,v = next(banstates) + if isstring(v) then + local temp = {} + + for k,v in pairs(banstates) do + permission = pac.global_combat_whitelist[player.GetBySteamID(k)] or "Default" + temp[util.CRC("gm_" .. v .. "_gm")] = {steamid = v, name = k, permission = permission} + end + + banstates = temp + end + end + + return banstates + end + + local function load_table_from_file() + tbl_on_file = get_combat_ban_states() + for id, data in pairs(tbl_on_file) do + if not pac.global_combat_whitelist[id] then + pac.global_combat_whitelist[id] = tbl_on_file[id] + end + end + end + + + util.AddNetworkString("pac.CombatBanUpdate") + util.AddNetworkString("pac.SendCombatBanStates") + util.AddNetworkString("pac.RequestCombatBanStates") + + pac.old_tbl_on_file = get_combat_ban_states() + + net.Receive("pac.CombatBanUpdate", function() + --get old states first + pac.old_tbl_on_file = get_combat_ban_states() + + load_table_from_file() + + local combatstates_update = net.ReadTable() + local is_id_table = net.ReadBool() + local banstates_for_file = pac.old_tbl_on_file + + --update + if not is_id_table then + for ply, perm in pairs(combatstates_update) do + banstates_for_file[ply:SteamID()] = { + steamid = ply:SteamID(), + nick = ply:Nick(), + permission = perm + } + + pac.global_combat_whitelist[ply:SteamID()] = { + steamid = ply:SteamID(), + nick = ply:Nick(), + permission = perm + } + end + else + pac.global_combat_whitelist = combatstates_update + banstates_for_file = combatstates_update + end + + file.Write("pac_combat_bans.txt", util.TableToKeyValues(banstates_for_file), "DATA") + end) + + net.Receive("pac.RequestCombatBanStates", function(len, ply) + net.Start("pac.SendCombatBanStates") + net.WriteTable(pac.global_combat_whitelist) + net.Broadcast() + end) + + concommand.Add("pac_read_combat_bans", function() + print("PAC3 combat bans and whitelist:") + for k,v in pairs(get_combat_ban_states()) do + print("\t" .. v.nick .. " is " .. v.permission .. " [" .. v.steamid .. "]") + end + end) + + concommand.Add("pac_read_outfit_bans", function() + PrintTable(pace.Bans) + end) + +end \ No newline at end of file diff --git a/lua/pac3/editor/server/init.lua b/lua/pac3/editor/server/init.lua index b9c6eb142..3a14feeb5 100644 --- a/lua/pac3/editor/server/init.lua +++ b/lua/pac3/editor/server/init.lua @@ -22,6 +22,8 @@ do end end +CreateConVar("pac_sv_prop_outfits", "0", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow applying parts on props serverside') + function pace.CanPlayerModify(ply, ent) if not IsValid(ply) or not IsValid(ent) then return false @@ -42,13 +44,19 @@ function pace.CanPlayerModify(ply, ent) if ent.CPPIGetOwner and ent:CPPIGetOwner() == ply then return true end - + + if GetConVar("pac_sv_prop_outfits"):GetBool() then + return true + end + do local tr = util.TraceLine({ start = ply:EyePos(), endpos = ent:WorldSpaceCenter(), filter = ply }) if tr.Entity == ent and hook.Run("CanTool", ply, tr, "paint") == true then return true end end + + return false end @@ -59,6 +67,7 @@ include("wear_filter.lua") include("bans.lua") include("spawnmenu.lua") include("show_outfit_on_use.lua") +include("pac_settings_manager.lua") do util.AddNetworkString("pac_in_editor") diff --git a/lua/pac3/editor/server/pac_settings_manager.lua b/lua/pac3/editor/server/pac_settings_manager.lua new file mode 100644 index 000000000..0a31bc019 --- /dev/null +++ b/lua/pac3/editor/server/pac_settings_manager.lua @@ -0,0 +1,13 @@ + +util.AddNetworkString("pac_send_sv_cvar") + +net.Receive("pac_send_sv_cvar", function(len,ply) + if ply == Entity(1) or ply:IsAdmin() then print("authenticated") else return end + local cmd = net.ReadString() + local val = net.ReadString() + --if cmd == "" then + + --end + GetConVar(cmd):SetString(val) + print("[PAC3]: Admin "..ply:GetName().." set "..cmd.." to "..val) +end) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 965b06b89..a4d48d6d7 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1,7 +1,78 @@ +--lua_openscript pac3/extra/shared/net_combat.lua +if SERVER then + include("pac3/editor/server/combat_bans.lua") + include("pac3/editor/server/bans.lua") +end + + +pac.global_combat_whitelist = pac.global_combat_whitelist or {} + +local hitscan_allow = CreateConVar('pac_sv_hitscan', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow hitscan parts serverside') +local hitscan_max_bullets = CreateConVar('pac_sv_hitscan_max_bullets', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum number of bullets') +local hitscan_max_damage = CreateConVar('pac_sv_hitscan_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum damage') +local hitscan_spreadout_dmg = CreateConVar('pac_sv_hitscan_divide_max_damage_by_max_bullets', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether or not force hitscans to divide their damage among the number of bullets fired') + +local damagezone_allow = CreateConVar('pac_sv_damage_zone', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow damage zone parts serverside') +local damagezone_max_damage = CreateConVar('pac_sv_damage_zone_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum damage') +local damagezone_max_length = CreateConVar('pac_sv_damage_zone_max_length', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum length') +local damagezone_max_radius = CreateConVar('pac_sv_damage_zone_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum radius') +local damagezone_allow_dissolve = CreateConVar('pac_sv_damage_zone_allow_dissolve', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to enable entity dissolvers and removing NPCs\' weapons on death for damagezone') + +local lock_allow = CreateConVar('pac_sv_lock', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock parts serverside') +local lock_allow_grab = CreateConVar('pac_sv_lock_grab', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part grabs serverside') +local lock_allow_teleport = CreateConVar('pac_sv_lock_teleport', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part teleports serverside') +local lock_max_radius = CreateConVar('pac_sv_lock_max_grab_radius', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'lock part maximum grab radius') +local lock_allow_grab_ply = CreateConVar('pac_sv_lock_allow_grab_ply', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing players with lock part') +local lock_allow_grab_npc = CreateConVar('pac_sv_lock_allow_grab_npc', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing NPCs with lock part') +local lock_allow_grab_ent = CreateConVar('pac_sv_lock_allow_grab_ent', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing other entities with lock part') + +local force_allow = CreateConVar('pac_sv_force', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow force parts serverside') +local force_max_length = CreateConVar('pac_sv_force_max_length', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum length') +local force_max_radius = CreateConVar('pac_sv_force_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum radius') +local force_max_amount = CreateConVar('pac_sv_force_max_amount', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum amount of force') + +local healthmod_allow = CreateConVar('pac_sv_healthmod', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow health modifier parts serverside') +local healthmod_allowed_extra_bars = CreateConVar('pac_sv_healthmod_extra_bars', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Extra health bars') +local healthmod_allow_change_maxhp = CreateConVar('pac_sv_healthmod_allow_maxhp', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow players to change their maximum health and armor.') +local healthmod_minimum_dmgscaling = CreateConVar('pac_sv_healthmod_min_damagescaling', -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Minimum health modifier amount. Negative values can heal.') + + +local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') +local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') + +local damageable_point_ent_classes = { + ["predicted_viewmodel"] = false, + ["prop_physics"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true +} + +local physics_point_ent_classes = { + ["prop_physics"] = true, + ["prop_physics_multiplayer"] = true, + ["prop_ragdoll"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true +} + +local contraption_classes = { + ["prop_physics"] = true, +} + + + local grab_consents = {} local damage_zone_consents = {} -local grab_pairs = {} -local pairs_grabbed_by = {} +local force_consents = {} +local hitscan_consents = {} +local calcview_consents = {} +local active_force_ids = {} local damage_types = { generic = 0, --generic damage @@ -36,7 +107,6 @@ local damage_types = { removenoragdoll = 4194304, --don't create a ragdoll on death slowburn = 2097152, -- - explosion = -1, -- util.BlastDamageInfo fire = -1, -- ent:Ignite(5) -- env_entity_dissolver @@ -49,7 +119,129 @@ local damage_types = { armor = -1, } +do --define a basic class for the bullet emitters + local ENT = {} + ENT.Type = "anim" + ENT.ClassName = "pac_bullet_emitter" + ENT.Spawnable = false + scripted_ents.Register(ENT, "pac_bullet_emitter") +end + if SERVER then + + --hack fix to stop GetOwner returning [NULL Entity] + hook.Add("PlayerSpawnedProp", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedNPC", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedRagdoll", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedSENT", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedSWEP", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedVehicle", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedEffect", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) + + local function IsPossibleContraptionEntity(ent) + if not IsValid(ent) then return false end + local b = (string.find(ent:GetClass(), "phys") ~= nil + or string.find(ent:GetClass(), "anchor") ~= nil + or string.find(ent:GetClass(), "rope") ~= nil + or string.find(ent:GetClass(), "gmod") ~= nil) + --print("entity", ent, "contraption?", b) + return b + end + + local function IsPropProtected(ent, ply) + + local reason = "" + local pac_sv_prop_protection = global_combat_prop_protection:GetBool() + + local prop_protected = ent:GetCreator():IsPlayer() and ent:GetCreator() ~= ply + + local contraption = IsPossibleContraptionEntity(ent) and ent:IsConstrained() + + if prop_protected and contraption then + reason = "it's a contraption owned by another player" + return true, reason + end + --apply prop protection + if pac_sv_prop_protection and prop_protected then + reason = "we enforce generic prop protection in the server" + return true, reason + end + return false, "it's fine" + end + + --whitelisting/blacklisting check + local function PlayerIsCombatAllowed(ply) + if pac.global_combat_whitelist[string.lower(ply:SteamID())] then + if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Allowed" then return true end + if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end + end + + if global_combat_whitelisting:GetBool() then --if server uses the high-trust whitelisting mode + if pac.global_combat_whitelist[string.lower(ply:SteamID())] then + if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission ~= "Allowed" then return false end --if player is not in whitelist, stop! + end + else --if server uses the default, blacklisting mode + if pac.global_combat_whitelist[string.lower(ply:SteamID())] then + if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end --if player is in blacklist, stop! + end + end + + return true + end + + --consent check + local function PlayerAllowsCalcView(ply) + return grab_consents[ply] and calcview_consents[ply] --oops it's redundant but I prefer it this way + end + + local function ApplyLockState(ent, bool) --Change the movement states and reset some other angle-related things + --the grab imposes MOVETYPE_NONE and no collisions + --reverting the state requires to reset the eyeang roll in case it was modified + if ent:IsPlayer() then + if bool then + ent:SetMoveType(MOVETYPE_NONE) + ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) + else + ent:SetMoveType(MOVETYPE_WALK) + ent:SetCollisionGroup(COLLISION_GROUP_NONE) + local eyeang = ent:EyeAngles() + eyeang.r = 0 + ent:SetEyeAngles(eyeang) + ent:SetPos(ent:GetPos() + Vector(0,0,10)) + net.Start("pac_lock_imposecalcview") + net.WriteBool(false) + net.WriteVector(Vector(0,0,0)) + net.WriteAngle(Angle(0,0,0)) + net.Send(ent) + ent.has_calcview = false + end + + elseif ent:IsNPC() then + if bool then + ent:SetMoveType(MOVETYPE_NONE) + ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) + else + ent:SetMoveType(MOVETYPE_STEP) + ent:SetCollisionGroup(COLLISION_GROUP_NONE) + ent_ang = ent:GetAngles() + ent_ang.r = 0 + ent:SetAngles(ent_ang) + end + end + + if bool == nil then + for i,ply in pairs(player.GetAll()) do + if ply.grabbed_ents[ent] then + ply.grabbed_ents[ent] = nil + print(ent , "no longer grabbed by", ply) + end + end + end + + ent:PhysWake() + ent:SetGravity(1) + end + local function maximized_ray_mins_maxs(startpos,endpos,padding) local maxsx,maxsy,maxsz local highest_sq_distance = 0 @@ -66,49 +258,408 @@ if SERVER then end return Vector(padding*maxsx,padding*maxsy,padding*maxsz),Vector(padding*-maxsx,padding*-maxsy,padding*-maxsz) end + + local function AddDamageScale(ply, id,scale, part_uid) + ply.pac_damage_scalings = ply.pac_damage_scalings or {} + ply.pac_damage_scalings[part_uid] = {scale = scale, id = id, uid = part_uid} + end + + local function FixMaxHealths(ply) + local biggest_health = 0 + local biggest_armor = 0 + local found_armor = false + local found_health = false + + if ply.pac_healthmods then + for uid,tbl in pairs(ply.pac_healthmods) do + if tbl.maxhealth then biggest_health = math.max(biggest_health,tbl.maxhealth) found_health = true end + if tbl.maxarmor then biggest_armor = math.max(biggest_armor,tbl.maxarmor) found_armor = true end + end + end + + if found_health then + ply:SetMaxHealth(biggest_health) + else + ply:SetMaxHealth(100) + ply:SetHealth(math.min(ply:Health(),100)) + end + ply.pac_maxhealth = ply:GetMaxHealth() + if found_armor then + ply:SetMaxArmor(biggest_armor) + else + ply:SetMaxArmor(100) + ply:SetArmor(math.min(ply:Armor(),100)) + end + ply.pac_maxhealth = ply:GetMaxArmor() + end + + hook.Add("PlayerSpawn", "PAC_AutoMaxHealth_On_Respawn", function(ply) + FixMaxHealths(ply) + end) + + local function GatherDamageScales(ent) + if not ent then return 0 end + if not ent:IsPlayer() then return 1 end + if not ent.pac_damage_scalings then return 1 end + local cumulative_dmg_scale = 1 + for uid, tbl in pairs(ent.pac_damage_scalings) do + cumulative_dmg_scale = cumulative_dmg_scale * tbl.scale + end + return math.max(cumulative_dmg_scale,healthmod_minimum_dmgscaling:GetFloat()) + end + + --healthbars work with a 2 levels-deep table + --for each player, an index table (priority) to decide which layer is damaged first + --for each layer, one table for each part uid + --for each uid, we have the current uid bar cluster's health value + --instead of keeping track of every bar, it will update the status with a remainder calculation + + local function UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) + local existing_uidlayer = true + local healthvalue = 0 + if not ply.pac_healthbars then + existing_uidlayer = false + ply.pac_healthbars = {} + end + if not ply.pac_healthbars[layer] then + existing_uidlayer = false + ply.pac_healthbars[layer] = {} + end + if not ply.pac_healthbars[layer][part_uid] then + existing_uidlayer = false + ply.pac_healthbars[layer][part_uid] = num*barsize + healthvalue = num*barsize + end + + if (not existing_uidlayer) or follow then + healthvalue = num*barsize + end + + ply.pac_healtbar_uid_absorbfactor = ply.pac_healtbar_uid_absorbfactor or {} + ply.pac_healtbar_uid_absorbfactor[part_uid] = absorbfactor + + if num == 0 then --remove + ply.pac_healthbars[layer] = nil + ply.pac_healtbar_uid_absorbfactor[part_uid] = nil + elseif num > 0 then --add if follow or created + ply.pac_healthbars[layer][part_uid] = healthvalue + ply.pac_healtbar_uid_absorbfactor[part_uid] = absorbfactor + end + for checklayer,tbl in pairs(ply.pac_healthbars) do + for uid,value in pairs(tbl) do + if layer ~= checklayer and part_uid == uid then + ply.pac_healthbars[checklayer][uid] = nil + end + end + end + + end + + local function CalculateHealthBarUIDCombinedHP(ply, uid) + + end + + local function CalculateHealthBarLayerCombinedHP(ply, layer) + + end + + local function GatherExtraHPBars(ply) + if not ply.pac_healthbars then return 0,nil end + local built_tbl = {} + local total_hp_value = 0 + + for layer,tbl in pairs(ply.pac_healthbars) do + built_tbl[layer] = {} + local layer_total = 0 + for uid,value in pairs(tbl) do + built_tbl[layer][uid] = value + total_hp_value = total_hp_value + value + layer_total = layer_total + value + end + end + return total_hp_value,built_tbl + + end + + --simulate on a healthbar layers copy + local function GetPredictedHPBarDamage(ply, dmg) + local BARS_COPY = {} + if ply.pac_healthbars then + BARS_COPY = table.Copy(ply.pac_healthbars) + else --this can happen with non-player ents + return dmg,nil,nil + end + + local remaining_dmg = dmg or 0 + local surviving_layer = 15 + local total_hp_value,built_tbl = GatherExtraHPBars(ply) + local side_effect_dmg = 0 + + if not built_tbl or total_hp_value == 0 then --no shields + return dmg,nil,nil + end + + for layer=15,0,-1 do --go progressively inward in the layers + if BARS_COPY[layer] then + surviving_layer = layer + for uid,value in pairs(BARS_COPY[layer]) do --check the healthbars by uid + + if value > 0 then --skip 0 HP healthbars + + local remainder = math.max(0,remaining_dmg - BARS_COPY[layer][uid]) + + local breakthrough_dmg = math.min(remaining_dmg, value) + + if remaining_dmg > value then --break through one of the uid clusters + surviving_layer = layer - 1 + BARS_COPY[layer][uid] = 0 + else + BARS_COPY[layer][uid] = math.max(0, value - remaining_dmg) + end + + local absorbfactor = ply.pac_healtbar_uid_absorbfactor[uid] + side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor + + remaining_dmg = math.max(0,remaining_dmg - value) + end + + end + end + end + return remaining_dmg,surviving_layer,side_effect_dmg + end + + --do the calculation and reduce the player's underlying values + local function GetHPBarDamage(ply, dmg) + local remaining_dmg = dmg or 0 + local surviving_layer = 15 + local total_hp_value,built_tbl = GatherExtraHPBars(ply) + local side_effect_dmg = 0 + + if not built_tbl or total_hp_value == 0 then --no shields + return dmg,nil,nil + end + + for layer=15,0,-1 do --go progressively inward in the layers + if ply.pac_healthbars[layer] then + surviving_layer = layer + for uid,value in pairs(ply.pac_healthbars[layer]) do --check the healthbars by uid + + if value > 0 then --skip 0 HP healthbars + + local remainder = math.max(0,remaining_dmg - ply.pac_healthbars[layer][uid]) + + local breakthrough_dmg = math.min(remaining_dmg, value) + + if remaining_dmg > value then --break through one of the uid clusters + surviving_layer = layer - 1 + ply.pac_healthbars[layer][uid] = 0 + else + ply.pac_healthbars[layer][uid] = math.max(0, value - remaining_dmg) + end + + local absorbfactor = ply.pac_healtbar_uid_absorbfactor[uid] + side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor + + remaining_dmg = math.max(0,remaining_dmg - value) + end + + end + end + end + return remaining_dmg,surviving_layer,side_effect_dmg + end + + util.AddNetworkString("pac_hitscan") - util.AddNetworkString("pac_request_position_override_on_entity") + util.AddNetworkString("pac_request_position_override_on_entity_teleport") + util.AddNetworkString("pac_request_position_override_on_entity_grab") util.AddNetworkString("pac_request_angle_reset_on_entity") - util.AddNetworkString("pac_request_velocity_force_on_entity") util.AddNetworkString("pac_request_zone_damage") + util.AddNetworkString("pac_hit_results") + util.AddNetworkString("pac_request_force") util.AddNetworkString("pac_signal_player_combat_consent") util.AddNetworkString("pac_signal_stop_lock") util.AddNetworkString("pac_request_lock_break") + util.AddNetworkString("pac_lock_imposecalcview") + util.AddNetworkString("pac_mark_grabbed_ent") + util.AddNetworkString("pac_notify_grabbed_player") util.AddNetworkString("pac_request_player_combat_consent_update") + util.AddNetworkString("pac_request_healthmod") + net.Receive("pac_request_healthmod", function(len,ply) + if not healthmod_allow:GetBool() then return end + local part_uid = net.ReadString() + local mod_id = net.ReadString() + local action = net.ReadString() + + if action == "MaxHealth" then + local num = net.ReadUInt(32) + local follow = net.ReadBool() + if not healthmod_allow_change_maxhp:GetBool() then return end + if ply:Health() == ply:GetMaxHealth() and follow then + ply:SetHealth(num) + elseif num < ply:Health() then + ply:SetHealth(num) + end + ply:SetMaxHealth(num) + ply.pac_healthmods = ply.pac_healthmods or {} + ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} + ply.pac_healthmods[part_uid].maxhealth = num + + elseif action == "MaxArmor" then + local num = net.ReadUInt(32) + local follow = net.ReadBool() + if not healthmod_allow_change_maxhp:GetBool() then return end + if ply:Armor() == ply:GetMaxArmor() and follow then + ply:SetArmor(num) + elseif num < ply:Armor() then + ply:SetArmor(num) + end + ply:SetMaxArmor(num) + ply.pac_healthmods = ply.pac_healthmods or {} + ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} + ply.pac_healthmods[part_uid].maxarmor = num + + elseif action == "DamageMultiplier" then + local scale = net.ReadFloat() + AddDamageScale(ply, mod_id, scale, part_uid) + + elseif action == "HealthBars" then + local num = net.ReadUInt(32) + local barsize = net.ReadUInt(32) + local layer = net.ReadUInt(4) + local absorbfactor = net.ReadFloat() + local follow = net.ReadBool() + + UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) + + elseif action == "OnRemove" then + if ply.pac_damage_scalings then + if ply.pac_damage_scalings[part_uid] then + ply.pac_damage_scalings[part_uid] = nil + end + end + if ply.pac_healthmods then + ply.pac_healthmods[part_uid] = nil + end + + FixMaxHealths(ply) + UpdateHealthBars(ply, 0, 0, 0, 0, part_uid, follow) + end + + end) net.Receive("pac_hitscan", function(len,ply) - print("WE SHOULD DO A BULLET IN THE SERVER!") - ent = net.ReadEntity() - bulletinfo = net.ReadTable() - print("hitscan!", ent) - PrintTable(bulletinfo) - ent:FireBullets(bulletinfo) + + if not hitscan_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + local ent = net.ReadEntity() + if not IsValid(ent) then ent = ply end + local bulletinfo = net.ReadTable() + local dir = net.ReadAngle() + bulletinfo.Dir = dir:Forward() + local part_uid = net.ReadString() + + bulletinfo.Num = math.Clamp(bulletinfo.Num, 1, hitscan_max_bullets:GetInt()) + bulletinfo.Damage = math.Clamp(bulletinfo.Damage, 0, hitscan_max_damage:GetInt()) + bulletinfo.DamageFalloffFraction = math.Clamp(bulletinfo.DamageFalloffFraction,0,1) + if hitscan_spreadout_dmg:GetBool() or bulletinfo.DistributeDamage then + bulletinfo.Damage = bulletinfo.Damage / bulletinfo.Num + end + + bulletinfo.IgnoreEntity = ent + ply.pac_bullet_emitters = ply.pac_bullet_emitters or {} + ply.pac_bullet_emitters[part_uid] = ply.pac_bullet_emitters[part_uid] or ents.Create("pac_bullet_emitter") + + bulletinfo.Callback = function(atk, trc, dmg) + if bulletinfo.DamageFalloff and trc.Hit then + local distance = (trc.HitPos):Distance(trc.StartPos) + local fraction = math.Clamp(1 - (1-bulletinfo.DamageFalloffFraction)*(distance / bulletinfo.DamageFalloffDistance),bulletinfo.DamageFalloffFraction,1) + dmg:SetDamage(fraction * dmg:GetDamage()) + end + end + + if IsValid(ply.pac_bullet_emitters[part_uid]) then + ply.pac_bullet_emitters[part_uid]:FireBullets(bulletinfo) + else + ply.pac_bullet_emitters[part_uid] = ents.Create("pac_bullet_emitter") + end + + end) + + --apply hitscan consents, eat into extra healthbars first and calculate final damage multipliers from pac3 + hook.Add( "EntityTakeDamage", "ApplyPACDamageModifiers", function( target, dmginfo ) + if target:IsPlayer() then + local cumulative_mult = GatherDamageScales(target) + + dmginfo:ScaleDamage(cumulative_mult) + local remaining_dmg,surviving_layer,side_effect_dmg = GetHPBarDamage(target, dmginfo:GetDamage()) + + + if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and not hitscan_consents[target] then + dmginfo:SetDamage(0) + else + local total_hp_value,built_tbl = GatherExtraHPBars(target) + if total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult + + if cumulative_mult < 0 then + target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(dmginfo:GetDamage()),0,target:GetMaxHealth()))) + return true + end + + else --shields = use the calculated cumulative side effect damage from each uid's related absorbfactor + + if side_effect_dmg < 0 then + target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(side_effect_dmg),0,target:GetMaxHealth()))) + return true + else + dmginfo:SetDamage(side_effect_dmg) + end + + end + + end + end end) net.Receive("pac_request_zone_damage", function(len,ply) - --print("message from ",ply) + + --server allow + if not damagezone_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + local pos = net.ReadVector() local ang = net.ReadAngle() local tbl = net.ReadTable() local ply_ent = net.ReadEntity() local dmg_info = DamageInfo() + + --server limits + tbl.Radius = math.Clamp(tbl.Radius,-damagezone_max_radius:GetInt(),damagezone_max_radius:GetInt()) + tbl.Length = math.Clamp(tbl.Length,-damagezone_max_length:GetInt(),damagezone_max_length:GetInt()) + tbl.Damage = math.Clamp(tbl.Damage,-damagezone_max_damage:GetInt(),damagezone_max_damage:GetInt()) + dmg_info:SetDamage(tbl.Damage) dmg_info:IsBulletDamage(tbl.Bullet) dmg_info:SetDamageForce(Vector(0,0,0)) dmg_info:SetAttacker(ply_ent) dmg_info:SetInflictor(ply_ent) - --print("entity: ",ply_ent) - dmg_info:SetDamageType(damage_types[tbl.DamageType]) --print(tbl.DamageType .. " resolves to " .. damage_types[tbl.DamageType]) + local ents_hits + local kill = false + local hit = false + + dmg_info:SetDamageType(damage_types[tbl.DamageType]) local ratio if tbl.Radius == 0 then ratio = tbl.Length else ratio = math.abs(tbl.Length / tbl.Radius) end if tbl.HitboxMode == "Sphere" then - local ents_hits = ents.FindInSphere(pos, tbl.Radius) - ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + ents_hits = ents.FindInSphere(pos, tbl.Radius) + elseif tbl.HitboxMode == "Box" or tbl.HitboxMode == "Cube" then local mins local maxs @@ -120,10 +671,10 @@ if SERVER then maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius) end - local ents_hits = ents.FindInBox(mins, maxs) - ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + ents_hits = ents.FindInBox(mins, maxs) + elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then - local ents_hits = {} + ents_hits = {} if tbl.Radius ~= 0 then local sides = tbl.Detail if tbl.Detail < 1 then sides = 1 end @@ -135,7 +686,6 @@ if SERVER then end steps = math.max(steps + math.abs(tbl.ExtraSteps),1) - --print("steps",steps, "total casts will be "..steps*self.Detail) for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier phase = math.random() local ray_thickness = math.Clamp(0.5*math.log(tbl.Radius) + 0.05*tbl.Radius,0,10)*(1 - 0.7*ringnumber) @@ -161,13 +711,12 @@ if SERVER then counter = counter + 1 end end - --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) end end elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "CylinderSpheres" then - local ents_hits = {} + ents_hits = {} if tbl.Length ~= 0 and tbl.Radius ~= 0 then local counter = 0 MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) @@ -177,11 +726,10 @@ if SERVER then counter = counter + 1 end MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) - --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then - local ents_hits = {} + ents_hits = {} if tbl.Radius ~= 0 then local sides = tbl.Detail if tbl.Detail < 1 then sides = 1 end @@ -193,16 +741,13 @@ if SERVER then steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) end steps = math.max(steps + math.abs(tbl.ExtraSteps),1) - --print("steps",steps, "total casts will be "..steps*self.Detail) local timestart = SysTime() local casts = 0 for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the ringnumber multiplier phase = math.random() local ray_thickness = 5 * (2 - ringnumber) - --print("ring " .. ringnumber .. " phase " .. phase) for i=1,0,-1/sides do - --print("radius " .. tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize)) if ringnumber == 0 then i = 0 end x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) @@ -211,7 +756,6 @@ if SERVER then casts = casts + 1 end end - print(casts .. " casts") if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then --fast sphere check on the wide end local radius_multiplier = math.atan(math.abs(ratio)) / (1.5 + 0.1*math.sqrt(ratio)) @@ -220,28 +764,23 @@ if SERVER then end end elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode == "ConeSpheres" then - local ents_hits = {} + ents_hits = {} local steps steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) for i = 1,0,-1/steps do - --PrintTable(ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) end steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) - for i = 0,1/8,1/128 do - --PrintTable(ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) - --MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) - end + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + elseif tbl.HitboxMode =="Ray" then local startpos = pos + Vector(0,0,0) local endpos = pos + ang:Forward()*tbl.Length ents_hits = ents.FindAlongRay(startpos, endpos) - ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) if tbl.Bullet then local bullet = {} @@ -253,68 +792,246 @@ if SERVER then dmg_info:GetInflictor():FireBullets(bullet) end end - + hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + highest_dmg = highest_dmg or 0 + net.Start("pac_hit_results") + net.WriteBool(hit) + net.WriteBool(kill) + net.WriteFloat(highest_dmg) + net.WriteTable(successful_hit_ents) + net.WriteTable(successful_kill_ents) + net.Broadcast() end) function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) - print("received", #ents_hits, "potential targets") - --print("process hurt ", #ents_hits) - --PrintTable(ents_hits) + + local pac_sv_damage_zone_allow_dissolve = GetConVar("pac_sv_damage_zone_allow_dissolve"):GetBool() + local pac_sv_prop_protection = global_combat_prop_protection:GetBool() + + local inflictor = dmg_info:GetInflictor() + local attacker = dmg_info:GetAttacker() + + local kill = false --whether a kill was done + local hit = false --whether a hit was done + local max_dmg = 0 --the max damage applied to targets. it should give the same damage by default, but I'm accounting for targets that can modify their damage + local successful_hit_ents = {} + local successful_kill_ents = {} + local bullet = {} bullet.Src = pos + ang:Forward() bullet.Dir = ang:Forward()*50000 bullet.Damage = -1 bullet.Force = 0 bullet.Entity = dmg_info:GetAttacker() - if #ents_hits == 0 then - if tbl.Bullet then - dmg_info:GetInflictor():FireBullets(bullet) + + --the function to determine if we can dissolve, based on policy and setting factors + local function IsDissolvable(ent) + local dissolvable = true + local prop_protected, reason = IsPropProtected(ent, attacker) + local prop_protected_final = prop_protected and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false + + if ent:IsPlayer() then + if not kill then dissolvable = false + elseif damage_zone_consents[ent] == false then dissolvable = false end + elseif inflictor == ent then + dissolvable = false --do we allow that? + end + if ent:IsWeapon() and IsValid(ent:GetCreator()) then + dissolvable = false + end + if ent:CreatedByMap() then + dissolvable = false + if ent:GetClass() == "prop_physics" then dissolvable = true end + end + if damageable_point_ent_classes[ent:GetClass()] == false then + dissolvable = false end - return + if prop_protected_final then + dissolvable = false + end + return dissolvable end - - for _,ent in pairs(ents_hits) do - if IsEntity(ent) then - if (not tbl.AffectSelf) and ent == dmg_info:GetInflictor() then --nothing - elseif (ent:IsPlayer() and tbl.Players) or (ent:IsNPC() and tbl.NPC) or (string.find(ent:GetClass(), "npc") ~= nil) or (string.find(ent:GetClass(), "drg_") ~= nil) or (ent:GetClass() == "prop_physics") then - --local oldvel = ent:GetVelocity() - local ents2 = {dmg_info:GetInflictor()} - if tbl.Bullet then - for _,v in ipairs(ents_hits) do - if v ~= ent then table.insert(ents2,v) end - end + + local dissolver_entity = NULL + local function dissolve(target, attacker, typ) + local dissolver_ent = ents.Create("env_entity_dissolver") + dissolver_ent:Spawn() + target:SetName(tostring({})) + dissolver_ent:SetKeyValue("dissolvetype", tostring(typ)) + dissolver_ent:Fire("Dissolve", target:GetName()) + timer.Simple(5, function() SafeRemoveEntity(dissolver_ent) end) + dissolver_entity = dissolver_ent + end + + --the giga function to determine if we can damage + local function DMGAllowed(ent) + + if ent:Health() == 0 then return false end --immediately exclude entities with 0 health + local canhit = false --whether the policies allow the hit + local prop_protected_consent = ent:GetCreator() ~= inflictor and ent ~= inflictor and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false-- and ent:GetCreator() ~= inflictor + local contraption = IsPossibleContraptionEntity(ent) + + --first pass: entity class blacklist + if IsEntity(ent) and ((damageable_point_ent_classes[ent:GetClass()] ~= false) or ((damageable_point_ent_classes[ent:GetClass()] == nil) or (damageable_point_ent_classes[ent:GetClass()] == true))) then + --second pass: the damagezone's settings + --1.don't hurt yourself + if (not tbl.AffectSelf) and ent == inflictor then --nothing + --2.main target types : players, NPC, point entities + elseif ((ent:IsPlayer() and tbl.Players) or (tbl.NPC and (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity)) or tbl.PointEntities) + and --one of the base classes + (damageable_point_ent_classes[ent:GetClass()] ~= false) --non-blacklisted class + and --enforce prop protection + (ent:GetCreator() == inflictor or ent == inflictor or (ent:GetCreator() ~= inflictor and pac_sv_prop_protection and damage_zone_consents[ent:GetCreator()] == true) or not pac_sv_prop_protection) + then + canhit = true + + if ent:IsPlayer() and tbl.Players then + --rules for players: + --self can always hurt itself if asked to + if (ent == inflictor and tbl.AffectSelf) then canhit = true + --self shouldn't hurt itself if asked not to + elseif (ent == inflictor and not tbl.AffectSelf) then canhit = false + --other players need to consent, bots don't care about it + elseif damage_zone_consents[ent] == true or ent:IsBot() then canhit = true + --other players that didn't consent are excluded + else canhit = false end + + elseif (tbl.NPC and damageable_point_ent_classes[ent:GetClass()] ~= false) or (tbl.PointEntities and (damageable_point_ent_classes[ent:GetClass()] == true)) then + canhit = true + end + + --apply prop protection + if IsPropProtected(ent, inflictor) or prop_protected_consent then + canhit = false end + + end + + end - if tbl.DamageType == "heal" then - ent:SetHealth(math.min(ent:Health() + tbl.Damage, ent:GetMaxHealth())) - elseif tbl.DamageType == "armor" then - ent:SetArmor(math.min(ent:Armor() + tbl.Damage, ent:GetMaxArmor())) - else - if tbl.Bullet then - traceresult = util.TraceLine({filter = ents2, start = pos, endpos = pos + 50000*(ent:WorldSpaceCenter() - dmg_info:GetAttacker():WorldSpaceCenter())}) - --print(traceresult.Fraction) - bullet.Dir = traceresult.Normal - bullet.Src = traceresult.HitPos + traceresult.HitNormal*5 - dmg_info:GetInflictor():FireBullets(bullet) - end - if ent:IsPlayer() then - if (ent == dmg_info:GetInflictor() and tbl.AffectSelf) then - ent:TakeDamageInfo(dmg_info) - print(ent, "hurt themself") - elseif damage_zone_consents[ent] == true or ent:IsBot() then - ent:TakeDamageInfo(dmg_info) - print(dmg_info:GetAttacker(), "hurt", ent) - else print("can't do that because",ent,damage_zone_consents[ent]) end + return canhit + end + + local function IsLiving(ent) --players and NPCs + return ent:IsPlayer() or (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) + end + + --final action to apply the DamageInfo + local function DoDamage(ent) + --we'll need to find out whether the damage will crack open a player's extra bars + local de_facto_dmg = GetPredictedHPBarDamage(ent, tbl.Damage) + + local distance = (ent:GetPos()):Distance(pos) + + local fraction = math.pow(math.Clamp(1 - distance / math.Clamp(math.max(tbl.Radius, tbl.Length),1,50000),0,1),tbl.DamageFalloffPower) + + if tbl.DamageFalloff then + dmg_info:SetDamage(fraction * tbl.Damage) + end + + successful_hit_ents[ent] = true + --fire bullets if asked + local ents2 = {inflictor} + if tbl.Bullet then + for _,v in ipairs(ents_hits) do + if v ~= ent then table.insert(ents2,v) end + end + + traceresult = util.TraceLine({filter = ents2, start = pos, endpos = pos + 50000*(ent:WorldSpaceCenter() - dmg_info:GetAttacker():WorldSpaceCenter())}) + + bullet.Dir = traceresult.Normal + bullet.Src = traceresult.HitPos + traceresult.HitNormal*5 + dmg_info:GetInflictor():FireBullets(bullet) + + end + + if tbl.DamageType == "heal" then + ent:SetHealth(math.min(ent:Health() + tbl.Damage, ent:GetMaxHealth())) + elseif tbl.DamageType == "armor" then + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, ent:GetMaxArmor())) + else + --only "living" entities can be killed, and we checked generic entities with a ghost 0 health previously + + --now, after checking the de facto damage after extra healthbars, there's a 80% absorbtion ratio of armor. + --so, the kill condition is either: + --if damage is 500% of health (no amount will save you, because the remainder of 80% means death) + --if damage is more than 125% of armor, and damage is more than health+armor + if IsLiving(ent) and ent:Health() - de_facto_dmg <= 0 then + if ent.Armor then + + if not (de_facto_dmg > 5*ent:Health()) and not (de_facto_dmg > 1.25*ent:Armor() and de_facto_dmg > ent:Health() + ent:Armor()) then + kill = false else - ent:TakeDamageInfo(dmg_info) - print(dmg_info:GetAttacker(), "hurt", ent) + kill = true + end + else + kill = true + end + + end + if tbl.DoNotKill then + kill = false --durr + end + if kill then successful_kill_ents[ent] = true end + + --remove weapons on kill if asked + if kill and not ent:IsPlayer() and tbl.RemoveNPCWeaponsOnKill and pac_sv_damage_zone_allow_dissolve then + if ent:IsNPC() then + if #ent:GetWeapons() >= 1 then + for _,wep in pairs(ent:GetWeapons()) do + SafeRemoveEntity(wep) + end end end - --local newvel = ent:GetVelocity() - --ent:SetVelocity( oldvel - newvel) end + + --leave at a critical health + if tbl.DoNotKill then + local dmg_info2 = DamageInfo() + + dmg_info2:SetDamage( math.min(ent:Health() - tbl.CriticalHealth, tbl.Damage)) + dmg_info2:IsBulletDamage(tbl.Bullet) + dmg_info2:SetDamageForce(Vector(0,0,0)) + + dmg_info2:SetAttacker(attacker) + + dmg_info2:SetInflictor(inflictor) + + ent:TakeDamageInfo(dmg_info2) + max_dmg = math.max(max_dmg, dmg_info2:GetDamage()) + + --finally we reached the normal damage event! + else + if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) and pac_sv_damage_zone_allow_dissolve then + dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType]) + end + ent:TakeDamageInfo(dmg_info) + max_dmg = math.max(max_dmg, dmg_info:GetDamage()) + end + end + + if tbl.DamageType == "fire" then ent:Ignite(5) end + end + + --the forward bullet, if applicable and no entity is found + if #ents_hits == 0 then + if tbl.Bullet then + dmg_info:GetInflictor():FireBullets(bullet) end + return hit,kill,dmg,successful_hit_ents,successful_kill_ents end + + --look through each entity + for _,ent in pairs(ents_hits) do + local canhit = DMGAllowed(ent) + local oldhp = ent:Health() + if canhit then + DoDamage(ent) + end + if not hit and (oldhp > 0 and canhit) then hit = true end + end + + return hit,kill,dmg,successful_hit_ents,successful_kill_ents end function MergeTargetsByID(tbl1, tbl2) @@ -323,81 +1040,502 @@ if SERVER then end end - net.Receive("pac_request_position_override_on_entity", function(len, ply) + --The lock part grab request net message + net.Receive("pac_request_position_override_on_entity_grab", function(len, ply) + --server allow + if not lock_allow:GetBool() then return end + if not lock_allow_grab:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + local did_grab = true + local need_breakup = false + local breakup_condition = "" + --monstrous net message + local is_first_time = net.ReadBool() + local lockpart_UID = net.ReadString() local pos = net.ReadVector() local ang = net.ReadAngle() local override_ang = net.ReadBool() + local override_eyeang = net.ReadBool() local targ_ent = net.ReadEntity() local auth_ent = net.ReadEntity() - if not grab_pairs[auth_ent] then - grab_pairs[auth_ent] = targ_ent - pairs_grabbed_by[targ_ent] = auth_ent - print(auth_ent, "grabs", targ_ent) - end + local override_viewposition = net.ReadBool() + local alt_pos = net.ReadVector() + local alt_ang = net.ReadAngle() + local ask_drawviewer = net.ReadBool() - if targ_ent:EntIndex() == 0 then return end - if targ_ent ~= auth_ent and grab_consents[targ_ent] == false then return end + local prop_protected, reason = IsPropProtected(targ_ent, ply) - if override_ang and not targ_ent:IsPlayer() then - targ_ent:SetAngles(ang) - elseif override_ang and (auth_ent == targ_ent) and targ_ent:IsPlayer() then - targ_ent:SetEyeAngles(ang) + local unconsenting_owner = targ_ent:GetCreator() ~= ply and (grab_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) + local calcview_unconsenting = targ_ent:GetCreator() ~= ply and (calcview_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and calcview_consents[targ_ent] == false)) + + if unconsenting_owner or (global_combat_prop_protection:GetBool() and prop_protected) then return end + + local targ_ent_owner = targ_ent:GetCreator() or targ_ent + local auth_ent_owner = ply + + auth_ent_owner.grabbed_ents = auth_ent_owner.grabbed_ents or {} + + local consent_break_condition = false + + if grab_consents[targ_ent_owner] == false then --if the target player is non-consenting + if targ_ent_owner == auth_ent_owner then consent_break_condition = false --player can still grab his owned entities + elseif targ_ent:IsPlayer() then --if not the same player, we cannot grab + consent_break_condition = true + breakup_condition = breakup_condition .. "cannot grab another player if they don't consent to grabs, " + elseif global_combat_prop_protection:GetBool() and targ_ent:GetCreator() ~= ply then + --if entity not owned by grabbing player, he cannot do it to other players' entities in the prop-protected mode + consent_break_condition = true + breakup_condition = breakup_condition .. "cannot grab another player's owned entities if they don't consent to grabs, " + end + end + + if not IsValid(targ_ent) then --invalid entity? + did_grab = false + return --nothing else matters, get out + end + if consent_break_condition then --any of the non-consenting conditions + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "non-consenting, " end - targ_ent:SetPos(pos) - if targ_ent:IsPlayer() then targ_ent:SetVelocity(-targ_ent:GetVelocity()) end + --dead ent = break + --but don't exclude about physics props + if targ_ent:Health() == 0 and not (physics_point_ent_classes[targ_ent:GetClass()] or string.find(targ_ent:GetClass(),"item_") or string.find(targ_ent:GetClass(),"ammo_") or targ_ent:IsWeapon()) then + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "dead, " + end + + if is_first_time then + if (auth_ent_owner ~= targ_ent and auth_ent_owner.grabbed_ents[targ_ent] == true) then + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "mutual grab prevention, " + end + end + + if did_grab then + + + if targ_ent:IsPlayer() and targ_ent:InVehicle() then --yank player out of vehicle + print("Kicking " .. targ_ent:Nick() .. " out of vehicle to be grabbed!") + targ_ent:ExitVehicle() + end + + if override_ang then + if not targ_ent:IsPlayer() then --non-players work with angles + targ_ent:SetAngles(ang) + else --players work with eyeangles + if override_eyeang then + + if PlayerAllowsCalcView(targ_ent) and override_viewposition then + targ_ent.nextcalcviewTick = targ_ent.nextcalcviewTick or CurTime() + if targ_ent.nextcalcviewTick < CurTime() then + net.Start("pac_lock_imposecalcview") + net.WriteBool(true) + net.WriteVector(alt_pos) + net.WriteAngle(alt_ang) + net.WriteBool(ask_drawviewer) + net.Send(targ_ent) + targ_ent.nextcalcviewTick = CurTime() + 0.1 + targ_ent.has_calcview = true + else print("skipping") end + targ_ent:SetEyeAngles(alt_ang) + targ_ent:SetAngles(alt_ang) + else + targ_ent:SetEyeAngles(ang) + targ_ent:SetAngles(ang) + end + elseif not override_eyeang or not override_viewposition or not PlayerAllowsCalcView(targ_ent) then --break any calcviews if we can't do that + if targ_ent.has_calcview then + net.Start("pac_lock_imposecalcview") + net.WriteBool(false) + net.WriteVector(Vector(0,0,0)) + net.WriteAngle(Angle(0,0,0)) + net.Send(targ_ent) + targ_ent.has_calcview = false + end + end + + end + end + + targ_ent:SetPos(pos) + + ApplyLockState(targ_ent, true) + if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end - if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end + --@@note lock assignation! IMPORTANT + if is_first_time then --successful, first + auth_ent_owner.grabbed_ents[targ_ent] = true + targ_ent.grabbed_by = auth_ent_owner + targ_ent.grabbed_by_uid = lockpart_UID + print(auth_ent, "grabbed", targ_ent, "owner grabber is", auth_ent_owner) + end + targ_ent.grabbed_by_time = CurTime() + else + auth_ent_owner.grabbed_ents[targ_ent] = nil + targ_ent.grabbed_by_uid = nil + targ_ent.grabbed_by = nil + end + + if need_breakup then + print("stop this now! reason: " .. breakup_condition) + net.Start("pac_request_lock_break") + net.WriteEntity(targ_ent) + net.WriteString(lockpart_UID) + net.WriteString(breakup_condition) + net.Send(auth_ent_owner) + + else + if is_first_time and did_grab then + net.Start("pac_mark_grabbed_ent") + net.WriteEntity(targ_ent) + net.WriteBool(did_grab) + net.WriteString(lockpart_UID) + net.Broadcast() + + if targ_ent:IsPlayer() then + net.Start("pac_notify_grabbed_player") + net.WriteEntity(ply) + net.Send(targ_ent) + end + end + end end) + --the lockpart teleport request net message + net.Receive("pac_request_position_override_on_entity_teleport", function(len, ply) + --server allow + if not lock_allow:GetBool() then return end + if not lock_allow_teleport:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + local lockpart_UID = net.ReadString() + local pos = net.ReadVector() + local ang = net.ReadAngle() + local override_ang = net.ReadBool() + + if IsValid(ply) then + if override_ang then + ply:SetEyeAngles(ang) + end + ply:SetPos(pos) + end + end) + --the lockpart grab end request net message net.Receive("pac_request_angle_reset_on_entity", function(len, ply) + + if not PlayerIsCombatAllowed(ply) then return end + local ang = net.ReadAngle() local delay = net.ReadFloat() local targ_ent = net.ReadEntity() local auth_ent = net.ReadEntity() targ_ent:SetAngles(ang) - targ_ent:PhysWake() - table.RemoveByValue(grab_pairs,auth_ent) + ApplyLockState(targ_ent, false) + end) - net.Receive("pac_request_velocity_force_on_entity", function(len,ply) + --first stage of force: look for targets and determine force amount if continuous + function ImpulseForce(tbl, pos, ang, ply) + if tbl.Continuous then + tbl.BaseForce = tbl.BaseForce * FrameTime() * 3.3333 --weird value to equalize how 600 cancels out gravity + tbl.AddedVectorForce = tbl.AddedVectorForce * FrameTime() * 3.3333 + tbl.Torque = tbl.Torque * FrameTime() * 3.3333 + end + if tbl.HitboxMode == "Sphere" then + local ents_hits = ents.FindInSphere(pos, tbl.Radius) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Box" then + local mins + local maxs + if tbl.HitboxMode == "Box" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) + end + + local ents_hits = ents.FindInBox(mins, maxs) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cylinder" then + local ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cone" then + local ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + + if tbl.Bullet then + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + dmg_info:GetInflictor():FireBullets(bullet) + end + end + end + --second stage of force: apply + function ProcessForcesList(ents_hits, tbl, pos, ang, ply) + + for _,ent in pairs(ents_hits) do + + local phys_ent + if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) + and ( + ent:IsPlayer() + or (string.find(ent:GetClass(), "npc") ~= nil) + or ent:IsNPC() + or physics_point_ent_classes[ent:GetClass()] + or string.find(ent:GetClass(), "item_") + ) then + + local is_phys = true + if ent:GetPhysicsObject() ~= nil then + phys_ent = ent:GetPhysicsObject() + if (string.find(ent:GetClass(), "npc") ~= nil) then + phys_ent = ent + end + else + phys_ent = ent + is_phys = false + end + + local oldvel = phys_ent:GetVelocity() + + local addvel = Vector(0,0,0) + local add_angvel = Vector(0,0,0) + + local ent_center = ent:WorldSpaceCenter() or ent:GetPos() + local force_center = tbl.Locus_pos or pos + + local dir = ent_center - force_center + + local dist_multiplier = 1 + local distance = (ent_center - force_center):Length() + + if tbl.BaseForceAngleMode == "Radial" then --radial on self + addvel = dir:GetNormalized() * tbl.BaseForce + elseif tbl.BaseForceAngleMode == "Locus" then --radial on locus + addvel = dir:GetNormalized() * tbl.BaseForce + elseif tbl.BaseForceAngleMode == "Local" then --forward on self + addvel = ang:Forward() * tbl.BaseForce + end + + if tbl.VectorForceAngleMode == "Global" then --global + addvel = addvel + tbl.AddedVectorForce + elseif tbl.VectorForceAngleMode == "Local" then --local on self + addvel = addvel + +ang:Forward()*tbl.AddedVectorForce.x + +ang:Right()*tbl.AddedVectorForce.y + +ang:Up()*tbl.AddedVectorForce.z + + elseif tbl.VectorForceAngleMode == "Radial" then --relative to locus or self + ang2 = dir:Angle() + addvel = addvel + +ang2:Forward()*tbl.AddedVectorForce.x + +ang2:Right()*tbl.AddedVectorForce.y + +ang2:Up()*tbl.AddedVectorForce.z + elseif tbl.VectorForceAngleMode == "RadialNoPitch" then --relative to locus or self + dir.z = 0 + ang2 = dir:Angle() + addvel = addvel + +ang2:Forward()*tbl.AddedVectorForce.x + +ang2:Right()*tbl.AddedVectorForce.y + +ang2:Up()*tbl.AddedVectorForce.z + end + + + + if tbl.TorqueMode == "Global" then + add_angvel = tbl.Torque + elseif tbl.TorqueMode == "Local" then + add_angvel = ang:Forward()*tbl.Torque.x + ang:Right()*tbl.Torque.y + ang:Up()*tbl.Torque.z + elseif tbl.TorqueMode == "TargetLocal" then + add_angvel = tbl.Torque + elseif tbl.TorqueMode == "Radial" then + ang2 = dir:Angle() + addvel = ang2:Forward()*tbl.Torque.x + ang2:Right()*tbl.Torque.y + ang2:Up()*tbl.Torque.z + end + + local islocaltorque = tbl.TorqueMode == "TargetLocal" + + if is_phys and tbl.AccountMass then + if not (string.find(ent:GetClass(), "npc") ~= nil) then + addvel = addvel * (1 / math.max(phys_ent:GetMass(),0.1)) + else + addvel = addvel + end + add_angvel = add_angvel * (1 / math.max(phys_ent:GetMass(),0.1)) + end + + if tbl.Falloff then + dist_multiplier = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + + addvel = addvel * dist_multiplier + add_angvel = add_angvel * dist_multiplier + + local unconsenting_owner = ent:GetCreator() ~= ply and force_consents[ent:GetCreator()] == false + + if (ent:IsPlayer() and tbl.Players) or (ent == ply and tbl.AffectSelf) then + if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then + phys_ent:SetVelocity(addvel) + ent:SetVelocity(addvel) + end + + elseif (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) and tbl.PhysicsProps then + if not IsPropProtected(ent, ply) and not (global_combat_prop_protection:GetBool() and unconsenting_owner) then + ent:PhysWake() + if islocaltorque then + phys_ent:AddAngleVelocity(add_angvel) + else + add_angvel = phys_ent:WorldToLocalVector( add_angvel ) + phys_ent:ApplyTorqueCenter(add_angvel) + end + ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces + phys_ent:SetVelocity(oldvel + addvel) + end + elseif (ent:IsNPC() or string.find(ent:GetClass(), "npc") ~= nil) and tbl.NPC then + if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then + if phys_ent:GetVelocity():Length() > 500 then + local vec = oldvel + addvel + local clamp_vec = vec:GetNormalized()*500 + ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer + + else ent:SetVelocity(oldvel + addvel) end + end + else + if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then + phys_ent:SetVelocity(oldvel + addvel) + end + end + hook.Run("PhysicsUpdate", ent) + hook.Run("PhysicsUpdate", phys_ent) + end + + end + end + --the force part impulse request net message + net.Receive("pac_request_force", function(len,ply) + + --server allow + if not force_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + local tbl = net.ReadTable() + local pos = net.ReadVector() + local ang = net.ReadAngle() + local on = net.ReadBool() + + --server limits + tbl.Radius = math.Clamp(tbl.Radius,-force_max_radius:GetInt(),force_max_radius:GetInt()) + tbl.Length = math.Clamp(tbl.Length,-force_max_length:GetInt(),force_max_length:GetInt()) + tbl.BaseForce = math.Clamp(tbl.BaseForce,-force_max_amount:GetInt(),force_max_amount:GetInt()) + + + if on then + if tbl.Continuous then + hook.Add("Tick", "pac_force_hold"..tbl.UniqueID, function() + ImpulseForce(tbl, pos, ang, ply) + end) + + active_force_ids[tbl.UniqueID] = CurTime() + else + ImpulseForce(tbl, pos, ang, ply) + active_force_ids[tbl.UniqueID] = nil + end + else + hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) + active_force_ids[tbl.UniqueID] = nil + end + + --check bad or inactive hooks + for i,v in pairs(active_force_ids) do + if not v then + hook.Remove("Tick", "pac_force_hold"..i) + --print("invalid force") + elseif v + 0.1 < CurTime() then + hook.Remove("Tick", "pac_force_hold"..i) + --print("outdated force") + end + end + end) + + --consent message from clients net.Receive("pac_signal_player_combat_consent", function(len,ply) mode = net.ReadString() b = net.ReadBool() - --print("message from", ply, "consent for",mode,"is",b) if mode == "grab" then grab_consents[ply] = b - --PrintTable(grab_consents) elseif mode == "damage_zone" then damage_zone_consents[ply] = b - --PrintTable(grab_consents) + elseif mode == "calcview" then + calcview_consents[ply] = b + elseif mode == "force" then + force_consents[ply] = b + elseif mode == "hitscan" then + hitscan_consents[ply] = b elseif mode == "all" then grab_consents[ply] = b damage_zone_consents[ply] = b + calcview_consents[ply] = b + force_consents[ply] = b + hitscan_consents[ply] = b end end) - + --lock break order from client net.Receive("pac_signal_stop_lock", function(len,ply) - print("Requesting lock break") - PrintTable(grab_pairs) - for p,targ in pairs(grab_pairs) do - if grab_pairs[p] == targ and ply == targ then - print("from",p) - if p:IsPlayer() then - net.Start("pac_request_lock_break") - net.WriteEntity(ply) - net.Send(p) - elseif targ:IsPlayer() then - net.Start("pac_request_lock_break") - net.WriteEntity(ply) - net.Send(targ) + ApplyLockState(ply, false) + MsgC(Color(0,255,255), "Requesting lock break!\n") + if ply.grabbed_by then --directly go for the grabbed_by player + net.Start("pac_request_lock_break") + net.WriteEntity(ply) + net.WriteString("") + net.Send(ply.grabbed_by) + end + --What if there's more? try to find it AMONG US SUS! + for _,ent in pairs(player.GetAll()) do + if ent.grabbed_ents and ent ~= ply.grabbed_by then --a player! time to inspect! but skip the already found grabber + for _,grabbed in pairs(ent.grabbed_ents) do --check all her entities + if ply == grabbed then --that's us! + net.Start("pac_request_lock_break") + net.WriteEntity(ply) + net.WriteString(ply.grabbed_by_uid) + net.Send(ent) + end end - end end end) @@ -406,22 +1544,58 @@ if SERVER then pac_combat_RefreshConsents() end) + concommand.Add("pac_damage_zone_whitelist_entity_class", function(ply, cmd, args, argStr) + for _,v in pairs(string.Explode(";",argStr)) do + damageable_point_ent_classes[v] = true + print("added " .. v .. " to the entities you can damage") + end + PrintTable(damageable_point_ent_classes) + end) + + concommand.Add("pac_damage_zone_blacklist_entity_class", function(ply, cmd, args, argStr) + for _,v in pairs(string.Explode(";",argStr)) do + damageable_point_ent_classes[v] = false + print("removed " .. v .. " from the entities you can damage") + end + PrintTable(damageable_point_ent_classes) + end) + function pac_combat_RefreshConsents() for _,ent in pairs(ents.GetAll()) do if ent:IsPlayer() then net.Start("pac_request_player_combat_consent_update") net.Send(ent) - --print(ent, "does that player consent grabs?", grab_consents[ent], "and damage zone?", damage_zone_consents[ent]) end end end timer.Create("pac_timer_refresh_client_consents", 10, 0, function() pac_combat_RefreshConsents() end) + + hook.Add("Tick", "pac_checklocks", function() + --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check + for _,ent in pairs(ents.GetAll()) do + if ent.grabbed_by then + if ent.grabbed_by_time + 0.5 < CurTime() then --restore the movetype + local grabber = ent.grabbed_by + ent.grabbed_by_uid = nil + ent.grabbed_by = nil + grabber.grabbed_ents[ent] = false + ApplyLockState(ent, false) + + end + end + end + end) end if CLIENT then - CreateConVar("pac_client_grab_consent", "0", FCVAR_ARCHIVE, true) - CreateConVar("pac_client_damage_zone_consent", "0", FCVAR_ARCHIVE, true) + CreateConVar("pac_client_grab_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part") + CreateConVar("pac_client_lock_camera_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to having lock parts override your view") + CreateConVar("pac_client_damage_zone_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") + CreateConVar("pac_client_force_consent", "1", FCVAR_ARCHIVE, "Whether you want to consent to pac3 physics forces") + CreateConVar("pac_client_hitscan_consent", "1", FCVAR_ARCHIVE, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") + + CreateConVar("pac_break_lock_verbosity", "3", FCVAR_ARCHIVE, "How much info you want for the PAC3 lock notifications\n3:full information\n2:grabbing player + basic reminder of the lock break command\n1:grabbing player\n0:suppress the notifications") concommand.Add( "pac_stop_lock", function() net.Start("pac_signal_stop_lock") @@ -432,9 +1606,51 @@ if CLIENT then net.Start("pac_signal_stop_lock") net.SendToServer() end, "asks the server to breakup any lockpart hold on your player") + + net.Receive("pac_lock_imposecalcview", function() + local authority_to_calcview = net.ReadBool() and GetConVar("pac_client_lock_camera_consent"):GetBool() + + local alt_pos = net.ReadVector() + local alt_ang = net.ReadAngle() + local ask_drawviewer = net.ReadBool() + + if authority_to_calcview then + LocalPlayer().last_calcview = CurTime() + LocalPlayer().has_calcview = true + hook.Add("CalcView", "PAC_lockpart_calcview", function(ply, pos, angles, fov) + if LocalPlayer().last_calcview + 0.5 < CurTime() then + hook.Remove("CalcView", "PAC_lockpart_calcview") + LocalPlayer().has_calcview = false + return nil + + end + local view = { + origin = alt_pos, + angles = alt_ang, + fov = fov, + drawviewer = ask_drawviewer + } + return view + end) + hook.Add("Tick", "pac_checkcalcview", function() + if LocalPlayer().has_calcview and LocalPlayer().last_calcview + 0.5 < CurTime() then + hook.Remove("CalcView", "PAC_lockpart_calcview") + LocalPlayer().has_calcview = false + --print("killed a calcview due to expiry") + end + if LocalPlayer().last_calcview + 0.5 < CurTime() then + hook.Remove("CalcView", "PAC_lockpart_calcview") + LocalPlayer().has_calcview = false + --print("killed a calcview again due to expiry") + end + end) + else --if LocalPlayer().has_calcview then + hook.Remove("CalcView", "PAC_lockpart_calcview") + --print("killed a calcview due to lack of authority") + end + end) net.Receive("pac_request_player_combat_consent_update", function() - --print("player receives request to update consents") net.Start("pac_signal_player_combat_consent") net.WriteString("grab") net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) @@ -444,6 +1660,40 @@ if CLIENT then net.WriteString("damage_zone") net.WriteBool(GetConVar("pac_client_damage_zone_consent"):GetBool()) net.SendToServer() + + net.Start("pac_signal_player_combat_consent") + net.WriteString("calcview") + net.WriteBool(GetConVar("pac_client_lock_camera_consent"):GetBool()) + net.SendToServer() + + net.Start("pac_signal_player_combat_consent") + net.WriteString("force") + net.WriteBool(GetConVar("pac_client_force_consent"):GetBool()) + net.SendToServer() + + net.Start("pac_signal_player_combat_consent") + net.WriteString("hitscan") + net.WriteBool(GetConVar("pac_client_hitscan_consent"):GetBool()) + net.SendToServer() + end) + + net.Receive("pac_notify_grabbed_player", function() + local grabber = net.ReadEntity() + local verbosity = GetConVar("pac_break_lock_verbosity"):GetInt() + local str + if verbosity == 3 then + str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "! You can break free with pac_break_lock or pac_stop_lock. You can suppress these messages with pac_break_lock_verbosity 0" + notification.AddLegacy( str, NOTIFY_HINT, 10 ) + elseif verbosity == 2 then + str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "! pac_break_lock to break free" + notification.AddLegacy( str, NOTIFY_HINT, 7 ) + elseif verbosity == 1 then + str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "!" + notification.AddLegacy( str, NOTIFY_HINT, 7 ) + end + + pac.Message("You've been grabbed by " .. grabber:Nick() .. "!") + end) end diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index e87752bbf..c2f990acc 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -1,6 +1,10 @@ -local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}) -local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}) -local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}) +local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow physical projectiles serverside') +local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum attract radius for physical projectiles') +local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum damage radius for physical projectiles') +local pac_sv_projectile_max_phys_radius = CreateConVar("pac_sv_projectile_max_phys_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum physical radius for physical projectiles') +local pac_sv_projectile_max_speed = CreateConVar("pac_sv_projectile_max_speed", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum speed for physical projectiles') +local pac_sv_projectile_max_damage = CreateConVar("pac_sv_projectile_max_damage", 100000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum damage for physical projectiles') +local pac_sv_projectile_max_mass = CreateConVar("pac_sv_projectile_max_mass", 50000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum speed for physical projectiles') do -- projectile entity local ENT = {} @@ -60,17 +64,49 @@ do -- projectile entity self.projectile_owner = ply - local radius = math.Clamp(part.Radius, 1, pac_sv_projectile_max_damage_radius:GetFloat()) + local radius = math.Clamp(part.Radius, 1, pac_sv_projectile_max_phys_radius:GetFloat()) if part.Sphere then - self:PhysicsInitSphere(radius) + self:PhysicsInitSphere(radius, part.SurfaceProperties) else - self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius) + local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel) + print("valid fallback? " .. part.FallbackSurfpropModel , valid_fallback) + self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties) + + if part.OverridePhysMesh and valid_fallback then + self:SetModel(part.FallbackSurfpropModel) + self:PhysicsInit(SOLID_VPHYSICS) + self:PhysicsInitMultiConvex(self:GetPhysicsObject():GetMeshConvexes(), part.SurfaceProperties) + end + + if part.RescalePhysMesh then + local physmesh = self:GetPhysicsObject():GetMeshConvexes() + --hack from prop resizer + for convexkey, convex in pairs( physmesh ) do + for poskey, postab in pairs( convex ) do + convex[ poskey ] = postab.pos * radius + end + end + + self:PhysicsInitMultiConvex( physmesh, part.SurfaceProperties) + self:EnableCustomCollisions( true ) + elseif not valid_fallback then + self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties) + end + end + local phys = self:GetPhysicsObject() + phys:SetMaterial(part.SurfaceProperties) + + phys:EnableGravity(part.Gravity) phys:AddVelocity((ang:Forward() + (VectorRand():Angle():Forward() * part.Spread)) * part.Speed * 1000) + phys:AddAngleVelocity(Vector(part.RandomAngleVelocity.x * math.Rand(-1,1), part.RandomAngleVelocity.y * math.Rand(-1,1), part.RandomAngleVelocity.z * math.Rand(-1,1))) + + phys:AddAngleVelocity(part.LocalAngleVelocity) + if part.AddOwnerSpeed then phys:AddVelocity(ply:GetVelocity()) end @@ -84,12 +120,14 @@ do -- projectile entity else phys:EnableCollisions(false) end + - phys:SetMass(math.Clamp(part.Mass, 0.001, 50000)) + phys:SetMass(math.Clamp(part.Mass, 0.001, pac_sv_projectile_max_mass:GetFloat())) phys:SetDamping(0, 0) + self.phys = phys self:SetAimDir(part.AimDir) - + self:DrawShadow(part.DrawShadow) self.part_data = part end @@ -314,7 +352,7 @@ do -- projectile entity end end - local damage_radius = math.Clamp(self.part_data.DamageRadius, 0, 300) + local damage_radius = math.Clamp(self.part_data.DamageRadius, 0, pac_sv_projectile_max_damage_radius:GetFloat()) if self.part_data.Damage > 0 then if self.part_data.DamageType == "heal" then @@ -359,7 +397,7 @@ do -- projectile entity end elseif self.part_data.DamageType == "explosion" then info:SetDamageType(damage_types.blast) - info:SetDamage(math.Clamp(self.part_data.Damage, 0, 100000)) + info:SetDamage(math.Clamp(self.part_data.Damage, 0, pac_sv_projectile_max_damage:GetFloat())) util.BlastDamageInfo(info, data.HitPos, damage_radius) else info:SetDamageForce(data.OurOldVelocity) @@ -406,12 +444,13 @@ if SERVER then util.AddNetworkString("pac_projectile") util.AddNetworkString("pac_projectile_attach") + util.AddNetworkString("pac_projectile_remove") net.Receive("pac_projectile", function(len, ply) if not enable:GetBool() then return end pace.suppress_prop_spawn = true - if hook.Run("PlayerSpawnProp", ply, "models/props_junk/popcan01a.mdl") == false then + if hook.Run("PlayerSpawnProp", ply, "models/props_junk/PopCan01a.mdl") == false then pace.suppress_prop_spawn = nil return end @@ -465,10 +504,11 @@ if SERVER then local ent = ents.Create("pac_projectile") SafeRemoveEntityDelayed(ent,math.Clamp(part.LifeTime, 0, 50)) - ent:SetModel("models/props_junk/popcan01a.mdl") + ent:SetModel(part.FallbackSurfpropModel) ent:SetPos(pos) ent:SetAngles(ang) ent:Spawn() + if not part.CollideWithOwner then ent:SetOwner(ply) @@ -486,6 +526,7 @@ if SERVER then net.WriteEntity(ply) net.WriteInt(ent:EntIndex(), 16) net.WriteString(part.UniqueID) + net.WriteString(part.SurfaceProperties) net.Broadcast() ent.pac_projectile_uid = part.UniqueID @@ -548,4 +589,15 @@ if SERVER then end end end) + + net.Receive("pac_projectile_remove", function() + local id = net.ReadInt(16) + local ent = ents.GetByIndex(id) + + if ent.part_data.RemoveOnHide then + SafeRemoveEntity(ent) + end + + end) + end diff --git a/models/pac/circle.dx80.vtx b/models/pac/circle.dx80.vtx new file mode 100644 index 0000000000000000000000000000000000000000..3619c0ca8069f1a5c2446760c5557eb348c74a03 GIT binary patch literal 1089 zcmZ9L$yypg6opTt4B`L{YNH5F2sn#?iiq)D@(OvnY_iKX&y!vLTYP7es_&fp_q|m; zR8?0cbx>1V+93Y@{p)*1Tx?1a2Pk6gzqrG7w5V+be4!coiGGyUg(J8WiD+NRqmad< zKk>Azl#NmvxHhy6w}ySgz2TeTz;J138m3{^v6m>8Z6 zWdkcp<4a!+DMn`Q%+$aN(kR6;C5S~!FgC=Vq|DHeb|Pk7II}XG85)K?L+o+7&$gj% zI5+GX)*H_wo}G!~lcv8J;vA&Ri{aQ1M;nQ$x_%QSg{RAAQ657xvUM6O$ zq*zuJs&O^r`0fTrXs1dWEKXLkqoY$@_k2 ag)i~={wcn~SJW5yCt7O#->uhv?f(GC^(1-# literal 0 HcmV?d00001 diff --git a/models/pac/circle.dx90.vtx b/models/pac/circle.dx90.vtx new file mode 100644 index 0000000000000000000000000000000000000000..892ea62bb59de2b45600e8712b3fc07e64a067ca GIT binary patch literal 1089 zcmZ9L$yOpk5Jg|14B`L^3WS*Rv8kj*ErI@A!F=+|LhFFu785+_`#9e33EDa}yhGEALYn-05X{Z}c z4cms*!t;tpXJY%L+g}Z_4^rmYaA=4vkxE^|o8idtVK_Ft8+wL*0ya|#t!W)E6Ejs( zEUOCD*qRk`HF~BxJti@JZj(%1Ta4Y-4&ES^8QVZjyh+T}B8KN`<8AGtTziadXkP`@ z7T)3jb?^hYqeHS?9pMGBi}&b-`gmW*sL%=do=$nzna<}8DTisE)=XwMU(F5N%(IY-3PVgCh$|0YjXEHOrKvTVt6kp;?>T~=9Ewnm!tGQqKKV?BAp8x;= literal 0 HcmV?d00001 diff --git a/models/pac/circle.mdl b/models/pac/circle.mdl new file mode 100644 index 0000000000000000000000000000000000000000..352ae80e6e085fe6df7feb3a0bb26726b0fcdc84 GIT binary patch literal 1692 zcmds2J4*vW5Z?GMglJ`{zd$S$1BN7rHiDfgECM09Tg|~8iMfO{DV8)^X=9PXB8@-6 zS_MfJ!6Jo4O2JkkjUdFC+Z`?!a+laTFgG*be)HJddmJ7dr?(hm-|Yuo=SJDoovd9n zxaDN>ImgtqP9ay~PDVG&y2Uqb!vb=E+%5zx0G#F@_ZMsHPnV$J?|`p?uLWg9U;(u` zBN?Sx8Iu(%W^-Nz@n~yP>@j_Frum?$sbfg7+?X64lT1<* zPadeNLiO|Y`Ib?SXQWW~Ak=gJ>P7yCC-l{Wyuvknz&itmjsjvss9(bmPZqoM9cIh& z>_z(h4RUXpT1pFVrgv^EjGkI17N$%sV+#|cmP-q3N-gJ>s%2!EwVYULmZ62sNaGAF zY(Z*azmm*bx|RjYfrU*>FF#otmVL{TW!Z9QS+VpjP0NvG)pBfUSxzl$mNQG+a$#Aw zY*?-=n-)HQS`Obm$*pC}a%b7LJXm%tkCu+**|KZdi^5ULp`sb6Y8Gl7)fz`~9u~9+ z4K2a4R-ma>XlV`FT89m7LZL0#)(&*E3wzoJiI1hO4xy($40HrT9m9!EVWcxS*9GLd zgt4w*!hM+NMz>Jv4({~;Q$50yo*~lO(8oZN|aU~@F#sR4QUbTLt|8=)e88XJ9~HTo%v!nS%$lF z?%DI5@7yo5tvmK~mKuif_f)>Sgs#auyy>=!slY0gz~92Fr0-DO_K9#H6Un|QK3`ew z&*NU{FY|F@!@3A@H+xA0&_OY0VVy z)`g~lxN*HF=EbjTSV~TW(^j%)w&7tPp6V5NuHI8GM16SjQ{Xv255%AK;i+FX)f+7IOY~RvYhk6!ho?MD z0*^e5hXXP{A`hwGdwh7x8{q??Lf&M4oV*S7`S6ryf#>r4Q*ZzO;q&lS<_F$$ctww` z=no3-89h$Dfe_VetNDlbT)m2)q7PCJyyyHxzu*t`OXbf1%k>L+fd7<-Nwr>R;qrjI z0Z)0;@CMb&<&AvL15bI@@bHq$v+NUx=gtdq0NtD(?mXhW1JBjVop;O=;0u1rdp1FDcS9z^Ck6pWNC1{Qp z&)3~+zl+~E@|N$soNFl+cw_2w%x8hed4Fo8JN|3;%Rc-o%d;XsbWN)M+^VXIH}yU5 z!#jEcA=0DiKfF|v;Pj9V8@!}To44m@M`N#6wfOk=-mVn$p=987)&F3}&CQ9$&>s%_Y)%efusE?v%FTdia&?WN*FF8M8@S5nALJi-#@maBKd6(_!}$Xp&;wnH z{}#?yjxz1#hd`H4P(-|^pl{(=A8{89YOD|rYa%bcI&5Ag>=r~@z6AJh=J zTpe~lPvzHd@61ic)0Trv<%hFpa%IVMP_8evwda28be`ME+x$nBjpx<()l(>vO-C2Y`nllo41>i>r7SNHE6ZqEH_I=_41$1~GS&|mO5 zmA_N{?VkDxFY1?mk)N904=WoZ9Z~oDNRQ@+xBl_@4s#{y)-ya=t?_;K{bFW<{DF47 zHW{>K zeDWcoU#@Sgvo$_*-($#b@%xY0EriSIcc#SOb?gbz54lb+H)#9cxH_S&z4)FNH){KX zKitlacDVJQ@aXgEKe{cU>A&aFqq*joTmP<^4ch&yH!Czf|Nd;4gOnes|I7Ic=crzK xIsR{=eqbTjTO2=%_g}it4)2b*e(tr?;mfAG{^NnQ;nSw8=Zm>(TWND~|9_43;SK-* literal 0 HcmV?d00001 diff --git a/models/pac/plane.dx80.vtx b/models/pac/plane.dx80.vtx new file mode 100644 index 0000000000000000000000000000000000000000..05795dbd6ff731d56d43656caef0dee53ca17182 GIT binary patch literal 189 zcmZ9GyA6Oq3`3n90#2f!gq{)Df*vv2Ey5J6!64Y-Q()idoJ`O1qA>A literal 0 HcmV?d00001 diff --git a/models/pac/plane.dx90.vtx b/models/pac/plane.dx90.vtx new file mode 100644 index 0000000000000000000000000000000000000000..cc63afd750f6b3663a88a5a639f2d47bff00cb35 GIT binary patch literal 189 zcmZ9G%ME}q3`3n10hJg4?u-Caa7T>RMVNv$7z8_f4p@41YUhv!Ku>05B>Hid)#y8s xkB_%M@A#LrlQmRiA~o5Fih;NSNehy})GaZ)xg6%o?m=fn@=?7KVNR-5eN9ocsvx zlDMIRk->qH4?v=$Nxf^|Q3`Fr)l2T(-S7SG-M#nvC!0Aw*#dx0zjazvd zs79_J8RcT$C`r0vsmbK9ah_%t*K|hT?s=q`KCT-0* z^ZL9Kvo%Ew3vNE~zZV_eHul16#D(7zTv~&KV#Z9&Bbmh(alORIUY#5>MfFIi2NLFR z+!S@4S$E``gtT}O^f{lIk0X0!?7!L%JS!_ig7W zk^K!bG0yHb3TUuEG^7fsJVv&MPtM=9nkVI(;f!WzVTi|lah{gUUc_?@&Hw-a literal 0 HcmV?d00001 diff --git a/models/pac/plane.vvd b/models/pac/plane.vvd new file mode 100644 index 0000000000000000000000000000000000000000..4ac2e17682a6ad41de8d8ed75cba28b8d5027cbb GIT binary patch literal 320 zcmeZt2@YdnU|=}?WJVYxkOr~wVW2dF0~9lWG%z&SgGeY~{QqCxaj!YsHUI%!$b6W3EaqcX4|5L|^TFmeAna`bxfLrPC=8TC H$iw&mp_w`k literal 0 HcmV?d00001 From 925fb228eb25c0bd64d9a496416e495d31c9c15e Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 12 Sep 2023 17:53:50 -0400 Subject: [PATCH 017/300] Update projectile.lua micro fix --- lua/pac3/core/client/parts/projectile.lua | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 3c6601339..a42e9d6ef 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -425,15 +425,18 @@ do -- physical if ent.pac_projectile_part then local tbl = ent.pac_projectile_part:GetChildren() local partchild = tbl[next(tbl)] --ent.pac_projectile_part is the root group, but outfit part is the first child - if partchild:IsHidden() then - if ent.pac_projectile.RemoveOnHide and not ent.markedforremove then - ent.markedforremove = true - net.Start("pac_projectile_remove") - net.WriteInt(ent_id, 16) - net.SendToServer() - + if IsValid(partchild) then + if partchild:IsHidden() then + if ent.pac_projectile.RemoveOnHide and not ent.markedforremove then + ent.markedforremove = true + net.Start("pac_projectile_remove") + net.WriteInt(ent_id, 16) + net.SendToServer() + + end end end + end end end) From 8f8314974b5a2801d8e5454e0daa33a2b725300d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 12 Sep 2023 18:09:29 -0400 Subject: [PATCH 018/300] small fix make popup convar actually block the popup if not enabled --- lua/pac3/editor/client/popups_part_tutorials.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index b0eadfe6a..05a090047 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -186,6 +186,7 @@ pac.InfoPopup(str, tbl, x, y) function pac.InfoPopup(str, tbl, x, y) + if not GetConVar("pac_popups_enable"):GetBool() then return end local x = x local y = y if not x or not y then From 038acdf38ea1f1e45fac69bb57abb2786e30ee2b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 13 Sep 2023 22:04:02 -0400 Subject: [PATCH 019/300] adjustment for addon compatibility set reported positions when applying damagezones, in case it's needed by other addons (i.e. hitmarkers) --- lua/pac3/extra/shared/net_combat.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index a4d48d6d7..afe7bd397 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -989,6 +989,8 @@ if SERVER then if tbl.DoNotKill then local dmg_info2 = DamageInfo() + dmg_info2:SetDamagePosition(ent:NearestPoint(pos)) + dmg_info2:SetReportedPosition(pos) dmg_info2:SetDamage( math.min(ent:Health() - tbl.CriticalHealth, tbl.Damage)) dmg_info2:IsBulletDamage(tbl.Bullet) dmg_info2:SetDamageForce(Vector(0,0,0)) @@ -1005,6 +1007,8 @@ if SERVER then if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) and pac_sv_damage_zone_allow_dissolve then dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType]) end + dmg_info:SetDamagePosition(ent:NearestPoint(pos)) + dmg_info:SetReportedPosition(pos) ent:TakeDamageInfo(dmg_info) max_dmg = math.max(max_dmg, dmg_info:GetDamage()) end From d5f78ae1e45eda047aa453686b54b9c0b9dbdbc6 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 14 Sep 2023 15:52:45 -0400 Subject: [PATCH 020/300] IsValid check --- lua/pac3/extra/shared/net_combat.lua | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index afe7bd397..91bd1b316 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1333,7 +1333,14 @@ if SERVER then is_phys = false end - local oldvel = phys_ent:GetVelocity() + local oldvel + + if IsValid(phys_ent) then + oldvel = phys_ent:GetVelocity() + else + oldvel = Vector(0,0,0) + end + local addvel = Vector(0,0,0) local add_angvel = Vector(0,0,0) @@ -1418,15 +1425,17 @@ if SERVER then elseif (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) and tbl.PhysicsProps then if not IsPropProtected(ent, ply) and not (global_combat_prop_protection:GetBool() and unconsenting_owner) then - ent:PhysWake() - if islocaltorque then - phys_ent:AddAngleVelocity(add_angvel) - else - add_angvel = phys_ent:WorldToLocalVector( add_angvel ) - phys_ent:ApplyTorqueCenter(add_angvel) + if IsValid(phys_ent) then + ent:PhysWake() + if islocaltorque then + phys_ent:AddAngleVelocity(add_angvel) + else + add_angvel = phys_ent:WorldToLocalVector( add_angvel ) + phys_ent:ApplyTorqueCenter(add_angvel) + end + ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces + phys_ent:SetVelocity(oldvel + addvel) end - ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces - phys_ent:SetVelocity(oldvel + addvel) end elseif (ent:IsNPC() or string.find(ent:GetClass(), "npc") ~= nil) and tbl.NPC then if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then From 050545135f43bd13080720ee58980854cabbd9ca Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 14 Sep 2023 23:55:38 -0400 Subject: [PATCH 021/300] default help shortcut --- lua/pac3/editor/client/shortcuts.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index aa9023a9b..61892bdac 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -71,6 +71,10 @@ pace.PACActionShortcut_Default = { ["hide_editor"] = { [1] = {"CTRL", "e"} }, + + ["help_info_popup"] = { + [1] = {"F1"} + }, ["hide_editor_visible"] = { [1] = {"ALT", "e"} From b959114f69f1d0d890b808d46f6fa6a9e50986df Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 17 Sep 2023 15:58:52 -0400 Subject: [PATCH 022/300] 2D outlined text --- lua/pac3/core/client/parts/text.lua | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index e03c0da58..d6d8470b6 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -92,7 +92,8 @@ BUILDER:StartStorableVars() :GetSet("Font", "default", {enums = default_fonts}) :GetSet("Size", 1, {editor_sensitivity = 0.25}) :GetSet("DrawMode", "DrawTextOutlined", {enums = { - ["draw.SimpleTextOutlined"] = "DrawTextOutlined", + ["draw.SimpleTextOutlined 3D2D"] = "DrawTextOutlined", + ["draw.SimpleTextOutlined 2D"] = "DrawTextOutlined2D", ["surface.DrawText"] = "SurfaceText" }}) @@ -406,12 +407,12 @@ function PART:OnDraw() DisableClipping(oldState) cam_End3D2D() cam_End3D() - elseif self.DrawMode == "SurfaceText" then + elseif self.DrawMode == "SurfaceText" or self.DrawMode == "DrawTextOutlined2D" then hook.Add("HUDPaint", "pac.DrawText"..self.UniqueID, function() if not pcall(surface_SetFont, self.UsedFont) then return end self:SetFont(self.UsedFont) - surface.SetTextColor(self.Color.r, self.Color.g, self.Color.b) + surface.SetTextColor(self.Color.r, self.Color.g, self.Color.b, 255*self.Alpha) surface.SetFont(self.UsedFont) local pos2d = self:GetDrawPosition():ToScreen() @@ -450,16 +451,27 @@ function PART:OnDraw() if dist < fadeenddist then if dist < fadestartdist then - surface.DrawText(DisplayText, self.ForceAdditive) + 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) + end + else local fade = math.pow(math.Clamp(1 - (dist-fadestartdist)/fadeenddist,0,1),3) - surface.SetTextColor(self.Color.r * fade, self.Color.g * fade, self.Color.b * fade) - surface.DrawText(DisplayText, true) + + 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) + end + end end end) end - if self.DrawMode ~= "SurfaceText" then + if self.DrawMode == "DrawTextOutlined" then hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) end else hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) end From f1ae1d0c3dbe39e364947b806f0290701b7de5c5 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 18 Sep 2023 20:50:58 -0400 Subject: [PATCH 023/300] Update event.lua fix take_damage, inflicting_damage changed eventwheel visibility rules convar because apparently 00 resets to 0 added the receiving net message for health_modifier's extra healthbar's updates (only used by proxies for now) --- lua/pac3/core/client/parts/event.lua | 71 ++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 9d6d08438..9aa61961f 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2064,7 +2064,7 @@ PART.OldEvents = { local damage_enums = {} for k,v in pairs(_G) do if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then - damage_enums[k] = tonumber(v) + damage_enums[k] = tostring(v) end end return damage_enums @@ -2072,13 +2072,15 @@ PART.OldEvents = { }}, callback = function(self, ent, time, damage, attackers, inflictors, damage_type) local time = time or 0 - local ent = self:GetRootPart():GetOwner() if not IsValid(ent) then return false end local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" local found_attacker = attackers == "" or attackers == "anyone" or attackers == "any" or attackers == "all" + local unspec_inflictor = found_inflictor local unspec_attacker = found_attacker + local unspec_dmg = damage_type == -1 + local matching_dmg = unspec_dmg local lastest_attacker = nil local latest_hit_time = 0 @@ -2114,7 +2116,8 @@ PART.OldEvents = { --print("tested attacker ", tbl.attacker:GetClass(), "it aint it.", v) end end - if tbl.hit_time > latest_hit_time and (bit.bor(tbl.dmg_type, damage_type) or damage_type == -1) then + if tbl.hit_time > latest_hit_time and (bit.band(tbl.dmg_type, damage_type) ~= 0 or unspec_dmg) then + matching_dmg = true latest_hit_time = tbl.hit_time lastest_attacker = attacker end @@ -2127,12 +2130,13 @@ PART.OldEvents = { end end - + if not unspec_dmg and not matching_dmg then return false end --print(ent.pac_damage_attributions.IngoingGraceTime) ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 --print("CurTime:"..CurTime(), "Grace:" .. ent.pac_damage_attributions.IngoingGraceTime) + if found_attacker and found_inflictor then if ent.pac_damage_attributions[lastest_attacker] then if CurTime() < ent.pac_damage_attributions[lastest_attacker].hit_time + time then @@ -2206,7 +2210,7 @@ PART.OldEvents = { local damage_enums = {} for k,v in pairs(_G) do if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then - damage_enums[k] = tonumber(v) + damage_enums[k] = tostring(v) end end return damage_enums @@ -2214,7 +2218,7 @@ PART.OldEvents = { }}, callback = function(self, ent, time, damage, targets, inflictors, damage_type) local time = time or 0 - local ent = self:GetRootPart():GetOwner() + if not IsValid(ent) then return false end local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" local found_target = targets == "" or targets == "anyone" or targets == "any" or targets == "all" @@ -2248,7 +2252,8 @@ PART.OldEvents = { end end end - if tbl.hit_time > latest_hit_time and (bit.bor(tbl.dmg_type, damage_type) or damage_type == -1) then + + if tbl.hit_time > latest_hit_time and (bit.band(tbl.dmg_type, damage_type) ~= 0 or unspec_dmg) then latest_hit_time = CurTime() ent.pac_damage_attributions.OutgoingGraceTime = CurTime() ent.pac_damage_attributions.OutgoingGraceTimeDMG = tbl.dmg_amount @@ -2533,9 +2538,8 @@ do --print("button update from" , ply, key, down) --PrintTable(ply.pac_buttons) - + ply.pac_broadcasted_buttons_lastpressed = ply.pac_broadcasted_buttons_lastpressed or {} if down then - ply.pac_broadcasted_buttons_lastpressed = ply.pac_broadcasted_buttons_lastpressed or {} ply.pac_broadcasted_buttons_lastpressed[key] = SysTime() end @@ -3696,15 +3700,15 @@ reload custom gesture --]] -local eventwheel_visibility_rule = CreateConVar("pac_eventwheel_visibility_rule" , "00", FCVAR_ARCHIVE, +local eventwheel_visibility_rule = CreateConVar("pac_eventwheel_visibility_rule" , "0", FCVAR_ARCHIVE, "Different ways to filter your command events for the wheel.\n".. "-1 ignores hide flags completely\n".. "0 will hide a command if at least one event of one name has the \"hide in event wheel\" flag\n".. -"00 will hide a command only if ALL events of one name have the \"hide in event wheel\" flag\n".. -"1 will hide a command as soon as one event of a name is being hidden\n".. -"11 will hide a command only if ALL events of a name are being hidden\n".. -"2 will only show commands containing the following substrings, separated by spaces\n".. -"-2 will hide commands containing the following substrings, separated by spaces") +"1 will hide a command only if ALL events of one name have the \"hide in event wheel\" flag\n".. +"2 will hide a command as soon as one event of a name is being hidden\n".. +"3 will hide a command only if ALL events of a name are being hidden\n".. +"4 will only show commands containing the following substrings, separated by spaces\n".. +"-4 will hide commands containing the following substrings, separated by spaces") -- Custom event selector wheel do local function get_events() @@ -3766,15 +3770,15 @@ do if v.wheel_hidden then remove = true end - elseif args[1] == "00" then --all hide_in_eventwheel + elseif args[1] == "1" then --all hide_in_eventwheel if v.all_wheel_hidden then remove = true end - elseif args[1] == "1" then --one hidden + elseif args[1] == "2" then --one hidden if v.possible_hidden then remove = true end - elseif args[1] == "11" then --all hidden + elseif args[1] == "3" then --all hidden if v.all_possible_hidden then remove = true end @@ -3789,9 +3793,9 @@ do end end - if args[1] == "2" and not match then + if args[1] == "4" and not match then remove = true - elseif args[1] == "-2" and match then + elseif args[1] == "-4" and match then remove = true end @@ -4056,4 +4060,29 @@ net.Receive("pac.BroadcastDamageAttributions", function() end end -end) \ No newline at end of file +end) + +net.Receive("pac_update_healthbars", function() + local ent = net.ReadEntity() + local tbl = net.ReadTable() + + ent.pac_healthbars = tbl + + ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {} + ent.pac_healthbars_uidtotals = ent.pac_healthbars_uidtotals or {} + ent.pac_healthbars_total = 0 + + for layer=15,0,-1 do --go progressively inward in the layers + ent.pac_healthbars_layertotals[layer] = 0 + if tbl[layer] then + for uid,value in pairs(tbl[layer]) do --check the healthbars by uid + ent.pac_healthbars_uidtotals[uid] = value + ent.pac_healthbars_layertotals[layer] = ent.pac_healthbars_layertotals[layer] + value + ent.pac_healthbars_total = ent.pac_healthbars_total + value + end + else + ent.pac_healthbars_layertotals[layer] = nil + end + end + +end) From f0217561c063280ef8de4fdc161afb2cc992531d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 18 Sep 2023 20:54:36 -0400 Subject: [PATCH 024/300] connectors for health_modifier extra healthbars --- lua/pac3/core/client/parts/proxy.lua | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 2a3bc1b83..1c526c738 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -944,6 +944,32 @@ PART.Inputs.flat_dot_right = function(self) return 0 end + +PART.Inputs.pac_healthbars_total = function(self) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars then + return ent.pac_healthbars_total or 0 + end + return 0 +end + +PART.Inputs.pac_healthbars_layertotal = function(self, layer) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars and ent.pac_healthbars_layertotals then + return ent.pac_healthbars_layertotals[layer] or 0 + end + return 0 +end + +PART.Inputs.pac_healthbar_uidvalue = function(self, uid) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars and ent.pac_healthbars_uidtotals then + return ent.pac_healthbars_uidtotals[uid] or 0 + end + return 0 +end + + net.Receive("pac_proxy", function() local ply = net.ReadEntity() local str = net.ReadString() From d21d45afb3fa3e11348e4a2eb6316fc697aa7d1d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 18 Sep 2023 20:57:48 -0400 Subject: [PATCH 025/300] fix healthbars and combat consents are now all off --- lua/pac3/extra/shared/net_combat.lua | 46 +++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 91bd1b316..1fbf07341 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -314,6 +314,10 @@ if SERVER then --for each uid, we have the current uid bar cluster's health value --instead of keeping track of every bar, it will update the status with a remainder calculation + --ply.pac_healthbars + --ply.pac_healthbars[layer] + --ply.pac_healthbars[layer][part_uid] = healthvalue + local function UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) local existing_uidlayer = true local healthvalue = 0 @@ -467,6 +471,7 @@ if SERVER then end end end + return remaining_dmg,surviving_layer,side_effect_dmg end @@ -486,6 +491,15 @@ if SERVER then util.AddNetworkString("pac_notify_grabbed_player") util.AddNetworkString("pac_request_player_combat_consent_update") util.AddNetworkString("pac_request_healthmod") + util.AddNetworkString("pac_update_healthbars") + + local function SendUpdateHealthBars(target) + if not target:IsPlayer() or not target.pac_healthbars then return end + net.Start("pac_update_healthbars") + net.WriteEntity(target) + net.WriteTable(target.pac_healthbars) + net.Broadcast() + end net.Receive("pac_request_healthmod", function(len,ply) if not healthmod_allow:GetBool() then return end @@ -547,7 +561,7 @@ if SERVER then FixMaxHealths(ply) UpdateHealthBars(ply, 0, 0, 0, 0, part_uid, follow) end - + SendUpdateHealthBars(ply) end) net.Receive("pac_hitscan", function(len,ply) @@ -589,6 +603,18 @@ if SERVER then end) + + --healthbars work with a 2 levels-deep table + --for each player, an index table (priority) to decide which layer is damaged first + --for each layer, one table for each part uid + --for each uid, we have the current uid bar cluster's health value + --instead of keeping track of every bar, it will update the status with a remainder calculation + + --ply.pac_healthbars + --ply.pac_healthbars[layer] + --ply.pac_healthbars[layer][part_uid] = healthvalue + + --apply hitscan consents, eat into extra healthbars first and calculate final damage multipliers from pac3 hook.Add( "EntityTakeDamage", "ApplyPACDamageModifiers", function( target, dmginfo ) if target:IsPlayer() then @@ -596,26 +622,30 @@ if SERVER then dmginfo:ScaleDamage(cumulative_mult) local remaining_dmg,surviving_layer,side_effect_dmg = GetHPBarDamage(target, dmginfo:GetDamage()) - - if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and not hitscan_consents[target] then + + if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and hitscan_consents[target] == false then dmginfo:SetDamage(0) else local total_hp_value,built_tbl = GatherExtraHPBars(target) - if total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult + if surviving_layer == nil or total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult if cumulative_mult < 0 then target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(dmginfo:GetDamage()),0,target:GetMaxHealth()))) return true + else + dmginfo:SetDamage(remaining_dmg) + if target.pac_healthbars then SendUpdateHealthBars(target) end end else --shields = use the calculated cumulative side effect damage from each uid's related absorbfactor - + if side_effect_dmg < 0 then target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(side_effect_dmg),0,target:GetMaxHealth()))) return true else - dmginfo:SetDamage(side_effect_dmg) + dmginfo:SetDamage(side_effect_dmg + remaining_dmg) + SendUpdateHealthBars(target) end end @@ -1605,8 +1635,8 @@ if CLIENT then CreateConVar("pac_client_grab_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part") CreateConVar("pac_client_lock_camera_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to having lock parts override your view") CreateConVar("pac_client_damage_zone_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") - CreateConVar("pac_client_force_consent", "1", FCVAR_ARCHIVE, "Whether you want to consent to pac3 physics forces") - CreateConVar("pac_client_hitscan_consent", "1", FCVAR_ARCHIVE, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") + CreateConVar("pac_client_force_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to pac3 physics forces") + CreateConVar("pac_client_hitscan_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") CreateConVar("pac_break_lock_verbosity", "3", FCVAR_ARCHIVE, "How much info you want for the PAC3 lock notifications\n3:full information\n2:grabbing player + basic reminder of the lock break command\n1:grabbing player\n0:suppress the notifications") From 539a153adc75508728e846ed4b13a4cd8a607092 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 14:03:28 -0400 Subject: [PATCH 026/300] variable checks --- lua/pac3/core/server/net_messages.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lua/pac3/core/server/net_messages.lua b/lua/pac3/core/server/net_messages.lua index 856473ef5..b4c04c0cf 100644 --- a/lua/pac3/core/server/net_messages.lua +++ b/lua/pac3/core/server/net_messages.lua @@ -99,10 +99,14 @@ do -- input event pac_broadcast_inputs[ply][v] = false end - if last_broadcast_inputs[ply][v] ~= pac_broadcast_inputs[ply][v] then - update = true - player_last_input_broadcast_times[ply] = CurTime() + if last_broadcast_inputs[ply] and pac_broadcast_inputs[ply] then + if last_broadcast_inputs[ply][v] ~= pac_broadcast_inputs[ply][v] then + update = true + player_last_input_broadcast_times[ply] = CurTime() + end end + + end end broadcast_inputs(update) @@ -149,7 +153,7 @@ do --is_using_entity end do --damage attribution - timer.Simple(1, --call it regularly in case a new hook overrides the damage, we want the final one + timer.Simple(1, function()--call it regularly in case a new hook overrides the damage, we want the final one pac.AddHook("EntityTakeDamage", "pac.AttributeDamage", function(ent, dmg) local time = CurTime() if IsValid(dmg:GetAttacker()) then @@ -163,5 +167,5 @@ do --damage attribution end end) - ) -end \ No newline at end of file + end) +end From 13a267154bbd11aac9390c4180b0e8d8b4b713f0 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 14:04:26 -0400 Subject: [PATCH 027/300] handle blank events --- lua/pac3/core/client/parts/event.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 9aa61961f..f1199203c 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -98,12 +98,14 @@ function PART:AttachEditorPopup(str) local info_string = str or "no information available" local verbosity = "" - - --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then - info_string = PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory" - --end - - str = info_string or str + if self.Event ~= "" then + + --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + info_string = PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory" + --end + + str = info_string or str + end self:SetupEditorPopup(info_string, true) end From f4d7041a93623afbebff8a0c5d1693d776e007fb Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 14:06:42 -0400 Subject: [PATCH 028/300] warn if event type is invalid --- lua/pac3/core/client/parts/event.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index f1199203c..fa7fa3c29 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -99,9 +99,13 @@ function PART:AttachEditorPopup(str) local info_string = str or "no information available" local verbosity = "" if self.Event ~= "" then - - --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + if PART.Events[self.Event] then info_string = PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory" + else + info_string = "invalid event" + end + --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + --end str = info_string or str From b72230da920e7b8ca4498b8065d00249ca04a952 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 14:07:56 -0400 Subject: [PATCH 029/300] block part popup if no part --- lua/pac3/editor/client/popups_part_tutorials.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index 05a090047..259e17897 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -522,6 +522,7 @@ function pac.InfoPopup(str, tbl, x, y) end function pac.AttachInfoPopupToPart(obj, str, tbl) + if not obj then return end obj:AttachEditorPopup(str, true, tbl) end From 79340e725551be51852f07557f60f0e0bcfd88ec Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 14:25:24 -0400 Subject: [PATCH 030/300] more info and close popups better --- lua/pac3/editor/client/popups_part_tutorials.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index 259e17897..6d51bb62d 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -236,7 +236,7 @@ function pac.InfoPopup(str, tbl, x, y) pnl.hoverfunc = function() end pnl.doclickfunc = function() end pnl.titletext = "Click for more information! (or F1)" - pnl.alternativetitle = "Right click to kill the popup. \"pac_popups_preserve_on_autofade\" is set to " .. GetConVar("pac_popups_preserve_on_autofade"):GetInt() .. ", " .. (GetConVar("pac_popups_preserve_on_autofade"):GetBool() and "If it fades away, the popup is allowed to reappear on hover or F1" or "If it fades away, the popup will not reappear") + pnl.alternativetitle = "Right click / Alt+P to kill popups. \"pac_popups_preserve_on_autofade\" is set to " .. GetConVar("pac_popups_preserve_on_autofade"):GetInt() .. ", " .. (GetConVar("pac_popups_preserve_on_autofade"):GetBool() and "If it fades away, the popup is allowed to reappear on hover or F1" or "If it fades away, the popup will not reappear") --pnl:SetPos(ScrW()/2 + math.Rand(-100,100), ScrH()/2 + math.Rand(-100,100)) @@ -362,6 +362,7 @@ function pac.InfoPopup(str, tbl, x, y) function pnl:Think() self:MoveToObj(tbl) if input.IsButtonDown(KEY_P) and input.IsButtonDown(KEY_LALT) then --auto-kill if alt-p + tbl.pac_part.killpopup = true self:Remove() end From b682f4da00acf08bc9daed21310dd00abd3c0b8d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 15:41:19 -0400 Subject: [PATCH 031/300] Update parts.lua if part menu is not created on a part, default to selecting the pace.current_part + possibly redundant safety checks for operations requiring a part handle event args better for bulk apply properties reorder movables operation to swap, cast or reorder base_movables --- lua/pac3/editor/client/parts.lua | 244 +++++++++++++++---------------- 1 file changed, 116 insertions(+), 128 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 567a0dcac..641e423dd 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -496,7 +496,6 @@ do -- menu end end end) - --@note partmenu definer sub.GetDeleteSelf = function() return false end @@ -730,8 +729,11 @@ do -- menu b_part:SetParent(a_part.Parent) b_part:SetEditorExpand(true) a_part:SetParent(b_part) a_part:SetEditorExpand(true) else - a_part:SetParent(b_part.Parent) - b_part:SetParent(a_part.Parent) + local a_parent = a_part.Parent + local b_parent = b_part.Parent + + a_part:SetParent(b_parent) + b_part:SetParent(a_parent) end pace.RefreshTree() end @@ -957,7 +959,9 @@ do -- menu if shared and not prop.udata.editor_friendly and basepart["Get" .. prop["key"]] ~= nil then shared_properties[#shared_properties + 1] = prop["key"] elseif shared and prop.udata.editor_friendly and basepart["Get" .. prop["key"]] == nil then - shared_udata_properties[#shared_udata_properties + 1] = "event_udata_"..prop["key"] + if not table.HasValue(shared_udata_properties, "event_udata_"..prop["key"]) then + shared_udata_properties[#shared_udata_properties + 1] = "event_udata_"..prop["key"] + end end end @@ -970,7 +974,9 @@ do -- menu if part2["Get" .. prop["key"]] ~= nil then initial_shared_properties[#initial_shared_properties + 1] = prop["key"] elseif part2["Get" .. prop["key"]] == nil then - initial_shared_udata_properties[#initial_shared_udata_properties + 1] = "event_udata_"..prop["key"] + if not table.HasValue(initial_shared_udata_properties, "event_udata_"..prop["key"]) then + initial_shared_udata_properties[#initial_shared_udata_properties + 1] = "event_udata_"..prop["key"] + end end end end @@ -1077,46 +1083,67 @@ do -- menu end --populate panels for event "userdata" packaged into arguments - if #shared_udata_properties > 1 then - local udata_types = { - hide_in_eventwheel = "boolean", - find = "string", - time = "number", - distance = "number", - compare = "number", - min = "number", - max = "number", - primary = "boolean", - amount = "number", - ammo_id = "number", - find_ammo = "string", - hide = "boolean", - interval = "number", - offset = "number", - find_sound = "string", - mute = "boolean", - all_players = "boolean"} - local udata_orders = { - ["command"] = {[1] = "find", [2] = "time", [3] = "hide_in_eventwheel"}, - ["timerx"] = {[1] = "seconds", [2] = "reset_on_hide", [3] = "synced_time"}, - ["ranger"] = {[1] = "distance", [2] = "compare", [3] = "npcs_and_players_only"}, - ["randint"] = {[1] = "compare", [2] = "min", [3] = "max"}, - ["random_timer"] = {[1] = "min", [2] = "max", [3] = "compare"}, - ["timersys"] = {[1] = "seconds", [2] = "reset_on_hide"}, - ["pose_parameter"] = {[1] = "name", [2] = "num"}, - ["ammo"] = {[1] = "primary", [2] = "amount"}, - ["total_ammo"] = {[1] = "ammo_id", [2] = "amount"}, - ["clipsize"] = {[1] = "primary", [2] = "amount"}, - ["weapon_class"] = {[1] = "find", [2] = "hide"}, - ["timer"] = {[1] = "interval", [2] = "offset"}, - ["animation_event"] = {[1] = "find", [2] = "time"}, - ["fire_bullets"] = {[1] = "find_ammo", [2] = "time"}, - ["emit_sound"] = {[1] = "find_sound", [2] = "time", [3] = "mute"}, - ["say"] = {[1] = "find", [2] = "time", [3] = "all_players"}} - for i,v in pairs(shared_udata_properties) do - local udata_val_name = string.gsub(v, "event_udata_", "") + if #shared_udata_properties > 0 then + local fallback_event_types = {} + local fallback_event + for i,v in ipairs(pace.BulkSelectList) do + if v.ClassName == "event" then + table.Add(fallback_event_types,v.Event) + fallback_event = v + end + end + + --[[example udata arg from part.Events[part.Event].__registeredArguments + 1: + 1 = button + 2 = string + 3: + default = mouse_left + enums = function: 0xa88929ea + group = arguments + ]] + + local function GetEventArgType(part, str) - local var_type = udata_types[udata_val_name] + for argn,arg in ipairs(part.Events[part.Event].__registeredArguments) do + if arg[1] == str then + return arg[2] + end + end + if fallback_event then + for i,e in ipairs(fallback_event_types) do + for argn,arg in ipairs(fallback_event.Events[e].__registeredArguments) do + if arg[1] == str then + return arg[2] + end + end + end + end + return "string" + end + + local function GetEventArgIndex(part,str) + str = string.gsub(str, "event_udata_", "") + + for argn,arg in ipairs(part.Events[part.Event].__registeredArguments) do + if arg[1] == str then + return argn + end + end + return 1 + end + + local function ApplyArgToIndex(args_str, str, index) + local args_tbl = string.Split(args_str,"@@") + args_tbl[index] = str + return table.concat(args_tbl,"@@") + end + + for i,v in ipairs(shared_udata_properties) do + + local udata_val_name = string.gsub(v, "event_udata_", "") + + local var_type = GetEventArgType(obj, udata_val_name) if var_type == nil then var_type = "string" end local VAR_PANEL = vgui.Create("DFrame") @@ -1149,7 +1176,7 @@ do -- menu end VAR_PANEL_EDITZONE:SetPos(200,0) - VAR_PANEL:SetTitle("[" .. i .. "] "..string.gsub(v, "event_udata_", "").." "..var_type) + VAR_PANEL:SetTitle("[" .. i .. "] "..udata_val_name.." "..var_type) VAR_PANEL_BUTTON:SetText("APPLY") @@ -1157,7 +1184,8 @@ do -- menu VAR_PANEL:DockMargin( 5, 0, 0, 5 ) VAR_PANEL_BUTTON.DoClick = function() - for i,part in pairs(pace.BulkSelectList) do + for i,part in ipairs(pace.BulkSelectList) do + --PrintTable(part.Events[part.Event].__registeredArguments) if part.ClassName == "event" and part.Event == basepart.Event then local sent_var if var_type == "number" then @@ -1178,28 +1206,10 @@ do -- menu sent_var = sent_var..i end else sent_var = VAR_PANEL_EDITZONE:GetValue() end - - local arg_split = string.Split(part:GetArguments(), "@@") - - if #arg_split > 1 then - if #udata_orders[part.Event] ~= #string.Split(part:GetArguments(), "@@") then arg_split[#arg_split + 1] = "0" end - - local sent_var_final = "" - -- PRESCRIBED X @@ Y @@ Z ... - -- EVERY STEP ADD - -- EVERY STEP EXCEPT LAST..@@ - for n,arg in ipairs(arg_split) do - if udata_orders[part.Event][n] == udata_val_name then - sent_var_final = sent_var_final .. sent_var - else - sent_var_final = sent_var_final .. arg_split[n] - end - if n ~= #arg_split then - sent_var_final = sent_var_final .. "@@" - end - end - else sent_var_final = sent_var end - part:SetArguments(sent_var_final) + + print(part, part:GetArguments(), v, sent_var, GetEventArgIndex(part,v)) + part:SetArguments(ApplyArgToIndex(part:GetArguments(), sent_var, GetEventArgIndex(part,v))) + print(part:GetArguments()) end if thoroughness_tickbox:GetChecked() then @@ -1225,22 +1235,7 @@ do -- menu end else sent_var = VAR_PANEL_EDITZONE:GetValue() end - local arg_split = string.Split(child:GetArguments(), "@@") - if #udata_orders[basepart.Event] ~= #string.Split(child:GetArguments(), "@@") then arg_split[#arg_split + 1] = "0" end - local sent_var_final = "" - - for n,arg in ipairs(arg_split) do - if udata_orders[child.Event][n] == udata_val_name then - sent_var_final = sent_var_final .. sent_var - else - sent_var_final = sent_var_final .. arg_split[n] - end - if n ~= #arg_split then - sent_var_final = sent_var_final .. "@@" - end - end - - child:SetArguments(sent_var_final) + child:SetArguments(ApplyArgToIndex(child:GetArguments(), sent_var, GetEventArgIndex(child,v))) end end end @@ -1369,7 +1364,7 @@ do -- menu menu:SetPos(input.GetCursorPos()) --new_operations_order --default_operations_order - + if not obj then obj = pace.current_part end for _,option_name in ipairs(pace.operations_order) do pace.addPartMenuComponent(menu, obj, option_name) end @@ -1623,43 +1618,38 @@ function pace.GetPartSizeInformation(obj) end function pace.addPartMenuComponent(menu, obj, option_name) - if option_name == "save" then - if obj then - local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end) - pnl:SetImage(pace.MiscIcons.save) - add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end) - end - elseif option_name == "load" then + + if option_name == "save" and obj then + local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end) + pnl:SetImage(pace.MiscIcons.save) + add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end) + elseif option_name == "load" and obj then local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) pnl:SetImage(pace.MiscIcons.load) - elseif option_name == "wear" then - if obj then - if not obj:HasParent() then - menu:AddOption(L"wear", function() - pace.SendPartToServer(obj) - pace.BulkSelectList = {} - end):SetImage(pace.MiscIcons.wear) - end - end - elseif option_name == "remove" then - if obj then - menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear) - end - elseif option_name == "copy" then + elseif option_name == "wear" and obj then + if not obj:HasParent() then + menu:AddOption(L"wear", function() + pace.SendPartToServer(obj) + pace.BulkSelectList = {} + end):SetImage(pace.MiscIcons.wear) + end + elseif option_name == "remove" and obj then + menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear) + elseif option_name == "copy" and obj then local menu2, pnl = menu:AddSubMenu(L"copy", function() pace.Copy(obj) end) pnl:SetIcon(pace.MiscIcons.copy) --menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy) menu2:AddOption(L"Copy part UniqueID", function() pace.CopyUID(obj) end):SetImage(pace.MiscIcons.uniqueid) - elseif option_name == "paste" then + elseif option_name == "paste" and obj then menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste) - elseif option_name == "cut" then + elseif option_name == "cut" and obj then menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png') - elseif option_name == "paste_properties" then + elseif option_name == "paste_properties" and obj then menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) - elseif option_name == "clone" then + elseif option_name == "clone" and obj then menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) - elseif option_name == "partsize_info" then + elseif option_name == "partsize_info" and obj then local function GetTableSizeInfo(obj_arg) return pace.GetPartSizeInformation(obj_arg) end @@ -1816,38 +1806,36 @@ function pace.addPartMenuComponent(menu, obj, option_name) pace.AddRegisteredPartsToMenu(menu, not obj) elseif option_name == "hide_editor" then menu:AddOption(L"hide editor / toggle focus", function() pace.Call("ToggleFocus") end):SetImage('icon16/zoom.png') - elseif option_name == "expand_all" then + elseif option_name == "expand_all" and obj then menu:AddOption(L"expand all", function() obj:CallRecursive('SetEditorExpand', true) pace.RefreshTree(true) end):SetImage('icon16/arrow_down.png') - elseif option_name == "collapse_all" then + elseif option_name == "collapse_all" and obj then menu:AddOption(L"collapse all", function() obj:CallRecursive('SetEditorExpand', false) pace.RefreshTree(true) end):SetImage('icon16/arrow_in.png') - elseif option_name == "copy_uid" then + elseif option_name == "copy_uid" and obj then local menu2, pnl = menu:AddSubMenu(L"Copy part UniqueID", function() pace.CopyUID(obj) end) pnl:SetIcon(pace.MiscIcons.uniqueid) - elseif option_name == "help_part_info" then + elseif option_name == "help_part_info" and obj then menu:AddOption(L"View help or info about this part", function() pac.AttachInfoPopupToPart(obj, nil, { obj_type = GetConVar("pac_popups_preferred_location"):GetString(), hoverfunc = "open", pac_part = pace.current_part, panel_exp_width = 900, panel_exp_height = 400 }) end):SetImage('icon16/information.png') - elseif option_name == "reorder_movables" then - if obj then - if obj.Position and obj.Angles and obj.PositionOffset then - local substitute, pnl = menu:AddSubMenu("Reorder / replace base movable") - pnl:SetImage('icon16/application_double.png') - substitute:AddOption("Create a parent for position substitution", function() pace.SubstituteBaseMovable(obj, "create_parent") end) - if obj.Parent then - if obj.Parent.Position and obj.Parent.Angles then - substitute:AddOption("Switch with parent", function() pace.SubstituteBaseMovable(obj, "reorder_child") end) - end + elseif option_name == "reorder_movables" and obj then + if (obj.Position and obj.Angles and obj.PositionOffset) then + local substitute, pnl = menu:AddSubMenu("Reorder / replace base movable") + pnl:SetImage('icon16/application_double.png') + substitute:AddOption("Create a parent for position substitution", function() pace.SubstituteBaseMovable(obj, "create_parent") end) + if obj.Parent then + if obj.Parent.Position and obj.Parent.Angles then + substitute:AddOption("Switch with parent", function() pace.SubstituteBaseMovable(obj, "reorder_child") end) end - substitute:AddOption("Switch with another (select two parts with bulk select)", function() pace.SwapBaseMovables(pace.BulkSelectList[1], pace.BulkSelectList[2], false) end) - substitute:AddOption("Recast into new class (warning!)", function() pace.SubstituteBaseMovable(obj, "cast") end) end + substitute:AddOption("Switch with another (select two parts with bulk select)", function() pace.SwapBaseMovables(pace.BulkSelectList[1], pace.BulkSelectList[2], false) end) + substitute:AddOption("Recast into new class (warning!)", function() pace.SubstituteBaseMovable(obj, "cast") end) end end @@ -2221,7 +2209,7 @@ do --hover highlight halo if refresh_halo_hook then return end if part_trace then for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do - if v.pace_tree_node then + if IsValid(v.pace_tree_node) then v.pace_tree_node:SetAlpha( 255 ) end From f1d1921ab8d542150bfd5650bc2a6717073f5bdb Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 15:47:29 -0400 Subject: [PATCH 032/300] Update event.lua --- lua/pac3/core/server/event.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/server/event.lua b/lua/pac3/core/server/event.lua index 252d9a843..ba156128a 100644 --- a/lua/pac3/core/server/event.lua +++ b/lua/pac3/core/server/event.lua @@ -6,6 +6,7 @@ util.AddNetworkString("pac_event_set_sequence") net.Receive("pac_event_set_sequence", function(len, ply) local event = net.ReadString() local num = net.ReadUInt(8) + ply.pac_command_events = ply.pac_command_events or {} for i=1,100,1 do ply.pac_command_events[event..i] = nil end From 65c877b64a1e78ac029b8e3c55e0142eebafa6d5 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 16:28:03 -0400 Subject: [PATCH 033/300] Update saved_parts.lua restore some old behavior when using the new prop outfit queuing system : clear parts when loading from menu bar pac tab --- lua/pac3/editor/client/saved_parts.lua | 42 ++++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index df13b127e..dd4de907b 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -232,33 +232,35 @@ function pace.LoadParts(name, clear, override_part) name = name:gsub("%.txt", "") local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") + local has_possible_prop_pacs = false + for i,part in pairs(data) do + if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end + end --queue up prop pacs for the next prop or npc you spawn when in singleplayer - if (auto_spawn_prop:GetInt() == 1 or auto_spawn_prop:GetInt() == 2) and game.SinglePlayer() then - + if (auto_spawn_prop:GetInt() == 2 or (auto_spawn_prop:GetInt() == 1 and #data == 1)) and game.SinglePlayer() and has_possible_prop_pacs then + if clear then pace.ClearParts() end LocalPlayer().pac_propload_queuedparts = LocalPlayer().pac_propload_queuedparts or {} - if auto_spawn_prop:GetInt() == 2 or (auto_spawn_prop:GetInt() == 1 and #data > 1) then - --check all root parts from data. format: each data member is a {self, children} table of the part and the list of children - for i,part in pairs(data) do - local possible_prop_pac = isnumber(tonumber(part.self.OwnerName)) - if part.self.ClassName == "group" and possible_prop_pac then - - part.self.ModelTracker = part.self.ModelTracker or "" - part.self.ClassTracker = part.self.ClassTracker or "" - local str = "" - if part.self.ClassTracker == "" or part.self.ClassTracker == "" then - str = "But the class or model is unknown" - else - str = part.self.ClassTracker .. " : " .. part.self.ModelTracker - end - --notify which model / entity should be spawned with the class tracker - notification.AddLegacy( "You have queued a pac part (" .. i .. ":" .. part.self.Name .. ") for a prop or NPC! " .. str, NOTIFY_HINT, 10 ) - LocalPlayer().pac_propload_queuedparts[i] = part + --check all root parts from data. format: each data member is a {self, children} table of the part and the list of children + for i,part in pairs(data) do + local possible_prop_pac = isnumber(tonumber(part.self.OwnerName)) + if part.self.ClassName == "group" and possible_prop_pac then + part.self.ModelTracker = part.self.ModelTracker or "" + part.self.ClassTracker = part.self.ClassTracker or "" + local str = "" + if part.self.ClassTracker == "" or part.self.ClassTracker == "" then + str = "But the class or model is unknown" else - pace.LoadPartsFromTable(part, false, false) + str = part.self.ClassTracker .. " : " .. part.self.ModelTracker end + --notify which model / entity should be spawned with the class tracker + notification.AddLegacy( "You have queued a pac part (" .. i .. ":" .. part.self.Name .. ") for a prop or NPC! " .. str, NOTIFY_HINT, 10 ) + LocalPlayer().pac_propload_queuedparts[i] = part + + else + pace.LoadPartsFromTable(part, false, false) end end From 79b8ff7205ea81c79c54d4e931588a4e954091cd Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 19 Sep 2023 16:31:43 -0400 Subject: [PATCH 034/300] access to prop outfit queuing options --- lua/pac3/editor/client/menu_bar.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index aefcfe5eb..0cc11532c 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -115,7 +115,14 @@ local function populate_options(menu) menu:AddCVar(L"enable shift+move/rotate clone", "pac_grab_clone", "1", "0") menu:AddCVar(L"remember editor position", "pac_editor_remember_position", "1", "0") menu:AddCVar(L"ask before loading autoload", "pac_prompt_for_autoload", "1", "0") - if game.SinglePlayer() then menu:AddCVar(L"queue prop / npc outfits for next spawned entity", "pac_prompt_for_autoload", "2", "0") end + + local prop_pac_load_mode, pnlpplm = menu:AddSubMenu("(singleplayer only) How to handle prop/npc outfits", function() end) + prop_pac_load_mode.GetDeleteSelf = function() return false end + pnlpplm:SetImage("icon16/transmit.png") + prop_pac_load_mode:AddOption(L"Load without queuing", function() RunConsoleCommand("pac_autoload_preferred_prop", "0") end) + prop_pac_load_mode:AddOption(L"Queue parts if there's only one group", function() RunConsoleCommand("pac_autoload_preferred_prop", "1") end) + prop_pac_load_mode:AddOption(L"Queue parts if there's one or more groups", function() RunConsoleCommand("pac_autoload_preferred_prop", "2") end) + menu:AddCVar(L"show parts IDs", "pac_show_uniqueid", "1", "0") local popups, pnlp = menu:AddSubMenu("configure editor popups", function() end) popups.GetDeleteSelf = function() return false end From 55120df317cd702a0fdacf076abf53ca4f664204 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 20 Sep 2023 21:51:23 -0400 Subject: [PATCH 035/300] fix ammo --- lua/pac3/core/client/parts/text.lua | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index d6d8470b6..c490fe961 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -251,7 +251,6 @@ function PART:OnDraw() self:CheckFont() if not pcall(surface_SetFont, self.UsedFont) then return end - local DisplayText = self.Text or "" if self.TextOverride == "Text" then goto DRAW end DisplayText = "" @@ -259,11 +258,11 @@ function PART:OnDraw() elseif self.TextOverride == "MaxHealth" then DisplayText = self:GetRootPart():GetOwner():GetMaxHealth() elseif self.TextOverride == "Ammo" then - DisplayText = self:GetPlayerOwner():GetActiveWeapon():Clip1() + DisplayText = IsValid(self:GetPlayerOwner():GetActiveWeapon()) and self:GetPlayerOwner():GetActiveWeapon():Clip1() or "" elseif self.TextOverride == "ClipSize" then - DisplayText = self:GetPlayerOwner():GetActiveWeapon():GetMaxClip1() + DisplayText = IsValid(self:GetPlayerOwner():GetActiveWeapon()) and self:GetPlayerOwner():GetActiveWeapon():GetMaxClip1() or "" elseif self.TextOverride == "AmmoReserve" then - DisplayText = self:GetPlayerOwner():GetAmmoCount(self:GetPlayerOwner():GetActiveWeapon():GetPrimaryAmmoType()) + DisplayText = IsValid(self:GetPlayerOwner():GetActiveWeapon()) and self:GetPlayerOwner():GetAmmoCount(self:GetPlayerOwner():GetActiveWeapon():GetPrimaryAmmoType()) or "" elseif self.TextOverride == "Armor" then DisplayText = self:GetPlayerOwner():Armor() elseif self.TextOverride == "MaxArmor" then @@ -358,8 +357,8 @@ function PART:OnDraw() elseif self.TextOverride == "MaxPlayers" then DisplayText = game.MaxPlayers() elseif self.TextOverride == "Weapon" then - if IsValid(self:GetRootPart():GetOwner():GetActiveWeapon()) then - DisplayText = self:GetRootPart():GetOwner():GetActiveWeapon():GetClass() + if IsValid(self:GetPlayerOwner():GetActiveWeapon()) then + DisplayText = self:GetPlayerOwner():GetActiveWeapon():GetClass() else DisplayText = "unarmed" end elseif self.TextOverride == "VehicleClass" then if IsValid(self:GetPlayerOwner():GetVehicle()) then @@ -447,8 +446,6 @@ function PART:OnDraw() fadeenddist = temp end - --print("dist:",dist,"start",fadestartdist,"end",fadeenddist, "factor = ", math.pow(math.Clamp((fadeenddist - dist)/fadestartdist,0,1),1)) - if dist < fadeenddist then if dist < fadestartdist then if self.DrawMode == "DrawTextOutlined2D" then From 515977dcbf13d1703d0865dddf53bd210a14e97b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 21 Sep 2023 18:51:35 -0400 Subject: [PATCH 036/300] Update init.lua --- lua/pac3/core/server/init.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/pac3/core/server/init.lua b/lua/pac3/core/server/init.lua index 7abebcfee..4e60b8688 100644 --- a/lua/pac3/core/server/init.lua +++ b/lua/pac3/core/server/init.lua @@ -17,7 +17,6 @@ include("pac3/core/shared/init.lua") include("effects.lua") include("event.lua") include("net_messages.lua") -include("net_combat.lua") include("test_suite_backdoor.lua") include("in_skybox.lua") From 082d606ac69982c6d0a39193e2cffe4520470ce4 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 22 Sep 2023 02:24:18 -0400 Subject: [PATCH 037/300] preliminary net fixes make a copy of replicated server cvars so they don't break the settings panel condemn the input event rework some net messages for a lighter load find part from incomplete UID operation (applied to shorten projectiles net message and retrieve the uid when receiving the feedback on the client) try unreliable net messages for some of the heavier operations restructured net_combat.lua : isolate each part's net declarators pac_sv_block_combat_features_on_next_restart convar stops loading network strings and net receivers for the combat parts' server cvars 0=don't block 1=block the ones that are disabled 2=block all new combat parts pac_sv_combat_reinitialize_missing_receivers command to declare them again skeleton of ideas for net message limiting enforcement enforce_netrate = milliseconds delay between net messages for each client enforce_netrate_buffer = buffer size (bit units or count? to be decided) like a budget that recharges over time and is consumed when sending net messages --- lua/pac3/core/client/part_pool.lua | 26 + lua/pac3/core/client/parts/damage_zone.lua | 146 +- lua/pac3/core/client/parts/event.lua | 64 +- lua/pac3/core/client/parts/force.lua | 76 +- .../core/client/parts/health_modifier.lua | 28 +- lua/pac3/core/client/parts/hitscan.lua | 182 +- lua/pac3/core/client/parts/lock.lua | 48 +- lua/pac3/core/client/parts/projectile.lua | 110 +- lua/pac3/core/server/net_messages.lua | 12 +- lua/pac3/editor/client/parts.lua | 14 +- lua/pac3/editor/client/saved_parts.lua | 10 +- lua/pac3/editor/client/settings.lua | 743 ++++++- lua/pac3/editor/server/combat_bans.lua | 163 +- lua/pac3/extra/shared/net_combat.lua | 1707 ++++++++++------- lua/pac3/extra/shared/projectiles.lua | 99 +- 15 files changed, 2413 insertions(+), 1015 deletions(-) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index d8da6b2f1..b1c928763 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -502,6 +502,32 @@ function pac.GetPartFromUniqueID(owner_id, id) return uid_parts[owner_id] and uid_parts[owner_id][id] or NULL end +function pac.FindPartByPartialUniqueID(owner_id, crumb) + if #crumb <= 3 then return NULL end + if not crumb then return NULL end + local closest_match + local length_of_closest_match = 0 + if uid_parts[owner_id] then + if uid_parts[owner_id][crumb] then + return uid_parts[owner_id][crumb] + end + + for _, part in pairs(uid_parts[owner_id]) do + local start_i,end_i = string.find(part.UniqueID, crumb) + if start_i or end_i then + closest_match = part + if length_of_closest_match < end_i - start_i + 1 then + closest_match = part + length_of_closest_match = end_i - start_i + 1 + end + + end + end + + end + return closest_match or NULL +end + function pac.FindPartByName(owner_id, str, exclude) if uid_parts[owner_id] then if uid_parts[owner_id][str] then diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 7ee2c4755..2306fd5f5 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -3,8 +3,8 @@ local BUILDER, PART = pac.PartTemplate("base_movable") --ultrakill parryables: club, slash, buckshot PART.ClassName = "damage_zone" -PART.Group = 'advanced' -PART.Icon = 'icon16/package.png' +PART.Group = "advanced" +PART.Icon = "icon16/package.png" local renderhooks = { "PostDraw2DSkyBox", @@ -29,8 +29,8 @@ BUILDER:StartStorableVars() :GetSet("NPC",true) :GetSet("PointEntities",true, {description = "Other source engine entities such as item_item_crate and prop_physics"}) :SetPropertyGroup("Shape and Sampling") - :GetSet("Radius", 20) - :GetSet("Length", 50) + :GetSet("Radius", 20, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) + :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) :GetSet("HitboxMode", "Box", {enums = { ["Box"] = "Box", ["Cube"] = "Cube", @@ -43,13 +43,13 @@ BUILDER:StartStorableVars() ["Cone (From Spheres)"] = "ConeSpheres", ["Ray"] = "Ray" }}) - :GetSet("Detail", 20) - :GetSet("ExtraSteps",0) - :GetSet("RadialRandomize", 1) - :GetSet("PhaseRandomize", 1) + :GetSet("Detail", 20, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32,31)) end}) + :GetSet("ExtraSteps",0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-8,7)) end}) + :GetSet("RadialRandomize", 1, {editor_onchange = function(self,num) return math.Clamp(num,-8,7) end}) + :GetSet("PhaseRandomize", 1, {editor_onchange = function(self,num) return math.Clamp(num,-8,7) end}) :SetPropertyGroup("Falloff") :GetSet("DamageFalloff", false) - :GetSet("DamageFalloffPower", 1) + :GetSet("DamageFalloffPower", 1, {editor_onchange = function(self,num) return math.Clamp(num,-64,63) end}) :SetPropertyGroup("Preview Rendering") :GetSet("Preview", false) :GetSet("RenderingHook", "PostDrawOpaqueRenderables", {enums = { @@ -68,7 +68,7 @@ BUILDER:StartStorableVars() }}) :SetPropertyGroup("DamageInfo") :GetSet("Bullet", false, {description = "Fires a bullet on each target for the added hit decal"}) - :GetSet("Damage", 0) + :GetSet("Damage", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,268435455)) end}) :GetSet("DamageType", "generic", {enums = { generic = 0, --generic damage crush = 1, --caused by physics interaction @@ -114,7 +114,7 @@ BUILDER:StartStorableVars() armor = -1, }}) :GetSet("DoNotKill",false, {description = "Will only damage to as low as critical health"}) - :GetSet("CriticalHealth",1) + :GetSet("CriticalHealth",1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,65535)) end}) :SetPropertyGroup("HitOutcome") :GetSetPart("HitSoundPart") :GetSetPart("KillSoundPart") @@ -366,7 +366,104 @@ local function RecursedHitmarker(part) end + +--NOT THE ACTUAL DAMAGE TYPES. UNIQUE IDS TO COMPRESS NET MESSAGES +local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + fire = 31, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 32, + dissolve_heavy_electrical = 33, + dissolve_light_electrical = 34, + dissolve_core_effect = 35, + + heal = 36, + armor = 37, +} + +local hitbox_ids = { + ["Box"] = 1, + ["Cube"] = 2, + ["Sphere"] = 3, + ["Cylinder"] = 4, + ["CylinderHybrid"] = 5, + ["CylinderSpheres"] = 6, + ["Cone"] = 7, + ["ConeHybrid"] = 8, + ["ConeSpheres"] = 9, + ["Ray"] = 10 +} + +--more compressed net message +function PART:SendNetMessage() + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + if not GetConVar('pac_sv_damage_zone'):GetBool() then return end + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then return end + end + + if not GetConVar("pac_sv_combat_enforce_netrate"):GetInt() == 0 then return end + + net.Start("pac_request_zone_damage", true) + + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + net.WriteUInt(self.Damage, 28) + net.WriteInt(self.Length, 16) + net.WriteInt(self.Radius, 16) + net.WriteBool(self.AffectSelf) + net.WriteBool(self.NPC) + net.WriteBool(self.Players) + net.WriteBool(self.PointEntities) + net.WriteUInt(hitbox_ids[self.HitboxMode] or 1,5) + net.WriteUInt(damage_ids[self.DamageType] or 0,7) + net.WriteInt(self.Detail,6) + net.WriteInt(self.ExtraSteps,4) + net.WriteInt(math.floor(math.Clamp(8*self.RadialRandomize,-64, 63)), 7) + net.WriteInt(math.floor(math.Clamp(8*self.PhaseRandomize,-64, 63)), 7) + net.WriteBool(self.DamageFalloff) + net.WriteInt(math.floor(math.Clamp(8*self.DamageFalloffPower,-512, 511)), 12) + net.WriteBool(self.Bullet) + net.WriteBool(self.DoNotKill) + net.WriteUInt(self.CriticalHealth, 16) + net.WriteBool(self.RemoveNPCWeaponsOnKill) + net.SendToServer() +end + function PART:OnShow() + if self.Preview then self:PreviewHitbox() end @@ -380,32 +477,13 @@ function PART:OnShow() if self.Preview then self:PreviewHitbox() end - if pac.LocalPlayer ~= self:GetPlayerOwner() then return end - local tbl = {} - for key in pairs(self:GetStorableVars()) do - tbl[key] = self[key] - end - net.Start("pac_request_zone_damage") - net.WriteVector(self:GetWorldPosition()) - net.WriteAngle(self:GetWorldAngles()) - net.WriteTable(tbl) - net.WriteEntity(self:GetPlayerOwner()) - net.SendToServer() + + self:SendNetMessage() end) elseif (self.validTime > SysTime()) then return else - if pac.LocalPlayer ~= self:GetPlayerOwner() then return end - local tbl = {} - for key in pairs(self:GetStorableVars()) do - tbl[key] = self[key] - end - net.Start("pac_request_zone_damage") - net.WriteVector(self:GetWorldPosition()) - net.WriteAngle(self:GetWorldAngles()) - net.WriteTable(tbl) - net.WriteEntity(self:GetPlayerOwner()) - net.SendToServer() + self:SendNetMessage() end net.Receive("pac_hit_results", function() @@ -837,6 +915,8 @@ function PART:BuildCone(obj) end function PART:Initialize() + + 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() + 2 end diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index fa7fa3c29..a99d76542 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2542,8 +2542,7 @@ do ply.pac_buttons = ply.pac_buttons or {} ply.pac_buttons[key] = down - --print("button update from" , ply, key, down) - --PrintTable(ply.pac_buttons) + ply.pac_broadcasted_buttons_lastpressed = ply.pac_broadcasted_buttons_lastpressed or {} if down then ply.pac_broadcasted_buttons_lastpressed[key] = SysTime() @@ -2555,6 +2554,7 @@ do part.holdtime = part.holdtime or 0 part.toggleimpulsekey = part.toggleimpulsekey or {} part.toggleimpulsekey[key] = down + part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime end end @@ -2654,32 +2654,32 @@ do end, } - PART.OldEvents.input = { - operator_type = "none", - arguments = {{UserInputs = "string"}, {RequireAllInputs = "boolean"}}, - userdata = {{enums = function() - return base_input_enums_names - end}}, - callback = function(self, ent, UserInputs, RequireAllInputs) - local ply = self:GetPlayerOwner() - UserInputs = UserInputs or "" - pac.player_inputs[ply] = pac.player_inputs[ply] or {} - local detect = false - local fulldetect = true - local input_list = string.Split(UserInputs, ";") - - for i,v in pairs(pac.player_inputs[ply]) do - for _,v2 in pairs(input_list) do - if pac.player_inputs[ply][input_aliases[v2]] then detect = true - else fulldetect = false end - end - end - if RequireAllInputs then return fulldetect - else return detect end - end - } - - PART.OldEvents.is_moving = { + --PART.OldEvents.input = { + -- operator_type = "none", + -- arguments = {{UserInputs = "string"}, {RequireAllInputs = "boolean"}}, + -- userdata = {{enums = function() + -- return base_input_enums_names + -- end}}, + -- callback = function(self, ent, UserInputs, RequireAllInputs) + -- local ply = self:GetPlayerOwner() + -- UserInputs = UserInputs or "" + -- pac.player_inputs[ply] = pac.player_inputs[ply] or {} + -- local detect = false + -- local fulldetect = true + -- local input_list = string.Split(UserInputs, ";") + -- + -- for i,v in pairs(pac.player_inputs[ply]) do + -- for _,v2 in pairs(input_list) do + -- if pac.player_inputs[ply][input_aliases[v2]] then detect = true + -- else fulldetect = false end + -- end + -- end + -- if RequireAllInputs then return fulldetect + -- else return detect end + -- end + --} + + --[[PART.OldEvents.is_moving = { operator_type = "none", callback = function(self) local ply = self:GetPlayerOwner() @@ -2691,9 +2691,9 @@ do pac.player_inputs[ply][IN_MOVERIGHT] or pac.player_inputs[ply][IN_JUMP] end - } + }]] - PART.OldEvents.afk = { + --[[PART.OldEvents.afk = { operator_type = "none", arguments = {{time = "number"}, {IncludeEyeAngles = "boolean"}}, callback = function(self, ent, time, IncludeEyeAngles) @@ -2724,7 +2724,7 @@ do end return true end - } + }]] end @@ -4091,4 +4091,4 @@ net.Receive("pac_update_healthbars", function() end end -end) +end) \ No newline at end of file diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 5341399a9..d0002e796 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -1,8 +1,8 @@ local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "force" -PART.Group = 'advanced' -PART.Icon = 'icon16/database_go.png' +PART.Group = "advanced" +PART.Icon = "icon16/database_go.png" PART.ManualDraw = true PART.HandleModifiersManually = true @@ -17,8 +17,8 @@ BUILDER:StartStorableVars() ["Cone"] = "Cone", ["Ray"] = "Ray" }}) - :GetSet("Length",50) - :GetSet("Radius",50) + :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) + :GetSet("Radius", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) :GetSet("Preview",false) :SetPropertyGroup("Forces") :GetSet("BaseForce", 0) @@ -38,12 +38,17 @@ BUILDER:StartStorableVars() :GetSet("NPC",false) :EndStorableVars() +local force_hitbox_ids = {["Box"] = 0,["Cube"] = 1,["Sphere"] = 2,["Cylinder"] = 3,["Cone"] = 4,["Ray"] = 5} +local base_force_mode_ids = {["Radial"] = 0, ["Locus"] = 1, ["Local"] = 2} +local vect_force_mode_ids = {["Global"] = 0, ["Local"] = 1, ["Radial"] = 2, ["RadialNoPitch"] = 3} +local ang_torque_mode_ids = {["Global"] = 0, ["TargetLocal"] = 1, ["Local"] = 2, ["Radial"] = 3} function PART:OnRemove() end function PART:Initialize() self.next_impulse = CurTime() + 0.05 + if not GetConVar("pac_sv_force"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("force parts are disabled on this server!") end end function PART:OnShow() @@ -136,39 +141,50 @@ function PART:OnThink() end function PART:Impulse(on) - self.next_impulse = CurTime() + 0.05 - local tbl = {} - tbl.HitboxMode = self.HitboxMode - tbl.Length = self.Length - tbl.Radius = self.Radius - tbl.BaseForce = self.BaseForce - tbl.BaseForceAngleMode = self.BaseForceAngleMode - tbl.AddedVectorForce = self.AddedVectorForce - tbl.VectorForceAngleMode = self.VectorForceAngleMode - tbl.Torque = self.Torque - tbl.TorqueMode = self.TorqueMode - tbl.Continuous = self.Continuous - tbl.AccountMass = self.AccountMass - tbl.Falloff = self.Falloff - tbl.AffectSelf = self.AffectSelf - tbl.Players = self.Players - tbl.PhysicsProps = self.PhysicsProps - tbl.NPC = self.NPC - - tbl.UniqueID = self.UniqueID - tbl.PlayerOwner = self:GetPlayerOwner() - tbl.RootPartOwner = self:GetRootPart():GetOwner() + self.next_impulse = CurTime() + 0.1 + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + if not on and not self.Continuous then return end + if not GetConVar("pac_sv_force"):GetBool() then return end + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then return end + end + local locus_pos = Vector(0,0,0) if self.Locus ~= nil then if self.Locus:IsValid() then - tbl.Locus_pos = self.Locus:GetWorldPosition() + locus_pos = self.Locus:GetWorldPosition() end - else tbl.Locus_pos = self:GetWorldPosition() end - net.Start("pac_request_force") - net.WriteTable(tbl) + else locus_pos = self:GetWorldPosition() end + + + net.Start("pac_request_force", true) net.WriteVector(self:GetWorldPosition()) net.WriteAngle(self:GetWorldAngles()) + net.WriteVector(locus_pos) net.WriteBool(on) + + net.WriteString(string.sub(self.UniqueID,1,12)) + net.WriteEntity(self:GetRootPart():GetOwner()) + + net.WriteUInt(force_hitbox_ids[self.HitboxMode],4) + net.WriteUInt(base_force_mode_ids[self.BaseForceAngleMode],3) + net.WriteUInt(vect_force_mode_ids[self.VectorForceAngleMode],2) + net.WriteUInt(ang_torque_mode_ids[self.TorqueMode],2) + + net.WriteInt(self.Length, 16) + net.WriteInt(self.Radius, 16) + + net.WriteInt(self.BaseForce, 18) + net.WriteVector(self.AddedVectorForce) + net.WriteVector(self.Torque) + + net.WriteBool(self.Continuous) + net.WriteBool(self.AccountMass) + net.WriteBool(self.Falloff) + net.WriteBool(self.AffectSelf) + net.WriteBool(self.Players) + net.WriteBool(self.PhysicsProps) + net.WriteBool(self.NPC) net.SendToServer() end diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 4d899ea43..9b771b1ad 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -2,8 +2,8 @@ local BUILDER, PART = pac.PartTemplate("base") PART.ClassName = "health_modifier" -PART.Group = 'advanced' -PART.Icon = 'icon16/heart.png' +PART.Group = "advanced" +PART.Icon = "icon16/heart.png" BUILDER:StartStorableVars() @@ -33,6 +33,12 @@ BUILDER:EndStorableVars() function PART:SendModifier(str) if self:IsHidden() then return end + if LocalPlayer() ~= self:GetPlayerOwner() then return end + if not GetConVar("pac_sv_health_modifier"):GetBool() then return end + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then return end + end + if str == "MaxHealth" and self.ChangeHealth then net.Start("pac_request_healthmod") net.WriteString(self.UniqueID) @@ -79,42 +85,42 @@ end function PART:SetHealthBars(val) self.HealthBars = val - if LocalPlayer() ~= self:GetPlayerOwner() then return end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("HealthBars") end function PART:SetBarsAmount(val) self.BarsAmount = val - if LocalPlayer() ~= self:GetPlayerOwner() then return end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("HealthBars") end function PART:SetBarsLayer(val) self.BarsLayer = val - if LocalPlayer() ~= self:GetPlayerOwner() then return end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("HealthBars") end function PART:SetMaxHealth(val) self.MaxHealth = val - if LocalPlayer() ~= self:GetPlayerOwner() then return end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("MaxHealth") end function PART:SetMaxArmor(val) self.MaxArmor = val - if LocalPlayer() ~= self:GetPlayerOwner() then return end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("MaxArmor") end function PART:SetDamageMultiplier(val) self.DamageMultiplier = val - if LocalPlayer() ~= self:GetPlayerOwner() then return end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("DamageMultiplier") end function PART:OnRemove() - if LocalPlayer() ~= self:GetPlayerOwner() then return end + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end local found_remaining_healthmod = false for _,part in pairs(pac.GetLocalParts()) do if part.ClassName == "health_modifier" and part ~= self then @@ -152,4 +158,8 @@ function PART:OnShow() self:SendModifier("all") end +function PART:Initialize() + if not GetConVar("pac_sv_health_modifier"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("health modifiers are disabled on this server!") end +end + BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index 5c712df14..f9ae0e939 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -6,19 +6,19 @@ language.Add("pac_hitscan", "Hitscan") local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "hitscan" -PART.Group = 'advanced' -PART.Icon = 'icon16/user_gray.png' +PART.Group = "advanced" +PART.Icon = "icon16/user_gray.png" BUILDER:StartStorableVars() - :GetSet("ServerBullets",true, {description = "serverside bullets can do damage and exert a physical impact force"}) + :GetSet("ServerBullets", true, {description = "serverside bullets can do damage and exert a physical impact force"}) :SetPropertyGroup("bullet properties") :GetSet("BulletImpact", false) - :GetSet("Damage", 0) - :GetSet("Force",1000) + :GetSet("Damage", 0, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,268435455)) end}) + :GetSet("Force",1000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end}) + :GetSet("AffectSelf", false, {description = "whether to allow to damage yourself"}) :GetSet("DamageFalloff", false, {description = "enable damage falloff. The lowest damage is not a fixed damage number, but a fraction of the total initial damage.\nThe server can still restrict the maximum distance of all bullets"}) - :GetSet("DamageFalloffDistance", 5000) - :GetSet("DamageFalloffFraction", 0.5) - + :GetSet("DamageFalloffDistance", 5000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end}) + :GetSet("DamageFalloffFraction", 0.5, {editor_clamp = {0,1}}) :GetSet("DamageType", "generic", {enums = { generic = 0, --generic damage crush = 1, --caused by physics interaction @@ -63,15 +63,15 @@ BUILDER:StartStorableVars() heal = -1, armor = -1, - } - }) + } + }) :GetSet("Spread", 0) :GetSet("SpreadX", 1) :GetSet("SpreadY", 1) - :GetSet("NumberBullets", 1) + :GetSet("NumberBullets", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,511)) end}) :GetSet("DistributeDamage", false, {description = "whether or not the damage should be divided equally to all bullets in NumberBullets.\nThe server can still force multi-shots to do that"}) - :GetSet("TracerSparseness", 1) - :GetSet("MaxDistance", 10000) + :GetSet("TracerSparseness", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,255)) end}) + :GetSet("MaxDistance", 10000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end}) :GetSet("TracerName", "Tracer", {enums = { ["Default bullet tracer"] = "Tracer", ["AR2 pulse-rifle tracer"] = "AR2Tracer", @@ -90,15 +90,11 @@ BUILDER:EndStorableVars() function PART:Initialize() self.bulletinfo = {} - self.ent = self:GetRootPart():GetOwner() - self:UpdateBulletInfo() + if not GetConVar("pac_sv_hitscan"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("hitscan parts are disabled on this server!") end end function PART:OnShow() - self:UpdateBulletInfo() - --self:GetWorldPosition() - --self:GetWorldAngles() - self:Shoot(self:GetDrawPosition()) + self:Shoot() end function PART:OnDraw() @@ -106,58 +102,126 @@ function PART:OnDraw() self:GetWorldAngles() end -function PART:Shoot(pos, ang) - if not self.ent then self:UpdateBulletInfo() end - if not IsValid(self.ent) then return end - - self.bulletinfo.Src = pos - self.bulletinfo.Dir = ang:Forward() - self.bulletinfo.Spread = Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0) - +function PART:Shoot() if self.NumberBullets == 0 then return end if self.ServerBullets and self.Damage ~= 0 then - --print("WE NEED A BULLET IN THE SERVER!") - --PrintTable(self.bulletinfo) - - net.Start("pac_hitscan") - net.WriteEntity(self:GetRootPart():GetOwner()) - net.WriteTable(self.bulletinfo) - net.WriteAngle(ang) - net.WriteString(self.UniqueID) - net.SendToServer() + self:SendNetMessage() else - self.ent:FireBullets(self.bulletinfo) + self.bulletinfo.Attacker = self:GetRootPart():GetOwner() + self.ent = self:GetRootPart():GetOwner() + if self.Damage ~= 0 then self.bulletinfo.Damage = self.Damage end + + self.bulletinfo.Src = self:GetWorldPosition() + self.bulletinfo.Dir = self:GetWorldAngles():Forward() + self.bulletinfo.Spread = Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0) + + self.bulletinfo.Force = self.Force + self.bulletinfo.Distance = self.MaxDistance + self.bulletinfo.Num = self.NumberBullets + self.bulletinfo.Tracer = self.TracerSparseness --tracer every x bullets + self.bulletinfo.TracerName = self.TracerName + self.bulletinfo.DistributeDamage = self.DistributeDamage + + self.bulletinfo.DamageFalloff = self.DamageFalloff + self.bulletinfo.DamageFalloffDistance = self.DamageFalloffDistance + self.bulletinfo.DamageFalloffFraction = self.DamageFalloffFraction + + if IsValid(self.ent) then self.ent:FireBullets(self.bulletinfo) end end end -function PART:UpdateBulletInfo() - self.bulletinfo.Attacker = self:GetRootPart():GetOwner() - self.ent = self:GetRootPart():GetOwner() - if self.Damage == 0 then - else self.bulletinfo.Damage = self.Damage end - self.bulletinfo.Tracer = self.TracerSparseness +--NOT THE ACTUAL DAMAGE TYPES. UNIQUE IDS TO COMPRESS NET MESSAGES +local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + fire = 31, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 32, + dissolve_heavy_electrical = 33, + dissolve_light_electrical = 34, + dissolve_core_effect = 35, + + heal = 36, + armor = 37, +} + +local tracer_ids = { + ["Tracer"] = 1, + ["AR2Tracer"] = 2, + ["HelicopterTracer"] = 3, + ["AirboatGunTracer"] = 4, + ["AirboatGunHeavyTracer"] = 5, + ["GaussTracer"] = 6, + ["HunterTracer"] = 7, + ["StriderTracer"] = 8, + ["GunshipTracer"] = 9, + ["ToolgunTracer"] = 10, + ["LaserTracer"] = 11 +} + + +function PART:SendNetMessage() + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + if not GetConVar('pac_sv_hitscan'):GetBool() then return end + if pac.Blocked_Combat_Parts[self.ClassName] then + return + end - self.bulletinfo.Force = self.Force - self.bulletinfo.Distance = self.MaxDistance - self.bulletinfo.Num = self.NumberBullets - self.bulletinfo.Tracer = self.TracerSparseness --tracer every x bullets - self.bulletinfo.TracerName = self.TracerName - self.bulletinfo.DistributeDamage = self.DistributeDamage + net.Start("pac_hitscan", true) + net.WriteBool(self.AffectSelf) + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) - self.bulletinfo.DamageFalloff = self.DamageFalloff - self.bulletinfo.DamageFalloffDistance = self.DamageFalloffDistance - self.bulletinfo.DamageFalloffFraction = self.DamageFalloffFraction + net.WriteUInt(damage_ids[self.DamageType] or 0,7) + net.WriteVector(Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0)) + net.WriteUInt(self.Damage, 28) + net.WriteUInt(self.TracerSparseness, 8) + net.WriteUInt(self.Force, 16) + net.WriteUInt(self.MaxDistance, 16) + net.WriteUInt(self.NumberBullets, 9) + net.WriteUInt(tracer_ids[self.TracerName], 4) + net.WriteBool(self.DistributeDamage) - --[[bulletinfo.ammodata = { - name = "" - dmgtype = self.DamageType - tracer = TRACER_LINE_AND_WHIZ - maxcarry = -2 + net.WriteBool(self.DamageFalloff) + net.WriteUInt(self.DamageFalloffDistance, 16) + net.WriteUInt(math.Clamp(math.floor(self.DamageFalloffFraction * 1000),0, 1000), 10) - }]]-- -end + net.WriteString(string.sub(self.UniqueID,1,8)) + net.SendToServer() +end BUILDER:Register() diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 84e5c78f5..58f80503a 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -23,8 +23,8 @@ local physics_point_ent_classes = { local BUILDER, PART = pac.PartTemplate("base_movable") PART.ClassName = "lock" -PART.Group = 'advanced' -PART.Icon = 'icon16/lock.png' +PART.Group = "advanced" +PART.Icon = "icon16/lock.png" BUILDER:StartStorableVars() @@ -61,6 +61,10 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:OnThink() + if not GetConVar('pac_sv_lock'):GetBool() then return end + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then return end + end if self.forcebreak then if self.next_allowed_grab < CurTime() then --we're able to resume if self.ContinuousSearch then @@ -74,7 +78,12 @@ function PART:OnThink() end if self.Mode == "Grab" then - + if not GetConVar('pac_sv_lock_grab'):GetBool() then return end + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then + return + end + end if self.ContinuousSearch then self:DecideTarget() end @@ -110,7 +119,7 @@ function PART:OnThink() local relative_offset_pos = offset_matrix:GetTranslation() local relative_offset_ang = offset_matrix:GetAngles() - if LocalPlayer() == self:GetPlayerOwner() then + if pac.LocalPlayer == self:GetPlayerOwner() then net.Start("pac_request_position_override_on_entity_grab") net.WriteBool(self.is_first_time) net.WriteString(self.UniqueID) @@ -127,7 +136,7 @@ function PART:OnThink() if self.target_ent:IsPlayer() then if self.OverrideEyeAngles then try_override_eyeang = true end end - if LocalPlayer() == self:GetPlayerOwner() then + if pac.LocalPlayer == self:GetPlayerOwner() then net.WriteBool(self.OverrideAngles) net.WriteBool(try_override_eyeang) net.WriteEntity(self.target_ent) @@ -281,6 +290,7 @@ function PART:SetRadius(val) end function PART:OnShow() + local origin_part self.is_first_time = true if self.resetting_condition or self.forcebreak then @@ -295,7 +305,7 @@ function PART:OnShow() else origin_part = self end - if origin_part == nil or not self.Preview or LocalPlayer() ~= self:GetPlayerOwner() then return end + if origin_part == nil or not self.Preview or pac.LocalPlayer ~= self:GetPlayerOwner() then return end local sv_dist = GetConVar("pac_sv_lock_max_grab_radius"):GetInt() render.DrawLine(origin_part:GetWorldPosition(),origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount),Color(255,255,255)) @@ -312,12 +322,13 @@ function PART:OnShow() end) if self.Mode == "Teleport" then + if not GetConVar('pac_sv_lock_teleport'):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then return end self.target_ent = nil local ang_yaw_only = self:GetWorldAngles() ang_yaw_only.p = 0 ang_yaw_only.r = 0 - if LocalPlayer() == self:GetPlayerOwner() then + if pac.LocalPlayer == self:GetPlayerOwner() then local teleport_pos_final = self:GetWorldPosition() @@ -360,7 +371,7 @@ function PART:reset_ent_ang() if reset_ent:IsValid() then timer.Simple(math.min(self.RestoreDelay,5), function() - if LocalPlayer() == self:GetPlayerOwner() then + if pac.LocalPlayer == self:GetPlayerOwner() then net.Start("pac_request_angle_reset_on_entity") net.WriteAngle(Angle(0,0,0)) net.WriteFloat(self.RestoreDelay) @@ -438,7 +449,7 @@ function PART:DecideTarget() if chosen_ent ~= nil then self.target_ent = chosen_ent - if LocalPlayer() == self:GetPlayerOwner() then + if pac.LocalPlayer == self:GetPlayerOwner() then print("selected ", chosen_ent, "dist ", (chosen_ent:GetPos()):Distance( self:GetWorldPosition() )) end self.valid_ent = true @@ -474,4 +485,23 @@ function PART:CalculateRelativeOffset() --print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles()) end +function PART:Initialize() + + if not GetConVar('pac_sv_lock_grab'):GetBool() then + if not GetConVar('pac_sv_lock_teleport'):GetBool() then + self:SetWarning("lock part grabs and teleports are disabled on this server!") + else + self:SetWarning("lock part grabs are disabled on this server!") + end + end + if not GetConVar('pac_sv_lock_teleport'):GetBool() then + if not GetConVar('pac_sv_lock_grab'):GetBool() then + self:SetWarning("lock part grabs and teleports are disabled on this server!") + else + self:SetWarning("lock part teleports are disabled on this server!") + end + end + if not GetConVar('pac_sv_lock'):GetBool() then self:SetError("lock parts are disabled on this server!") end +end + BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index a42e9d6ef..e93000f83 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -1,6 +1,6 @@ local physprop_enums = {} local physprop_indices = {} -for i=0,500,1 do +for i=0,200,1 do local name = util.GetSurfacePropName(i) if name ~= "" then physprop_enums[name] = name @@ -15,8 +15,8 @@ language.Add("pac_projectile", "Projectile") local BUILDER, PART = pac.PartTemplate("base_movable") PART.ClassName = "projectile" -PART.Group = 'advanced' -PART.Icon = 'icon16/bomb.png' +PART.Group = "advanced" +PART.Icon = "icon16/bomb.png" BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("Firing") @@ -184,6 +184,57 @@ end local enable = CreateClientConVar("pac_sv_projectiles", 0, true) +local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + explosion = 31, -- ent:Ignite(5) + fire = 32, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 33, + dissolve_heavy_electrical = 34, + dissolve_light_electrical = 35, + dissolve_core_effect = 36, + + heal = 37, + armor = 38, +} +local attract_ids = { + hitpos = 0, + hitpos_radius = 1, + closest_to_projectile = 2, + closest_to_hitpos = 3, +} function PART:Shoot(pos, ang, multi_projectile_count) local physics = self.Physical local multi_projectile_count = multi_projectile_count or 1 @@ -192,15 +243,53 @@ function PART:Shoot(pos, ang, multi_projectile_count) if pac.LocalPlayer ~= self:GetPlayerOwner() then return end local tbl = {} - for key in pairs(self:GetStorableVars()) do - tbl[key] = self[key] - end - net.Start("pac_projectile") + net.Start("pac_projectile",true) net.WriteUInt(multi_projectile_count,7) net.WriteVector(pos) net.WriteAngle(ang) - net.WriteTable(tbl) + + --bools + net.WriteBool(self.Sphere) + net.WriteBool(self.RemoveOnCollide) + net.WriteBool(self.CollideWithOwner) + net.WriteBool(self.RemoveOnHide) + net.WriteBool(self.OverridePhysMesh) + net.WriteBool(self.Gravity) + net.WriteBool(self.AddOwnerSpeed) + net.WriteBool(self.Collisions) + net.WriteBool(self.CollideWithSelf) + net.WriteBool(self.AimDir) + net.WriteBool(self.DrawShadow) + net.WriteBool(self.Sticky) + net.WriteBool(self.BulletImpact) + + --vectors + net.WriteVector(self.RandomAngleVelocity) + net.WriteVector(self.LocalAngleVelocity) + + --strings + net.WriteString(self.OverridePhysMesh and string.sub(string.gsub(self.FallbackSurfpropModel, "^models/", ""),1,150) or "") --custom model is an unavoidable string + net.WriteString(string.sub(self.UniqueID,1,12)) --long string but we can probably truncate it + net.WriteUInt(physprop_indices[self.SurfaceProperties],10) + net.WriteUInt(damage_ids[self.DamageType],7) + net.WriteUInt(attract_ids[self.AttractMode],3) + + --numbers + net.WriteUInt(self.Radius,8) + net.WriteUInt(self.DamageRadius,10) + net.WriteUInt(self.Damage,24) + net.WriteUInt(1000*self.Speed,16) + net.WriteUInt(self.Maximum,7) + net.WriteUInt(100*self.LifeTime,14) --might need decimals + net.WriteUInt(100*self.Delay,9) --might need decimals + net.WriteUInt(self.Mass,18) + net.WriteInt(100*self.Spread,10) + net.WriteInt(100*self.Damping,20) --might need decimals + net.WriteInt(self.Attract,14) + net.WriteUInt(self.AttractRadius,10) + net.WriteInt(100*self.Bounce,8) --might need decimals + net.SendToServer() else self.projectiles = self.projectiles or {} @@ -397,13 +486,13 @@ do -- physical local ent = Entity(data.ent_id) if ent:IsValid() and ent:GetClass() == "pac_projectile" then - local part = pac.GetPartFromUniqueID(pac.Hash(data.ply), data.partuid) + local part = pac.FindPartByPartialUniqueID(pac.Hash(data.ply), data.partuid) if part:IsValid() and part:GetPlayerOwner() == data.ply then part:AttachToEntity(ent, true) end projectiles[key] = nil end - + ::CONTINUE:: end end) @@ -413,7 +502,6 @@ do -- physical local ent_id = net.ReadInt(16) local partuid = net.ReadString() local surfprop = net.ReadString() - if ply:IsValid() then table.insert(projectiles, {ply = ply, ent_id = ent_id, partuid = partuid}) diff --git a/lua/pac3/core/server/net_messages.lua b/lua/pac3/core/server/net_messages.lua index b4c04c0cf..235b07ea0 100644 --- a/lua/pac3/core/server/net_messages.lua +++ b/lua/pac3/core/server/net_messages.lua @@ -37,7 +37,7 @@ do -- button event end) end -do -- input event +--[[do -- input event local input_enums = { IN_ATTACK, --1 IN_JUMP, --2 @@ -70,10 +70,10 @@ do -- input event local last_input_broadcast = 0 local player_last_input_broadcast_times = {} - local function broadcast_inputs(ply, update) + local function broadcast_inputs(update) - if not update and not (last_input_broadcast + 0.05 < CurTime()) then return - else + if not update or not (last_input_broadcast + 0.05 < CurTime()) then return + elseif update net.Start("pac.BroadcastPlayerInputs") net.WriteTable(pac_broadcast_inputs) net.WriteTable(player_last_input_broadcast_times) @@ -112,7 +112,7 @@ do -- input event broadcast_inputs(update) end) -end +end]] do --is_using_entity local function send_player_used_object(client, ent, class, b, from_client) @@ -168,4 +168,4 @@ do --damage attribution end) end) -end +end \ No newline at end of file diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 9a0931000..d302371eb 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -182,8 +182,6 @@ function pace.OnPartSelected(part, is_selecting) else last_select_was_span = false end last_span_select_part = part - - local parent = part:GetRootPart() if parent:IsValid() and (parent.OwnerName == "viewmodel" or parent.OwnerName == "hands") then pace.editing_viewmodel = parent.OwnerName == "viewmodel" @@ -198,11 +196,9 @@ function pace.OnPartSelected(part, is_selecting) pace.mctrl.SetTarget(part) pace.SetViewPart(part) - if pace.Editor:IsValid() then pace.Editor:InvalidateLayout() end - pace.SafeRemoveSpecialPanel() if pace.tree:IsValid() then @@ -789,9 +785,13 @@ do -- menu if str == obj.ClassName then return end if str == "model" then str = "model2" end --I don't care, stop using legacy local uid = obj.UniqueID - pace.RefreshTree() - pace.Editor:InvalidateLayout() - pace.RefreshTree() + + if pace.Editor:IsValid() then + pace.RefreshTree() + pace.Editor:InvalidateLayout() + pace.RefreshTree() + end + obj.ClassName = str diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index dd4de907b..8ab22522e 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -233,9 +233,15 @@ function pace.LoadParts(name, clear, override_part) local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") local has_possible_prop_pacs = false - for i,part in pairs(data) do - if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end + + if IsValid(data) then + if istable(data) then + for i,part in pairs(data) do + if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end + end + end end + --queue up prop pacs for the next prop or npc you spawn when in singleplayer if (auto_spawn_prop:GetInt() == 2 or (auto_spawn_prop:GetInt() == 1 and #data == 1)) and game.SinglePlayer() and has_possible_prop_pacs then diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 6cf13bba5..3f9c4ab59 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1,11 +1,276 @@ include("parts.lua") include("shortcuts.lua") -if SERVER then - include("pac3/editor/server/combat_bans.lua") -end + +local pac_submit_spam = CreateConVar('pac_submit_spam', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'Prevent users from spamming pac_submit') +local pac_submit_limit = CreateConVar('pac_submit_limit', '30', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'pac_submit spam limit') +local hitscan_allow = CreateConVar('pac_sv_hitscan', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow hitscan parts serverside') +local hitscan_max_bullets = CreateConVar('pac_sv_hitscan_max_bullets', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum number of bullets') +local hitscan_max_damage = CreateConVar('pac_sv_hitscan_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum damage') +local hitscan_spreadout_dmg = CreateConVar('pac_sv_hitscan_divide_max_damage_by_max_bullets', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether or not force hitscans to divide their damage among the number of bullets fired') + +local damagezone_allow = CreateConVar('pac_sv_damage_zone', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow damage zone parts serverside') +local damagezone_max_damage = CreateConVar('pac_sv_damage_zone_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum damage') +local damagezone_max_length = CreateConVar('pac_sv_damage_zone_max_length', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum length') +local damagezone_max_radius = CreateConVar('pac_sv_damage_zone_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum radius') +local damagezone_allow_dissolve = CreateConVar('pac_sv_damage_zone_allow_dissolve', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to enable entity dissolvers and removing NPCs\' weapons on death for damagezone') + +local lock_allow = CreateConVar('pac_sv_lock', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock parts serverside') +local lock_allow_grab = CreateConVar('pac_sv_lock_grab', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part grabs serverside') +local lock_allow_teleport = CreateConVar('pac_sv_lock_teleport', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part teleports serverside') +local lock_max_radius = CreateConVar('pac_sv_lock_max_grab_radius', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'lock part maximum grab radius') +local lock_allow_grab_ply = CreateConVar('pac_sv_lock_allow_grab_ply', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing players with lock part') +local lock_allow_grab_npc = CreateConVar('pac_sv_lock_allow_grab_npc', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing NPCs with lock part') +local lock_allow_grab_ent = CreateConVar('pac_sv_lock_allow_grab_ent', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing other entities with lock part') + +local force_allow = CreateConVar('pac_sv_force', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow force parts serverside') +local force_max_length = CreateConVar('pac_sv_force_max_length', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum length') +local force_max_radius = CreateConVar('pac_sv_force_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum radius') +local force_max_amount = CreateConVar('pac_sv_force_max_amount', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum amount of force') + +local healthmod_allow = CreateConVar('pac_sv_health_modifier', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow health modifier parts serverside') +local healthmod_allowed_extra_bars = CreateConVar('pac_sv_health_modifier_extra_bars', 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Extra health bars') +local healthmod_allow_change_maxhp = CreateConVar('pac_sv_health_modifier_allow_maxhp', 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow players to change their maximum health and armor.') +local healthmod_minimum_dmgscaling = CreateConVar('pac_sv_health_modifier_min_damagescaling', -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Minimum health modifier amount. Negative values can heal.') + +local master_init_featureblocker = CreateConVar('pac_sv_block_combat_features_on_next_restart', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on') +local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds') +local enforce_netrate_buffer = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 5000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'the budgeted allowance to limit how often pac combat net messages can be sent. 0 to disable, otherwise a number in bit size') + +local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') +local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') + + +include("pac3/editor/server/combat_bans.lua") + pace = pace +pace.partmenu_categories_cedrics = +{ + ["new!"] = + { + ["icon"] = "icon16/new.png", + ["interpolated_multibone"]= "interpolated_multibone", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + }, + ["logic"] = + { + ["icon"] = "icon16/server_chart.png", + ["proxy"] = "proxy", + ["command"] = "command", + ["event"] = "event", + ["text"] = "text", + ["link"] = "link", + }, + ["scaffolds"] = + { + ["tooltip"] = "useful to build up structures with specific positioning rules", + ["icon"] = "map", + ["jiggle"] = "jiggle", + ["model2"] = "model2", + ["projectile"] = "projectile", + ["interpolated_multibone"]= "interpolated_multibone", + }, + ["combat"] = + { + ["icon"] = "icon16/joystick.png", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["projectile"] = "projectile", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + ["player_movement"] = "player_movement", + }, + ["animation"]= + { + ["icon"] = "icon16/world.png", + ["group"] = "group", + ["event"] = "event", + ["custom_animation"] = "custom_animation", + ["proxy"] = "proxy", + ["sprite"] = "sprite", + ["particle"] = "particle", + }, + ["materials"]= + { + ["icon"] = "pace.MiscIcons.appearance", + ["material_3d"] = "material_3d", + ["material_2d"] = "material_2d", + ["material_refract"] = "material_refract", + ["material_eye refract"]= "material_eye refract", + ["submaterial"] = "submaterial", + }, + ["entity"] = + { + ["icon"] = "icon16/cd_go.png", + ["bone3"] = "bone3", + ["custom_animation"] = "custom_animation", + ["gesture"] = "gesture", + ["entity2"] = "entity2", + ["poseparameter"] = "poseparameter", + ["camera"] = "camera", + ["holdtype"] = "holdtype", + ["effect"] = "effect", + ["player_config"] = "player_config", + ["player_movement"] = "player_movement", + ["animation"] = "animation", + ["submaterial"] = "submaterial", + ["faceposer"] = "faceposer", + ["flex"] = "flex", + ["material_3d"] = "material_3d", + ["weapon"] = "weapon", + }, + ["model"] = + { + ["icon"] = "icon16/bricks.png", + ["jiggle"] = "jiggle", + ["physics"] = "physics", + ["animation"] = "animation", + ["bone3"] = "bone3", + ["effect"] = "effect", + ["submaterial"] = "submaterial", + ["clip2"] = "clip2", + ["halo"] = "halo", + ["material_3d"] = "material_3d", + ["model2"] = "model2", + }, + ["modifiers"] = + { + ["icon"] = "icon16/connect.png", + ["fog"] = "fog", + ["motion_blur"] = "motion_blur", + ["halo"] = "halo", + ["clip2"] = "clip2", + ["bone3"] = "bone3", + ["poseparameter"] = "poseparameter", + ["material_3d"] = "material_3d", + ["proxy"]= "proxy", + }, + ["effects"] = + { + ["icon"] = "icon16/wand.png", + ["sprite"] = "sprite", + ["sound2"] = "sound2", + ["effect"] = "effect", + ["halo"] = "halo", + ["particles"]= "particles", + ["sunbeams"]= "sunbeams", + ["beam"]= "beam", + ["projected_texture"]= "projected_texture", + ["decal"]= "decal", + ["text"]= "text", + ["trail2"]= "trail2", + ["sound"]= "sound", + ["woohoo"]= "woohoo", + ["light2"]= "light2", + ["shake"]= "shake", + } +} + +pace.partmenu_categories_default = +{ + ["legacy"]= + { + ["icon"] = pace.GroupsIcons.legacy, + ["trail"]= "trail", + ["bone2"]= "bone2", + ["model"]= "model", + ["bodygroup"]= "bodygroup", + ["material"]= "material", + ["light"]= "light", + ["entity"]= "entity", + ["clip"]= "clip", + ["bone"]= "bone", + ["webaudio"]= "webaudio", + ["ogg"] = "ogg", + }, + ["advanced"]= + { + ["icon"] = pace.GroupsIcons.advanced, + ["lock"]= "lock", + ["force"]= "force", + ["custom_animation"]= "custom_animation", + ["material_refract"]= "material_refract", + ["projectile"]= "projectile", + ["link"] = "link", + ["damage_zone"] = "damage_zone", + ["interpolated_multibone"] = "interpolated_multibone", + ["material_2d"] = "material_2d", + ["material_eye refract"] = "material_eye refract", + ["hitscan"] = "hitscan", + ["health_modifier"] = "health_modifier", + ["command"] ="command", + }, + ["entity"]= + { + ["icon"] = pace.GroupsIcons.entity, + ["bone3"] = "bone3", + ["gesture"] = "gesture", + ["entity2"] ="entity2", + ["poseparameter"] = "poseparameter", + ["camera"] = "camera", + ["holdtype"]= "holdtype", + ["effect"] = "effect", + ["player_config"] = "player_config", + ["player_movement"] = "player_movement", + ["animation"] = "animation", + ["submaterial"] = "submaterial", + ["faceposer"] = "faceposer", + ["flex"] = "flex", + ["material_3d"] = "material_3d", + ["weapon"]= "weapon", + }, + ["model"]= + { + ["icon"] = pace.GroupsIcons.model, + ["jiggle"] = "jiggle", + ["physics"] = "physics", + ["animation"]= "animation", + ["bone3"] = "bone3", + ["effect"] = "effect", + ["submaterial"] ="submaterial", + ["clip2"] = "clip2", + ["halo"] = "halo", + ["material_3d"] = "material_3d", + ["model2"]= "model2", + }, + ["modifiers"]= + { + ["icon"] = pace.GroupsIcons.modifiers, + ["animation"] = "animation", + ["fog"] = "fog", + ["motion_blur"] = "motion_blur", + ["clip2"]= "clip2", + ["poseparameter"] = "poseparameter", + ["material_3d"] = "material_3d", + ["proxy"] = "proxy", + }, + ["effects"]= + { + ["icon"] = pace.GroupsIcons.effects, + ["sprite"] = "sprite", + ["sound2"] = "sound2", + ["effect"] = "effect", + ["halo"] = "halo", + ["particles"]= "particles", + ["sunbeams"] = "sunbeams", + ["beam"] = "beam", + ["projected_texture"]= "projected_texture", + ["decal"] = "decal", + ["text"] = "text", + ["trail2"] = "trail2", + ["sound"] = "sound", + ["woohoo"] = "woohoo", + ["light2"] = "light2", + ["shake"] = "shake" + } +} + local function rebuild_bookmarks() pace.bookmarked_ressources = pace.bookmarked_ressources or {} @@ -505,7 +770,7 @@ function pace.FillCombatBanPanel(pnl) --ban_request_list_button:SetColor(Color(255,0,0)) ban_request_list_button:SetSize(200, 40) ban_request_list_button:SetPos(450, 60) - + function ban_request_list_button:DoClick() net.Start("pac.RequestCombatBanStates") net.SendToServer() @@ -513,9 +778,22 @@ function pace.FillCombatBanPanel(pnl) net.Receive("pac.SendCombatBanStates", function() pac.global_combat_whitelist = net.ReadTable() - PrintTable(pac.global_combat_whitelist) + ban_list:Clear() + local combat_bans_temp_merger = {} + + for _,ply in pairs(player.GetAll()) do + combat_bans_temp_merger[ply:SteamID()] = pac.global_combat_whitelist[ply:SteamID()]-- or {nick = ply:Nick(), steamid = ply:SteamID(), permission = "Default"} + end + + for id,data in pairs(pac.global_combat_whitelist) do + combat_bans_temp_merger[id] = data + end + + for id,data in pairs(combat_bans_temp_merger) do + ban_list:AddLine(data.nick,data.steamid,data.permission) + end end) - + return BAN end @@ -535,17 +813,23 @@ function pace.FillCombatSettings(pnl) general_list:SetContents(general_list_list) local sv_prop_protection_props_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_prop_protection_props_box:SetText("Enforce generic prop protection for player-owned props and physics entities") + sv_prop_protection_props_box:SetText("Enforce generic prop protection for player-owned props and physics entities.\nRelated to client consents, but the policies for each part are not uniform.") sv_prop_protection_props_box:SetSize(400,30) sv_prop_protection_props_box:SetConVar("pac_sv_prop_protection") local sv_combat_whitelisting_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_combat_whitelisting_box:SetText("Restrict new pac3 combat (damage zone, lock, force) to only whitelisted users.") + sv_combat_whitelisting_box:SetText("Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.") sv_combat_whitelisting_box:SetSize(400,30) sv_combat_whitelisting_box:SetConVar("pac_sv_combat_whitelisting") sv_combat_whitelisting_box:SetTooltip("off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed") + local sv_master_break_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_master_break_box:SetText("Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts") + sv_master_break_box:SetSize(400,30) + sv_master_break_box:SetConVar("pac_sv_block_combat_features_on_next_restart") + sv_master_break_box:SetTooltip("You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers") + end do --hitscan @@ -564,9 +848,9 @@ function pace.FillCombatSettings(pnl) hitscans_list:SetContents(hitscans_list_list) local sv_hitscans_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) - sv_hitscans_box:SetText("allow serverside physical projectiles") + sv_hitscans_box:SetText("allow serverside bullets") sv_hitscans_box:SetSize(400,30) - sv_hitscans_box:SetConVar("pac_sv_projectiles") + sv_hitscans_box:SetConVar("pac_sv_hitscan") local hitscans_max_dmg_numbox = vgui.Create("DNumSlider", hitscans_list_list) hitscans_max_dmg_numbox:SetText("Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)") @@ -746,25 +1030,57 @@ function pace.FillCombatSettings(pnl) local max_force_radius_numbox = vgui.Create("DNumSlider", force_list_list) max_force_radius_numbox:SetText("Max force part radius") - max_force_radius_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + max_force_radius_numbox:SetValue(GetConVar("pac_sv_force_max_radius"):GetInt()) max_force_radius_numbox:SetMin(0) max_force_radius_numbox:SetDecimals(0) max_force_radius_numbox:SetMax(50000) max_force_radius_numbox:SetSize(400,30) max_force_radius_numbox:SetConVar("pac_sv_force_max_radius") local max_force_length_numbox = vgui.Create("DNumSlider", force_list_list) max_force_length_numbox:SetText("Max force part length") - max_force_length_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + max_force_length_numbox:SetValue(GetConVar("pac_sv_force_max_length"):GetInt()) max_force_length_numbox:SetMin(0) max_force_length_numbox:SetDecimals(0) max_force_length_numbox:SetMax(50000) max_force_length_numbox:SetSize(400,30) max_force_length_numbox:SetConVar("pac_sv_force_max_length") local max_force_amount_numbox = vgui.Create("DNumSlider", force_list_list) max_force_amount_numbox:SetText("Max force part amount") - max_force_amount_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + max_force_amount_numbox:SetValue(GetConVar("pac_sv_force_max_amount"):GetInt()) max_force_amount_numbox:SetMin(0) max_force_amount_numbox:SetDecimals(0) max_force_amount_numbox:SetMax(10000000) max_force_amount_numbox:SetSize(400,30) max_force_amount_numbox:SetConVar("pac_sv_force_max_amount") end + + do --health_modifier + local healthmod_list = master_list:Add("Health modifier part") + healthmod_list.Header:SetSize(40,40) + healthmod_list.Header:SetFont("DermaLarge") + local healthmod_list_list = vgui.Create("DListLayout") + healthmod_list_list:DockPadding(20,0,20,20) + healthmod_list:SetContents(healthmod_list_list) + + local sv_healthmod_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + sv_healthmod_box:SetText("Allow health modifier part") + sv_healthmod_box:SetSize(400,30) + sv_healthmod_box:SetConVar("pac_sv_health_modifier") + + local healthmod_extrabars_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + healthmod_extrabars_box:SetText("Allow changing max health and max armor") + healthmod_extrabars_box:SetSize(400,30) + healthmod_extrabars_box:SetConVar("pac_sv_health_modifier_allow_maxhp") + + local min_healthmod_dmgmult_box = vgui.Create("DNumSlider", healthmod_list_list) + min_healthmod_dmgmult_box:SetText("Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.") + min_healthmod_dmgmult_box:SetValue(GetConVar("pac_sv_health_modifier_min_damagescaling"):GetInt()) + min_healthmod_dmgmult_box:SetMin(-10) min_healthmod_dmgmult_box:SetDecimals(2) min_healthmod_dmgmult_box:SetMax(1) + min_healthmod_dmgmult_box:SetSize(400,30) + min_healthmod_dmgmult_box:SetConVar("pac_sv_health_modifier_min_damagescaling") + + local healthmod_extrabars_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + healthmod_extrabars_box:SetText("Allow extra healthbars") + healthmod_extrabars_box:SetSize(400,30) + healthmod_extrabars_box:SetConVar("pac_sv_health_modifier_extra_bars") + healthmod_extrabars_box:SetToolTip("What are those? It's like an armor layer that takes damage before it gets applied to the entity.") + end return master_list end @@ -972,7 +1288,6 @@ function pace.FillServerSettings(pnl) end - --part order, shortcuts function pace.FillEditorSettings(pnl) @@ -1081,11 +1396,18 @@ function pace.FillEditorSettings(pnl) shortcutaction_presets:AddChoice("factory preset", pace.PACActionShortcut_Default) shortcutaction_presets:AddChoice("no CTRL preset", pace.PACActionShortcut_NoCTRL) shortcutaction_presets:AddChoice("Cedric's preset", pace.PACActionShortcut_Cedric) + + for i,filename in ipairs(file.Find("pac3_config/pac_editor_shortcuts*.txt","DATA")) do + local data = file.Read("pac3_config/" .. filename, "DATA") + shortcutaction_presets:AddChoice(string.GetFileFromFilename(filename), util.KeyValuesToTable(data)) + end + shortcutaction_presets:SetX(10) shortcutaction_presets:SetY(420) shortcutaction_presets:SetWidth(170) shortcutaction_presets:SetHeight(20) function shortcutaction_presets:OnSelect(num, name, data) pace.PACActionShortcut = data + pace.FlashNotification("Selected shortcut preset: " .. name .. ". View console for more info") pac.Message("Selected shortcut preset: " .. name) for i,v in pairs(data) do if #v > 0 then MsgC(Color(50,250,50), i .. "\n") end @@ -1200,13 +1522,14 @@ function pace.FillEditorSettings(pnl) end local bindclear = vgui.Create("DButton", LeftPanel) - bindclear:SetText("clear keys") + bindclear:SetText("clear") bindclear:SetTooltip("deletes the current shortcut at the current index") bindclear:SetX(10) bindclear:SetY(480) bindclear:SetHeight(30) bindclear:SetWidth(90) bindclear:SetColor(Color(200,0,0)) + bindclear:SetIcon("icon16/keyboard_delete.png") function bindclear:DoClick() binder1:SetSelectedNumber(0) binder2:SetSelectedNumber(0) @@ -1223,6 +1546,7 @@ function pace.FillEditorSettings(pnl) bindoverwrite:SetHeight(30) bindoverwrite:SetWidth(90) bindoverwrite:SetColor(Color(0,200,0)) + bindoverwrite:SetIcon("icon16/disk.png") function bindoverwrite:DoClick() local tbl = {} local i = 1 @@ -1241,6 +1565,14 @@ function pace.FillEditorSettings(pnl) encode_table_to_file("pac_editor_shortcuts") end + function bindoverwrite:DoRightClick() + Derma_StringRequest("Save preset", "Save a keyboard shortcuts preset?", "pac_editor_shortcuts", + function(name) file.Write("pac3_config/"..name..".txt", util.TableToKeyValues(pace.PACActionShortcut)) + shortcutaction_presets:AddChoice(name..".txt") + end + ) + end + local bindcapture_text = vgui.Create("DLabel", LeftPanel) bindcapture_text:SetFont("DermaDefaultBold") bindcapture_text:SetText("") @@ -1248,6 +1580,7 @@ function pace.FillEditorSettings(pnl) bindcapture_text:SetX(300) bindcapture_text:SetY(480) bindcapture_text:SetSize(300, 30) + function bindcapture_text:Think() self:SetText(pace.bindcapturelabel_text) end @@ -1495,14 +1828,28 @@ function pace.FillEditorSettings2(pnl) } ]] - local movement_binders_label = vgui.Create("DLabel", panel) + local LeftPanel = vgui.Create( "DPanel", panel ) -- Can be any panel, it will be stretched + local RightPanel = vgui.Create( "DPanel", panel ) -- Can be any panel, it will be stretched + LeftPanel:SetSize(300,600) + RightPanel:SetSize(300,600) + local div = vgui.Create( "DHorizontalDivider", panel ) + div:Dock( FILL ) + div:SetLeft( LeftPanel ) + div:SetRight( RightPanel ) + + div:SetDividerWidth( 8 ) + div:SetLeftMin( 50 ) + div:SetRightMin( 50 ) + div:SetLeftWidth( 400 ) + + local movement_binders_label = vgui.Create("DLabel", LeftPanel) movement_binders_label:SetText("PAC editor camera movement") movement_binders_label:SetFont("DermaDefaultBold") movement_binders_label:SetColor(Color(0,0,0)) movement_binders_label:SetSize(200,40) movement_binders_label:SetPos(30,5) - local forward_binder = vgui.Create("DBinder", panel) + local forward_binder = vgui.Create("DBinder", LeftPanel) forward_binder:SetSize(40,40) forward_binder:SetPos(100,40) forward_binder:SetTooltip("move forward") @@ -1511,7 +1858,7 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["forward"]:SetString(input.GetKeyName( num )) end - local back_binder = vgui.Create("DBinder", panel) + local back_binder = vgui.Create("DBinder", LeftPanel) back_binder:SetSize(40,40) back_binder:SetPos(100,80) back_binder:SetTooltip("move back") @@ -1520,7 +1867,7 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["back"]:SetString(input.GetKeyName( num )) end - local moveleft_binder = vgui.Create("DBinder", panel) + local moveleft_binder = vgui.Create("DBinder", LeftPanel) moveleft_binder:SetSize(40,40) moveleft_binder:SetPos(60,80) moveleft_binder:SetTooltip("move left") @@ -1529,7 +1876,7 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["moveleft"]:SetString(input.GetKeyName( num )) end - local moveright_binder = vgui.Create("DBinder", panel) + local moveright_binder = vgui.Create("DBinder", LeftPanel) moveright_binder:SetSize(40,40) moveright_binder:SetPos(140,80) moveright_binder:SetTooltip("move right") @@ -1538,7 +1885,7 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["moveright"]:SetString(input.GetKeyName( num )) end - local up_binder = vgui.Create("DBinder", panel) + local up_binder = vgui.Create("DBinder", LeftPanel) up_binder:SetSize(40,40) up_binder:SetPos(180,40) up_binder:SetTooltip("move up") @@ -1547,7 +1894,7 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["up"]:SetString(input.GetKeyName( num )) end - local down_binder = vgui.Create("DBinder", panel) + local down_binder = vgui.Create("DBinder", LeftPanel) down_binder:SetSize(40,40) down_binder:SetPos(180,80) down_binder:SetTooltip("move down") @@ -1557,7 +1904,7 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["down"]:SetString(input.GetKeyName( num )) end - local slow_binder = vgui.Create("DBinder", panel) + local slow_binder = vgui.Create("DBinder", LeftPanel) slow_binder:SetSize(40,40) slow_binder:SetPos(20,80) slow_binder:SetTooltip("go slow") @@ -1566,7 +1913,7 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["slow"]:SetString(input.GetKeyName( num )) end - local speed_binder = vgui.Create("DBinder", panel) + local speed_binder = vgui.Create("DBinder", LeftPanel) speed_binder:SetSize(40,40) speed_binder:SetPos(20,40) speed_binder:SetTooltip("go fast") @@ -1575,6 +1922,347 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["speed"]:SetString(input.GetKeyName( num )) end + --[[pace.partmenu_categories_cedrics = + { + ["new!"] = + { + ["icon"] = "icon16/new.png", + ["interpolated_multibone"]= "interpolated_multibone", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + }, + ["logic"] = + { + ["icon"] = "icon16/server_chart.png", + ["proxy"] = "proxy", + ["command"] = "command", + ["event"] = "event", + ["text"] = "text", + ["link"] = "link", + },]] + local Parts = pac.GetRegisteredParts() + local function get_icon(str, fallback) + if str then + if pace.MiscIcons[string.gsub(str, "pace.MiscIcons.", "")] then + return pace.MiscIcons[string.gsub(str, "pace.MiscIcons.", "")] + else + local img = string.gsub(str, ".png", "") --remove the png extension + img = string.gsub(img, "icon16/", "") --remove the icon16 base path + img = "icon16/" .. img .. ".png" --why do this? to be able to write any form and let the program fix the form + return img + end + elseif Parts[fallback] then + return Parts[fallback].Icon + else + return "icon16/page_white.png" + end + + end + + local categorytree = vgui.Create("DTree", RightPanel) + categorytree:SetY(30) + categorytree:SetSize(360,400) + + local function class_partnode_add(parentnode, class) + if Parts[class] then + for i,v in ipairs(parentnode:GetChildNodes()) do --can't make duplicates so remove to place it at the end + if v:GetText() == class then v:Remove() end + end + + local part_node = parentnode:AddNode(class) + part_node:SetIcon(get_icon(nil, class)) + part_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("remove", function() part_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + end + end + local function bring_up_partclass_list(cat_node) + + --function from pace.OnAddPartMenu(obj) + local base = vgui.Create("EditablePanel") + base:SetPos(input.GetCursorPos()) + base:SetSize(200, 300) + + base:MakePopup() + + function base:OnRemove() + pac.RemoveHook("VGUIMousePressed", "search_part_menu") + end + + local edit = base:Add("DTextEntry") + edit:SetTall(20) + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + + local result = base:Add("DScrollPanel") + result:Dock(FILL) + + function edit:OnEnter() + if result.found[1] then + class_partnode_add(cat_node, result.found[1].ClassName) + end + base:Remove() + end + + edit.OnValueChange = function(_, str) + result:Clear() + result.found = {} + + for _, part in ipairs(pace.GetRegisteredParts()) do + if (part.FriendlyName or part.ClassName):find(str, nil, true) then + table.insert(result.found, part) + end + end + + table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end) + + for _, part in ipairs(result.found) do + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + line.DoClick = function() + class_partnode_add(cat_node, part.ClassName) + end + + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + if part.Icon then + btn:SetImage(part.Icon) + end + + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText((part.FriendlyName or part.ClassName):Replace('_', ' ')) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() + + line:Dock(TOP) + end + + --base:SetHeight(20 * #result.found + edit:GetTall()) + base:SetHeight(600 + edit:GetTall()) + + end + + edit:OnValueChange("") + + pac.AddHook("VGUIMousePressed", "search_part_menu", function(pnl, code) + if code == MOUSE_LEFT or code == MOUSE_RIGHT then + if not base:IsOurChild(pnl) then + base:Remove() + end + end + end) + end + local function bring_up_category_icon_browser(category_node) + local master_frame = vgui.Create("DFrame") + master_frame:SetPos(input.GetCursorPos()) + master_frame:SetSize(400,400) + + local browser = vgui.Create("DIconBrowser", master_frame) + function browser:OnChange() + category_node:SetIcon(self:GetSelectedIcon()) + end + browser:SetSize(400,380) + browser:SetPos(0,40) + + local frame = vgui.Create("EditablePanel", master_frame) + local edit = vgui.Create("DTextEntry", frame) + frame:SetSize(300,20) + function browser:Think() + if not IsValid(category_node) then master_frame:Remove() end + x = master_frame:GetX() + y = master_frame:GetY() + frame:SetPos(x,y+20) + frame:MakePopup() + end + + function edit:OnValueChange(value) + browser:FilterByText( value ) + end + master_frame:MakePopup() + frame:MakePopup() + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + local function bring_up_tooltip_edit(category_node) + local frame = vgui.Create("EditablePanel") + local edit = vgui.Create("DTextEntry", frame) + function edit:OnEnter(value) + category_node:SetTooltip(value) + frame:Remove() + end + function frame:Think() + if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end + end + frame:MakePopup() + + frame:SetSize(300,30) + frame:SetPos(input.GetCursorPos()) + + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + local function bring_up_name_edit(category_node) + local frame = vgui.Create("EditablePanel") + local edit = vgui.Create("DTextEntry", frame) + edit:SetText(category_node:GetText()) + function edit:OnEnter(value) + category_node:SetText(value) + frame:Remove() + end + function frame:Think() + if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end + end + frame:MakePopup() + + frame:SetSize(300,30) + frame:SetPos(category_node.Label:LocalToScreen(category_node.Label:GetPos())) + + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + + local function load_partgroup_template_into_tree(categorytree, tbl) + + categorytree:Clear() + for category,category_contents in pairs(tbl) do + + local category_node = categorytree:AddNode(category) + category_node:SetIcon(get_icon(category_contents.icon, category)) + + category_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("insert part in category", function() bring_up_partclass_list(category_node) end):SetImage("icon16/add.png") + menu:AddOption("select icon", function() bring_up_category_icon_browser(category_node) end):SetImage("icon16/picture.png") + menu:AddOption("write a tooltip", function() bring_up_tooltip_edit(category_node) end):SetImage("icon16/comment.png") + menu:AddOption("rename this category", function() bring_up_name_edit(category_node) end):SetImage("icon16/textfield_rename.png") + menu:AddOption("remove this category", function() category_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + + if category_contents["tooltip"] then + category_node:SetTooltip(category_contents["tooltip"]) + end + + for field,value in pairs(category_contents) do + if Parts[field] then + class_partnode_add(category_node, field) + end + end + end + end + + local function extract_partgroup_template_from_tree(categorytree) + local tbl = {} + for i,category_node in ipairs(categorytree:Root():GetChildNodes()) do + tbl[category_node:GetText()] = {} + --print(i,category_node:GetText(),category_node.Label:GetTooltip(), category_node:GetIcon()) + if category_node:GetTooltip() ~= nil and category_node:GetTooltip() ~= "" then tbl[category_node:GetText()]["tooltip"] = category_node:GetTooltip() end + tbl[category_node:GetText()]["icon"] = category_node:GetIcon() + + for i2,part_node in ipairs(category_node:GetChildNodes()) do + tbl[category_node:GetText()][part_node:GetText()] = part_node:GetText() + --print("\t",part_node:GetText()) + end + end + return tbl + end + + load_partgroup_template_into_tree(categorytree, pace.partgroups) + + local part_categories_presets = vgui.Create("DComboBox", RightPanel) + part_categories_presets:SetText("Select a part category preset") + part_categories_presets:AddChoice("active preset") + part_categories_presets:AddChoice("factory preset") + part_categories_presets:AddChoice("Cedric's preset") + local default_partgroup_presets = { + ["pac_part_categories.txt"] = true, + ["pac_part_categories_cedrics.txt"] = true, + ["pac_part_categories_default.txt"] = true + } + for i,filename in ipairs(file.Find("pac3_config/pac_part_categories*.txt","DATA")) do + if not default_partgroup_presets[string.GetFileFromFilename(filename)] then + part_categories_presets:AddChoice(string.GetFileFromFilename(filename)) + end + end + + part_categories_presets:SetX(10) part_categories_presets:SetY(10) + part_categories_presets:SetWidth(170) + part_categories_presets:SetHeight(20) + + part_categories_presets.OnSelect = function( self, index, value ) + if value == "factory preset" then + pace.partgroups = pace.partmenu_categories_default + elseif value == "Cedric's preset" then + pace.partgroups = pace.partmenu_categories_cedrics + elseif string.find(value, ".txt") then + pace.partgroups = util.KeyValuesToTable(file.Read("pac3_config/"..value)) + elseif value == "active preset" then + decode_table_from_file("pac_part_categories") + if not pace.partgroups_user then pace.partgroups_user = pace.partgroups end + file.Write("pac3_config/pac_part_categories_user.txt", util.TableToKeyValues(pace.partgroups_user)) + end + load_partgroup_template_into_tree(categorytree, pace.partgroups) + end + + local part_categories_save = vgui.Create("DButton", RightPanel) + part_categories_save:SetText("Save") + part_categories_save:SetImage("icon16/disk.png") + part_categories_save:SetX(180) part_categories_save:SetY(10) + part_categories_save:SetWidth(80) + part_categories_save:SetHeight(20) + part_categories_save:SetTooltip("Left click to save preset to the active slot\nRight click to save to a new file") + part_categories_save.DoClick = function() + pace.partgroups = extract_partgroup_template_from_tree(categorytree) + file.Write("pac3_config/pac_part_categories.txt", util.TableToKeyValues(extract_partgroup_template_from_tree(categorytree))) + end + part_categories_save.DoRightClick = function() + Derma_StringRequest("Save preset", "Save a part category preset?", "pac_part_categories", + function(name) file.Write("pac3_config/"..name..".txt", util.TableToKeyValues(extract_partgroup_template_from_tree(categorytree))) + part_categories_presets:AddChoice(name..".txt") + end + ) + end + + local part_categories_add_cat = vgui.Create("DButton", RightPanel) + part_categories_add_cat:SetText("Add category") + part_categories_add_cat:SetImage("icon16/add.png") + part_categories_add_cat:SetX(260) part_categories_add_cat:SetY(10) + part_categories_add_cat:SetWidth(100) + part_categories_add_cat:SetHeight(20) + part_categories_add_cat.DoClick = function() + local category_node = categorytree:AddNode("Category " .. categorytree:Root():GetChildNodeCount() + 1) + category_node:SetIcon("icon16/page_white.png") + + category_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("insert part in category", function() bring_up_partclass_list(category_node) end):SetImage("icon16/add.png") + menu:AddOption("select icon", function() bring_up_category_icon_browser(category_node) end):SetImage("icon16/picture.png") + menu:AddOption("write a tooltip", function() bring_up_tooltip_edit(category_node) end):SetImage("icon16/comment.png") + menu:AddOption("rename this category", function() bring_up_name_edit(category_node) end):SetImage("icon16/textfield_rename.png") + menu:AddOption("remove this category", function() category_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + end + return panel end @@ -1587,4 +2275,11 @@ end decode_table_from_file("pac_editor_shortcuts") decode_table_from_file("pac_editor_partmenu_layouts") + +if not file.Exists("pac_part_categories_cedrics.txt", "DATA") then + file.Write("pac3_config/pac_part_categories_cedrics.txt", util.TableToKeyValues(pace.partmenu_categories_cedrics)) +end +if not file.Exists("pac_part_categories_default.txt", "DATA") then + file.Write("pac3_config/pac_part_categories_default.txt", util.TableToKeyValues(pace.partmenu_categories_default)) +end decode_table_from_file("pac_part_categories") \ No newline at end of file diff --git a/lua/pac3/editor/server/combat_bans.lua b/lua/pac3/editor/server/combat_bans.lua index 2d919bb24..c83c72595 100644 --- a/lua/pac3/editor/server/combat_bans.lua +++ b/lua/pac3/editor/server/combat_bans.lua @@ -1,98 +1,101 @@ -util.AddNetworkString("pac.BanUpdate") -util.AddNetworkString("pac.RequestBanStates") -util.AddNetworkString("pac.SendBanStates") -if SERVER then - local function get_combat_ban_states() - local str = file.Read("pac_combat_bans.txt", "DATA") - - local banstates = {} - - if str and str ~= "" then - banstates = util.KeyValuesToTable(str) - end - - do -- check if this needs to be rebuilt - local k,v = next(banstates) - if isstring(v) then - local temp = {} - - for k,v in pairs(banstates) do - permission = pac.global_combat_whitelist[player.GetBySteamID(k)] or "Default" - temp[util.CRC("gm_" .. v .. "_gm")] = {steamid = v, name = k, permission = permission} - end - - banstates = temp +local function get_combat_ban_states() + local str = file.Read("pac_combat_bans.txt", "DATA") + + local banstates = {} + + if str and str ~= "" then + banstates = util.KeyValuesToTable(str) + end + + do -- check if this needs to be rebuilt + local k,v = next(banstates) + if isstring(v) then + local temp = {} + + for k,v in pairs(banstates) do + permission = pac.global_combat_whitelist[player.GetBySteamID(k)] or "Default" + temp[util.CRC("gm_" .. v .. "_gm")] = {steamid = v, name = k, permission = permission} end + + banstates = temp end - - return banstates end - local function load_table_from_file() - tbl_on_file = get_combat_ban_states() - for id, data in pairs(tbl_on_file) do - if not pac.global_combat_whitelist[id] then - pac.global_combat_whitelist[id] = tbl_on_file[id] - end + return banstates +end + +local function load_table_from_file() + tbl_on_file = get_combat_ban_states() + for id, data in pairs(tbl_on_file) do + if not pac.global_combat_whitelist[id] then + pac.global_combat_whitelist[id] = tbl_on_file[id] end end +end - util.AddNetworkString("pac.CombatBanUpdate") - util.AddNetworkString("pac.SendCombatBanStates") - util.AddNetworkString("pac.RequestCombatBanStates") +util.AddNetworkString("pac.BanUpdate") +util.AddNetworkString("pac.RequestBanStates") +util.AddNetworkString("pac.SendBanStates") + +util.AddNetworkString("pac.CombatBanUpdate") +util.AddNetworkString("pac.SendCombatBanStates") +util.AddNetworkString("pac.RequestCombatBanStates") + +net.Receive("pac.CombatBanUpdate", function() + --get old states first pac.old_tbl_on_file = get_combat_ban_states() - net.Receive("pac.CombatBanUpdate", function() - --get old states first - pac.old_tbl_on_file = get_combat_ban_states() - - load_table_from_file() - - local combatstates_update = net.ReadTable() - local is_id_table = net.ReadBool() - local banstates_for_file = pac.old_tbl_on_file - - --update - if not is_id_table then - for ply, perm in pairs(combatstates_update) do - banstates_for_file[ply:SteamID()] = { - steamid = ply:SteamID(), - nick = ply:Nick(), - permission = perm - } - - pac.global_combat_whitelist[ply:SteamID()] = { - steamid = ply:SteamID(), - nick = ply:Nick(), - permission = perm - } - end - else - pac.global_combat_whitelist = combatstates_update - banstates_for_file = combatstates_update - end + load_table_from_file() - file.Write("pac_combat_bans.txt", util.TableToKeyValues(banstates_for_file), "DATA") - end) + local combatstates_update = net.ReadTable() + local is_id_table = net.ReadBool() + local banstates_for_file = pac.old_tbl_on_file - net.Receive("pac.RequestCombatBanStates", function(len, ply) - net.Start("pac.SendCombatBanStates") - net.WriteTable(pac.global_combat_whitelist) - net.Broadcast() - end) + --update + if not is_id_table then + for ply, perm in pairs(combatstates_update) do + banstates_for_file[ply:SteamID()] = { + steamid = ply:SteamID(), + nick = ply:Nick(), + permission = perm + } - concommand.Add("pac_read_combat_bans", function() - print("PAC3 combat bans and whitelist:") - for k,v in pairs(get_combat_ban_states()) do - print("\t" .. v.nick .. " is " .. v.permission .. " [" .. v.steamid .. "]") + pac.global_combat_whitelist[ply:SteamID()] = { + steamid = ply:SteamID(), + nick = ply:Nick(), + permission = perm + } end - end) + else + pac.global_combat_whitelist = combatstates_update + banstates_for_file = combatstates_update + end + + file.Write("pac_combat_bans.txt", util.TableToKeyValues(banstates_for_file), "DATA") +end) - concommand.Add("pac_read_outfit_bans", function() - PrintTable(pace.Bans) - end) +net.Receive("pac.RequestCombatBanStates", function(len, ply) + pac.global_combat_whitelist = get_combat_ban_states() + net.Start("pac.SendCombatBanStates") + net.WriteTable(pac.global_combat_whitelist) + net.Send(ply) +end) + + +pac.old_tbl_on_file = get_combat_ban_states() + + + +concommand.Add("pac_read_combat_bans", function() + print("PAC3 combat bans and whitelist:") + for k,v in pairs(get_combat_ban_states()) do + print("\t" .. v.nick .. " is " .. v.permission .. " [" .. v.steamid .. "]") + end +end) -end \ No newline at end of file +concommand.Add("pac_read_outfit_bans", function() + PrintTable(pace.Bans) +end) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 1fbf07341..6a6ed74ad 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -7,38 +7,43 @@ end pac.global_combat_whitelist = pac.global_combat_whitelist or {} -local hitscan_allow = CreateConVar('pac_sv_hitscan', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow hitscan parts serverside') -local hitscan_max_bullets = CreateConVar('pac_sv_hitscan_max_bullets', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum number of bullets') -local hitscan_max_damage = CreateConVar('pac_sv_hitscan_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum damage') -local hitscan_spreadout_dmg = CreateConVar('pac_sv_hitscan_divide_max_damage_by_max_bullets', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether or not force hitscans to divide their damage among the number of bullets fired') - -local damagezone_allow = CreateConVar('pac_sv_damage_zone', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow damage zone parts serverside') -local damagezone_max_damage = CreateConVar('pac_sv_damage_zone_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum damage') -local damagezone_max_length = CreateConVar('pac_sv_damage_zone_max_length', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum length') -local damagezone_max_radius = CreateConVar('pac_sv_damage_zone_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum radius') -local damagezone_allow_dissolve = CreateConVar('pac_sv_damage_zone_allow_dissolve', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to enable entity dissolvers and removing NPCs\' weapons on death for damagezone') - -local lock_allow = CreateConVar('pac_sv_lock', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock parts serverside') -local lock_allow_grab = CreateConVar('pac_sv_lock_grab', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part grabs serverside') -local lock_allow_teleport = CreateConVar('pac_sv_lock_teleport', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part teleports serverside') -local lock_max_radius = CreateConVar('pac_sv_lock_max_grab_radius', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'lock part maximum grab radius') -local lock_allow_grab_ply = CreateConVar('pac_sv_lock_allow_grab_ply', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing players with lock part') -local lock_allow_grab_npc = CreateConVar('pac_sv_lock_allow_grab_npc', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing NPCs with lock part') -local lock_allow_grab_ent = CreateConVar('pac_sv_lock_allow_grab_ent', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing other entities with lock part') - -local force_allow = CreateConVar('pac_sv_force', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow force parts serverside') -local force_max_length = CreateConVar('pac_sv_force_max_length', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum length') -local force_max_radius = CreateConVar('pac_sv_force_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum radius') -local force_max_amount = CreateConVar('pac_sv_force_max_amount', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum amount of force') - -local healthmod_allow = CreateConVar('pac_sv_healthmod', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow health modifier parts serverside') -local healthmod_allowed_extra_bars = CreateConVar('pac_sv_healthmod_extra_bars', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Extra health bars') -local healthmod_allow_change_maxhp = CreateConVar('pac_sv_healthmod_allow_maxhp', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow players to change their maximum health and armor.') -local healthmod_minimum_dmgscaling = CreateConVar('pac_sv_healthmod_min_damagescaling', -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Minimum health modifier amount. Negative values can heal.') - - -local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') -local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') +local hitscan_allow = CreateConVar('pac_sv_hitscan', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow hitscan parts serverside') +local hitscan_max_bullets = CreateConVar('pac_sv_hitscan_max_bullets', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum number of bullets') +local hitscan_max_damage = CreateConVar('pac_sv_hitscan_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum damage') +local hitscan_spreadout_dmg = CreateConVar('pac_sv_hitscan_divide_max_damage_by_max_bullets', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether or not force hitscans to divide their damage among the number of bullets fired') + +local damagezone_allow = CreateConVar('pac_sv_damage_zone', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow damage zone parts serverside') +local damagezone_max_damage = CreateConVar('pac_sv_damage_zone_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum damage') +local damagezone_max_length = CreateConVar('pac_sv_damage_zone_max_length', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum length') +local damagezone_max_radius = CreateConVar('pac_sv_damage_zone_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum radius') +local damagezone_allow_dissolve = CreateConVar('pac_sv_damage_zone_allow_dissolve', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to enable entity dissolvers and removing NPCs\' weapons on death for damagezone') + +local lock_allow = CreateConVar('pac_sv_lock', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock parts serverside') +local lock_allow_grab = CreateConVar('pac_sv_lock_grab', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part grabs serverside') +local lock_allow_teleport = CreateConVar('pac_sv_lock_teleport', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part teleports serverside') +local lock_max_radius = CreateConVar('pac_sv_lock_max_grab_radius', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'lock part maximum grab radius') +local lock_allow_grab_ply = CreateConVar('pac_sv_lock_allow_grab_ply', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing players with lock part') +local lock_allow_grab_npc = CreateConVar('pac_sv_lock_allow_grab_npc', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing NPCs with lock part') +local lock_allow_grab_ent = CreateConVar('pac_sv_lock_allow_grab_ent', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing other entities with lock part') + +local force_allow = CreateConVar('pac_sv_force', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow force parts serverside') +local force_max_length = CreateConVar('pac_sv_force_max_length', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum length') +local force_max_radius = CreateConVar('pac_sv_force_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum radius') +local force_max_amount = CreateConVar('pac_sv_force_max_amount', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum amount of force') + +local healthmod_allow = CreateConVar('pac_sv_health_modifier', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow health modifier parts serverside') +local healthmod_allowed_extra_bars = CreateConVar('pac_sv_health_modifier_extra_bars', 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow extra health bars') +local healthmod_allow_change_maxhp = CreateConVar('pac_sv_health_modifier_allow_maxhp', 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow players to change their maximum health and armor.') +local healthmod_minimum_dmgscaling = CreateConVar('pac_sv_health_modifier_min_damagescaling', -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Minimum health modifier amount. Negative values can heal.') + +local master_init_featureblocker = CreateConVar('pac_sv_block_combat_features_on_next_restart', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on') +cvars.AddChangeCallback('pac_sv_block_combat_features_on_next_restart', function() print("Remember that pac_sv_block_combat_features_on_next_restart is applied on server startup! Only do it if you know what you're doing. You'll need to restart the server.") end) + +local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds') +local enforce_netrate_buffer = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 5000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'the budgeted allowance to limit how often pac combat net messages can be sent. 0 to disable, otherwise a number in bit size') + +local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') +local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') local damageable_point_ent_classes = { ["predicted_viewmodel"] = false, @@ -475,24 +480,6 @@ if SERVER then return remaining_dmg,surviving_layer,side_effect_dmg end - - util.AddNetworkString("pac_hitscan") - util.AddNetworkString("pac_request_position_override_on_entity_teleport") - util.AddNetworkString("pac_request_position_override_on_entity_grab") - util.AddNetworkString("pac_request_angle_reset_on_entity") - util.AddNetworkString("pac_request_zone_damage") - util.AddNetworkString("pac_hit_results") - util.AddNetworkString("pac_request_force") - util.AddNetworkString("pac_signal_player_combat_consent") - util.AddNetworkString("pac_signal_stop_lock") - util.AddNetworkString("pac_request_lock_break") - util.AddNetworkString("pac_lock_imposecalcview") - util.AddNetworkString("pac_mark_grabbed_ent") - util.AddNetworkString("pac_notify_grabbed_player") - util.AddNetworkString("pac_request_player_combat_consent_update") - util.AddNetworkString("pac_request_healthmod") - util.AddNetworkString("pac_update_healthbars") - local function SendUpdateHealthBars(target) if not target:IsPlayer() or not target.pac_healthbars then return end net.Start("pac_update_healthbars") @@ -501,109 +488,6 @@ if SERVER then net.Broadcast() end - net.Receive("pac_request_healthmod", function(len,ply) - if not healthmod_allow:GetBool() then return end - local part_uid = net.ReadString() - local mod_id = net.ReadString() - local action = net.ReadString() - - if action == "MaxHealth" then - local num = net.ReadUInt(32) - local follow = net.ReadBool() - if not healthmod_allow_change_maxhp:GetBool() then return end - if ply:Health() == ply:GetMaxHealth() and follow then - ply:SetHealth(num) - elseif num < ply:Health() then - ply:SetHealth(num) - end - ply:SetMaxHealth(num) - ply.pac_healthmods = ply.pac_healthmods or {} - ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} - ply.pac_healthmods[part_uid].maxhealth = num - - elseif action == "MaxArmor" then - local num = net.ReadUInt(32) - local follow = net.ReadBool() - if not healthmod_allow_change_maxhp:GetBool() then return end - if ply:Armor() == ply:GetMaxArmor() and follow then - ply:SetArmor(num) - elseif num < ply:Armor() then - ply:SetArmor(num) - end - ply:SetMaxArmor(num) - ply.pac_healthmods = ply.pac_healthmods or {} - ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} - ply.pac_healthmods[part_uid].maxarmor = num - - elseif action == "DamageMultiplier" then - local scale = net.ReadFloat() - AddDamageScale(ply, mod_id, scale, part_uid) - - elseif action == "HealthBars" then - local num = net.ReadUInt(32) - local barsize = net.ReadUInt(32) - local layer = net.ReadUInt(4) - local absorbfactor = net.ReadFloat() - local follow = net.ReadBool() - - UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) - - elseif action == "OnRemove" then - if ply.pac_damage_scalings then - if ply.pac_damage_scalings[part_uid] then - ply.pac_damage_scalings[part_uid] = nil - end - end - if ply.pac_healthmods then - ply.pac_healthmods[part_uid] = nil - end - - FixMaxHealths(ply) - UpdateHealthBars(ply, 0, 0, 0, 0, part_uid, follow) - end - SendUpdateHealthBars(ply) - end) - - net.Receive("pac_hitscan", function(len,ply) - - if not hitscan_allow:GetBool() then return end - if not PlayerIsCombatAllowed(ply) then return end - - local ent = net.ReadEntity() - if not IsValid(ent) then ent = ply end - local bulletinfo = net.ReadTable() - local dir = net.ReadAngle() - bulletinfo.Dir = dir:Forward() - local part_uid = net.ReadString() - - bulletinfo.Num = math.Clamp(bulletinfo.Num, 1, hitscan_max_bullets:GetInt()) - bulletinfo.Damage = math.Clamp(bulletinfo.Damage, 0, hitscan_max_damage:GetInt()) - bulletinfo.DamageFalloffFraction = math.Clamp(bulletinfo.DamageFalloffFraction,0,1) - if hitscan_spreadout_dmg:GetBool() or bulletinfo.DistributeDamage then - bulletinfo.Damage = bulletinfo.Damage / bulletinfo.Num - end - - bulletinfo.IgnoreEntity = ent - ply.pac_bullet_emitters = ply.pac_bullet_emitters or {} - ply.pac_bullet_emitters[part_uid] = ply.pac_bullet_emitters[part_uid] or ents.Create("pac_bullet_emitter") - - bulletinfo.Callback = function(atk, trc, dmg) - if bulletinfo.DamageFalloff and trc.Hit then - local distance = (trc.HitPos):Distance(trc.StartPos) - local fraction = math.Clamp(1 - (1-bulletinfo.DamageFalloffFraction)*(distance / bulletinfo.DamageFalloffDistance),bulletinfo.DamageFalloffFraction,1) - dmg:SetDamage(fraction * dmg:GetDamage()) - end - end - - if IsValid(ply.pac_bullet_emitters[part_uid]) then - ply.pac_bullet_emitters[part_uid]:FireBullets(bulletinfo) - else - ply.pac_bullet_emitters[part_uid] = ents.Create("pac_bullet_emitter") - end - - end) - - --healthbars work with a 2 levels-deep table --for each player, an index table (priority) to decide which layer is damaged first --for each layer, one table for each part uid @@ -654,186 +538,13 @@ if SERVER then end end) - net.Receive("pac_request_zone_damage", function(len,ply) - - --server allow - if not damagezone_allow:GetBool() then return end - if not PlayerIsCombatAllowed(ply) then return end - - local pos = net.ReadVector() - local ang = net.ReadAngle() - local tbl = net.ReadTable() - local ply_ent = net.ReadEntity() - local dmg_info = DamageInfo() - - --server limits - tbl.Radius = math.Clamp(tbl.Radius,-damagezone_max_radius:GetInt(),damagezone_max_radius:GetInt()) - tbl.Length = math.Clamp(tbl.Length,-damagezone_max_length:GetInt(),damagezone_max_length:GetInt()) - tbl.Damage = math.Clamp(tbl.Damage,-damagezone_max_damage:GetInt(),damagezone_max_damage:GetInt()) - - dmg_info:SetDamage(tbl.Damage) - dmg_info:IsBulletDamage(tbl.Bullet) - dmg_info:SetDamageForce(Vector(0,0,0)) - dmg_info:SetAttacker(ply_ent) - dmg_info:SetInflictor(ply_ent) - - local ents_hits - local kill = false - local hit = false - - dmg_info:SetDamageType(damage_types[tbl.DamageType]) - - local ratio - if tbl.Radius == 0 then ratio = tbl.Length - else ratio = math.abs(tbl.Length / tbl.Radius) end - - if tbl.HitboxMode == "Sphere" then - ents_hits = ents.FindInSphere(pos, tbl.Radius) - - elseif tbl.HitboxMode == "Box" or tbl.HitboxMode == "Cube" then - local mins - local maxs - if tbl.HitboxMode == "Box" then - mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) - maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) - elseif tbl.HitboxMode == "Cube" then - mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Radius) - maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius) - end - - ents_hits = ents.FindInBox(mins, maxs) - - elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then - ents_hits = {} - if tbl.Radius ~= 0 then - local sides = tbl.Detail - if tbl.Detail < 1 then sides = 1 end - local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection - local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) - if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then - area_factor = 0.15*area_factor - steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) - end - steps = math.max(steps + math.abs(tbl.ExtraSteps),1) - - for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier - phase = math.random() - local ray_thickness = math.Clamp(0.5*math.log(tbl.Radius) + 0.05*tbl.Radius,0,10)*(1 - 0.7*ringnumber) - for i=1,0,-1/sides do - if ringnumber == 0 then i = 0 end - x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) - y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) - local startpos = pos + x + y - local endpos = pos + ang:Forward()*tbl.Length + x + y - MergeTargetsByID(ents_hits, ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness))) - end - end - if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then - --fast sphere check on the wide end - if tbl.Length/tbl.Radius >= 2 then - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius), tbl.Radius)) - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Radius, tbl.Radius)) - if tbl.Radius ~= 0 then - local counter = 0 - for i=math.floor(tbl.Length / tbl.Radius) - 1,1,-1 do - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Radius*i), tbl.Radius)) - if counter == 100 then break end - counter = counter + 1 - end - end - end - end - elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - - elseif tbl.HitboxMode == "CylinderSpheres" then - ents_hits = {} - if tbl.Length ~= 0 and tbl.Radius ~= 0 then - local counter = 0 - MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) - for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) - if counter == 200 then break end - counter = counter + 1 - end - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) - elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - - elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then - ents_hits = {} - if tbl.Radius ~= 0 then - local sides = tbl.Detail - if tbl.Detail < 1 then sides = 1 end - local startpos = pos-- + Vector(0, self.Radius,self.Radius) - local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection - local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) - if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then - area_factor = 0.15*area_factor - steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) - end - steps = math.max(steps + math.abs(tbl.ExtraSteps),1) - local timestart = SysTime() - local casts = 0 - for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the ringnumber multiplier - phase = math.random() - local ray_thickness = 5 * (2 - ringnumber) - - for i=1,0,-1/sides do - if ringnumber == 0 then i = 0 end - x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) - y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) - local endpos = pos + ang:Forward()*tbl.Length + x + y - MergeTargetsByID(ents_hits,ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness))) - casts = casts + 1 - end - end - if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then - --fast sphere check on the wide end - local radius_multiplier = math.atan(math.abs(ratio)) / (1.5 + 0.1*math.sqrt(ratio)) - if ratio > 0.5 then - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius * radius_multiplier), tbl.Radius * radius_multiplier)) - end - end - elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - - elseif tbl.HitboxMode == "ConeSpheres" then - ents_hits = {} - local steps - steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) - for i = 1,0,-1/steps do - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) - end - - steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) - - if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - - elseif tbl.HitboxMode =="Ray" then - local startpos = pos + Vector(0,0,0) - local endpos = pos + ang:Forward()*tbl.Length - ents_hits = ents.FindAlongRay(startpos, endpos) - - if tbl.Bullet then - local bullet = {} - bullet.Src = pos + ang:Forward() - bullet.Dir = ang:Forward()*50000 - bullet.Damage = -1 - bullet.Force = 0 - bullet.Entity = dmg_info:GetAttacker() - dmg_info:GetInflictor():FireBullets(bullet) - end + local function MergeTargetsByID(tbl1, tbl2) + for i,v in pairs(tbl2) do + tbl1[v:EntIndex()] = v end - hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) - highest_dmg = highest_dmg or 0 - net.Start("pac_hit_results") - net.WriteBool(hit) - net.WriteBool(kill) - net.WriteFloat(highest_dmg) - net.WriteTable(successful_hit_ents) - net.WriteTable(successful_kill_ents) - net.Broadcast() - end) + end - function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + local function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) local pac_sv_damage_zone_allow_dissolve = GetConVar("pac_sv_damage_zone_allow_dissolve"):GetBool() local pac_sv_prop_protection = global_combat_prop_protection:GetBool() @@ -976,9 +687,10 @@ if SERVER then end if tbl.DamageType == "heal" then - ent:SetHealth(math.min(ent:Health() + tbl.Damage, ent:GetMaxHealth())) + + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) elseif tbl.DamageType == "armor" then - ent:SetArmor(math.min(ent:Armor() + tbl.Damage, ent:GetMaxArmor())) + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) else --only "living" entities can be killed, and we checked generic entities with a ghost 0 health previously @@ -1068,278 +780,81 @@ if SERVER then return hit,kill,dmg,successful_hit_ents,successful_kill_ents end - function MergeTargetsByID(tbl1, tbl2) - for i,v in pairs(tbl2) do - tbl1[v:EntIndex()] = v - end - end - - --The lock part grab request net message - net.Receive("pac_request_position_override_on_entity_grab", function(len, ply) - --server allow - if not lock_allow:GetBool() then return end - if not lock_allow_grab:GetBool() then return end - if not PlayerIsCombatAllowed(ply) then return end - - local did_grab = true - local need_breakup = false - local breakup_condition = "" - --monstrous net message - local is_first_time = net.ReadBool() - local lockpart_UID = net.ReadString() - local pos = net.ReadVector() - local ang = net.ReadAngle() - local override_ang = net.ReadBool() - local override_eyeang = net.ReadBool() - local targ_ent = net.ReadEntity() - local auth_ent = net.ReadEntity() - local override_viewposition = net.ReadBool() - local alt_pos = net.ReadVector() - local alt_ang = net.ReadAngle() - local ask_drawviewer = net.ReadBool() - - local prop_protected, reason = IsPropProtected(targ_ent, ply) - - local unconsenting_owner = targ_ent:GetCreator() ~= ply and (grab_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) - local calcview_unconsenting = targ_ent:GetCreator() ~= ply and (calcview_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and calcview_consents[targ_ent] == false)) - - if unconsenting_owner or (global_combat_prop_protection:GetBool() and prop_protected) then return end - - local targ_ent_owner = targ_ent:GetCreator() or targ_ent - local auth_ent_owner = ply - - auth_ent_owner.grabbed_ents = auth_ent_owner.grabbed_ents or {} - - local consent_break_condition = false - - if grab_consents[targ_ent_owner] == false then --if the target player is non-consenting - if targ_ent_owner == auth_ent_owner then consent_break_condition = false --player can still grab his owned entities - elseif targ_ent:IsPlayer() then --if not the same player, we cannot grab - consent_break_condition = true - breakup_condition = breakup_condition .. "cannot grab another player if they don't consent to grabs, " - elseif global_combat_prop_protection:GetBool() and targ_ent:GetCreator() ~= ply then - --if entity not owned by grabbing player, he cannot do it to other players' entities in the prop-protected mode - consent_break_condition = true - breakup_condition = breakup_condition .. "cannot grab another player's owned entities if they don't consent to grabs, " - end - end - - if not IsValid(targ_ent) then --invalid entity? - did_grab = false - return --nothing else matters, get out - end - if consent_break_condition then --any of the non-consenting conditions - did_grab = false - need_breakup = true - breakup_condition = breakup_condition .. "non-consenting, " - end - - --dead ent = break - --but don't exclude about physics props - if targ_ent:Health() == 0 and not (physics_point_ent_classes[targ_ent:GetClass()] or string.find(targ_ent:GetClass(),"item_") or string.find(targ_ent:GetClass(),"ammo_") or targ_ent:IsWeapon()) then - did_grab = false - need_breakup = true - breakup_condition = breakup_condition .. "dead, " - end - - if is_first_time then - if (auth_ent_owner ~= targ_ent and auth_ent_owner.grabbed_ents[targ_ent] == true) then - did_grab = false - need_breakup = true - breakup_condition = breakup_condition .. "mutual grab prevention, " - end - end - - if did_grab then - - - if targ_ent:IsPlayer() and targ_ent:InVehicle() then --yank player out of vehicle - print("Kicking " .. targ_ent:Nick() .. " out of vehicle to be grabbed!") - targ_ent:ExitVehicle() - end - - if override_ang then - if not targ_ent:IsPlayer() then --non-players work with angles - targ_ent:SetAngles(ang) - else --players work with eyeangles - if override_eyeang then - - if PlayerAllowsCalcView(targ_ent) and override_viewposition then - targ_ent.nextcalcviewTick = targ_ent.nextcalcviewTick or CurTime() - if targ_ent.nextcalcviewTick < CurTime() then - net.Start("pac_lock_imposecalcview") - net.WriteBool(true) - net.WriteVector(alt_pos) - net.WriteAngle(alt_ang) - net.WriteBool(ask_drawviewer) - net.Send(targ_ent) - targ_ent.nextcalcviewTick = CurTime() + 0.1 - targ_ent.has_calcview = true - else print("skipping") end - targ_ent:SetEyeAngles(alt_ang) - targ_ent:SetAngles(alt_ang) - else - targ_ent:SetEyeAngles(ang) - targ_ent:SetAngles(ang) - end - elseif not override_eyeang or not override_viewposition or not PlayerAllowsCalcView(targ_ent) then --break any calcviews if we can't do that - if targ_ent.has_calcview then - net.Start("pac_lock_imposecalcview") - net.WriteBool(false) - net.WriteVector(Vector(0,0,0)) - net.WriteAngle(Angle(0,0,0)) - net.Send(targ_ent) - targ_ent.has_calcview = false - end - end - - end - end - - targ_ent:SetPos(pos) - - ApplyLockState(targ_ent, true) - if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end - - --@@note lock assignation! IMPORTANT - if is_first_time then --successful, first - auth_ent_owner.grabbed_ents[targ_ent] = true - targ_ent.grabbed_by = auth_ent_owner - targ_ent.grabbed_by_uid = lockpart_UID - print(auth_ent, "grabbed", targ_ent, "owner grabber is", auth_ent_owner) - end - targ_ent.grabbed_by_time = CurTime() - else - auth_ent_owner.grabbed_ents[targ_ent] = nil - targ_ent.grabbed_by_uid = nil - targ_ent.grabbed_by = nil - end - - if need_breakup then - print("stop this now! reason: " .. breakup_condition) - net.Start("pac_request_lock_break") - net.WriteEntity(targ_ent) - net.WriteString(lockpart_UID) - net.WriteString(breakup_condition) - net.Send(auth_ent_owner) - - else - if is_first_time and did_grab then - net.Start("pac_mark_grabbed_ent") - net.WriteEntity(targ_ent) - net.WriteBool(did_grab) - net.WriteString(lockpart_UID) - net.Broadcast() - - if targ_ent:IsPlayer() then - net.Start("pac_notify_grabbed_player") - net.WriteEntity(ply) - net.Send(targ_ent) - end - end - end - end) - --the lockpart teleport request net message - net.Receive("pac_request_position_override_on_entity_teleport", function(len, ply) - --server allow - if not lock_allow:GetBool() then return end - if not lock_allow_teleport:GetBool() then return end - if not PlayerIsCombatAllowed(ply) then return end - - local lockpart_UID = net.ReadString() - local pos = net.ReadVector() - local ang = net.ReadAngle() - local override_ang = net.ReadBool() - - if IsValid(ply) then - if override_ang then - ply:SetEyeAngles(ang) - end - ply:SetPos(pos) - end - - end) - --the lockpart grab end request net message - net.Receive("pac_request_angle_reset_on_entity", function(len, ply) - - if not PlayerIsCombatAllowed(ply) then return end - - local ang = net.ReadAngle() - local delay = net.ReadFloat() - local targ_ent = net.ReadEntity() - local auth_ent = net.ReadEntity() - - targ_ent:SetAngles(ang) - ApplyLockState(targ_ent, false) - - end) - - --first stage of force: look for targets and determine force amount if continuous - function ImpulseForce(tbl, pos, ang, ply) - if tbl.Continuous then - tbl.BaseForce = tbl.BaseForce * FrameTime() * 3.3333 --weird value to equalize how 600 cancels out gravity - tbl.AddedVectorForce = tbl.AddedVectorForce * FrameTime() * 3.3333 - tbl.Torque = tbl.Torque * FrameTime() * 3.3333 - end - if tbl.HitboxMode == "Sphere" then - local ents_hits = ents.FindInSphere(pos, tbl.Radius) - ProcessForcesList(ents_hits, tbl, pos, ang, ply) - elseif tbl.HitboxMode == "Box" then - local mins - local maxs - if tbl.HitboxMode == "Box" then - mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) - maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) - end - - local ents_hits = ents.FindInBox(mins, maxs) - ProcessForcesList(ents_hits, tbl, pos, ang, ply) - elseif tbl.HitboxMode == "Cylinder" then - local ents_hits = {} - if tbl.Length ~= 0 and tbl.Radius ~= 0 then - local counter = 0 - MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) - for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) - if counter == 200 then break end - counter = counter + 1 - end - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) - --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) - elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - ProcessForcesList(ents_hits, tbl, pos, ang, ply) - elseif tbl.HitboxMode == "Cone" then - local ents_hits = {} - local steps - steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) - for i = 1,0,-1/steps do - MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) - end - - steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) - if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - ProcessForcesList(ents_hits, tbl, pos, ang, ply) - elseif tbl.HitboxMode =="Ray" then - local startpos = pos + Vector(0,0,0) - local endpos = pos + ang:Forward()*tbl.Length - ents_hits = ents.FindAlongRay(startpos, endpos) - ProcessForcesList(ents_hits, tbl, pos, ang, ply) + local hitbox_ids = { + ["Box"] = 1, + ["Cube"] = 2, + ["Sphere"] = 3, + ["Cylinder"] = 4, + ["CylinderHybrid"] = 5, + ["CylinderSpheres"] = 6, + ["Cone"] = 7, + ["ConeHybrid"] = 8, + ["ConeSpheres"] = 9, + ["Ray"] = 10 + } + + local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + fire = 31, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 32, + dissolve_heavy_electrical = 33, + dissolve_light_electrical = 34, + dissolve_core_effect = 35, + + heal = 36, + armor = 37, + } + + local tracer_ids = { + ["Tracer"] = 1, + ["AR2Tracer"] = 2, + ["HelicopterTracer"] = 3, + ["AirboatGunTracer"] = 4, + ["AirboatGunHeavyTracer"] = 5, + ["GaussTracer"] = 6, + ["HunterTracer"] = 7, + ["StriderTracer"] = 8, + ["GunshipTracer"] = 9, + ["ToolgunTracer"] = 10, + ["LaserTracer"] = 11 + } - if tbl.Bullet then - local bullet = {} - bullet.Src = pos + ang:Forward() - bullet.Dir = ang:Forward()*50000 - bullet.Damage = -1 - bullet.Force = 0 - bullet.Entity = dmg_info:GetAttacker() - dmg_info:GetInflictor():FireBullets(bullet) - end - end - end --second stage of force: apply - function ProcessForcesList(ents_hits, tbl, pos, ang, ply) - + local function ProcessForcesList(ents_hits, tbl, pos, ang, ply) for _,ent in pairs(ents_hits) do local phys_ent @@ -1376,17 +891,17 @@ if SERVER then local add_angvel = Vector(0,0,0) local ent_center = ent:WorldSpaceCenter() or ent:GetPos() - local force_center = tbl.Locus_pos or pos - local dir = ent_center - force_center + local dir = ent_center - pos --part + local dir2 = ent_center - tbl.Locus_pos--locus local dist_multiplier = 1 - local distance = (ent_center - force_center):Length() + local distance = (ent_center - pos):Length() if tbl.BaseForceAngleMode == "Radial" then --radial on self addvel = dir:GetNormalized() * tbl.BaseForce elseif tbl.BaseForceAngleMode == "Locus" then --radial on locus - addvel = dir:GetNormalized() * tbl.BaseForce + addvel = dir2:GetNormalized() * tbl.BaseForce elseif tbl.BaseForceAngleMode == "Local" then --forward on self addvel = ang:Forward() * tbl.BaseForce end @@ -1487,77 +1002,85 @@ if SERVER then end end - --the force part impulse request net message - net.Receive("pac_request_force", function(len,ply) - - --server allow - if not force_allow:GetBool() then return end - if not PlayerIsCombatAllowed(ply) then return end - - local tbl = net.ReadTable() - local pos = net.ReadVector() - local ang = net.ReadAngle() - local on = net.ReadBool() - - --server limits - tbl.Radius = math.Clamp(tbl.Radius,-force_max_radius:GetInt(),force_max_radius:GetInt()) - tbl.Length = math.Clamp(tbl.Length,-force_max_length:GetInt(),force_max_length:GetInt()) - tbl.BaseForce = math.Clamp(tbl.BaseForce,-force_max_amount:GetInt(),force_max_amount:GetInt()) - + --first stage of force: look for targets and determine force amount if continuous + local function ImpulseForce(tbl, pos, ang, ply) + if tbl.Continuous then + tbl.BaseForce = tbl.BaseForce * FrameTime() * 3.3333 --weird value to equalize how 600 cancels out gravity + tbl.AddedVectorForce = tbl.AddedVectorForce * FrameTime() * 3.3333 + tbl.Torque = tbl.Torque * FrameTime() * 3.3333 + end + if tbl.HitboxMode == "Sphere" then + local ents_hits = ents.FindInSphere(pos, tbl.Radius) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Box" then + local mins + local maxs + if tbl.HitboxMode == "Box" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) + end - if on then - if tbl.Continuous then - hook.Add("Tick", "pac_force_hold"..tbl.UniqueID, function() - ImpulseForce(tbl, pos, ang, ply) - end) - - active_force_ids[tbl.UniqueID] = CurTime() - else - ImpulseForce(tbl, pos, ang, ply) - active_force_ids[tbl.UniqueID] = nil + local ents_hits = ents.FindInBox(mins, maxs) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cylinder" then + local ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cone" then + local ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) end - else - hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) - active_force_ids[tbl.UniqueID] = nil - end - --check bad or inactive hooks - for i,v in pairs(active_force_ids) do - if not v then - hook.Remove("Tick", "pac_force_hold"..i) - --print("invalid force") - elseif v + 0.1 < CurTime() then - hook.Remove("Tick", "pac_force_hold"..i) - --print("outdated force") + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + + if tbl.Bullet then + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + dmg_info:GetInflictor():FireBullets(bullet) end end - - - end) + end --consent message from clients net.Receive("pac_signal_player_combat_consent", function(len,ply) - mode = net.ReadString() - b = net.ReadBool() - if mode == "grab" then - grab_consents[ply] = b - elseif mode == "damage_zone" then - damage_zone_consents[ply] = b - elseif mode == "calcview" then - calcview_consents[ply] = b - elseif mode == "force" then - force_consents[ply] = b - elseif mode == "hitscan" then - hitscan_consents[ply] = b - elseif mode == "all" then - grab_consents[ply] = b - damage_zone_consents[ply] = b - calcview_consents[ply] = b - force_consents[ply] = b - hitscan_consents[ply] = b - end + local grab = net.ReadBool() -- GetConVar("pac_client_grab_consent"):GetBool() + local damagezone = net.ReadBool() -- GetConVar("pac_client_damage_zone_consent"):GetBool() + local calcview = net.ReadBool() -- GetConVar("pac_client_lock_camera_consent"):GetBool() + local force = net.ReadBool() -- GetConVar("pac_client_force_consent"):GetBool() + local hitscan = net.ReadBool() -- GetConVar("pac_client_hitscan_consent"):GetBool() + grab_consents[ply] = grab + damage_zone_consents[ply] = damagezone + calcview_consents[ply] = calcview + force_consents[ply] = force + hitscan_consents[ply] = hitscan end) + --lock break order from client net.Receive("pac_signal_stop_lock", function(len,ply) ApplyLockState(ply, false) @@ -1583,10 +1106,6 @@ if SERVER then end end) - concommand.Add("pac_refresh_consents", function() - pac_combat_RefreshConsents() - end) - concommand.Add("pac_damage_zone_whitelist_entity_class", function(ply, cmd, args, argStr) for _,v in pairs(string.Explode(";",argStr)) do damageable_point_ent_classes[v] = true @@ -1603,18 +1122,9 @@ if SERVER then PrintTable(damageable_point_ent_classes) end) - function pac_combat_RefreshConsents() - for _,ent in pairs(ents.GetAll()) do - if ent:IsPlayer() then - net.Start("pac_request_player_combat_consent_update") - net.Send(ent) - end - end - end - - timer.Create("pac_timer_refresh_client_consents", 10, 0, function() pac_combat_RefreshConsents() end) - + local nextchecklock = CurTime() hook.Add("Tick", "pac_checklocks", function() + if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.1 end --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check for _,ent in pairs(ents.GetAll()) do if ent.grabbed_by then @@ -1624,21 +1134,816 @@ if SERVER then ent.grabbed_by = nil grabber.grabbed_ents[ent] = false ApplyLockState(ent, false) + end + end + end + end) + + util.AddNetworkString("pac_signal_player_combat_consent") + util.AddNetworkString("pac_request_blocked_parts") + util.AddNetworkString("pac_inform_blocked_parts") + + local FINAL_BLOCKED_COMBAT_FEATURES = { + hitscan = false, + damage_zone = false, + lock = false, + force = false, + health_modifier = false, + } + + + function net.Incoming( len, client ) + + local i = net.ReadHeader() + local strName = util.NetworkIDToString( i ) + if strName ~= "DrGBasePlayerLuminosity" and strName ~= "pac_in_editor_posang" then + print(strName, client, "message with " .. len .." bits") + end + + if ( !strName ) then return end + + local func = net.Receivers[ strName:lower() ] + if ( !func ) then return end + + -- + -- len includes the 16 bit int which told us the message name + -- + len = len - 16 + + func( len, client ) + + end + + local force_hitbox_ids = {["Box"] = 0,["Cube"] = 1,["Sphere"] = 2,["Cylinder"] = 3,["Cone"] = 4,["Ray"] = 5} + local base_force_mode_ids = {["Radial"] = 0, ["Locus"] = 1, ["Local"] = 2} + local vect_force_mode_ids = {["Global"] = 0, ["Local"] = 1, ["Radial"] = 2, ["RadialNoPitch"] = 3} + local ang_torque_mode_ids = {["Global"] = 0, ["TargetLocal"] = 1, ["Local"] = 2, ["Radial"] = 3} + + function DeclareForceReceivers() + util.AddNetworkString("pac_request_force") + --the force part impulse request net message + net.Receive("pac_request_force", function(len,ply) + --server allow + if not force_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + local tbl = {} + local pos = net.ReadVector() + local ang = net.ReadAngle() + tbl.Locus_pos = net.ReadVector() + local on = net.ReadBool() + + tbl.UniqueID = net.ReadString() + tbl.RootPartOwner = net.ReadEntity() + + tbl.HitboxMode = table.KeyFromValue(force_hitbox_ids, net.ReadUInt(4)) + tbl.BaseForceAngleMode = table.KeyFromValue(base_force_mode_ids, net.ReadUInt(3)) + tbl.VectorForceAngleMode = table.KeyFromValue(vect_force_mode_ids, net.ReadUInt(2)) + tbl.TorqueMode = table.KeyFromValue(ang_torque_mode_ids, net.ReadUInt(2)) + + tbl.Length = net.ReadInt(16) + tbl.Radius = net.ReadInt(16) + + tbl.BaseForce = net.ReadInt(18) + tbl.AddedVectorForce = net.ReadVector() + tbl.Torque = net.ReadVector() + + tbl.Continuous = net.ReadBool() + tbl.AccountMass = net.ReadBool() + tbl.Falloff = net.ReadBool() + tbl.AffectSelf = net.ReadBool() + tbl.Players = net.ReadBool() + tbl.PhysicsProps = net.ReadBool() + tbl.NPC = net.ReadBool() + + --server limits + tbl.Radius = math.Clamp(tbl.Radius,-force_max_radius:GetInt(),force_max_radius:GetInt()) + tbl.Length = math.Clamp(tbl.Length,-force_max_length:GetInt(),force_max_length:GetInt()) + tbl.BaseForce = math.Clamp(tbl.BaseForce,-force_max_amount:GetInt(),force_max_amount:GetInt()) + + + if on then + if tbl.Continuous then + hook.Add("Tick", "pac_force_hold"..tbl.UniqueID, function() + ImpulseForce(tbl, pos, ang, ply) + + end) + + active_force_ids[tbl.UniqueID] = CurTime() + else + ImpulseForce(tbl, pos, ang, ply) + active_force_ids[tbl.UniqueID] = nil + end + else + hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) + active_force_ids[tbl.UniqueID] = nil + end + + --check bad or inactive hooks + for i,v in pairs(active_force_ids) do + if not v then + hook.Remove("Tick", "pac_force_hold"..i) + --print("invalid force") + elseif v + 0.1 < CurTime() then + hook.Remove("Tick", "pac_force_hold"..i) + --print("outdated force") + end + end + + + end) + end + + function DeclareDamageZoneReceivers() + util.AddNetworkString("pac_request_zone_damage") + util.AddNetworkString("pac_hit_results") + net.Receive("pac_request_zone_damage", function(len,ply) + --server allow + if not damagezone_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + local pos = net.ReadVector() + local ang = net.ReadAngle() + local tbl = {} + + tbl.Damage = net.ReadUInt(28) + tbl.Length = net.ReadInt(16) + tbl.Radius = net.ReadInt(16) + + tbl.AffectSelf = net.ReadBool() + tbl.NPC = net.ReadBool() + tbl.Players = net.ReadBool() + tbl.PointEntities = net.ReadBool() + + tbl.HitboxMode = table.KeyFromValue(hitbox_ids, net.ReadUInt(5)) + tbl.DamageType = table.KeyFromValue(damage_ids, net.ReadUInt(7)) + + tbl.Detail = net.ReadInt(6) + tbl.ExtraSteps = net.ReadInt(4) + tbl.RadialRandomize = net.ReadInt(7) / 8 + tbl.PhaseRandomize = net.ReadInt(7) / 8 + tbl.DamageFalloff = net.ReadBool() + tbl.DamageFalloffPower = net.ReadInt(12) / 8 + tbl.Bullet = net.ReadBool() + tbl.DoNotKill = net.ReadBool() + tbl.CriticalHealth = net.ReadUInt(16) + tbl.RemoveNPCWeaponsOnKill = net.ReadBool() + + local dmg_info = DamageInfo() + + --server limits + tbl.Radius = math.Clamp(tbl.Radius,-damagezone_max_radius:GetInt(),damagezone_max_radius:GetInt()) + tbl.Length = math.Clamp(tbl.Length,-damagezone_max_length:GetInt(),damagezone_max_length:GetInt()) + tbl.Damage = math.Clamp(tbl.Damage,-damagezone_max_damage:GetInt(),damagezone_max_damage:GetInt()) + + dmg_info:SetDamage(tbl.Damage) + dmg_info:IsBulletDamage(tbl.Bullet) + dmg_info:SetDamageForce(Vector(0,0,0)) + dmg_info:SetAttacker(ply) + dmg_info:SetInflictor(ply) + + local ents_hits + local kill = false + local hit = false + + dmg_info:SetDamageType(damage_types[tbl.DamageType]) + + local ratio + if tbl.Radius == 0 then ratio = tbl.Length + else ratio = math.abs(tbl.Length / tbl.Radius) end + + if tbl.HitboxMode == "Sphere" then + ents_hits = ents.FindInSphere(pos, tbl.Radius) + + elseif tbl.HitboxMode == "Box" or tbl.HitboxMode == "Cube" then + local mins + local maxs + if tbl.HitboxMode == "Box" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) + elseif tbl.HitboxMode == "Cube" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Radius) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius) + end + + ents_hits = ents.FindInBox(mins, maxs) + + elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then + ents_hits = {} + if tbl.Radius ~= 0 then + local sides = tbl.Detail + if tbl.Detail < 1 then sides = 1 end + local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + end + steps = math.max(steps + math.abs(tbl.ExtraSteps),1) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + local ray_thickness = math.Clamp(0.5*math.log(tbl.Radius) + 0.05*tbl.Radius,0,10)*(1 - 0.7*ringnumber) + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + local startpos = pos + x + y + local endpos = pos + ang:Forward()*tbl.Length + x + y + MergeTargetsByID(ents_hits, ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness))) + end + end + if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then + --fast sphere check on the wide end + if tbl.Length/tbl.Radius >= 2 then + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius), tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Radius, tbl.Radius)) + if tbl.Radius ~= 0 then + local counter = 0 + for i=math.floor(tbl.Length / tbl.Radius) - 1,1,-1 do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Radius*i), tbl.Radius)) + if counter == 100 then break end + counter = counter + 1 + end + end + end + end + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + + elseif tbl.HitboxMode == "CylinderSpheres" then + ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + + elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then + ents_hits = {} + if tbl.Radius ~= 0 then + local sides = tbl.Detail + if tbl.Detail < 1 then sides = 1 end + local startpos = pos-- + Vector(0, self.Radius,self.Radius) + local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + end + steps = math.max(steps + math.abs(tbl.ExtraSteps),1) + local timestart = SysTime() + local casts = 0 + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the ringnumber multiplier + phase = math.random() + local ray_thickness = 5 * (2 - ringnumber) + + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + local endpos = pos + ang:Forward()*tbl.Length + x + y + MergeTargetsByID(ents_hits,ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness))) + casts = casts + 1 + end + end + if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then + --fast sphere check on the wide end + local radius_multiplier = math.atan(math.abs(ratio)) / (1.5 + 0.1*math.sqrt(ratio)) + if ratio > 0.5 then + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius * radius_multiplier), tbl.Radius * radius_multiplier)) + end + end + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + + elseif tbl.HitboxMode == "ConeSpheres" then + ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) end + + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + + if tbl.Bullet then + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + dmg_info:GetInflictor():FireBullets(bullet) + end + end + hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + highest_dmg = highest_dmg or 0 + net.Start("pac_hit_results") + net.WriteBool(hit) + net.WriteBool(kill) + net.WriteFloat(highest_dmg) + net.WriteTable(successful_hit_ents) + net.WriteTable(successful_kill_ents) + net.Broadcast() + end) + end + + function DeclareLockReceivers() + util.AddNetworkString("pac_request_position_override_on_entity_teleport") + util.AddNetworkString("pac_request_position_override_on_entity_grab") + util.AddNetworkString("pac_request_angle_reset_on_entity") + --The lock part grab request net message + net.Receive("pac_request_position_override_on_entity_grab", function(len, ply) + --server allow + if not lock_allow:GetBool() then return end + if not lock_allow_grab:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + local did_grab = true + local need_breakup = false + local breakup_condition = "" + --monstrous net message + local is_first_time = net.ReadBool() + local lockpart_UID = net.ReadString() + local pos = net.ReadVector() + local ang = net.ReadAngle() + local override_ang = net.ReadBool() + local override_eyeang = net.ReadBool() + local targ_ent = net.ReadEntity() + local auth_ent = net.ReadEntity() + local override_viewposition = net.ReadBool() + local alt_pos = net.ReadVector() + local alt_ang = net.ReadAngle() + local ask_drawviewer = net.ReadBool() + + local prop_protected, reason = IsPropProtected(targ_ent, ply) + + local unconsenting_owner = targ_ent:GetCreator() ~= ply and (grab_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) + local calcview_unconsenting = targ_ent:GetCreator() ~= ply and (calcview_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and calcview_consents[targ_ent] == false)) + + if unconsenting_owner or (global_combat_prop_protection:GetBool() and prop_protected) then return end + + local targ_ent_owner = targ_ent:GetCreator() or targ_ent + local auth_ent_owner = ply + + auth_ent_owner.grabbed_ents = auth_ent_owner.grabbed_ents or {} + + local consent_break_condition = false + + if grab_consents[targ_ent_owner] == false then --if the target player is non-consenting + if targ_ent_owner == auth_ent_owner then consent_break_condition = false --player can still grab his owned entities + elseif targ_ent:IsPlayer() then --if not the same player, we cannot grab + consent_break_condition = true + breakup_condition = breakup_condition .. "cannot grab another player if they don't consent to grabs, " + elseif global_combat_prop_protection:GetBool() and targ_ent:GetCreator() ~= ply then + --if entity not owned by grabbing player, he cannot do it to other players' entities in the prop-protected mode + consent_break_condition = true + breakup_condition = breakup_condition .. "cannot grab another player's owned entities if they don't consent to grabs, " + end + end + + if not IsValid(targ_ent) then --invalid entity? + did_grab = false + return --nothing else matters, get out + end + if consent_break_condition then --any of the non-consenting conditions + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "non-consenting, " end + + --dead ent = break + --but don't exclude about physics props + if targ_ent:Health() == 0 and not (physics_point_ent_classes[targ_ent:GetClass()] or string.find(targ_ent:GetClass(),"item_") or string.find(targ_ent:GetClass(),"ammo_") or targ_ent:IsWeapon()) then + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "dead, " + end + + if is_first_time then + if (auth_ent_owner ~= targ_ent and auth_ent_owner.grabbed_ents[targ_ent] == true) then + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "mutual grab prevention, " + end + end + + if did_grab then + + + if targ_ent:IsPlayer() and targ_ent:InVehicle() then --yank player out of vehicle + print("Kicking " .. targ_ent:Nick() .. " out of vehicle to be grabbed!") + targ_ent:ExitVehicle() + end + + if override_ang then + if not targ_ent:IsPlayer() then --non-players work with angles + targ_ent:SetAngles(ang) + else --players work with eyeangles + if override_eyeang then + + if PlayerAllowsCalcView(targ_ent) and override_viewposition then + targ_ent.nextcalcviewTick = targ_ent.nextcalcviewTick or CurTime() + if targ_ent.nextcalcviewTick < CurTime() then + net.Start("pac_lock_imposecalcview") + net.WriteBool(true) + net.WriteVector(alt_pos) + net.WriteAngle(alt_ang) + net.WriteBool(ask_drawviewer) + net.Send(targ_ent) + targ_ent.nextcalcviewTick = CurTime() + 0.1 + targ_ent.has_calcview = true + else print("skipping") end + targ_ent:SetEyeAngles(alt_ang) + targ_ent:SetAngles(alt_ang) + else + targ_ent:SetEyeAngles(ang) + targ_ent:SetAngles(ang) + end + elseif not override_eyeang or not override_viewposition or not PlayerAllowsCalcView(targ_ent) then --break any calcviews if we can't do that + if targ_ent.has_calcview then + net.Start("pac_lock_imposecalcview") + net.WriteBool(false) + net.WriteVector(Vector(0,0,0)) + net.WriteAngle(Angle(0,0,0)) + net.Send(targ_ent) + targ_ent.has_calcview = false + end + end + + end + end + + targ_ent:SetPos(pos) + + ApplyLockState(targ_ent, true) + if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end + + --@@note lock assignation! IMPORTANT + if is_first_time then --successful, first + auth_ent_owner.grabbed_ents[targ_ent] = true + targ_ent.grabbed_by = auth_ent_owner + targ_ent.grabbed_by_uid = lockpart_UID + print(auth_ent, "grabbed", targ_ent, "owner grabber is", auth_ent_owner) + end + targ_ent.grabbed_by_time = CurTime() + else + auth_ent_owner.grabbed_ents[targ_ent] = nil + targ_ent.grabbed_by_uid = nil + targ_ent.grabbed_by = nil + end + + if need_breakup then + print("stop this now! reason: " .. breakup_condition) + net.Start("pac_request_lock_break") + net.WriteEntity(targ_ent) + net.WriteString(lockpart_UID) + net.WriteString(breakup_condition) + net.Send(auth_ent_owner) + + else + if is_first_time and did_grab then + net.Start("pac_mark_grabbed_ent") + net.WriteEntity(targ_ent) + net.WriteBool(did_grab) + net.WriteString(lockpart_UID) + net.Broadcast() + + if targ_ent:IsPlayer() then + net.Start("pac_notify_grabbed_player") + net.WriteEntity(ply) + net.Send(targ_ent) + end + end + end + end) + --the lockpart teleport request net message + net.Receive("pac_request_position_override_on_entity_teleport", function(len, ply) + --server allow + if not lock_allow:GetBool() then return end + if not lock_allow_teleport:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + local lockpart_UID = net.ReadString() + local pos = net.ReadVector() + local ang = net.ReadAngle() + local override_ang = net.ReadBool() + + if IsValid(ply) then + if override_ang then + ply:SetEyeAngles(ang) + end + ply:SetPos(pos) + end + + end) + --the lockpart grab end request net message + net.Receive("pac_request_angle_reset_on_entity", function(len, ply) + + if not PlayerIsCombatAllowed(ply) then return end + + local ang = net.ReadAngle() + local delay = net.ReadFloat() + local targ_ent = net.ReadEntity() + local auth_ent = net.ReadEntity() + + targ_ent:SetAngles(ang) + ApplyLockState(targ_ent, false) + + end) + end + + function DeclareHitscanReceivers() + util.AddNetworkString("pac_hitscan") + net.Receive("pac_hitscan", function(len,ply) + + if not hitscan_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + local bulletinfo = {} + local affect_self = net.ReadBool() + bulletinfo.Src = net.ReadVector() + local dir = net.ReadAngle() + bulletinfo.Dir = dir:Forward() + + bulletinfo.dmgtype_str = table.KeyFromValue(damage_ids, net.ReadUInt(7)) + bulletinfo.dmgtype = damage_types[bulletinfo.dmgtype_str] + bulletinfo.Spread = net.ReadVector() + bulletinfo.Damage = net.ReadUInt(28) + bulletinfo.Tracer = net.ReadUInt(8) + bulletinfo.Force = net.ReadUInt(16) + + bulletinfo.Distance = net.ReadUInt(16) + bulletinfo.Num = net.ReadUInt(9) + bulletinfo.TracerName = table.KeyFromValue(tracer_ids, net.ReadUInt(4)) + bulletinfo.DistributeDamage = net.ReadBool() + + bulletinfo.DamageFalloff = net.ReadBool() + bulletinfo.DamageFalloffDistance = net.ReadUInt(16) + bulletinfo.DamageFalloffFraction = net.ReadUInt(10) / 1000 + + local part_uid = ply:Nick() .. net.ReadString() + + bulletinfo.Num = math.Clamp(bulletinfo.Num, 1, hitscan_max_bullets:GetInt()) + bulletinfo.Damage = math.Clamp(bulletinfo.Damage, 0, hitscan_max_damage:GetInt()) + bulletinfo.DamageFalloffFraction = math.Clamp(bulletinfo.DamageFalloffFraction,0,1) + + if hitscan_spreadout_dmg:GetBool() or bulletinfo.DistributeDamage then + bulletinfo.Damage = bulletinfo.Damage / bulletinfo.Num + end + + if not affect_self then bulletinfo.IgnoreEntity = ply end + ply.pac_bullet_emitters = ply.pac_bullet_emitters or {} + ply.pac_bullet_emitters[part_uid] = ply.pac_bullet_emitters[part_uid] or ents.Create("pac_bullet_emitter") + + bulletinfo.Callback = function(atk, trc, dmg) + dmg:SetDamageType(bulletinfo.dmgtype) + if trc.Hit and IsValid(trc.Entity) then + local distance = (trc.HitPos):Distance(trc.StartPos) + local fraction = math.Clamp(1 - (1-bulletinfo.DamageFalloffFraction)*(distance / bulletinfo.DamageFalloffDistance),bulletinfo.DamageFalloffFraction,1) + local ent = trc.Entity + + if bulletinfo.dmgtype_str == "heal" then + dmg:SetDamageType(0) + ent:SetHealth(math.min(ent:Health() + fraction * dmg:GetDamage(), math.max(ent:Health(), ent:GetMaxHealth()))) + dmg:SetDamage(0) + return + elseif bulletinfo.dmgtype_str == "armor" then + dmg:SetDamageType(0) + ent:SetArmor(math.min(ent:Armor() + fraction * dmg:GetDamage(), math.max(ent:Armor(), ent:GetMaxArmor()))) + dmg:SetDamage(0) + return + end + if bulletinfo.DamageFalloff and trc.Hit and IsValid(trc.Entity) then + if not bulletinfo.dmgtype_str == "heal" and not bulletinfo.dmgtype_str == "armor" then + dmg:SetDamage(fraction * dmg:GetDamage()) + end + end + end + end + + if IsValid(ply.pac_bullet_emitters[part_uid]) then + ply.pac_bullet_emitters[part_uid]:FireBullets(bulletinfo) + else + ply.pac_bullet_emitters[part_uid] = ents.Create("pac_bullet_emitter") + end + + end) + end + + function DeclareHealthModifierReceivers() + util.AddNetworkString("pac_request_healthmod") + util.AddNetworkString("pac_update_healthbars") + net.Receive("pac_request_healthmod", function(len,ply) + if not healthmod_allow:GetBool() then return end + local part_uid = net.ReadString() + local mod_id = net.ReadString() + local action = net.ReadString() + + if action == "MaxHealth" then + if not healthmod_allow:GetBool() then return end + local num = net.ReadUInt(32) + local follow = net.ReadBool() + if not healthmod_allow_change_maxhp:GetBool() then return end + if ply:Health() == ply:GetMaxHealth() and follow then + ply:SetHealth(num) + elseif num < ply:Health() then + ply:SetHealth(num) + end + ply:SetMaxHealth(num) + ply.pac_healthmods = ply.pac_healthmods or {} + ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} + ply.pac_healthmods[part_uid].maxhealth = num + + elseif action == "MaxArmor" then + if not healthmod_allow:GetBool() then return end + local num = net.ReadUInt(32) + local follow = net.ReadBool() + if not healthmod_allow_change_maxhp:GetBool() then return end + if ply:Armor() == ply:GetMaxArmor() and follow then + ply:SetArmor(num) + elseif num < ply:Armor() then + ply:SetArmor(num) + end + ply:SetMaxArmor(num) + ply.pac_healthmods = ply.pac_healthmods or {} + ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} + ply.pac_healthmods[part_uid].maxarmor = num + + elseif action == "DamageMultiplier" then + local scale = net.ReadFloat() + AddDamageScale(ply, mod_id, scale, part_uid) + + elseif action == "HealthBars" then + if not healthmod_allowed_extra_bars:GetBool() then return end + local num = net.ReadUInt(32) + local barsize = net.ReadUInt(32) + local layer = net.ReadUInt(4) + local absorbfactor = net.ReadFloat() + local follow = net.ReadBool() + + UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) + + elseif action == "OnRemove" then + if ply.pac_damage_scalings then + if ply.pac_damage_scalings[part_uid] then + ply.pac_damage_scalings[part_uid] = nil + end + end + if ply.pac_healthmods then + ply.pac_healthmods[part_uid] = nil + end + + FixMaxHealths(ply) + UpdateHealthBars(ply, 0, 0, 0, 0, part_uid, follow) + end + SendUpdateHealthBars(ply) + end) + end + + --[[util.AddNetworkString("pac_hitscan") + util.AddNetworkString("pac_request_position_override_on_entity_teleport") + util.AddNetworkString("pac_request_position_override_on_entity_grab") + util.AddNetworkString("pac_request_angle_reset_on_entity") + util.AddNetworkString("pac_request_zone_damage") + util.AddNetworkString("pac_hit_results") + util.AddNetworkString("pac_request_force") + + util.AddNetworkString("pac_signal_stop_lock") + util.AddNetworkString("pac_request_lock_break") + util.AddNetworkString("pac_lock_imposecalcview") + util.AddNetworkString("pac_mark_grabbed_ent") + util.AddNetworkString("pac_notify_grabbed_player") + util.AddNetworkString("pac_request_healthmod") + util.AddNetworkString("pac_update_healthbars")]] + + if master_init_featureblocker:GetInt() == 0 then + FINAL_BLOCKED_COMBAT_FEATURES = { + hitscan = false, + damage_zone = false, + lock = false, + force = false, + health_modifier = false, + } + + + elseif master_init_featureblocker:GetInt() == 1 then + FINAL_BLOCKED_COMBAT_FEATURES = { + hitscan = not hitscan_allow:GetBool(), + damage_zone = not damagezone_allow:GetBool(), + lock = not lock_allow:GetBool(), + force = not force_allow:GetBool(), + health_modifier = not healthmod_allow:GetBool(), + } + + else -- if it's not 0 or 1, all net combat features will be removed! + FINAL_BLOCKED_COMBAT_FEATURES = { + hitscan = true, + damage_zone = true, + lock = true, + force = true, + health_modifier = true, + } + end + + if not FINAL_BLOCKED_COMBAT_FEATURES["force"] then DeclareForceReceivers() end + if not FINAL_BLOCKED_COMBAT_FEATURES["damage_zone"] then DeclareDamageZoneReceivers() end + if not FINAL_BLOCKED_COMBAT_FEATURES["lock"] then DeclareLockReceivers() end + if not FINAL_BLOCKED_COMBAT_FEATURES["hitscan"] then DeclareHitscanReceivers() end + if not FINAL_BLOCKED_COMBAT_FEATURES["health_modifier"] then DeclareHealthModifierReceivers() end + + concommand.Add("pac_sv_combat_reinitialize_missing_receivers", function() + for name,blocked in pairs(FINAL_BLOCKED_COMBAT_FEATURES) do + local update = blocked and (blocked == GetConVar("pac_sv_"..name):GetBool()) + local new_bool = not (blocked or not GetConVar("pac_sv_"..name):GetBool()) + + if update then + FINAL_BLOCKED_COMBAT_FEATURES[name] = new_bool + if name == "force" then DeclareForceReceivers() print("reinitialized " .. name) + elseif name == "damage_zone" then DeclareDamageZoneReceivers() print("reinitialized " .. name) + elseif name == "lock" then DeclareLockReceivers() print("reinitialized " .. name) + elseif name == "hitscan" then DeclareHitscanReceivers() print("reinitialized " .. name) + elseif name == "health_modifier" then DeclareHealthModifierReceivers() print("reinitialized " .. name) + end + end + end + + net.Start("pac_inform_blocked_parts") + net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES) + net.Broadcast() + end) + + net.Receive("pac_request_blocked_parts", function(len, ply) + net.Start("pac_inform_blocked_parts") + net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES) + net.Send(ply) end) end if CLIENT then - CreateConVar("pac_client_grab_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part") - CreateConVar("pac_client_lock_camera_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to having lock parts override your view") - CreateConVar("pac_client_damage_zone_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") - CreateConVar("pac_client_force_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to pac3 physics forces") - CreateConVar("pac_client_hitscan_consent", "0", FCVAR_ARCHIVE, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") + CreateConVar("pac_client_grab_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part") + CreateConVar("pac_client_lock_camera_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to having lock parts override your view") + CreateConVar("pac_client_damage_zone_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") + CreateConVar("pac_client_force_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to pac3 physics forces") + CreateConVar("pac_client_hitscan_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") + + local function SendConsents() + net.Start("pac_signal_player_combat_consent") + net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) + net.WriteBool(GetConVar("pac_client_damage_zone_consent"):GetBool()) + net.WriteBool(GetConVar("pac_client_lock_camera_consent"):GetBool()) + net.WriteBool(GetConVar("pac_client_force_consent"):GetBool()) + net.WriteBool(GetConVar("pac_client_hitscan_consent"):GetBool()) + net.SendToServer() + end + + local function RequestBlockedParts() + + net.Start("pac_request_blocked_parts") + net.SendToServer() + end + concommand.Add("pac_inform_about_blocked_parts", RequestBlockedParts) + + net.Receive("pac_inform_blocked_parts", function() + pac.Blocked_Combat_Parts = net.ReadTable() + print("Are these pac combat parts blocked?") + + for name,b in pairs(pac.Blocked_Combat_Parts) do + local blocked = b + local disabled = not GetConVar("pac_sv_"..name):GetBool() + + local bool_str + + if disabled and blocked then bool_str = "disabled and blocked -> unavailable" + elseif disabled and not blocked then bool_str = "disabled -> unavailable" + elseif not disabled and blocked then bool_str = "blocked - > unavailable" + elseif not disabled and not blocked then bool_str = "available" + else bool_str = "??" end + + print(name .. " is " .. bool_str) + end + end) + + local consent_cvars = {"pac_client_grab_consent", "pac_client_lock_camera_consent", "pac_client_damage_zone_consent", "pac_client_force_consent", "pac_client_hitscan_consent"} + for _,cmd in ipairs(consent_cvars) do + cvars.AddChangeCallback(cmd, SendConsents) + end + CreateConVar("pac_break_lock_verbosity", "3", FCVAR_ARCHIVE, "How much info you want for the PAC3 lock notifications\n3:full information\n2:grabbing player + basic reminder of the lock break command\n1:grabbing player\n0:suppress the notifications") + concommand.Add( "pac_stop_lock", function() net.Start("pac_signal_stop_lock") @@ -1694,30 +1999,7 @@ if CLIENT then end) net.Receive("pac_request_player_combat_consent_update", function() - net.Start("pac_signal_player_combat_consent") - net.WriteString("grab") - net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) - net.SendToServer() - - net.Start("pac_signal_player_combat_consent") - net.WriteString("damage_zone") - net.WriteBool(GetConVar("pac_client_damage_zone_consent"):GetBool()) - net.SendToServer() - - net.Start("pac_signal_player_combat_consent") - net.WriteString("calcview") - net.WriteBool(GetConVar("pac_client_lock_camera_consent"):GetBool()) - net.SendToServer() - - net.Start("pac_signal_player_combat_consent") - net.WriteString("force") - net.WriteBool(GetConVar("pac_client_force_consent"):GetBool()) - net.SendToServer() - - net.Start("pac_signal_player_combat_consent") - net.WriteString("hitscan") - net.WriteBool(GetConVar("pac_client_hitscan_consent"):GetBool()) - net.SendToServer() + SendConsents() end) net.Receive("pac_notify_grabbed_player", function() @@ -1738,5 +2020,8 @@ if CLIENT then pac.Message("You've been grabbed by " .. grabber:Nick() .. "!") end) + + hook.Add("InitPostEntity", "PAC_Send_Consents_On_Join", SendConsents) + hook.Add("InitPostEntity", "PAC_Request_BlockedParts_On_Join", RequestBlockedParts) end diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index c2f990acc..e685d5c52 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -70,7 +70,7 @@ do -- projectile entity self:PhysicsInitSphere(radius, part.SurfaceProperties) else local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel) - print("valid fallback? " .. part.FallbackSurfpropModel , valid_fallback) + --print("valid fallback? " .. part.FallbackSurfpropModel , valid_fallback) self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties) if part.OverridePhysMesh and valid_fallback then @@ -437,6 +437,59 @@ do -- projectile entity scripted_ents.Register(ENT, ENT.ClassName) end + +local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + explosion = 31, -- ent:Ignite(5) + fire = 32, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 33, + dissolve_heavy_electrical = 34, + dissolve_light_electrical = 35, + dissolve_core_effect = 36, + + heal = 37, + armor = 38, +} +local attract_ids = { + hitpos = 0, + hitpos_radius = 1, + closest_to_projectile = 2, + closest_to_hitpos = 3, +} + if SERVER then for key, ent in pairs(ents.FindByClass("pac_projectile")) do ent:Remove() @@ -446,6 +499,7 @@ if SERVER then util.AddNetworkString("pac_projectile_attach") util.AddNetworkString("pac_projectile_remove") + --REWORKED NET MESSAGE STRUCTURE MEANS THERE'S A LIMITED AMOUNT OF RECEIVED TABLE FIELDS net.Receive("pac_projectile", function(len, ply) if not enable:GetBool() then return end @@ -459,7 +513,48 @@ if SERVER then local multi_projectile_count = net.ReadUInt(7) local pos = net.ReadVector() local ang = net.ReadAngle() - local part = net.ReadTable() + local part = {} + + --bools + part.Sphere = net.ReadBool() + part.RemoveOnCollide = net.ReadBool() + part.CollideWithOwner = net.ReadBool() + part.RemoveOnHide = net.ReadBool() + part.OverridePhysMesh = net.ReadBool() + part.Gravity = net.ReadBool() + part.AddOwnerSpeed = net.ReadBool() + part.Collisions = net.ReadBool() + part.CollideWithSelf = net.ReadBool() + part.AimDir = net.ReadBool() + part.DrawShadow = net.ReadBool() + part.Sticky = net.ReadBool() + part.BulletImpact = net.ReadBool() + + --vectors + part.RandomAngleVelocity = net.ReadVector() + part.LocalAngleVelocity = net.ReadVector() + + --strings + part.FallbackSurfpropModel = "models/" .. net.ReadString() + part.UniqueID = net.ReadString() + part.SurfaceProperties = util.GetSurfacePropName(net.ReadUInt(10)) + part.DamageType = table.KeyFromValue(damage_ids, net.ReadUInt(7)) + part.AttractMode = table.KeyFromValue(attract_ids, net.ReadUInt(3)) + + --numbers + part.Radius = net.ReadUInt(8) + part.DamageRadius = net.ReadUInt(10) + part.Damage = net.ReadUInt(24) + part.Speed = net.ReadUInt(16) / 1000 + part.Maximum = net.ReadUInt(7) + part.LifeTime = net.ReadUInt(14) / 100 + part.Delay = net.ReadUInt(9) / 100 + part.Mass = net.ReadUInt(18) + part.Spread = net.ReadInt(10) / 100 + part.Damping = net.ReadInt(20) / 100 + part.Attract = net.ReadInt(14) + part.AttractRadius = net.ReadUInt(10) + part.Bounce = net.ReadInt(8) / 100 local radius_limit = 2000 From 3c59e392ce012189ac86aa1ede72eaa81688dc31 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 22 Sep 2023 02:34:42 -0400 Subject: [PATCH 038/300] remove printing override --- lua/pac3/extra/shared/net_combat.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 6a6ed74ad..6c55e6176 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1152,11 +1152,11 @@ if SERVER then } - function net.Incoming( len, client ) + --[[function net.Incoming( len, client ) local i = net.ReadHeader() local strName = util.NetworkIDToString( i ) - if strName ~= "DrGBasePlayerLuminosity" and strName ~= "pac_in_editor_posang" then + if strName ~= "pac_in_editor_posang" then print(strName, client, "message with " .. len .." bits") end @@ -1172,7 +1172,7 @@ if SERVER then func( len, client ) - end + end]] local force_hitbox_ids = {["Box"] = 0,["Cube"] = 1,["Sphere"] = 2,["Cylinder"] = 3,["Cone"] = 4,["Ray"] = 5} local base_force_mode_ids = {["Radial"] = 0, ["Locus"] = 1, ["Local"] = 2} From d546d5f5f50053cc0c462f3884e45de1e90533d9 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 22 Sep 2023 13:21:16 -0400 Subject: [PATCH 039/300] smaller ent list to check lock states --- lua/pac3/extra/shared/net_combat.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 6c55e6176..b963c9aa2 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -78,6 +78,7 @@ local force_consents = {} local hitscan_consents = {} local calcview_consents = {} local active_force_ids = {} +local active_grabbed_ents = {} local damage_types = { generic = 0, --generic damage @@ -204,9 +205,11 @@ if SERVER then --reverting the state requires to reset the eyeang roll in case it was modified if ent:IsPlayer() then if bool then + active_grabbed_ents[ent] = true ent:SetMoveType(MOVETYPE_NONE) ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) else + active_grabbed_ents[ent] = nil ent:SetMoveType(MOVETYPE_WALK) ent:SetCollisionGroup(COLLISION_GROUP_NONE) local eyeang = ent:EyeAngles() @@ -223,9 +226,11 @@ if SERVER then elseif ent:IsNPC() then if bool then + active_grabbed_ents[ent] = true ent:SetMoveType(MOVETYPE_NONE) ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) else + active_grabbed_ents[ent] = nil ent:SetMoveType(MOVETYPE_STEP) ent:SetCollisionGroup(COLLISION_GROUP_NONE) ent_ang = ent:GetAngles() @@ -1124,16 +1129,17 @@ if SERVER then local nextchecklock = CurTime() hook.Add("Tick", "pac_checklocks", function() - if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.1 end + if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.14 end --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check - for _,ent in pairs(ents.GetAll()) do - if ent.grabbed_by then + for ent,bool in pairs(active_grabbed_ents) do + if ent.grabbed_by or bool then if ent.grabbed_by_time + 0.5 < CurTime() then --restore the movetype local grabber = ent.grabbed_by ent.grabbed_by_uid = nil ent.grabbed_by = nil grabber.grabbed_ents[ent] = false ApplyLockState(ent, false) + active_grabbed_ents[ent] = nil end end end From 8ace6e3a93b7daa222fff040e3addf4d1a01a759 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 22 Sep 2023 14:52:05 -0400 Subject: [PATCH 040/300] handle setting model better fallback to the old default popcan model if model was not valid --- lua/pac3/extra/shared/projectiles.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index e685d5c52..4dac3dedf 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -536,6 +536,7 @@ if SERVER then --strings part.FallbackSurfpropModel = "models/" .. net.ReadString() + part.UniqueID = net.ReadString() part.SurfaceProperties = util.GetSurfacePropName(net.ReadUInt(10)) part.DamageType = table.KeyFromValue(damage_ids, net.ReadUInt(7)) @@ -599,6 +600,9 @@ if SERVER then local ent = ents.Create("pac_projectile") SafeRemoveEntityDelayed(ent,math.Clamp(part.LifeTime, 0, 50)) + local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel) + if not valid_fallback or part.FallbackSurfpropModel == "models/" or not part.OverridePhysMesh then part.FallbackSurfpropModel = "models/props_junk/PopCan01a.mdl" end + ent:SetModel(part.FallbackSurfpropModel) ent:SetPos(pos) ent:SetAngles(ang) From a6593ba1185bc87e046eec7fd7f103fa0fe03d30 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 22 Sep 2023 14:54:54 -0400 Subject: [PATCH 041/300] fallback for compressed ids fallback to default value if the string is not present in the table --- lua/pac3/core/client/parts/projectile.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index e93000f83..2b1ecafdd 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -271,9 +271,9 @@ function PART:Shoot(pos, ang, multi_projectile_count) --strings net.WriteString(self.OverridePhysMesh and string.sub(string.gsub(self.FallbackSurfpropModel, "^models/", ""),1,150) or "") --custom model is an unavoidable string net.WriteString(string.sub(self.UniqueID,1,12)) --long string but we can probably truncate it - net.WriteUInt(physprop_indices[self.SurfaceProperties],10) - net.WriteUInt(damage_ids[self.DamageType],7) - net.WriteUInt(attract_ids[self.AttractMode],3) + net.WriteUInt(physprop_indices[self.SurfaceProperties] or 0,10) + net.WriteUInt(damage_ids[self.DamageType] or 0,7) + net.WriteUInt(attract_ids[self.AttractMode] or 2,3) --numbers net.WriteUInt(self.Radius,8) From 5c9831d620f78745f8a582559ee0a00829af9b38 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 22 Sep 2023 14:57:36 -0400 Subject: [PATCH 042/300] compressed id fallbacks --- lua/pac3/core/client/parts/force.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index d0002e796..1b2efb8ae 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -166,10 +166,10 @@ function PART:Impulse(on) net.WriteString(string.sub(self.UniqueID,1,12)) net.WriteEntity(self:GetRootPart():GetOwner()) - net.WriteUInt(force_hitbox_ids[self.HitboxMode],4) - net.WriteUInt(base_force_mode_ids[self.BaseForceAngleMode],3) - net.WriteUInt(vect_force_mode_ids[self.VectorForceAngleMode],2) - net.WriteUInt(ang_torque_mode_ids[self.TorqueMode],2) + net.WriteUInt(force_hitbox_ids[self.HitboxMode] or 0,4) + net.WriteUInt(base_force_mode_ids[self.BaseForceAngleMode] or 0,3) + net.WriteUInt(vect_force_mode_ids[self.VectorForceAngleMode] or 0,2) + net.WriteUInt(ang_torque_mode_ids[self.TorqueMode] or 0,2) net.WriteInt(self.Length, 16) net.WriteInt(self.Radius, 16) @@ -246,4 +246,4 @@ function PART:BuildCone(obj) end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From 566036e37c2db2277d9c6a5f2920afc1ef9ac9ab Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 23 Sep 2023 22:25:34 -0400 Subject: [PATCH 043/300] more fixes small adjustments and fixes additional server net safeguards to reduce engine entity messages: +block force net messages if no force / low force +hard entity limit to drop the entire damage zone or force operation instead of sending a morbillion engine entity updates. also a limit of entities per player that's multiplied with the server population to give another hard limit to drop entity operations. But then again, there already were the radius/length limits to stop people from selecting a square kilometer area to begin with! +if more than 5 players hit in a damage zone, delay the damage so not everyone dies at once making a giga spike overflowing the reliable buffer. apparently spreading deaths out helped in my local server tests +also drop the damage zone if there's more than 12 player targets and the number of player targets is more than a certain percentage of the server population (pac_sv_player_limit_as_fraction_to_drop_damage_zone being a float between 0 and 1) new pac icon representing the new pac combat. switchable with pac_icon --- lua/pac3/core/client/parts/event.lua | 12 ++-- lua/pac3/core/client/parts/force.lua | 9 ++- lua/pac3/editor/client/parts.lua | 7 ++- lua/pac3/editor/client/settings.lua | 35 +++++++++++ lua/pac3/editor/client/spawnmenu.lua | 30 ++++++++- lua/pac3/editor/server/init.lua | 12 +++- lua/pac3/extra/shared/net_combat.lua | 87 ++++++++++++++++++++++++--- materials/icon64/new pac icon.png | Bin 0 -> 7621 bytes 8 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 materials/icon64/new pac icon.png diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index a99d76542..4b6fe199a 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2322,7 +2322,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, time, damage, uid) uid = uid or "" - local valid_uid, err = xpcall(pac.GetPartFromUniqueID, uid) + local valid_uid, err = pcall(pac.GetPartFromUniqueID, uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do if part.ClassName == "damage_zone" then @@ -2368,7 +2368,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, time, uid) uid = uid or "" - local valid_uid, err = xpcall(pac.GetPartFromUniqueID, uid) + local valid_uid, err = pcall(pac.GetPartFromUniqueID, uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do if part.ClassName == "damage_zone" then @@ -2421,7 +2421,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, uid) uid = uid or "" - local valid_uid, err = xpcall(pac.GetPartFromUniqueID, uid) + local valid_uid, err = pcall(pac.GetPartFromUniqueID, uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do if part.ClassName == "lock" then @@ -2444,13 +2444,13 @@ PART.OldEvents = { end return false end - } + }, } do - local base_input_enums_names = { + --[[local base_input_enums_names = { ["IN_ATTACK"] = 1, ["IN_JUMP"] = 2, ["IN_DUCK"] = 4, @@ -2487,7 +2487,7 @@ do input_aliases[alternative0] = value input_aliases[alternative1] = value input_aliases[alternative2] = value - end + end]] local enums = {} diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 1b2efb8ae..49ef7a2ca 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -156,6 +156,13 @@ function PART:Impulse(on) end else locus_pos = self:GetWorldPosition() end + if self.BaseForce == 0 and not game.SinglePlayer() then + if math.abs(self.AddedVectorForce.x) < 10 and math.abs(self.AddedVectorForce.y) < 10 and math.abs(self.AddedVectorForce.z) < 10 then + if math.abs(self.Torque.x) < 10 and math.abs(self.Torque.y) < 10 and math.abs(self.Torque.z) < 10 then + return + end + end + end net.Start("pac_request_force", true) net.WriteVector(self:GetWorldPosition()) @@ -246,4 +253,4 @@ function PART:BuildCone(obj) end -BUILDER:Register() +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index d302371eb..ab21efac1 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -818,6 +818,7 @@ do -- menu end //@note pace.DoBulkSelect function pace.DoBulkSelect(obj, silent) + obj = obj or pace.current_part refresh_halo_hook = false --print(obj.pace_tree_node, "color", obj.pace_tree_node:GetFGColor().r .. " " .. obj.pace_tree_node:GetFGColor().g .. " " .. obj.pace_tree_node:GetFGColor().b) if obj.ClassName == "timeline_dummy_bone" then return end @@ -1366,7 +1367,7 @@ do -- menu menu:SetPos(input.GetCursorPos()) --new_operations_order --default_operations_order - if not obj then obj = pace.current_part end + --if not obj then obj = pace.current_part end for _,option_name in ipairs(pace.operations_order) do pace.addPartMenuComponent(menu, obj, option_name) end @@ -1620,12 +1621,12 @@ function pace.GetPartSizeInformation(obj) end function pace.addPartMenuComponent(menu, obj, option_name) - + if option_name == "save" and obj then local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end) pnl:SetImage(pace.MiscIcons.save) add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end) - elseif option_name == "load" and obj then + elseif option_name == "load" then local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) pnl:SetImage(pace.MiscIcons.load) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 3f9c4ab59..b533eb2b5 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -33,8 +33,11 @@ local healthmod_allow_change_maxhp = CreateConVar('pac_sv_health_modifier_allow_ local healthmod_minimum_dmgscaling = CreateConVar('pac_sv_health_modifier_min_damagescaling', -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Minimum health modifier amount. Negative values can heal.') local master_init_featureblocker = CreateConVar('pac_sv_block_combat_features_on_next_restart', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on') + local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds') local enforce_netrate_buffer = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 5000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'the budgeted allowance to limit how often pac combat net messages can be sent. 0 to disable, otherwise a number in bit size') +local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") +local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') @@ -830,6 +833,38 @@ function pace.FillCombatSettings(pnl) sv_master_break_box:SetConVar("pac_sv_block_combat_features_on_next_restart") sv_master_break_box:SetTooltip("You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers") + local sv_netrate_time_numbox = vgui.Create("DNumSlider", general_list_list) + sv_netrate_time_numbox:SetText("Client net message limit (milliseconds delay)") + sv_netrate_time_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate"):GetInt()) + sv_netrate_time_numbox:SetMin(0) sv_netrate_time_numbox:SetDecimals(0) sv_netrate_time_numbox:SetMax(1000) + sv_netrate_time_numbox:SetSize(400,30) + sv_netrate_time_numbox:SetConVar("pac_sv_combat_enforce_netrate") + sv_netrate_time_numbox:SetTooltip("Not yet implemented! Leave it at 0 for now.") + + local sv_netrate_buffer_numbox = vgui.Create("DNumSlider", general_list_list) + sv_netrate_buffer_numbox:SetText("Client net message limit (buffer size / bit budget per second)") + sv_netrate_buffer_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt()) + sv_netrate_buffer_numbox:SetMin(0) sv_netrate_buffer_numbox:SetDecimals(0) sv_netrate_buffer_numbox:SetMax(50000) + sv_netrate_buffer_numbox:SetSize(400,30) + sv_netrate_buffer_numbox:SetConVar("pac_sv_combat_enforce_netrate_buffersize") + sv_netrate_buffer_numbox:SetTooltip("Not yet implemented!") + + local sv_hard_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) + sv_hard_ent_limit_numbox:SetText("Hard entity limit to cutoff damage zones and force parts") + sv_hard_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_combat_operation"):GetInt()) + sv_hard_ent_limit_numbox:SetMin(0) sv_hard_ent_limit_numbox:SetDecimals(0) sv_hard_ent_limit_numbox:SetMax(1000) + sv_hard_ent_limit_numbox:SetSize(400,30) + sv_hard_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_combat_operation") + sv_hard_ent_limit_numbox:SetTooltip("If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") + + local sv_per_player_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) + sv_per_player_ent_limit_numbox:SetText("Entity limit per player to cutoff damage zones and force parts") + sv_per_player_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_player_per_combat_operation"):GetInt()) + sv_per_player_ent_limit_numbox:SetMin(0) sv_per_player_ent_limit_numbox:SetDecimals(0) sv_per_player_ent_limit_numbox:SetMax(500) + sv_per_player_ent_limit_numbox:SetSize(400,30) + sv_per_player_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_player_per_combat_operation") + sv_per_player_ent_limit_numbox:SetTooltip("When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") + end do --hitscan diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index d18234abe..406eaed61 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -110,9 +110,37 @@ function pace.ClientSettingsMenu(self) end -local icon = "icon64/pac3.png" + +local icon_cvar = CreateConVar("pac_icon", "1", {FCVAR_ARCHIVE}, "Use the new PAC4.5 icon or the old PAC icon.\n0 = use the old one\n1 = use the new one") +local icon = icon_cvar:GetBool() and "icon64/new pac icon.png" or "icon64/pac3.png" + icon = file.Exists("materials/"..icon,'GAME') and icon or "icon64/playermodel.png" +local function ResetPACIcon() + if icon_cvar:GetBool() then icon = "icon64/new pac icon.png" else icon = "icon64/pac3.png" end + list.Set( + "DesktopWindows", + "PACEditor", + { + title = "PAC Editor", + icon = icon, + width = 960, + height = 700, + onewindow = true, + init = function(icn, pnl) + pnl:Remove() + RunConsoleCommand("pac_editor") + end + } + ) + RunConsoleCommand("spawnmenu_reload") +end + +cvars.AddChangeCallback("pac_icon", ResetPACIcon) + +concommand.Add("pac_change_icon", function() RunConsoleCommand("pac_icon", (not icon_cvar:GetBool()) and "1" or "0") ResetPACIcon() end) + + list.Set( "DesktopWindows", "PACEditor", diff --git a/lua/pac3/editor/server/init.lua b/lua/pac3/editor/server/init.lua index 3a14feeb5..282dc2920 100644 --- a/lua/pac3/editor/server/init.lua +++ b/lua/pac3/editor/server/init.lua @@ -22,7 +22,7 @@ do end end -CreateConVar("pac_sv_prop_outfits", "0", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow applying parts on props serverside') +CreateConVar("pac_sv_prop_outfits", "0", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow applying parts on other entities serverside\n0=don't\n1=allow on props but not players\n2=allow on other players") function pace.CanPlayerModify(ply, ent) if not IsValid(ply) or not IsValid(ent) then @@ -45,8 +45,13 @@ function pace.CanPlayerModify(ply, ent) return true end - if GetConVar("pac_sv_prop_outfits"):GetBool() then - return true + if GetConVar("pac_sv_prop_outfits"):GetInt() ~= 0 then + if GetConVar("pac_sv_prop_outfits"):GetInt() == 1 then + return not (ply ~= ent and ent:IsPlayer()) + elseif GetConVar("pac_sv_prop_outfits"):GetInt() == 2 then + return true + end + end do @@ -101,4 +106,5 @@ end CreateConVar("has_pac3_editor", "1", {FCVAR_NOTIFY}) +resource.AddSingleFile("materials/icon64/new pac icon.png") resource.AddSingleFile("materials/icon64/pac3.png") diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index b963c9aa2..369fca3ad 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -41,6 +41,9 @@ cvars.AddChangeCallback('pac_sv_block_combat_features_on_next_restart', function local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds') local enforce_netrate_buffer = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 5000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'the budgeted allowance to limit how often pac combat net messages can be sent. 0 to disable, otherwise a number in bit size') +local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") +local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") +local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') @@ -70,6 +73,17 @@ local contraption_classes = { ["prop_physics"] = true, } +local pre_excluded_ent_classes = { + ["info_player_start"] = true, + ["env_tonemap_controller"] = true, + ["env_fog_controller"] = true, + ["env_skypaint"] = true, + ["shadow_control"] = true, + ["env_sun"] = true, + ["predicted_viewmodel"] = true, + ["physgun_beam"] = true, +} + local grab_consents = {} @@ -195,6 +209,29 @@ if SERVER then return true end + --stopping condition to stop force or damage operation if too many entities, because net impact is proportional to players + local function TooManyEnts(count) + local playercount = player.GetCount() + local hard_limit = raw_ent_limit:GetInt() + local per_ply = per_ply_limit:GetInt() + print(count .. " compared against hard limit " .. hard_limit .. " and " .. playercount .. " players*" .. per_ply .. " limit (" .. count*playercount .. " | " .. playercount*per_ply .. ")") + if count > hard_limit then + MsgC(Color(255,0,0), "TOO MANY ENTS.\n") + return true + end + if not game.SinglePlayer() then + if count > per_ply_limit:GetInt() * playercount then + MsgC(Color(255,0,0), "TOO MANY ENTS.\n") + return true + end + if count * playercount > math.min(hard_limit, per_ply*playercount) then + MsgC(Color(255,0,0), "TOO MANY ENTS.\n") + return true + end + end + return false + end + --consent check local function PlayerAllowsCalcView(ply) return grab_consents[ply] and calcview_consents[ply] --oops it's redundant but I prefer it this way @@ -544,12 +581,27 @@ if SERVER then end) local function MergeTargetsByID(tbl1, tbl2) - for i,v in pairs(tbl2) do + for i,v in ipairs(tbl2) do tbl1[v:EntIndex()] = v end end local function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + local ent_count = 0 + local ply_count = 0 + local ply_prog_count = 0 + for i,v in pairs(ents_hits) do + if pre_excluded_ent_classes[v:GetClass()] or (v:IsNPC() and not tbl.NPC) or (v:IsPlayer() and not tbl.Players) then ents_hits[i] = nil + else + ent_count = ent_count + 1 + if v:IsPlayer() then ply_count = ply_count + 1 end + end + end + + --dangerous conditions: absurd amounts of entities, damaging a large percentage of the server's players beyond a certain point + if TooManyEnts(ent_count) or ((ply_count) > 12 and (ply_count > player_fraction:GetFloat() * player.GetCount())) then + return false,false,nil,{},{} + end local pac_sv_damage_zone_allow_dissolve = GetConVar("pac_sv_damage_zone_allow_dissolve"):GetBool() local pac_sv_prop_protection = global_combat_prop_protection:GetBool() @@ -616,7 +668,10 @@ if SERVER then local canhit = false --whether the policies allow the hit local prop_protected_consent = ent:GetCreator() ~= inflictor and ent ~= inflictor and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false-- and ent:GetCreator() ~= inflictor local contraption = IsPossibleContraptionEntity(ent) - + local bot_exception = true + if ent:IsPlayer() then + if ent:IsBot() then bot_exception = true end + end --first pass: entity class blacklist if IsEntity(ent) and ((damageable_point_ent_classes[ent:GetClass()] ~= false) or ((damageable_point_ent_classes[ent:GetClass()] == nil) or (damageable_point_ent_classes[ent:GetClass()] == true))) then --second pass: the damagezone's settings @@ -627,10 +682,9 @@ if SERVER then and --one of the base classes (damageable_point_ent_classes[ent:GetClass()] ~= false) --non-blacklisted class and --enforce prop protection - (ent:GetCreator() == inflictor or ent == inflictor or (ent:GetCreator() ~= inflictor and pac_sv_prop_protection and damage_zone_consents[ent:GetCreator()] == true) or not pac_sv_prop_protection) + (bot_exception or (ent:GetCreator() == inflictor or ent == inflictor or (ent:GetCreator() ~= inflictor and pac_sv_prop_protection and damage_zone_consents[ent:GetCreator()] == true) or not pac_sv_prop_protection)) then canhit = true - if ent:IsPlayer() and tbl.Players then --rules for players: --self can always hurt itself if asked to @@ -763,7 +817,7 @@ if SERVER then if tbl.DamageType == "fire" then ent:Ignite(5) end end - + --the forward bullet, if applicable and no entity is found if #ents_hits == 0 then if tbl.Bullet then @@ -771,13 +825,19 @@ if SERVER then end return hit,kill,dmg,successful_hit_ents,successful_kill_ents end - + --look through each entity for _,ent in pairs(ents_hits) do local canhit = DMGAllowed(ent) local oldhp = ent:Health() if canhit then - DoDamage(ent) + if ent:IsPlayer() and ply_count > 5 then + --jank fix to delay players damage in case they die all at once overflowing the reliable buffer + timer.Simple(ply_prog_count / 32, function() DoDamage(ent) end) + ply_prog_count = ply_prog_count + 1 + else + DoDamage(ent) + end end if not hit and (oldhp > 0 and canhit) then hit = true end end @@ -860,6 +920,13 @@ if SERVER then --second stage of force: apply local function ProcessForcesList(ents_hits, tbl, pos, ang, ply) + local ent_count = 0 + for i,v in pairs(ents_hits) do + if pre_excluded_ent_classes[v:GetClass()] or (v:IsNPC() and not tbl.NPC) or (v:IsPlayer() and not tbl.Players) then ents_hits[i] = nil + else ent_count = ent_count + 1 end + end + + if TooManyEnts(ent_count) then return end for _,ent in pairs(ents_hits) do local phys_ent @@ -869,7 +936,9 @@ if SERVER then or (string.find(ent:GetClass(), "npc") ~= nil) or ent:IsNPC() or physics_point_ent_classes[ent:GetClass()] - or string.find(ent:GetClass(), "item_") + or string.find(ent:GetClass(),"item_") + or string.find(ent:GetClass(),"ammo_") + or (ent:IsWeapon() and not IsValid(ent:GetOwner())) ) then local is_phys = true @@ -1162,7 +1231,7 @@ if SERVER then local i = net.ReadHeader() local strName = util.NetworkIDToString( i ) - if strName ~= "pac_in_editor_posang" then + if strName ~= "pac_in_editor_posang" and strName ~= "DrGBasePlayerLuminosity" then print(strName, client, "message with " .. len .." bits") end diff --git a/materials/icon64/new pac icon.png b/materials/icon64/new pac icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7088ceab7d99d776791e99c6dac68fbae9bac46 GIT binary patch literal 7621 zcmV;$9XjHPP)EX>4Tx04R}tkv&MmKpe$iQ>9ue9V{Z^kfA!+MMWJ;6^me@v=v%)FuC*#nlvOW zE{=k0!NHHks)LKOt`4q(Aou~|>f)s6A|?JWDYS_3;J6>}?mh0_0YbCFRI?`vs9McP z#baVNw<`Kx(T^bnF$=%MOg)ia%)oVg-NVDzy9m#6KlkStQ1T`NJR)(F=|+usgLr1M zrgPpW4zZG?5T6rI7<576N3P2bzi}?wEbz>bkx9)Hhls^u7t3ADN`^{2O&nHKjq-)8 z!wTmu&T6I3+V|uy4CJ+yG}md4B90{_kc0>sb(B#-4G~&3QcR?1Kjz{evHeMM$>b`7 zkz)Z>sE}+w_#gc4)+|g-I!S>T(EDOpA7g-T7icvs>-*TUS|@<_8Mx9q{%RAL`y{>5 z(ZWYSa2vR|?r8EJaJd5vKk1So*^-y0P$&TJXY@@uVDJ|3uQ|Q7_i_3FWT>m<8{ps& z7%x)xy34!!x_kTgOuN4y(B5*)ktHM800006VoOIv05eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{02`f2L_t(|+P#~3oLyCw@4suG;f^&AsY)s{Nx)zNCI~_^ z$P^;Nqai4^s2_)RplP1^y0x|2h+sQCeeD>u(T9B0ZiWQWR!Ky~(11t?kU$bbNJ6SI zRZ^9ts_wmYhjaFNf1GoxZVd^+w%_@D&gYza?m2t!_1kN&d5d0}Zp9{UiSOy@nb3IrUm5u3 z>eZ|N{&<6^8M4++kjC_=jJ+Z!Zlo3z)yez3L}3`2z4w7HEoD4O&dT_ zkc|ahv0{bR5-%cno(Dh}hFELq>guWhyH>AWed>w0K;!<$F9Ty`WC>K4!^9;JvHuh0 z`{dd1RC)f^n*-E>+OZvdc1?1mgWAsG)3=s{4Ry)Ep74f`onsjSN;-n7=visGe&$=o zv|0;*=XvaLn@UDQ>RX?KH9i?_3EYL0Vr!i6d4(ltpz}9WI%kKKzs<_$!|6f zgfkna-*sDmrLhAw4q^kuT2SSQ6}MbGsSFs)NTI6~>ecqXt~lKHp{E`xo%@OE5*K8} ziWQId^z@ut{b2g^>0>~sa)9GF7-M2KRIIhNK;(gAt5>h?s3iI4EKPSx>PQ*Gew$3}Rj;M#fjf7VI%ZPI)PpLmgos);oO#jbCZ$-%+*ZX-$%8!& zYM)y=T-bH@MJ>fz0JNv42V)Gz7{V~5tE+1QHyC3`CX^PiW_B_l9LK@) zJd80TKvcOQ2!h&gwi`q5PwxH2MdP`k3c!;`?vo$%U6j}oq}q;R=9Eu32?$PVk^jTq+u;IY8fLYO+qm-UbEx5fs$kz6BonV#f7YPN;tI@FXlNVQw2$ zqL;46wscWy>7?YRu*NVoof`Rne_ZJhzvkM}`@j5;E{d&} zl50PMz^|(=g~M6&kuyrAryn@KDenSXrca;V4zwLOa3GTS#>KXDQJjAUr71J8o=*kX zsR{~hzDpK3Tn;b-uE-es#Sy5eA}HNJi{~j0HXn} z6mZIi@do-ZK>;+t1bLj{y*R~}F_mqgM~+MUPp_{b{(f*|KmI@miOe)YHx;RUsE@e7 z+kMa7PYg?qk@GSzx2LBElc|R@-%R1Or5wyO&3$E?q|B?}N% z=o>u5gZp;j4{pIJ?v7_Uh&Vy}kx2+&vm&})cR#GTezdzUJ_1k2Ok%zBD70V2Q2SDX z#6&J|^;ap0^zX0c-SCsQ+&Xi{^lQ{hF52ZK2waZ>P-iVkE#qnBqyZRX&7I(IG@zl} z%eMV5kUqQtQ+fV`vpgx=Uq6cTw;zLz{ZZEnNM##|!FdGP84SBAGGGL4@Rhm4#_Sed zn-r|XMo1*lzBKJc|A7Y{7%t9ROs}7Ua*>B-AEY_hhl#w1Nnw!JNW2i>**s3Uhp%pZ zf!&4eK;igK&Lq$8yKeOUOOL=a{S)7@-no?8uO{EVgdkDJv1v=c>!JIbtX=EkAY%p_ z+seTWH($cWM}Hgr2xt!WF|G6CfLVWz* zzh67{uYb4*-mxZnrxrU2UL-L%pP+6gxJi5zSMf78%(^($0n6Cdf!xvyZ)4L_D?u>? z7(I%kFT6UN*EV+&hil-Y^WdIt_+u{m_O?@i+u_>J!s$1Twsm2pg+8ak7Be_9fOoD@ zeiDOWCUK3?BbeeY{NX;rbUU7^x*(pa8w2p#*Y1D$sOh85hm3^}&V}W9X!4;o*2wq& zFZkFu99DF;a%1}xb~_bvp2eACc;^EHBW=8M&8u`5_TvNv92?+8PNhcfuv_(yU*oj> z83?aEj6l+XdG*zq6DYXMuhjF)8?z9mV7JHoQ-tnA@Yc#SrXHOez+`bBTNGbX;2w6-u&28d1=sQ>3AaMEvMLppSzoP}Hi3^qZ|Kv;?z zPdm_*hExh>D4Y_&E5p!dps5WSnjurq9B+VfI>oBFv-woRvQ_aKsP)4zs`<3F-JQk4 z;YbEl*yY;$h>kH5?lbl)-BH;ca0kZo~syVlJy_odk)N<`+7W8 z>eSMt0oncA=yG6Y#$ifRf@Dh)!yp_x25A@Cvrw0Xg$m6>&@V7|D%8)Ul4@nRyO+v9 znqn@^j|b~%Kf3j%L$hAD%BU7h6w6ctrL?u(GfKmo&fC4cFXjLpiMrMjpF*ijLgdyL zo}WnAWE?Pwc_Bc;fqGHg-J6)2fVHgva@1yc|X)I0~P>Vpi;swhXfT%SP3{qju0kVFkyi7A7t6S zSN=HX;NFs7DKTry#<24z->`_Ni#v|(1zK!g0Omcre&t3d#fHH=k1smNwD}L?=oT)0 z;V&lY@yNx+0IouUNLVLDNgK#IO0%Q1xR!QLY4nvkM@hLt(lI2A!FLUwYa*oT4Ai+G zDL8E!ENz8!GjJ~O4DeSV=L5e6%|WS*ltbLIrC#!!Edh!B(pU{X`;UK9nDhF#GB9WHtLy9ctWHH%?6)042 zO3|WCaEve;4a|yANF)_RM-vgzq`Q$xpiyp1;O39LE+i3qQ3z$EMhlGcyVbEv!2*Fu z>??v7LKY=~a!vXubzVr)1>*=f4!8~sII!7=v`1@N!IenWbvD3^W|-X!4V}OoU zt`l%}D*q?W9pJRwLiI{PsI!c8AoIK~Qy4CPT`%i|cp zPGU8O8qG@}a3mCR3dhu8zzRPcXr{vpX?8=hu99*rz5{Q_6u_x~I1m&e=RuH%U=YfE z5Y)pNz2Nr8Ny5v}T4#NX=qyT~x_>C}iA-LB8^Cs!- z6<{GS9a>)kyA^aFN*`JdP|Y&i7U`}P0}qkfHm z*l&f_&|z9&>GVR*$pXa$l+)m62)!0)Z-Y5A;jA;DsRJkhF8~K(4%i6X3tR&1fmilH z*@wIZ(%AYbNV!bKHq$;_f(TOX{+GYbzg>(sUfsL3Z2y`)I(-%OSaU5S^dTGzmA%%` z7`nW>y$r=P3}-;w3GH-(&JNsU8usjifx{q0IC=oi_%giww{XgQIFN(ie-CCahFl&F zcSEiSp+gWTroNtBLlXy05~pTZS9Y1oFZZc8DFEY-C$78Y;yiL8oB#OPllI_hX8w4< z1PD1G7Xry4@GWFgP^vJnw}f~2Fm65pM-A*OLSX=|kE0zweGFCz>>GgIqwva;&^#5& z0Spxh3k8aS;?J#Pz-wfyq<|tu6%*xuxpJCRetCcq5GGhUcl>Cp;R|b$Ct`s0pLp!V zZti$BIsU5f;72w`sS0NZyxox4Nk`jmj`j9YI@k+4yWyoh;5R{X32gaxPSmb{=msWy+s=n1c?MWF|RJ90r!rtY;$>8gkCe6z?<>^35+oy zXv7KrU?dlsXG2q6nqoX5PsS-lfk( zxsIoeK?TyPvs#Cw(XErb{5iusQLcQ!M|5nFReZXx&}b}IHk7$&MvAGkGx%*O+$W#G z-O-Kd&(WBv1AjR3x19=j0c1jGDMRNlWXoLj?Kff1s-0!U4@xHhphJ}xjbX?bDhNjm z3wwK<%+wCah{*73AaP0@!}mS#ozaPPYeN!7*uFb5m>0}-*}fyP-4`zOc>LJ{^c~{S zPhLaOOE7EEh3vU^6*HGy0}F;Z@aT7ugKm_lof&|sK&Aw3!;mda5)-chLMPYUF7s76 zD2iVV1yvn~*C^B!dVd7dV{?7yzKI6rf-W~s2F9IFCZWQB&`WUYH6Mm@nZZK`;mj6D zR>%_~L7BOWC7S(Y3F-=vDv#*1DpQTP=3zKytm9X<3nFx=ata}P#UR39GHvo=G>_HjxalL4y3+H)}s5Wrfk}PMnv@&N(CwssD75roUxC4jj z{@0J(;FamMy!^ z;UF;0h9GQg?J&p6q9+uBCn#;}`2j4rIqGHm?q~)U%#6_3-ZNUKws5Y;mY1R>uy~Qj z!_N&v81Sx3=J2Dt)(+#vUr{coC>NCfEV6(5x1-h<%ymY1b9;9U@v}U(?5qj6*_cCIk1Yd-Ad!M} znir(V)z?quIk9}^qd~;(iCFrZnxLwVD?^z?<&B>?p~e(myKarWV%Z9FqR6gu=^wz) z#+_Oy4MFcF*!o`3U2!1bVz~6zAP(ExzrupIe=-6=Y9r5v&AVbZSe50MpF37I_$?eg zHb_%bGw*ocw|L+5za8b<>ba~`TZ90`L0H>#0@W>;xNL9ivzlY=y47|B%}I`800#~n zVCBk{eC^9$VDsipF!KVK_pQ;FI^RVhUm$n%FmznN_HV^bhZmciV%V}}3wPgrHybu= z01>|SjqkH*Yh|Ra^yZtt%)mgD_j})2jB{ayul{sCh;Z+{_p);3N;coK68`x4QF9_* z7IxuTt0w0JsVS8ORHX)l`z>=ht}oE$m$>bm+X+JGejb7|%Qy|QM;Iq)dn0{&UuGyj z%)--O4-zeh8-MhdgrQ~s{{38g<>_qMQDo1aZk~PiSvosAx#}$!v%mW>+*Fo_AKuC4 z^=p_l>qATs;RsXOqaw|>Pu~KGFGk#WWSk5DTvcJog`el8XaA|T9m6QYO=>S*z3Zvs zt66Zaw}?4!ybIU!s0{3jTU&%_nTOZ6k>Q@FBU`@tGXVVflgn@%hn9hjB-5~DQHpD? zT)=g2UBU%tO=bVi$JqG4;jS-ym6WUO-TLsDz(s9QaU$Iu{Ws&SaOEIeb`aWcjmy;| z-(FO4Rq<+PcM{e&nM~cJG~RmX+22iSIkn7!D;2KT1gF1;T`z6JfBDl<#~mL;9G5AD z)l4sbkFZjPj;NZo;zViRG)LL=HQ4ljluciUU3bDu--C@`hjJivEx8(R?>aExCl;e)r?AqCpwX`Z&0YKn1A+5P#i%-A9%&j`gcD!Oi7zWV4ZA%jI%oi2dl` zp3i<-Yf@K4GT@Fo?!XC)v=%qB;tlJdF}7ja&czWyE?>)(ku|d@iYpEs!~;mQjAf-7 zLAKNXCJQoY*!$0*izfy%<73nF&vD69ce3E{&vfX25#|4&EcKtipQ zC9=Ce`OeQbeEGZY9hhhe$Ao9IS+dzIqRL>t5TS@tH_m`;^Gn=z_U)5!RTtWNDo;QC zG(Z3O&jGmXvdcLC{PUsi7LvI~2!fk&r!0jXHM4r^L(qCUl|Outh8asR$vU`f8#h$r zpQqmhySiZa+eU!+{JUO)!OuXDW7(O)#)8XyfFL*0t|%m}H6nsLT_T_AN;t|fe z;Br{{JnUZ&nHlKJOUNJE!;Y=L<8ya-BUR`f9UZJ(xsqVpeYm|pfYiHS!N=k71}N-? zg&%{SCz0+yvUt&Q$o0YC&p@&@6=>i{jSMj61opu=ATr4W8PB0bEv+X}oNT9DK&4zF zG4O+tXI<{s$qx-e`x$`H@ZvJouYHgpGZhwpm~i@=d12Et$de!9%o!OTe8+lLo&RH2 zUHC)h%hqUyDt%B{4{jT@oQ6%$;`VQTmweAgbo#j@F8CBqP=KdC3mlHO@EEKyR75mH zv?4|;p05~3AIzVfsNG0nuDJYd6F|u4@)U-Ph$@-*1m&K8I)$O%%$^j0ZNE5|4c|t; zKS?EUk+Z+Zkq0uEm#*a6^9uaUhc9Q@(mHRQy%pAVetM~1ZGVw5Ycc$-5HpdFN&GG1@k1EybyEvL+nI zDj&XjwE7O<^tte@|EXh4SX)iYsYb@TE4=qAA(SX+U_4>(y=Aglj}YP1+3L?m2psq%~gJOwFY1}Xta?6)5mxmsDNNZ~rVm2alJ&w9lL9Ssm z`SmJ3k{ri?ka2~xvC4|7zay=njzL9g6of!092zM@w5YfchLLqy6KoB|<(S*aA*Xe# zqbiDsZ!O7kS+I^^RnaoPJaV?Xy!pw%4Qi5L%a<<~y}Vf-`q;l&jguN(4Rvf8$Z^Q) zq~B{l`BGV_y^RyHV`5-h^I9}t)wVS**pM;a_~=T#Abkao7S_f4(oAgD=}3iSw>Nde zCV%SA7#&SmYqNzsXdzCql|cg@>RaHt;W&ci*GKg4RxU~WXI>xW+1u%NApd2P-7 zp~gkfcxsa4A{xaYM8*&XM#dIdKb-Ac<;IfPWz<;lvEqREVsStmQH+eY;-Xac1!eyF zuIyjjN!p9{5{A)cDx(&QN+kRcK|^14xGty^{Bi*}CQ)Hg5N9z%b)EH&lr9X$pzzhk z*5K?g*#FIjxmS7U!Ve7X`BIg8wU&XyQFk`wyQH15oL{VzN0KB}0InYFzG+OVt_Cm> z9kJDZSvT{a0cJU6Z&Q#eHArY?Ib{zr5nyim=!hE~gcut_1VB`-FJgVA&`_ytKV{%Z z@2uyav~UzOBAj?no0NZC?}S~dsf&ijdWtV-;_NVJIVz?z5HfYB3-Iy2=YIRD+%HNx znT&mxi7Hga1)7G7#gXULB@tQ&6NA%WhFFyTPo^a%-zW85f5-K4@e_ylXUZX>fk@!n zFgJbZ*uh0FZ|y(x`?n5bLyNT*wTe|xs}g*v!P73a?pts|A@GHLC-aOuTYwy=f%8R@uep|J$r z!?3#_j_ifpK`0(mDg{e8YzfO24~uIR8z@>4LJKx9*otu(#1$01Ows9TgO%B+i1?!1 zm?hybaW`xA{)#KA#+0yb-97fWIK(oo3WTa45~=1mUP>hQMuVJHPPzEv|7d}$fKSD( z9RePQhBBNMK=%M>s02wUHCoCEOW<2<(PB#$Td`1#hh2_1ThtZ3vA&4}fi`G`ES46v zygu5Jr5CD>#a~_}(I>T038@+&>wfc-U~)}aYpPTQpmNFVTN_Yg2#t_Nm^lMj3_K0Y zhXb1vbpDSbWZ=<9m19$&*HtzhPsH*4M;mI Date: Sat, 23 Sep 2023 23:35:26 -0400 Subject: [PATCH 044/300] fixes --- lua/pac3/editor/client/settings.lua | 10 ++++++++ lua/pac3/extra/shared/net_combat.lua | 37 ++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index b533eb2b5..7fea74583 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -38,6 +38,7 @@ local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT local enforce_netrate_buffer = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 5000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'the budgeted allowance to limit how often pac combat net messages can be sent. 0 to disable, otherwise a number in bit size') local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") +local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') @@ -865,6 +866,15 @@ function pace.FillCombatSettings(pnl) sv_per_player_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_player_per_combat_operation") sv_per_player_ent_limit_numbox:SetTooltip("When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") + + local sv_player_fraction_slider = vgui.Create("DNumSlider", general_list_list) + sv_player_fraction_slider:SetText("block damage zones targeting this fraction of players") + sv_player_fraction_slider:SetValue(GetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone"):GetFloat()) + sv_player_fraction_slider:SetMin(0) sv_player_fraction_slider:SetDecimals(2) sv_player_fraction_slider:SetMax(1) + sv_player_fraction_slider:SetSize(400,30) + sv_player_fraction_slider:SetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone") + sv_player_fraction_slider:SetTooltip("This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.") + end do --hitscan diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 369fca3ad..a339e6cad 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -75,6 +75,8 @@ local contraption_classes = { local pre_excluded_ent_classes = { ["info_player_start"] = true, + ["aoc_spawnpoint"] = true, + ["info_player_teamspawn"] = true, ["env_tonemap_controller"] = true, ["env_fog_controller"] = true, ["env_skypaint"] = true, @@ -82,6 +84,21 @@ local pre_excluded_ent_classes = { ["env_sun"] = true, ["predicted_viewmodel"] = true, ["physgun_beam"] = true, + ["ambient_generic"] = true, + ["trigger_once"] = true, + ["trigger_multiple"] = true, + ["trigger_hurt"] = true, + ["info_ladder_dismount"] = true, + ["info_particle_system"] = true, + ["env_sprite"] = true, + ["env_fire"] = true, + ["env_soundscape"] = true, + ["env_smokestack"] = true, + ["light"] = true, + ["move_rope"] = true, + ["keyframe_rope"] = true, + ["env_soundscape_proxy"] = true, + ["gmod_hands"] = true, } @@ -216,16 +233,16 @@ if SERVER then local per_ply = per_ply_limit:GetInt() print(count .. " compared against hard limit " .. hard_limit .. " and " .. playercount .. " players*" .. per_ply .. " limit (" .. count*playercount .. " | " .. playercount*per_ply .. ")") if count > hard_limit then - MsgC(Color(255,0,0), "TOO MANY ENTS.\n") + MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond hard limit.\n") return true end if not game.SinglePlayer() then if count > per_ply_limit:GetInt() * playercount then - MsgC(Color(255,0,0), "TOO MANY ENTS.\n") + MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond per-player sending limit.\n") return true end if count * playercount > math.min(hard_limit, per_ply*playercount) then - MsgC(Color(255,0,0), "TOO MANY ENTS.\n") + MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond hard limit or player limit\n") return true end end @@ -586,20 +603,23 @@ if SERVER then end end - local function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + local function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply) local ent_count = 0 local ply_count = 0 local ply_prog_count = 0 for i,v in pairs(ents_hits) do - if pre_excluded_ent_classes[v:GetClass()] or (v:IsNPC() and not tbl.NPC) or (v:IsPlayer() and not tbl.Players) then ents_hits[i] = nil + if not (v:IsPlayer() or v:IsNPC() or string.find(v:GetClass(), "npc_")) and not tbl.PointEntities then ents_hits[i] = nil end + if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil else ent_count = ent_count + 1 + --print(v, "counted") if v:IsPlayer() then ply_count = ply_count + 1 end end end --dangerous conditions: absurd amounts of entities, damaging a large percentage of the server's players beyond a certain point if TooManyEnts(ent_count) or ((ply_count) > 12 and (ply_count > player_fraction:GetFloat() * player.GetCount())) then + print("early exit") return false,false,nil,{},{} end @@ -676,7 +696,8 @@ if SERVER then if IsEntity(ent) and ((damageable_point_ent_classes[ent:GetClass()] ~= false) or ((damageable_point_ent_classes[ent:GetClass()] == nil) or (damageable_point_ent_classes[ent:GetClass()] == true))) then --second pass: the damagezone's settings --1.don't hurt yourself - if (not tbl.AffectSelf) and ent == inflictor then --nothing + if (tbl.AffectSelf) and ent == inflictor then --nothing + canhit = true --2.main target types : players, NPC, point entities elseif ((ent:IsPlayer() and tbl.Players) or (tbl.NPC and (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity)) or tbl.PointEntities) and --one of the base classes @@ -819,7 +840,7 @@ if SERVER then end --the forward bullet, if applicable and no entity is found - if #ents_hits == 0 then + if ent_count == 0 then if tbl.Bullet then dmg_info:GetInflictor():FireBullets(bullet) end @@ -1522,7 +1543,7 @@ if SERVER then dmg_info:GetInflictor():FireBullets(bullet) end end - hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang) + hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply) highest_dmg = highest_dmg or 0 net.Start("pac_hit_results") net.WriteBool(hit) From 3bb9a9e9f6b3fe8526b7c3976ef583a97470cc70 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 23 Sep 2023 23:48:48 -0400 Subject: [PATCH 045/300] clarify comment on changed bit --- lua/pac3/extra/shared/net_combat.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index a339e6cad..907787995 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -695,8 +695,8 @@ if SERVER then --first pass: entity class blacklist if IsEntity(ent) and ((damageable_point_ent_classes[ent:GetClass()] ~= false) or ((damageable_point_ent_classes[ent:GetClass()] == nil) or (damageable_point_ent_classes[ent:GetClass()] == true))) then --second pass: the damagezone's settings - --1.don't hurt yourself - if (tbl.AffectSelf) and ent == inflictor then --nothing + --1.player hurt self if asked + if (tbl.AffectSelf) and ent == inflictor then canhit = true --2.main target types : players, NPC, point entities elseif ((ent:IsPlayer() and tbl.Players) or (tbl.NPC and (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity)) or tbl.PointEntities) From c6c530a241da960d608dcf30c163054295e86f53 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 25 Sep 2023 12:32:27 -0400 Subject: [PATCH 046/300] Update settings.lua fallback table if no part categories file --- lua/pac3/editor/client/settings.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 7fea74583..7643acce7 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -2184,7 +2184,7 @@ function pace.FillEditorSettings2(pnl) end local function load_partgroup_template_into_tree(categorytree, tbl) - + tbl = tbl or pace.partgroups or pace.partmenu_categories_default categorytree:Clear() for category,category_contents in pairs(tbl) do @@ -2327,4 +2327,5 @@ end if not file.Exists("pac_part_categories_default.txt", "DATA") then file.Write("pac3_config/pac_part_categories_default.txt", util.TableToKeyValues(pace.partmenu_categories_default)) end -decode_table_from_file("pac_part_categories") \ No newline at end of file +decode_table_from_file("pac_part_categories") +pace.partgroups = pace.partgroups or pace.partmenu_categories_default From 301210a4b7bc572413bbed112d9a706ec969f7a8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 25 Sep 2023 23:38:51 -0400 Subject: [PATCH 047/300] restore forgotten network strings --- lua/pac3/extra/shared/net_combat.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 907787995..101847492 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1545,7 +1545,7 @@ if SERVER then end hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply) highest_dmg = highest_dmg or 0 - net.Start("pac_hit_results") + net.Start("pac_hit_results", true) net.WriteBool(hit) net.WriteBool(kill) net.WriteFloat(highest_dmg) @@ -1559,6 +1559,11 @@ if SERVER then util.AddNetworkString("pac_request_position_override_on_entity_teleport") util.AddNetworkString("pac_request_position_override_on_entity_grab") util.AddNetworkString("pac_request_angle_reset_on_entity") + util.AddNetworkString("pac_lock_imposecalcview") + util.AddNetworkString("pac_signal_stop_lock") + util.AddNetworkString("pac_request_lock_break") + util.AddNetworkString("pac_mark_grabbed_ent") + util.AddNetworkString("pac_notify_grabbed_player") --The lock part grab request net message net.Receive("pac_request_position_override_on_entity_grab", function(len, ply) --server allow From 90d5695e1a3412e8c6f9710d0654ebbe97f3572d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 26 Sep 2023 16:49:28 -0400 Subject: [PATCH 048/300] trying a different validity check --- lua/pac3/editor/client/saved_parts.lua | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 8ab22522e..94ec4bb56 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -233,15 +233,13 @@ function pace.LoadParts(name, clear, override_part) local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") local has_possible_prop_pacs = false - - if IsValid(data) then - if istable(data) then - for i,part in pairs(data) do - if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end - end + + if data and istable(data) then + for i,part in pairs(data) do + print(part, part.self, part.seld) + if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end end end - --queue up prop pacs for the next prop or npc you spawn when in singleplayer if (auto_spawn_prop:GetInt() == 2 or (auto_spawn_prop:GetInt() == 1 and #data == 1)) and game.SinglePlayer() and has_possible_prop_pacs then From 8327a56d539aa83417a603b76d6ce7bc13eb302e Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 30 Sep 2023 15:18:30 -0400 Subject: [PATCH 049/300] more net limiters net rate limiter and allowance: -players have a limited amount of net messages they can send, and it regenerates over time -this can be monitored serverside to view who's making frequent net messages; or enforced by clients to save bandwidth. either way the server will check the allowance and do early returns to prevent the effect of those net messages small adjustments related to tree refresh: -run a timer to check if command part should actually run damage zone hitmarker reassignment (experimental): -instead of removing the parts and re-creating them (creating pac parts is expensive), hide and try to reassign dynamically --- lua/pac3/core/client/parts/command.lua | 5 +- lua/pac3/core/client/parts/damage_zone.lua | 250 +++++++++---- lua/pac3/core/client/parts/force.lua | 15 +- .../core/client/parts/health_modifier.lua | 5 + lua/pac3/core/client/parts/hitscan.lua | 7 +- lua/pac3/core/client/parts/lock.lua | 12 + lua/pac3/core/client/parts/proxy.lua | 23 +- lua/pac3/editor/client/panels/tree.lua | 6 +- lua/pac3/editor/client/saved_parts.lua | 1 - lua/pac3/editor/client/settings.lua | 124 ++++--- lua/pac3/editor/server/combat_bans.lua | 14 +- lua/pac3/extra/shared/net_combat.lua | 338 +++++++++++++----- 12 files changed, 558 insertions(+), 242 deletions(-) diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index f902b6475..8e320a7b3 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -25,7 +25,10 @@ end function PART:OnShow(from_rendering) if not from_rendering and self:GetExecuteOnShow() then - self:Execute() + timer.Simple(0, function() + if self.Hide or self:IsHidden() then return end + self:Execute() + end) end end diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 2306fd5f5..1efd24d82 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -242,19 +242,32 @@ local part_setup_runtimes = 0 } ]] +--[[ + owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} + owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} +]] -local function FindOrCreateFloatingPart(owner, part_uid) +function PART:FindOrCreateFloatingPart(owner, ent, part_uid, id) owner.hitmarker_partpool = owner.hitmarker_partpool or {} - for spec_uid,tbl in pairs(owner.hitmarker_partpool) do if tbl.template_uid == part_uid then if not tbl.active then - return part --whoowee we found an already existing part + return pac.GetPartFromUniqueID(pac.Hash(owner), spec_uid) --whoowee we found an already existing part end end end + --what if we don't! + local tbl = pac.GetPartFromUniqueID(pac.Hash(owner), part_uid):ToTable() + local group = pac.CreatePart("group", self:GetPlayerOwner()) print("\tcreated a group for " .. id) + group:SetShowInEditor(false) - return part + local part = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl)) + group:AddChild(part) + + group:CallRecursive("Think") + owner.hitmarker_partpool[group.UniqueID] = {active = true, hitmarker_id = id, template_uid = part_uid, group_part_data = group} + + return group, owner.hitmarker_partpool[group.UniqueID] end @@ -272,73 +285,125 @@ local function FreeSpotInStack(owner) return nil end -local function MatchInStack(owner, ent) +--[[ + owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} + owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} +]] + +local function MatchInStack(owner, ent, uid, id) owner.hitparts = owner.hitparts or {} for i=1,20,1 do if owner.hitparts[i] then + if owner.hitparts[i].template_uid == ent.template_uid and owner.hitparts[i].hitmarker_id == ent.marker_id then + return i + end --match: entry's template uid is the same as entity's template uid --if there's more, still match entry's specimen ID with specimen ID - if owner.hitparts[i].pac_hitmarker_template_id == ent.pac_hitmarker_template_id and owner.hitparts[i].hitmarker_id == ent.marker_id then - return i + end + end + + return nil +end + +local function UIDMatchInStackForExistingPart(owner, ent, part_uid, ent_id) + owner.hitparts = owner.hitparts or {} + for i=1,20,1 do + if owner.hitparts[i] then + --print(i, "match compare:", owner.hitparts[i].active, owner.hitparts[i].specimen_part, owner.hitparts[i].hitmarker_id, owner.hitparts[i].template_uid == part_uid) + if owner.hitparts[i].template_uid == part_uid then + if owner.hitmarker_partpool then + for spec_uid,tbl in pairs(owner.hitmarker_partpool) do + if tbl.template_uid == part_uid then + if not tbl.active then + return tbl.group_part_data + end + end + end + end end end end + return nil end -local function AddHitMarkerToStack(owner, ent, part_uid, ent_id) +--[[ + owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} + owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} +]] +function PART:AddHitMarkerToStack(owner, ent, part_uid, ent_id) owner.hitparts = owner.hitparts or {} - local free = FreeSpotInStack() - if free then - owner.hitparts[free] = {active = true, specimen_part = FindOrCreateFloatingPart(ent, part_uid), hitmarker_id = ent_id, template_uid = part_uid} + local free = FreeSpotInStack(owner) + local returned_part = nil + local existingpart = UIDMatchInStackForExistingPart(owner, ent, part_uid, ent_id) + returned_part = existingpart + + if free and not existingpart then + local group, tbl = self:FindOrCreateFloatingPart(owner, ent, part_uid, ent_id) + owner.hitparts[free] = {active = true, specimen_part = group, hitmarker_id = ent_id, template_uid = part_uid} + returned_part = owner.hitparts[free].specimen_part + else + owner.hitparts[free] = {active = true, specimen_part = returned_part, hitmarker_id = ent_id, template_uid = part_uid} end + + return returned_part end -local function RemoveHitMarker(owner, ent) +local function RemoveHitMarker(owner, ent, uid, id) owner.hitparts = owner.hitparts or {} - local match = MatchInStack(owner, ent) + + local match = MatchInStack(owner, ent, uid, id) if match then - owner.hitparts[match].active = false + if owner.hitparts[match] then + owner.hitparts[match].active = false + end + end + if owner.hitmarker_partpool then + for spec_uid,tbl in pairs(owner.hitmarker_partpool) do + if tbl.hitmarker_id == id then + tbl.active = false + tbl.group_part_data:SetHide(true) + tbl.group_part_data:SetShowInEditor(false) + tbl.group_part_data:SetOwner(owner) + print(tbl.group_part_data, "dormant") + end + end end - SafeRemoveEntity(ent) + --SafeRemoveEntity(ent) end ---grabbed the function from projectile.lua -function PART:AttachToEntity(part, ent, parent_ent, marker_id) - if not part:IsValid() then return false end +--[[ + owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} + owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} +]] +function PART:AssignFloatingPartToEntity(part, owner, ent, parent_ent, template_uid, marker_id) + if not IsValid(part) then return false end ent.pac_draw_distance = 0 - local tbl = part:ToTable() - - local group = pac.CreatePart("group", self:GetPlayerOwner()) - group:SetShowInEditor(false) + local group = part + local part2 = group:GetChildrenList()[1] - local part = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl)) - group:AddChild(part) + group:CallRecursive("Think") - group:SetOwner(ent) - group.SetOwner = function(s) s.Owner = ent end - part:SetHide(false) + owner.hitmarker_partpool[group.UniqueID] = {active = true, hitmarker_id = marker_id, template_uid = template_uid, group_part_data = group} + owner.hitparts[FreeSpotInStack(owner) or 1] = {active = true, specimen_part = group, hitmarker_id = marker_id, template_uid = template_uid} - local id = group.Id - local owner_id = self:GetPlayerOwnerId() - if owner_id then - id = id .. owner_id - end - - ent:CallOnRemove("pac_dmgzone_hitmarker_" .. id, function() - group:Remove() - end) - group:CallRecursive("Think") - parent_ent.pac_dmgzone_hitmarker_ents = parent_ent.pac_dmgzone_hitmarker_ents or {} ent.part = group - ent.pac_hitmarker_template_id = part.UniqueID + ent.parent_ent = parent_ent + ent.template_uid = template_uid parent_ent.pac_dmgzone_hitmarker_ents[marker_id] = ent ent.marker_id = marker_id - return true + group:SetShowInEditor(false) + group:SetOwner(ent) + group.Owner = ent + group:SetHide(false) + part2:SetHide(false) + group:CallRecursive("Think") + print(group, "assigned to " .. marker_id .. " / " .. parent_ent:EntIndex()) + end local function RecursedHitmarker(part) @@ -428,13 +493,17 @@ local hitbox_ids = { --more compressed net message function PART:SendNetMessage() + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if not GetConVar('pac_sv_damage_zone'):GetBool() then return end if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end end + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") end - if not GetConVar("pac_sv_combat_enforce_netrate"):GetInt() == 0 then return end + if GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end net.Start("pac_request_zone_damage", true) @@ -518,9 +587,7 @@ function PART:OnShow() if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker --what if people employ a more roundabout method? CRACKDOWN! - if not owner.hitparts then owner.hitparts = {} end - if not owner.hitparts_floatingparts then owner.hitparts_floatingparts = {} end if owner.stop_hit_markers_until then if owner.stop_hit_markers_until > CurTime() then return end @@ -529,19 +596,6 @@ function PART:OnShow() if not self:IsValid() then return end if not part:IsValid() then return end - if not HasBudget(owner, part) then - local overbudget = math.abs(owner.pac_dmgzone_hitmarker_budget - CalculateHitMarkerPrice(part)) - if overbudget > 10000 then - self:LaunchAuditAndEnforceSoftBan(overbudget) - end - --print(owner, "is trying to go over budget by " .. string.NiceSize(math.abs(overbudget))) - return - else - timer.Simple(1, function() self:ClearBudgetAdmonishmentWarning() end) - local budget_add = CalculateHitMarkerPrice(part) - --print(owner, "is within budget with " .. owner.pac_dmgzone_hitmarker_budget .. " left. can add " .. string.NiceSize(budget_add)) - owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget - CalculateHitMarkerPrice(part) - end local start = SysTime() local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") @@ -551,6 +605,7 @@ function PART:OnShow() ent:SetPos(pos) ent:SetAngles(ang) global_hitmarker_CSEnt_seed = global_hitmarker_CSEnt_seed + 1 + local csent_id = global_hitmarker_CSEnt_seed --the spawn order needs to decide whether it can or can't create an ent or part @@ -558,44 +613,49 @@ function PART:OnShow() if flush then --go through the entity and remove the clientside hitmarkers entities if parent_ent.pac_dmgzone_hitmarker_ents then - for id,ent in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do - if IsValid(ent) then - if ent.part:IsValid() then - owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget + CalculateHitMarkerPrice(part) - ent.part:Remove() + for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do + if IsValid(ent2) then + if ent2.part:IsValid() then + --owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget + CalculateHitMarkerPrice(part) + ent2.part:SetHide(true) end - RemoveHitMarker(ent, part.UniqueID, id) + --RemoveHitMarker(owner, ent, part.UniqueID, id) end end end end + if FreeSpotInStack(owner) then - if self:AttachToEntity(part, ent, parent_ent, global_hitmarker_CSEnt_seed) then + if part:IsValid() then --self:AttachToEntity(part, ent, parent_ent, global_hitmarker_CSEnt_seed) + local newpart + local bool = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) + + newpart = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) or self:AddHitMarkerToStack(owner, ent, part.UniqueID, csent_id) + + self:AssignFloatingPartToEntity(newpart, owner, ent, parent_ent, part.UniqueID, csent_id) + + MsgC(bool and Color(0,255,0) or Color(0,200,255), bool and "existing" or "created", " : ", newpart, "\n") timer.Simple(math.Clamp(duration, 0, 8), function() if ent:IsValid() then if parent_ent.pac_dmgzone_hitmarker_ents then - for id,ent in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do - if IsValid(ent) then - if ent.part:IsValid() then - owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget + CalculateHitMarkerPrice(part) - ent.part:Remove() - end - RemoveHitMarker(ent, part.UniqueID, id) + for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do + if IsValid(ent2) then + RemoveHitMarker(owner, ent2, part.UniqueID, id) + --SafeRemoveEntity(ent2) end end end - - timer.Simple(0.5, function() - RemoveHitMarker(ent, part.UniqueID, id) - end) + --[[timer.Simple(0.5, function() + RemoveHitMarker(owner, ent, part.UniqueID, csent_id) + SafeRemoveEntity(ent) + end)]] end end) end end - + local creation_delta = SysTime() - start - --print(string.NiceTime(creation_delta)) - --print(100000 * creation_delta / CalculateHitMarkerPrice(part) .. "budget score") + return creation_delta end @@ -644,10 +704,43 @@ function PART:OnShow() if IsValid(self.HitMarkerPart) then --print("runtimes were " .. string.FormattedTime( part_setup_runtimes).ms .. "ms.\n\t" .. 1000*part_setup_runtimes .. " impact.\n\t" .. table.Count(self.HitMarkerPart:GetChildrenList()) .. " children, " .. 1000*part_setup_runtimes / table.Count(self.HitMarkerPart:GetChildrenList()) .. " impact per children") end + if owner.hitparts then + for i,v in ipairs(owner.hitparts) do + --print(i,v,v.active,v.specimen_part,v.hitmarker_id,v.template_uid) + end + self:SetInfo(table.Count(owner.hitparts) .. " hitmarkers in slot") + end + end) - + end +concommand.Add("pac_cleanup_hitmarks", function() + if LocalPlayer().hitparts then + for i,v in pairs(LocalPlayer().hitparts) do + v.specimen_part:Remove() + end + end + + LocalPlayer().hitmarker_partpool = nil + LocalPlayer().hitparts = nil +end) + +concommand.Add("pac_count_inactive_hitmarks", function() + print("first the markers") + if LocalPlayer().hitparts then + for i,v in pairs(LocalPlayer().hitparts) do + print(i,v.active) + end + end + print("second the partpool") + if LocalPlayer().hitmarker_partpool then + for i,v in pairs(LocalPlayer().hitmarker_partpool) do + print(i,v.active, pac.GetPartFromUniqueID(pac.Hash(LocalPlayer()),v.spec_uid)) + end + end +end) + function PART:OnHide() hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) for _,v in pairs(renderhooks) do @@ -920,4 +1013,5 @@ function PART:Initialize() self.validTime = SysTime() + 2 end + BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 49ef7a2ca..2e1ddb9dd 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -31,10 +31,15 @@ BUILDER:StartStorableVars() :GetSet("Continuous", true, {description = "If set to false, the force will be a single, stronger impulse"}) :GetSet("AccountMass", false, {description = "Apply acceleration according to mass."}) :GetSet("Falloff", false, {description = "Whether the force to apply should fade with distance"}) + :GetSet("ReverseFalloff", false, {description = "The reverse of the falloff means the force fades when getting closer."}) + :GetSet("Damping", 0, {editor_clamp = {0,1}, editor_sensitivity = 0.1}) + :GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part"}) + :GetSet("LevitationHeight", 0) :SetPropertyGroup("Targets") :GetSet("AffectSelf",false) :GetSet("Players",true) :GetSet("PhysicsProps", true) + :GetSet("PointEntities",true, {description = "other entities not covered by physics props but with potential physics"}) :GetSet("NPC",false) :EndStorableVars() @@ -141,13 +146,17 @@ function PART:OnThink() end function PART:Impulse(on) - self.next_impulse = CurTime() + 0.1 + self.next_impulse = CurTime() + 0.05 if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if not on and not self.Continuous then return end if not GetConVar("pac_sv_force"):GetBool() then return end + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end end + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end local locus_pos = Vector(0,0,0) if self.Locus ~= nil then @@ -184,10 +193,14 @@ function PART:Impulse(on) net.WriteInt(self.BaseForce, 18) net.WriteVector(self.AddedVectorForce) net.WriteVector(self.Torque) + net.WriteUInt(self.Damping*1000, 10) + net.WriteInt(self.LevitationHeight,14) net.WriteBool(self.Continuous) net.WriteBool(self.AccountMass) net.WriteBool(self.Falloff) + net.WriteBool(self.ReverseFalloff) + net.WriteBool(self.Levitation) net.WriteBool(self.AffectSelf) net.WriteBool(self.Players) net.WriteBool(self.PhysicsProps) diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 9b771b1ad..e6fdee83c 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -35,9 +35,13 @@ function PART:SendModifier(str) if self:IsHidden() then return end if LocalPlayer() ~= self:GetPlayerOwner() then return end if not GetConVar("pac_sv_health_modifier"):GetBool() then return end + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end end + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end if str == "MaxHealth" and self.ChangeHealth then net.Start("pac_request_healthmod") @@ -162,4 +166,5 @@ function PART:Initialize() if not GetConVar("pac_sv_health_modifier"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("health modifiers are disabled on this server!") end end + BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index f9ae0e939..0f75ea085 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -13,7 +13,7 @@ BUILDER:StartStorableVars() :GetSet("ServerBullets", true, {description = "serverside bullets can do damage and exert a physical impact force"}) :SetPropertyGroup("bullet properties") :GetSet("BulletImpact", false) - :GetSet("Damage", 0, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,268435455)) end}) + :GetSet("Damage", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,268435455)) end}) :GetSet("Force",1000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end}) :GetSet("AffectSelf", false, {description = "whether to allow to damage yourself"}) :GetSet("DamageFalloff", false, {description = "enable damage falloff. The lowest damage is not a fixed damage number, but a fraction of the total initial damage.\nThe server can still restrict the maximum distance of all bullets"}) @@ -195,9 +195,13 @@ local tracer_ids = { function PART:SendNetMessage() if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if not GetConVar('pac_sv_hitscan'):GetBool() then return end + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts[self.ClassName] then return end + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end net.Start("pac_hitscan", true) net.WriteBool(self.AffectSelf) @@ -223,5 +227,6 @@ function PART:SendNetMessage() net.SendToServer() end + BUILDER:Register() diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 58f80503a..4aa13eacf 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -62,9 +62,11 @@ BUILDER:EndStorableVars() function PART:OnThink() if not GetConVar('pac_sv_lock'):GetBool() then return end + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end end + if self.forcebreak then if self.next_allowed_grab < CurTime() then --we're able to resume if self.ContinuousSearch then @@ -120,6 +122,9 @@ function PART:OnThink() local relative_offset_ang = offset_matrix:GetAngles() if pac.LocalPlayer == self:GetPlayerOwner() then + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end net.Start("pac_request_position_override_on_entity_grab") net.WriteBool(self.is_first_time) net.WriteString(self.UniqueID) @@ -342,6 +347,9 @@ function PART:OnShow() end end if self.SlopeSafety then teleport_pos_final = teleport_pos_final + Vector(0,0,30) end + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end net.Start("pac_request_position_override_on_entity_teleport") net.WriteString(self.UniqueID) net.WriteVector(teleport_pos_final) @@ -372,6 +380,9 @@ function PART:reset_ent_ang() if reset_ent:IsValid() then timer.Simple(math.min(self.RestoreDelay,5), function() if pac.LocalPlayer == self:GetPlayerOwner() then + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end net.Start("pac_request_angle_reset_on_entity") net.WriteAngle(Angle(0,0,0)) net.WriteFloat(self.RestoreDelay) @@ -504,4 +515,5 @@ function PART:Initialize() if not GetConVar('pac_sv_lock'):GetBool() then self:SetError("lock parts are disabled on this server!") end end + BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 9b05053c4..d2e0475fc 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -44,10 +44,10 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Pow", 1) BUILDER:SetPropertyGroup("behavior") - BUILDER:GetSet("Additive", false) - BUILDER:GetSet("PlayerAngles", false) - BUILDER:GetSet("ZeroEyePitch", false) - BUILDER:GetSet("ResetVelocitiesOnHide", true) + BUILDER:GetSet("Additive", false, {description = "This means that every computation frame, the proxy will add its output to its current stored memory. This can quickly get out of control if you don't know what you're doing! This is like using the feedback() function"}) + BUILDER:GetSet("PlayerAngles", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will choose between the owner entity's Angles or EyeAngles. Unsure of whether this makes a difference."}) + BUILDER:GetSet("ZeroEyePitch", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will force the angle to be horizon level."}) + BUILDER:GetSet("ResetVelocitiesOnHide", true, {description = "Because velocity calculators use smoothing that makes the output converge toward a crude rolling average, it might matter whether you want to get a clean slate readout.\n(VelocityRoughness is how close to the snapshots it will be. Lower means smoother but delayed. Higher means less smoothing but it might overshoot and be inaccurate because of frame time works and varies)"}) BUILDER:GetSet("VelocityRoughness", 10) BUILDER:EndStorableVars() @@ -357,22 +357,25 @@ end PART.Inputs.part_distance = function(self, uid1, uid2) if not uid1 or not uid2 then return 0 end + local owner = self:GetPlayerOwner() - local PartA = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid1, self) end + local PartA = pac.GetPartFromUniqueID(pac.Hash(self:GetPlayerOwner()), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) + if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end - local PartB = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid2) - if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid2, self) end + local PartB = pac.GetPartFromUniqueID(pac.Hash(self:GetPlayerOwner()), uid2) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid2) + if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(owner), uid2, self) end if not PartA:IsValid() or not PartB:IsValid() then return 0 end + if not PartA.Position or not PartB.Position then return 0 end return (PartB:GetWorldPosition() - PartA:GetWorldPosition()):Length() end PART.Inputs.event_alternative = function(self, uid1, num1, num2) if not uid1 then return 0 end + local owner = self:GetPlayerOwner() - local PartA = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid1, self) end + local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) + if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end if PartA.ClassName == "event" then if PartA.event_triggered then return num1 or 0 diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index 44925fc84..ba24b1056 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -208,7 +208,7 @@ function PANEL:OnMouseReleased(mc) end function PANEL:SetModel(path) - if not file.Exists(path, "GAME") then + if not file.Exists(path or "", "GAME") then path = player_manager.TranslatePlayerModel(path) if not file.Exists(path, "GAME") then print(path, "is invalid") @@ -583,10 +583,10 @@ pac.AddHook("pace_OnVariableChanged", "pace_create_tree_nodes", function(part, k end) local function refresh_events_gated() - pace.final_scheduled_event_refresh = pace.final_scheduled_event_refresh or CurTime() + 0.2 + pace.final_scheduled_event_refresh = pace.final_scheduled_event_refresh or CurTime() + 0.08 pace.event_refresh_spam_time = CurTime() hook.Add("Tick", "pace_refresh_events", function() - if CurTime() < pace.event_refresh_spam_time + 0.2 then return end + if CurTime() < pace.event_refresh_spam_time + 0.08 then return end if CurTime() > pace.final_scheduled_event_refresh then pace.RefreshEvents() pace.final_scheduled_event_refresh = nil diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 94ec4bb56..0e6564104 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -236,7 +236,6 @@ function pace.LoadParts(name, clear, override_part) if data and istable(data) then for i,part in pairs(data) do - print(part, part.self, part.seld) if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end end end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 7643acce7..b57e92529 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -3,45 +3,46 @@ include("shortcuts.lua") local pac_submit_spam = CreateConVar('pac_submit_spam', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'Prevent users from spamming pac_submit') local pac_submit_limit = CreateConVar('pac_submit_limit', '30', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'pac_submit spam limit') -local hitscan_allow = CreateConVar('pac_sv_hitscan', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow hitscan parts serverside') -local hitscan_max_bullets = CreateConVar('pac_sv_hitscan_max_bullets', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum number of bullets') -local hitscan_max_damage = CreateConVar('pac_sv_hitscan_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum damage') -local hitscan_spreadout_dmg = CreateConVar('pac_sv_hitscan_divide_max_damage_by_max_bullets', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether or not force hitscans to divide their damage among the number of bullets fired') - -local damagezone_allow = CreateConVar('pac_sv_damage_zone', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow damage zone parts serverside') -local damagezone_max_damage = CreateConVar('pac_sv_damage_zone_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum damage') -local damagezone_max_length = CreateConVar('pac_sv_damage_zone_max_length', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum length') -local damagezone_max_radius = CreateConVar('pac_sv_damage_zone_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum radius') -local damagezone_allow_dissolve = CreateConVar('pac_sv_damage_zone_allow_dissolve', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to enable entity dissolvers and removing NPCs\' weapons on death for damagezone') - -local lock_allow = CreateConVar('pac_sv_lock', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock parts serverside') -local lock_allow_grab = CreateConVar('pac_sv_lock_grab', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part grabs serverside') -local lock_allow_teleport = CreateConVar('pac_sv_lock_teleport', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part teleports serverside') -local lock_max_radius = CreateConVar('pac_sv_lock_max_grab_radius', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'lock part maximum grab radius') -local lock_allow_grab_ply = CreateConVar('pac_sv_lock_allow_grab_ply', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing players with lock part') -local lock_allow_grab_npc = CreateConVar('pac_sv_lock_allow_grab_npc', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing NPCs with lock part') -local lock_allow_grab_ent = CreateConVar('pac_sv_lock_allow_grab_ent', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing other entities with lock part') - -local force_allow = CreateConVar('pac_sv_force', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow force parts serverside') -local force_max_length = CreateConVar('pac_sv_force_max_length', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum length') -local force_max_radius = CreateConVar('pac_sv_force_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum radius') -local force_max_amount = CreateConVar('pac_sv_force_max_amount', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum amount of force') - -local healthmod_allow = CreateConVar('pac_sv_health_modifier', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow health modifier parts serverside') -local healthmod_allowed_extra_bars = CreateConVar('pac_sv_health_modifier_extra_bars', 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Extra health bars') -local healthmod_allow_change_maxhp = CreateConVar('pac_sv_health_modifier_allow_maxhp', 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow players to change their maximum health and armor.') -local healthmod_minimum_dmgscaling = CreateConVar('pac_sv_health_modifier_min_damagescaling', -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Minimum health modifier amount. Negative values can heal.') - -local master_init_featureblocker = CreateConVar('pac_sv_block_combat_features_on_next_restart', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on') - -local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds') -local enforce_netrate_buffer = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 5000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'the budgeted allowance to limit how often pac combat net messages can be sent. 0 to disable, otherwise a number in bit size') +local hitscan_allow = CreateConVar("pac_sv_hitscan", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside") +local hitscan_max_bullets = CreateConVar("pac_sv_hitscan_max_bullets", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum number of bullets") +local hitscan_max_damage = CreateConVar("pac_sv_hitscan_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum damage") +local hitscan_spreadout_dmg = CreateConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not force hitscans to divide their damage among the number of bullets fired") + +local damagezone_allow = CreateConVar("pac_sv_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside") +local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum damage") +local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length") +local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius") +local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone") + +local lock_allow = CreateConVar("pac_sv_lock", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") +local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") +local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside") +local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius") +local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing players with lock part") +local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part") +local lock_allow_grab_ent = CreateConVar("pac_sv_lock_allow_grab_ent", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing other entities with lock part") + +local force_allow = CreateConVar("pac_sv_force", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside") +local force_max_length = CreateConVar("pac_sv_force_max_length", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum length") +local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum radius") +local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force") + +local healthmod_allow = CreateConVar("pac_sv_health_modifier", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") +local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") +local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") +local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") + +local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") + +local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter") +local netrate_allowance = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 60, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "the budgeted allowance to limit how many pac combat net messages can be sent in bursts. 0 to disable, otherwise a number of net messages of allowance.") +local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_netrate_monitor_serverside", 0, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not to let clients enforce their net message rates.\nSet this to 1 to get serverside prints telling you whenever someone is going over their allowance, but it'll still take the network bandwidth.\nSet this to 0 to let clients enforce their net rate and save some bandwidth but the server won't know who's spamming net messages.") local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") -local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') -local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') +local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") +local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") include("pac3/editor/server/combat_bans.lua") @@ -809,7 +810,7 @@ function pace.FillCombatSettings(pnl) master_list:Dock(FILL) --general do - local general_list = master_list:Add("General") + local general_list = master_list:Add("General (Global policy and Network protection)") general_list.Header:SetSize(40,40) general_list.Header:SetFont("DermaLarge") local general_list_list = vgui.Create("DListLayout") @@ -817,38 +818,44 @@ function pace.FillCombatSettings(pnl) general_list:SetContents(general_list_list) local sv_prop_protection_props_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_prop_protection_props_box:SetText("Enforce generic prop protection for player-owned props and physics entities.\nRelated to client consents, but the policies for each part are not uniform.") - sv_prop_protection_props_box:SetSize(400,30) - sv_prop_protection_props_box:SetConVar("pac_sv_prop_protection") + sv_prop_protection_props_box:SetText("Enforce generic prop protection for player-owned props and physics entities.\nRelated to client consents, but the policies for each part are not uniform.") + sv_prop_protection_props_box:SetSize(400,30) + sv_prop_protection_props_box:SetConVar("pac_sv_prop_protection") local sv_combat_whitelisting_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_combat_whitelisting_box:SetText("Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.") - sv_combat_whitelisting_box:SetSize(400,30) - sv_combat_whitelisting_box:SetConVar("pac_sv_combat_whitelisting") - sv_combat_whitelisting_box:SetTooltip("off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed") + sv_combat_whitelisting_box:SetText("Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.") + sv_combat_whitelisting_box:SetSize(400,30) + sv_combat_whitelisting_box:SetConVar("pac_sv_combat_whitelisting") + sv_combat_whitelisting_box:SetTooltip("off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed") local sv_master_break_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_master_break_box:SetText("Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts") - sv_master_break_box:SetSize(400,30) - sv_master_break_box:SetConVar("pac_sv_block_combat_features_on_next_restart") - sv_master_break_box:SetTooltip("You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers") + sv_master_break_box:SetText("Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts") + sv_master_break_box:SetSize(400,30) + sv_master_break_box:SetConVar("pac_sv_block_combat_features_on_next_restart") + sv_master_break_box:SetTooltip("You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers") + + local sv_netrate_monitoring_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_netrate_monitoring_box:SetText("Enable serverside monitoring prints for allowance and rate limiters") + sv_netrate_monitoring_box:SetSize(400,30) + sv_netrate_monitoring_box:SetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") + sv_netrate_monitoring_box:SetTooltip("Enable serverside monitoring prints.\n0=let clients enforce their netrate allowance before sending messages\n1=the server will receive net messages and print the outcome.") local sv_netrate_time_numbox = vgui.Create("DNumSlider", general_list_list) - sv_netrate_time_numbox:SetText("Client net message limit (milliseconds delay)") + sv_netrate_time_numbox:SetText("Rate limiter (milliseconds)") sv_netrate_time_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate"):GetInt()) sv_netrate_time_numbox:SetMin(0) sv_netrate_time_numbox:SetDecimals(0) sv_netrate_time_numbox:SetMax(1000) sv_netrate_time_numbox:SetSize(400,30) sv_netrate_time_numbox:SetConVar("pac_sv_combat_enforce_netrate") - sv_netrate_time_numbox:SetTooltip("Not yet implemented! Leave it at 0 for now.") + sv_netrate_time_numbox:SetTooltip("The milliseconds delay between net messages.\nIf this is 0, the allowance won't matter, otherwise early net messages use up the player's allowance.\nThe allowance regenerates gradually when unused, and one unit gets spent if the message is earlier than the rate limiter's delay.") local sv_netrate_buffer_numbox = vgui.Create("DNumSlider", general_list_list) - sv_netrate_buffer_numbox:SetText("Client net message limit (buffer size / bit budget per second)") + sv_netrate_buffer_numbox:SetText("Allowance, in number of messages") sv_netrate_buffer_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt()) - sv_netrate_buffer_numbox:SetMin(0) sv_netrate_buffer_numbox:SetDecimals(0) sv_netrate_buffer_numbox:SetMax(50000) + sv_netrate_buffer_numbox:SetMin(0) sv_netrate_buffer_numbox:SetDecimals(0) sv_netrate_buffer_numbox:SetMax(400) sv_netrate_buffer_numbox:SetSize(400,30) sv_netrate_buffer_numbox:SetConVar("pac_sv_combat_enforce_netrate_buffersize") - sv_netrate_buffer_numbox:SetTooltip("Not yet implemented!") + sv_netrate_buffer_numbox:SetTooltip("Allowance:\nIf this is 0, only the time limiter will stop pac combat messages if they're too fast.\nOtherwise, players trying to use a pac combat message earlier will deduct 1 from the player's allowance, and only stop the messages if the allowance reaches 0.") local sv_hard_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) sv_hard_ent_limit_numbox:SetText("Hard entity limit to cutoff damage zones and force parts") @@ -866,7 +873,6 @@ function pace.FillCombatSettings(pnl) sv_per_player_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_player_per_combat_operation") sv_per_player_ent_limit_numbox:SetTooltip("When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") - local sv_player_fraction_slider = vgui.Create("DNumSlider", general_list_list) sv_player_fraction_slider:SetText("block damage zones targeting this fraction of players") sv_player_fraction_slider:SetValue(GetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone"):GetFloat()) @@ -900,7 +906,7 @@ function pace.FillCombatSettings(pnl) local hitscans_max_dmg_numbox = vgui.Create("DNumSlider", hitscans_list_list) hitscans_max_dmg_numbox:SetText("Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)") hitscans_max_dmg_numbox:SetValue(GetConVar("pac_sv_hitscan_max_damage"):GetInt()) - hitscans_max_dmg_numbox:SetMin(0) hitscans_max_dmg_numbox:SetDecimals(0) hitscans_max_dmg_numbox:SetMax(1000000) + hitscans_max_dmg_numbox:SetMin(0) hitscans_max_dmg_numbox:SetDecimals(0) hitscans_max_dmg_numbox:SetMax(268435455) hitscans_max_dmg_numbox:SetSize(400,30) hitscans_max_dmg_numbox:SetConVar("pac_sv_hitscan_max_damage") @@ -989,21 +995,21 @@ function pace.FillCombatSettings(pnl) local max_dmgzone_radius_numbox = vgui.Create("DNumSlider", damagezone_list_list) max_dmgzone_radius_numbox:SetText("Max damage zone radius") max_dmgzone_radius_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_radius"):GetInt()) - max_dmgzone_radius_numbox:SetMin(0) max_dmgzone_radius_numbox:SetDecimals(0) max_dmgzone_radius_numbox:SetMax(50000) + max_dmgzone_radius_numbox:SetMin(0) max_dmgzone_radius_numbox:SetDecimals(0) max_dmgzone_radius_numbox:SetMax(32767) max_dmgzone_radius_numbox:SetSize(400,30) max_dmgzone_radius_numbox:SetConVar("pac_sv_damage_zone_max_radius") local max_dmgzone_length_numbox = vgui.Create("DNumSlider", damagezone_list_list) max_dmgzone_length_numbox:SetText("Max damage zone length") max_dmgzone_length_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_length"):GetInt()) - max_dmgzone_length_numbox:SetMin(0) max_dmgzone_length_numbox:SetDecimals(0) max_dmgzone_length_numbox:SetMax(50000) + max_dmgzone_length_numbox:SetMin(0) max_dmgzone_length_numbox:SetDecimals(0) max_dmgzone_length_numbox:SetMax(32767) max_dmgzone_length_numbox:SetSize(400,30) max_dmgzone_length_numbox:SetConVar("pac_sv_damage_zone_max_length") local max_dmgzone_damage_numbox = vgui.Create("DNumSlider", damagezone_list_list) max_dmgzone_damage_numbox:SetText("Max damage zone damage") max_dmgzone_damage_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_damage"):GetInt()) - max_dmgzone_damage_numbox:SetMin(0) max_dmgzone_damage_numbox:SetDecimals(0) max_dmgzone_damage_numbox:SetMax(100000000) + max_dmgzone_damage_numbox:SetMin(0) max_dmgzone_damage_numbox:SetDecimals(0) max_dmgzone_damage_numbox:SetMax(268435455) max_dmgzone_damage_numbox:SetSize(400,30) max_dmgzone_damage_numbox:SetConVar("pac_sv_damage_zone_max_damage") @@ -1076,14 +1082,14 @@ function pace.FillCombatSettings(pnl) local max_force_radius_numbox = vgui.Create("DNumSlider", force_list_list) max_force_radius_numbox:SetText("Max force part radius") max_force_radius_numbox:SetValue(GetConVar("pac_sv_force_max_radius"):GetInt()) - max_force_radius_numbox:SetMin(0) max_force_radius_numbox:SetDecimals(0) max_force_radius_numbox:SetMax(50000) + max_force_radius_numbox:SetMin(0) max_force_radius_numbox:SetDecimals(0) max_force_radius_numbox:SetMax(32767) max_force_radius_numbox:SetSize(400,30) max_force_radius_numbox:SetConVar("pac_sv_force_max_radius") local max_force_length_numbox = vgui.Create("DNumSlider", force_list_list) max_force_length_numbox:SetText("Max force part length") max_force_length_numbox:SetValue(GetConVar("pac_sv_force_max_length"):GetInt()) - max_force_length_numbox:SetMin(0) max_force_length_numbox:SetDecimals(0) max_force_length_numbox:SetMax(50000) + max_force_length_numbox:SetMin(0) max_force_length_numbox:SetDecimals(0) max_force_length_numbox:SetMax(32767) max_force_length_numbox:SetSize(400,30) max_force_length_numbox:SetConVar("pac_sv_force_max_length") diff --git a/lua/pac3/editor/server/combat_bans.lua b/lua/pac3/editor/server/combat_bans.lua index c83c72595..635f44c52 100644 --- a/lua/pac3/editor/server/combat_bans.lua +++ b/lua/pac3/editor/server/combat_bans.lua @@ -34,15 +34,17 @@ local function load_table_from_file() end end +if SERVER then + util.AddNetworkString("pac.BanUpdate") + util.AddNetworkString("pac.RequestBanStates") + util.AddNetworkString("pac.SendBanStates") -util.AddNetworkString("pac.BanUpdate") -util.AddNetworkString("pac.RequestBanStates") -util.AddNetworkString("pac.SendBanStates") + util.AddNetworkString("pac.CombatBanUpdate") + util.AddNetworkString("pac.SendCombatBanStates") + util.AddNetworkString("pac.RequestCombatBanStates") +end -util.AddNetworkString("pac.CombatBanUpdate") -util.AddNetworkString("pac.SendCombatBanStates") -util.AddNetworkString("pac.RequestCombatBanStates") net.Receive("pac.CombatBanUpdate", function() --get old states first diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 101847492..77d1fd6fe 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -7,46 +7,47 @@ end pac.global_combat_whitelist = pac.global_combat_whitelist or {} -local hitscan_allow = CreateConVar('pac_sv_hitscan', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow hitscan parts serverside') -local hitscan_max_bullets = CreateConVar('pac_sv_hitscan_max_bullets', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum number of bullets') -local hitscan_max_damage = CreateConVar('pac_sv_hitscan_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'hitscan part maximum damage') -local hitscan_spreadout_dmg = CreateConVar('pac_sv_hitscan_divide_max_damage_by_max_bullets', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether or not force hitscans to divide their damage among the number of bullets fired') - -local damagezone_allow = CreateConVar('pac_sv_damage_zone', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow damage zone parts serverside') -local damagezone_max_damage = CreateConVar('pac_sv_damage_zone_max_damage', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum damage') -local damagezone_max_length = CreateConVar('pac_sv_damage_zone_max_length', '20000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum length') -local damagezone_max_radius = CreateConVar('pac_sv_damage_zone_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'damage zone maximum radius') -local damagezone_allow_dissolve = CreateConVar('pac_sv_damage_zone_allow_dissolve', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to enable entity dissolvers and removing NPCs\' weapons on death for damagezone') - -local lock_allow = CreateConVar('pac_sv_lock', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock parts serverside') -local lock_allow_grab = CreateConVar('pac_sv_lock_grab', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part grabs serverside') -local lock_allow_teleport = CreateConVar('pac_sv_lock_teleport', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow lock part teleports serverside') -local lock_max_radius = CreateConVar('pac_sv_lock_max_grab_radius', '200', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'lock part maximum grab radius') -local lock_allow_grab_ply = CreateConVar('pac_sv_lock_allow_grab_ply', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing players with lock part') -local lock_allow_grab_npc = CreateConVar('pac_sv_lock_allow_grab_npc', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing NPCs with lock part') -local lock_allow_grab_ent = CreateConVar('pac_sv_lock_allow_grab_ent', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow grabbing other entities with lock part') - -local force_allow = CreateConVar('pac_sv_force', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow force parts serverside') -local force_max_length = CreateConVar('pac_sv_force_max_length', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum length') -local force_max_radius = CreateConVar('pac_sv_force_max_radius', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum radius') -local force_max_amount = CreateConVar('pac_sv_force_max_amount', '10000', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'force part maximum amount of force') - -local healthmod_allow = CreateConVar('pac_sv_health_modifier', 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow health modifier parts serverside') -local healthmod_allowed_extra_bars = CreateConVar('pac_sv_health_modifier_extra_bars', 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow extra health bars') -local healthmod_allow_change_maxhp = CreateConVar('pac_sv_health_modifier_allow_maxhp', 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow players to change their maximum health and armor.') -local healthmod_minimum_dmgscaling = CreateConVar('pac_sv_health_modifier_min_damagescaling', -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Minimum health modifier amount. Negative values can heal.') - -local master_init_featureblocker = CreateConVar('pac_sv_block_combat_features_on_next_restart', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on') -cvars.AddChangeCallback('pac_sv_block_combat_features_on_next_restart', function() print("Remember that pac_sv_block_combat_features_on_next_restart is applied on server startup! Only do it if you know what you're doing. You'll need to restart the server.") end) - -local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds') -local enforce_netrate_buffer = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 5000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'the budgeted allowance to limit how often pac combat net messages can be sent. 0 to disable, otherwise a number in bit size') +local hitscan_allow = CreateConVar("pac_sv_hitscan", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside") +local hitscan_max_bullets = CreateConVar("pac_sv_hitscan_max_bullets", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum number of bullets") +local hitscan_max_damage = CreateConVar("pac_sv_hitscan_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum damage") +local hitscan_spreadout_dmg = CreateConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not force hitscans to divide their damage among the number of bullets fired") + +local damagezone_allow = CreateConVar("pac_sv_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside") +local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum damage") +local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length") +local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius") +local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone") + +local lock_allow = CreateConVar("pac_sv_lock", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") +local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") +local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside") +local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius") +local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing players with lock part") +local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part") +local lock_allow_grab_ent = CreateConVar("pac_sv_lock_allow_grab_ent", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing other entities with lock part") + +local force_allow = CreateConVar("pac_sv_force", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside") +local force_max_length = CreateConVar("pac_sv_force_max_length", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum length") +local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum radius") +local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force") + +local healthmod_allow = CreateConVar("pac_sv_health_modifier", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") +local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") +local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") +local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") + +local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") +cvars.AddChangeCallback("pac_sv_block_combat_features_on_next_restart", function() print("Remember that pac_sv_block_combat_features_on_next_restart is applied on server startup! Only do it if you know what you're doing. You'll need to restart the server.") end) + +local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter") +local netrate_allowance = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 60, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "the budgeted allowance to limit how many pac combat net messages can be sent in bursts. 0 to disable, otherwise a number of net messages of allowance.") +local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_netrate_monitor_serverside", 0, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not to let clients enforce their net message rates.\nSet this to 1 to get serverside prints telling you whenever someone is going over their allowance, but it'll still take the network bandwidth.\nSet this to 0 to let clients enforce their net rate and save some bandwidth but the server won't know who's spamming net messages.") local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") -local global_combat_whitelisting = CreateConVar('pac_sv_combat_whitelisting', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ') -local global_combat_prop_protection = CreateConVar('pac_sv_prop_protection', 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Whether players\' owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.') +local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") +local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") local damageable_point_ent_classes = { ["predicted_viewmodel"] = false, @@ -166,6 +167,38 @@ end if SERVER then + local function CountNetMessage(ply) + local stime = SysTime() + local ms_basis = enforce_netrate:GetInt()/1000 + local base_allowance = netrate_allowance:GetInt() + + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance or base_allowance + ply.pac_netmessage_allowance_time = ply.pac_netmessage_allowance_time or 0 --initialize fields + + local timedelta = stime - ply.pac_netmessage_allowance_time --in seconds + ply.pac_netmessage_allowance_time = stime + local regen_rate = math.Clamp(ms_basis,0.01,10) / 20 --delay (converted from milliseconds) -> frequency (1/seconds) + local regens = timedelta / regen_rate + --print(timedelta .. " s, " .. 1/regen_rate .. "/s, " .. regens .. " regens") + if base_allowance == 0 then --limiting only by time, with no reserves + return timedelta > ms_basis + elseif ms_basis == 0 then --allowance with 0 time means ??? I guess automatic pass + return true + else + if timedelta > ms_basis then --good, count up + --print("good time: +"..regens .. "->" .. math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance)) + ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance) + else --earlier than base delay, so count down the allowance + --print("bad time: -1") + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance - 1 + end + ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance,-1,base_allowance) + ply.pac_netmessage_allowance_time = stime + return ply.pac_netmessage_allowance ~= -1 + end + + end + --hack fix to stop GetOwner returning [NULL Entity] hook.Add("PlayerSpawnedProp", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) hook.Add("PlayerSpawnedNPC", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) @@ -231,7 +264,7 @@ if SERVER then local playercount = player.GetCount() local hard_limit = raw_ent_limit:GetInt() local per_ply = per_ply_limit:GetInt() - print(count .. " compared against hard limit " .. hard_limit .. " and " .. playercount .. " players*" .. per_ply .. " limit (" .. count*playercount .. " | " .. playercount*per_ply .. ")") + --print(count .. " compared against hard limit " .. hard_limit .. " and " .. playercount .. " players*" .. per_ply .. " limit (" .. count*playercount .. " | " .. playercount*per_ply .. ")") if count > hard_limit then MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond hard limit.\n") return true @@ -943,13 +976,12 @@ if SERVER then local function ProcessForcesList(ents_hits, tbl, pos, ang, ply) local ent_count = 0 for i,v in pairs(ents_hits) do - if pre_excluded_ent_classes[v:GetClass()] or (v:IsNPC() and not tbl.NPC) or (v:IsPlayer() and not tbl.Players) then ents_hits[i] = nil + if pre_excluded_ent_classes[v:GetClass()] or (v:IsNPC() and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectSelf)) then ents_hits[i] = nil else ent_count = ent_count + 1 end end if TooManyEnts(ent_count) then return end for _,ent in pairs(ents_hits) do - local phys_ent if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) and ( @@ -991,7 +1023,18 @@ if SERVER then local dir2 = ent_center - tbl.Locus_pos--locus local dist_multiplier = 1 + local up_mult = 1 local distance = (ent_center - pos):Length() + local height_delta = pos.z + tbl.LevitationHeight - ent_center.z + + --what it do + --if delta is -100 (ent is lower than the desired height), that means +100 adjustment direction + --height decides how much to knee the force until it equalizes at 0 + --clamp the delta to the ratio levitation height + + if tbl.Levitation then + up_mult = math.Clamp(height_delta / (5 + math.abs(tbl.LevitationHeight)),-1,1) + end if tbl.BaseForceAngleMode == "Radial" then --radial on self addvel = dir:GetNormalized() * tbl.BaseForce @@ -1051,30 +1094,40 @@ if SERVER then if tbl.Falloff then dist_multiplier = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) end - + if tbl.ReverseFalloff then + dist_multiplier = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + if tbl.Levitation then + addvel.z = addvel.z * up_mult + end + addvel = addvel * dist_multiplier add_angvel = add_angvel * dist_multiplier - + local unconsenting_owner = ent:GetCreator() ~= ply and force_consents[ent:GetCreator()] == false - + if (ent:IsPlayer() and tbl.Players) or (ent == ply and tbl.AffectSelf) then if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then - phys_ent:SetVelocity(addvel) - ent:SetVelocity(addvel) + phys_ent:SetVelocity(oldvel * (-tbl.Damping) + addvel) + ent:SetVelocity(oldvel * (-tbl.Damping) + addvel) end elseif (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) and tbl.PhysicsProps then if not IsPropProtected(ent, ply) and not (global_combat_prop_protection:GetBool() and unconsenting_owner) then if IsValid(phys_ent) then ent:PhysWake() + ent:SetVelocity(tbl.Damping * oldvel + addvel) if islocaltorque then + phys_ent:SetAngleVelocity(tbl.Damping * phys_ent:GetAngleVelocity()) phys_ent:AddAngleVelocity(add_angvel) + else + phys_ent:SetAngleVelocity(tbl.Damping * phys_ent:GetAngleVelocity()) add_angvel = phys_ent:WorldToLocalVector( add_angvel ) phys_ent:ApplyTorqueCenter(add_angvel) end ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces - phys_ent:SetVelocity(oldvel + addvel) + phys_ent:SetVelocity((oldvel * tbl.Damping) + addvel) end end elseif (ent:IsNPC() or string.find(ent:GetClass(), "npc") ~= nil) and tbl.NPC then @@ -1084,11 +1137,11 @@ if SERVER then local clamp_vec = vec:GetNormalized()*500 ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer - else ent:SetVelocity(oldvel + addvel) end + else ent:SetVelocity((oldvel * tbl.Damping) + addvel) end end - else + elseif tbl.PointEntities then if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then - phys_ent:SetVelocity(oldvel + addvel) + phys_ent:SetVelocity(tbl.Damping * oldvel + addvel) end end hook.Run("PhysicsUpdate", ent) @@ -1099,11 +1152,17 @@ if SERVER then end --first stage of force: look for targets and determine force amount if continuous local function ImpulseForce(tbl, pos, ang, ply) + local ftime = 0.016 --approximate tick duration if tbl.Continuous then - tbl.BaseForce = tbl.BaseForce * FrameTime() * 3.3333 --weird value to equalize how 600 cancels out gravity - tbl.AddedVectorForce = tbl.AddedVectorForce * FrameTime() * 3.3333 - tbl.Torque = tbl.Torque * FrameTime() * 3.3333 + tbl.BaseForce = tbl.BaseForce1 * ftime * 3.3333 --weird value to equalize how 600 cancels out gravity + tbl.AddedVectorForce = tbl.AddedVectorForce1 * ftime * 3.3333 + tbl.Torque = tbl.Torque1 * ftime * 3.3333 + else + tbl.BaseForce = tbl.BaseForce1 + tbl.AddedVectorForce = tbl.AddedVectorForce1 + tbl.Torque = tbl.Torque1 end + if tbl.HitboxMode == "Sphere" then local ents_hits = ents.FindInSphere(pos, tbl.Radius) ProcessForcesList(ents_hits, tbl, pos, ang, ply) @@ -1217,23 +1276,6 @@ if SERVER then PrintTable(damageable_point_ent_classes) end) - local nextchecklock = CurTime() - hook.Add("Tick", "pac_checklocks", function() - if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.14 end - --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check - for ent,bool in pairs(active_grabbed_ents) do - if ent.grabbed_by or bool then - if ent.grabbed_by_time + 0.5 < CurTime() then --restore the movetype - local grabber = ent.grabbed_by - ent.grabbed_by_uid = nil - ent.grabbed_by = nil - grabber.grabbed_ents[ent] = false - ApplyLockState(ent, false) - active_grabbed_ents[ent] = nil - end - end - end - end) util.AddNetworkString("pac_signal_player_combat_consent") util.AddNetworkString("pac_request_blocked_parts") @@ -1274,6 +1316,7 @@ if SERVER then local base_force_mode_ids = {["Radial"] = 0, ["Locus"] = 1, ["Local"] = 2} local vect_force_mode_ids = {["Global"] = 0, ["Local"] = 1, ["Radial"] = 2, ["RadialNoPitch"] = 3} local ang_torque_mode_ids = {["Global"] = 0, ["TargetLocal"] = 1, ["Local"] = 2, ["Radial"] = 3} + local nextcheckforce = SysTime() function DeclareForceReceivers() util.AddNetworkString("pac_request_force") @@ -1282,6 +1325,7 @@ if SERVER then --server allow if not force_allow:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end + local tbl = {} local pos = net.ReadVector() @@ -1290,6 +1334,18 @@ if SERVER then local on = net.ReadBool() tbl.UniqueID = net.ReadString() + + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Force part: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + + hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) + active_force_ids[tbl.UniqueID] = nil + + return + end + tbl.RootPartOwner = net.ReadEntity() tbl.HitboxMode = table.KeyFromValue(force_hitbox_ids, net.ReadUInt(4)) @@ -1300,13 +1356,18 @@ if SERVER then tbl.Length = net.ReadInt(16) tbl.Radius = net.ReadInt(16) - tbl.BaseForce = net.ReadInt(18) - tbl.AddedVectorForce = net.ReadVector() - tbl.Torque = net.ReadVector() + tbl.BaseForce1 = net.ReadInt(18) + tbl.AddedVectorForce1 = net.ReadVector() + tbl.Torque1 = net.ReadVector() + + tbl.Damping = 1 - net.ReadUInt(10)/1000 + tbl.LevitationHeight = net.ReadInt(14) tbl.Continuous = net.ReadBool() tbl.AccountMass = net.ReadBool() tbl.Falloff = net.ReadBool() + tbl.ReverseFalloff = net.ReadBool() + tbl.Levitation = net.ReadBool() tbl.AffectSelf = net.ReadBool() tbl.Players = net.ReadBool() tbl.PhysicsProps = net.ReadBool() @@ -1315,21 +1376,25 @@ if SERVER then --server limits tbl.Radius = math.Clamp(tbl.Radius,-force_max_radius:GetInt(),force_max_radius:GetInt()) tbl.Length = math.Clamp(tbl.Length,-force_max_length:GetInt(),force_max_length:GetInt()) - tbl.BaseForce = math.Clamp(tbl.BaseForce,-force_max_amount:GetInt(),force_max_amount:GetInt()) - + tbl.BaseForce = math.Clamp(tbl.BaseForce1,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.AddedVectorForce1.x = math.Clamp(tbl.AddedVectorForce1.x,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.AddedVectorForce1.y = math.Clamp(tbl.AddedVectorForce1.y,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.AddedVectorForce1.z = math.Clamp(tbl.AddedVectorForce1.z,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.Torque1.x = math.Clamp(tbl.Torque1.x,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.Torque1.y = math.Clamp(tbl.Torque1.y,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.Torque1.z = math.Clamp(tbl.Torque1.z,-force_max_amount:GetInt(),force_max_amount:GetInt()) if on then if tbl.Continuous then hook.Add("Tick", "pac_force_hold"..tbl.UniqueID, function() ImpulseForce(tbl, pos, ang, ply) - end) - + active_force_ids[tbl.UniqueID] = CurTime() else - ImpulseForce(tbl, pos, ang, ply) active_force_ids[tbl.UniqueID] = nil end + ImpulseForce(tbl, pos, ang, ply) else hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) active_force_ids[tbl.UniqueID] = nil @@ -1348,6 +1413,20 @@ if SERVER then end) + + hook.Add("Tick", "pac_check_force_hooks", function() + if nextcheckforce > SysTime() then return else nextcheckforce = SysTime() + 0.2 end + for i,v in pairs(active_force_ids) do + if not v then + hook.Remove("Tick", "pac_force_hold"..i) + --print("removed an invalid force") + elseif v + 0.1 < CurTime() then + hook.Remove("Tick", "pac_force_hold"..i) + --print("removed an outdated force") + end + end + + end) end function DeclareDamageZoneReceivers() @@ -1358,6 +1437,14 @@ if SERVER then if not damagezone_allow:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Damage zone: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end + local pos = net.ReadVector() local ang = net.ReadAngle() local tbl = {} @@ -1555,6 +1642,7 @@ if SERVER then end) end + local nextchecklock = CurTime() function DeclareLockReceivers() util.AddNetworkString("pac_request_position_override_on_entity_teleport") util.AddNetworkString("pac_request_position_override_on_entity_grab") @@ -1570,6 +1658,14 @@ if SERVER then if not lock_allow:GetBool() then return end if not lock_allow_grab:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end + + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Lock grab: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end local did_grab = true local need_breakup = false @@ -1736,6 +1832,14 @@ if SERVER then if not lock_allow_teleport:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Lock teleport: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end + local lockpart_UID = net.ReadString() local pos = net.ReadVector() local ang = net.ReadAngle() @@ -1763,6 +1867,24 @@ if SERVER then ApplyLockState(targ_ent, false) end) + + + hook.Add("Tick", "pac_checklocks", function() + if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.2 end + --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check + for ent,bool in pairs(active_grabbed_ents) do + if ent.grabbed_by or bool then + if ent.grabbed_by_time + 0.5 < CurTime() then --restore the movetype + local grabber = ent.grabbed_by + ent.grabbed_by_uid = nil + ent.grabbed_by = nil + grabber.grabbed_ents[ent] = false + ApplyLockState(ent, false) + active_grabbed_ents[ent] = nil + end + end + end + end) end function DeclareHitscanReceivers() @@ -1771,6 +1893,14 @@ if SERVER then if not hitscan_allow:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end + + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Hitscan: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end local bulletinfo = {} local affect_self = net.ReadBool() @@ -1848,10 +1978,19 @@ if SERVER then util.AddNetworkString("pac_update_healthbars") net.Receive("pac_request_healthmod", function(len,ply) if not healthmod_allow:GetBool() then return end + + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Health modifier: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end + local part_uid = net.ReadString() local mod_id = net.ReadString() local action = net.ReadString() - + if action == "MaxHealth" then if not healthmod_allow:GetBool() then return end local num = net.ReadUInt(32) @@ -1994,12 +2133,46 @@ if SERVER then end if CLIENT then - CreateConVar("pac_client_grab_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part") - CreateConVar("pac_client_lock_camera_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to having lock parts override your view") - CreateConVar("pac_client_damage_zone_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") - CreateConVar("pac_client_force_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to pac3 physics forces") - CreateConVar("pac_client_hitscan_consent", "0", {FCVAR_ARCHIVE and FCVAR_USERINFO}, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") + CreateConVar("pac_client_grab_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part") + CreateConVar("pac_client_lock_camera_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to having lock parts override your view") + CreateConVar("pac_client_damage_zone_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") + CreateConVar("pac_client_force_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to pac3 physics forces") + CreateConVar("pac_client_hitscan_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") + function pac.CountNetMessage() + local ply = LocalPlayer() + + local stime = SysTime() + local ms_basis = GetConVar("pac_sv_combat_enforce_netrate"):GetInt()/1000 + local base_allowance = GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt() + + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance or base_allowance + ply.pac_netmessage_allowance_time = ply.pac_netmessage_allowance_time or 0 --initialize fields + + local timedelta = stime - ply.pac_netmessage_allowance_time --in seconds + ply.pac_netmessage_allowance_time = stime + local regen_rate = math.Clamp(ms_basis,0.01,10) / 20 --delay (converted from milliseconds) -> frequency (1/seconds) + local regens = timedelta / regen_rate + --print(timedelta .. " s, " .. 1/regen_rate .. "/s, " .. regens .. " regens") + if base_allowance == 0 then --limiting only by time, with no reserves + return timedelta > ms_basis + elseif ms_basis == 0 then --allowance with 0 time means ??? I guess automatic pass + return true + else + if timedelta > ms_basis then --good, count up + --print("good time: +"..regens .. "->" .. math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance)) + ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance) + else --earlier than base delay, so count down the allowance + --print("bad time: -1") + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance - 1 + end + ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance,-1,base_allowance) + ply.pac_netmessage_allowance_time = stime + return ply.pac_netmessage_allowance ~= -1 + end + + end + local function SendConsents() net.Start("pac_signal_player_combat_consent") net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) @@ -2124,5 +2297,6 @@ if CLIENT then hook.Add("InitPostEntity", "PAC_Send_Consents_On_Join", SendConsents) hook.Add("InitPostEntity", "PAC_Request_BlockedParts_On_Join", RequestBlockedParts) + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} end From 3238ca70c47bb26db31313da1dc79324829e0d4c Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 30 Sep 2023 16:10:43 -0400 Subject: [PATCH 050/300] Update proxy.lua --- lua/pac3/core/client/parts/proxy.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index d2e0475fc..885f83a08 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -359,10 +359,10 @@ PART.Inputs.part_distance = function(self, uid1, uid2) if not uid1 or not uid2 then return 0 end local owner = self:GetPlayerOwner() - local PartA = pac.GetPartFromUniqueID(pac.Hash(self:GetPlayerOwner()), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) + local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end - local PartB = pac.GetPartFromUniqueID(pac.Hash(self:GetPlayerOwner()), uid2) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid2) + local PartB = pac.GetPartFromUniqueID(pac.Hash(owner), uid2) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid2) if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(owner), uid2, self) end if not PartA:IsValid() or not PartB:IsValid() then return 0 end From 90536bc6529cbccdaaa49b98896dfa8bb240c39f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 13:04:49 -0400 Subject: [PATCH 051/300] final merge possibly the last changes before going back to mainline just a few additions, fixes, and warnings when going beyond the limits for combat parts --- lua/pac3/core/client/base_part.lua | 1 + lua/pac3/core/client/parts/beam.lua | 11 +- lua/pac3/core/client/parts/damage_zone.lua | 54 +- lua/pac3/core/client/parts/event.lua | 521 ++++++++++++++++-- lua/pac3/core/client/parts/force.lua | 43 +- .../core/client/parts/health_modifier.lua | 34 +- lua/pac3/core/client/parts/hitscan.lua | 20 + .../client/parts/interpolated_multibone.lua | 2 +- lua/pac3/core/client/parts/light.lua | 1 - lua/pac3/core/client/parts/lock.lua | 24 +- lua/pac3/core/client/parts/projectile.lua | 66 ++- lua/pac3/core/shared/movement.lua | 9 +- lua/pac3/editor/client/init.lua | 10 + lua/pac3/editor/client/menu_bar.lua | 26 +- lua/pac3/editor/client/panels/editor.lua | 30 +- lua/pac3/editor/client/panels/properties.lua | 236 ++++++++ lua/pac3/editor/client/parts.lua | 489 +++++++++++++++- lua/pac3/editor/client/saved_parts.lua | 1 + lua/pac3/editor/client/settings.lua | 253 ++++++++- lua/pac3/editor/client/shortcuts.lua | 93 ++-- lua/pac3/editor/client/view.lua | 4 +- lua/pac3/extra/shared/net_combat.lua | 65 ++- 22 files changed, 1794 insertions(+), 199 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 0218a5a81..15196ba3d 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -62,6 +62,7 @@ function PART:PreInitialize() self.hide_disturbing = false self.active_events = {} self.active_events_ref_count = 0 + if not GetConVar("pac_enable_" .. self.ClassName):GetBool() then self:SetWarning("This part class is disabled! Enable it with " .. "pac_enable_" .. self.ClassName .. " 1") end end function PART:Initialize() end diff --git a/lua/pac3/core/client/parts/beam.lua b/lua/pac3/core/client/parts/beam.lua index ab83b816e..01ec42205 100644 --- a/lua/pac3/core/client/parts/beam.lua +++ b/lua/pac3/core/client/parts/beam.lua @@ -24,8 +24,8 @@ do local vector = Vector() local color = Color(255, 255, 255, 255) - function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size, width_start_mul, width_end_mul) - + function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size, width_start_mul, width_end_mul, width_pow) + if not veca or not vecb or not dira or not dirb then return end ax = veca.x; ay = veca.y; az = veca.z @@ -46,6 +46,7 @@ do tex_scroll = tex_scroll or 0 width_start_mul = width_start_mul or 1 width_end_mul = width_end_mul or 1 + width_pow = width_pow or 1 render_StartBeam(res + 1) @@ -66,7 +67,7 @@ do render_AddBeam( vector, - (width + ((math_sin(wave) ^ width_bend_size) * width_bend)) * Lerp(frac, width_start_mul, width_end_mul), + (width + ((math_sin(wave) ^ width_bend_size) * width_bend)) * Lerp(math.pow(frac,width_pow), width_start_mul, width_end_mul), (i / tex_stretch) + tex_scroll, color ) @@ -98,6 +99,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("WidthBendSize", 1) BUILDER:GetSet("StartWidthMultiplier", 1) BUILDER:GetSet("EndWidthMultiplier", 1) + BUILDER:GetSet("WidthMorphPower", 1) BUILDER:GetSet("TextureStretch", 1) BUILDER:GetSet("TextureScroll", 0) BUILDER:SetPropertyGroup("orientation") @@ -222,7 +224,8 @@ function PART:OnDraw() self.WidthBend, self.WidthBendSize, self.StartWidthMultiplier, - self.EndWidthMultiplier + self.EndWidthMultiplier, + self.WidthMorphPower ) end end diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 1efd24d82..d7888c32f 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -115,6 +115,7 @@ BUILDER:StartStorableVars() }}) :GetSet("DoNotKill",false, {description = "Will only damage to as low as critical health"}) :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("HitOutcome") :GetSetPart("HitSoundPart") :GetSetPart("KillSoundPart") @@ -258,7 +259,7 @@ function PART:FindOrCreateFloatingPart(owner, ent, part_uid, id) end --what if we don't! local tbl = pac.GetPartFromUniqueID(pac.Hash(owner), part_uid):ToTable() - local group = pac.CreatePart("group", self:GetPlayerOwner()) print("\tcreated a group for " .. id) + local group = pac.CreatePart("group", self:GetPlayerOwner()) --print("\tcreated a group for " .. id) group:SetShowInEditor(false) local part = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl)) @@ -365,7 +366,7 @@ local function RemoveHitMarker(owner, ent, uid, id) tbl.group_part_data:SetHide(true) tbl.group_part_data:SetShowInEditor(false) tbl.group_part_data:SetOwner(owner) - print(tbl.group_part_data, "dormant") + --print(tbl.group_part_data, "dormant") end end end @@ -402,7 +403,7 @@ function PART:AssignFloatingPartToEntity(part, owner, ent, parent_ent, template_ group:SetHide(false) part2:SetHide(false) group:CallRecursive("Think") - print(group, "assigned to " .. marker_id .. " / " .. parent_ent:EntIndex()) + --print(group, "assigned to " .. marker_id .. " / " .. parent_ent:EntIndex()) end @@ -510,6 +511,7 @@ function PART:SendNetMessage() net.WriteVector(self:GetWorldPosition()) net.WriteAngle(self:GetWorldAngles()) net.WriteUInt(self.Damage, 28) + net.WriteUInt(self.MaxHpScaling*1000,10) net.WriteInt(self.Length, 16) net.WriteInt(self.Radius, 16) net.WriteBool(self.AffectSelf) @@ -715,7 +717,7 @@ function PART:OnShow() end -concommand.Add("pac_cleanup_hitmarks", function() +concommand.Add("pac_cleanup_damagezone_hitmarks", function() if LocalPlayer().hitparts then for i,v in pairs(LocalPlayer().hitparts) do v.specimen_part:Remove() @@ -726,20 +728,6 @@ concommand.Add("pac_cleanup_hitmarks", function() LocalPlayer().hitparts = nil end) -concommand.Add("pac_count_inactive_hitmarks", function() - print("first the markers") - if LocalPlayer().hitparts then - for i,v in pairs(LocalPlayer().hitparts) do - print(i,v.active) - end - end - print("second the partpool") - if LocalPlayer().hitmarker_partpool then - for i,v in pairs(LocalPlayer().hitmarker_partpool) do - print(i,v.active, pac.GetPartFromUniqueID(pac.Hash(LocalPlayer()),v.spec_uid)) - end - end -end) function PART:OnHide() hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) @@ -1013,5 +1001,35 @@ function PART:Initialize() self.validTime = SysTime() + 2 end +function PART:SetRadius(val) + self.Radius = val + local sv_dist = GetConVar("pac_sv_damage_zone_max_radius"):GetInt() + if self.Radius > sv_dist then + self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetLength(val) + self.Length = val + local sv_dist = GetConVar("pac_sv_damage_zone_max_length"):GetInt() + if self.Length > sv_dist then + self:SetInfo("Your length is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetDamage(val) + self.Damage = val + local sv_max = GetConVar("pac_sv_damage_zone_max_damage"):GetInt() + if self.Damage > sv_max then + self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 4b6fe199a..3548a718b 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -117,6 +117,33 @@ function PART:SetEvent(event) local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) + if not self.Events[event] then --invalid event? try a command event + local command_was_found_in_cmd_part = false + for i,v in ipairs(pac.GetLocalParts()) do + if v.ClassName == "command" then + if string.find(v.String, "pac_event " .. event) then + command_was_found_in_cmd_part = true + end + end + end + if self:GetPlayerOwner().pac_command_events or command_was_found_in_cmd_part then + self:GetPlayerOwner().pac_command_events = self:GetPlayerOwner().pac_command_events or {} + if self:GetPlayerOwner().pac_command_events[event] or command_was_found_in_cmd_part or GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then + timer.Simple(0.2, function() + --now we'll use event as a command name + self:SetEvent("command") + self.pace_properties["Event"]:SetValue("command") + + self:SetArguments(event .. "@@0") + self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") + pace.PopulateProperties(self) + + end) + return + end + end + end + self.Event = event self:SetWarning() self:SetInfo() @@ -128,10 +155,11 @@ function PART:SetEvent(event) pace.changed_event = self --a reference to make it refresh the popup label panel pace.changed_event_time = CurTime() - if self == pace.current_part then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit + if self == pace.current_part and GetConVar("pac_copilot_make_popup_when_selecting_event"):GetBool() then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit self:GetDynamicProperties(reset) - + if not GetConVar("pac_editor_remember_divider_height"):GetBool() then pace.Editor.div:SetTopHeight(ScrH() - 520) end + end function PART:Initialize() @@ -691,8 +719,9 @@ PART.OldEvents = { callback = function(self, ent, name, num) ent = try_viewmodel(ent) if ent:IsValid() then - min,max = ent:GetPoseParameterRange(ent:LookupPoseParameter(name)) - actual_value = min + (max - min)*(ent:GetPoseParameter(name)) + local min,max = ent:GetPoseParameterRange(ent:LookupPoseParameter(name)) + if not min or not max then return 0 end + local actual_value = min + (max - min)*(ent:GetPoseParameter(name)) return self:NumberOperator(actual_value, num) end end, @@ -1140,6 +1169,7 @@ PART.OldEvents = { end, nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return false end local radius = ent:BoundingRadius() if radius == 0 and IsValid(ent.pac_projectile) then @@ -2322,7 +2352,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, time, damage, uid) uid = uid or "" - local valid_uid, err = pcall(pac.GetPartFromUniqueID, uid) + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do if part.ClassName == "damage_zone" then @@ -2336,7 +2366,7 @@ PART.OldEvents = { elseif not valid_uid and err then self:SetError("invalid part Unique ID\n"..err) elseif valid_uid then - local part = pac.GetPartFromUniqueID(uid) + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) if part.ClassName == "damage_zone" then if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then if part.dmgzone_hit_done + time > CurTime() then @@ -2368,7 +2398,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, time, uid) uid = uid or "" - local valid_uid, err = pcall(pac.GetPartFromUniqueID, uid) + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do if part.ClassName == "damage_zone" then @@ -2382,7 +2412,7 @@ PART.OldEvents = { elseif not valid_uid and err then self:SetError("invalid part Unique ID\n"..err) elseif valid_uid then - local part = pac.GetPartFromUniqueID(uid) + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) if part.ClassName == "damage_zone" then if part.dmgzone_kill_done then if part.dmgzone_kill_done + time > CurTime() then @@ -2421,7 +2451,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, uid) uid = uid or "" - local valid_uid, err = pcall(pac.GetPartFromUniqueID, uid) + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do if part.ClassName == "lock" then @@ -2433,7 +2463,7 @@ PART.OldEvents = { elseif not valid_uid and err then self:SetError("invalid part Unique ID\n"..err) elseif valid_uid then - local part = pac.GetPartFromUniqueID(uid) + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) if part.ClassName == "lock" then if part.grabbing then return IsValid(part.target_ent) @@ -2445,6 +2475,98 @@ PART.OldEvents = { return false end }, + + --[[ + ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {} + ent.pac_healthbars_uidtotals = ent.pac_healthbars_uidtotals or {} + ent.pac_healthbars_total = 0 + ]] + healthmod_bar_total = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}}, + userdata = {{default = 0}}, + callback = function(self, ent, HpValue) + if ent.pac_healthbars and ent.pac_healthbars_total then + return self:NumberOperator(ent.pac_healthbars_total, HpValue) + end + return false + end, + nice = function(self, ent, HpValue) + local str = "healthmod_bar_total : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_total then + str = str .. " | " .. ent.pac_healthbars_total + end + return str + end + }, + + healthmod_bar_layertotal = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}, {layer = "number"}}, + userdata = {{default = 0}, {default = 0}}, + callback = function(self, ent, HpValue, layer) + if ent.pac_healthbars and ent.pac_healthbars_layertotals then + if ent.pac_healthbars_layertotals[layer] then + return self:NumberOperator(ent.pac_healthbars_layertotals[layer], HpValue) + end + + end + return false + end, + nice = function(self, ent, HpValue, layer) + local str = "healthmod_layer_total at layer " .. layer .. " : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_layertotals then + if ent.pac_healthbars_layertotals[layer] then + str = str .. " | " .. ent.pac_healthbars_layertotals[layer] + else + str = str .. " | not found" + end + + else + str = str .. " | not found" + end + return str + end + }, + + healthmod_bar_uidvalue = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}, {part_uid = "string"}}, + userdata = {{default = 0}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "health_modifier" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, HpValue, part_uid) + if ent.pac_healthbars and ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals[part_uid] then + return self:NumberOperator(ent.pac_healthbars_uidtotals[part_uid], HpValue) + end + end + return false + end, + nice = function(self, ent, HpValue, part_uid) + local str = "healthmod_bar_uidvalue : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals[part_uid] then + str = str .. " | " .. ent.pac_healthbars_uidtotals[part_uid] + else + str = str .. " | nothing for UID "..part_uid + end + else + str = str .. " | nothing for UID "..part_uid + end + return str + end + }, + } @@ -2555,6 +2677,7 @@ do part.toggleimpulsekey = part.toggleimpulsekey or {} part.toggleimpulsekey[key] = down part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 + ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0 part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime end end @@ -3602,7 +3725,7 @@ concommand.Add("pac_event_sequenced", function(ply, cmd, args) if sequence_number == 0 then sequence_number = i end found = true end - elseif ply.pac_command_events[event..i] == nil then + --elseif ply.pac_command_events[event..i] == nil then ply.pac_command_events[event..i] = {name = event..i, time = 0, on = 0} end end @@ -3715,12 +3838,25 @@ local eventwheel_visibility_rule = CreateConVar("pac_eventwheel_visibility_rule" "3 will hide a command only if ALL events of a name are being hidden\n".. "4 will only show commands containing the following substrings, separated by spaces\n".. "-4 will hide commands containing the following substrings, separated by spaces") + +local eventwheel_style = CreateConVar("pac_eventwheel_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel.\n0 is the default legacy style with one circle\n1 is the new style with colors, using one circle for the color and one circle for the activation indicator\n2 is an alternative style using a smaller indicator circle on the corner of the circle") +local eventlist_style = CreateConVar("pac_eventlist_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel list alternative.\n0 is like the default eventwheel legacy style with one indicator for the activation\n1 is the new style with colors, using one rectangle for the color and one rectangle for the activation indicator\n2 is an alternative style using a smaller indicator on the corner") + +local eventwheel_font = CreateConVar("pac_eventwheel_font", "DermaDefault", FCVAR_ARCHIVE, "pac3 eventwheel font. try pac_font_ such as pac_font_20 or pac_font_bold30. the pac fonts go up to 34") +local eventwheel_clickable = CreateConVar("pac_eventwheel_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel.\n-1 : not clickable, but activate on menu close\n0 : clickable, and activate on menu close\n1 : clickable, but doesn't activate on menu close") +local eventlist_clickable = CreateConVar("pac_eventlist_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel list alternative.\n-1 : not clickable, but activate a hovered event on menu close\n0 : clickable, and activate a hovered event on menu close\n1 : clickable, but doesn't do anything on menu close") + +local event_list_font_size = CreateConVar("pac_eventlist_fontsize", "12", FCVAR_ARCHIVE, "How big the font should be for the eventwheel's rectangle list counterpart.\nMight not work if the corresponding pac_font is missing") + -- Custom event selector wheel do + local function get_events() + pace.command_colors = pace.command_colors or {} local available = {} local names = {} local args = string.Split(eventwheel_visibility_rule:GetString(), " ") + local uncolored_events = {} for k,v in pairs(pac.GetLocalParts()) do if v.ClassName == "event" then @@ -3728,6 +3864,7 @@ do if e == "command" then local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) local this_event_hidden = v:IsHiddenBySomethingElse(false) + if not names[cmd] then --wheel_hidden is the hide_in_eventwheel box @@ -3763,11 +3900,12 @@ do end - available[cmd] = {type = e, time = time} + available[cmd] = {type = e, time = time, trigger = cmd} end end end for cmd,v in pairs(names) do + uncolored_events[cmd] = not pace.command_colors[cmd] local remove = false if args[1] == "-1" then --skip @@ -3805,7 +3943,7 @@ do remove = true end - else --why would you use the 2 or -2 mode if you didn't set keywords?? + else --why would you use the 4 or -4 mode if you didn't set keywords?? remove = false end end @@ -3814,24 +3952,94 @@ do available[cmd] = nil end end - + local list = {} - for k,v in pairs(available) do - if k == names[k].name then - v.trigger = k - table.insert(list, v) + + if true then + local colors = {} + + for name,colstr in pairs(pace.command_colors) do + colors[colstr] = colors[colstr] or {} + colors[colstr][name] = available[name] end - end + - table.sort(list, function(a, b) return a.trigger > b.trigger end) + for col,tbl in pairs(colors) do + + local sublist = {} + for k,v in pairs(tbl) do + table.insert(sublist,available[k]) + end + + table.sort(sublist, function(a, b) return a.trigger < b.trigger end) + + for i,v in pairs(sublist) do + table.insert(list,v) + end + end + + local uncolored_sublist = {} + for k,v in pairs(available) do + if uncolored_events[k] then + table.insert(uncolored_sublist,available[k]) + end + end + + table.sort(uncolored_sublist, function(a, b) return a.trigger < b.trigger end) + + for k,v in ipairs(uncolored_sublist) do + table.insert(list, v) + end + else + + for k,v in pairs(available) do + if k == names[k].name then + v.trigger = k + table.insert(list, v) + end + end + + table.sort(list, function(a, b) return a.trigger > b.trigger end) + end + return list end local selectorBg = Material("sgm/playercircle") local selected + local clicking = false + local open_btn + + + local clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1 + local close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0 + + local clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1 + local close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0 function pac.openEventSelectionWheel() + if not IsValid(open_btn) then open_btn = vgui.Create("DButton") end + open_btn:SetSize(80,30) + open_btn:SetText("Customize") + open_btn:SetPos(ScrW() - 80,0) + pace.command_event_menu_opened = nil + + function open_btn:DoClick() + + if (pace.command_event_menu_opened == nil) then + pace.ConfigureEventWheelMenu() + elseif IsValid(pace.command_event_menu_opened) then + pace.command_event_menu_opened:Remove() + end + + end + + open_btn:Show() + pace.command_colors = pace.command_colors or {} + clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1 + close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0 + gui.EnableScreenClicker(true) local scrw, scrh = ScrW(), ScrH() @@ -3896,6 +4104,13 @@ do local ply = pac.LocalPlayer local data = ply.pac_command_events and ply.pac_command_events[self.event.trigger] and ply.pac_command_events[self.event.trigger] + + + local d1 = 64 --indicator + + + local d2 = 50 --color + local indicator_color if data then local is_oneshot = self.event.time and self.event.time > 0 @@ -3903,29 +4118,84 @@ do local f = (pac.RealTime - data.time) / self.event.time local s = Lerp(math.Clamp(f,0,1), 1, 0) local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15) - surface.SetDrawColor(HSVToColor(210,s,v)) + indicator_color = HSVToColor(210,s,v) + else if data.on == 1 then - surface.SetDrawColor(HSVToColor(210,1,0.55)) + indicator_color = HSVToColor(210,1,0.55) else - surface.SetDrawColor(HSVToColor(210,0,0.15)) + indicator_color = HSVToColor(210,0,0.15) end end else - surface.SetDrawColor(HSVToColor(210,0,0.15)) + indicator_color = HSVToColor(210,0,0.15) end - --surface.DrawTexturedRect(x-48, y-48, 96, 96) - surface.DrawTexturedRect(x-48, y-48, 96, 96) - draw.SimpleText(self.name, "DermaDefault", x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) + if eventwheel_style:GetInt() == 0 then + d2 = 96 + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2) + elseif eventwheel_style:GetInt() == 1 then + if pace.command_colors[self.name] then + local col_str_tbl = string.Split(pace.command_colors[self.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + + d1 = 100 --color + d2 = 50 --indicator + surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1) + + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2) + + draw.RoundedBox(0,x-40,y-8,80,16,Color(0,0,0)) + + elseif eventwheel_style:GetInt() == 2 then + if pace.command_colors[self.name] then + local col_str_tbl = string.Split(pace.command_colors[self.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + + d1 = 96 --color + d2 = 40 --indicator + + surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1) + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-1.2*d2, y-1.2*d2, d2, d2) + end + + draw.SimpleText(self.name, eventwheel_font:GetString(), x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) + cam.PopModelMatrix() end pac.AddHook("HUDPaint","custom_event_selector",function() -- Right clicking cancels if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionWheel(true) return end - + if input.IsButtonDown(MOUSE_LEFT) and not pace.command_event_menu_opened and not open_btn:IsHovered() and clickable then + + if not clicking and selected then + if not selected.event.time then + RunConsoleCommand("pac_event", selected.event.trigger, "toggle") + elseif selected.event.time > 0 then + RunConsoleCommand("pac_event", selected.event.trigger) + else + local ply = pac.LocalPlayer + + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then + RunConsoleCommand("pac_event", selected.event.trigger, "0") + else + RunConsoleCommand("pac_event", selected.event.trigger, "1") + end + end + end + clicking = true + else clicking = false end -- Normalize mouse vector from center of screen local x, y = input.GetCursorPos() x = x - scrw2 @@ -3952,10 +4222,11 @@ do end function pac.closeEventSelectionWheel(cancel) + open_btn:Hide() gui.EnableScreenClicker(false) pac.RemoveHook("HUDPaint","custom_event_selector") - if selected and cancel ~= true then + if selected and cancel ~= true and close_click then if not selected.event.time then RunConsoleCommand("pac_event", selected.event.trigger, "toggle") elseif selected.event.time > 0 then @@ -3973,68 +4244,216 @@ do selected = nil end + local panels = {} function pac.openEventSelectionList() + if not IsValid(open_btn) then open_btn = vgui.Create("DButton") end + open_btn:SetSize(80,30) + open_btn:SetText("Customize") + open_btn:SetPos(ScrW() - 80,0) + pace.command_event_menu_opened = nil + + function open_btn:DoClick() + + if (pace.command_event_menu_opened == nil) then + pace.ConfigureEventWheelMenu() + elseif IsValid(pace.command_event_menu_opened) then + pace.command_event_menu_opened:Remove() + end + + end + + open_btn:Show() + pace.command_colors = pace.command_colors or {} + clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1 + close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0 + + local height = 2*event_list_font_size:GetInt() + 8 + panels = panels or {} + if not table.IsEmpty(panels) then + for i, v in pairs(panels) do + v:Remove() + end + end local selections = {} local events = get_events() - for k, v in ipairs(events) do + for i, v in ipairs(events) do - selections[k] = { + + local list_element = vgui.Create("DPanel") + + panels[i] = list_element + list_element:SetSize(250,height) + list_element.event = v + + selections[i] = { grow = 0, name = v.trigger, - event = v + event = v, + pnl = list_element } + function list_element:Paint() end + function list_element:DoCommand() + if not selected.event.time then + RunConsoleCommand("pac_event", selected.event.trigger, "toggle") + elseif selected.event.time > 0 then + RunConsoleCommand("pac_event", selected.event.trigger) + else + local ply = pac.LocalPlayer + + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then + RunConsoleCommand("pac_event", selected.event.trigger, "0") + else + RunConsoleCommand("pac_event", selected.event.trigger, "1") + end + end + end + function list_element:Think() + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_G) then + self:Remove() + end + if self:IsHovered() then + selected = self + if input.IsMouseDown(MOUSE_LEFT) and not self.was_clicked and not IsValid(pace.command_event_menu_opened) and not open_btn:IsHovered() and clickable2 then + self.was_clicked = true + self:DoCommand() + elseif not input.IsMouseDown(MOUSE_LEFT) then self.was_clicked = false end + end + end + end gui.EnableScreenClicker(true) pac.AddHook("HUDPaint","custom_event_selector_list",function() + local height = 2*event_list_font_size:GetInt() + 8 -- Right clicking cancels - if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionWheel(true) return end + if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionList(true) return end DisableClipping(true) render.PushFilterMag(TEXFILTER.ANISOTROPIC) render.PushFilterMin(TEXFILTER.ANISOTROPIC) draw.SimpleText("Right click to cancel", "DermaDefault", ScrW()/2, ScrH()/2, color_red, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) - for _, v in ipairs(selections) do - draw_circle(v, x, y) + local x = 0 + local y = 0 + for i, v in ipairs(selections) do + if IsValid(v.pnl) then + + if y + height > ScrH() then + y = 0 + x = x + 200 + end + local list_element = v.pnl + list_element:SetPos(x,y) + list_element:SetSize(250,height) + + local ply = pac.LocalPlayer + local data = ply.pac_command_events and ply.pac_command_events[list_element.event.trigger] + local indicator_color + + if data then + local is_oneshot = list_element.event.time and list_element.event.time > 0 + + if is_oneshot then + local f = (pac.RealTime - data.time) / list_element.event.time + local s = Lerp(math.Clamp(f,0,1), 1, 0) + local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15) + + indicator_color = HSVToColor(210,s,v) + else + if data.on == 1 then + indicator_color = HSVToColor(210,1,0.55) + else + indicator_color = HSVToColor(210,0,0.15) + end + end + else + indicator_color = HSVToColor(210,0,0.15) + end + + local main_color = HSVToColor(210,0,0.15) + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + main_color = Color(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + end + + local hue, sat, lightness_value = ColorToHSL(main_color) + + + if eventlist_style:GetInt() == 0 then + surface.SetDrawColor(indicator_color) + surface.DrawRect(x,y,200,height) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x,y,200,height,2) + elseif eventlist_style:GetInt() == 1 then + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + surface.DrawRect(x,y,200,height) + + surface.SetDrawColor(indicator_color) + surface.DrawRect(x + 200/6,y + height/6,200 * 0.666,height * 0.666,2) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x,y,200,height,2) + + elseif eventlist_style:GetInt() == 2 then + surface.DrawOutlinedRect(x,y,200,height,2) + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + surface.DrawRect(x,y,200,height) + + surface.SetDrawColor(indicator_color) + surface.DrawRect(x + 150,y,50,height/2,2) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x + 150,y,50,height/2,2) + surface.DrawOutlinedRect(x,y,200,height,2) + end + + local text_color = Color(255,255,255) + if lightness_value > 0.5 and eventlist_style:GetInt() ~= 0 then + text_color = Color(0,0,0) + end + draw.SimpleText(v.name,"pac_font_" .. event_list_font_size:GetString(),x + 4,y + 4, text_color, TEXT_ALIGN_LEFT) + y = y + height + + end + end render.PopFilterMag() render.PopFilterMin() DisableClipping(false) + end) end function pac.closeEventSelectionList(cancel) + open_btn:Hide() gui.EnableScreenClicker(false) pac.RemoveHook("HUDPaint","custom_event_selector_list") - if selected and cancel ~= true then - if not selected.event.time then - RunConsoleCommand("pac_event", selected.event.trigger, "toggle") - elseif selected.event.time > 0 then - RunConsoleCommand("pac_event", selected.event.trigger) - else - local ply = pac.LocalPlayer - - if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then - RunConsoleCommand("pac_event", selected.event.trigger, "0") - else - RunConsoleCommand("pac_event", selected.event.trigger, "1") - end + if IsValid(selected) and close_click2 then + if selected:IsHovered() then + selected:DoCommand() end end + for i,v in pairs(panels) do v:Remove() end selected = nil end + concommand.Add("+pac_events", pac.openEventSelectionWheel) concommand.Add("-pac_events", pac.closeEventSelectionWheel) concommand.Add("+pac_events_list", pac.openEventSelectionList) concommand.Add("-pac_events_list", pac.closeEventSelectionList) - - end net.Receive("pac.SendPlayerObjUsed", function() @@ -4091,4 +4510,4 @@ net.Receive("pac_update_healthbars", function() end end -end) \ No newline at end of file +end) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 2e1ddb9dd..52d31f0d7 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -20,7 +20,8 @@ BUILDER:StartStorableVars() :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) :GetSet("Radius", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) :GetSet("Preview",false) - :SetPropertyGroup("Forces") + + :SetPropertyGroup("BaseForces") :GetSet("BaseForce", 0) :GetSet("AddedVectorForce", Vector(0,0,0)) :GetSet("Torque", Vector(0,0,0)) @@ -28,13 +29,20 @@ BUILDER:StartStorableVars() :GetSet("VectorForceAngleMode", "Global", {enums = {["Global"] = "Global", ["Local"] = "Local", ["Radial"] = "Radial", ["RadialNoPitch"] = "RadialNoPitch"}}) :GetSet("TorqueMode", "TargetLocal", {enums = {["Global"] = "Global", ["TargetLocal"] = "TargetLocal", ["Local"] = "Local", ["Radial"] = "Radial"}}) :GetSetPart("Locus", nil) + + :SetPropertyGroup("Behaviors") :GetSet("Continuous", true, {description = "If set to false, the force will be a single, stronger impulse"}) :GetSet("AccountMass", false, {description = "Apply acceleration according to mass."}) :GetSet("Falloff", false, {description = "Whether the force to apply should fade with distance"}) :GetSet("ReverseFalloff", false, {description = "The reverse of the falloff means the force fades when getting closer."}) - :GetSet("Damping", 0, {editor_clamp = {0,1}, editor_sensitivity = 0.1}) :GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part"}) :GetSet("LevitationHeight", 0) + + :SetPropertyGroup("Damping") + :GetSet("Damping", 0, {editor_clamp = {0,1}, editor_sensitivity = 0.1}) + :GetSet("DampingFalloff", false, {description = "Whether the damping should fade with distance"}) + :GetSet("DampingReverseFalloff", false, {description = "Whether the damping should fade with distance but reverse"}) + :SetPropertyGroup("Targets") :GetSet("AffectSelf",false) :GetSet("Players",true) @@ -200,6 +208,8 @@ function PART:Impulse(on) net.WriteBool(self.AccountMass) net.WriteBool(self.Falloff) net.WriteBool(self.ReverseFalloff) + net.WriteBool(self.DampingFalloff) + net.WriteBool(self.DampingReverseFalloff) net.WriteBool(self.Levitation) net.WriteBool(self.AffectSelf) net.WriteBool(self.Players) @@ -265,5 +275,34 @@ function PART:BuildCone(obj) obj:BuildFromTriangles( circle_tris ) end +function PART:SetRadius(val) + self.Radius = val + local sv_dist = GetConVar("pac_sv_force_max_radius"):GetInt() + if self.Radius > sv_dist then + self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetLength(val) + self.Length = val + local sv_dist = GetConVar("pac_sv_force_max_length"):GetInt() + if self.Length > sv_dist then + self:SetInfo("Your length is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetBaseForce(val) + self.BaseForce = val + local sv_max = GetConVar("pac_sv_force_max_amount"):GetInt() + if self.BaseForce > sv_max then + self:SetInfo("Your base force is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index e6fdee83c..dae8d0b8c 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -13,11 +13,12 @@ BUILDER:StartStorableVars() BUILDER:GetSet("MaxHealth", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) BUILDER:SetPropertyGroup("ExtraHpBars") - BUILDER:GetSet("FollowHealthBars", true, {description = "whether changing the extra health bars should try to set your health at the same time"}) + BUILDER:GetSet("FollowHealthBars", true, {description = "whether changing the extra health bars should try to update them at the same time"}) BUILDER:GetSet("HealthBars", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,100)) end}) BUILDER:GetSet("BarsAmount", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) BUILDER:GetSet("BarsLayer", 1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,15)) end}) BUILDER:GetSet("AbsorbFactor", 0, {editor_onchange = function(self,num) return math.Clamp(num,-1,1) end}) + BUILDER:GetSet("HPBarsResetOnHide", false) BUILDER:SetPropertyGroup("Armor") BUILDER:GetSet("ChangeArmor", false) @@ -27,6 +28,7 @@ BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("DamageMultipliers") BUILDER:GetSet("DamageMultiplier", 1) BUILDER:GetSet("ModifierId", "") + BUILDER:GetSet("MultiplierResetOnHide", false) BUILDER:EndStorableVars() @@ -121,6 +123,12 @@ function PART:SetDamageMultiplier(val) self.DamageMultiplier = val if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("DamageMultiplier") + local sv_min = GetConVar("pac_sv_health_modifier_min_damagescaling"):GetInt() + if self.DamageMultiplier < sv_min then + self:SetInfo("Your damage scaling is beyond the server's minimum permitted! Server minimum is " .. sv_min) + else + self:SetInfo(nil) + end end function PART:OnRemove() @@ -162,6 +170,30 @@ function PART:OnShow() self:SendModifier("all") end +function PART:OnHide() + if self.HPBarsResetOnHide then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("HealthBars") + net.WriteUInt(0, 32) + net.WriteUInt(0, 32) + net.WriteUInt(self.BarsLayer, 4) + net.WriteFloat(1) + net.WriteBool(self.FollowHealthBars) + net.SendToServer() + end + if self.MultiplierResetOnHide then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("DamageMultiplier") + net.WriteFloat(1) + net.WriteBool(true) + net.SendToServer() + end +end + function PART:Initialize() if not GetConVar("pac_sv_health_modifier"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("health modifiers are disabled on this server!") end end diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index 0f75ea085..7b211ec94 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -227,6 +227,26 @@ function PART:SendNetMessage() net.SendToServer() end +function PART:SetDamage(val) + self.Damage = val + local sv_max = GetConVar("pac_sv_hitscan_max_damage"):GetInt() + if self.Damage > sv_max then + self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + +function PART:SetNumberBullets(val) + self.NumberBullets = val + local sv_max = GetConVar("pac_sv_hitscan_max_bullets"):GetInt() + if self.NumberBullets > sv_max then + self:SetInfo("Your bullet count is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + BUILDER:Register() diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index 4b6c54321..c3da33ff3 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -67,7 +67,7 @@ end --PROPORTION 0 0.5 0 0.5 0 0.5 3 function PART:OnDraw() self:UpdateNodes() - if self.valid_time > CurTime() then print("returned") return end + if self.valid_time > CurTime() then return end self.pos = self.pos or self:GetWorldPosition() self.ang = self.ang or self:GetWorldAngles() diff --git a/lua/pac3/core/client/parts/light.lua b/lua/pac3/core/client/parts/light.lua index c89436100..8c3d3d15c 100644 --- a/lua/pac3/core/client/parts/light.lua +++ b/lua/pac3/core/client/parts/light.lua @@ -84,7 +84,6 @@ function PART:OnDraw() self:GetLight().dir = ang:Forward() end function PART:SetStyle(val) - print(val, type(val)) self.Style = val self:GetLight().Style = self.Style end diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 4aa13eacf..e51648a93 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -61,6 +61,7 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:OnThink() + if not GetConVar('pac_sv_lock'):GetBool() then return end pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then @@ -215,7 +216,7 @@ do self.grabbing = false pac.Message(Color(255, 50, 50), "lock break result:") MsgC(Color(0,255,255), "\t", self) MsgC(Color(200, 200, 200), " in your group ") MsgC(Color(0,255,255), self:GetRootPart(),"\n") - MsgC(Color(200, 200, 200), "\tIt will now be in the forcebreak state until the next allowed grab, 3 seconds from now.\n") + MsgC(Color(200, 200, 200), "\tIt will now be in the forcebreak state until the next allowed grab, 3 seconds from now\nalso this entity can't be grabbed for 10 seconds.\n") if not self.ContinuousSearch then self.resetcondition = true end @@ -241,7 +242,7 @@ do if uid ~= "" then --if a uid is provided MsgC(Color(255, 50, 50), "AND IT KNOWS YOUR UID! " .. uid .. "\n") - local part = pac.GetPartFromUniqueID(found_uid) + local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid) if part then if part.ClassName == "lock" then part:BreakLock(target_to_release) @@ -269,7 +270,7 @@ do local target_to_mark = net.ReadEntity() local successful_grab = net.ReadBool() local uid = net.ReadString() - local part = pac.GetPartFromUniqueID(uid) + local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid) --print(target_to_mark,"is grabbed by",uid) if not successful_grab then @@ -291,7 +292,6 @@ function PART:SetRadius(val) else self:SetInfo(nil) end - end function PART:OnShow() @@ -350,12 +350,16 @@ function PART:OnShow() if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end end - net.Start("pac_request_position_override_on_entity_teleport") - net.WriteString(self.UniqueID) - net.WriteVector(teleport_pos_final) - net.WriteAngle(ang_yaw_only) - net.WriteBool(self.OverrideAngles) - net.SendToServer() + timer.Simple(0, function() + if self:IsHidden() or self:IsDrawHidden() then return end + net.Start("pac_request_position_override_on_entity_teleport") + net.WriteString(self.UniqueID) + net.WriteVector(teleport_pos_final) + net.WriteAngle(ang_yaw_only) + net.WriteBool(self.OverrideAngles) + net.SendToServer() + end) + end self.grabbing = false elseif self.Mode == "Grab" then diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 2b1ecafdd..deb107e86 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -425,13 +425,67 @@ function PART:Shoot(pos, ang, multi_projectile_count) end end -function PART:OnRemove() - if not self.Physical and self.projectiles then - for key, ent in pairs(self.projectiles) do - SafeRemoveEntity(ent) - end +function PART:SetDamage(val) + +end - self.projectiles = {} +function PART:SetRadius(val) + self.Radius = val + local sv_dist = GetConVar("pac_sv_projectile_max_radius"):GetInt() + if self.Radius > sv_dist then + self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetDamageRadius(val) + self.DamageRadius = val + local sv_dist = GetConVar("pac_sv_projectile_max_damage_radius"):GetInt() + if self.DamageRadius > sv_dist then + self:SetInfo("Your damage radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetAttractRadius(val) + self.AttractRadius = val + local sv_dist = GetConVar("pac_sv_projectile_max_attract_radius"):GetInt() + if self.AttractRadius > sv_dist then + self:SetInfo("Your attract radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetSpeed(val) + self.Speed = val + local sv_max = GetConVar("pac_sv_projectile_max_speed"):GetInt() + if self.Speed > sv_max then + self:SetInfo("Your speed is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + +function PART:SetMass(val) + self.Mass = val + local sv_max = GetConVar("pac_sv_projectile_max_mass"):GetInt() + if self.Mass > sv_max then + self:SetInfo("Your mass is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + +function PART:SetDamage(val) + self.Damage = val + local sv_max = GetConVar("pac_sv_damage_zone_max_damage"):GetInt() + if self.Damage > sv_max then + self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) end end diff --git a/lua/pac3/core/shared/movement.lua b/lua/pac3/core/shared/movement.lua index a6b13caf3..43111e866 100644 --- a/lua/pac3/core/shared/movement.lua +++ b/lua/pac3/core/shared/movement.lua @@ -43,6 +43,7 @@ if SERVER then if str == "disable" then ply.pac_movement = nil ply:GetPhysicsObject():SetMass(default.Mass) + ply.scale_mass = 1 else if default[str] ~= nil then local val = net.ReadType() @@ -162,14 +163,14 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) end if (movementConvar:GetInt() == 1 or (movementConvar:GetInt() == -1 and hook.Run("PlayerNoClip", ply, true) == true)) and massDamageScale:GetInt() == 1 then - scale_mass = 85/math.Clamp(self.Mass, math.max(massLowerLimit:GetFloat(), 0.01), massUpperLimit:GetFloat()) + ply.scale_mass = 85/math.Clamp(self.Mass, math.max(massLowerLimit:GetFloat(), 0.01), massUpperLimit:GetFloat()) else - scale_mass = 1 + ply.scale_mass = 1 end pac.AddHook("EntityTakeDamage", "PAC3MassDamageScale", function(target, dmginfo) if (target:IsPlayer() and dmginfo:IsDamageType(DMG_CRUSH or DMG_VEHICLE)) then - dmginfo:ScaleDamage(scale_mass) + dmginfo:ScaleDamage(target.scale_mass or 1) end end) @@ -282,7 +283,7 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) if self.MaxGroundSpeed == 0 then self.MaxGroundSpeed = 400 end if self.MaxAirSpeed == 0 then self.MaxAirSpeed = 400 end local water_speed = math.min(ground_speed, self.MaxAirSpeed, self.MaxGroundSpeed) - print("water speed " .. water_speed) + --print("water speed " .. water_speed) ang = ply:EyeAngles() local vel2 = Vector() diff --git a/lua/pac3/editor/client/init.lua b/lua/pac3/editor/client/init.lua index 933d58a9b..eff163823 100644 --- a/lua/pac3/editor/client/init.lua +++ b/lua/pac3/editor/client/init.lua @@ -86,6 +86,7 @@ pace.ActivePanels = pace.ActivePanels or {} pace.Editor = NULL local remember = CreateConVar("pac_editor_remember_position", "1", {FCVAR_ARCHIVE}, "Remember PAC3 editor position on screen") +local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's vertical divider position") local positionMode = CreateConVar("pac_editor_position_mode", "0", {FCVAR_ARCHIVE}, "Editor position mode. 0 - Left, 1 - middle, 2 - Right. Has no effect if pac_editor_remember_position is true") local showCameras = CreateConVar("pac_show_cameras", "1", {FCVAR_ARCHIVE}, "Show the PAC cameras of players using the editor") local showInEditor = CreateConVar("pac_show_in_editor", "1", {FCVAR_ARCHIVE}, "Show the 'In PAC3 Editor' text above players using the editor") @@ -134,6 +135,15 @@ function pace.OpenEditor() editor:SetPos(0, 0) end end + + if remember_divider:GetBool() then + pace.vertical_div_height = pace.vertical_div_height or ScrH()/1.4 + + timer.Simple(0, function() + editor.div:SetTopHeight(pace.vertical_div_height) + end) + + end if ctp and ctp.Disable then ctp:Disable() diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 0cc11532c..40347b728 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -95,6 +95,8 @@ local function populate_pac(menu) do menu:AddOption(L"exit", function() pace.CloseEditor() end):SetImage(pace.MiscIcons.exit) end + + end local function populate_view(menu) @@ -114,6 +116,7 @@ local function populate_options(menu) menu:AddCVar(L"inverse collapse/expand controls", "pac_reverse_collapse", "1", "0") menu:AddCVar(L"enable shift+move/rotate clone", "pac_grab_clone", "1", "0") menu:AddCVar(L"remember editor position", "pac_editor_remember_position", "1", "0") + menu:AddCVar(L"remember divider position", "pac_editor_remember_divider_height", "1", "0") menu:AddCVar(L"ask before loading autoload", "pac_prompt_for_autoload", "1", "0") local prop_pac_load_mode, pnlpplm = menu:AddSubMenu("(singleplayer only) How to handle prop/npc outfits", function() end) @@ -138,6 +141,23 @@ local function populate_options(menu) popup_pref_mode:AddOption(L"menu bar", function() RunConsoleCommand("pac_popups_preferred_location", "menu bar") end):SetImage('icon16/layout_header.png') popup_pref_mode:AddOption(L"cursor", function() RunConsoleCommand("pac_popups_preferred_location", "cursor") end):SetImage('icon16/mouse.png') popup_pref_mode:AddOption(L"screen", function() RunConsoleCommand("pac_popups_preferred_location", "screen") end):SetImage('icon16/monitor.png') + + menu:AddOption(L"configure event wheel", pace.ConfigureEventWheelMenu):SetImage("icon16/color_wheel.png") + + local copilot, pnlc = menu:AddSubMenu("configure editor copilot", function() end) + copilot.GetDeleteSelf = function() return false end + pnlc:SetImage("icon16/award_star_gold_3.png") + copilot:AddCVar(L"show info popup when changing an event's type", "pac_copilot_make_popup_when_selecting_event", "1", "0") + copilot:AddCVar(L"auto-focus on the main property when creating some parts", "pac_copilot_auto_focus_main_property_when_creating_part","1","0") + copilot:AddCVar(L"auto-setup a command event when entering a name as an event type", "pac_copilot_auto_setup_command_events", "1", "0") + copilot:AddCVar(L"open asset browser when creating some parts", "pac_copilot_open_asset_browser_when_creating_part", "1", "0") + copilot:AddCVar(L"disable the editor view when creating a camera part", "pac_copilot_force_preview_cameras", "1", "0") + local copilot_add_part_search_menu, pnlaps = copilot:AddSubMenu("configure the searchable add part menu", function() end) + pnlaps:SetImage("icon16/add.png") + copilot_add_part_search_menu.GetDeleteSelf = function() return false end + copilot_add_part_search_menu:AddOption(L"No copilot", function() RunConsoleCommand("pac_copilot_partsearch_depth", "-1") end):SetImage('icon16/page_white.png') + copilot_add_part_search_menu:AddOption(L"automatically select a text field after creating the part (e.g. event type)", function() RunConsoleCommand("pac_copilot_partsearch_depth", "0") end):SetImage('icon16/layout_edit.png') + copilot_add_part_search_menu:AddOption(L"open another quick list menu (event types, favorite models...)", function() RunConsoleCommand("pac_copilot_partsearch_depth", "1") end):SetImage('icon16/application_view_list.png') local combat_consents, pnlcc = menu:AddSubMenu("pac combat consents", function() end) combat_consents.GetDeleteSelf = function() return false end @@ -168,6 +188,7 @@ local function populate_options(menu) menu:AddCVar(L"enable language identifier in text fields", "pac_editor_languageid", "1", "0") pace.AddLanguagesToMenu(menu) pace.AddFontsToMenu(menu) + menu:AddCVar(L"Use the new PAC4.5 icon", "pac_icon", "1", "0") menu:AddSpacer() @@ -204,6 +225,7 @@ function pace.PopulateMenuBarTab(menu, tab) elseif tab == "view" then populate_view(menu) end + --timer.Simple(0.3, function() menu:RequestFocus() end) end function pace.OnMenuBarPopulate(bar) @@ -218,11 +240,11 @@ function pace.OnMenuBarPopulate(bar) pace.AddToolsToMenu(bar:AddMenu(L"tools")) bar:RequestFocus(true) - timer.Simple(0.2, function() + --[[timer.Simple(0.2, function() if IsValid(bar) then bar:RequestFocus(true) end - end) + end)]] end function pace.OnOpenMenu() diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index f4ff29d86..36c086daa 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -17,6 +17,7 @@ local use_tabs = CreateClientConVar("pac_property_tabs", 1, true) local zoom_persistent = CreateClientConVar("pac_zoom_persistent", 0, true, false, 'Keep zoom between sessions.') local zoom_mousewheel = CreateClientConVar("pac_zoom_mousewheel", 0, true, false, 'Enable zooming with mouse wheel.') local zoom_smooth = CreateClientConVar("pac_zoom_smooth", 0, true, false, 'Enable smooth zooming.') +local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's vertical divider position") function PANEL:Init() self:SetTitle("") @@ -172,6 +173,10 @@ function PANEL:MakeBar() end function PANEL:OnRemove() + if remember_divider:GetBool() then + pace.vertical_div_height = self.div:GetTopHeight() + end + if self.menu_bar:IsValid() then self.menu_bar:Remove() end @@ -204,10 +209,11 @@ function PANEL:Think(...) local bar = self.menu_bar - self:SetTall(ScrH()) + + self:SetTall(ScrH() - (self.y_offset or 0)) local w = math.max(self:GetWide(), 200) self:SetWide(w) - self:SetPos(math.Clamp(self:GetPos(), 0, ScrW() - w), 0) + self:SetPos(math.Clamp(self:GetPos(), 0, ScrW() - w), (self.y_offset or 0)) if x ~= self.last_x then self:SetCookie("x", x) @@ -287,6 +293,9 @@ function PANEL:PerformLayout() end if self.old_part ~= pace.current_part then + if remember_divider:GetBool() then + pace.vertical_div_height = self.div:GetTopHeight() + end self.div:InvalidateLayout() self.bottom:PerformLayout() pace.properties:PerformLayout() @@ -301,10 +310,23 @@ function PANEL:PerformLayout() local oldh = self.div:GetTopHeight() if newh= 1 then - self.div:SetTopHeight(newh) + + if remember_divider:GetBool() then + if remember_divider:GetBool() then + self.div:SetTopHeight(pace.vertical_div_height) + else + self.div:SetTopHeight(newh) + end + else + self.div:SetTopHeight(newh) + end end end end diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 7d969a9ad..101ff769e 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -3,6 +3,7 @@ local L = pace.LanguageString local languageID = CreateClientConVar("pac_editor_languageid", 1, true, false, "Whether we should show the language indicator inside of editable text entries.") local favorites_menu_expansion = CreateClientConVar("pac_favorites_try_to_build_asset_series", "0", true, false) + local searched_cache_series_results = {} function pace.ShowSpecial(pnl, parent, size) @@ -288,6 +289,25 @@ pac.AddHook("GUIMousePressed", "pace_SafeRemoveSpecialPanel", function() end end) +pac.AddHook("PostRenderVGUI", "flash_properties", function() + if not pace.flashes then return end + for pnl, tbl in pairs(pace.flashes) do + if IsValid(pnl) then + --print(pnl:LocalToScreen(0,0)) + local x,y = pnl:LocalToScreen(0,0) + local flash_alpha = 255*math.pow(math.Clamp((tbl.flash_end - CurTime()) / 2.5,0,1), 0.6) + surface.SetDrawColor(Color(tbl.color.r, tbl.color.g, tbl.color.b, flash_alpha)) + local flash_size = 300*math.pow(math.Clamp((tbl.flash_end - 1.8 - CurTime()) / 0.7,0,1), 8) + 5 + if pnl:GetY() > 4 then + surface.DrawOutlinedRect(-flash_size + x,-flash_size + y,pnl:GetWide() + 2*flash_size,pnl:GetTall() + 2*flash_size,5) + surface.SetDrawColor(Color(tbl.color.r, tbl.color.g, tbl.color.b, flash_alpha/2)) + surface.DrawOutlinedRect(-flash_size + x - 3,-flash_size + y - 3,pnl:GetWide() + 2*flash_size + 6,pnl:GetTall() + 2*flash_size + 6,2) + end + if tbl.flash_end < CurTime() then pace.flashes[pnl] = nil end + end + end +end) + do -- container local PANEL = {} @@ -313,6 +333,28 @@ do -- container derma.SkinHook( "Paint", "CategoryButton", self, w, h ) end + function PANEL:Flash() + pace.flashes = pace.flashes or {} + pace.flashes[self] = {start = CurTime(), flash_end = CurTime() + 2.5, color = Color(255,0,0)} + + do --scroll to the property + local _,y = self:LocalToScreen(0,0) + local _,py = pace.properties:LocalToScreen(0,0) + local scry = pace.properties.scr:GetScroll() + + if y > ScrH() then + pace.properties.scr:SetScroll(scry - py + y) + elseif y < py - 200 then + pace.properties.scr:SetScroll(scry + (y - py) - 100) + end + end + + do --scroll to the tree node + pace.tree:ScrollToChild(self:GetChildren()[1].part.pace_tree_node) + end + + end + function PANEL:SetContent(pnl) pnl:SetParent(self) self.content = pnl @@ -880,11 +922,17 @@ end do -- base editable local PANEL = {} + PANEL.ClassName = "properties_base_type" PANEL.Base = "DLabel" PANEL.SingleClick = true + function PANEL:Flash() + --redirect to the parent (container) + self:GetParent():Flash() + end + function PANEL:OnCursorMoved() self:SetCursor("hand") end @@ -1246,12 +1294,22 @@ do -- base editable pnl:AddOption("Current 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") for id,mdl in ipairs(pace.bookmarked_ressources["models"]) do pnl: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 @@ -1988,6 +2046,7 @@ do -- vector end, 0.25 ) + end do -- number @@ -2151,3 +2210,180 @@ do -- boolean pace.RegisterPanel(PANEL) end + + +local tree_search_excluded_vars = { + ["ParentUID"] = true, + ["UniqueID"] = true, + ["ModelTracker"] = true, + ["ClassTracker"] = true, + ["LoadVmt"] = true +} + +function pace.OpenTreeSearch() + if pace.tree_search_open then return end + pace.Editor.y_offset = 24 + pace.tree_search_open = true + pace.tree_search_match_index = 0 + pace.tree_search_matches = {} + local resulting_part + local search_term = "friend" + local matched_property + local matches = {} + + local base = vgui.Create("DFrame") + pace.tree_searcher = base + local edit = vgui.Create("DTextEntry", base) + local search_button = vgui.Create("DButton", base) + local range_label = vgui.Create("DLabel", base) + local close_button = vgui.Create("DButton", base) + local case_box = vgui.Create("DButton", base) + + case_box:SetText("Aa") + case_box:SetPos(325,2) + case_box:SetSize(25,20) + case_box:SetTooltip("case sensitive") + case_box:SetColor(Color(150,150,150)) + case_box:SetFont("DermaDefaultBold") + function case_box:DoClick() + self.on = not self.on + if self.on then + self:SetColor(Color(0,0,0)) + else + self:SetColor(Color(150,150,150)) + end + end + + + local function select_match() + if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end + if not pace.tree_search_matches[pace.tree_search_match_index] then return end + + resulting_part = pace.tree_search_matches[pace.tree_search_match_index].part_matched + matched_property = pace.tree_search_matches[pace.tree_search_match_index].key_matched + if resulting_part ~= pace.current_part then pace.OnPartSelected(resulting_part, true) end + local parent = resulting_part:GetParent() + while IsValid(parent) and (parent:GetParent() ~= parent) do + parent.pace_tree_node:SetExpanded(true) + parent = parent:GetParent() + if parent:IsValid() then + parent.pace_tree_node:SetExpanded(true) + end + end + --pace.RefreshTree() + pace.FlashProperty(resulting_part, matched_property, false) + end + + function base.OnRemove() + pace.tree_search_open = false + if not IsValid(pace.Editor) then return end + pace.Editor.y_offset = 0 + end + + function base.Think() + if not IsValid(pace.Editor) then base:Remove() return end + if not pace.Focused then base:Remove() end + base:SetX(pace.Editor:GetX()) + end + function base.Paint(_,w,h) + surface.SetDrawColor(Color(255,255,255)) + surface.DrawRect(0,0,w,h) + end + base:SetDraggable(false) + base:SetX(pace.Editor:GetX()) + base:ShowCloseButton(false) + + close_button:SetSize(40,20) + close_button:SetPos(450,2) + close_button:SetText("close") + function close_button.DoClick() + base:Remove() + end + + local fwd = vgui.Create("DButton", base) + local bck = vgui.Create("DButton", base) + + local function perform_search() + local case_sensitive = case_box.on + matches = {} + pace.tree_search_matches = {} + search_term = edit:GetText() + if not case_sensitive then search_term = string.lower(search_term) end + for _,part in pairs(pac.GetLocalParts()) do + + for k,v in pairs(part:GetProperties()) do + local value = v.get(part) + + if (type(value) ~= "number" and type(value) ~= "string") or tree_search_excluded_vars[v.key] then continue end + + value = tostring(value) + if not case_sensitive then value = string.lower(value) end + + if string.find(case_sensitive and v.key or string.lower(v.key), search_term) or (string.find(value, search_term)) then + if v.key == "Name" and part.Name == "" then continue end + table.insert(matches, #matches + 1, {part_matched = part, key_matched = v.key}) + table.insert(pace.tree_search_matches, #matches, {part_matched = part, key_matched = v.key}) + end + end + end + table.sort(pace.tree_search_matches, function(a, b) return select(2, a.part_matched.pace_tree_node:LocalToScreen()) < select(2, b.part_matched.pace_tree_node:LocalToScreen()) end) + if table.IsEmpty(matches) then range_label:SetText("0 / 0") else pace.tree_search_match_index = 1 end + range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) + end + + base:SetSize(492,24) + edit:SetSize(290,20) + edit:SetPos(0,2) + base:MakePopup() + edit:RequestFocus() + edit:SetUpdateOnType(true) + edit.previous_search = "" + + range_label:SetSize(50,20) + range_label:SetPos(295,2) + range_label:SetText("0 / 0") + range_label:SetTextColor(Color(0,0,0)) + + fwd:SetSize(25,20) + fwd:SetPos(375,2) + fwd:SetText(">") + function fwd.DoClick() + if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end + pace.tree_search_match_index = (pace.tree_search_match_index % math.max(#matches,1)) + 1 + range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) + select_match() + end + + search_button:SetSize(50,20) + search_button:SetPos(400,2) + search_button:SetText("search") + function search_button.DoClick() + perform_search() + select_match() + end + + bck:SetSize(25,20) + bck:SetPos(350,2) + bck:SetText("<") + function bck.DoClick() + if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end + pace.tree_search_match_index = ((pace.tree_search_match_index - 2 + #matches) % math.max(#matches,1)) + 1 + range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) + select_match() + end + + function edit.OnEnter() + if self.previous_search ~= edit:GetText() then + perform_search() + self.previous_search = edit:GetText() + elseif not table.IsEmpty(pace.tree_search_matches) then + fwd:DoClick() + else + perform_search() + end + select_match() + + timer.Simple(0.1,function() edit:RequestFocus() end) + end + +end \ No newline at end of file diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index ab21efac1..ec92f2e36 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -22,6 +22,28 @@ CreateConVar( "pac_hover_halo_limit", 100, FCVAR_ARCHIVE, "max number of parts b CreateConVar( "pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") CreateConVar( "pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") +CreateConVar("pac_copilot_partsearch_depth", -1, FCVAR_ARCHIVE, "amount of copiloting in the searchable part menu\n-1:none\n0:auto-focus on the text edit for events\n1:bring up a list of clickable event types\nother parts aren't supported yet") +CreateConVar("pac_copilot_make_popup_when_selecting_event", 1, FCVAR_ARCHIVE, "whether to create a popup so you can read what an event does") +CreateConVar("pac_copilot_open_asset_browser_when_creating_part", 0, FCVAR_ARCHIVE, "whether to open the asset browser for models, materials, or sounds") +CreateConVar("pac_copilot_force_preview_cameras", 1, FCVAR_ARCHIVE, "whether to force the editor camera off when creating a camera part") +CreateConVar("pac_copilot_auto_setup_command_events", 0, FCVAR_ARCHIVE, "whether to automatically setup a command event if the name you type doesn't match an existing event. we'll assume you want a command event name.\nif this is set to 0, it will only auto-setup in such a case if you already have such a command event actively present in your events or waiting to be activated by your command parts") +CreateConVar("pac_copilot_auto_focus_main_property_when_creating_part", 1, FCVAR_ARCHIVE, "whether to automatically focus on the main property that defines a part, such as the event's event type, the text's text, the proxy's expression or the command's string.") + +--the necessary properties we always edit for certain parts +--others might be opened with the asset browser so this is not the full list +--should be a minimal list because we don't want to get too much in the way of routine editing +local star_properties = { + ["event"] = "Event", + ["proxy"] = "Expression", + ["text"] = "Text", + ["command"] = "String", + ["animation"] = "SequenceName", + ["flex"] = "Flex", + ["bone3"] = "Bone", + ["poseparameter"] = "PoseParameter", + ["damage_zone"] = "Damage", + ["hitscan"] = "Damage" +} -- load only when hovered above local function add_expensive_submenu_load(pnl, callback) @@ -126,6 +148,96 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) pace.RefreshTree() timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) + + if GetConVar("pac_copilot_open_asset_browser_when_creating_part"):GetBool() then + timer.Simple(0.5, function() + local self = nil + if class_name == "model2" then + self = pace.current_part.pace_properties["Model"] + + pace.AssetBrowser(function(path) + if not part:IsValid() then return end + -- because we refresh the properties + + if IsValid(self) and self.OnValueChanged then + self.OnValueChanged(path) + end + + if pace.current_part.SetMaterials then + local model = pace.current_part:GetModel() + local part = pace.current_part + if part.pace_last_model and part.pace_last_model ~= model then + part:SetMaterials("") + end + part.pace_last_model = model + end + + pace.PopulateProperties(pace.current_part) + + for k,v in ipairs(pace.properties.List) do + if v.panel and v.panel.part == part and v.key == key then + self = v.panel + break + end + end + + end, "models") + elseif class_name == "sound" or class_name == "sound2" then + if class_name == "sound"then + self = pace.current_part.pace_properties["Sound"] + elseif class_name == "sound2" then + self = pace.current_part.pace_properties["Path"] + end + + pace.AssetBrowser(function(path) + if not self:IsValid() then return end + + self:SetValue(path) + self.OnValueChanged(path) + + end, "sound") + elseif pace.current_part.pace_properties["LoadVmt"] then + self = pace.current_part.pace_properties["LoadVmt"] + pace.AssetBrowser(function(path) + if not self:IsValid() then return end + path = string.gsub(string.StripExtension(path), "^materials/", "") or "error" + self:SetValue(path) + self.OnValueChanged(path) + pace.current_part:SetLoadVmt(path) + end, "materials") + + end + end) + + end + if class_name == "camera" and GetConVar("pac_copilot_force_preview_cameras"):GetBool() then + RunConsoleCommand("pac_enable_editor_view", "0") + pace.EnableView(true) + pac.RemoveHook("CalcView", "editor") + pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) + part:CalcShowHide() + pos, ang, fov, nearz, farz = part:CalcView(_,_,ply:EyeAngles()) + local temp = {} + temp.origin = pos + temp.angles = ang + temp.fov = fov + temp.znear = nearz + temp.zfar = farz + temp.drawviewer = not part.DrawViewModel + return temp + + end) + end + if GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool() then + if star_properties[part.ClassName] then + timer.Simple(0.2, function() + + pace.FlashProperty(part, star_properties[part.ClassName], true) + + end) + end + end + return part end @@ -213,6 +325,28 @@ function pace.OnPartSelected(part, is_selecting) end +pace.suppress_flashing_property = false + +function pace.FlashProperty(obj, key, edit) + if pace.suppress_flashing_property then return end + if not obj.flashing_property then + obj.flashing_property = true + timer.Simple(0.1, function() + if not obj.pace_properties[key] then return end + obj.pace_properties[key]:Flash() + pace.current_flashed_property = key + if edit then + obj.pace_properties[key]:RequestFocus() + if obj.pace_properties[key].EditText then + obj.pace_properties[key]:EditText() + end + end + end) + timer.Simple(0.3, function() obj.flashing_property = false end) + end + +end + function pace.OnVariableChanged(obj, key, val, not_from_editor) local valType = type(val) if valType == 'Vector' then @@ -550,6 +684,15 @@ do -- menu end function pace.OnAddPartMenu(obj) + local event_part_template + for _, part in ipairs(pace.GetRegisteredParts()) do + if part.ClassName == "event" then + event_part_template = part + end + end + local mode = GetConVar("pac_copilot_partsearch_depth"):GetInt() + pace.suppress_flashing_property = false + local base = vgui.Create("EditablePanel") base:SetPos(input.GetCursorPos()) base:SetSize(200, 300) @@ -568,36 +711,210 @@ do -- menu local result = base:Add("DScrollPanel") result:Dock(FILL) + base.search_mode = "classes" + + local function populate_with_sounds(base,result,filter) + base.search_mode = "sounds" + for _,snd in ipairs(pace.bookmarked_ressources["sound"]) do + if filter ~= nil and filter ~= "" then + if snd:find(filter, nil, true) then + table.insert(result.found, snd) + end + else + table.insert(result.found, snd) + end + end + for _,snd in ipairs(result.found) do + if not isstring(snd) then continue end + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + local icon = "icon16/sound.png" - function edit:OnEnter() - if result.found[1] then - pace.RecordUndoHistory() - pace.Call("CreatePart", result.found[1].ClassName) - pace.RecordUndoHistory() + 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 + + btn:SetIcon(icon) + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText(snd) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() + + line.DoClick = function() + if pace.current_part.ClassName == "sound" then + pace.current_part:SetSound(snd) + elseif pace.current_part.ClassName == "sound2" then + pace.current_part:SetPath(snd) + end + pace.PopulateProperties(pace.current_part) + base:Remove() + end + + line:Dock(TOP) end - base:Remove() end - edit.OnValueChange = function(_, str) - result:Clear() - result.found = {} + local function populate_with_models(base,result,filter) + base.search_mode = "models" + for _,mdl in ipairs(pace.bookmarked_ressources["models"]) do + if filter ~= nil and filter ~= "" then + if mdl:find(filter, nil, true) then + table.insert(result.found, mdl) + end + else + table.insert(result.found, mdl) + end + end + for _,mdl in ipairs(result.found) do + if not isstring(mdl) then continue end + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + btn:SetIcon("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText(mdl) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() + + line.DoClick = function() + if pace.current_part.Model then + pace.current_part:SetModel(mdl) + pace.PopulateProperties(pace.current_part) + end + base:Remove() + end + + line:Dock(TOP) + end + end - for _, part in ipairs(pace.GetRegisteredParts()) do - if (part.FriendlyName or part.ClassName):find(str, nil, true) then - table.insert(result.found, part) + local function populate_with_events(base,result,filter) + base.search_mode = "events" + for e,tbl in pairs(event_part_template.Events) do + if filter ~= nil and filter ~= "" then + if e:find(filter, nil, true) then + table.insert(result.found, e) + end + else + table.insert(result.found, e) end end + for _,e in ipairs(result.found) do + if not isstring(e) then continue end + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + btn:SetIcon("icon16/clock.png") + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText(e) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() - table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end) + line.DoClick = function() + if pace.current_part.Event then + pace.current_part:SetEvent(e) + pace.PopulateProperties(pace.current_part) + end + base:Remove() + end + + line:Dock(TOP) + end + end + local function populate_with_classes(base, result, filter) + for i, part in ipairs(pace.GetRegisteredParts()) do + if filter then + if (part.FriendlyName or part.ClassName):find(filter, nil, true) then + table.insert(result.found, part) + end + else table.insert(result.found, part) end + end + table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end) for _, part in ipairs(result.found) do local line = result:Add("DButton") line:SetText("") line:SetTall(20) + local remove_now = false line.DoClick = function() pace.RecordUndoHistory() pace.Call("CreatePart", part.ClassName) - base:Remove() + + if part.ClassName == "event" then + remove_now = false + result:Clear() + result.found = {} + + if mode == 0 then --auto-focus mode + remove_now = true + timer.Simple(0.1, function() + pace.FlashProperty(pace.current_part, "Event", true) + end) + elseif mode == 1 then --event partsearch + pace.suppress_flashing_property = true + populate_with_events(base,result,"") + edit:SetText("") + edit:RequestFocus() + else + remove_now = true + end + elseif part.ClassName == "model2" and mode == 1 then --model partsearch + remove_now = false + result:Clear() + result.found = {} + populate_with_models(base,result,"") + pace.suppress_flashing_property = true + edit:SetText("") + edit:RequestFocus() + elseif (part.ClassName == "sound" or part.ClassName == "sound2") and mode == 1 then + remove_now = false + result:Clear() + result.found = {} + populate_with_sounds(base,result,"") + pace.suppress_flashing_property = true + edit:SetText("") + edit:RequestFocus() + elseif star_properties[result.found[1].ClassName] and (mode == 0 or GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool()) then + pace.suppress_flashing_property = false + local classname = part.ClassName + timer.Simple(0.1, function() + pace.FlashProperty(pace.current_part, star_properties[classname], true) + end) + remove_now = true + else + remove_now = true + end + timer.Simple(0.4, function() + pace.suppress_flashing_property = false + end) + if remove_now then base:Remove() end pace.RecordUndoHistory() end @@ -628,7 +945,106 @@ do -- menu line:Dock(TOP) end + end + + function edit:OnEnter() + local remove_now = true + if result.found[1] then + if base.search_mode == "classes" then + pace.RecordUndoHistory() + local part = pace.Call("CreatePart", result.found[1].ClassName) + pace.RecordUndoHistory() + + if mode == 1 then + if result.found[1].ClassName == "event" then + result:Clear() + populate_with_events(base,result,"") + elseif result.found[1].ClassName == "model2" then + result:Clear() + populate_with_models(base,result,"") + end + + else + base:Remove() + end + + elseif base.search_mode == "events" then + if pace.current_part.Event then + pace.current_part:SetEvent() + pace.PopulateProperties(pace.current_part) + end + base:Remove() + + elseif base.search_mode == "models" then + if mode == 1 then + result:Clear() + pace.current_part:SetModel(result.found[1]) + pace.PopulateProperties(pace.current_part) + else + base:Remove() + end + elseif base.search_mode == "sounds" then + if mode == 1 then + result:Clear() + if pace.current_part.ClassName == "sound" then + pace.current_part:SetSound(result.found[1]) + elseif pace.current_part.ClassName == "sound2" then + pace.current_part:SetPath(result.found[1]) + end + pace.PopulateProperties(pace.current_part) + else + base:Remove() + end + + end + + if result.found[1].ClassName == "event" then + remove_now = false + result:Clear() + result.found = {} + if mode == 0 then + remove_now = true + timer.Simple(0.1, function() + pace.FlashProperty(pace.current_part, "Event", true) + end) + elseif mode == 1 then + pace.suppress_flashing_property = true + populate_with_events(base,result,"") + edit:SetText("") + edit:RequestFocus() + else + remove_now = true + end + elseif star_properties[result.found[1].ClassName] and (mode == 0 or GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool()) then + local classname = result.found[1].ClassName + timer.Simple(0.1, function() + pace.FlashProperty(pace.current_part, star_properties[classname], true) + end) + end + end + timer.Simple(0.4, function() + pace.suppress_flashing_property = false + end) + if remove_now then base:Remove() end + end + + edit.OnValueChange = function(_, str) + result:Clear() + result.found = {} + local remove_now = true + if base.search_mode == "classes" then + populate_with_classes(base, result, str) + + elseif base.search_mode == "events" then + populate_with_events(base,result,str,event_template) + + elseif base.search_mode == "models" then + populate_with_models(base,result,str) + elseif base.search_mode == "sounds" then + populate_with_sounds(base,result,str) + end + --base:SetHeight(20 * #result.found + edit:GetTall()) base:SetHeight(600 + edit:GetTall()) @@ -643,6 +1059,12 @@ do -- menu end end end) + + timer.Simple(0.1, function() + base:MoveToFront() + base:RequestFocus() + end) + end function pace.Copy(obj) @@ -695,6 +1117,17 @@ do -- menu if not obj:HasParent() and obj.ClassName == "group" then pace.RemovePartOnServer(obj:GetUniqueID(), false, true) end + if obj.ClassName == "camera" and GetConVar("pac_copilot_force_preview_cameras"):GetBool() then + local no_camera_part = true + for i,v in ipairs(pac.GetLocalParts()) do + if v.ClassName == "camera" then no_camera_part = false end + end + if no_camera_part then + RunConsoleCommand("pac_enable_editor_view", "1") + pac.RemoveHook("CalcView", "camera_part") + pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil) + end + end end function pace.SwapBaseMovables(obj1, obj2, promote) @@ -943,8 +1376,13 @@ do -- menu basepart_label:SetText("base part: "..partinfo) basepart_label:SetFont("Font") - local excluded_vars = {"Duplicate","OwnerName","ParentUID","UniqueID","TargetEntityUID"} - local var_candidates = {} + local excluded_vars = { + ["Duplicate"] = true, + ["OwnerName"] = true, + ["ParentUID"] = true, + ["UniqueID"] = true, + ["TargetEntityUID"] = true + } local shared_properties = {} local shared_udata_properties = {} @@ -987,6 +1425,11 @@ do -- menu shared_properties = initial_shared_properties shared_udata_properties = initial_shared_udata_properties end + + for i,v in ipairs(shared_properties) do + if excluded_vars[v] then table.remove(shared_properties,i) end + end + --populate panels for standard GetSet part properties for i,v in pairs(shared_properties) do local VAR_PANEL = vgui.Create("DFrame") @@ -1209,10 +1652,8 @@ do -- menu sent_var = sent_var..i end else sent_var = VAR_PANEL_EDITZONE:GetValue() end - - print(part, part:GetArguments(), v, sent_var, GetEventArgIndex(part,v)) + part:SetArguments(ApplyArgToIndex(part:GetArguments(), sent_var, GetEventArgIndex(part,v))) - print(part:GetArguments()) end if thoroughness_tickbox:GetChecked() then @@ -1858,11 +2299,13 @@ function pace.UltraCleanup(obj) local marked_for_deletion = {} local function IsImportantMarked(part) - if part.Notes and part.Notes == "important" then return true end + if not IsValid(part) then return false end + if part.Notes == "important" then return true end return false end local function FoundImportantMarkedParent(part) + if not IsValid(part) then return false end if IsImportantMarked(part) then return true end local root = part:GetRootPart() local parent = part @@ -1874,6 +2317,7 @@ function pace.UltraCleanup(obj) end local function Important(part) + if not IsValid(part) then return false end return IsImportantMarked(part) or FoundImportantMarkedParent(part) end @@ -2062,7 +2506,10 @@ function pace.UltraCleanup(obj) --first pass: absolute unsafes: hidden parts for i,v in pairs(root:GetChildrenList()) do if v:IsHidden() or v.Hide then - v:Remove() + if not FoundImportantMarkedParent(part) then + v:Remove() + end + end end diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 0e6564104..38589542f 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -201,6 +201,7 @@ function pace.LoadParts(name, clear, override_part) end else + if name ~= "autoload.txt" and not string.find(name, "pac3/__backup") then cookie.Set( "pac_last_loaded_outfit", name ) end if hook.Run("PrePACLoadOutfit", name) == false then return end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index b57e92529..2e753054f 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -40,6 +40,8 @@ local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_ne local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") +local enforce_distance = CreateConVar("pac_sv_combat_distance_enforced", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enforce a limit on how far a pac combat action can originate.\nIf set to a distance, it will prevent actions that are too far from the acting player.\n0 to disable.") + local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") @@ -547,7 +549,9 @@ local function encode_table_to_file(str) str = category file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data)) end - + elseif str == "eventwheel_colors" then + data = pace.command_colors or {} + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) end end @@ -575,6 +579,8 @@ local function decode_table_from_file(str) elseif str == "pac_part_categories" then pace.partgroups = util.KeyValuesToTable(data) + elseif str == "eventwheel_colors" then + pace.command_colors = util.KeyValuesToTable(data) end @@ -881,6 +887,14 @@ function pace.FillCombatSettings(pnl) sv_player_fraction_slider:SetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone") sv_player_fraction_slider:SetTooltip("This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.") + local sv_distance_slider = vgui.Create("DNumSlider", general_list_list) + sv_distance_slider:SetText("distance to block combat actions that are too far") + sv_distance_slider:SetValue(GetConVar("pac_sv_combat_distance_enforced"):GetFloat()) + sv_distance_slider:SetMin(0) sv_distance_slider:SetDecimals(0) sv_distance_slider:SetMax(64000) + sv_distance_slider:SetSize(400,30) + sv_distance_slider:SetConVar("pac_sv_combat_distance_enforced") + sv_distance_slider:SetTooltip("The distance is compared between the action's origin and the player's position.\n0 to ignore.") + end do --hitscan @@ -1353,8 +1367,8 @@ function pace.FillEditorSettings(pnl) partmenu_order_presets:SetText("Select a part menu preset") partmenu_order_presets:AddChoice("factory preset") partmenu_order_presets:AddChoice("expanded PAC4.5 preset") - partmenu_order_presets:AddChoice("custom preset") partmenu_order_presets:AddChoice("bulk select poweruser") + partmenu_order_presets:AddChoice("user preset") partmenu_order_presets:SetX(10) partmenu_order_presets:SetY(10) partmenu_order_presets:SetWidth(200) partmenu_order_presets:SetHeight(20) @@ -1756,7 +1770,7 @@ function pace.FillEditorSettings(pnl) partmenu_choices:SetY(50) partmenu_choices:SetX(10) for i,v in pairs(pace.operations_all_operations) do - local pnl = vgui.Create("DButton", menu) + local pnl = vgui.Create("DButton", f) pnl:SetText(string.Replace(string.upper(v),"_"," ")) pnl:SetImage(FindImage(v)) @@ -1812,8 +1826,8 @@ function pace.FillEditorSettings(pnl) temp_list = table.Copy(pace.operations_experimental) elseif value == "bulk select poweruser" then temp_list = table.Copy(pace.operations_bulk_poweruser) - elseif value == "custom preset" then - temp_list = {"wear","save","load"} + elseif value == "user preset" then + temp_list = pace.operations_order end ClearPartMenuPreviewList() for i,v in ipairs(temp_list) do @@ -1846,6 +1860,14 @@ function pace.FillEditorSettings(pnl) PrintTable(buildlist_partmenu) end + + if pace.operations_order then + for i,v in pairs(pace.operations_order) do + table.insert(buildlist_partmenu,v) + partmenu_previews:AddLine(#buildlist_partmenu,v) + end + end + return f end @@ -2323,9 +2345,230 @@ function pace.GetPartMenuComponentPreviewForMenuEdit(menu, option_name) return pnl end +function pace.ConfigureEventWheelMenu() + pace.command_colors = pace.command_colors or {} + local master_panel = vgui.Create("DFrame") + master_panel:SetTitle("event wheel config") + master_panel:SetSize(500,800) + master_panel:Center() + local mid_panel = vgui.Create("DPanel", master_panel) + mid_panel:Dock(FILL) + + local scr_pnl = vgui.Create("DScrollPanel", mid_panel) + scr_pnl:SetSize(490,800) + scr_pnl:SetPos(0,45) + local list = vgui.Create("DListLayout", scr_pnl) list:Dock(FILL) + + local first_panel = vgui.Create("DPanel", mid_panel) + first_panel:SetSize(500,40) + first_panel:Dock(TOP) + + local circle_style_listmenu = vgui.Create("DComboBox",first_panel) + circle_style_listmenu:SetText("Choose eventwheel style") + circle_style_listmenu:SetSize(200,20) + circle_style_listmenu:AddChoice("legacy") + circle_style_listmenu:AddChoice("concentric") + circle_style_listmenu:AddChoice("alternative") + function circle_style_listmenu:OnSelect( index, value ) + if value == "legacy" then + GetConVar("pac_eventwheel_style"):SetString("0") + elseif value == "concentric" then + GetConVar("pac_eventwheel_style"):SetString("1") + elseif value == "alternative" then + GetConVar("pac_eventwheel_style"):SetString("2") + end + end + + local circle_clickmode = vgui.Create("DComboBox",first_panel) + circle_clickmode:SetText("Choose eventwheel clickmode") + circle_clickmode:SetSize(200,20) + circle_clickmode:SetPos(200,0) + circle_clickmode:AddChoice("clickable and activates on close") + circle_clickmode:AddChoice("not clickable, but activate on close") + circle_clickmode:AddChoice("clickable, but do not activate on close") + function circle_clickmode:OnSelect( index, value ) + if value == "clickable and activates on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("0") + elseif value == "not clickable, but activate on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("-1") + elseif value == "clickable, but do not activate on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("1") + end + end + + + local rectangle_style_listmenu = vgui.Create("DComboBox",first_panel) + rectangle_style_listmenu:SetText("Choose eventlist style") + rectangle_style_listmenu:SetSize(200,20) + rectangle_style_listmenu:SetPos(0,20) + rectangle_style_listmenu:AddChoice("legacy-like") + rectangle_style_listmenu:AddChoice("concentric") + rectangle_style_listmenu:AddChoice("alternative") + + function rectangle_style_listmenu:OnSelect( index, value ) + if value == "legacy-like" then + GetConVar("pac_eventlist_style"):SetString("0") + elseif value == "concentric" then + GetConVar("pac_eventlist_style"):SetString("1") + elseif value == "alternative" then + GetConVar("pac_eventlist_style"):SetString("2") + end + end + + local rectangle_clickmode = vgui.Create("DComboBox",first_panel) + rectangle_clickmode:SetText("Choose eventlist clickmode") + rectangle_clickmode:SetSize(200,20) + rectangle_clickmode:SetPos(200,20) + rectangle_clickmode:AddChoice("clickable and activates on close") + rectangle_clickmode:AddChoice("not clickable, but activate on close") + rectangle_clickmode:AddChoice("clickable, but do not activate on close") + function rectangle_clickmode:OnSelect( index, value ) + if value == "clickable and activates on close" then + GetConVar("pac_eventlist_clickmode"):SetString("0") + elseif value == "not clickable, but activate on close" then + GetConVar("pac_eventlist_clickmode"):SetString("-1") + elseif value == "clickable, but do not activate on close" then + GetConVar("pac_eventlist_clickmode"):SetString("1") + end + end + + local events = {} + for i,v in pairs(pac.GetLocalParts()) do + if v.ClassName == "event" then + local e = v:GetEvent() + if e == "command" then + local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) + local this_event_hidden = v:IsHiddenBySomethingElse(false) + events[cmd] = cmd + end + end + + end + + local names = table.GetKeys( events ) + table.sort(names, function(a, b) return a < b end) + + local copied_color = nil + local lanes = {} + local colorpanel + if LocalPlayer().pac_command_events then + if table.Count(names) == 0 then + local error_label = vgui.Create("DLabel", list) + error_label:SetText("Uh oh, nothing to see here! Looks like you don't have any command events in your outfit!\nPlease go back to the editor.") + error_label:SetPos(100,200) + error_label:SetFont("DermaDefaultBold") + error_label:SetSize(450,50) + error_label:SetColor(Color(150,0,0)) + end + for _, name in ipairs(names) do + local pnl = vgui.Create("DPanel") list:Add(pnl) pnl:SetSize(400,20) + local btn = vgui.Create("DButton", pnl) + + btn:SetSize(200,25) + btn:SetText(name) + btn:SetTooltip(name) + + + if pace.command_colors[name] then + local tbl = string.Split(pace.command_colors[name], " ") + btn:SetColor(Color(tonumber(tbl[1]),tonumber(tbl[2]),tonumber(tbl[3]))) + end + local colorbutton = vgui.Create("DButton", pnl) + colorbutton:SetText("Color") + colorbutton:SetIcon("icon16/color_wheel.png") + colorbutton:SetPos(200,0) colorbutton:SetSize(65,20) + function colorbutton:DoClick() + if IsValid(colorpanel) then colorpanel:Remove() end + local clr_frame = vgui.Create("DPanel") + colorpanel = clr_frame + function clr_frame:Think() + if not pace.command_event_menu_opened and not IsValid(master_panel) then self:Remove() end + end + + local clr_pnl = vgui.Create("DColorMixer", clr_frame) + if pace.command_colors[name] then + local str_tbl = string.Split(pace.command_colors[name], " ") + clr_pnl:SetBaseColor(Color(tonumber(str_tbl[1]),tonumber(str_tbl[2]),tonumber(str_tbl[3]))) + end + + clr_frame:SetSize(300,200) clr_pnl:Dock(FILL) + clr_frame:SetPos(self:LocalToScreen(0,0)) + clr_frame:RequestFocus() + function clr_pnl:Think() + if input.IsMouseDown(MOUSE_LEFT) then + + if not IsValid(vgui.GetHoveredPanel()) then + self:Remove() clr_frame:Remove() + else + if vgui.GetHoveredPanel():GetClassName() == "CGModBase" and not self.clicking then + self:Remove() clr_frame:Remove() + end + end + self.clicking = true + else + self.clicking = false + end + end + function clr_pnl:ValueChanged(col) + pace.command_colors = pace.command_colors or {} + pace.command_colors[name] = col.r .. " " .. col.g .. " " .. col.b + btn:SetColor(col) + end + + end + + local copypastebutton = vgui.Create("DButton", pnl) + copypastebutton:SetText("Copy/Paste") + copypastebutton:SetToolTip("right click to copy\nleft click to paste") + copypastebutton:SetIcon("icon16/page_copy.png") + copypastebutton:SetPos(265,0) copypastebutton:SetSize(150,20) + function copypastebutton:DoClick() + if not copied_color then return end + pace.command_colors[name] = copied_color + btn:SetColor(Color(tonumber(string.Split(copied_color, " ")[1]), tonumber(string.Split(copied_color, " ")[2]), tonumber(string.Split(copied_color, " ")[3]))) + end + function copypastebutton:DoRightClick() + for _,tbl in pairs(lanes) do + if tbl.cmd ~= name then + tbl.copypaste:SetText("Copy/Paste") + end + end + copied_color = pace.command_colors[name] + if copied_color then + self:SetText("copied: " .. pace.command_colors[name]) + else + self:SetText("no color to copy!") + end + end + + local clearbutton = vgui.Create("DButton", pnl) + clearbutton:SetText("Clear") + clearbutton:SetIcon("icon16/cross.png") + clearbutton:SetPos(415,0) clearbutton:SetSize(60,20) + function clearbutton:DoClick() + btn:SetColor(Color(0,0,0)) + pace.command_colors[name] = nil + end + + lanes[name] = {cmd = name, main_btn = btn, color_btn = colorbutton, copypaste = copypastebutton, clear = clearbutton} + end + end + + function master_panel:OnRemove() + gui.EnableScreenClicker(false) + pace.command_event_menu_opened = nil + encode_table_to_file("eventwheel_colors", pace.command_colors) + end + + master_panel:RequestFocus() + gui.EnableScreenClicker(true) + pace.command_event_menu_opened = master_panel +end + decode_table_from_file("pac_editor_shortcuts") decode_table_from_file("pac_editor_partmenu_layouts") +decode_table_from_file("eventwheel_colors") if not file.Exists("pac_part_categories_cedrics.txt", "DATA") then file.Write("pac3_config/pac_part_categories_cedrics.txt", util.TableToKeyValues(pace.partmenu_categories_cedrics)) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 61892bdac..19355f393 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -36,6 +36,8 @@ pace.PACActionShortcut_Dictionary = { "restart", "partmenu", "add_part", + "property_search_current_part", + "property_search_in_tree", "toolbar_pac", "toolbar_tools", "toolbar_player", @@ -108,6 +110,12 @@ pace.PACActionShortcut_Default = { ["T_Pose"] = { [1] = {"CTRL", "t"} }, + ["property_search_current_part"] = { + [1] = {"CTRL", "f"} + }, + ["property_search_in_tree"] = { + [1] = {"CTRL", "SHIFT", "f"} + }, ["editor_up"] = { [1] = {"UPARROW"} }, @@ -505,6 +513,26 @@ function pace.DoShortcutFunc(action) end if action == "partmenu" then pace.OnPartMenu(pace.current_part) end + if action == "property_search_current_part" then + if pace.properties.search:IsVisible() then + pace.properties.search:SetVisible(false) + pace.properties.search:SetEnabled(false) + pace.property_searching = false + else + pace.properties.search:SetVisible(true) + pace.properties.search:RequestFocus() + pace.properties.search:SetEnabled(true) + pace.property_searching = true + end + + end + if action == "property_search_in_tree" then + if pace.tree_search_open then + pace.tree_searcher:Remove() + else + pace.OpenTreeSearch() + end + end if action == "add_part" then pace.OnAddPartMenu(pace.current_part) end if action == "toolbar_tools" then menu = DermaMenu() @@ -692,12 +720,16 @@ function pace.CheckShortcuts() end if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_F) then - pace.properties.search:SetVisible(true) - pace.properties.search:RequestFocus() - pace.properties.search:SetEnabled(true) - pace.property_searching = true - - last = RealTime() + 0.2 + if not input.IsKeyDown(KEY_LSHIFT)then + pace.properties.search:SetVisible(true) + pace.properties.search:RequestFocus() + pace.properties.search:SetEnabled(true) + pace.property_searching = true + + last = RealTime() + 0.2 + else + pace.OpenTreeSearch() + end end end @@ -736,9 +768,6 @@ function pace.CheckShortcuts() if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end if gui.IsConsoleVisible() then return end if not pace.Editor or not pace.Editor:IsValid() then return end - - - local master_bool = true if skip and not no_input_override then return end @@ -762,52 +791,6 @@ function pace.CheckShortcuts() end end - if master_bool then return end - - - if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_E) then - pace.Call("ToggleFocus", true) - last = RealTime() + 0.2 - end - - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_E) then - pace.Call("ToggleFocus") - last = RealTime() + 0.2 - end - - - if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then - RunConsoleCommand("pac_restart") - end - - -- Only if the editor is in the foreground - if pace.IsFocused() then - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_S) then - pace.Call("ShortcutSave") - last = RealTime() + 0.2 - end - - -- CTRL + (W)ear? - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_N) then - pace.Call("ShortcutWear") - last = RealTime() + 0.2 - end - - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_T) then - pace.SetTPose(not pace.GetTPose()) - last = RealTime() + 0.2 - end - - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_F) then - pace.properties.search:SetVisible(true) - pace.properties.search:RequestFocus() - pace.properties.search:SetEnabled(true) - pace.property_searching = true - - last = RealTime() + 0.2 - end - - end end pac.AddHook("Think", "pace_shortcuts", pace.CheckShortcuts) diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index bd35dcb3f..3bc04a55f 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -18,8 +18,8 @@ pace.camera_moveleft_bind = CreateClientConVar("pac_editor_camera_moveleft_bind" pace.camera_moveright_bind = CreateClientConVar("pac_editor_camera_moveright_bind", "d", true) pace.camera_up_bind = CreateClientConVar("pac_editor_camera_up_bind", "space", true) pace.camera_down_bind = CreateClientConVar("pac_editor_camera_down_bind", "", true) -pace.camera_slow_bind = CreateClientConVar("pac_editor_camera_slow_bind", "lctrl", true) -pace.camera_speed_bind = CreateClientConVar("pac_editor_camera_speed_bind", "lshift", true) +pace.camera_slow_bind = CreateClientConVar("pac_editor_camera_slow_bind", "ctrl", true) +pace.camera_speed_bind = CreateClientConVar("pac_editor_camera_speed_bind", "shift", true) pace.camera_movement_binds = { ["forward"] = pace.camera_forward_bind, diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 77d1fd6fe..369c1242e 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -45,6 +45,10 @@ local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_ne local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") +local enforce_distance = CreateConVar("pac_sv_combat_distance_enforced", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enforce a limit on how far a pac combat action can originate.\nIf set to a distance, it will prevent actions that are too far from the acting player.\n0 to disable.") +local ENFORCE_DISTANCE_SQR = math.pow(enforce_distance:GetInt(),2) +cvars.AddChangeCallback("pac_sv_combat_distance_enforced", function() ENFORCE_DISTANCE_SQR = math.pow(enforce_distance:GetInt(),2) end) + local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") @@ -637,6 +641,7 @@ if SERVER then end local function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply) + local base_damage = tbl.Damage local ent_count = 0 local ply_count = 0 local ply_prog_count = 0 @@ -772,6 +777,9 @@ if SERVER then --final action to apply the DamageInfo local function DoDamage(ent) + --add the max hp-scaled damage calculated with this entity's max health + tbl.Damage = base_damage + tbl.MaxHpScaling * ent:GetMaxHealth() + dmg_info:SetDamage(tbl.Damage) --we'll need to find out whether the damage will crack open a player's extra bars local de_facto_dmg = GetPredictedHPBarDamage(ent, tbl.Damage) @@ -882,6 +890,7 @@ if SERVER then --look through each entity for _,ent in pairs(ents_hits) do + local canhit = DMGAllowed(ent) local oldhp = ent:Health() if canhit then @@ -1023,6 +1032,7 @@ if SERVER then local dir2 = ent_center - tbl.Locus_pos--locus local dist_multiplier = 1 + local damping_dist_mult = 1 local up_mult = 1 local distance = (ent_center - pos):Length() local height_delta = pos.z + tbl.LevitationHeight - ent_center.z @@ -1097,6 +1107,16 @@ if SERVER then if tbl.ReverseFalloff then dist_multiplier = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) end + + if tbl.DampingFalloff then + damping_dist_mult = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + if tbl.DampingReverseFalloff then + damping_dist_mult = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + damping_dist_mult = damping_dist_mult + local final_damping = 1 - (tbl.Damping * damping_dist_mult) + if tbl.Levitation then addvel.z = addvel.z * up_mult end @@ -1108,26 +1128,26 @@ if SERVER then if (ent:IsPlayer() and tbl.Players) or (ent == ply and tbl.AffectSelf) then if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then - phys_ent:SetVelocity(oldvel * (-tbl.Damping) + addvel) - ent:SetVelocity(oldvel * (-tbl.Damping) + addvel) + phys_ent:SetVelocity(oldvel * (-final_damping) + addvel) + ent:SetVelocity(oldvel * (-final_damping) + addvel) end elseif (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) and tbl.PhysicsProps then if not IsPropProtected(ent, ply) and not (global_combat_prop_protection:GetBool() and unconsenting_owner) then if IsValid(phys_ent) then ent:PhysWake() - ent:SetVelocity(tbl.Damping * oldvel + addvel) + ent:SetVelocity(final_damping * oldvel + addvel) if islocaltorque then - phys_ent:SetAngleVelocity(tbl.Damping * phys_ent:GetAngleVelocity()) + phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) phys_ent:AddAngleVelocity(add_angvel) else - phys_ent:SetAngleVelocity(tbl.Damping * phys_ent:GetAngleVelocity()) + phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) add_angvel = phys_ent:WorldToLocalVector( add_angvel ) phys_ent:ApplyTorqueCenter(add_angvel) end ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces - phys_ent:SetVelocity((oldvel * tbl.Damping) + addvel) + phys_ent:SetVelocity((oldvel * final_damping) + addvel) end end elseif (ent:IsNPC() or string.find(ent:GetClass(), "npc") ~= nil) and tbl.NPC then @@ -1137,11 +1157,11 @@ if SERVER then local clamp_vec = vec:GetNormalized()*500 ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer - else ent:SetVelocity((oldvel * tbl.Damping) + addvel) end + else ent:SetVelocity((oldvel * final_damping) + addvel) end end elseif tbl.PointEntities then if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then - phys_ent:SetVelocity(tbl.Damping * oldvel + addvel) + phys_ent:SetVelocity(final_damping * oldvel + addvel) end end hook.Run("PhysicsUpdate", ent) @@ -1329,6 +1349,7 @@ if SERVER then local tbl = {} local pos = net.ReadVector() + if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then return end local ang = net.ReadAngle() tbl.Locus_pos = net.ReadVector() local on = net.ReadBool() @@ -1360,13 +1381,15 @@ if SERVER then tbl.AddedVectorForce1 = net.ReadVector() tbl.Torque1 = net.ReadVector() - tbl.Damping = 1 - net.ReadUInt(10)/1000 + tbl.Damping = net.ReadUInt(10)/1000 tbl.LevitationHeight = net.ReadInt(14) tbl.Continuous = net.ReadBool() tbl.AccountMass = net.ReadBool() tbl.Falloff = net.ReadBool() tbl.ReverseFalloff = net.ReadBool() + tbl.DampingFalloff = net.ReadBool() + tbl.DampingReverseFalloff = net.ReadBool() tbl.Levitation = net.ReadBool() tbl.AffectSelf = net.ReadBool() tbl.Players = net.ReadBool() @@ -1410,7 +1433,6 @@ if SERVER then --print("outdated force") end end - end) @@ -1446,10 +1468,12 @@ if SERVER then end local pos = net.ReadVector() + if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then return end local ang = net.ReadAngle() local tbl = {} tbl.Damage = net.ReadUInt(28) + tbl.MaxHpScaling = net.ReadUInt(10) / 1000 tbl.Length = net.ReadInt(16) tbl.Radius = net.ReadInt(16) @@ -1684,6 +1708,18 @@ if SERVER then local alt_ang = net.ReadAngle() local ask_drawviewer = net.ReadBool() + if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then + ApplyLockState(targ_ent, false) + if ply.grabbed_ents then + net.Start("pac_request_lock_break") + net.WriteEntity(targ_ent) + net.WriteString(lockpart_UID) + net.WriteString(breakup_condition) + net.Send(ply) + end + return + end + local prop_protected, reason = IsPropProtected(targ_ent, ply) local unconsenting_owner = targ_ent:GetCreator() ~= ply and (grab_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) @@ -1873,12 +1909,17 @@ if SERVER then if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.2 end --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check for ent,bool in pairs(active_grabbed_ents) do - if ent.grabbed_by or bool then + if not IsValid(ent) then + active_grabbed_ents[ent] = nil + elseif (ent.grabbed_by or bool) then if ent.grabbed_by_time + 0.5 < CurTime() then --restore the movetype local grabber = ent.grabbed_by ent.grabbed_by_uid = nil ent.grabbed_by = nil - grabber.grabbed_ents[ent] = false + if grabber then + grabber.grabbed_ents[ent] = false + end + ApplyLockState(ent, false) active_grabbed_ents[ent] = nil end From 86a4848f31c9e70d8258fca451db0897a74e8e3a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 14:20:06 -0400 Subject: [PATCH 052/300] reorder type/validity checks before string length --- lua/pac3/core/client/part_pool.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index b1c928763..7daa288ea 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -503,8 +503,9 @@ function pac.GetPartFromUniqueID(owner_id, id) end function pac.FindPartByPartialUniqueID(owner_id, crumb) - if #crumb <= 3 then return NULL end if not crumb then return NULL end + if not isstring(crumb) then return NULL end + if #crumb <= 3 then return NULL end local closest_match local length_of_closest_match = 0 if uid_parts[owner_id] then From e3d9354385d7031a5d30c497b90971a8545b251d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 14:48:11 -0400 Subject: [PATCH 053/300] load changes for the combat branch see the readme on my repository no guarantee that everything will work separately, given that there's still 2 more major parts to my update. things are related via the editor, etc. --- lua/pac3/core/client/part_pool.lua | 31 + lua/pac3/core/client/parts/damage_zone.lua | 1035 +++++++ lua/pac3/core/client/parts/event.lua | 2453 ++++++++++++++++- lua/pac3/core/client/parts/force.lua | 308 +++ .../core/client/parts/health_modifier.lua | 202 ++ lua/pac3/core/client/parts/hitscan.lua | 252 ++ lua/pac3/core/client/parts/lock.lua | 523 ++++ lua/pac3/core/client/parts/movement.lua | 33 +- lua/pac3/core/client/parts/projectile.lua | 445 ++- lua/pac3/core/client/parts/proxy.lua | 84 +- lua/pac3/core/shared/movement.lua | 138 +- lua/pac3/editor/server/bans.lua | 40 + lua/pac3/editor/server/combat_bans.lua | 103 + lua/pac3/editor/server/init.lua | 17 +- .../editor/server/pac_settings_manager.lua | 13 + lua/pac3/extra/shared/init.lua | 1 + lua/pac3/extra/shared/net_combat.lua | 2343 ++++++++++++++++ lua/pac3/extra/shared/projectiles.lua | 233 +- 18 files changed, 8025 insertions(+), 229 deletions(-) create mode 100644 lua/pac3/core/client/parts/damage_zone.lua create mode 100644 lua/pac3/core/client/parts/force.lua create mode 100644 lua/pac3/core/client/parts/health_modifier.lua create mode 100644 lua/pac3/core/client/parts/hitscan.lua create mode 100644 lua/pac3/core/client/parts/lock.lua create mode 100644 lua/pac3/editor/server/combat_bans.lua create mode 100644 lua/pac3/editor/server/pac_settings_manager.lua create mode 100644 lua/pac3/extra/shared/net_combat.lua diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 8bf2da5a9..7daa288ea 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -44,6 +44,10 @@ local ent_parts = _G.pac_local_parts or {} local all_parts = _G.pac_all_parts or {} local uid_parts = _G.pac_uid_parts or {} +function pac.getentparts() return ent_parts or _G.pac_ent_parts or {} end +function pac.getallparts() return all_parts or _G.pac_all_parts or {} end +function pac.getuidparts() return uid_parts or _G.pac_uid_parts or {} end + if game.SinglePlayer() or (player.GetCount() == 1 and LocalPlayer():IsSuperAdmin()) then _G.pac_local_parts = ent_parts _G.pac_all_parts = all_parts @@ -498,6 +502,33 @@ function pac.GetPartFromUniqueID(owner_id, id) return uid_parts[owner_id] and uid_parts[owner_id][id] or NULL end +function pac.FindPartByPartialUniqueID(owner_id, crumb) + if not crumb then return NULL end + if not isstring(crumb) then return NULL end + if #crumb <= 3 then return NULL end + local closest_match + local length_of_closest_match = 0 + if uid_parts[owner_id] then + if uid_parts[owner_id][crumb] then + return uid_parts[owner_id][crumb] + end + + for _, part in pairs(uid_parts[owner_id]) do + local start_i,end_i = string.find(part.UniqueID, crumb) + if start_i or end_i then + closest_match = part + if length_of_closest_match < end_i - start_i + 1 then + closest_match = part + length_of_closest_match = end_i - start_i + 1 + end + + end + end + + end + return closest_match or NULL +end + function pac.FindPartByName(owner_id, str, exclude) if uid_parts[owner_id] then if uid_parts[owner_id][str] then diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua new file mode 100644 index 000000000..d7888c32f --- /dev/null +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -0,0 +1,1035 @@ +local BUILDER, PART = pac.PartTemplate("base_movable") + +--ultrakill parryables: club, slash, buckshot + +PART.ClassName = "damage_zone" +PART.Group = "advanced" +PART.Icon = "icon16/package.png" + +local renderhooks = { + "PostDraw2DSkyBox", + "PostDrawOpaqueRenderables", + "PostDrawSkyBox", + "PostDrawTranslucentRenderables", + "PostDrawViewModel", + "PostPlayerDraw", + "PreDrawEffects", + "PreDrawHalos", + "PreDrawOpaqueRenderables", + "PreDrawSkyBox", + "PreDrawTranslucentRenderables", + "PreDrawViewModel" +} + + +BUILDER:StartStorableVars() + :SetPropertyGroup("Targets") + :GetSet("AffectSelf",false) + :GetSet("Players",true) + :GetSet("NPC",true) + :GetSet("PointEntities",true, {description = "Other source engine entities such as item_item_crate and prop_physics"}) + :SetPropertyGroup("Shape and Sampling") + :GetSet("Radius", 20, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) + :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) + :GetSet("HitboxMode", "Box", {enums = { + ["Box"] = "Box", + ["Cube"] = "Cube", + ["Sphere"] = "Sphere", + ["Cylinder (Raycasts Only)"] = "Cylinder", + ["Cylinder (Hybrid)"] = "CylinderHybrid", + ["Cylinder (From Spheres)"] = "CylinderSpheres", + ["Cone (Raycasts Only)"] = "Cone", + ["Cone (Hybrid)"] = "ConeHybrid", + ["Cone (From Spheres)"] = "ConeSpheres", + ["Ray"] = "Ray" + }}) + :GetSet("Detail", 20, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32,31)) end}) + :GetSet("ExtraSteps",0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-8,7)) end}) + :GetSet("RadialRandomize", 1, {editor_onchange = function(self,num) return math.Clamp(num,-8,7) end}) + :GetSet("PhaseRandomize", 1, {editor_onchange = function(self,num) return math.Clamp(num,-8,7) end}) + :SetPropertyGroup("Falloff") + :GetSet("DamageFalloff", false) + :GetSet("DamageFalloffPower", 1, {editor_onchange = function(self,num) return math.Clamp(num,-64,63) end}) + :SetPropertyGroup("Preview Rendering") + :GetSet("Preview", false) + :GetSet("RenderingHook", "PostDrawOpaqueRenderables", {enums = { + ["PostDraw2DSkyBox"] = "PostDraw2DSkyBox", + ["PostDrawOpaqueRenderables"] = "PostDrawOpaqueRenderables", + ["PostDrawSkyBox"] = "PostDrawSkyBox", + ["PostDrawTranslucentRenderables"] = "PostDrawTranslucentRenderables", + ["PostDrawViewModel"] = "PostDrawViewModel", + ["PostPlayerDraw"] = "PostPlayerDraw", + ["PreDrawEffects"] = "PreDrawEffects", + ["PreDrawHalos"] = "PreDrawHalos", + ["PreDrawOpaqueRenderables"] = "PreDrawOpaqueRenderables", + ["PreDrawSkyBox"] = "PreDrawSkyBox", + ["PreDrawTranslucentRenderables"] = "PreDrawTranslucentRenderables", + ["PreDrawViewModel"] = "PreDrawViewModel" + }}) + :SetPropertyGroup("DamageInfo") + :GetSet("Bullet", false, {description = "Fires a bullet on each target for the added hit decal"}) + :GetSet("Damage", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,268435455)) end}) + :GetSet("DamageType", "generic", {enums = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + }}) + :GetSet("DoNotKill",false, {description = "Will only damage to as low as critical health"}) + :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("HitOutcome") + :GetSetPart("HitSoundPart") + :GetSetPart("KillSoundPart") + :GetSetPart("HitMarkerPart") + :GetSet("HitMarkerLifetime", 1) + :GetSetPart("KillMarkerPart") + :GetSet("KillMarkerLifetime", 1) + :GetSet("AllowOverlappingHitSounds", false, {description = "If false, then when there are entities killed, do not play the hit sound part at the same time, since the kill sound takes priority"}) + :GetSet("AllowOverlappingHitMarkers", false, {description = "If false, then for entities killed, do not spawn the hit marker part, since the kill marker takes priority and we don't want an overlap"}) + :GetSet("RemoveDuplicateHitMarkers", true, {description = "If true, hit markers on an entity will be removed before creating a new one.\nBE WARNED. You still have a limited budget to create hit markers. It will be enforced."}) + :GetSet("RemoveNPCWeaponsOnKill",false) +BUILDER:EndStorableVars() + + + + + +--a budget system to prevent mass abuse of hit marker parts +function CalculateHitMarkerPrice(part) + if not part then return end + + if not part.known_hitmarker_size then part.known_hitmarker_size = 2*#util.TableToJSON(part:ToTable()) end + return part.known_hitmarker_size +end + +function HasBudget(owner, part) + if not owner.pac_dmgzone_hitmarker_budget then + owner.pac_dmgzone_hitmarker_budget = 50000 --50kB's worth of pac parts + end + + if part then --calculate based on an additional part added + --print("budget:" .. string.NiceSize(owner.pac_dmgzone_hitmarker_budget) .. ", cost: " .. string.NiceSize(CalculateHitMarkerPrice(part))) + return owner.pac_dmgzone_hitmarker_budget - CalculateHitMarkerPrice(part) > 0 + else --get result from current state + --print("budget:" .. string.NiceSize(owner.pac_dmgzone_hitmarker_budget)) + return owner.pac_dmgzone_hitmarker_budget > 0 + end +end + +function PART:LaunchAuditAndEnforceSoftBan(amount, reason) + if reason == "recursive loop" then + self.stop_until = CurTime() + 3600 + owner.stop_hit_markers_until = CurTime() + 3600 + Derma_Message("HEY! You know infinite recursive loops are super duper dangerous?") + surface.PlaySound("garrysmod/ui_return.wav") + return + end + local owner = self:GetPlayerOwner() + if owner ~= LocalPlayer() then return end + owner.stop_hit_markers_admonishment_count = owner.stop_hit_markers_admonishment_count or 1 + owner.stop_hit_markers_admonishment_message_up = false + local str_admonishment = "WARNING.\n" + str_admonishment = str_admonishment .. "One of your hit marker parts is way too big. It went ".. string.NiceSize(amount) .. " overbudget at ONCE.\n" + if self.HitBoxMode ~= "Ray" then + if self.Radius > 300 or self.Length > 300 then + str_admonishment = str_admonishment .. "Your damage zone is oversized too. Are you purposefully trying to target large numbers of targets?\n" + end + end + str_admonishment = str_admonishment .. owner.stop_hit_markers_admonishment_count .. " warnings so far\n" + if owner.stop_hit_markers_admonishment_count > 5 then + self.stop_until = CurTime() + 2 + owner.stop_hit_markers_until = CurTime() + 180 --that's rough but necessary + str_admonishment = str_admonishment .. "FIVE TIMES REPEAT OFFENDER. ENJOY YOUR BAN.\n" + end + + self:SetWarning("One of your hit marker parts is way too big. It went ".. string.NiceSize(amount) .. " overbudget at ONCE.") + timer.Simple(0.5, function() --don't open duplicate windows + if not owner.stop_hit_markers_admonishment_message_up then + surface.PlaySound("garrysmod/ui_return.wav") + Derma_Message(str_admonishment) + self:SetError(str_admonishment.."This part will be limited for 3 minutes") + owner.stop_hit_markers_admonishment_message_up = true + owner.stop_hit_markers_admonishment_count = owner.stop_hit_markers_admonishment_count + 1 + print(str_admonishment) + end + end) + +end + +function PART:ClearBudgetAdmonishmentWarning() + self:SetError() self:SetWarning() + owner = self:GetPlayerOwner() + owner.stop_hit_markers_admonishment_message_up = false + owner.stop_hit_markers_until = 0 +end + +local global_hitmarker_CSEnt_seed = 0 + +local spawn_queue = {} +local tick = 0 + +--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. + +--next solution: +--a table of 20 hit part slots to unhide instead of creating parts every time +--each player commands a table of hitmarker slots +--each slot has an entry for a hitpart which will be like a pigeon-hole for clientside hitmarker ents to share +--hitmarker removal will free up the slot +--[[ + owner.hitparts[free] = { + active = true, + specimen_part = FindOrCreateFloatingPart(ent, part_uid), + hitmarker_id = ent_id, + template_uid = part_uid + } +]] + +--add : go up until we find a free spot, register it in the table until the marker is removed and the entry is marked as inactive +--remove: go up until we find the spot with the same ent id and part uid + + +--hook.Add("Tick", "pac_spawn_hit") + +local part_setup_runtimes = 0 + +--the floating part pool is player-owned +--uid-indexed for every individual part instance +--each entry is a table +--[[ + { + active + template_uid --to identify from which part it's derived + hitmarker_id --to identify what entity it's attached to + + } +]] +--[[ + owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} + owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} +]] + +function PART:FindOrCreateFloatingPart(owner, ent, part_uid, id) + owner.hitmarker_partpool = owner.hitmarker_partpool or {} + for spec_uid,tbl in pairs(owner.hitmarker_partpool) do + if tbl.template_uid == part_uid then + if not tbl.active then + return pac.GetPartFromUniqueID(pac.Hash(owner), spec_uid) --whoowee we found an already existing part + end + end + end + --what if we don't! + local tbl = pac.GetPartFromUniqueID(pac.Hash(owner), part_uid):ToTable() + local group = pac.CreatePart("group", self:GetPlayerOwner()) --print("\tcreated a group for " .. id) + group:SetShowInEditor(false) + + local part = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl)) + group:AddChild(part) + + group:CallRecursive("Think") + owner.hitmarker_partpool[group.UniqueID] = {active = true, hitmarker_id = id, template_uid = part_uid, group_part_data = group} + + return group, owner.hitmarker_partpool[group.UniqueID] + +end + +local function FreeSpotInStack(owner) + owner.hitparts = owner.hitparts or {} + for i=1,20,1 do + if owner.hitparts[i] then + if not owner.hitparts[i].active then + return i + end + else + return i + end + end + return nil +end + +--[[ + owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} + owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} +]] + +local function MatchInStack(owner, ent, uid, id) + owner.hitparts = owner.hitparts or {} + for i=1,20,1 do + if owner.hitparts[i] then + if owner.hitparts[i].template_uid == ent.template_uid and owner.hitparts[i].hitmarker_id == ent.marker_id then + return i + end + --match: entry's template uid is the same as entity's template uid + --if there's more, still match entry's specimen ID with specimen ID + end + end + + return nil +end + +local function UIDMatchInStackForExistingPart(owner, ent, part_uid, ent_id) + owner.hitparts = owner.hitparts or {} + for i=1,20,1 do + if owner.hitparts[i] then + --print(i, "match compare:", owner.hitparts[i].active, owner.hitparts[i].specimen_part, owner.hitparts[i].hitmarker_id, owner.hitparts[i].template_uid == part_uid) + if owner.hitparts[i].template_uid == part_uid then + if owner.hitmarker_partpool then + for spec_uid,tbl in pairs(owner.hitmarker_partpool) do + if tbl.template_uid == part_uid then + if not tbl.active then + return tbl.group_part_data + end + end + end + end + end + end + end + + return nil +end + +--[[ + owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} + owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} +]] +function PART:AddHitMarkerToStack(owner, ent, part_uid, ent_id) + owner.hitparts = owner.hitparts or {} + local free = FreeSpotInStack(owner) + local returned_part = nil + local existingpart = UIDMatchInStackForExistingPart(owner, ent, part_uid, ent_id) + returned_part = existingpart + + if free and not existingpart then + local group, tbl = self:FindOrCreateFloatingPart(owner, ent, part_uid, ent_id) + owner.hitparts[free] = {active = true, specimen_part = group, hitmarker_id = ent_id, template_uid = part_uid} + returned_part = owner.hitparts[free].specimen_part + else + owner.hitparts[free] = {active = true, specimen_part = returned_part, hitmarker_id = ent_id, template_uid = part_uid} + end + + return returned_part +end + +local function RemoveHitMarker(owner, ent, uid, id) + owner.hitparts = owner.hitparts or {} + + local match = MatchInStack(owner, ent, uid, id) + if match then + if owner.hitparts[match] then + owner.hitparts[match].active = false + end + end + if owner.hitmarker_partpool then + for spec_uid,tbl in pairs(owner.hitmarker_partpool) do + if tbl.hitmarker_id == id then + tbl.active = false + tbl.group_part_data:SetHide(true) + tbl.group_part_data:SetShowInEditor(false) + tbl.group_part_data:SetOwner(owner) + --print(tbl.group_part_data, "dormant") + end + end + end + --SafeRemoveEntity(ent) +end + +--[[ + owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} + owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} +]] +function PART:AssignFloatingPartToEntity(part, owner, ent, parent_ent, template_uid, marker_id) + if not IsValid(part) then return false end + + ent.pac_draw_distance = 0 + + local group = part + local part2 = group:GetChildrenList()[1] + + group:CallRecursive("Think") + + owner.hitmarker_partpool[group.UniqueID] = {active = true, hitmarker_id = marker_id, template_uid = template_uid, group_part_data = group} + owner.hitparts[FreeSpotInStack(owner) or 1] = {active = true, specimen_part = group, hitmarker_id = marker_id, template_uid = template_uid} + + parent_ent.pac_dmgzone_hitmarker_ents = parent_ent.pac_dmgzone_hitmarker_ents or {} + ent.part = group + ent.parent_ent = parent_ent + ent.template_uid = template_uid + parent_ent.pac_dmgzone_hitmarker_ents[marker_id] = ent + ent.marker_id = marker_id + + group:SetShowInEditor(false) + group:SetOwner(ent) + group.Owner = ent + group:SetHide(false) + part2:SetHide(false) + group:CallRecursive("Think") + --print(group, "assigned to " .. marker_id .. " / " .. parent_ent:EntIndex()) + +end + +local function RecursedHitmarker(part) + if part.HitMarkerPart == part or part.KillMarkerPart == part then + return true + end + if IsValid(part.HitMarkerPart) then + for i,child in pairs(part.HitMarkerPart:GetChildrenList()) do + if child.ClassName == "damage_zone" then + if child.HitMarkerPart == part or child.KillMarkerPart == part then + return true + end + end + end + end + if IsValid(part.KillMarkerPart) then + for i,child in pairs(part.KillMarkerPart:GetChildrenList()) do + if child.ClassName == "damage_zone" then + if child.HitMarkerPart == part or child.KillMarkerPart == part then + return true + end + end + end + end + +end + + +--NOT THE ACTUAL DAMAGE TYPES. UNIQUE IDS TO COMPRESS NET MESSAGES +local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + fire = 31, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 32, + dissolve_heavy_electrical = 33, + dissolve_light_electrical = 34, + dissolve_core_effect = 35, + + heal = 36, + armor = 37, +} + +local hitbox_ids = { + ["Box"] = 1, + ["Cube"] = 2, + ["Sphere"] = 3, + ["Cylinder"] = 4, + ["CylinderHybrid"] = 5, + ["CylinderSpheres"] = 6, + ["Cone"] = 7, + ["ConeHybrid"] = 8, + ["ConeSpheres"] = 9, + ["Ray"] = 10 +} + +--more compressed net message +function PART:SendNetMessage() + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + if not GetConVar('pac_sv_damage_zone'):GetBool() then return end + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then return end + end + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") end + + if GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end + + net.Start("pac_request_zone_damage", true) + + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + net.WriteUInt(self.Damage, 28) + net.WriteUInt(self.MaxHpScaling*1000,10) + net.WriteInt(self.Length, 16) + net.WriteInt(self.Radius, 16) + net.WriteBool(self.AffectSelf) + net.WriteBool(self.NPC) + net.WriteBool(self.Players) + net.WriteBool(self.PointEntities) + net.WriteUInt(hitbox_ids[self.HitboxMode] or 1,5) + net.WriteUInt(damage_ids[self.DamageType] or 0,7) + net.WriteInt(self.Detail,6) + net.WriteInt(self.ExtraSteps,4) + net.WriteInt(math.floor(math.Clamp(8*self.RadialRandomize,-64, 63)), 7) + net.WriteInt(math.floor(math.Clamp(8*self.PhaseRandomize,-64, 63)), 7) + net.WriteBool(self.DamageFalloff) + net.WriteInt(math.floor(math.Clamp(8*self.DamageFalloffPower,-512, 511)), 12) + net.WriteBool(self.Bullet) + net.WriteBool(self.DoNotKill) + net.WriteUInt(self.CriticalHealth, 16) + net.WriteBool(self.RemoveNPCWeaponsOnKill) + net.SendToServer() +end + +function PART:OnShow() + + if self.Preview then + self:PreviewHitbox() + end + self.stop_until = self.stop_until or 0 + 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: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 + if self.Preview then + self:PreviewHitbox() + end + + self:SendNetMessage() + end) + elseif (self.validTime > SysTime()) then + return + else + self:SendNetMessage() + end + + net.Receive("pac_hit_results", function() + + local hit = net.ReadBool() + local kill = net.ReadBool() + local highest_dmg = net.ReadFloat() + local ents_hit = net.ReadTable() + local ents_kill = net.ReadTable() + part_setup_runtimes = 0 + + if RecursedHitmarker(self) then + self:LaunchAuditAndEnforceSoftBan(nil,"recursive loop") + end + + local pos = self:GetWorldPosition() + local owner = self:GetPlayerOwner() + + self.lag_risk = table.Count(ents_hit) > 15 + + local function ValidSound(part) + if part ~= nil then + if part.ClassName == "sound" or part.ClassName == "sound2" then + return true + end + end + return false + end + --grabbed the function from projectile.lua + --here, we spawn a static hitmarker and the max delay is 8 seconds + local function spawn(part, pos, ang, parent_ent, duration, owner) + if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker + --what if people employ a more roundabout method? CRACKDOWN! + + if not owner.hitparts then owner.hitparts = {} end + + if owner.stop_hit_markers_until then + if owner.stop_hit_markers_until > CurTime() then return end + end + if self.lag_risk and math.random() > 0.5 then return end + if not self:IsValid() then return end + if not part:IsValid() then return end + + + local start = SysTime() + local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") + if not ent:IsValid() then return end + ent:SetNoDraw(true) + ent:SetOwner(self:GetPlayerOwner(true)) + ent:SetPos(pos) + ent:SetAngles(ang) + global_hitmarker_CSEnt_seed = global_hitmarker_CSEnt_seed + 1 + local csent_id = global_hitmarker_CSEnt_seed + + --the spawn order needs to decide whether it can or can't create an ent or part + + local flush = self.RemoveDuplicateHitMarkers + if flush then + --go through the entity and remove the clientside hitmarkers entities + if parent_ent.pac_dmgzone_hitmarker_ents then + for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do + if IsValid(ent2) then + if ent2.part:IsValid() then + --owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget + CalculateHitMarkerPrice(part) + ent2.part:SetHide(true) + end + --RemoveHitMarker(owner, ent, part.UniqueID, id) + end + end + end + end + + if FreeSpotInStack(owner) then + if part:IsValid() then --self:AttachToEntity(part, ent, parent_ent, global_hitmarker_CSEnt_seed) + local newpart + local bool = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) + + newpart = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) or self:AddHitMarkerToStack(owner, ent, part.UniqueID, csent_id) + + self:AssignFloatingPartToEntity(newpart, owner, ent, parent_ent, part.UniqueID, csent_id) + + MsgC(bool and Color(0,255,0) or Color(0,200,255), bool and "existing" or "created", " : ", newpart, "\n") + timer.Simple(math.Clamp(duration, 0, 8), function() + if ent:IsValid() then + if parent_ent.pac_dmgzone_hitmarker_ents then + for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do + if IsValid(ent2) then + RemoveHitMarker(owner, ent2, part.UniqueID, id) + --SafeRemoveEntity(ent2) + end + end + end + --[[timer.Simple(0.5, function() + RemoveHitMarker(owner, ent, part.UniqueID, csent_id) + SafeRemoveEntity(ent) + end)]] + end + end) + end + end + + local creation_delta = SysTime() - start + + return creation_delta + end + + if IsValid(self.HitMarkerPart) then + --(self.HitMarkerPart, "price is", string.NiceSize(CalculateHitMarkerPrice(self.HitMarkerPart))) + end + + if hit then + self.dmgzone_hit_done = CurTime() + --try not to play both sounds at once + if ValidSound(self.HitSoundPart) then + --if can overlap, always play + if self.AllowOverlappingHitSounds then + self.HitSoundPart:PlaySound() + --if cannot overlap, only play if there's only one entity or if we didn't kill + elseif (table.Count(ents_kill) == 1) or not (kill and ValidSound(self.KillSoundPart)) then + self.HitSoundPart:PlaySound() + end + end + for ent,_ in pairs(ents_hit) do + if IsValid(ent) then + local ang = (ent:GetPos() - pos):Angle() + if ents_kill[ent] then + if self.AllowOverlappingHitMarkers then + part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) + end + else + part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) + end + end + end + end + if kill then + self.dmgzone_kill_done = CurTime() + if ValidSound(self.KillSoundPart) then + self.KillSoundPart:PlaySound() + end + + for ent,_ in pairs(ents_kill) do + if IsValid(ent) then + local ang = (ent:GetPos() - pos):Angle() + part_setup_runtimes = part_setup_runtimes + (spawn(self.KillMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.KillMarkerLifetime, owner) or 0) + end + end + end + if IsValid(self.HitMarkerPart) then + --print("runtimes were " .. string.FormattedTime( part_setup_runtimes).ms .. "ms.\n\t" .. 1000*part_setup_runtimes .. " impact.\n\t" .. table.Count(self.HitMarkerPart:GetChildrenList()) .. " children, " .. 1000*part_setup_runtimes / table.Count(self.HitMarkerPart:GetChildrenList()) .. " impact per children") + end + if owner.hitparts then + for i,v in ipairs(owner.hitparts) do + --print(i,v,v.active,v.specimen_part,v.hitmarker_id,v.template_uid) + end + self:SetInfo(table.Count(owner.hitparts) .. " hitmarkers in slot") + end + + end) + +end + +concommand.Add("pac_cleanup_damagezone_hitmarks", function() + if LocalPlayer().hitparts then + for i,v in pairs(LocalPlayer().hitparts) do + v.specimen_part:Remove() + end + end + + LocalPlayer().hitmarker_partpool = nil + LocalPlayer().hitparts = nil +end) + + +function PART:OnHide() + hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox"..self.UniqueID) + end +end + +function PART:OnRemove() + hook.Remove(self.RenderingHook, "pace_draw_hitbox") + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox") + end +end + +local previousRenderingHook + +function PART:PreviewHitbox() + + if previousRenderingHook ~= self.RenderingHook then + for _,v in pairs(renderhooks) do + hook.Remove(v, "pace_draw_hitbox"..self.UniqueID) + end + previousRenderingHook = self.RenderingHook + end + + if not self.Preview then return end + + hook.Add(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID, function() + if not self.Preview then hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end + self:GetWorldPosition() + if self.HitboxMode == "Box" then + local mins = Vector(-self.Radius, -self.Radius, -self.Length) + local maxs = Vector(self.Radius, self.Radius, self.Length) + render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Cube" then + --mat:Rotate(Angle(SysTime()*100,0,0)) + local mins = Vector(-self.Radius, -self.Radius, -self.Radius) + local maxs = Vector(self.Radius, self.Radius, self.Radius) + render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Sphere" then + render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Cylinder" or self.HitboxMode == "CylinderHybrid" then + local obj = Mesh() + self:BuildCylinder(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if LocalPlayer() == self:GetPlayerOwner() then + if self.Radius ~= 0 then + local sides = self.Detail + if self.Detail < 1 then sides = 1 end + + local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + end + steps = math.max(steps + math.abs(self.ExtraSteps),1) + + --print("steps",steps, "total casts will be "..steps*self.Detail) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + y = self:GetWorldAngles():Up() *math.sin(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + local startpos = self:GetWorldPosition() + x + y + local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y + render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false ) + end + end + if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then + --fast sphere check on the wide end + if self.Length/self.Radius >= 2 then + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - self.Radius), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Radius), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if self.Radius ~= 0 then + local counter = 0 + for i=math.floor(self.Length / self.Radius) - 1,1,-1 do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Radius*i), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if counter == 100 then break end + counter = counter + 1 + end + end + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + end + elseif self.Radius == 0 then render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) end + end + elseif self.HitboxMode == "CylinderSpheres" then + local obj = Mesh() + self:BuildCylinder(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Length ~= 0 and self.Radius ~= 0 then + local counter = 0 + --render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + for i=0,1,1/(math.abs(self.Length/self.Radius)) do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if counter == 200 then break end + counter = counter + 1 + end + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Cone" or self.HitboxMode == "ConeHybrid" then + local obj = Mesh() + self:BuildCone(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if LocalPlayer() == self:GetPlayerOwner() then + if self.Radius ~= 0 then + local sides = self.Detail + if self.Detail < 1 then sides = 1 end + local startpos = self:GetWorldPosition() + local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) + end + steps = math.max(steps + math.abs(self.ExtraSteps),1) + + --print("steps",steps, "total casts will be "..steps*self.Detail) + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + local ray_thickness = math.Clamp(0.5*math.log(self.Radius) + 0.05*self.Radius,0,10)*(1.5 - 0.7*ringnumber) + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + y = self:GetWorldAngles():Up() *math.sin(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize) + local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y + render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false ) + end + --[[render.DrawWireframeBox(self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + self:GetWorldAngles():Right() * self.Radius * ringnumber, Angle(0,0,0), + Vector(ray_thickness,ray_thickness,ray_thickness), + Vector(-ray_thickness,-ray_thickness,-ray_thickness), + Color(255,255,255))]] + end + if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then + --fast sphere check on the wide end + local radius_multiplier = math.atan(math.abs(self.Length/self.Radius)) / (1.5 + 0.1*math.sqrt(self.Length/self.Radius)) + if self.Length/self.Radius > 0.5 then + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - self.Radius * radius_multiplier), self.Radius * radius_multiplier, 10, 10, Color( 255, 255, 255 ) ) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + end + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + end + elseif self.HitboxMode == "ConeSpheres" then + local obj = Mesh() + self:BuildCone(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Radius ~= 0 then + local steps + steps = math.Clamp(4*math.ceil(self.Length / (self.Radius or 1)),1,50) + for i = 1,0,-1/steps do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + + steps = math.Clamp(math.ceil(self.Length / (self.Radius or 1)),1,4) + for i = 0,1/8,1/128 do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Ray" then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + end) +end + +function PART:OnThink() + if self.Preview then self:PreviewHitbox() end +end + +function PART:BuildCylinder(obj) + local sides = 30 + local circle_tris = {} + for i=1,sides,1 do + local vert1 = {pos = Vector(0, self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(0, self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + local vert3 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert4 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + --print(vert1.pos,vert3.pos,vert2.pos,vert4.pos) + --{vert1,vert2,vert3} + --{vert4,vert3,vert2} + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + + table.insert(circle_tris, vert4) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert4) + + --circle_tris[8*(i-1) + 1] = vert1 + --circle_tris[8*(i-1) + 2] = vert2 + --circle_tris[8*(i-1) + 3] = vert3 + --circle_tris[8*(i-1) + 4] = vert4 + --circle_tris[8*(i-1) + 5] = vert3 + --circle_tris[8*(i-1) + 6] = vert2 + end + obj:BuildFromTriangles( circle_tris ) +end + +function PART:BuildCone(obj) + local sides = 30 + local circle_tris = {} + local verttip = {pos = Vector(0,0,0), u = 0, v = 0 } + for i=1,sides,1 do + local vert1 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + --print(vert1.pos,vert3.pos,vert2.pos,vert4.pos) + --{vert1,vert2,vert3} + --{vert4,vert3,vert2} + table.insert(circle_tris, verttip) + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + table.insert(circle_tris, verttip) + + --circle_tris[8*(i-1) + 1] = vert1 + --circle_tris[8*(i-1) + 2] = vert2 + --circle_tris[8*(i-1) + 3] = vert3 + --circle_tris[8*(i-1) + 4] = vert4 + --circle_tris[8*(i-1) + 5] = vert3 + --circle_tris[8*(i-1) + 6] = vert2 + end + obj:BuildFromTriangles( circle_tris ) +end + +function PART:Initialize() + + 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() + 2 +end + +function PART:SetRadius(val) + self.Radius = val + local sv_dist = GetConVar("pac_sv_damage_zone_max_radius"):GetInt() + if self.Radius > sv_dist then + self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetLength(val) + self.Length = val + local sv_dist = GetConVar("pac_sv_damage_zone_max_length"):GetInt() + if self.Length > sv_dist then + self:SetInfo("Your length is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetDamage(val) + self.Damage = val + local sv_max = GetConVar("pac_sv_damage_zone_max_damage"):GetInt() + if self.Damage > sv_max then + self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 4673a301a..3548a718b 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1,3 +1,6 @@ +include("pac3/core/client/part_pool.lua") +--include("pac3/editor/client/parts.lua") + local FrameTime = FrameTime local CurTime = CurTime local NULL = NULL @@ -5,6 +8,7 @@ local Vector = Vector local util = util local SysTime = SysTime + local BUILDER, PART = pac.PartTemplate("base") PART.ClassName = "event" @@ -26,21 +30,151 @@ BUILDER:StartStorableVars() return output end}) BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end}) - BUILDER:GetSet("Arguments", "", {hidden = true}) + BUILDER:GetSet("Arguments", "", {hidden = false}) BUILDER:GetSet("Invert", true) BUILDER:GetSet("RootOwner", true) BUILDER:GetSet("AffectChildrenOnly", false) BUILDER:GetSet("ZeroEyePitch", false) - BUILDER:GetSetPart("TargetPart") + BUILDER:GetSetPart("TargetPart", {editor_friendly = "ExternalOriginPart"}) + BUILDER:GetSetPart("DestinationPart", {editor_friendly = "TargetedPart"}) BUILDER:EndStorableVars() +local registered_command_event_series = {} + +function PART:register_command_event(str,b) + local ply = self:GetPlayerOwner() + + local event = str + local flush = b + + local num = tonumber(string.sub(event, string.find(event,"[%d]+$") or 0)) or 0 + + if string.find(event,"[%d]+$") then + event = string.gsub(event,"[%d]+$","") + end + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} + + if flush then + ply.pac_command_event_sequencebases[event] = nil + return + end + + if ply.pac_command_event_sequencebases[event] and string.find(str,"[%d]+$") then + ply.pac_command_event_sequencebases[event].max = math.max(ply.pac_command_event_sequencebases[event].max,num) + else + ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = num} + end + +end + +function PART:fix_event_operator() + --check if exists + --check class + --check current operator + --PrintTable(PART.Events[self.Event]) + if PART.Events[self.Event] then + local event_type = PART.Events[self.Event].operator_type + if event_type == "number" then + if self.Operator == "find" or self.Operator == "find simple" then + self.Operator = PART.Events[self.Event].preferred_operator --which depends, it's usually above but we'll have cases where it's best to have below, or equal + self:SetInfo("The operator was automatically changed to work with this event type, which handles numbers") + end + + elseif event_type == "string" then + if self.Operator ~= "find" and self.Operator ~= "find simple" and self.Operator ~= "equal" then + self.Operator = PART.Events[self.Event].preferred_operator --find simple + self:SetInfo("The operator was automatically changed to work with this event type, which handles strings (text)") + end + elseif event_type == "mixed" then + self:SetInfo("This event is mixed, which means it might have different behaviour with numeric operators or string operators. Some of these are that way because they're using different sources of data at once (e.g. addons' weapons can use different formats for fire modes), and we want to catch the most valid uses possible to fit what the event says") + --do nothing but still warn about it being a special complex event + elseif event_type == "none" then + --do nothing + end + end +end + +function PART:AttachEditorPopup(str) + + local info_string = str or "no information available" + local verbosity = "" + if self.Event ~= "" then + if PART.Events[self.Event] then + info_string = PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory" + else + info_string = "invalid event" + end + --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + + --end + + str = info_string or str + end + self:SetupEditorPopup(info_string, true) +end + function PART:SetEvent(event) local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) + + if not self.Events[event] then --invalid event? try a command event + local command_was_found_in_cmd_part = false + for i,v in ipairs(pac.GetLocalParts()) do + if v.ClassName == "command" then + if string.find(v.String, "pac_event " .. event) then + command_was_found_in_cmd_part = true + end + end + end + if self:GetPlayerOwner().pac_command_events or command_was_found_in_cmd_part then + self:GetPlayerOwner().pac_command_events = self:GetPlayerOwner().pac_command_events or {} + if self:GetPlayerOwner().pac_command_events[event] or command_was_found_in_cmd_part or GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then + timer.Simple(0.2, function() + --now we'll use event as a command name + self:SetEvent("command") + self.pace_properties["Event"]:SetValue("command") + + self:SetArguments(event .. "@@0") + self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") + pace.PopulateProperties(self) + + end) + return + end + end + end self.Event = event self:SetWarning() + self:SetInfo() + + --foolproofing: fix the operator to match the event's type, and fix arguments as needed + self:fix_event_operator() + self:fix_args() + + pace.changed_event = self --a reference to make it refresh the popup label panel + pace.changed_event_time = CurTime() + + if self == pace.current_part and GetConVar("pac_copilot_make_popup_when_selecting_event"):GetBool() then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit + self:GetDynamicProperties(reset) + if not GetConVar("pac_editor_remember_divider_height"):GetBool() then pace.Editor.div:SetTopHeight(ScrH() - 520) end + +end + +function PART:Initialize() + if self:GetPlayerOwner() == LocalPlayer() then + timer.Simple(0.2, function() + if self.Event == "command" then + local cmd, time, hide = self:GetParsedArgumentsForObject(self.Events.command) + self:register_command_event(cmd, true) + timer.Simple(0.2, function() + self:register_command_event(cmd, false) + end) + end + end) + end + end local function get_default(typ) @@ -149,9 +283,92 @@ for k,v in pairs(_G) do end end +local grounds_enums = { + ["MAT_ANTLION"] = "65", + ["MAT_BLOODYFLESH"] = "66", + ["MAT_CONCRETE"] = "67", + ["MAT_DIRT"] = "68", + ["MAT_EGGSHELL"] = "69", + ["MAT_FLESH"] = "70", + ["MAT_GRATE"] = "71", + ["MAT_ALIENFLESH"] = "72", + ["MAT_CLIP"] = "73", + ["MAT_SNOW"] = "74", + ["MAT_PLASTIC"] = "76", + ["MAT_METAL"] = "77", + ["MAT_SAND"] = "78", + ["MAT_FOLIAGE"] = "79", + ["MAT_COMPUTER"] = "80", + ["MAT_SLOSH"] = "83", + ["MAT_TILE"] = "84", + ["MAT_GRASS"] = "85", + ["MAT_VENT"] = "86", + ["MAT_WOOD"] = "87", + ["MAT_DEFAULT"] = "88", + ["MAT_GLASS"] = "89", + ["MAT_WARPSHIELD"] = "90" +} + +local grounds_enums_reverse = { + ["65"] = "antlion", + ["66"] = "bloody flesh", + ["67"] = "concrete", + ["68"] = "dirt", + ["69"] = "egg shell", + ["70"] = "flesh", + ["71"] = "grate", + ["72"] = "alien flesh", + ["73"] = "clip", + ["74"] = "snow", + ["76"] = "plastic", + ["77"] = "metal", + ["78"] = "sand", + ["79"] = "foliage", + ["80"] = "computer", + ["83"] = "slosh", + ["84"] = "tile", + ["85"] = "grass", + ["86"] = "vent", + ["87"] = "wood", + ["88"] = "default", + ["89"] = "glass", + ["90"] = "warp shield" +} + +local animation_event_enums = { + "attack primary", + "swim", + "flinch rightleg", + "flinch leftarm", + "flinch head", + "cancel", + "attack secondary", + "flinch rightarm", + "jump", + "snap yaw", + "attack grenade", + "custom", + "cancel reload", + "reload loop", + "custom gesture sequence", + "custom sequence", + "spawn", + "doublejump", + "flinch leftleg", + "flinch chest", + "die", + "reload end", + "reload", + "custom gesture" +} + + + PART.Events = {} PART.OldEvents = { + random = { + operator_type = "number", preferred_operator = "above", arguments = {{compare = "number"}}, callback = function(self, ent, compare) return self:NumberOperator(math.random(), compare) @@ -159,6 +376,7 @@ PART.OldEvents = { }, randint = { + operator_type = "number", preferred_operator = "above", arguments = {{compare = "number"}, {min = "number"}, {max = "number"}}, callback = function(self, ent, compare, min, max) min = min or 0 @@ -169,6 +387,8 @@ PART.OldEvents = { }, random_timer = { + operator_type = "none", + tutorial_explanation = "random_timer picks a number between min and max, waits this amount of seconds,\nthen activates for the amount of seconds from holdtime.\nafter this is over, it picks a new random number and starts waiting again", arguments = {{min = "number"}, {max = "number"}, {holdtime = "number"}}, callback = function(self, ent, min, max, holdtime) @@ -203,11 +423,18 @@ PART.OldEvents = { }, timerx = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "timerx is a stopwatch that counts time since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes", arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}}, + userdata = { + {default = 0, timerx_property = "seconds"}, + {default = true, timerx_property = "reset_on_hide"} + }, nice = function(self, ent, seconds) return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?" end, callback = function(self, ent, seconds, reset_on_hide, synced_time) + local time = synced_time and CurTime() or RealTime() self.time = self.time or time @@ -217,15 +444,18 @@ PART.OldEvents = { return false end self.number = time - self.time - + return self:NumberOperator(self.number, seconds) end, }, timersys = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "like timerx, timersys is a stopwatch that counts time (it uses SysTime()) since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes", arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}}, callback = function(self, ent, seconds, reset_on_hide) + local time = SysTime() self.time = self.time or time @@ -239,6 +469,7 @@ PART.OldEvents = { }, map_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(game.GetMap(), find) @@ -246,6 +477,7 @@ PART.OldEvents = { }, fov = { + operator_type = "number", preferred_operator = "above", arguments = {{fov = "number"}}, callback = function(self, ent, fov) ent = try_viewmodel(ent) @@ -258,6 +490,7 @@ PART.OldEvents = { end, }, health_lost = { + operator_type = "number", preferred_operator = "above", arguments = {{amount = "number"}}, callback = function(self, ent, amount) @@ -299,6 +532,7 @@ PART.OldEvents = { }, holdtype = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -307,9 +541,18 @@ PART.OldEvents = { return true end end, + nice = function(self, ent, find) + local str = "holdtype ["..self.Operator.. " " .. find .. "] | " + local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL + if wep:IsValid() then + str = str .. wep:GetHoldType() + end + return str + end }, is_crouching = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.Crouching and ent:Crouching() @@ -317,6 +560,7 @@ PART.OldEvents = { }, is_typing = { + operator_type = "none", callback = function(self, ent) ent = self:GetPlayerOwner() return ent.IsTyping and ent:IsTyping() @@ -324,6 +568,7 @@ PART.OldEvents = { }, using_physgun = { + operator_type = "none", callback = function(self, ent) ent = self:GetPlayerOwner() local pac_drawphysgun_event_part = ent.pac_drawphysgun_event_part @@ -336,11 +581,42 @@ PART.OldEvents = { end, }, + is_using_entity = { + operator_type = "none", + tutorial_explanation = "For when you're picking up props, clicking buttons etc.\nAlthough not all entities will do things if you +use them, the event tries to take the class of the entity you used,\nand compares it with the one written in class", + arguments = {{class = "string"}}, + callback = function(self, ent, class) + ent = self:GetPlayerOwner() + local b = false + + if not ent:IsPlayer() then return false + elseif ent == LocalPlayer() and self.singleactivatestate then + net.Start("pac.RequestPlayerObjUsed") + net.SendToServer() + self.singleactivatestate = false + self.nextactivationrefresh = CurTime() + 0.05 + end + if ent.entity_inuse or ent.entity_inuse_classname then + if ent.entity_inuse_classname == "player_pickup" then + b = true + end + if IsValid(ent.entity_inuse) and string.find(ent.entity_inuse_classname, class, 1, true) and string.find(ent.entity_inuse:GetClass(), class, 1, true) then + b = true + end + end + + return b + end + }, + eyetrace_entity_class = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "this compares the class of the entity you point to with the one(s) written in class", arguments = {{class = "string"}}, callback = function(self, ent, find) if ent.GetEyeTrace then ent = ent:GetEyeTrace().Entity + if not IsValid(ent) then return false end if self:StringOperator(ent:GetClass(), find) then return true end @@ -349,8 +625,10 @@ PART.OldEvents = { }, owner_health = { + operator_type = "number", preferred_operator = "above", arguments = {{health = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.Health then return self:NumberOperator(ent:Health(), num) @@ -360,8 +638,10 @@ PART.OldEvents = { end, }, owner_max_health = { + operator_type = "number", preferred_operator = "above", arguments = {{health = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.GetMaxHealth then return self:NumberOperator(ent:GetMaxHealth(), num) @@ -371,6 +651,7 @@ PART.OldEvents = { end, }, owner_alive = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) if ent.Alive then @@ -380,8 +661,10 @@ PART.OldEvents = { end, }, owner_armor = { + operator_type = "number", preferred_operator = "above", arguments = {{armor = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.Armor then return self:NumberOperator(ent:Armor(), num) @@ -392,31 +675,36 @@ PART.OldEvents = { }, owner_scale_x = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, owner_scale_y = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, owner_scale_z = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, pose_parameter = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "pose parameters are values used in models for body movement and animation.\nthis event searches a pose parameter and compares its normalized (0-1 range) value with the number defined in num", arguments = {{name = "string"}, {num = "number"}}, callback = function(self, ent, name, num) ent = try_viewmodel(ent) @@ -424,7 +712,23 @@ PART.OldEvents = { end, }, + pose_parameter_true = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "pose parameters are values used in models for body movement and animation.\nthis event searches a pose parameter and compares its true (as opposed to normalized into the 0-1 range) value with number defined in num", + arguments = {{name = "string"}, {num = "number"}}, + callback = function(self, ent, name, num) + ent = try_viewmodel(ent) + if ent:IsValid() then + local min,max = ent:GetPoseParameterRange(ent:LookupPoseParameter(name)) + if not min or not max then return 0 end + local actual_value = min + (max - min)*(ent:GetPoseParameter(name)) + return self:NumberOperator(actual_value, num) + end + end, + }, + speed = { + operator_type = "number", preferred_operator = "equal", arguments = {{speed = "number"}}, callback = function(self, ent, num) ent = try_viewmodel(ent) @@ -433,6 +737,8 @@ PART.OldEvents = { }, 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", arguments = {{level = "number"}}, callback = function(self, ent, num) ent = try_viewmodel(ent) @@ -441,6 +747,7 @@ PART.OldEvents = { }, is_on_fire = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent:IsOnFire() @@ -448,17 +755,23 @@ PART.OldEvents = { }, client_spawned = { + operator_type = "number", preferred_operator = "below", + tutorial_explanation = "client_spawned supposedly activates for some time after you spawn", + arguments = {{time = "number"}}, callback = function(self, ent, time) time = time or 0.1 ent = try_viewmodel(ent) - if ent.pac_playerspawn and ent.pac_playerspawn + time > pac.RealTime then - return true + if ent.pac_playerspawn then + return self:NumberOperator(pac.RealTime, ent.pac_playerspawn + time) end + return false end, }, is_client = { + operator_type = "none", + tutorial_explanation = "is_client makes something visible only for you, or others (uninverted)", callback = function(self, ent) ent = try_viewmodel(ent) return self:GetPlayerOwner() == ent @@ -466,6 +779,7 @@ PART.OldEvents = { }, is_flashlight_on = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.FlashlightIsOn and ent:FlashlightIsOn() @@ -473,6 +787,7 @@ PART.OldEvents = { }, collide = { + operator_type = "none", callback = function(self, ent) ent.pac_event_collide_callback = ent.pac_event_collide_callback or ent:AddCallback("PhysicsCollide", function(ent, data) ent.pac_event_collision_data = data @@ -489,9 +804,15 @@ PART.OldEvents = { }, ranger = { + operator_type = "number", preferred_operator = "below", + tutorial_explanation = "ranger looks in a line to see if something is in front (red arrow) of its host's (parent) model;\ndetected things could be found before(below) or beyond(above) the distance defined in compare;\nthe event will only look as far as the distance defined in distance", arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}}, - userdata = {{editor_panel = "ranger", ranger_property = "distance"}, {editor_panel = "ranger", ranger_property = "compare"}}, + userdata = { + {default = 15, editor_panel = "ranger", ranger_property = "distance"}, + {default = 5, editor_panel = "ranger", ranger_property = "compare"} + }, callback = function(self, ent, distance, compare, npcs_and_players_only) + local parent = self:GetParentEx() if parent:IsValid() and parent.GetWorldPosition then @@ -508,7 +829,7 @@ PART.OldEvents = { if npcs_and_players_only and (not res.Entity:IsPlayer() and not res.Entity:IsNPC()) then return false end - + return self:NumberOperator(res.Fraction * distance, compare) else local classname = parent:GetNiceName() @@ -516,6 +837,100 @@ PART.OldEvents = { self:SetWarning(("ranger doesn't work on [%s] %s"):format(classname, classname ~= name and "(" .. name .. ")" or "")) end end, + nice = function(self, ent, distance, compare, npcs_and_players_only) + local str = "ranger: [" .. self.Operator .. " " .. compare .. "]" + return str + end + }, + + ground_surface = { + operator_type = "mixed", + tutorial_explanation = "ground_surface checks what ground you're standing on, and activates if it matches among the IDs written in surfaces.\nMatch multiple with ;", + arguments = {{exclude_noclip = "boolean"}, {surfaces = "string"}}, + userdata = {{}, {enums = function() + --grounds_enums = + return + { + ["MAT_ANTLION"] = "65", + ["MAT_BLOODYFLESH"] = "66", + ["MAT_CONCRETE"] = "67", + ["MAT_DIRT"] = "68", + ["MAT_EGGSHELL"] = "69", + ["MAT_FLESH"] = "70", + ["MAT_GRATE"] = "71", + ["MAT_ALIENFLESH"] = "72", + ["MAT_CLIP"] = "73", + ["MAT_SNOW"] = "74", + ["MAT_PLASTIC"] = "76", + ["MAT_METAL"] = "77", + ["MAT_SAND"] = "78", + ["MAT_FOLIAGE"] = "79", + ["MAT_COMPUTER"] = "80", + ["MAT_SLOSH"] = "83", + ["MAT_TILE"] = "84", + ["MAT_GRASS"] = "85", + ["MAT_VENT"] = "86", + ["MAT_WOOD"] = "87", + ["MAT_DEFAULT"] = "88", + ["MAT_GLASS"] = "89", + ["MAT_WARPSHIELD"] = "90" + } end}}, + nice = function(self, ent, exclude_noclip, surfaces) + local grounds_enums_reverse = { + ["65"] = "antlion", + ["66"] = "bloody flesh", + ["67"] = "concrete", + ["68"] = "dirt", + ["69"] = "egg shell", + ["70"] = "flesh", + ["71"] = "grate", + ["72"] = "alien flesh", + ["73"] = "clip", + ["74"] = "snow", + ["76"] = "plastic", + ["77"] = "metal", + ["78"] = "sand", + ["79"] = "foliage", + ["80"] = "computer", + ["83"] = "slosh", + ["84"] = "tile", + ["85"] = "grass", + ["86"] = "vent", + ["87"] = "wood", + ["88"] = "default", + ["89"] = "glass", + ["90"] = "warp shield" + } + surfaces = surfaces or "" + local str = "ground surface: " + for i,v in ipairs(string.Split(surfaces,";")) do + local element = grounds_enums_reverse[v] or "" + str = str .. element + if i ~= #string.Split(surfaces,";") then + str = str .. ", " + end + end + return str + end, + callback = function(self, ent, exclude_noclip, surfaces, down) + surfaces = surfaces or "" + if exclude_noclip and ent:GetMoveType() == MOVETYPE_NOCLIP then return false end + local trace = util.TraceLine( { + start = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, 10), + endpos = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, -30 ), + filter = function(ent) + if ent == self:GetRootPart():GetOwner() or ent == self:GetPlayerOwner() then return false end + end + }) + local found = false + if trace.Hit then + local surfs = string.Split(surfaces,";") + for _,surf in pairs(surfs) do + if surf == tostring(trace.MatType) then found = true end + end + end + return found + end }, is_on_ground = { @@ -546,12 +961,68 @@ PART.OldEvents = { return false end, }, - + + --this one uses util.TraceHull is_touching = { - arguments = {{extra_radius = "number"}}, - userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius"}}, - callback = function(self, ent, extra_radius) + operator_type = "none", + tutorial_explanation = "is_touching checks in a box (util.TraceHull) around the host model to see if there's something inside it.\nusually it's the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner,to override the old root owner setting,\nin case of issues when stacking this event inside other events", + arguments = {{extra_radius = "number"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = 0}}, + callback = function(self, ent, extra_radius, nearest_model) + if nearest_model then ent = self:GetOwner() end + extra_radius = extra_radius or 0 + + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.max(radius + extra_radius + 1, 1) + + local mins = Vector(-1,-1,-1) + local maxs = Vector(1,1,1) + local startpos = ent:WorldSpaceCenter() + mins = mins * radius + maxs = maxs * radius + + + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {ent, self:GetRootPart():GetOwner()} + }) + + return tr.Hit + end, + nice = function(self, ent, extra_radius, nearest_model) + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. "]" + return str + end, + }, + --this one uses ents.FindInBox + is_touching_filter = { + operator_type = "none", + tutorial_explanation = "is_touching_filter checks in a box (ents.FindInBox) around the host model to see if there's something inside it, but you can selectively exclude living things (NPCs or players) from being detected.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + arguments = {{extra_radius = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = false}, {default = false}, {default = false}}, + callback = function(self, ent, extra_radius, no_npc, no_players, nearest_model) + if nearest_model then ent = self:GetOwner() end extra_radius = extra_radius or 0 + no_npc = no_npc or false + no_players = no_players or false + nearest_model = nearest_model or false local radius = ent:BoundingRadius() @@ -564,6 +1035,126 @@ PART.OldEvents = { local mins = Vector(-1,-1,-1) local maxs = Vector(1,1,1) local startpos = ent:WorldSpaceCenter() + mins = startpos + mins * radius + maxs = startpos + maxs * radius + + local b = false + local ents_hits = ents.FindInBox(mins, maxs) + for _,ent2 in pairs(ents_hits) do + if (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) and + not ( (no_npc and ent2:IsNPC()) or (no_players and ent2:IsPlayer()) ) + then b = true end + end + + return b + end, + nice = function(self, ent, extra_radius, no_npc, no_players, nearest_model) + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. "]" + if no_npc or no_players then str = str .. " | " end + if no_npc then str = str .. "no_npc " end + if no_players then str = str .. "no_players " end + return str + end + }, + --this one uses ents.FindInBox + is_touching_life = { + operator_type = "none", + tutorial_explanation = "is_touching_life checks in a stretchable box (ents.FindInBox) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}, {default = false}, {default = false}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) + + if nearest_model then ent = self:GetOwner() end + extra_radius = extra_radius or 0 + no_npc = no_npc or false + no_players = no_players or false + x_stretch = x_stretch or 1 + y_stretch = y_stretch or 1 + z_stretch = z_stretch or 1 + nearest_model = nearest_model or false + + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.max(radius + extra_radius + 1, 1) + + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + local startpos = ent:WorldSpaceCenter() + mins = startpos + mins * radius + maxs = startpos + maxs * radius + + local ents_hits = ents.FindInBox(mins, maxs) + local b = false + for _,ent2 in pairs(ents_hits) do + if IsValid(ent2) and (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) + + then + b = true + if ent2:IsNPC() and no_npc then + b = false + elseif ent2:IsPlayer() and no_players then + b = false + end + if b then return b end + end + end + + return b + end, + nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) + + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]" + if no_npc or no_players then str = str .. " | " end + if no_npc then str = str .. "no_npc " end + if no_players then str = str .. "no_players " end + return str + end, + }, + --this one uses util.TraceHull + is_touching_scalable = { + operator_type = "none", + tutorial_explanation = "is_touching_life checks in a stretchable box (util.TraceHull) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) + if nearest_model then ent = self:GetOwner() end + extra_radius = extra_radius or 15 + x_stretch = x_stretch or 1 + y_stretch = y_stretch or 1 + z_stretch = z_stretch or 1 + nearest_model = nearest_model or false + + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + local startpos = ent:WorldSpaceCenter() + + radius = math.max(extra_radius, 1) mins = mins * radius maxs = maxs * radius @@ -572,13 +1163,37 @@ PART.OldEvents = { endpos = startpos, maxs = maxs, mins = mins, - filter = ent + filter = {self:GetRootPart():GetOwner(),ent} } ) return tr.Hit end, + nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) + if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return false end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]" + return str + end, + }, + + is_explicit = { + operator_type = "none", + tutorial_explanation = "is_explicit activates for viewers who want to hide explicit content with pac_hide_disturbing.\nyou can make special censoring effects for them, for example", + + callback = function(self, ent) + return GetConVar("pac_hide_disturbing"):GetBool() + end }, is_in_noclip = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent:GetMoveType() == MOVETYPE_NOCLIP and (not ent.GetVehicle or not ent:GetVehicle():IsValid()) @@ -586,6 +1201,7 @@ PART.OldEvents = { }, is_voice_chatting = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.IsSpeaking and ent:IsSpeaking() @@ -593,6 +1209,7 @@ PART.OldEvents = { }, ammo = { + operator_type = "number", preferred_operator = "above", arguments = {{primary = "boolean"}, {amount = "number"}}, userdata = {{editor_onchange = function(part, num) return math.Round(num) end}}, callback = function(self, ent, primary, amount) @@ -605,6 +1222,7 @@ PART.OldEvents = { end, }, total_ammo = { + operator_type = "number", preferred_operator = "above", arguments = {{ammo_id = "string"}, {amount = "number"}}, callback = function(self, ent, ammo_id, amount) if ent.GetAmmoCount then @@ -623,6 +1241,7 @@ PART.OldEvents = { }, clipsize = { + operator_type = "number", preferred_operator = "above", arguments = {{primary = "boolean"}, {amount = "number"}}, callback = function(self, ent, primary, amount) ent = try_viewmodel(ent) @@ -635,6 +1254,7 @@ PART.OldEvents = { }, vehicle_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -647,6 +1267,7 @@ PART.OldEvents = { }, vehicle_model = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -659,6 +1280,7 @@ PART.OldEvents = { }, driver_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = ent.GetDriver and ent:GetDriver() or NULL @@ -670,6 +1292,7 @@ PART.OldEvents = { }, entity_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(ent:GetClass(), find) @@ -677,6 +1300,7 @@ PART.OldEvents = { }, weapon_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}, {hide = "boolean"}}, callback = function(self, ent, find, hide) ent = try_viewmodel(ent) @@ -701,6 +1325,7 @@ PART.OldEvents = { }, has_weapon = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -717,6 +1342,7 @@ PART.OldEvents = { }, model_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(ent:GetModel(), find) @@ -724,9 +1350,14 @@ PART.OldEvents = { }, sequence_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, - nice = function(self, ent) - return self.sequence_name or "invalid sequence" + nice = function(self, ent, find) + local anim = find + if find == "" then anim = "" end + local str = self.Event .. " ["..self.Operator.. " " .. anim .. "] | " + local seq = self.sequence_name or "invalid sequence" + return str .. seq end, callback = function(self, ent, find) ent = get_owner(self) @@ -738,6 +1369,7 @@ PART.OldEvents = { }, timer = { + operator_type = "none", arguments = {{interval = "number"}, {offset = "number"}}, callback = function(self, ent, interval, offset) interval = interval or 1 @@ -753,21 +1385,42 @@ PART.OldEvents = { }, animation_event = { - arguments = {{find = "string"}, {time = "number"}}, - nice = function(self) - return self.anim_name or "" + operator_type = "string", preferred_operator = "find simple", + arguments = {{find = "string"}, {time = "number"}, {try_stop_gesture = "boolean"}}, + userdata = {{default = "attack primary", enums = function() + local tbl = {} + for i,v in pairs(animation_event_enums) do + tbl[i] = v + end + return tbl + end}, {default = 0.5}}, + nice = function(self, ent, find, time) + find = find or "" + time = time or 0 + local anim = self.anim_name or "" + local str = self.Event .. " ["..self.Operator.. " \"" .. find .. "\" : " .. time .. " seconds] | " + return str .. anim end, - callback = function(self, ent, find, time) + callback = function(self, ent, find, time, try_stop_gesture) time = time or 0.1 ent = get_owner(self) local data = ent.pac_anim_event local b = false - + if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then data.reset = false b = true + if try_stop_gesture then + if string.find(find, "attack grenade") then + ent:AnimResetGestureSlot( GESTURE_SLOT_GRENADE ) + elseif string.find(find, "attack") or string.find(find, "reload") then + ent:AnimResetGestureSlot( GESTURE_SLOT_ATTACK_AND_RELOAD ) + elseif string.find(find, "flinch") then + ent:AnimResetGestureSlot( GESTURE_SLOT_FLINCH ) + end + end end if b then @@ -781,6 +1434,8 @@ PART.OldEvents = { }, fire_bullets = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "fire_bullets supposedly checks what types of bullets you're firing", arguments = {{find_ammo = "string"}, {time = "number"}}, callback = function(self, ent, find, time) time = time or 0.1 @@ -790,7 +1445,7 @@ PART.OldEvents = { local data = ent.pac_fire_bullets local b = false - if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then + if data and (self:StringOperator(data.name, find_ammo) and (time == 0 or data.time + time > pac.RealTime)) then data.reset = false b = true end @@ -800,6 +1455,7 @@ PART.OldEvents = { }, emit_sound = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find_sound = "string"}, {time = "number"}, {mute = "boolean"}}, callback = function(self, ent, find, time, mute) time = time or 0.1 @@ -822,19 +1478,22 @@ PART.OldEvents = { }, command = { + operator_type = "string", preferred_operator = "equal", + tutorial_explanation = "the command event reads your pac_event states.\nthe pac_event command can turn on (1), off (0) or toggle (2) a state that has a name.\nfor example, \"pac_event myhat 2\" can be used with a myhat command event to put the hat on or off\n\nwith this event, you read the states that contain this find name\n(equal being an exact match; find and find simple allowing to detect from different states having a part of the name)\n\nthe final result is to activate if:\n\tA) there's one active, or \n\tB) there's one recently turned off not too long ago", arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}}, userdata = { {default = "change_me", editor_friendly = "CommandName"}, - {default = 0.1, editor_friendly = "EventDuration"}, + {default = 0, editor_friendly = "EventDuration"}, {default = false, group = "event wheel", editor_friendly = "HideInEventWheel"} }, nice = function(self, ent, find, time) find = find or "?" time = time or "?" - return "command: " .. find .. " | " .. "duration: " .. time + return "command: [" .. self.Operator .. " " .. find .."] | " .. "duration: " .. time end, callback = function(self, ent, find, time) - time = time or 0.1 + + time = time or 0 local ply = self:GetPlayerOwner() @@ -857,6 +1516,8 @@ PART.OldEvents = { }, say = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "say looks at the chat to find if a certain thing has been said some time ago", arguments = {{find = "string"}, {time = "number"}, {all_players = "boolean"}}, callback = function(self, ent, find, time, all_players) time = time or 0.1 @@ -886,6 +1547,7 @@ PART.OldEvents = { -- outfit owner owner_velocity_length = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -899,6 +1561,7 @@ PART.OldEvents = { end, }, owner_velocity_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -911,6 +1574,7 @@ PART.OldEvents = { end, }, owner_velocity_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -923,6 +1587,7 @@ PART.OldEvents = { end, }, owner_velocity_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -935,6 +1600,7 @@ PART.OldEvents = { end, }, owner_velocity_world_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -947,6 +1613,7 @@ PART.OldEvents = { end, }, owner_velocity_world_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -959,6 +1626,7 @@ PART.OldEvents = { end, }, owner_velocity_world_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -973,6 +1641,7 @@ PART.OldEvents = { -- parent part parent_velocity_length = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -989,6 +1658,7 @@ PART.OldEvents = { end, }, parent_velocity_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1005,6 +1675,7 @@ PART.OldEvents = { end, }, parent_velocity_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1021,6 +1692,7 @@ PART.OldEvents = { end, }, parent_velocity_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1038,6 +1710,7 @@ PART.OldEvents = { }, parent_scale_x = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1054,6 +1727,7 @@ PART.OldEvents = { end, }, parent_scale_y = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1070,6 +1744,7 @@ PART.OldEvents = { end, }, parent_scale_z = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1087,6 +1762,7 @@ PART.OldEvents = { }, gravitygun_punt = { + operator_type = "number", preferred_operator = "above", arguments = {{time = "number"}}, callback = function(self, ent, time) time = time or 0.1 @@ -1095,13 +1771,14 @@ PART.OldEvents = { local punted = ent.pac_gravgun_punt - if punted and punted + time > pac.RealTime then - return true + if punted then + return self:NumberOperator(pac.RealTime, punted + time) end end, }, movetype = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) local mt = ent:GetMoveType() @@ -1112,6 +1789,8 @@ PART.OldEvents = { }, dot_forward = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles", arguments = {{normal = "number"}}, callback = function(self, ent, normal) @@ -1128,6 +1807,9 @@ PART.OldEvents = { }, dot_right = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) @@ -1144,6 +1826,9 @@ PART.OldEvents = { }, flat_dot_forward = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) local owner = self:GetRootPart():GetOwner() @@ -1163,6 +1848,9 @@ PART.OldEvents = { }, flat_dot_right = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) local owner = self:GetRootPart():GetOwner() @@ -1179,48 +1867,838 @@ PART.OldEvents = { return 0 end - } -} + }, -do - local enums = {} - local enums2 = {} - for key, val in pairs(_G) do - if isstring(key) and isnumber(val) then - if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then - enums[val] = key:sub(5):lower() - enums2[enums[val]] = val - elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then - enums[val] = key:lower() - enums2[enums[val]] = val + is_sitting = { + operator_type = "none", + callback = function(self, ent) + if not ent:GetVehicle() then return false end + if ent.GetSitting then return (IsValid(ent:GetVehicle()) or ent:GetSitting()) and ent:GetVehicle():GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" end --sit anywhere script + return IsValid(ent:GetVehicle()) and ent:GetVehicle():GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" --no prison pod! + end + }, + + is_driving = { + operator_type = "none", + callback = function(self, ent) + if not ent:IsPlayer() then return false end + local vehicle = ent:GetVehicle() + + if IsValid(vehicle) then --vehicle entity exists + if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent + --print(vehicle:GetParent().PassengerSeats) + + if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys + return ent:IsDrivingSimfphys() and ent:GetVehicle() == ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat + elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too + --print(vehicle:GetParent().BaseClass.ClassName, #vehicle:GetParent().Seats) + --PrintTable(vehicle:GetParent().Seats[1]) + return vehicle == vehicle.wac_seatswitcher.seats[1] --first seat + end + elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we don't want bare seats or prisoner pod + if vehicle.HandleAnimation == true and not isfunction(vehicle.HandleAnimation) and vehicle:GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" then --exclude prisoner pod and narrow down to SCars + return true + end + return false + else --assume that most other classes than prop_vehicle_prisoner_pod are drivable vehicles + return true + end end + return false end - end + }, - pac.key_enums = enums + is_passenger = { + operator_type = "none", + callback = function(self, ent) + if not ent:IsPlayer() then return false end + local vehicle = ent:GetVehicle() + + if IsValid(vehicle) then --vehicle entity exists + if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent + if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys + return ent:IsDrivingSimfphys() and ent:GetVehicle() ~= ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat + elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too + return vehicle ~= vehicle.wac_seatswitcher.seats[1] --first seat + end + elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we can count bare seats and prisoner pods as passengers + return true + else --assume that most other classes than prop_vehicle_prisoner_pod are drivable vehicles, but they're also probably single seaters so... + return false + end + end + return false + end + }, - --TODO: Rate limit!!! - net.Receive("pac.BroadcastPlayerButton", function() - local ply = net.ReadEntity() + weapon_iron_sight = { + operator_type = "none", + callback = function(self, ent) + if not IsValid(ent) or ent:Health() < 1 then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + if wep.IsFAS2Weapon then + return wep.dt.Status == FAS_STAT_ADS + end - if not ply:IsValid() then return end + if wep.GetIronSights then return wep:GetIronSights() end + if wep.Sighted then return wep:GetActiveSights() end --arccw + return false + end + }, - if ply == pac.LocalPlayer and (pace and pace.IsFocused() or gui.IsConsoleVisible()) then return end + weapon_firemode = { + operator_type = "mixed", + arguments = {{name_or_id = "string"}}, + callback = function(self, ent, name_or_id) + name_or_id = string.lower(name_or_id) + if not IsValid(ent) or ent:Health() < 1 then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + + if wep.ArcCW then + if wep.Firemodes[wep:GetFireMode()] then --some use a Firemodes table + if wep.Firemodes[wep:GetFireMode()].PrintName then + return + self:StringOperator(name_or_id, wep.Firemodes[wep:GetFireMode()].PrintName) + or self:StringOperator(name_or_id, wep:GetFiremodeName()) + or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end + elseif wep.Primary then + if wep.Primary.Automatic ~= nil then + if wep.Primary.Automatic == true then + return name_or_id == "automatic" or name_or_id == "auto" + else + return name_or_id == "semi-automatic" or name_or_id == "semi-auto" or name_or_id == "single" + end + end + self:StringOperator(name_or_id, wep:GetFiremodeName()) + end + return self:StringOperator(name_or_id, wep:GetFiremodeName()) or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end - local key = net.ReadUInt(8) - local down = net.ReadBool() + if wep.IsFAS2Weapon then + if not wep.FireMode then return name_or_id == "" or name_or_id == "nil" or name_or_id == "null" or name_or_id == "none" + else return self:StringOperator(wep.FireMode, name_or_id) end + end - key = pac.key_enums[key] or key + if wep.GetFireModeName then --TFA base is an arbitrary number and name (language-specific) + return self:StringOperator(string.lower(wep:GetFireModeName()), name_or_id) or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end + + if wep.Primary then + if wep.Primary.Automatic ~= nil then --M9K is a boolean + if wep.Primary.Automatic == true then + return name_or_id == "automatic" or name_or_id == "auto" or name_or_id == "1" + else + return name_or_id == "semi-automatic" or name_or_id == "semi-auto" or name_or_id == "single" or name_or_id == "0" + end + end + end + + + return false + end, + nice = function(self, ent, name_or_id) + if not IsValid(ent) then return end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return "invalid weapon" end + wep = ent:GetActiveWeapon() + local str = "weapon_firemode ["..self.Operator.. " " .. name_or_id .. "] | " + + if wep.IsFAS2Weapon then + + if wep.FireMode then + str = str .. wep.FireMode .. " | options : " + for i,v in ipairs(wep.FireModes) do + str = str .. "(" .. v .. " = " .. i.. "), " + end + else str = str .. "" end + return str + end + + if wep.ArcCW then + if not IsValid(wep) then return "no active weapon" end + if wep.GetFiremodeName then + str = str .. wep:GetFiremodeName() .. " | options : " + for i,v in ipairs(wep.Firemodes) do + if v.PrintName then + str = str .. "(" .. i .. " = " .. v.PrintName .. "), " + end + end + if wep.Primary.Automatic then + str = str .. "(" .. "Automatic" .. "), " + end + end + return str + end + + if wep.GetFireModeName then --TFA base or arccw + if not IsValid(wep) then return "no active weapon" end + if wep.GetFireModeName then + str = str .. wep:GetFireModeName() .. " | options : " + for i,v in ipairs(wep:GetStatL("FireModes")) do + str = str .. "(" .. v .. " = " .. i.. "), " + end + end + return str + end + + if wep.Primary then --M9K + if wep.Primary.Automatic ~= nil then + if wep.Primary.Automatic then + str = str .. "automatic" + else + str = str .. "semi-auto" + end + end + str = str .. " | options : 1/auto/automatic, 0/single/semi-auto/semi-automatic" + return str + end + + + return str + end + }, + + weapon_safety = { + operator_type = "none", + callback = function(self, ent) + if not ent or not IsValid(ent) then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + if wep.IsSafety then + return wep:IsSafety() + end + if wep.ArcCW then + return wep:GetFiremodeName() == "Safety" + end + + return false + end + }, +--@note take damage is like health_lost but 400% better + + take_damage = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {damage = "number"}, {attackers = "string"}, {inflictors = "string"}, {damage_type = "number"}}, + userdata = {{default = 1}, {default = 10}, + {default = "any", + enums = function() + local players = {} + for i,v in ipairs(player.GetAll()) do + players[v:Nick()] = v:SteamID() + end + return players + end + }, {default = "any"}, + {default = -1, + enums = function() + local damage_enums = {} + for k,v in pairs(_G) do + if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then + damage_enums[k] = tostring(v) + end + end + return damage_enums + end + }}, + callback = function(self, ent, time, damage, attackers, inflictors, damage_type) + local time = time or 0 + if not IsValid(ent) then return false end + local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" + local found_attacker = attackers == "" or attackers == "anyone" or attackers == "any" or attackers == "all" + + local unspec_inflictor = found_inflictor + local unspec_attacker = found_attacker + + local unspec_dmg = damage_type == -1 + local matching_dmg = unspec_dmg + + local lastest_attacker = nil + local latest_hit_time = 0 + + if not ent.pac_damage_attributions then + ent.pac_damage_attributions = {} + return false + elseif (table.Count(ent.pac_damage_attributions) < 1) then + return false + end + + ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 + + if CurTime() < ent.pac_damage_attributions.IngoingGraceTime + time then return true end + if not ent.pac_damage_attributions then return false end --the entity is a damage virgin, it's not hurt + + + for attacker,tbl in pairs(ent.pac_damage_attributions) do + if IsValid(attacker) and IsValid(tbl.inflictor) then + local found_inflictor_class = false + for i,v in ipairs(string.Split(inflictors, ";")) do + if v == tbl.inflictor:GetClass() then + found_inflictor = true + end + end + for i,v in ipairs(string.Split(attackers, ";")) do + if v == tbl.attacker:GetClass() then + found_attacker = true + elseif tbl.attacker:IsPlayer() then + if tbl.attacker:SteamID() == v or tbl.attacker:Nick() == v then + found_attacker = true + end + --print("tested attacker ", tbl.attacker:GetClass(), "it aint it.", v) + end + end + if tbl.hit_time > latest_hit_time and (bit.band(tbl.dmg_type, damage_type) ~= 0 or unspec_dmg) then + matching_dmg = true + latest_hit_time = tbl.hit_time + lastest_attacker = attacker + end + else --lost entity! i.e. grenades get removed so we can't use direct entity reference anymore + --so we give it a grace period for next time + if not type(attacker) == "table" then --exclude the grace fields... + ent.pac_damage_attributions.IngoingGraceTime = CurTime() + ent.pac_damage_attributions[attacker] = nil + end + end + + end + if not unspec_dmg and not matching_dmg then return false end + --print(ent.pac_damage_attributions.IngoingGraceTime) + ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 + + --print("CurTime:"..CurTime(), "Grace:" .. ent.pac_damage_attributions.IngoingGraceTime) + + + if found_attacker and found_inflictor then + if ent.pac_damage_attributions[lastest_attacker] then + if CurTime() < ent.pac_damage_attributions[lastest_attacker].hit_time + time then + return self:NumberOperator(ent.pac_damage_attributions[lastest_attacker].dmg_amount, damage) + end + elseif CurTime() < ent.pac_damage_attributions.IngoingGraceTime + time then + return true + end + + end + + if unspec_attacker and unspec_inflictor then + + if ent.pac_damage_attributions.latest then + + if (CurTime() < ent.pac_damage_attributions.latest.hit_time + time) then + return self:NumberOperator(ent.pac_damage_attributions.latest.dmg_amount, damage) + end + end + end + + return false + end, + nice = function(self, ent, time, damage, attackers, inflictors, damage_type) + time = time or 0 + damage = damage or 0 + attackers = attackers or "" + inflictors = inflictors or "" + damage_type = damage_type or -1 + local str = "take_damage : [" .. self.Operator .. " " .. damage .. "]" + if attackers == "" or attackers == "any" or attackers == "anyone" or attackers == "all" then + str = str .. " | from any attacker " + else + str = str .. " | from attackers: " + for i,v in ipairs(string.Split(attackers, ";")) do + str = str .. v .. " " + end + end + for i,v in ipairs(string.Split(attackers, ";")) do + str = str .. v .. " " + end + if inflictors == "" or inflictors == "any" or inflictors == "all" then + str = str .. " | from any inflictor" + else + str = str .. " | from inflictors: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + str = str .. " | with damage types : " .. damage_type + return str + end + }, + + inflicting_damage = { + operator_type = "mixed", preferred_operator = "above", + tutorial_explanation = "", + arguments = {{time = "number"}, {damage = "number"}, {targets = "string"}, {inflictors = "string"}, {damage_type = "number"}}, + userdata = {{default = 1}, {default = 10}, + {default = "any", + enums = function() + local players = {} + for i,v in ipairs(player.GetAll()) do + players[v:Nick()] = v:SteamID() + end + return players + end + }, {default = "any"}, + {default = -1, + enums = function() + local damage_enums = {} + for k,v in pairs(_G) do + if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then + damage_enums[k] = tostring(v) + end + end + return damage_enums + end + }}, + callback = function(self, ent, time, damage, targets, inflictors, damage_type) + local time = time or 0 + + if not IsValid(ent) then return false end + local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" + local found_target = targets == "" or targets == "anyone" or targets == "any" or targets == "all" + local unspec_dmg = damage_type == -1 + + ent.pac_damage_attributions = ent.pac_damage_attributions or {} + ent.pac_damage_attributions.OutgoingGraceTime = ent.pac_damage_attributions.OutgoingGraceTime or 0 + ent.pac_damage_attributions.OutgoingGraceTimeDMG = ent.pac_damage_attributions.OutgoingGraceTimeDMG or 0 + local latest_hit_time = ent.pac_damage_attributions.OutgoingGraceTime or 0 + + for _,target in pairs(ents.GetAll()) do --check ents we could hurt + if target.pac_damage_attributions then --skip the virgins + if target.pac_damage_attributions[ent] then --we're in. + + tbl = target.pac_damage_attributions[ent] + if not found_inflictor then + for i,v in ipairs(string.Split(inflictors, ";")) do + if v == tbl.inflictor:GetClass() then + found_inflictor = true + end + end + end + if not found_target then + for i,v in ipairs(string.Split(targets, ";")) do + if v == target:GetClass() then + found_target = true + elseif target:IsPlayer() then + if target:SteamID() == v or target:Nick() == v then + found_target = true + end + end + end + end + + if tbl.hit_time > latest_hit_time and (bit.band(tbl.dmg_type, damage_type) ~= 0 or unspec_dmg) then + latest_hit_time = CurTime() + ent.pac_damage_attributions.OutgoingGraceTime = CurTime() + ent.pac_damage_attributions.OutgoingGraceTimeDMG = tbl.dmg_amount + end + end + end + end + --WHAT ABOUT KILLS?? DONT WORRY ABOUT IT (TM) + --print("CurTime:" .. CurTime(), "out grace"..ent.pac_damage_attributions.OutgoingGraceTime) + if found_target and found_inflictor then + if CurTime() < ent.pac_damage_attributions.OutgoingGraceTime + time then + return self:NumberOperator(ent.pac_damage_attributions.OutgoingGraceTimeDMG, damage) + end + end + + return false + end, + nice = function(self, ent, time, damage, targets, inflictors, damage_type) + time = time or 0 + damage = damage or 0 + attackers = attackers or "" + targets = targets or "" + damage_type = damage_type or -1 + local str = "inflicting_damage : [" .. self.Operator .. " " .. damage .. "]" + if targets == "" or targets == "anyone" or targets == "any" or targets == "all" then + str = str .. " | to any target" + else + str = str .. " | to targets: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + if inflictors == "" or inflictors == "any" or inflictors == "all" then + str = str .. " | from any inflictor " + else + str = str .. " | from inflictors: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + str = str .. " | with damage types : " .. damage_type + return str + end + }, + + damage_zone_hit = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {damage = "number"}, {uid = "string"}}, + userdata = {{default = 1}, {default = 0}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "damage_zone" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, time, damage, uid) + uid = uid or "" + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "damage_zone" then + if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then + if part.dmgzone_hit_done + time > CurTime() then + return true + end + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + if part.ClassName == "damage_zone" then + if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then + if part.dmgzone_hit_done + time > CurTime() then + return true + end + end + else + self:SetError("You set a UID that's not a damage zone!") + end + end + return false + end, + }, + + damage_zone_kill = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {uid = "string"}}, + userdata = {{default = 1}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "damage_zone" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, time, uid) + uid = uid or "" + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "damage_zone" then + if part.dmgzone_kill_done then + if part.dmgzone_kill_done + time > CurTime() then + return true + end + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + if part.ClassName == "damage_zone" then + if part.dmgzone_kill_done then + if part.dmgzone_kill_done + time > CurTime() then + return true + end + end + else + self:SetError("You set a UID that's not a damage zone!") + end + end + return false + end, + }, + + lockpart_grabbed = { + operator_type = "none", + callback = function(self, ent) + return ent.IsGrabbed and ent.IsGrabbedByUID + end + }, + + lockpart_grabbing = { + operator_type = "none", + arguments = {{uid = "string"}}, + userdata = {{enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "lock" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, uid) + uid = uid or "" + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "lock" then + if part.grabbing then + return IsValid(part.target_ent) + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + if part.ClassName == "lock" then + if part.grabbing then + return IsValid(part.target_ent) + end + else + self:SetError("You set a UID that's not a lock part!") + end + end + return false + end + }, + + --[[ + ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {} + ent.pac_healthbars_uidtotals = ent.pac_healthbars_uidtotals or {} + ent.pac_healthbars_total = 0 + ]] + healthmod_bar_total = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}}, + userdata = {{default = 0}}, + callback = function(self, ent, HpValue) + if ent.pac_healthbars and ent.pac_healthbars_total then + return self:NumberOperator(ent.pac_healthbars_total, HpValue) + end + return false + end, + nice = function(self, ent, HpValue) + local str = "healthmod_bar_total : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_total then + str = str .. " | " .. ent.pac_healthbars_total + end + return str + end + }, + + healthmod_bar_layertotal = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}, {layer = "number"}}, + userdata = {{default = 0}, {default = 0}}, + callback = function(self, ent, HpValue, layer) + if ent.pac_healthbars and ent.pac_healthbars_layertotals then + if ent.pac_healthbars_layertotals[layer] then + return self:NumberOperator(ent.pac_healthbars_layertotals[layer], HpValue) + end + + end + return false + end, + nice = function(self, ent, HpValue, layer) + local str = "healthmod_layer_total at layer " .. layer .. " : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_layertotals then + if ent.pac_healthbars_layertotals[layer] then + str = str .. " | " .. ent.pac_healthbars_layertotals[layer] + else + str = str .. " | not found" + end + + else + str = str .. " | not found" + end + return str + end + }, + + healthmod_bar_uidvalue = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}, {part_uid = "string"}}, + userdata = {{default = 0}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "health_modifier" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, HpValue, part_uid) + if ent.pac_healthbars and ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals[part_uid] then + return self:NumberOperator(ent.pac_healthbars_uidtotals[part_uid], HpValue) + end + end + return false + end, + nice = function(self, ent, HpValue, part_uid) + local str = "healthmod_bar_uidvalue : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals[part_uid] then + str = str .. " | " .. ent.pac_healthbars_uidtotals[part_uid] + else + str = str .. " | nothing for UID "..part_uid + end + else + str = str .. " | nothing for UID "..part_uid + end + return str + end + }, + +} + + +do + + --[[local base_input_enums_names = { + ["IN_ATTACK"] = 1, + ["IN_JUMP"] = 2, + ["IN_DUCK"] = 4, + ["IN_FORWARD"] = 8, + ["IN_BACK"] = 16, + ["IN_USE"] = 32, + ["IN_CANCEL"] = 64, + ["IN_LEFT"] = 128, + ["IN_RIGHT"] = 256, + ["IN_MOVELEFT"] = 512, + ["IN_MOVERIGHT"] = 1024, + ["IN_ATTACK2"] = 2048, + ["IN_RUN"] = 4096, + ["IN_RELOAD"] = 8192, + ["IN_ALT1"] = 16384, + ["IN_ALT2"] = 32768, + ["IN_SCORE"] = 65536, + ["IN_SPEED"] = 131072, + ["IN_WALK"] = 262144, + ["IN_ZOOM"] = 524288, + ["IN_WEAPON1"] = 1048576, + ["IN_WEAPON2"] = 2097152, + ["IN_BULLRUSH"] = 4194304, + ["IN_GRENADE1"] = 8388608, + ["IN_GRENADE2"] = 16777216 + } + local input_aliases = {} + + for name,value in pairs(base_input_enums_names) do + local alternative0 = string.lower(name) + local alternative1 = string.Replace(string.lower(name),"in_","") + local alternative2 = "+"..alternative1 + input_aliases[name] = value + input_aliases[alternative0] = value + input_aliases[alternative1] = value + input_aliases[alternative2] = value + end]] + + + local enums = {} + local enums2 = {} + for key, val in pairs(_G) do + if isstring(key) and isnumber(val) then + if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:sub(5):lower() + enums2[enums[val]] = val + elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:lower() + enums2[enums[val]] = val + end + end + end + + pac.key_enums = enums + +--@note button broadcast + + --TODO: Rate limit!!! + net.Receive("pac.BroadcastPlayerButton", function() + local ply = net.ReadEntity() + + if not ply:IsValid() then return end + + if ply == pac.LocalPlayer and (pace and pace.IsFocused() or gui.IsConsoleVisible()) then return end + + local key = net.ReadUInt(8) + local down = net.ReadBool() + + if not pac.key_enums then --rebuild the enums + local enums = {} + local enums2 = {} + for key, val in pairs(_G) do + if isstring(key) and isnumber(val) then + if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:sub(5):lower() + enums2[enums[val]] = val + elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:lower() + enums2[enums[val]] = val + end + end + end + + pac.key_enums = enums + end + + key = pac.key_enums[key] or key ply.pac_buttons = ply.pac_buttons or {} ply.pac_buttons[key] = down + + + ply.pac_broadcasted_buttons_lastpressed = ply.pac_broadcasted_buttons_lastpressed or {} + if down then + ply.pac_broadcasted_buttons_lastpressed[key] = SysTime() + end + + for _,part in pairs(pac.getallparts()) do --locate the corresponding parts among the part pool + if part:GetPlayerOwner() == ply and part.ClassName == "event" and part.Event == "button" then + part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {} + part.holdtime = part.holdtime or 0 + part.toggleimpulsekey = part.toggleimpulsekey or {} + part.toggleimpulsekey[key] = down + part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 + ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0 + part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime + end + end + end) + pac.player_inputs = {} + pac.player_inputs_update_times = {} + + net.Receive("pac.BroadcastPlayerInputs", function() + pac.player_inputs = net.ReadTable() + pac.player_inputs_update_times = net.ReadTable() + end) + + PART.OldEvents.button = { - arguments = {{button = "string"}}, + operator_type = "none", + arguments = {{button = "string"}, {holdtime = "number"}, {toggle = "boolean"}}, userdata = {{enums = function() return enums - end}}, + end, default = "mouse_left"}, {default = 0}, {default = false}}, nice = function(self, ent, button) local ply = self:GetPlayerOwner() @@ -1240,11 +2718,31 @@ do return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")" end, - callback = function(self, ent, button) + callback = function(self, ent, button, holdtime, toggle) + + local holdtime = holdtime or 0 + local toggle = toggle or false + + self.togglestate = self.togglestate or false + self.holdtime = holdtime + self.toggle = toggle + + self.toggleimpulsekey = self.toggleimpulsekey or {} + + if self.toggleimpulsekey[button] then + self.togglestate = not self.togglestate + self.toggleimpulsekey[button] = false + end + + --print(button, "hold" ,self.holdtime) local ply = self:GetPlayerOwner() + self.pac_broadcasted_buttons_holduntil = self.pac_broadcasted_buttons_holduntil or {} + if ply == pac.LocalPlayer then + ply.pac_broadcast_buttons = ply.pac_broadcast_buttons or {} + if not ply.pac_broadcast_buttons[button] then local val = enums2[button:lower()] if val then @@ -1254,15 +2752,103 @@ do end ply.pac_broadcast_buttons[button] = true end + + --print(button, ply.pac_broadcasted_buttons_holduntil[button], ply.pac_broadcast_buttons[button]) + --PrintTable(ply.pac_broadcast_buttons) + --PrintTable(self.pac_broadcasted_buttons_holduntil) end local buttons = ply.pac_buttons + self.pac_broadcasted_buttons_holduntil[button] = self.pac_broadcasted_buttons_holduntil[button] or SysTime() + --print(button, self.toggle, self.togglestate) + --print(button,"until",self.pac_broadcasted_buttons_holduntil[button]) if buttons then - return buttons[button] + --print("trying to compare " .. SysTime() .. " > " .. self.pac_broadcasted_buttons_holduntil[button] - 0.05) + if self.toggle then + return self.togglestate + elseif self.holdtime > 0 then + return SysTime() < self.pac_broadcasted_buttons_holduntil[button] + else + return buttons[button] + end + end end, } + + --PART.OldEvents.input = { + -- operator_type = "none", + -- arguments = {{UserInputs = "string"}, {RequireAllInputs = "boolean"}}, + -- userdata = {{enums = function() + -- return base_input_enums_names + -- end}}, + -- callback = function(self, ent, UserInputs, RequireAllInputs) + -- local ply = self:GetPlayerOwner() + -- UserInputs = UserInputs or "" + -- pac.player_inputs[ply] = pac.player_inputs[ply] or {} + -- local detect = false + -- local fulldetect = true + -- local input_list = string.Split(UserInputs, ";") + -- + -- for i,v in pairs(pac.player_inputs[ply]) do + -- for _,v2 in pairs(input_list) do + -- if pac.player_inputs[ply][input_aliases[v2]] then detect = true + -- else fulldetect = false end + -- end + -- end + -- if RequireAllInputs then return fulldetect + -- else return detect end + -- end + --} + + --[[PART.OldEvents.is_moving = { + operator_type = "none", + callback = function(self) + local ply = self:GetPlayerOwner() + pac.player_inputs = pac.player_inputs or {} + pac.player_inputs[ply] = pac.player_inputs[ply] or {} + return pac.player_inputs[ply][IN_FORWARD] or + pac.player_inputs[ply][IN_BACK] or + pac.player_inputs[ply][IN_MOVELEFT] or + pac.player_inputs[ply][IN_MOVERIGHT] or + pac.player_inputs[ply][IN_JUMP] + end + }]] + + --[[PART.OldEvents.afk = { + operator_type = "none", + arguments = {{time = "number"}, {IncludeEyeAngles = "boolean"}}, + callback = function(self, ent, time, IncludeEyeAngles) + local time = time or 0 + local IncludeEyeAngles = IncludeEyeAngles + local ply = self:GetPlayerOwner() + local time_bool = false + local eyes_bool = false + + if pac.player_inputs_update_times then + pac.player_inputs_update_times[ply] = pac.player_inputs_update_times[ply] or 0 + time_bool = pac.player_inputs_update_times[ply] + time > CurTime() + end + + ply.last_eyeang = ply.last_eyeang or ply:EyeAngles() + ply.eyeang_update_time = ply.eyeang_update_time or CurTime() + + if ply.last_eyeang ~= ply:EyeAngles() then + ply.eyeang_update_time = CurTime() + end + + eyes_bool = (ply.last_eyeang ~= ply:EyeAngles()) or (ply.eyeang_update_time + time > CurTime()) + ply.last_eyeang = ply:EyeAngles() + if IncludeEyeAngles then + return not (time_bool or eyes_bool) + else + return not time_bool + end + return true + end + }]] + end do @@ -1417,7 +3003,7 @@ do local arguments = data.arguments local think = data.callback local eventObject = pac.CreateEvent(classname) - + if arguments then for i, data2 in ipairs(arguments) do local key, Type = next(data2) @@ -1427,6 +3013,13 @@ do eventObject.extra_nice_name = data.nice + local operator_type = data.operator_type + local preferred_operator = data.preferred_operator + local tutorial_explanation = data.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + function eventObject:Think(event, ent, ...) return think(event, ent, ...) end @@ -1443,6 +3036,8 @@ end do local animations = pac.animations local event = { + operator_type = "none", + tutorial_explanation = "selecting a custom animation part via UID,\nthis event activates whenever the linked custom animation is currently playing somewhere between the frames specified", name = "custom_animation_frame", nice = function(self, ent, animation) if animation == "" then self:SetWarning("no animation selected") return "no animation" end @@ -1517,6 +3112,15 @@ do eventObject.IsAvailable = event.available eventObject.extra_nice_name = event.nice + data = event + + local operator_type = data.operator_type + local preferred_operator = data.preferred_operator + local tutorial_explanation = data.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + pac.RegisterEvent(eventObject) end @@ -1617,6 +3221,13 @@ do return isDarkRP() and available() end + local operator_type = v.operator_type + local preferred_operator = v.preferred_operator + local tutorial_explanation = v.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + pac.RegisterEvent(eventObject) end end @@ -1643,7 +3254,7 @@ local function is_hidden_by_something_else(part, ignored_part) if part.active_events_ref_count > 0 and not part.active_events[ignored_part] then return true end - + return part.Hide end @@ -1690,13 +3301,50 @@ end PART.last_event_triggered = false +function PART:fix_args() + local args = string.Split(self.Arguments, "@@") + if self.Events[self.Event] then + if self.Events[self.Event].__registeredArguments then + --PrintTable(self.Events[self.Event].__registeredArguments) + if #self.Events[self.Event].__registeredArguments ~= #args then + for argn,arg in ipairs(self.Events[self.Event].__registeredArguments) do + if not args[argn] or args[argn] == "" then + local added_arg = "0" + if arg[2] == "boolean" then + if arg[3] then + if arg[3].default then added_arg = "1" + else added_arg = "0" end + end + else + if arg[3] then + if arg[3].default then + added_arg = tostring(arg[3].default) + end + end + end + args[argn] = added_arg + end + end + self.Arguments = table.concat(args, "@@") + end + end + end +end + function PART:OnThink() + self.nextactivationrefresh = self.nextactivationrefresh or CurTime() + if not self.singleactivatestate and self.nextactivationrefresh < CurTime() then + self.singleactivatestate = true + end + local ent = get_owner(self) if not ent:IsValid() then return end local data = PART.Events[self.Event] + if not data then return end + self:fix_args() self:TriggerEvent(should_trigger(self, ent, data)) if pace and pace.IsActive() and self.Name == "" then @@ -1707,6 +3355,51 @@ function PART:OnThink() end +function PART:SetAffectChildrenOnly(b) + if b == nil then return end + + if self.AffectChildrenOnly ~= nil and self.AffectChildrenOnly ~= b then + --print("changing") + local ent = get_owner(self) + local data = PART.Events[self.Event] + + if ent:IsValid() and data then + local b = should_trigger(self, ent, data) + if self.AffectChildrenOnly then + local parent = self:GetParent() + if parent:IsValid() then + parent:SetEventTrigger(self, b) + + for _, child in ipairs(self:GetChildren()) do + if child.active_events[self] then + child.active_events[self] = nil + child.active_events_ref_count = child.active_events_ref_count - 1 + child:CallRecursive("CalcShowHide", false) + end + end + end + + else + for _, child in ipairs(self:GetChildren()) do + child:SetEventTrigger(self, b) + end + if self:GetParent():IsValid() then + local parent = self:GetParent() + if parent.active_events[self] then + parent.active_events[self] = nil + parent.active_events_ref_count = parent.active_events_ref_count - 1 + parent:CallRecursive("CalcShowHide", false) + end + end + + end + end + end + self.AffectChildrenOnly = b + +end + + function PART:TriggerEvent(b) self.event_triggered = b -- event_triggered is just used for the editor @@ -1720,6 +3413,16 @@ function PART:TriggerEvent(b) parent:SetEventTrigger(self, b) end end + if IsValid(self.DestinationPart) then --target part. the proper one. + if IsValid(self.previousdestinationpart) then + if self.DestinationPart ~= self.previousdestinationpart then --once we change the destination part we need to reset the old one + self.previousdestinationpart:SetEventTrigger(self, false) + end + end + + (self.DestinationPart):SetEventTrigger(self, b) + self.previousdestinationpart = (self.DestinationPart) + end end PART.Operators = { @@ -1866,6 +3569,8 @@ function PART:NumberOperator(a, b) end end + + function PART:OnHide() if self.timerx_reset then self.time = nil @@ -1889,6 +3594,8 @@ function PART:OnShow() self.time = nil self.number = 0 end + self.showtime = CurTime() + self.singleactivatestate = true end function PART:OnAnimationEvent(ent) @@ -1963,6 +3670,7 @@ pac.AddHook("EntityFireBullets", "firebullets", function(ent, data) end end) + net.Receive("pac_event", function(umr) local ply = net.ReadEntity() local str = net.ReadString() @@ -1979,6 +3687,95 @@ net.Receive("pac_event", function(umr) end end) +concommand.Add("pac_wipe_events", function(ply) + ply.pac_command_events = nil + ply.pac_command_event_sequencebases = nil +end) +concommand.Add("pac_print_events", function(ply) + ply.pac_command_events = ply.pac_command_events or {} + PrintTable(ply.pac_command_events) +end) + +concommand.Add("pac_event_sequenced", function(ply, cmd, args) + + if not args[1] then return end + + local event = args[1] + local action = args[2] or "+" + local sequence_number = 0 + local set_target = args[3] or 1 + local found = false + + ply.pac_command_events = ply.pac_command_events or {} + ply.pac_command_events[event..1] = ply.pac_command_events[event..1] or {name = event..1, time = 0, on = 1} + + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} + + if not ply.pac_command_event_sequencebases[event] then + ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = 1} + end + + local target_number = 1 + local min = 1 + local max = ply.pac_command_event_sequencebases[event].max + + for i=1,100,1 do + if ply.pac_command_events[event..i] then + if ply.pac_command_events[event..i].on == 1 then + if sequence_number == 0 then sequence_number = i end + found = true + end + --elseif ply.pac_command_events[event..i] == nil then + ply.pac_command_events[event..i] = {name = event..i, time = 0, on = 0} + end + end + + if found then + if action == "+" or action == "forward" or action == "add" or action == "sequence+" or action == "advance" then + + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 0} + if sequence_number == max then + target_number = min + else target_number = sequence_number + 1 end + + pac.Message("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) + ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} + + RunConsoleCommand("pac_event", event..sequence_number, "0") + RunConsoleCommand("pac_event", event..target_number, "1") + + + elseif action == "-" or action == "backward" or action == "sub" or action == "sequence-" then + + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 0} + if sequence_number == min then + target_number = max + else target_number = sequence_number - 1 end + + print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) + ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} + + RunConsoleCommand("pac_event", event..sequence_number, "0") + RunConsoleCommand("pac_event", event..target_number, "1") + + elseif action == "set" then + print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. set_target .. " / " .. max) + + sequence_number = set_target or 1 + for i=1,100,1 do + ply.pac_command_events[event..i] = nil + end + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 1} + target_number = set_target + net.Start("pac_event_set_sequence") + net.WriteString(event) + net.WriteUInt(sequence_number,8) + net.SendToServer() + end + end +end) + + pac.AddHook("OnPlayerChat", "say_event", function(ply, str) if ply:IsValid() then ply.pac_say_event = {str = str, time = pac.RealTime} @@ -2032,39 +3829,217 @@ reload custom gesture --]] +local eventwheel_visibility_rule = CreateConVar("pac_eventwheel_visibility_rule" , "0", FCVAR_ARCHIVE, +"Different ways to filter your command events for the wheel.\n".. +"-1 ignores hide flags completely\n".. +"0 will hide a command if at least one event of one name has the \"hide in event wheel\" flag\n".. +"1 will hide a command only if ALL events of one name have the \"hide in event wheel\" flag\n".. +"2 will hide a command as soon as one event of a name is being hidden\n".. +"3 will hide a command only if ALL events of a name are being hidden\n".. +"4 will only show commands containing the following substrings, separated by spaces\n".. +"-4 will hide commands containing the following substrings, separated by spaces") + +local eventwheel_style = CreateConVar("pac_eventwheel_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel.\n0 is the default legacy style with one circle\n1 is the new style with colors, using one circle for the color and one circle for the activation indicator\n2 is an alternative style using a smaller indicator circle on the corner of the circle") +local eventlist_style = CreateConVar("pac_eventlist_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel list alternative.\n0 is like the default eventwheel legacy style with one indicator for the activation\n1 is the new style with colors, using one rectangle for the color and one rectangle for the activation indicator\n2 is an alternative style using a smaller indicator on the corner") + +local eventwheel_font = CreateConVar("pac_eventwheel_font", "DermaDefault", FCVAR_ARCHIVE, "pac3 eventwheel font. try pac_font_ such as pac_font_20 or pac_font_bold30. the pac fonts go up to 34") +local eventwheel_clickable = CreateConVar("pac_eventwheel_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel.\n-1 : not clickable, but activate on menu close\n0 : clickable, and activate on menu close\n1 : clickable, but doesn't activate on menu close") +local eventlist_clickable = CreateConVar("pac_eventlist_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel list alternative.\n-1 : not clickable, but activate a hovered event on menu close\n0 : clickable, and activate a hovered event on menu close\n1 : clickable, but doesn't do anything on menu close") + +local event_list_font_size = CreateConVar("pac_eventlist_fontsize", "12", FCVAR_ARCHIVE, "How big the font should be for the eventwheel's rectangle list counterpart.\nMight not work if the corresponding pac_font is missing") -- Custom event selector wheel do + local function get_events() + pace.command_colors = pace.command_colors or {} local available = {} + local names = {} + local args = string.Split(eventwheel_visibility_rule:GetString(), " ") + local uncolored_events = {} for k,v in pairs(pac.GetLocalParts()) do if v.ClassName == "event" then local e = v:GetEvent() if e == "command" then local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) - if hide then continue end + local this_event_hidden = v:IsHiddenBySomethingElse(false) + + + if not names[cmd] then + --wheel_hidden is the hide_in_eventwheel box + --possible_hidden is part hidden + names[cmd] = { + name = cmd, event = v, + + wheel_hidden = hide, + all_wheel_hidden = hide, + + possible_hidden = this_event_hidden, + all_possible_hidden = this_event_hidden, + } + else + --if already exists, we need to check counter examples for whether all members are hidden or hide_in_eventwheel + + if not hide then + names[cmd].all_wheel_hidden = false + end + + if not this_event_hidden then + names[cmd].all_possible_hidden = false + end - available[cmd] = {type = e, time = time} + if not names[cmd].wheel_hidden and hide then + names[cmd].wheel_hidden = true + end + + if not names[cmd].possible_hidden and this_event_hidden then + names[cmd].possible_hidden = true + end + + + end + + available[cmd] = {type = e, time = time, trigger = cmd} end end end + for cmd,v in pairs(names) do + uncolored_events[cmd] = not pace.command_colors[cmd] + local remove = false + + if args[1] == "-1" then --skip + remove = false + elseif args[1] == "0" then --one hide_in_eventwheel + if v.wheel_hidden then + remove = true + end + elseif args[1] == "1" then --all hide_in_eventwheel + if v.all_wheel_hidden then + remove = true + end + elseif args[1] == "2" then --one hidden + if v.possible_hidden then + remove = true + end + elseif args[1] == "3" then --all hidden + if v.all_possible_hidden then + remove = true + end + elseif args[2] then + if #args > 1 then --args contains many strings + local match = false + + for i=2, #args, 1 do + local str = args[i] + if string.find(cmd, str) then + match = true + end + end - local list = {} - for k,v in pairs(available) do - v.trigger = k - table.insert(list, v) + if args[1] == "4" and not match then + remove = true + elseif args[1] == "-4" and match then + remove = true + end + + else --why would you use the 4 or -4 mode if you didn't set keywords?? + remove = false + end + end + + if remove then + available[cmd] = nil + end end + + local list = {} + + if true then + local colors = {} + + for name,colstr in pairs(pace.command_colors) do + colors[colstr] = colors[colstr] or {} + colors[colstr][name] = available[name] + end + + + for col,tbl in pairs(colors) do + + local sublist = {} + for k,v in pairs(tbl) do + table.insert(sublist,available[k]) + end + + table.sort(sublist, function(a, b) return a.trigger < b.trigger end) + + for i,v in pairs(sublist) do + table.insert(list,v) + end + end + + local uncolored_sublist = {} - table.sort(list, function(a, b) return a.trigger > b.trigger end) + for k,v in pairs(available) do + if uncolored_events[k] then + table.insert(uncolored_sublist,available[k]) + end + end + + table.sort(uncolored_sublist, function(a, b) return a.trigger < b.trigger end) + for k,v in ipairs(uncolored_sublist) do + table.insert(list, v) + end + else + + for k,v in pairs(available) do + if k == names[k].name then + v.trigger = k + table.insert(list, v) + end + end + + table.sort(list, function(a, b) return a.trigger > b.trigger end) + end + return list end local selectorBg = Material("sgm/playercircle") local selected + local clicking = false + local open_btn + + + local clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1 + local close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0 + + local clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1 + local close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0 function pac.openEventSelectionWheel() + if not IsValid(open_btn) then open_btn = vgui.Create("DButton") end + open_btn:SetSize(80,30) + open_btn:SetText("Customize") + open_btn:SetPos(ScrW() - 80,0) + pace.command_event_menu_opened = nil + + function open_btn:DoClick() + + if (pace.command_event_menu_opened == nil) then + pace.ConfigureEventWheelMenu() + elseif IsValid(pace.command_event_menu_opened) then + pace.command_event_menu_opened:Remove() + end + + end + + open_btn:Show() + pace.command_colors = pace.command_colors or {} + clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1 + close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0 + gui.EnableScreenClicker(true) local scrw, scrh = ScrW(), ScrH() @@ -2129,6 +4104,13 @@ do local ply = pac.LocalPlayer local data = ply.pac_command_events and ply.pac_command_events[self.event.trigger] and ply.pac_command_events[self.event.trigger] + + + local d1 = 64 --indicator + + + local d2 = 50 --color + local indicator_color if data then local is_oneshot = self.event.time and self.event.time > 0 @@ -2136,28 +4118,84 @@ do local f = (pac.RealTime - data.time) / self.event.time local s = Lerp(math.Clamp(f,0,1), 1, 0) local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15) - surface.SetDrawColor(HSVToColor(210,s,v)) + indicator_color = HSVToColor(210,s,v) + else if data.on == 1 then - surface.SetDrawColor(HSVToColor(210,1,0.55)) + indicator_color = HSVToColor(210,1,0.55) else - surface.SetDrawColor(HSVToColor(210,0,0.15)) + indicator_color = HSVToColor(210,0,0.15) end end else - surface.SetDrawColor(HSVToColor(210,0,0.15)) + indicator_color = HSVToColor(210,0,0.15) end - surface.DrawTexturedRect(x-48, y-48, 96, 96) - draw.SimpleText(self.name, "DermaDefault", x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) + if eventwheel_style:GetInt() == 0 then + d2 = 96 + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2) + elseif eventwheel_style:GetInt() == 1 then + if pace.command_colors[self.name] then + local col_str_tbl = string.Split(pace.command_colors[self.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + + d1 = 100 --color + d2 = 50 --indicator + + surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1) + + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2) + + draw.RoundedBox(0,x-40,y-8,80,16,Color(0,0,0)) + + elseif eventwheel_style:GetInt() == 2 then + if pace.command_colors[self.name] then + local col_str_tbl = string.Split(pace.command_colors[self.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + + d1 = 96 --color + d2 = 40 --indicator + surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1) + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-1.2*d2, y-1.2*d2, d2, d2) + end + + draw.SimpleText(self.name, eventwheel_font:GetString(), x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) + cam.PopModelMatrix() end pac.AddHook("HUDPaint","custom_event_selector",function() -- Right clicking cancels if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionWheel(true) return end - + if input.IsButtonDown(MOUSE_LEFT) and not pace.command_event_menu_opened and not open_btn:IsHovered() and clickable then + + if not clicking and selected then + if not selected.event.time then + RunConsoleCommand("pac_event", selected.event.trigger, "toggle") + elseif selected.event.time > 0 then + RunConsoleCommand("pac_event", selected.event.trigger) + else + local ply = pac.LocalPlayer + + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then + RunConsoleCommand("pac_event", selected.event.trigger, "0") + else + RunConsoleCommand("pac_event", selected.event.trigger, "1") + end + end + end + clicking = true + else clicking = false end -- Normalize mouse vector from center of screen local x, y = input.GetCursorPos() x = x - scrw2 @@ -2184,10 +4222,11 @@ do end function pac.closeEventSelectionWheel(cancel) + open_btn:Hide() gui.EnableScreenClicker(false) pac.RemoveHook("HUDPaint","custom_event_selector") - if selected and cancel ~= true then + if selected and cancel ~= true and close_click then if not selected.event.time then RunConsoleCommand("pac_event", selected.event.trigger, "toggle") elseif selected.event.time > 0 then @@ -2205,6 +4244,270 @@ do selected = nil end + local panels = {} + function pac.openEventSelectionList() + if not IsValid(open_btn) then open_btn = vgui.Create("DButton") end + open_btn:SetSize(80,30) + open_btn:SetText("Customize") + open_btn:SetPos(ScrW() - 80,0) + pace.command_event_menu_opened = nil + + function open_btn:DoClick() + + if (pace.command_event_menu_opened == nil) then + pace.ConfigureEventWheelMenu() + elseif IsValid(pace.command_event_menu_opened) then + pace.command_event_menu_opened:Remove() + end + + end + + open_btn:Show() + pace.command_colors = pace.command_colors or {} + clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1 + close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0 + + local height = 2*event_list_font_size:GetInt() + 8 + panels = panels or {} + if not table.IsEmpty(panels) then + for i, v in pairs(panels) do + v:Remove() + end + end + local selections = {} + local events = get_events() + for i, v in ipairs(events) do + + + local list_element = vgui.Create("DPanel") + + panels[i] = list_element + list_element:SetSize(250,height) + list_element.event = v + + selections[i] = { + grow = 0, + name = v.trigger, + event = v, + pnl = list_element + } + function list_element:Paint() end + function list_element:DoCommand() + if not selected.event.time then + RunConsoleCommand("pac_event", selected.event.trigger, "toggle") + elseif selected.event.time > 0 then + RunConsoleCommand("pac_event", selected.event.trigger) + else + local ply = pac.LocalPlayer + + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then + RunConsoleCommand("pac_event", selected.event.trigger, "0") + else + RunConsoleCommand("pac_event", selected.event.trigger, "1") + end + end + end + function list_element:Think() + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_G) then + self:Remove() + end + if self:IsHovered() then + selected = self + if input.IsMouseDown(MOUSE_LEFT) and not self.was_clicked and not IsValid(pace.command_event_menu_opened) and not open_btn:IsHovered() and clickable2 then + self.was_clicked = true + self:DoCommand() + elseif not input.IsMouseDown(MOUSE_LEFT) then self.was_clicked = false end + end + end + + end + + gui.EnableScreenClicker(true) + + pac.AddHook("HUDPaint","custom_event_selector_list",function() + local height = 2*event_list_font_size:GetInt() + 8 + -- Right clicking cancels + if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionList(true) return end + + DisableClipping(true) + render.PushFilterMag(TEXFILTER.ANISOTROPIC) + render.PushFilterMin(TEXFILTER.ANISOTROPIC) + draw.SimpleText("Right click to cancel", "DermaDefault", ScrW()/2, ScrH()/2, color_red, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) + local x = 0 + local y = 0 + for i, v in ipairs(selections) do + if IsValid(v.pnl) then + + if y + height > ScrH() then + y = 0 + x = x + 200 + end + local list_element = v.pnl + list_element:SetPos(x,y) + list_element:SetSize(250,height) + + local ply = pac.LocalPlayer + local data = ply.pac_command_events and ply.pac_command_events[list_element.event.trigger] + local indicator_color + + if data then + local is_oneshot = list_element.event.time and list_element.event.time > 0 + + if is_oneshot then + local f = (pac.RealTime - data.time) / list_element.event.time + local s = Lerp(math.Clamp(f,0,1), 1, 0) + local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15) + + indicator_color = HSVToColor(210,s,v) + else + if data.on == 1 then + indicator_color = HSVToColor(210,1,0.55) + else + indicator_color = HSVToColor(210,0,0.15) + end + end + else + indicator_color = HSVToColor(210,0,0.15) + end + + local main_color = HSVToColor(210,0,0.15) + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + main_color = Color(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + end + + local hue, sat, lightness_value = ColorToHSL(main_color) + + + if eventlist_style:GetInt() == 0 then + surface.SetDrawColor(indicator_color) + surface.DrawRect(x,y,200,height) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x,y,200,height,2) + elseif eventlist_style:GetInt() == 1 then + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + surface.DrawRect(x,y,200,height) + + surface.SetDrawColor(indicator_color) + surface.DrawRect(x + 200/6,y + height/6,200 * 0.666,height * 0.666,2) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x,y,200,height,2) + + elseif eventlist_style:GetInt() == 2 then + surface.DrawOutlinedRect(x,y,200,height,2) + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + surface.DrawRect(x,y,200,height) + + surface.SetDrawColor(indicator_color) + surface.DrawRect(x + 150,y,50,height/2,2) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x + 150,y,50,height/2,2) + surface.DrawOutlinedRect(x,y,200,height,2) + end + + local text_color = Color(255,255,255) + if lightness_value > 0.5 and eventlist_style:GetInt() ~= 0 then + text_color = Color(0,0,0) + end + draw.SimpleText(v.name,"pac_font_" .. event_list_font_size:GetString(),x + 4,y + 4, text_color, TEXT_ALIGN_LEFT) + y = y + height + + end + + end + + render.PopFilterMag() + render.PopFilterMin() + DisableClipping(false) + + end) + end + + function pac.closeEventSelectionList(cancel) + open_btn:Hide() + gui.EnableScreenClicker(false) + pac.RemoveHook("HUDPaint","custom_event_selector_list") + + if IsValid(selected) and close_click2 then + if selected:IsHovered() then + selected:DoCommand() + end + end + for i,v in pairs(panels) do v:Remove() end + selected = nil + end + + concommand.Add("+pac_events", pac.openEventSelectionWheel) concommand.Add("-pac_events", pac.closeEventSelectionWheel) + + concommand.Add("+pac_events_list", pac.openEventSelectionList) + concommand.Add("-pac_events_list", pac.closeEventSelectionList) + end + +net.Receive("pac.SendPlayerObjUsed", function() + + local ply = net.ReadEntity() + local ent = net.ReadEntity() + local class = net.ReadString() + local override = net.ReadBool() + + if ply then + if ply:IsPlayer() and override then + ply.entity_inuse = ent + ply.entity_inuse_classname = class + end + end +end) + +net.Receive("pac.BroadcastDamageAttributions", function() + local ent = net.ReadEntity() + local tbl = net.ReadTable() + local kill = net.ReadBool() + if IsValid(ent) and tbl and IsValid(tbl.inflictor) then + ent.pac_damage_attributions = ent.pac_damage_attributions or {} + ent.pac_damage_attributions[tbl.attacker] = tbl + ent.pac_damage_attributions.latest = tbl + ent.pac_damage_attributions.is_kill = kill + if tbl.inflictor:GetClass() == "npc_grenade_frag" then + ent.pac_damage_attributions.IngoingGraceTime = CurTime() + end + end + +end) + +net.Receive("pac_update_healthbars", function() + local ent = net.ReadEntity() + local tbl = net.ReadTable() + + ent.pac_healthbars = tbl + + ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {} + ent.pac_healthbars_uidtotals = ent.pac_healthbars_uidtotals or {} + ent.pac_healthbars_total = 0 + + for layer=15,0,-1 do --go progressively inward in the layers + ent.pac_healthbars_layertotals[layer] = 0 + if tbl[layer] then + for uid,value in pairs(tbl[layer]) do --check the healthbars by uid + ent.pac_healthbars_uidtotals[uid] = value + ent.pac_healthbars_layertotals[layer] = ent.pac_healthbars_layertotals[layer] + value + ent.pac_healthbars_total = ent.pac_healthbars_total + value + end + else + ent.pac_healthbars_layertotals[layer] = nil + end + end + +end) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua new file mode 100644 index 000000000..52d31f0d7 --- /dev/null +++ b/lua/pac3/core/client/parts/force.lua @@ -0,0 +1,308 @@ +local BUILDER, PART = pac.PartTemplate("base_drawable") + +PART.ClassName = "force" +PART.Group = "advanced" +PART.Icon = "icon16/database_go.png" + +PART.ManualDraw = true +PART.HandleModifiersManually = true + +BUILDER:StartStorableVars() + :SetPropertyGroup("AreaShape") + :GetSet("HitboxMode", "Box", {enums = { + ["Box"] = "Box", + ["Cube"] = "Cube", + ["Sphere"] = "Sphere", + ["Cylinder"] = "Cylinder", + ["Cone"] = "Cone", + ["Ray"] = "Ray" + }}) + :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) + :GetSet("Radius", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) + :GetSet("Preview",false) + + :SetPropertyGroup("BaseForces") + :GetSet("BaseForce", 0) + :GetSet("AddedVectorForce", Vector(0,0,0)) + :GetSet("Torque", Vector(0,0,0)) + :GetSet("BaseForceAngleMode","Radial",{enums = {["Radial"] = "Radial", ["Locus"] = "Locus", ["Local"] = "Local"}}) + :GetSet("VectorForceAngleMode", "Global", {enums = {["Global"] = "Global", ["Local"] = "Local", ["Radial"] = "Radial", ["RadialNoPitch"] = "RadialNoPitch"}}) + :GetSet("TorqueMode", "TargetLocal", {enums = {["Global"] = "Global", ["TargetLocal"] = "TargetLocal", ["Local"] = "Local", ["Radial"] = "Radial"}}) + :GetSetPart("Locus", nil) + + :SetPropertyGroup("Behaviors") + :GetSet("Continuous", true, {description = "If set to false, the force will be a single, stronger impulse"}) + :GetSet("AccountMass", false, {description = "Apply acceleration according to mass."}) + :GetSet("Falloff", false, {description = "Whether the force to apply should fade with distance"}) + :GetSet("ReverseFalloff", false, {description = "The reverse of the falloff means the force fades when getting closer."}) + :GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part"}) + :GetSet("LevitationHeight", 0) + + :SetPropertyGroup("Damping") + :GetSet("Damping", 0, {editor_clamp = {0,1}, editor_sensitivity = 0.1}) + :GetSet("DampingFalloff", false, {description = "Whether the damping should fade with distance"}) + :GetSet("DampingReverseFalloff", false, {description = "Whether the damping should fade with distance but reverse"}) + + :SetPropertyGroup("Targets") + :GetSet("AffectSelf",false) + :GetSet("Players",true) + :GetSet("PhysicsProps", true) + :GetSet("PointEntities",true, {description = "other entities not covered by physics props but with potential physics"}) + :GetSet("NPC",false) +:EndStorableVars() + +local force_hitbox_ids = {["Box"] = 0,["Cube"] = 1,["Sphere"] = 2,["Cylinder"] = 3,["Cone"] = 4,["Ray"] = 5} +local base_force_mode_ids = {["Radial"] = 0, ["Locus"] = 1, ["Local"] = 2} +local vect_force_mode_ids = {["Global"] = 0, ["Local"] = 1, ["Radial"] = 2, ["RadialNoPitch"] = 3} +local ang_torque_mode_ids = {["Global"] = 0, ["TargetLocal"] = 1, ["Local"] = 2, ["Radial"] = 3} + +function PART:OnRemove() +end + +function PART:Initialize() + self.next_impulse = CurTime() + 0.05 + if not GetConVar("pac_sv_force"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("force parts are disabled on this server!") end +end + +function PART:OnShow() + self.next_impulse = CurTime() + 0.05 + self:Impulse(true) +end + +function PART:OnHide() + hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) + self:Impulse(false) +end + +function PART:OnRemove() + hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) + self:Impulse(false) +end + + +function PART:OnDraw() + self.pos,self.ang = self:GetDrawPosition() + if not self.Preview then hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end + + if self.Preview then + hook.Add("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID, function() + if self.HitboxMode == "Box" then + local mins = Vector(-self.Radius, -self.Radius, -self.Length) + local maxs = Vector(self.Radius, self.Radius, self.Length) + render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Sphere" then + render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.HitboxMode == "Cylinder" then + local obj = Mesh() + self:BuildCylinder(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Length ~= 0 and self.Radius ~= 0 then + local counter = 0 + --render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + for i=0,1,1/(math.abs(self.Length/self.Radius)) do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, self.Radius, 10, 10, Color( 255, 255, 255 ) ) + if counter == 200 then break end + counter = counter + 1 + end + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Cone" then + local obj = Mesh() + self:BuildCone(obj) + render.SetMaterial( Material( "models/wireframe" ) ) + mat = Matrix() + mat:Translate(self:GetWorldPosition()) + mat:Rotate(self:GetWorldAngles()) + cam.PushModelMatrix( mat ) + obj:Draw() + cam.PopModelMatrix() + if self.Radius ~= 0 then + local steps + steps = math.Clamp(4*math.ceil(self.Length / (self.Radius or 1)),1,50) + for i = 1,0,-1/steps do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + + steps = math.Clamp(math.ceil(self.Length / (self.Radius or 1)),1,4) + for i = 0,1/8,1/128 do + render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) + end + elseif self.Radius == 0 then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + elseif self.HitboxMode == "Ray" then + render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) + end + end) + end +end + + + +function PART:OnThink() + if self.Continuous and self.next_impulse < CurTime() then + self:Impulse(true) + end +end + +function PART:Impulse(on) + self.next_impulse = CurTime() + 0.05 + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + if not on and not self.Continuous then return end + if not GetConVar("pac_sv_force"):GetBool() then return end + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then return end + end + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end + + local locus_pos = Vector(0,0,0) + if self.Locus ~= nil then + if self.Locus:IsValid() then + locus_pos = self.Locus:GetWorldPosition() + end + else locus_pos = self:GetWorldPosition() end + + if self.BaseForce == 0 and not game.SinglePlayer() then + if math.abs(self.AddedVectorForce.x) < 10 and math.abs(self.AddedVectorForce.y) < 10 and math.abs(self.AddedVectorForce.z) < 10 then + if math.abs(self.Torque.x) < 10 and math.abs(self.Torque.y) < 10 and math.abs(self.Torque.z) < 10 then + return + end + end + end + + net.Start("pac_request_force", true) + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + net.WriteVector(locus_pos) + net.WriteBool(on) + + net.WriteString(string.sub(self.UniqueID,1,12)) + net.WriteEntity(self:GetRootPart():GetOwner()) + + net.WriteUInt(force_hitbox_ids[self.HitboxMode] or 0,4) + net.WriteUInt(base_force_mode_ids[self.BaseForceAngleMode] or 0,3) + net.WriteUInt(vect_force_mode_ids[self.VectorForceAngleMode] or 0,2) + net.WriteUInt(ang_torque_mode_ids[self.TorqueMode] or 0,2) + + net.WriteInt(self.Length, 16) + net.WriteInt(self.Radius, 16) + + net.WriteInt(self.BaseForce, 18) + net.WriteVector(self.AddedVectorForce) + net.WriteVector(self.Torque) + net.WriteUInt(self.Damping*1000, 10) + net.WriteInt(self.LevitationHeight,14) + + net.WriteBool(self.Continuous) + net.WriteBool(self.AccountMass) + net.WriteBool(self.Falloff) + net.WriteBool(self.ReverseFalloff) + net.WriteBool(self.DampingFalloff) + net.WriteBool(self.DampingReverseFalloff) + net.WriteBool(self.Levitation) + net.WriteBool(self.AffectSelf) + net.WriteBool(self.Players) + net.WriteBool(self.PhysicsProps) + net.WriteBool(self.NPC) + net.SendToServer() +end + + + +function PART:BuildCylinder(obj) + local sides = 30 + local circle_tris = {} + for i=1,sides,1 do + local vert1 = {pos = Vector(0, self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(0, self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + local vert3 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert4 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + + table.insert(circle_tris, vert4) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert3) + table.insert(circle_tris, vert4) + + end + obj:BuildFromTriangles( circle_tris ) +end + +function PART:BuildCone(obj) + local sides = 30 + local circle_tris = {} + local verttip = {pos = Vector(0,0,0), u = 0, v = 0 } + for i=1,sides,1 do + local vert1 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 } + local vert2 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 } + + table.insert(circle_tris, verttip) + table.insert(circle_tris, vert1) + table.insert(circle_tris, vert2) + + table.insert(circle_tris, vert2) + table.insert(circle_tris, vert1) + table.insert(circle_tris, verttip) + + --circle_tris[8*(i-1) + 1] = vert1 + --circle_tris[8*(i-1) + 2] = vert2 + --circle_tris[8*(i-1) + 3] = vert3 + --circle_tris[8*(i-1) + 4] = vert4 + --circle_tris[8*(i-1) + 5] = vert3 + --circle_tris[8*(i-1) + 6] = vert2 + end + obj:BuildFromTriangles( circle_tris ) +end + +function PART:SetRadius(val) + self.Radius = val + local sv_dist = GetConVar("pac_sv_force_max_radius"):GetInt() + if self.Radius > sv_dist then + self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetLength(val) + self.Length = val + local sv_dist = GetConVar("pac_sv_force_max_length"):GetInt() + if self.Length > sv_dist then + self:SetInfo("Your length is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetBaseForce(val) + self.BaseForce = val + local sv_max = GetConVar("pac_sv_force_max_amount"):GetInt() + if self.BaseForce > sv_max then + self:SetInfo("Your base force is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua new file mode 100644 index 000000000..dae8d0b8c --- /dev/null +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -0,0 +1,202 @@ +local BUILDER, PART = pac.PartTemplate("base") + +PART.ClassName = "health_modifier" + +PART.Group = "advanced" +PART.Icon = "icon16/heart.png" + +BUILDER:StartStorableVars() + + BUILDER:SetPropertyGroup("Health") + BUILDER:GetSet("ChangeHealth", false) + BUILDER:GetSet("FollowHealth", true, {description = "whether changing the max health should try to set your health at the same time"}) + BUILDER:GetSet("MaxHealth", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) + + BUILDER:SetPropertyGroup("ExtraHpBars") + BUILDER:GetSet("FollowHealthBars", true, {description = "whether changing the extra health bars should try to update them at the same time"}) + BUILDER:GetSet("HealthBars", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,100)) end}) + BUILDER:GetSet("BarsAmount", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) + BUILDER:GetSet("BarsLayer", 1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,15)) end}) + BUILDER:GetSet("AbsorbFactor", 0, {editor_onchange = function(self,num) return math.Clamp(num,-1,1) end}) + BUILDER:GetSet("HPBarsResetOnHide", false) + + BUILDER:SetPropertyGroup("Armor") + BUILDER:GetSet("ChangeArmor", false) + BUILDER:GetSet("FollowArmor", true, {description = "whether changing the max armor should try to set your armor at the same time"}) + BUILDER:GetSet("MaxArmor", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) + + BUILDER:SetPropertyGroup("DamageMultipliers") + BUILDER:GetSet("DamageMultiplier", 1) + BUILDER:GetSet("ModifierId", "") + BUILDER:GetSet("MultiplierResetOnHide", false) + +BUILDER:EndStorableVars() + + +function PART:SendModifier(str) + if self:IsHidden() then return end + if LocalPlayer() ~= self:GetPlayerOwner() then return end + if not GetConVar("pac_sv_health_modifier"):GetBool() then return end + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then return end + end + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end + + if str == "MaxHealth" and self.ChangeHealth then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("MaxHealth") + net.WriteUInt(self.MaxHealth, 32) + net.WriteBool(self.FollowHealth) + net.SendToServer() + elseif str == "MaxArmor" and self.ChangeArmor then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("MaxArmor") + net.WriteUInt(self.MaxArmor, 32) + net.WriteBool(self.FollowArmor) + net.SendToServer() + elseif str == "DamageMultiplier" then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("DamageMultiplier") + net.WriteFloat(self.DamageMultiplier) + net.WriteBool(true) + net.SendToServer() + elseif str == "HealthBars" then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("HealthBars") + net.WriteUInt(self.HealthBars, 32) + net.WriteUInt(self.BarsAmount, 32) + net.WriteUInt(self.BarsLayer, 4) + net.WriteFloat(self.AbsorbFactor) + net.WriteBool(self.FollowHealthBars) + net.SendToServer() + + elseif str == "all" then + self:SendModifier("MaxHealth") + self:SendModifier("MaxArmor") + self:SendModifier("DamageMultiplier") + self:SendModifier("HealthBars") + end +end + +function PART:SetHealthBars(val) + self.HealthBars = val + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + self:SendModifier("HealthBars") +end + +function PART:SetBarsAmount(val) + self.BarsAmount = val + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + self:SendModifier("HealthBars") +end + +function PART:SetBarsLayer(val) + self.BarsLayer = val + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + self:SendModifier("HealthBars") +end + +function PART:SetMaxHealth(val) + self.MaxHealth = val + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + self:SendModifier("MaxHealth") +end + +function PART:SetMaxArmor(val) + self.MaxArmor = val + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + self:SendModifier("MaxArmor") +end + +function PART:SetDamageMultiplier(val) + self.DamageMultiplier = val + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + self:SendModifier("DamageMultiplier") + local sv_min = GetConVar("pac_sv_health_modifier_min_damagescaling"):GetInt() + if self.DamageMultiplier < sv_min then + self:SetInfo("Your damage scaling is beyond the server's minimum permitted! Server minimum is " .. sv_min) + else + self:SetInfo(nil) + end +end + +function PART:OnRemove() + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + local found_remaining_healthmod = false + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "health_modifier" and part ~= self then + found_remaining_healthmod = true + end + end + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("OnRemove") + net.WriteFloat(0) + net.WriteBool(true) + net.SendToServer() + + if not found_remaining_healthmod then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("MaxHealth") + net.WriteUInt(100,32) + net.WriteBool(true) + net.SendToServer() + + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("MaxArmor") + net.WriteUInt(100,32) + net.WriteBool(false) + net.SendToServer() + end +end + +function PART:OnShow() + self:SendModifier("all") +end + +function PART:OnHide() + if self.HPBarsResetOnHide then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("HealthBars") + net.WriteUInt(0, 32) + net.WriteUInt(0, 32) + net.WriteUInt(self.BarsLayer, 4) + net.WriteFloat(1) + net.WriteBool(self.FollowHealthBars) + net.SendToServer() + end + if self.MultiplierResetOnHide then + net.Start("pac_request_healthmod") + net.WriteString(self.UniqueID) + net.WriteString(self.ModifierId) + net.WriteString("DamageMultiplier") + net.WriteFloat(1) + net.WriteBool(true) + net.SendToServer() + end +end + +function PART:Initialize() + if not GetConVar("pac_sv_health_modifier"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("health modifiers are disabled on this server!") end +end + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua new file mode 100644 index 000000000..7b211ec94 --- /dev/null +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -0,0 +1,252 @@ +language.Add("pac_hitscan", "Hitscan") +--local vector_origin = vector_origin +--local angle_origin = Angle(0,0,0) +--local WorldToLocal = WorldToLocal + +local BUILDER, PART = pac.PartTemplate("base_drawable") + +PART.ClassName = "hitscan" +PART.Group = "advanced" +PART.Icon = "icon16/user_gray.png" + +BUILDER:StartStorableVars() + :GetSet("ServerBullets", true, {description = "serverside bullets can do damage and exert a physical impact force"}) + :SetPropertyGroup("bullet properties") + :GetSet("BulletImpact", false) + :GetSet("Damage", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,268435455)) end}) + :GetSet("Force",1000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end}) + :GetSet("AffectSelf", false, {description = "whether to allow to damage yourself"}) + :GetSet("DamageFalloff", false, {description = "enable damage falloff. The lowest damage is not a fixed damage number, but a fraction of the total initial damage.\nThe server can still restrict the maximum distance of all bullets"}) + :GetSet("DamageFalloffDistance", 5000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end}) + :GetSet("DamageFalloffFraction", 0.5, {editor_clamp = {0,1}}) + :GetSet("DamageType", "generic", {enums = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamage + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + } + }) + :GetSet("Spread", 0) + :GetSet("SpreadX", 1) + :GetSet("SpreadY", 1) + :GetSet("NumberBullets", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,511)) end}) + :GetSet("DistributeDamage", false, {description = "whether or not the damage should be divided equally to all bullets in NumberBullets.\nThe server can still force multi-shots to do that"}) + :GetSet("TracerSparseness", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,255)) end}) + :GetSet("MaxDistance", 10000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end}) + :GetSet("TracerName", "Tracer", {enums = { + ["Default bullet tracer"] = "Tracer", + ["AR2 pulse-rifle tracer"] = "AR2Tracer", + ["Helicopter tracer"] = "HelicopterTracer", + ["Airboat gun tracer"] = "AirboatGunTracer", + ["Airboat gun heavy tracer"] = "AirboatGunHeavyTracer", + ["Gauss tracer"] = "GaussTracer", + ["Hunter tracer"] = "HunterTracer", + ["Strider tracer"] = "StriderTracer", + ["Gunship tracer"] = "GunshipTracer", + ["Toolgun tracer"] = "ToolTracer", + ["Laser tracer"] = "LaserTracer" + }}) + +BUILDER:EndStorableVars() + +function PART:Initialize() + self.bulletinfo = {} + if not GetConVar("pac_sv_hitscan"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("hitscan parts are disabled on this server!") end +end + +function PART:OnShow() + self:Shoot() +end + +function PART:OnDraw() + self:GetWorldPosition() + self:GetWorldAngles() +end + +function PART:Shoot() + if self.NumberBullets == 0 then return end + if self.ServerBullets and self.Damage ~= 0 then + self:SendNetMessage() + else + self.bulletinfo.Attacker = self:GetRootPart():GetOwner() + self.ent = self:GetRootPart():GetOwner() + if self.Damage ~= 0 then self.bulletinfo.Damage = self.Damage end + + self.bulletinfo.Src = self:GetWorldPosition() + self.bulletinfo.Dir = self:GetWorldAngles():Forward() + self.bulletinfo.Spread = Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0) + + self.bulletinfo.Force = self.Force + self.bulletinfo.Distance = self.MaxDistance + self.bulletinfo.Num = self.NumberBullets + self.bulletinfo.Tracer = self.TracerSparseness --tracer every x bullets + self.bulletinfo.TracerName = self.TracerName + self.bulletinfo.DistributeDamage = self.DistributeDamage + + self.bulletinfo.DamageFalloff = self.DamageFalloff + self.bulletinfo.DamageFalloffDistance = self.DamageFalloffDistance + self.bulletinfo.DamageFalloffFraction = self.DamageFalloffFraction + + if IsValid(self.ent) then self.ent:FireBullets(self.bulletinfo) end + end +end + + +--NOT THE ACTUAL DAMAGE TYPES. UNIQUE IDS TO COMPRESS NET MESSAGES +local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + fire = 31, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 32, + dissolve_heavy_electrical = 33, + dissolve_light_electrical = 34, + dissolve_core_effect = 35, + + heal = 36, + armor = 37, +} + +local tracer_ids = { + ["Tracer"] = 1, + ["AR2Tracer"] = 2, + ["HelicopterTracer"] = 3, + ["AirboatGunTracer"] = 4, + ["AirboatGunHeavyTracer"] = 5, + ["GaussTracer"] = 6, + ["HunterTracer"] = 7, + ["StriderTracer"] = 8, + ["GunshipTracer"] = 9, + ["ToolgunTracer"] = 10, + ["LaserTracer"] = 11 +} + + +function PART:SendNetMessage() + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + if not GetConVar('pac_sv_hitscan'):GetBool() then return end + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} + if pac.Blocked_Combat_Parts[self.ClassName] then + return + end + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end + + net.Start("pac_hitscan", true) + net.WriteBool(self.AffectSelf) + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + + net.WriteUInt(damage_ids[self.DamageType] or 0,7) + net.WriteVector(Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0)) + net.WriteUInt(self.Damage, 28) + net.WriteUInt(self.TracerSparseness, 8) + net.WriteUInt(self.Force, 16) + net.WriteUInt(self.MaxDistance, 16) + net.WriteUInt(self.NumberBullets, 9) + net.WriteUInt(tracer_ids[self.TracerName], 4) + net.WriteBool(self.DistributeDamage) + + net.WriteBool(self.DamageFalloff) + net.WriteUInt(self.DamageFalloffDistance, 16) + net.WriteUInt(math.Clamp(math.floor(self.DamageFalloffFraction * 1000),0, 1000), 10) + + net.WriteString(string.sub(self.UniqueID,1,8)) + + net.SendToServer() +end + +function PART:SetDamage(val) + self.Damage = val + local sv_max = GetConVar("pac_sv_hitscan_max_damage"):GetInt() + if self.Damage > sv_max then + self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + +function PART:SetNumberBullets(val) + self.NumberBullets = val + local sv_max = GetConVar("pac_sv_hitscan_max_bullets"):GetInt() + if self.NumberBullets > sv_max then + self:SetInfo("Your bullet count is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + + +BUILDER:Register() + diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua new file mode 100644 index 000000000..e51648a93 --- /dev/null +++ b/lua/pac3/core/client/parts/lock.lua @@ -0,0 +1,523 @@ +include("pac3/extra/shared/net_combat.lua") +--pac3/extra/shared/net_combat.lua + + +local pac = pac +local Vector = Vector +local Angle = Angle +local NULL = NULL +local Matrix = Matrix + +local physics_point_ent_classes = { + ["prop_physics"] = true, + ["prop_physics_multiplayer"] = true, + ["prop_ragdoll"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true +} + + +local BUILDER, PART = pac.PartTemplate("base_movable") + +PART.ClassName = "lock" +PART.Group = "advanced" +PART.Icon = "icon16/lock.png" + + +BUILDER:StartStorableVars() + :SetPropertyGroup("Behaviour") + :GetSet("Mode", "None", {enums = {["None"] = "None", ["Grab"] = "Grab", ["Teleport"] = "Teleport"}}) + :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"}) + :GetSet("RelativeGrab", false) + :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before self.grabbing are re-applied"}) + + :SetPropertyGroup("DetectionOrigin") + :GetSet("Radius", 20) + :GetSet("OffsetDownAmount", 0, {description = "Lowers the detect origin by some amount"}) + :GetSetPart("TargetPart") + :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."}) + :GetSet("Preview", false) + + :SetPropertyGroup("TeleportSafety") + :GetSet("ClampDistance", false, {description = "Prevents the teleport from going too far (By Radius amount). For example, if you use hitpos bone on a pac model, it can act as a safety in case the raycast falls out of bounds."}) + :GetSet("SlopeSafety", false, {description = "Teleports a bit up in case you end up on a slope and get stuck."}) + + :SetPropertyGroup("PlayerCameraOverride") + :GetSet("OverrideEyeAngles", false, {description = "Whether the part will try to override players' eye angles. Requires OverrideAngles and user consent"}) + :GetSet("OverrideEyePosition", false, {description = "Whether the part will try to override players' view position to a selected base_movable part with a CalcView hook as well. Requires OverrideEyeAngles, OverrideAngles, a valid base_movable OverrideEyePositionPart and user consent"}) + :GetSetPart("OverrideEyePositionPart") + :GetSet("DrawLocalPlayer", true, {description = "Whether the resulting calcview will draw the target player as in third person, otherwise hide the player"}) + + :SetPropertyGroup("Targets") + :GetSet("AffectPlayerOwner", false) + :GetSet("Players", false) + :GetSet("PhysicsProps", false) + :GetSet("NPC", false) + + +BUILDER:EndStorableVars() + +function PART:OnThink() + + if not GetConVar('pac_sv_lock'):GetBool() then return end + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then return end + end + + if self.forcebreak then + if self.next_allowed_grab < CurTime() then --we're able to resume + if self.ContinuousSearch then + self.forcebreak = false + else + --wait for the next showing to reset the search because we have self.resetcondition + end + else + return + end + end + + if self.Mode == "Grab" then + if not GetConVar('pac_sv_lock_grab'):GetBool() then return end + if pac.Blocked_Combat_Parts then + if pac.Blocked_Combat_Parts[self.ClassName] then + return + end + end + if self.ContinuousSearch then + self:DecideTarget() + end + self:CheckEntValidity() + if self.valid_ent then + local final_ang = Angle(0,0,0) + if self.OverrideAngles then --if overriding angles + if self.is_first_time then + self.default_ang = self.target_ent:GetAngles() --record the initial ent angles + end + if self.OverrideEyeAngles then self.default_ang.y = self:GetWorldAngles().y end --if we want to override players eye angles we will keep recording the yaw + + elseif not self.grabbing then + self.default_ang = self.target_ent:GetAngles() --record the initial ent angles anyway + end + + local relative_transform_matrix = Matrix() + relative_transform_matrix:Identity() + + if self.RelativeGrab then + if self.is_first_time then self:CalculateRelativeOffset() end + relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity() + else + relative_transform_matrix = Matrix() + relative_transform_matrix:Identity() + end + + local offset_matrix = Matrix() + offset_matrix:Translate(self:GetWorldPosition()) + offset_matrix:Rotate(self:GetWorldAngles()) + offset_matrix:Mul(relative_transform_matrix) + + local relative_offset_pos = offset_matrix:GetTranslation() + local relative_offset_ang = offset_matrix:GetAngles() + + if pac.LocalPlayer == self:GetPlayerOwner() then + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end + net.Start("pac_request_position_override_on_entity_grab") + net.WriteBool(self.is_first_time) + net.WriteString(self.UniqueID) + if self.RelativeGrab then + net.WriteVector(relative_offset_pos) + net.WriteAngle(relative_offset_ang) + else + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + end + end + + local try_override_eyeang = false + if self.target_ent:IsPlayer() then + if self.OverrideEyeAngles then try_override_eyeang = true end + end + if pac.LocalPlayer == self:GetPlayerOwner() then + net.WriteBool(self.OverrideAngles) + net.WriteBool(try_override_eyeang) + net.WriteEntity(self.target_ent) + net.WriteEntity(self:GetRootPart():GetOwner()) + local can_calcview = false + if self.OverrideEyePosition and IsValid(self.OverrideEyePositionPart) then + if self.OverrideEyePositionPart.GetWorldAngles then + can_calcview = true + end + end + net.WriteBool(can_calcview) + --print(IsValid(self.OverrideEyePositionPart), self.OverrideEyeAngles) + if can_calcview then + net.WriteVector(self.OverrideEyePositionPart:GetWorldPosition()) + net.WriteAngle(self.OverrideEyePositionPart:GetWorldAngles()) + else + net.WriteVector(self:GetWorldPosition()) + net.WriteAngle(self:GetWorldAngles()) + end + net.WriteBool(self.DrawLocalPlayer) + net.SendToServer() + end + --print(self:GetRootPart():GetOwner()) + if self.Players and self.target_ent:IsPlayer() and self.OverrideAngles then + local mat = Matrix() + mat:Identity() + + if self.OverrideAngles then + final_ang = self:GetWorldAngles() + end + if self.OverrideEyeAngles then + final_ang = self:GetWorldAngles() + --final_ang = Angle(0,180,0) + --print("chose part ang") + end + if self.OverrideEyePosition and can_calcview then + final_ang = self.OverrideEyePositionPart:GetWorldAngles() + --print("chose alt part ang") + end + + local eyeang = self.target_ent:EyeAngles() + --print("eyeang", eyeang) + eyeang.p = 0*eyeang.p + eyeang.y = eyeang.y + eyeang.r = 0*eyeang.r + mat:Rotate(final_ang - eyeang) --this works + --mat:Rotate(eyeang) + --print("transform ang", final_ang) + --print("part's angles", self:GetWorldAngles()) + --mat:Rotate(self:GetWorldAngles()) + + + self.target_ent:EnableMatrix("RenderMultiply", mat) + + end + + self.grabbing = true + self.teleported = false + end + end + --if self.is_first_time then print("lock " .. self.UniqueID .. "did its first clock") end + self.is_first_time = false +end + +do + function PART:BreakLock(ent) + self.forcebreak = true + self.next_allowed_grab = CurTime() + 3 + self.target_ent.IsGrabbedID = nil + self.target_ent = nil + self.grabbing = false + pac.Message(Color(255, 50, 50), "lock break result:") + MsgC(Color(0,255,255), "\t", self) MsgC(Color(200, 200, 200), " in your group ") MsgC(Color(0,255,255), self:GetRootPart(),"\n") + MsgC(Color(200, 200, 200), "\tIt will now be in the forcebreak state until the next allowed grab, 3 seconds from now\nalso this entity can't be grabbed for 10 seconds.\n") + if not self.ContinuousSearch then + self.resetcondition = true + end + + ent:SetGravity(1) + + ent.pac_recently_broken_free_from_lock = CurTime() + ent:DisableMatrix("RenderMultiply") + end + net.Receive("pac_request_lock_break", function(len) + --[[format: + net.Start("pac_request_lock_break") + net.WriteEntity(ply) --the breaker + net.WriteString("") --the uid if applicable + net.Send(ent) --that's us! the locker + ]] + local target_to_release = net.ReadEntity() + local uid = net.ReadString() + local reason = net.ReadString() + pac.Message(Color(255, 255, 255), "------------CEASE AND DESIST!------------") + MsgC(Color(0,255,255), tostring(target_to_release)) MsgC(Color(255,50,50), " WANTS TO BREAK FREE!!\n") + MsgC(Color(255,50,50), "reason:") MsgC(Color(0,255,255), reason .."\n") + + if uid ~= "" then --if a uid is provided + MsgC(Color(255, 50, 50), "AND IT KNOWS YOUR UID! " .. uid .. "\n") + local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid) + if part then + if part.ClassName == "lock" then + part:BreakLock(target_to_release) + + end + end + + else + MsgC(Color(200, 200, 200), "NOW! WE SEARCH YOUR LOCAL PARTS!\n") + for i,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "lock" then + if part.grabbing then + if IsValid(part.target_ent) and part.target_ent == target_to_release then + part:BreakLock(target_to_release) + end + end + end + end + end + + end) + + net.Receive("pac_mark_grabbed_ent", function(len) + + local target_to_mark = net.ReadEntity() + local successful_grab = net.ReadBool() + local uid = net.ReadString() + local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid) + --print(target_to_mark,"is grabbed by",uid) + + if not successful_grab then + part:BreakLock(target_to_mark) --yes we will employ the aggressive lock break here + else + target_to_mark.IsGrabbed = successful_grab + target_to_mark.IsGrabbedID = uid + target_to_mark:SetGravity(0) + end + + end) +end + +function PART:SetRadius(val) + self.Radius = val + local sv_dist = GetConVar("pac_sv_lock_max_grab_radius"):GetInt() + if self.Radius > sv_dist then + self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:OnShow() + + local origin_part + self.is_first_time = true + if self.resetting_condition or self.forcebreak then + if self.next_allowed_grab < CurTime() then + self.forcebreak = false + self.resetting_condition = false + end + end + hook.Add("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID, function() + if self.TargetPart:IsValid() then + origin_part = self.TargetPart + else + origin_part = self + end + if origin_part == nil or not self.Preview or pac.LocalPlayer ~= self:GetPlayerOwner() then return end + local sv_dist = GetConVar("pac_sv_lock_max_grab_radius"):GetInt() + + render.DrawLine(origin_part:GetWorldPosition(),origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount),Color(255,255,255)) + + if self.Radius < sv_dist then + self:SetInfo(nil) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), sv_dist, 30, 30, Color(50,50,150),true) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), self.Radius, 30, 30, Color(255,255,255),true) + else + self:SetInfo("Your radius is beyond the server max! Active max is " .. sv_dist) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), sv_dist, 30, 30, Color(0,255,255),true) + render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), self.Radius, 30, 30, Color(100,100,100),true) + end + + end) + if self.Mode == "Teleport" then + if not GetConVar('pac_sv_lock_teleport'):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then return end + self.target_ent = nil + + local ang_yaw_only = self:GetWorldAngles() + ang_yaw_only.p = 0 + ang_yaw_only.r = 0 + if pac.LocalPlayer == self:GetPlayerOwner() then + + local teleport_pos_final = self:GetWorldPosition() + + if self.ClampDistance then + local ply_pos = self:GetPlayerOwner():GetPos() + local pos = self:GetWorldPosition() + + if pos:Distance(ply_pos) > self.Radius then + local clamped_pos = ply_pos + (pos - ply_pos):GetNormalized()*self.Radius + teleport_pos_final = clamped_pos + end + end + if self.SlopeSafety then teleport_pos_final = teleport_pos_final + Vector(0,0,30) end + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end + timer.Simple(0, function() + if self:IsHidden() or self:IsDrawHidden() then return end + net.Start("pac_request_position_override_on_entity_teleport") + net.WriteString(self.UniqueID) + net.WriteVector(teleport_pos_final) + net.WriteAngle(ang_yaw_only) + net.WriteBool(self.OverrideAngles) + net.SendToServer() + end) + + end + self.grabbing = false + elseif self.Mode == "Grab" then + self:DecideTarget() + self:CheckEntValidity() + end +end + +function PART:OnHide() + hook.Remove("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID) + self.teleported = false + self.grabbing = false + if self.target_ent == nil then return + else self.target_ent.IsGrabbed = false self.target_ent.IsGrabbedID = nil end + self:reset_ent_ang() +end + +function PART:reset_ent_ang() + if self.target_ent == nil then return end + local reset_ent = self.target_ent + + if reset_ent:IsValid() then + timer.Simple(math.min(self.RestoreDelay,5), function() + if pac.LocalPlayer == self:GetPlayerOwner() then + if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end + end + net.Start("pac_request_angle_reset_on_entity") + net.WriteAngle(Angle(0,0,0)) + net.WriteFloat(self.RestoreDelay) + net.WriteEntity(reset_ent) + net.WriteEntity(self:GetPlayerOwner()) + net.SendToServer() + end + if self.Players and reset_ent:IsPlayer() then + reset_ent:DisableMatrix("RenderMultiply") + end + end) + end +end + +function PART:OnRemove() +end + +function PART:DecideTarget() + + local RADIUS = math.Clamp(self.Radius,0,GetConVar("pac_sv_lock_max_grab_radius"):GetInt()) + local ents_candidates = {} + local chosen_ent = nil + local target_part = self.TargetPart + local origin + + if self.TargetPart and (self.TargetPart):IsValid() then + origin = (self.TargetPart):GetWorldPosition() + else + origin = self:GetWorldPosition() + end + origin:Add(Vector(0,0,-self.OffsetDownAmount)) + + for i, ent_candidate in ipairs(ents.GetAll()) do + + if IsValid(ent_candidate) then + local check_further = true + if ent_candidate.pac_recently_broken_free_from_lock then + if ent_candidate.pac_recently_broken_free_from_lock + 10 > CurTime() then + check_further = false + end + else check_further = true end + + if check_further then + if ent_candidate:GetPos():Distance( origin ) < RADIUS then + if self.Players and ent_candidate:IsPlayer() then + --we don't want to grab ourselves + if (ent_candidate ~= self:GetRootPart():GetOwner()) or (self.AffectPlayerOwner and ent_candidate == self:GetPlayerOwner()) then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + elseif (self:GetPlayerOwner() ~= ent_candidate) then --if it's another player, good + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + end + elseif self.PhysicsProps and (physics_point_ent_classes[ent_candidate:GetClass()] or string.find(ent_candidate:GetClass(),"item_") or string.find(ent_candidate:GetClass(),"ammo_")) then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + elseif self.NPC and (ent_candidate:IsNPC() or ent_candidate.IsDrGEntity or ent_candidate.IsVJBaseSNPC) then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + end + end + end + end + end + local closest_distance = math.huge + + --sort for the closest + for i,ent_candidate in ipairs(ents_candidates) do + local test_distance = (ent_candidate:GetPos()):Distance( self:GetWorldPosition()) + if (test_distance < closest_distance) then + closest_distance = test_distance + chosen_ent = ent_candidate + end + end + + if chosen_ent ~= nil then + self.target_ent = chosen_ent + if pac.LocalPlayer == self:GetPlayerOwner() then + print("selected ", chosen_ent, "dist ", (chosen_ent:GetPos()):Distance( self:GetWorldPosition() )) + end + self.valid_ent = true + else + self.target_ent = nil + self.valid_ent = false + end +end + + + +function PART:CheckEntValidity() + + if self.target_ent == nil then + self.valid_ent = false + elseif self.target_ent:EntIndex() == 0 then + self.valid_ent = false + elseif IsValid(self.target_ent) then + self.valid_ent = true + end + if self.target_ent ~= nil then + if self.target_ent.IsGrabbedID and self.target_ent.IsGrabbedID ~= self.UniqueID then self.valid_ent = false end + end + if not self.valid_ent then self.target_ent = nil end + --print("ent check:",self.valid_ent) +end + +function PART:CalculateRelativeOffset() + if self.target_ent == nil or not IsValid(self.target_ent) then self.relative_transform_matrix = Matrix() return end + self.relative_transform_matrix = Matrix() + self.relative_transform_matrix:Rotate(self.target_ent:GetAngles() - self:GetWorldAngles()) + self.relative_transform_matrix:Translate(self.target_ent:GetPos() - self:GetWorldPosition()) + --print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles()) +end + +function PART:Initialize() + + if not GetConVar('pac_sv_lock_grab'):GetBool() then + if not GetConVar('pac_sv_lock_teleport'):GetBool() then + self:SetWarning("lock part grabs and teleports are disabled on this server!") + else + self:SetWarning("lock part grabs are disabled on this server!") + end + end + if not GetConVar('pac_sv_lock_teleport'):GetBool() then + if not GetConVar('pac_sv_lock_grab'):GetBool() then + self:SetWarning("lock part grabs and teleports are disabled on this server!") + else + self:SetWarning("lock part teleports are disabled on this server!") + end + end + if not GetConVar('pac_sv_lock'):GetBool() then self:SetError("lock parts are disabled on this server!") end +end + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/movement.lua b/lua/pac3/core/client/parts/movement.lua index e5fb4de5e..94273c9d5 100644 --- a/lua/pac3/core/client/parts/movement.lua +++ b/lua/pac3/core/client/parts/movement.lua @@ -16,7 +16,8 @@ local function ADD(PART, name, default, ...) PART["Set" .. name] = function(self, val) self[name] = val - local ply = self:GetRootPart():GetOwner() + local ply = self:GetPlayerOwner() + --if ply:GetClass() == "viewmodel" then ply = self:GetRootPart():GetOwner() end if ply == pac.LocalPlayer then @@ -44,6 +45,8 @@ BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") ADD(PART, "Noclip", false) ADD(PART, "Gravity", Vector(0, 0, -600)) + ADD(PART, "Mass", 85) + BUILDER:GetSet("PreserveInFirstPerson", false, {description = "keeps the movement modification active in first person"}) BUILDER:SetPropertyGroup("movement") ADD(PART, "SprintSpeed", 400) @@ -60,7 +63,10 @@ BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("air") ADD(PART, "AllowZVelocity", false) ADD(PART, "AirFriction", 0.01, {editor_clamp = {0, 1}, editor_sensitivity = 0.1}) - ADD(PART, "MaxAirSpeed", 1) + ADD(PART, "HorizontalAirFrictionMultiplier", 1, {editor_clamp = {0, 1}, editor_sensitivity = 0.1}) + ADD(PART, "MaxAirSpeed", 750) + ADD(PART, "StrafingStrengthMultiplier", 1) + BUILDER:SetPropertyGroup("view angles") ADD(PART, "ReversePitch", false) @@ -94,9 +100,10 @@ function PART:GetNiceName() end function PART:OnShow() - local ent = self:GetRootPart():GetOwner() - - if ent:IsValid() then + local ent = self:GetPlayerOwner() + + if ent:IsValid() then + if ent:GetClass() == "viewmodel" then ent = self:GetPlayerOwner() end ent.last_movement_part = self:GetUniqueID() for i,v in ipairs(update_these) do v(self) @@ -105,12 +112,24 @@ function PART:OnShow() end function PART:OnHide() - local ent = self:GetRootPart():GetOwner() - + local ent = self:GetRootPart():GetOwner() or self:GetOwner() or self:GetPlayerOwner() + if not IsValid(ent) then return end + if ent:GetClass() == "viewmodel" then ent = self:GetPlayerOwner() end + if not self:IsHidden() and self.PreserveInFirstPerson then return end if ent == pac.LocalPlayer and ent.last_movement_part == self:GetUniqueID() then net.Start("pac_modify_movement", true) net.WriteString("disable") net.SendToServer() + ent.pac_movement = nil + end +end + +function PART:OnRemove() + local ent = self:GetRootPart():GetOwner() + if ent == pac.LocalPlayer then + net.Start("pac_modify_movement", true) + net.WriteString("disable") + net.SendToServer() ent.pac_movement = nil end diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index b5b35b4d8..deb107e86 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -1,93 +1,120 @@ +local physprop_enums = {} +local physprop_indices = {} +for i=0,200,1 do + local name = util.GetSurfacePropName(i) + if name ~= "" then + physprop_enums[name] = name + physprop_indices[name] = i + end +end + language.Add("pac_projectile", "Projectile") + + local BUILDER, PART = pac.PartTemplate("base_movable") PART.ClassName = "projectile" -PART.Group = 'advanced' -PART.Icon = 'icon16/bomb.png' +PART.Group = "advanced" +PART.Icon = "icon16/bomb.png" BUILDER:StartStorableVars() - BUILDER:GetSet("Speed", 1) - BUILDER:GetSet("AddOwnerSpeed", false) - BUILDER:GetSet("Damping", 0) - BUILDER:GetSet("Gravity", true) - BUILDER:GetSet("Collisions", true) - BUILDER:GetSet("Sphere", false) - BUILDER:GetSet("Radius", 1) - BUILDER:GetSet("DamageRadius", 50) - BUILDER:GetSet("LifeTime", 5) - BUILDER:GetSet("AimDir", false) - BUILDER:GetSet("Sticky", false) - BUILDER:GetSet("Bounce", 0) - BUILDER:GetSet("BulletImpact", false) - BUILDER:GetSet("Damage", 0) - BUILDER:GetSet("DamageType", "generic", {enums = { - generic = 0, --generic damage - crush = 1, --caused by physics interaction - bullet = 2, --bullet damage - slash = 4, --sharp objects, such as manhacks or other npcs attacks - burn = 8, --damage from fire - vehicle = 16, --hit by a vehicle - fall = 32, --fall damage - blast = 64, --explosion damage - club = 128, --crowbar damage - shock = 256, --electrical damage, shows smoke at the damage position - sonic = 512, --sonic damage,used by the gargantua and houndeye npcs - energybeam = 1024, --laser - nevergib = 4096, --don't create gibs - alwaysgib = 8192, --always create gibs - drown = 16384, --drown damage - paralyze = 32768, --same as dmg_poison - nervegas = 65536, --neurotoxin damage - poison = 131072, --poison damage - acid = 1048576, -- - airboat = 33554432, --airboat gun damage - blast_surface = 134217728, --this won't hurt the player underwater - buckshot = 536870912, --the pellets fired from a shotgun - direct = 268435456, -- - dissolve = 67108864, --forces the entity to dissolve on death - drownrecover = 524288, --damage applied to the player to restore health after drowning - physgun = 8388608, --damage done by the gravity gun - plasma = 16777216, -- - prevent_physics_force = 2048, -- - radiation = 262144, --radiation - removenoragdoll = 4194304, --don't create a ragdoll on death - slowburn = 2097152, -- - - explosion = -1, -- util.BlastDamage - fire = -1, -- ent:Ignite(5) - - -- env_entity_dissolver - dissolve_energy = 0, - dissolve_heavy_electrical = 1, - dissolve_light_electrical = 2, - dissolve_core_effect = 3, - - heal = -1, - armor = -1, - } - }) - BUILDER:GetSet("Spread", 0) - BUILDER:GetSet("Delay", 0) - BUILDER:GetSet("Maximum", 0) - BUILDER:GetSet("Mass", 100) - BUILDER:GetSet("Attract", 0) - BUILDER:GetSet("AttractMode", "projectile_nearest", {enums = { - hitpos = "hitpos", - hitpos_radius = "hitpos_radius", - closest_to_projectile = "closest_to_projectile", - closest_to_hitpos = "closest_to_hitpos", - }}) - BUILDER:GetSet("AttractRadius", 200) - BUILDER:GetSetPart("OutfitPart") - BUILDER:GetSet("Physical", false) - BUILDER:GetSet("CollideWithOwner", false) - BUILDER:GetSet("CollideWithSelf", false) - BUILDER:GetSet("RemoveOnCollide", false) + BUILDER:SetPropertyGroup("Firing") + BUILDER:GetSet("Speed", 1) + BUILDER:GetSet("AddOwnerSpeed", false) + BUILDER:GetSet("Spread", 0) + BUILDER:GetSet("NumberProjectiles", 1) + BUILDER:GetSet("Delay", 0) + BUILDER:GetSet("Maximum", 0) + BUILDER:GetSet("RandomAngleVelocity", Vector(0,0,0)) + BUILDER:GetSet("LocalAngleVelocity", Vector(0,0,0)) + BUILDER:SetPropertyGroup("Physics") + BUILDER:GetSet("Mass", 100) + BUILDER:GetSet("SurfaceProperties", "default", {enums = physprop_enums}) + BUILDER:GetSet("RescalePhysMesh", false, {description = "experimental! tries to scale the collide mesh by the radius! Stay within small numbers! 1 radius should be associated with a full-size model"}) + BUILDER:GetSet("OverridePhysMesh", false, {description = "experimental! tries to redefine the projectile's model to change the physics mesh"}) + BUILDER:GetSet("FallbackSurfpropModel", "models/props_junk/PopCan01a.mdl") + BUILDER:GetSet("Damping", 0) + BUILDER:GetSet("Gravity", true) + BUILDER:GetSet("Collisions", true) + BUILDER:GetSet("Sphere", false) + BUILDER:GetSet("Radius", 1, {editor_panel = "projectile_radii"}) + BUILDER:GetSet("Bounce", 0) + BUILDER:GetSet("Sticky", false) + BUILDER:GetSet("CollideWithOwner", false) + BUILDER:GetSet("CollideWithSelf", false) + BUILDER:SetPropertyGroup("Appearance") + BUILDER:GetSetPart("OutfitPart") + BUILDER:GetSet("RemoveOnHide", false) + BUILDER:GetSet("AimDir", false) + BUILDER:GetSet("DrawShadow", true) + BUILDER:SetPropertyGroup("ActiveBehavior") + BUILDER:GetSet("Physical", false) + BUILDER:GetSet("DamageRadius", 50, {editor_panel = "projectile_radii"}) + BUILDER:GetSet("LifeTime", 5) + BUILDER:GetSet("RemoveOnCollide", false) + BUILDER:GetSet("BulletImpact", false) + BUILDER:GetSet("Damage", 0) + BUILDER:GetSet("DamageType", "generic", {enums = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + explosion = -1, -- util.BlastDamage + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + } + }) + BUILDER:GetSet("Attract", 0, {editor_friendly = "attract force"}) + BUILDER:GetSet("AttractMode", "closest_to_projectile", {enums = { + hitpos = "hitpos", + hitpos_radius = "hitpos_radius", + closest_to_projectile = "closest_to_projectile", + closest_to_hitpos = "closest_to_hitpos", + }}) + BUILDER:GetSet("AttractRadius", 200) + BUILDER:EndStorableVars() PART.Translucent = false + function PART:OnShow(from_rendering) if not from_rendering then -- TODO: @@ -103,11 +130,20 @@ function PART:OnShow(from_rendering) part:Draw("opaque") end end - self:Shoot(self:GetDrawPosition()) + if self.NumberProjectiles <= 0 then self.NumberProjectiles = 0 end + if self.NumberProjectiles <= 50 then + local pos,ang = self:GetDrawPosition() + self:Shoot(pos,ang,self.NumberProjectiles) + else chat.AddText(Color(255,0,0),"[PAC3] Trying to spawn too many projectiles! The limit is " .. 50) end end end -function PART:AttachToEntity(ent) +function PART:GetSurfacePropsTable() --to view info over in the properties + return util.GetSurfaceData(physprop_indices[self.SurfaceProperties]) +end + +local_projectiles = {} +function PART:AttachToEntity(ent, physical) if not self.OutfitPart:IsValid() then return false end ent.pac_draw_distance = 0 @@ -140,28 +176,120 @@ function PART:AttachToEntity(ent) end ent.pac_projectile_part = group - ent.pac_projectile = self + ent.pac_projectile = self --that's just the launcher though + if not physical then local_projectiles[group] = ent end return true end local enable = CreateClientConVar("pac_sv_projectiles", 0, true) -function PART:Shoot(pos, ang) +local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + explosion = 31, -- ent:Ignite(5) + fire = 32, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 33, + dissolve_heavy_electrical = 34, + dissolve_light_electrical = 35, + dissolve_core_effect = 36, + + heal = 37, + armor = 38, +} +local attract_ids = { + hitpos = 0, + hitpos_radius = 1, + closest_to_projectile = 2, + closest_to_hitpos = 3, +} +function PART:Shoot(pos, ang, multi_projectile_count) local physics = self.Physical + local multi_projectile_count = multi_projectile_count or 1 if physics then if pac.LocalPlayer ~= self:GetPlayerOwner() then return end local tbl = {} - for key in pairs(self:GetStorableVars()) do - tbl[key] = self[key] - end - net.Start("pac_projectile") + net.Start("pac_projectile",true) + net.WriteUInt(multi_projectile_count,7) net.WriteVector(pos) net.WriteAngle(ang) - net.WriteTable(tbl) + + --bools + net.WriteBool(self.Sphere) + net.WriteBool(self.RemoveOnCollide) + net.WriteBool(self.CollideWithOwner) + net.WriteBool(self.RemoveOnHide) + net.WriteBool(self.OverridePhysMesh) + net.WriteBool(self.Gravity) + net.WriteBool(self.AddOwnerSpeed) + net.WriteBool(self.Collisions) + net.WriteBool(self.CollideWithSelf) + net.WriteBool(self.AimDir) + net.WriteBool(self.DrawShadow) + net.WriteBool(self.Sticky) + net.WriteBool(self.BulletImpact) + + --vectors + net.WriteVector(self.RandomAngleVelocity) + net.WriteVector(self.LocalAngleVelocity) + + --strings + net.WriteString(self.OverridePhysMesh and string.sub(string.gsub(self.FallbackSurfpropModel, "^models/", ""),1,150) or "") --custom model is an unavoidable string + net.WriteString(string.sub(self.UniqueID,1,12)) --long string but we can probably truncate it + net.WriteUInt(physprop_indices[self.SurfaceProperties] or 0,10) + net.WriteUInt(damage_ids[self.DamageType] or 0,7) + net.WriteUInt(attract_ids[self.AttractMode] or 2,3) + + --numbers + net.WriteUInt(self.Radius,8) + net.WriteUInt(self.DamageRadius,10) + net.WriteUInt(self.Damage,24) + net.WriteUInt(1000*self.Speed,16) + net.WriteUInt(self.Maximum,7) + net.WriteUInt(100*self.LifeTime,14) --might need decimals + net.WriteUInt(100*self.Delay,9) --might need decimals + net.WriteUInt(self.Mass,18) + net.WriteInt(100*self.Spread,10) + net.WriteInt(100*self.Damping,20) --might need decimals + net.WriteInt(self.Attract,14) + net.WriteUInt(self.AttractRadius,10) + net.WriteInt(100*self.Bounce,8) --might need decimals + net.SendToServer() else self.projectiles = self.projectiles or {} @@ -190,7 +318,7 @@ function PART:Shoot(pos, ang) if not self:IsValid() then return end - local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") + local ent = pac.CreateEntity(self.FallbackSurfpropModel) if not ent:IsValid() then return end local idx = table.insert(self.projectiles, ent) @@ -232,9 +360,9 @@ function PART:Shoot(pos, ang) ent:SetCollisionGroup(COLLISION_GROUP_PROJECTILE) if self.Sphere then - ent:PhysicsInitSphere(math.Clamp(self.Radius, 1, 30)) + ent:PhysicsInitSphere(math.Clamp(self.Radius, 1, 500), self.SurfaceProperties) else - ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 30), Vector(1,1,1) * math.Clamp(self.Radius, 1, 30)) + ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 500), Vector(1,1,1) * math.Clamp(self.Radius, 1, 500), self.SurfaceProperties) end ent.RenderOverride = function() @@ -268,7 +396,8 @@ function PART:Shoot(pos, ang) ent:SetCollisionGroup(COLLISION_GROUP_PROJECTILE) - if self:AttachToEntity(ent) then + if self:AttachToEntity(ent, false) then + timer.Simple(math.Clamp(self.LifeTime, 0, 10), function() if ent:IsValid() then if ent.pac_projectile_part and ent.pac_projectile_part:IsValid() then @@ -280,26 +409,102 @@ function PART:Shoot(pos, ang) end) end end) + end + + end if self.Delay == 0 then - spawn() + for i = multi_projectile_count,1,-1 do + spawn() + end else timer.Simple(self.Delay, spawn) end end end -function PART:OnRemove() - if not self.Physical and self.projectiles then - for key, ent in pairs(self.projectiles) do - SafeRemoveEntity(ent) - end +function PART:SetDamage(val) + +end + +function PART:SetRadius(val) + self.Radius = val + local sv_dist = GetConVar("pac_sv_projectile_max_radius"):GetInt() + if self.Radius > sv_dist then + self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetDamageRadius(val) + self.DamageRadius = val + local sv_dist = GetConVar("pac_sv_projectile_max_damage_radius"):GetInt() + if self.DamageRadius > sv_dist then + self:SetInfo("Your damage radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end + +function PART:SetAttractRadius(val) + self.AttractRadius = val + local sv_dist = GetConVar("pac_sv_projectile_max_attract_radius"):GetInt() + if self.AttractRadius > sv_dist then + self:SetInfo("Your attract radius is beyond the server's maximum permitted! Server max is " .. sv_dist) + else + self:SetInfo(nil) + end +end - self.projectiles = {} +function PART:SetSpeed(val) + self.Speed = val + local sv_max = GetConVar("pac_sv_projectile_max_speed"):GetInt() + if self.Speed > sv_max then + self:SetInfo("Your speed is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + +function PART:SetMass(val) + self.Mass = val + local sv_max = GetConVar("pac_sv_projectile_max_mass"):GetInt() + if self.Mass > sv_max then + self:SetInfo("Your mass is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) end end + +function PART:SetDamage(val) + self.Damage = val + local sv_max = GetConVar("pac_sv_damage_zone_max_damage"):GetInt() + if self.Damage > sv_max then + self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max) + else + self:SetInfo(nil) + end +end + +pac.AddHook("Think", "pac_cleanup_CS_projectiles", function() + for rootpart,ent in pairs(local_projectiles) do + if ent.pac_projectile_part == rootpart then + local tbl = ent.pac_projectile_part:GetChildren() + local partchild = tbl[next(tbl)] --ent.pac_projectile_part is the root group, but outfit part is the first child + + if IsValid(partchild) then + if partchild:IsHidden() then + SafeRemoveEntity(ent) + end + end + end + end + +end) + --[[ function PART:OnHide() if self.RemoveOnHide then @@ -307,10 +512,25 @@ function PART:OnHide() end end ]] + +--[[if ent.pac_projectile_part then + local partchild = next(ent.pac_projectile_part:GetChildren()) --ent.pac_projectile_part is the root group, but outfit part is the first child + if IsValid(part) then + if partchild:IsHidden() then + if ent.pac_projectile.RemoveOnHide then + net.Start("pac_projectile_remove") + net.WriteInt(data.ent_id) + net.SendToServer() + end + end + end +end]] + do -- physical local Entity = Entity local projectiles = {} pac.AddHook("Think", "pac_projectile", function() + for key, data in pairs(projectiles) do if not data.ply:IsValid() then projectiles[key] = nil @@ -320,12 +540,13 @@ do -- physical local ent = Entity(data.ent_id) if ent:IsValid() and ent:GetClass() == "pac_projectile" then - local part = pac.GetPartFromUniqueID(pac.Hash(data.ply), data.partuid) + local part = pac.FindPartByPartialUniqueID(pac.Hash(data.ply), data.partuid) if part:IsValid() and part:GetPlayerOwner() == data.ply then - part:AttachToEntity(ent) + part:AttachToEntity(ent, true) end projectiles[key] = nil end + ::CONTINUE:: end end) @@ -334,10 +555,32 @@ do -- physical local ply = net.ReadEntity() local ent_id = net.ReadInt(16) local partuid = net.ReadString() + local surfprop = net.ReadString() if ply:IsValid() then table.insert(projectiles, {ply = ply, ent_id = ent_id, partuid = partuid}) end + + local ent = Entity(ent_id) + + ent.Think = function() + if ent.pac_projectile_part then + local tbl = ent.pac_projectile_part:GetChildren() + local partchild = tbl[next(tbl)] --ent.pac_projectile_part is the root group, but outfit part is the first child + if IsValid(partchild) then + if partchild:IsHidden() then + if ent.pac_projectile.RemoveOnHide and not ent.markedforremove then + ent.markedforremove = true + net.Start("pac_projectile_remove") + net.WriteInt(ent_id, 16) + net.SendToServer() + + end + end + end + + end + end end) end diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index b111bd342..885f83a08 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -44,10 +44,10 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Pow", 1) BUILDER:SetPropertyGroup("behavior") - BUILDER:GetSet("Additive", false) - BUILDER:GetSet("PlayerAngles", false) - BUILDER:GetSet("ZeroEyePitch", false) - BUILDER:GetSet("ResetVelocitiesOnHide", true) + BUILDER:GetSet("Additive", false, {description = "This means that every computation frame, the proxy will add its output to its current stored memory. This can quickly get out of control if you don't know what you're doing! This is like using the feedback() function"}) + BUILDER:GetSet("PlayerAngles", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will choose between the owner entity's Angles or EyeAngles. Unsure of whether this makes a difference."}) + BUILDER:GetSet("ZeroEyePitch", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will force the angle to be horizon level."}) + BUILDER:GetSet("ResetVelocitiesOnHide", true, {description = "Because velocity calculators use smoothing that makes the output converge toward a crude rolling average, it might matter whether you want to get a clean slate readout.\n(VelocityRoughness is how close to the snapshots it will be. Lower means smoother but delayed. Higher means less smoothing but it might overshoot and be inaccurate because of frame time works and varies)"}) BUILDER:GetSet("VelocityRoughness", 10) BUILDER:EndStorableVars() @@ -219,6 +219,20 @@ PART.Inputs.property = function(self, property_name, field) return 0 end +PART.Inputs.polynomial = function(self, x, ...) + x = x or 1 + local total = 0 + local args = { ... } + + pow = 0 + for _, coefficient in ipairs(args) do + total = total + coefficient*math.pow(x, pow) + pow = pow + 1 + end + return total + +end + PART.Inputs.owner_position = function(self) local owner = get_owner(self) @@ -307,7 +321,7 @@ PART.Inputs.random_once = function(self, seed, min, max) self.rand_id = self.rand_id or {} if seed then self.rand_id[seed] = self.rand_id[seed] or min + math.random()*(max-min) - else + else self.rand = self.rand or min + math.random()*(max-min) end @@ -343,22 +357,25 @@ end PART.Inputs.part_distance = function(self, uid1, uid2) if not uid1 or not uid2 then return 0 end + local owner = self:GetPlayerOwner() - local PartA = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid1, self) end + local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) + if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end - local PartB = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid2) - if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid2, self) end + local PartB = pac.GetPartFromUniqueID(pac.Hash(owner), uid2) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid2) + if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(owner), uid2, self) end if not PartA:IsValid() or not PartB:IsValid() then return 0 end + if not PartA.Position or not PartB.Position then return 0 end return (PartB:GetWorldPosition() - PartA:GetWorldPosition()):Length() end PART.Inputs.event_alternative = function(self, uid1, num1, num2) if not uid1 then return 0 end + local owner = self:GetPlayerOwner() - local PartA = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid1, self) end + local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) + if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end if PartA.ClassName == "event" then if PartA.event_triggered then return num1 or 0 @@ -929,6 +946,32 @@ PART.Inputs.flat_dot_right = function(self) return 0 end + +PART.Inputs.pac_healthbars_total = function(self) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars then + return ent.pac_healthbars_total or 0 + end + return 0 +end + +PART.Inputs.pac_healthbars_layertotal = function(self, layer) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars and ent.pac_healthbars_layertotals then + return ent.pac_healthbars_layertotals[layer] or 0 + end + return 0 +end + +PART.Inputs.pac_healthbar_uidvalue = function(self, uid) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars and ent.pac_healthbars_uidtotals then + return ent.pac_healthbars_uidtotals[uid] or 0 + end + return 0 +end + + net.Receive("pac_proxy", function() local ply = net.ReadEntity() local str = net.ReadString() @@ -940,6 +983,9 @@ net.Receive("pac_proxy", function() if ply:IsValid() then ply.pac_proxy_events = ply.pac_proxy_events or {} ply.pac_proxy_events[str] = {name = str, x = x, y = y, z = z} + if LocalPlayer() == ply then + pac.Message("pac_proxy -> command(\""..str.."\") is " .. x .. "," .. y .. "," .. z) + end end end) @@ -1093,7 +1139,21 @@ end function PART:OnThink() local part = self:GetTarget() if not part:IsValid() then return end - if part.ClassName == 'woohoo' then return end + if part.ClassName == 'woohoo' then --why a part hardcode exclusion?? + --ok fine I guess it's because it's super expensive, but at least we can be selective about it, the other parameters are safe + if self.VariableName == "Resolution" or self.VariableName == "BlurFiltering" and self.touched then + return + end + end + + --foolproofing: scream at the user if they didn't set a variable name + if self == pace.current_part then self.touched = true end + if self ~= pace.current_part and self.VariableName == "" and self.touched then + self:AttachEditorPopup("You forgot to set a variable name! The proxy won't work until it knows where to send the math!", true) + pace.FlashNotification("An edited proxy still has no variable name! The proxy won't work until it knows where to send the math!") + self:SetWarning("You forgot to set a variable name! The proxy won't work until it knows where to send the math!") + self.touched = false + elseif self.VariableName ~= "" then self:SetWarning() end self:CalcVelocity() diff --git a/lua/pac3/core/shared/movement.lua b/lua/pac3/core/shared/movement.lua index f0052f22a..43111e866 100644 --- a/lua/pac3/core/shared/movement.lua +++ b/lua/pac3/core/shared/movement.lua @@ -1,24 +1,31 @@ local movementConvar = CreateConVar("pac_free_movement", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow players to modify movement. -1 apply only allow when noclip is allowed, 1 allow for all gamemodes, 0 to disable") +local allowMass = CreateConVar("pac_player_movement_allow_mass", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "enables changing player mass in player movement. 1 to enable, 0 to disable", 0, 1) +local massUpperLimit = CreateConVar("pac_player_movement_max_mass", 50000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the maximum mass that players can use with player movement", 85, 50000) +local massLowerLimit = CreateConVar("pac_player_movement_min_mass", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the minimum mass that players can use with player movement", 0, 85) +local massDamageScale = CreateConVar("pac_player_movement_physics_damage_scaling", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the damage scaling applied to players by modified mass values. 1 to enable, 0 to disable", 0, 1) local default = { JumpHeight = 200, StickToGround = true, GroundFriction = 0.12, AirFriction = 0.01, + HorizontalAirFrictionMultiplier = 1, + StrafingStrengthMultiplier = 1, Gravity = Vector(0,0,-600), + Mass = 85, Noclip = false, MaxGroundSpeed = 750, - MaxAirSpeed = 1, + MaxAirSpeed = 750, AllowZVelocity = false, ReversePitch = false, UnlockPitch = false, VelocityToViewAngles = 0, RollAmount = 0, - SprintSpeed = 750, - RunSpeed = 300, + SprintSpeed = 400, + RunSpeed = 200, WalkSpeed = 100, - DuckSpeed = 25, + DuckSpeed = 50, FinEfficiency = 0, FinLiftMode = "normal", @@ -35,6 +42,8 @@ if SERVER then local str = net.ReadString() if str == "disable" then ply.pac_movement = nil + ply:GetPhysicsObject():SetMass(default.Mass) + ply.scale_mass = 1 else if default[str] ~= nil then local val = net.ReadType() @@ -113,7 +122,9 @@ local function badMovetype(ply) end local frictionConvar = GetConVar("sv_friction") +local lasttime = 0 pac.AddHook("Move", "custom_movement", function(ply, mv) + lasttime = SysTime() local self = ply.pac_movement if not self then @@ -145,6 +156,25 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) ply:SetJumpPower(self.JumpHeight) + if SERVER then + if allowMass:GetInt() == 1 then + ply:GetPhysicsObject():SetMass(math.Clamp(self.Mass, massLowerLimit:GetFloat(), massUpperLimit:GetFloat())) + end + end + + if (movementConvar:GetInt() == 1 or (movementConvar:GetInt() == -1 and hook.Run("PlayerNoClip", ply, true) == true)) and massDamageScale:GetInt() == 1 then + ply.scale_mass = 85/math.Clamp(self.Mass, math.max(massLowerLimit:GetFloat(), 0.01), massUpperLimit:GetFloat()) + else + ply.scale_mass = 1 + end + + pac.AddHook("EntityTakeDamage", "PAC3MassDamageScale", function(target, dmginfo) + if (target:IsPlayer() and dmginfo:IsDamageType(DMG_CRUSH or DMG_VEHICLE)) then + dmginfo:ScaleDamage(target.scale_mass or 1) + end + end) + + if self.Noclip then ply:SetMoveType(MOVETYPE_NONE) else @@ -173,7 +203,11 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) speed = self.DuckSpeed end --- speed = speed * FrameTime() + if not on_ground and not self.AllowZVelocity then + speed = speed * self.StrafingStrengthMultiplier + end + + --speed = speed * FrameTime() local ang = mv:GetAngles() local vel = Vector() @@ -193,7 +227,8 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) elseif mv:KeyDown(IN_MOVELEFT) then vel = vel - ang:Right() end - + + vel = vel:GetNormalized() * speed if self.AllowZVelocity then @@ -208,15 +243,19 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) vel.z = 0 end - local speed = vel + local speed = vel --That makes speed the driver (added velocity) + if not on_ground and not self.AllowZVelocity then + speed = speed * self.StrafingStrengthMultiplier + end local vel = mv:GetVelocity() + --@note ground friction if on_ground and not self.Noclip and self.StickToGround then -- work against ground friction local sv_friction = frictionConvar:GetInt() - + --ice and glass go too fast? what do? if sv_friction > 0 then - sv_friction = 1 - (sv_friction * 15) / 1000 + sv_friction = 1 - (sv_friction * 15) / 1000 --default is 8, and the formula ends up being equivalent to 0.12 groundfriction variable multiplying vel by 0.88 vel = vel / sv_friction end end @@ -226,14 +265,64 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) -- todo: don't allow adding more velocity to existing velocity if it exceeds -- but allow decreasing if not on_ground then - local friction = self.AirFriction - friction = -(friction) + 1 + + if ply:WaterLevel() >= 2 then + local ground_speed = self.RunSpeed - vel = vel * friction + if mv:KeyDown(IN_SPEED) then + ground_speed = self.SprintSpeed + end - vel = vel + self.Gravity * 0.015 - speed = speed:GetNormalized() * math.Clamp(speed:Length(), 0, self.MaxAirSpeed) - vel = vel + (speed * FrameTime()*(66.666*(-friction+1))) + if mv:KeyDown(IN_WALK) then + ground_speed = self.WalkSpeed + end + + if mv:KeyDown(IN_DUCK) then + ground_speed = self.DuckSpeed + end + if self.MaxGroundSpeed == 0 then self.MaxGroundSpeed = 400 end + if self.MaxAirSpeed == 0 then self.MaxAirSpeed = 400 end + local water_speed = math.min(ground_speed, self.MaxAirSpeed, self.MaxGroundSpeed) + --print("water speed " .. water_speed) + + ang = ply:EyeAngles() + local vel2 = Vector() + + if mv:KeyDown(IN_FORWARD) then + vel2 = water_speed*ang:Forward() + elseif mv:KeyDown(IN_BACK) then + vel2 = -water_speed*ang:Forward() + end + + if mv:KeyDown(IN_MOVERIGHT) then + vel2 = vel2 + ang:Right() + elseif mv:KeyDown(IN_MOVELEFT) then + vel2 = vel2 - ang:Right() + end + + vel = vel + vel2 * math.min(FrameTime(),0.3) * 2 + + else + local friction = self.AirFriction + local friction_mult = -(friction) + 1 + + local hfric = friction * self.HorizontalAirFrictionMultiplier + local hfric_mult = -(hfric) + 1 + + vel.x = vel.x * hfric_mult + vel.y = vel.y * hfric_mult + vel.z = vel.z * friction_mult + vel = vel + self.Gravity * 0.015 + + speed = speed:GetNormalized() * math.Clamp(speed:Length(), 0, self.MaxAirSpeed) --base driver speed but not beyond max? + --why should the base driver speed depend on friction? + + --reminder: vel is the existing speed, speed is the driver (added velocity) + --vel = vel + (speed * FrameTime()*(66.666*friction)) + vel.x = vel.x + (speed.x * math.min(FrameTime(),0.3)*(66.666*hfric)) + vel.y = vel.y + (speed.y * math.min(FrameTime(),0.3)*(66.666*hfric)) + vel.z = vel.z + (speed.z * math.min(FrameTime(),0.3)*(66.666*friction)) + end else local friction = self.GroundFriction friction = -(friction) + 1 @@ -241,7 +330,24 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) vel = vel * friction speed = speed:GetNormalized() * math.min(speed:Length(), self.MaxGroundSpeed) - vel = vel + (speed * FrameTime()*(75.77*(-friction+1))) + + local trace = { + start = mv:GetOrigin(), + endpos = mv:GetOrigin() + Vector(0, 0, -20), + mask = MASK_SOLID_BRUSHONLY + } + local trc = util.TraceLine(trace) + local special_surf_fric = 1 + --print(trc.MatType) + if trc.MatType == MAT_GLASS then + special_surf_fric = 0.6 + elseif trc.MatType == MAT_SNOW then + special_surf_fric = 0.4 + end + + --vel = vel + (special_surf_fric * speed * FrameTime()*(75.77*(-friction+1))) + vel = vel + (special_surf_fric * speed * math.min(FrameTime(),0.3)*(75.77*(-friction+1))) + vel = vel + self.Gravity * 0.015 end diff --git a/lua/pac3/editor/server/bans.lua b/lua/pac3/editor/server/bans.lua index 2d3bfdb81..101be43fe 100644 --- a/lua/pac3/editor/server/bans.lua +++ b/lua/pac3/editor/server/bans.lua @@ -1,3 +1,7 @@ +util.AddNetworkString("pac.BanUpdate") +util.AddNetworkString("pac.RequestBanStates") +util.AddNetworkString("pac.SendBanStates") + local function get_bans() local str = file.Read("pac_bans.txt", "DATA") @@ -111,3 +115,39 @@ function pace.IsBanned(ply) return pace.Bans[ply:UniqueID()] ~= nil end + +net.Receive("pac.BanUpdate", function(len, ply) + pac.Message("Received ban list update operation from : ", ply) + pac.Message("Time : ", os.date( "%a %X %x", os.time() )) + local playerlist = net.ReadTable() + for i,v in pairs(playerlist) do + if playerlist[i] == "Allowed" then + pace.Unban(i) + elseif playerlist[i] == "Banned" then + pace.Ban(i) + end + print(i, "banned?", pace.IsBanned(i), "Update ->", playerlist[i]) + end +end) + +net.Receive("pac.RequestBanStates", function(len,ply) + local archive = net.ReadBool() + pac.Message("Received ban list request from : ", ply) + pac.Message("Time : ", os.date( "%a %X %x", os.time() )) + local players = {} + for _,v in pairs(player.GetAll()) do + players[v] = false + end + if not pace.Bans then + pace.Bans = get_bans() + end + for i,v in pairs(pace.Bans) do + print(player.GetBySteamID(i), player.GetBySteamID(v[1])) + local ply = player.GetBySteamID(v[1]) + players[ply] = true + end + + net.Start("pac.SendBanStates") + net.WriteTable(players) + net.Send(ply) +end) diff --git a/lua/pac3/editor/server/combat_bans.lua b/lua/pac3/editor/server/combat_bans.lua new file mode 100644 index 000000000..635f44c52 --- /dev/null +++ b/lua/pac3/editor/server/combat_bans.lua @@ -0,0 +1,103 @@ + +local function get_combat_ban_states() + local str = file.Read("pac_combat_bans.txt", "DATA") + + local banstates = {} + + if str and str ~= "" then + banstates = util.KeyValuesToTable(str) + end + + do -- check if this needs to be rebuilt + local k,v = next(banstates) + if isstring(v) then + local temp = {} + + for k,v in pairs(banstates) do + permission = pac.global_combat_whitelist[player.GetBySteamID(k)] or "Default" + temp[util.CRC("gm_" .. v .. "_gm")] = {steamid = v, name = k, permission = permission} + end + + banstates = temp + end + end + + return banstates +end + +local function load_table_from_file() + tbl_on_file = get_combat_ban_states() + for id, data in pairs(tbl_on_file) do + if not pac.global_combat_whitelist[id] then + pac.global_combat_whitelist[id] = tbl_on_file[id] + end + end +end + +if SERVER then + util.AddNetworkString("pac.BanUpdate") + util.AddNetworkString("pac.RequestBanStates") + util.AddNetworkString("pac.SendBanStates") + + + util.AddNetworkString("pac.CombatBanUpdate") + util.AddNetworkString("pac.SendCombatBanStates") + util.AddNetworkString("pac.RequestCombatBanStates") +end + + +net.Receive("pac.CombatBanUpdate", function() + --get old states first + pac.old_tbl_on_file = get_combat_ban_states() + + load_table_from_file() + + local combatstates_update = net.ReadTable() + local is_id_table = net.ReadBool() + local banstates_for_file = pac.old_tbl_on_file + + --update + if not is_id_table then + for ply, perm in pairs(combatstates_update) do + banstates_for_file[ply:SteamID()] = { + steamid = ply:SteamID(), + nick = ply:Nick(), + permission = perm + } + + pac.global_combat_whitelist[ply:SteamID()] = { + steamid = ply:SteamID(), + nick = ply:Nick(), + permission = perm + } + end + else + pac.global_combat_whitelist = combatstates_update + banstates_for_file = combatstates_update + end + + file.Write("pac_combat_bans.txt", util.TableToKeyValues(banstates_for_file), "DATA") +end) + +net.Receive("pac.RequestCombatBanStates", function(len, ply) + pac.global_combat_whitelist = get_combat_ban_states() + net.Start("pac.SendCombatBanStates") + net.WriteTable(pac.global_combat_whitelist) + net.Send(ply) +end) + + +pac.old_tbl_on_file = get_combat_ban_states() + + + +concommand.Add("pac_read_combat_bans", function() + print("PAC3 combat bans and whitelist:") + for k,v in pairs(get_combat_ban_states()) do + print("\t" .. v.nick .. " is " .. v.permission .. " [" .. v.steamid .. "]") + end +end) + +concommand.Add("pac_read_outfit_bans", function() + PrintTable(pace.Bans) +end) diff --git a/lua/pac3/editor/server/init.lua b/lua/pac3/editor/server/init.lua index b9c6eb142..282dc2920 100644 --- a/lua/pac3/editor/server/init.lua +++ b/lua/pac3/editor/server/init.lua @@ -22,6 +22,8 @@ do end end +CreateConVar("pac_sv_prop_outfits", "0", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow applying parts on other entities serverside\n0=don't\n1=allow on props but not players\n2=allow on other players") + function pace.CanPlayerModify(ply, ent) if not IsValid(ply) or not IsValid(ent) then return false @@ -42,13 +44,24 @@ function pace.CanPlayerModify(ply, ent) if ent.CPPIGetOwner and ent:CPPIGetOwner() == ply then return true end - + + if GetConVar("pac_sv_prop_outfits"):GetInt() ~= 0 then + if GetConVar("pac_sv_prop_outfits"):GetInt() == 1 then + return not (ply ~= ent and ent:IsPlayer()) + elseif GetConVar("pac_sv_prop_outfits"):GetInt() == 2 then + return true + end + + end + do local tr = util.TraceLine({ start = ply:EyePos(), endpos = ent:WorldSpaceCenter(), filter = ply }) if tr.Entity == ent and hook.Run("CanTool", ply, tr, "paint") == true then return true end end + + return false end @@ -59,6 +72,7 @@ include("wear_filter.lua") include("bans.lua") include("spawnmenu.lua") include("show_outfit_on_use.lua") +include("pac_settings_manager.lua") do util.AddNetworkString("pac_in_editor") @@ -92,4 +106,5 @@ end CreateConVar("has_pac3_editor", "1", {FCVAR_NOTIFY}) +resource.AddSingleFile("materials/icon64/new pac icon.png") resource.AddSingleFile("materials/icon64/pac3.png") diff --git a/lua/pac3/editor/server/pac_settings_manager.lua b/lua/pac3/editor/server/pac_settings_manager.lua new file mode 100644 index 000000000..0a31bc019 --- /dev/null +++ b/lua/pac3/editor/server/pac_settings_manager.lua @@ -0,0 +1,13 @@ + +util.AddNetworkString("pac_send_sv_cvar") + +net.Receive("pac_send_sv_cvar", function(len,ply) + if ply == Entity(1) or ply:IsAdmin() then print("authenticated") else return end + local cmd = net.ReadString() + local val = net.ReadString() + --if cmd == "" then + + --end + GetConVar(cmd):SetString(val) + print("[PAC3]: Admin "..ply:GetName().." set "..cmd.." to "..val) +end) diff --git a/lua/pac3/extra/shared/init.lua b/lua/pac3/extra/shared/init.lua index 78de1c3e9..4728c341f 100644 --- a/lua/pac3/extra/shared/init.lua +++ b/lua/pac3/extra/shared/init.lua @@ -2,6 +2,7 @@ include("hands.lua") include("pac_weapon.lua") include("projectiles.lua") +include("net_combat.lua") local cvar = CreateConVar("pac_restrictions", "0", FCVAR_REPLICATED) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua new file mode 100644 index 000000000..369c1242e --- /dev/null +++ b/lua/pac3/extra/shared/net_combat.lua @@ -0,0 +1,2343 @@ +--lua_openscript pac3/extra/shared/net_combat.lua +if SERVER then + include("pac3/editor/server/combat_bans.lua") + include("pac3/editor/server/bans.lua") +end + + +pac.global_combat_whitelist = pac.global_combat_whitelist or {} + +local hitscan_allow = CreateConVar("pac_sv_hitscan", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside") +local hitscan_max_bullets = CreateConVar("pac_sv_hitscan_max_bullets", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum number of bullets") +local hitscan_max_damage = CreateConVar("pac_sv_hitscan_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum damage") +local hitscan_spreadout_dmg = CreateConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not force hitscans to divide their damage among the number of bullets fired") + +local damagezone_allow = CreateConVar("pac_sv_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside") +local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum damage") +local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length") +local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius") +local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone") + +local lock_allow = CreateConVar("pac_sv_lock", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") +local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") +local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside") +local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius") +local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing players with lock part") +local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part") +local lock_allow_grab_ent = CreateConVar("pac_sv_lock_allow_grab_ent", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing other entities with lock part") + +local force_allow = CreateConVar("pac_sv_force", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside") +local force_max_length = CreateConVar("pac_sv_force_max_length", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum length") +local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum radius") +local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force") + +local healthmod_allow = CreateConVar("pac_sv_health_modifier", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") +local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") +local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") +local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") + +local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") +cvars.AddChangeCallback("pac_sv_block_combat_features_on_next_restart", function() print("Remember that pac_sv_block_combat_features_on_next_restart is applied on server startup! Only do it if you know what you're doing. You'll need to restart the server.") end) + +local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter") +local netrate_allowance = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 60, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "the budgeted allowance to limit how many pac combat net messages can be sent in bursts. 0 to disable, otherwise a number of net messages of allowance.") +local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_netrate_monitor_serverside", 0, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not to let clients enforce their net message rates.\nSet this to 1 to get serverside prints telling you whenever someone is going over their allowance, but it'll still take the network bandwidth.\nSet this to 0 to let clients enforce their net rate and save some bandwidth but the server won't know who's spamming net messages.") +local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") +local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") +local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") +local enforce_distance = CreateConVar("pac_sv_combat_distance_enforced", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enforce a limit on how far a pac combat action can originate.\nIf set to a distance, it will prevent actions that are too far from the acting player.\n0 to disable.") +local ENFORCE_DISTANCE_SQR = math.pow(enforce_distance:GetInt(),2) +cvars.AddChangeCallback("pac_sv_combat_distance_enforced", function() ENFORCE_DISTANCE_SQR = math.pow(enforce_distance:GetInt(),2) end) + + +local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") +local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") + +local damageable_point_ent_classes = { + ["predicted_viewmodel"] = false, + ["prop_physics"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true +} + +local physics_point_ent_classes = { + ["prop_physics"] = true, + ["prop_physics_multiplayer"] = true, + ["prop_ragdoll"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true +} + +local contraption_classes = { + ["prop_physics"] = true, +} + +local pre_excluded_ent_classes = { + ["info_player_start"] = true, + ["aoc_spawnpoint"] = true, + ["info_player_teamspawn"] = true, + ["env_tonemap_controller"] = true, + ["env_fog_controller"] = true, + ["env_skypaint"] = true, + ["shadow_control"] = true, + ["env_sun"] = true, + ["predicted_viewmodel"] = true, + ["physgun_beam"] = true, + ["ambient_generic"] = true, + ["trigger_once"] = true, + ["trigger_multiple"] = true, + ["trigger_hurt"] = true, + ["info_ladder_dismount"] = true, + ["info_particle_system"] = true, + ["env_sprite"] = true, + ["env_fire"] = true, + ["env_soundscape"] = true, + ["env_smokestack"] = true, + ["light"] = true, + ["move_rope"] = true, + ["keyframe_rope"] = true, + ["env_soundscape_proxy"] = true, + ["gmod_hands"] = true, +} + + + +local grab_consents = {} +local damage_zone_consents = {} +local force_consents = {} +local hitscan_consents = {} +local calcview_consents = {} +local active_force_ids = {} +local active_grabbed_ents = {} + +local damage_types = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, +} + +do --define a basic class for the bullet emitters + local ENT = {} + ENT.Type = "anim" + ENT.ClassName = "pac_bullet_emitter" + ENT.Spawnable = false + scripted_ents.Register(ENT, "pac_bullet_emitter") +end + +if SERVER then + + local function CountNetMessage(ply) + local stime = SysTime() + local ms_basis = enforce_netrate:GetInt()/1000 + local base_allowance = netrate_allowance:GetInt() + + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance or base_allowance + ply.pac_netmessage_allowance_time = ply.pac_netmessage_allowance_time or 0 --initialize fields + + local timedelta = stime - ply.pac_netmessage_allowance_time --in seconds + ply.pac_netmessage_allowance_time = stime + local regen_rate = math.Clamp(ms_basis,0.01,10) / 20 --delay (converted from milliseconds) -> frequency (1/seconds) + local regens = timedelta / regen_rate + --print(timedelta .. " s, " .. 1/regen_rate .. "/s, " .. regens .. " regens") + if base_allowance == 0 then --limiting only by time, with no reserves + return timedelta > ms_basis + elseif ms_basis == 0 then --allowance with 0 time means ??? I guess automatic pass + return true + else + if timedelta > ms_basis then --good, count up + --print("good time: +"..regens .. "->" .. math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance)) + ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance) + else --earlier than base delay, so count down the allowance + --print("bad time: -1") + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance - 1 + end + ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance,-1,base_allowance) + ply.pac_netmessage_allowance_time = stime + return ply.pac_netmessage_allowance ~= -1 + end + + end + + --hack fix to stop GetOwner returning [NULL Entity] + hook.Add("PlayerSpawnedProp", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedNPC", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedRagdoll", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedSENT", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedSWEP", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedVehicle", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) + hook.Add("PlayerSpawnedEffect", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) + + local function IsPossibleContraptionEntity(ent) + if not IsValid(ent) then return false end + local b = (string.find(ent:GetClass(), "phys") ~= nil + or string.find(ent:GetClass(), "anchor") ~= nil + or string.find(ent:GetClass(), "rope") ~= nil + or string.find(ent:GetClass(), "gmod") ~= nil) + --print("entity", ent, "contraption?", b) + return b + end + + local function IsPropProtected(ent, ply) + + local reason = "" + local pac_sv_prop_protection = global_combat_prop_protection:GetBool() + + local prop_protected = ent:GetCreator():IsPlayer() and ent:GetCreator() ~= ply + + local contraption = IsPossibleContraptionEntity(ent) and ent:IsConstrained() + + if prop_protected and contraption then + reason = "it's a contraption owned by another player" + return true, reason + end + --apply prop protection + if pac_sv_prop_protection and prop_protected then + reason = "we enforce generic prop protection in the server" + return true, reason + end + return false, "it's fine" + end + + --whitelisting/blacklisting check + local function PlayerIsCombatAllowed(ply) + if pac.global_combat_whitelist[string.lower(ply:SteamID())] then + if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Allowed" then return true end + if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end + end + + if global_combat_whitelisting:GetBool() then --if server uses the high-trust whitelisting mode + if pac.global_combat_whitelist[string.lower(ply:SteamID())] then + if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission ~= "Allowed" then return false end --if player is not in whitelist, stop! + end + else --if server uses the default, blacklisting mode + if pac.global_combat_whitelist[string.lower(ply:SteamID())] then + if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end --if player is in blacklist, stop! + end + end + + return true + end + + --stopping condition to stop force or damage operation if too many entities, because net impact is proportional to players + local function TooManyEnts(count) + local playercount = player.GetCount() + local hard_limit = raw_ent_limit:GetInt() + local per_ply = per_ply_limit:GetInt() + --print(count .. " compared against hard limit " .. hard_limit .. " and " .. playercount .. " players*" .. per_ply .. " limit (" .. count*playercount .. " | " .. playercount*per_ply .. ")") + if count > hard_limit then + MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond hard limit.\n") + return true + end + if not game.SinglePlayer() then + if count > per_ply_limit:GetInt() * playercount then + MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond per-player sending limit.\n") + return true + end + if count * playercount > math.min(hard_limit, per_ply*playercount) then + MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond hard limit or player limit\n") + return true + end + end + return false + end + + --consent check + local function PlayerAllowsCalcView(ply) + return grab_consents[ply] and calcview_consents[ply] --oops it's redundant but I prefer it this way + end + + local function ApplyLockState(ent, bool) --Change the movement states and reset some other angle-related things + --the grab imposes MOVETYPE_NONE and no collisions + --reverting the state requires to reset the eyeang roll in case it was modified + if ent:IsPlayer() then + if bool then + active_grabbed_ents[ent] = true + ent:SetMoveType(MOVETYPE_NONE) + ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) + else + active_grabbed_ents[ent] = nil + ent:SetMoveType(MOVETYPE_WALK) + ent:SetCollisionGroup(COLLISION_GROUP_NONE) + local eyeang = ent:EyeAngles() + eyeang.r = 0 + ent:SetEyeAngles(eyeang) + ent:SetPos(ent:GetPos() + Vector(0,0,10)) + net.Start("pac_lock_imposecalcview") + net.WriteBool(false) + net.WriteVector(Vector(0,0,0)) + net.WriteAngle(Angle(0,0,0)) + net.Send(ent) + ent.has_calcview = false + end + + elseif ent:IsNPC() then + if bool then + active_grabbed_ents[ent] = true + ent:SetMoveType(MOVETYPE_NONE) + ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) + else + active_grabbed_ents[ent] = nil + ent:SetMoveType(MOVETYPE_STEP) + ent:SetCollisionGroup(COLLISION_GROUP_NONE) + ent_ang = ent:GetAngles() + ent_ang.r = 0 + ent:SetAngles(ent_ang) + end + end + + if bool == nil then + for i,ply in pairs(player.GetAll()) do + if ply.grabbed_ents[ent] then + ply.grabbed_ents[ent] = nil + print(ent , "no longer grabbed by", ply) + end + end + end + + ent:PhysWake() + ent:SetGravity(1) + end + + local function maximized_ray_mins_maxs(startpos,endpos,padding) + local maxsx,maxsy,maxsz + local highest_sq_distance = 0 + for xsign = -1, 1, 2 do + for ysign = -1, 1, 2 do + for zsign = -1, 1, 2 do + local distance_tried = (startpos + Vector(padding*xsign,padding*ysign,padding*zsign)):DistToSqr(endpos - Vector(padding*xsign,padding*ysign,padding*zsign)) + if distance_tried > highest_sq_distance then + highest_sq_distance = distance_tried + maxsx,maxsy,maxsz = xsign,ysign,zsign + end + end + end + end + return Vector(padding*maxsx,padding*maxsy,padding*maxsz),Vector(padding*-maxsx,padding*-maxsy,padding*-maxsz) + end + + local function AddDamageScale(ply, id,scale, part_uid) + ply.pac_damage_scalings = ply.pac_damage_scalings or {} + ply.pac_damage_scalings[part_uid] = {scale = scale, id = id, uid = part_uid} + end + + local function FixMaxHealths(ply) + local biggest_health = 0 + local biggest_armor = 0 + local found_armor = false + local found_health = false + + if ply.pac_healthmods then + for uid,tbl in pairs(ply.pac_healthmods) do + if tbl.maxhealth then biggest_health = math.max(biggest_health,tbl.maxhealth) found_health = true end + if tbl.maxarmor then biggest_armor = math.max(biggest_armor,tbl.maxarmor) found_armor = true end + end + end + + if found_health then + ply:SetMaxHealth(biggest_health) + else + ply:SetMaxHealth(100) + ply:SetHealth(math.min(ply:Health(),100)) + end + ply.pac_maxhealth = ply:GetMaxHealth() + if found_armor then + ply:SetMaxArmor(biggest_armor) + else + ply:SetMaxArmor(100) + ply:SetArmor(math.min(ply:Armor(),100)) + end + ply.pac_maxhealth = ply:GetMaxArmor() + end + + hook.Add("PlayerSpawn", "PAC_AutoMaxHealth_On_Respawn", function(ply) + FixMaxHealths(ply) + end) + + local function GatherDamageScales(ent) + if not ent then return 0 end + if not ent:IsPlayer() then return 1 end + if not ent.pac_damage_scalings then return 1 end + local cumulative_dmg_scale = 1 + for uid, tbl in pairs(ent.pac_damage_scalings) do + cumulative_dmg_scale = cumulative_dmg_scale * tbl.scale + end + return math.max(cumulative_dmg_scale,healthmod_minimum_dmgscaling:GetFloat()) + end + + --healthbars work with a 2 levels-deep table + --for each player, an index table (priority) to decide which layer is damaged first + --for each layer, one table for each part uid + --for each uid, we have the current uid bar cluster's health value + --instead of keeping track of every bar, it will update the status with a remainder calculation + + --ply.pac_healthbars + --ply.pac_healthbars[layer] + --ply.pac_healthbars[layer][part_uid] = healthvalue + + local function UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) + local existing_uidlayer = true + local healthvalue = 0 + if not ply.pac_healthbars then + existing_uidlayer = false + ply.pac_healthbars = {} + end + if not ply.pac_healthbars[layer] then + existing_uidlayer = false + ply.pac_healthbars[layer] = {} + end + if not ply.pac_healthbars[layer][part_uid] then + existing_uidlayer = false + ply.pac_healthbars[layer][part_uid] = num*barsize + healthvalue = num*barsize + end + + if (not existing_uidlayer) or follow then + healthvalue = num*barsize + end + + ply.pac_healtbar_uid_absorbfactor = ply.pac_healtbar_uid_absorbfactor or {} + ply.pac_healtbar_uid_absorbfactor[part_uid] = absorbfactor + + if num == 0 then --remove + ply.pac_healthbars[layer] = nil + ply.pac_healtbar_uid_absorbfactor[part_uid] = nil + elseif num > 0 then --add if follow or created + ply.pac_healthbars[layer][part_uid] = healthvalue + ply.pac_healtbar_uid_absorbfactor[part_uid] = absorbfactor + end + for checklayer,tbl in pairs(ply.pac_healthbars) do + for uid,value in pairs(tbl) do + if layer ~= checklayer and part_uid == uid then + ply.pac_healthbars[checklayer][uid] = nil + end + end + end + + end + + local function CalculateHealthBarUIDCombinedHP(ply, uid) + + end + + local function CalculateHealthBarLayerCombinedHP(ply, layer) + + end + + local function GatherExtraHPBars(ply) + if not ply.pac_healthbars then return 0,nil end + local built_tbl = {} + local total_hp_value = 0 + + for layer,tbl in pairs(ply.pac_healthbars) do + built_tbl[layer] = {} + local layer_total = 0 + for uid,value in pairs(tbl) do + built_tbl[layer][uid] = value + total_hp_value = total_hp_value + value + layer_total = layer_total + value + end + end + return total_hp_value,built_tbl + + end + + --simulate on a healthbar layers copy + local function GetPredictedHPBarDamage(ply, dmg) + local BARS_COPY = {} + if ply.pac_healthbars then + BARS_COPY = table.Copy(ply.pac_healthbars) + else --this can happen with non-player ents + return dmg,nil,nil + end + + local remaining_dmg = dmg or 0 + local surviving_layer = 15 + local total_hp_value,built_tbl = GatherExtraHPBars(ply) + local side_effect_dmg = 0 + + if not built_tbl or total_hp_value == 0 then --no shields + return dmg,nil,nil + end + + for layer=15,0,-1 do --go progressively inward in the layers + if BARS_COPY[layer] then + surviving_layer = layer + for uid,value in pairs(BARS_COPY[layer]) do --check the healthbars by uid + + if value > 0 then --skip 0 HP healthbars + + local remainder = math.max(0,remaining_dmg - BARS_COPY[layer][uid]) + + local breakthrough_dmg = math.min(remaining_dmg, value) + + if remaining_dmg > value then --break through one of the uid clusters + surviving_layer = layer - 1 + BARS_COPY[layer][uid] = 0 + else + BARS_COPY[layer][uid] = math.max(0, value - remaining_dmg) + end + + local absorbfactor = ply.pac_healtbar_uid_absorbfactor[uid] + side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor + + remaining_dmg = math.max(0,remaining_dmg - value) + end + + end + end + end + return remaining_dmg,surviving_layer,side_effect_dmg + end + + --do the calculation and reduce the player's underlying values + local function GetHPBarDamage(ply, dmg) + local remaining_dmg = dmg or 0 + local surviving_layer = 15 + local total_hp_value,built_tbl = GatherExtraHPBars(ply) + local side_effect_dmg = 0 + + if not built_tbl or total_hp_value == 0 then --no shields + return dmg,nil,nil + end + + for layer=15,0,-1 do --go progressively inward in the layers + if ply.pac_healthbars[layer] then + surviving_layer = layer + for uid,value in pairs(ply.pac_healthbars[layer]) do --check the healthbars by uid + + if value > 0 then --skip 0 HP healthbars + + local remainder = math.max(0,remaining_dmg - ply.pac_healthbars[layer][uid]) + + local breakthrough_dmg = math.min(remaining_dmg, value) + + if remaining_dmg > value then --break through one of the uid clusters + surviving_layer = layer - 1 + ply.pac_healthbars[layer][uid] = 0 + else + ply.pac_healthbars[layer][uid] = math.max(0, value - remaining_dmg) + end + + local absorbfactor = ply.pac_healtbar_uid_absorbfactor[uid] + side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor + + remaining_dmg = math.max(0,remaining_dmg - value) + end + + end + end + end + + return remaining_dmg,surviving_layer,side_effect_dmg + end + + local function SendUpdateHealthBars(target) + if not target:IsPlayer() or not target.pac_healthbars then return end + net.Start("pac_update_healthbars") + net.WriteEntity(target) + net.WriteTable(target.pac_healthbars) + net.Broadcast() + end + + --healthbars work with a 2 levels-deep table + --for each player, an index table (priority) to decide which layer is damaged first + --for each layer, one table for each part uid + --for each uid, we have the current uid bar cluster's health value + --instead of keeping track of every bar, it will update the status with a remainder calculation + + --ply.pac_healthbars + --ply.pac_healthbars[layer] + --ply.pac_healthbars[layer][part_uid] = healthvalue + + + --apply hitscan consents, eat into extra healthbars first and calculate final damage multipliers from pac3 + hook.Add( "EntityTakeDamage", "ApplyPACDamageModifiers", function( target, dmginfo ) + if target:IsPlayer() then + local cumulative_mult = GatherDamageScales(target) + + dmginfo:ScaleDamage(cumulative_mult) + local remaining_dmg,surviving_layer,side_effect_dmg = GetHPBarDamage(target, dmginfo:GetDamage()) + + + if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and hitscan_consents[target] == false then + dmginfo:SetDamage(0) + else + local total_hp_value,built_tbl = GatherExtraHPBars(target) + if surviving_layer == nil or total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult + + if cumulative_mult < 0 then + target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(dmginfo:GetDamage()),0,target:GetMaxHealth()))) + return true + else + dmginfo:SetDamage(remaining_dmg) + if target.pac_healthbars then SendUpdateHealthBars(target) end + end + + else --shields = use the calculated cumulative side effect damage from each uid's related absorbfactor + + if side_effect_dmg < 0 then + target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(side_effect_dmg),0,target:GetMaxHealth()))) + return true + else + dmginfo:SetDamage(side_effect_dmg + remaining_dmg) + SendUpdateHealthBars(target) + end + + end + + end + end + end) + + local function MergeTargetsByID(tbl1, tbl2) + for i,v in ipairs(tbl2) do + tbl1[v:EntIndex()] = v + end + end + + local function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply) + local base_damage = tbl.Damage + local ent_count = 0 + local ply_count = 0 + local ply_prog_count = 0 + for i,v in pairs(ents_hits) do + if not (v:IsPlayer() or v:IsNPC() or string.find(v:GetClass(), "npc_")) and not tbl.PointEntities then ents_hits[i] = nil end + if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil + else + ent_count = ent_count + 1 + --print(v, "counted") + if v:IsPlayer() then ply_count = ply_count + 1 end + end + end + + --dangerous conditions: absurd amounts of entities, damaging a large percentage of the server's players beyond a certain point + if TooManyEnts(ent_count) or ((ply_count) > 12 and (ply_count > player_fraction:GetFloat() * player.GetCount())) then + print("early exit") + return false,false,nil,{},{} + end + + local pac_sv_damage_zone_allow_dissolve = GetConVar("pac_sv_damage_zone_allow_dissolve"):GetBool() + local pac_sv_prop_protection = global_combat_prop_protection:GetBool() + + local inflictor = dmg_info:GetInflictor() + local attacker = dmg_info:GetAttacker() + + local kill = false --whether a kill was done + local hit = false --whether a hit was done + local max_dmg = 0 --the max damage applied to targets. it should give the same damage by default, but I'm accounting for targets that can modify their damage + local successful_hit_ents = {} + local successful_kill_ents = {} + + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + + --the function to determine if we can dissolve, based on policy and setting factors + local function IsDissolvable(ent) + local dissolvable = true + local prop_protected, reason = IsPropProtected(ent, attacker) + local prop_protected_final = prop_protected and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false + + if ent:IsPlayer() then + if not kill then dissolvable = false + elseif damage_zone_consents[ent] == false then dissolvable = false end + elseif inflictor == ent then + dissolvable = false --do we allow that? + end + if ent:IsWeapon() and IsValid(ent:GetCreator()) then + dissolvable = false + end + if ent:CreatedByMap() then + dissolvable = false + if ent:GetClass() == "prop_physics" then dissolvable = true end + end + if damageable_point_ent_classes[ent:GetClass()] == false then + dissolvable = false + end + if prop_protected_final then + dissolvable = false + end + return dissolvable + end + + local dissolver_entity = NULL + local function dissolve(target, attacker, typ) + local dissolver_ent = ents.Create("env_entity_dissolver") + dissolver_ent:Spawn() + target:SetName(tostring({})) + dissolver_ent:SetKeyValue("dissolvetype", tostring(typ)) + dissolver_ent:Fire("Dissolve", target:GetName()) + timer.Simple(5, function() SafeRemoveEntity(dissolver_ent) end) + dissolver_entity = dissolver_ent + end + + --the giga function to determine if we can damage + local function DMGAllowed(ent) + + if ent:Health() == 0 then return false end --immediately exclude entities with 0 health + local canhit = false --whether the policies allow the hit + local prop_protected_consent = ent:GetCreator() ~= inflictor and ent ~= inflictor and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false-- and ent:GetCreator() ~= inflictor + local contraption = IsPossibleContraptionEntity(ent) + local bot_exception = true + if ent:IsPlayer() then + if ent:IsBot() then bot_exception = true end + end + --first pass: entity class blacklist + if IsEntity(ent) and ((damageable_point_ent_classes[ent:GetClass()] ~= false) or ((damageable_point_ent_classes[ent:GetClass()] == nil) or (damageable_point_ent_classes[ent:GetClass()] == true))) then + --second pass: the damagezone's settings + --1.player hurt self if asked + if (tbl.AffectSelf) and ent == inflictor then + canhit = true + --2.main target types : players, NPC, point entities + elseif ((ent:IsPlayer() and tbl.Players) or (tbl.NPC and (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity)) or tbl.PointEntities) + and --one of the base classes + (damageable_point_ent_classes[ent:GetClass()] ~= false) --non-blacklisted class + and --enforce prop protection + (bot_exception or (ent:GetCreator() == inflictor or ent == inflictor or (ent:GetCreator() ~= inflictor and pac_sv_prop_protection and damage_zone_consents[ent:GetCreator()] == true) or not pac_sv_prop_protection)) + then + canhit = true + if ent:IsPlayer() and tbl.Players then + --rules for players: + --self can always hurt itself if asked to + if (ent == inflictor and tbl.AffectSelf) then canhit = true + --self shouldn't hurt itself if asked not to + elseif (ent == inflictor and not tbl.AffectSelf) then canhit = false + --other players need to consent, bots don't care about it + elseif damage_zone_consents[ent] == true or ent:IsBot() then canhit = true + --other players that didn't consent are excluded + else canhit = false end + + elseif (tbl.NPC and damageable_point_ent_classes[ent:GetClass()] ~= false) or (tbl.PointEntities and (damageable_point_ent_classes[ent:GetClass()] == true)) then + canhit = true + end + + --apply prop protection + if IsPropProtected(ent, inflictor) or prop_protected_consent then + canhit = false + end + + end + + end + + return canhit + end + + local function IsLiving(ent) --players and NPCs + return ent:IsPlayer() or (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) + end + + --final action to apply the DamageInfo + local function DoDamage(ent) + --add the max hp-scaled damage calculated with this entity's max health + tbl.Damage = base_damage + tbl.MaxHpScaling * ent:GetMaxHealth() + dmg_info:SetDamage(tbl.Damage) + --we'll need to find out whether the damage will crack open a player's extra bars + local de_facto_dmg = GetPredictedHPBarDamage(ent, tbl.Damage) + + local distance = (ent:GetPos()):Distance(pos) + + local fraction = math.pow(math.Clamp(1 - distance / math.Clamp(math.max(tbl.Radius, tbl.Length),1,50000),0,1),tbl.DamageFalloffPower) + + if tbl.DamageFalloff then + dmg_info:SetDamage(fraction * tbl.Damage) + end + + successful_hit_ents[ent] = true + --fire bullets if asked + local ents2 = {inflictor} + if tbl.Bullet then + for _,v in ipairs(ents_hits) do + if v ~= ent then table.insert(ents2,v) end + end + + traceresult = util.TraceLine({filter = ents2, start = pos, endpos = pos + 50000*(ent:WorldSpaceCenter() - dmg_info:GetAttacker():WorldSpaceCenter())}) + + bullet.Dir = traceresult.Normal + bullet.Src = traceresult.HitPos + traceresult.HitNormal*5 + dmg_info:GetInflictor():FireBullets(bullet) + + end + + if tbl.DamageType == "heal" then + + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) + elseif tbl.DamageType == "armor" then + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) + else + --only "living" entities can be killed, and we checked generic entities with a ghost 0 health previously + + --now, after checking the de facto damage after extra healthbars, there's a 80% absorbtion ratio of armor. + --so, the kill condition is either: + --if damage is 500% of health (no amount will save you, because the remainder of 80% means death) + --if damage is more than 125% of armor, and damage is more than health+armor + if IsLiving(ent) and ent:Health() - de_facto_dmg <= 0 then + if ent.Armor then + + if not (de_facto_dmg > 5*ent:Health()) and not (de_facto_dmg > 1.25*ent:Armor() and de_facto_dmg > ent:Health() + ent:Armor()) then + kill = false + else + kill = true + end + else + kill = true + end + + end + if tbl.DoNotKill then + kill = false --durr + end + if kill then successful_kill_ents[ent] = true end + + --remove weapons on kill if asked + if kill and not ent:IsPlayer() and tbl.RemoveNPCWeaponsOnKill and pac_sv_damage_zone_allow_dissolve then + if ent:IsNPC() then + if #ent:GetWeapons() >= 1 then + for _,wep in pairs(ent:GetWeapons()) do + SafeRemoveEntity(wep) + end + end + end + end + + --leave at a critical health + if tbl.DoNotKill then + local dmg_info2 = DamageInfo() + + dmg_info2:SetDamagePosition(ent:NearestPoint(pos)) + dmg_info2:SetReportedPosition(pos) + dmg_info2:SetDamage( math.min(ent:Health() - tbl.CriticalHealth, tbl.Damage)) + dmg_info2:IsBulletDamage(tbl.Bullet) + dmg_info2:SetDamageForce(Vector(0,0,0)) + + dmg_info2:SetAttacker(attacker) + + dmg_info2:SetInflictor(inflictor) + + ent:TakeDamageInfo(dmg_info2) + max_dmg = math.max(max_dmg, dmg_info2:GetDamage()) + + --finally we reached the normal damage event! + else + if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) and pac_sv_damage_zone_allow_dissolve then + dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType]) + end + dmg_info:SetDamagePosition(ent:NearestPoint(pos)) + dmg_info:SetReportedPosition(pos) + ent:TakeDamageInfo(dmg_info) + max_dmg = math.max(max_dmg, dmg_info:GetDamage()) + end + end + + if tbl.DamageType == "fire" then ent:Ignite(5) end + end + + --the forward bullet, if applicable and no entity is found + if ent_count == 0 then + if tbl.Bullet then + dmg_info:GetInflictor():FireBullets(bullet) + end + return hit,kill,dmg,successful_hit_ents,successful_kill_ents + end + + --look through each entity + for _,ent in pairs(ents_hits) do + + local canhit = DMGAllowed(ent) + local oldhp = ent:Health() + if canhit then + if ent:IsPlayer() and ply_count > 5 then + --jank fix to delay players damage in case they die all at once overflowing the reliable buffer + timer.Simple(ply_prog_count / 32, function() DoDamage(ent) end) + ply_prog_count = ply_prog_count + 1 + else + DoDamage(ent) + end + end + if not hit and (oldhp > 0 and canhit) then hit = true end + end + + return hit,kill,dmg,successful_hit_ents,successful_kill_ents + end + + + local hitbox_ids = { + ["Box"] = 1, + ["Cube"] = 2, + ["Sphere"] = 3, + ["Cylinder"] = 4, + ["CylinderHybrid"] = 5, + ["CylinderSpheres"] = 6, + ["Cone"] = 7, + ["ConeHybrid"] = 8, + ["ConeSpheres"] = 9, + ["Ray"] = 10 + } + + local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + fire = 31, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 32, + dissolve_heavy_electrical = 33, + dissolve_light_electrical = 34, + dissolve_core_effect = 35, + + heal = 36, + armor = 37, + } + + local tracer_ids = { + ["Tracer"] = 1, + ["AR2Tracer"] = 2, + ["HelicopterTracer"] = 3, + ["AirboatGunTracer"] = 4, + ["AirboatGunHeavyTracer"] = 5, + ["GaussTracer"] = 6, + ["HunterTracer"] = 7, + ["StriderTracer"] = 8, + ["GunshipTracer"] = 9, + ["ToolgunTracer"] = 10, + ["LaserTracer"] = 11 + } + + --second stage of force: apply + local function ProcessForcesList(ents_hits, tbl, pos, ang, ply) + local ent_count = 0 + for i,v in pairs(ents_hits) do + if pre_excluded_ent_classes[v:GetClass()] or (v:IsNPC() and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectSelf)) then ents_hits[i] = nil + else ent_count = ent_count + 1 end + end + + if TooManyEnts(ent_count) then return end + for _,ent in pairs(ents_hits) do + local phys_ent + if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) + and ( + ent:IsPlayer() + or (string.find(ent:GetClass(), "npc") ~= nil) + or ent:IsNPC() + or physics_point_ent_classes[ent:GetClass()] + or string.find(ent:GetClass(),"item_") + or string.find(ent:GetClass(),"ammo_") + or (ent:IsWeapon() and not IsValid(ent:GetOwner())) + ) then + + local is_phys = true + if ent:GetPhysicsObject() ~= nil then + phys_ent = ent:GetPhysicsObject() + if (string.find(ent:GetClass(), "npc") ~= nil) then + phys_ent = ent + end + else + phys_ent = ent + is_phys = false + end + + local oldvel + + if IsValid(phys_ent) then + oldvel = phys_ent:GetVelocity() + else + oldvel = Vector(0,0,0) + end + + + local addvel = Vector(0,0,0) + local add_angvel = Vector(0,0,0) + + local ent_center = ent:WorldSpaceCenter() or ent:GetPos() + + local dir = ent_center - pos --part + local dir2 = ent_center - tbl.Locus_pos--locus + + local dist_multiplier = 1 + local damping_dist_mult = 1 + local up_mult = 1 + local distance = (ent_center - pos):Length() + local height_delta = pos.z + tbl.LevitationHeight - ent_center.z + + --what it do + --if delta is -100 (ent is lower than the desired height), that means +100 adjustment direction + --height decides how much to knee the force until it equalizes at 0 + --clamp the delta to the ratio levitation height + + if tbl.Levitation then + up_mult = math.Clamp(height_delta / (5 + math.abs(tbl.LevitationHeight)),-1,1) + end + + if tbl.BaseForceAngleMode == "Radial" then --radial on self + addvel = dir:GetNormalized() * tbl.BaseForce + elseif tbl.BaseForceAngleMode == "Locus" then --radial on locus + addvel = dir2:GetNormalized() * tbl.BaseForce + elseif tbl.BaseForceAngleMode == "Local" then --forward on self + addvel = ang:Forward() * tbl.BaseForce + end + + if tbl.VectorForceAngleMode == "Global" then --global + addvel = addvel + tbl.AddedVectorForce + elseif tbl.VectorForceAngleMode == "Local" then --local on self + addvel = addvel + +ang:Forward()*tbl.AddedVectorForce.x + +ang:Right()*tbl.AddedVectorForce.y + +ang:Up()*tbl.AddedVectorForce.z + + elseif tbl.VectorForceAngleMode == "Radial" then --relative to locus or self + ang2 = dir:Angle() + addvel = addvel + +ang2:Forward()*tbl.AddedVectorForce.x + +ang2:Right()*tbl.AddedVectorForce.y + +ang2:Up()*tbl.AddedVectorForce.z + elseif tbl.VectorForceAngleMode == "RadialNoPitch" then --relative to locus or self + dir.z = 0 + ang2 = dir:Angle() + addvel = addvel + +ang2:Forward()*tbl.AddedVectorForce.x + +ang2:Right()*tbl.AddedVectorForce.y + +ang2:Up()*tbl.AddedVectorForce.z + end + + + + if tbl.TorqueMode == "Global" then + add_angvel = tbl.Torque + elseif tbl.TorqueMode == "Local" then + add_angvel = ang:Forward()*tbl.Torque.x + ang:Right()*tbl.Torque.y + ang:Up()*tbl.Torque.z + elseif tbl.TorqueMode == "TargetLocal" then + add_angvel = tbl.Torque + elseif tbl.TorqueMode == "Radial" then + ang2 = dir:Angle() + addvel = ang2:Forward()*tbl.Torque.x + ang2:Right()*tbl.Torque.y + ang2:Up()*tbl.Torque.z + end + + local islocaltorque = tbl.TorqueMode == "TargetLocal" + + if is_phys and tbl.AccountMass then + if not (string.find(ent:GetClass(), "npc") ~= nil) then + addvel = addvel * (1 / math.max(phys_ent:GetMass(),0.1)) + else + addvel = addvel + end + add_angvel = add_angvel * (1 / math.max(phys_ent:GetMass(),0.1)) + end + + if tbl.Falloff then + dist_multiplier = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + if tbl.ReverseFalloff then + dist_multiplier = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + + if tbl.DampingFalloff then + damping_dist_mult = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + if tbl.DampingReverseFalloff then + damping_dist_mult = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + damping_dist_mult = damping_dist_mult + local final_damping = 1 - (tbl.Damping * damping_dist_mult) + + if tbl.Levitation then + addvel.z = addvel.z * up_mult + end + + addvel = addvel * dist_multiplier + add_angvel = add_angvel * dist_multiplier + + local unconsenting_owner = ent:GetCreator() ~= ply and force_consents[ent:GetCreator()] == false + + if (ent:IsPlayer() and tbl.Players) or (ent == ply and tbl.AffectSelf) then + if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then + phys_ent:SetVelocity(oldvel * (-final_damping) + addvel) + ent:SetVelocity(oldvel * (-final_damping) + addvel) + end + + elseif (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) and tbl.PhysicsProps then + if not IsPropProtected(ent, ply) and not (global_combat_prop_protection:GetBool() and unconsenting_owner) then + if IsValid(phys_ent) then + ent:PhysWake() + ent:SetVelocity(final_damping * oldvel + addvel) + if islocaltorque then + phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) + phys_ent:AddAngleVelocity(add_angvel) + + else + phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) + add_angvel = phys_ent:WorldToLocalVector( add_angvel ) + phys_ent:ApplyTorqueCenter(add_angvel) + end + ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces + phys_ent:SetVelocity((oldvel * final_damping) + addvel) + end + end + elseif (ent:IsNPC() or string.find(ent:GetClass(), "npc") ~= nil) and tbl.NPC then + if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then + if phys_ent:GetVelocity():Length() > 500 then + local vec = oldvel + addvel + local clamp_vec = vec:GetNormalized()*500 + ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer + + else ent:SetVelocity((oldvel * final_damping) + addvel) end + end + elseif tbl.PointEntities then + if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then + phys_ent:SetVelocity(final_damping * oldvel + addvel) + end + end + hook.Run("PhysicsUpdate", ent) + hook.Run("PhysicsUpdate", phys_ent) + end + + end + end + --first stage of force: look for targets and determine force amount if continuous + local function ImpulseForce(tbl, pos, ang, ply) + local ftime = 0.016 --approximate tick duration + if tbl.Continuous then + tbl.BaseForce = tbl.BaseForce1 * ftime * 3.3333 --weird value to equalize how 600 cancels out gravity + tbl.AddedVectorForce = tbl.AddedVectorForce1 * ftime * 3.3333 + tbl.Torque = tbl.Torque1 * ftime * 3.3333 + else + tbl.BaseForce = tbl.BaseForce1 + tbl.AddedVectorForce = tbl.AddedVectorForce1 + tbl.Torque = tbl.Torque1 + end + + if tbl.HitboxMode == "Sphere" then + local ents_hits = ents.FindInSphere(pos, tbl.Radius) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Box" then + local mins + local maxs + if tbl.HitboxMode == "Box" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) + end + + local ents_hits = ents.FindInBox(mins, maxs) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cylinder" then + local ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cone" then + local ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + + if tbl.Bullet then + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + dmg_info:GetInflictor():FireBullets(bullet) + end + end + end + + + --consent message from clients + net.Receive("pac_signal_player_combat_consent", function(len,ply) + local grab = net.ReadBool() -- GetConVar("pac_client_grab_consent"):GetBool() + local damagezone = net.ReadBool() -- GetConVar("pac_client_damage_zone_consent"):GetBool() + local calcview = net.ReadBool() -- GetConVar("pac_client_lock_camera_consent"):GetBool() + local force = net.ReadBool() -- GetConVar("pac_client_force_consent"):GetBool() + local hitscan = net.ReadBool() -- GetConVar("pac_client_hitscan_consent"):GetBool() + grab_consents[ply] = grab + damage_zone_consents[ply] = damagezone + calcview_consents[ply] = calcview + force_consents[ply] = force + hitscan_consents[ply] = hitscan + end) + + --lock break order from client + net.Receive("pac_signal_stop_lock", function(len,ply) + ApplyLockState(ply, false) + MsgC(Color(0,255,255), "Requesting lock break!\n") + if ply.grabbed_by then --directly go for the grabbed_by player + net.Start("pac_request_lock_break") + net.WriteEntity(ply) + net.WriteString("") + net.Send(ply.grabbed_by) + end + --What if there's more? try to find it AMONG US SUS! + for _,ent in pairs(player.GetAll()) do + if ent.grabbed_ents and ent ~= ply.grabbed_by then --a player! time to inspect! but skip the already found grabber + for _,grabbed in pairs(ent.grabbed_ents) do --check all her entities + if ply == grabbed then --that's us! + net.Start("pac_request_lock_break") + net.WriteEntity(ply) + net.WriteString(ply.grabbed_by_uid) + net.Send(ent) + end + end + end + end + end) + + concommand.Add("pac_damage_zone_whitelist_entity_class", function(ply, cmd, args, argStr) + for _,v in pairs(string.Explode(";",argStr)) do + damageable_point_ent_classes[v] = true + print("added " .. v .. " to the entities you can damage") + end + PrintTable(damageable_point_ent_classes) + end) + + concommand.Add("pac_damage_zone_blacklist_entity_class", function(ply, cmd, args, argStr) + for _,v in pairs(string.Explode(";",argStr)) do + damageable_point_ent_classes[v] = false + print("removed " .. v .. " from the entities you can damage") + end + PrintTable(damageable_point_ent_classes) + end) + + + util.AddNetworkString("pac_signal_player_combat_consent") + util.AddNetworkString("pac_request_blocked_parts") + util.AddNetworkString("pac_inform_blocked_parts") + + local FINAL_BLOCKED_COMBAT_FEATURES = { + hitscan = false, + damage_zone = false, + lock = false, + force = false, + health_modifier = false, + } + + + --[[function net.Incoming( len, client ) + + local i = net.ReadHeader() + local strName = util.NetworkIDToString( i ) + if strName ~= "pac_in_editor_posang" and strName ~= "DrGBasePlayerLuminosity" then + print(strName, client, "message with " .. len .." bits") + end + + if ( !strName ) then return end + + local func = net.Receivers[ strName:lower() ] + if ( !func ) then return end + + -- + -- len includes the 16 bit int which told us the message name + -- + len = len - 16 + + func( len, client ) + + end]] + + local force_hitbox_ids = {["Box"] = 0,["Cube"] = 1,["Sphere"] = 2,["Cylinder"] = 3,["Cone"] = 4,["Ray"] = 5} + local base_force_mode_ids = {["Radial"] = 0, ["Locus"] = 1, ["Local"] = 2} + local vect_force_mode_ids = {["Global"] = 0, ["Local"] = 1, ["Radial"] = 2, ["RadialNoPitch"] = 3} + local ang_torque_mode_ids = {["Global"] = 0, ["TargetLocal"] = 1, ["Local"] = 2, ["Radial"] = 3} + local nextcheckforce = SysTime() + + function DeclareForceReceivers() + util.AddNetworkString("pac_request_force") + --the force part impulse request net message + net.Receive("pac_request_force", function(len,ply) + --server allow + if not force_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + + local tbl = {} + local pos = net.ReadVector() + if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then return end + local ang = net.ReadAngle() + tbl.Locus_pos = net.ReadVector() + local on = net.ReadBool() + + tbl.UniqueID = net.ReadString() + + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Force part: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + + hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) + active_force_ids[tbl.UniqueID] = nil + + return + end + + tbl.RootPartOwner = net.ReadEntity() + + tbl.HitboxMode = table.KeyFromValue(force_hitbox_ids, net.ReadUInt(4)) + tbl.BaseForceAngleMode = table.KeyFromValue(base_force_mode_ids, net.ReadUInt(3)) + tbl.VectorForceAngleMode = table.KeyFromValue(vect_force_mode_ids, net.ReadUInt(2)) + tbl.TorqueMode = table.KeyFromValue(ang_torque_mode_ids, net.ReadUInt(2)) + + tbl.Length = net.ReadInt(16) + tbl.Radius = net.ReadInt(16) + + tbl.BaseForce1 = net.ReadInt(18) + tbl.AddedVectorForce1 = net.ReadVector() + tbl.Torque1 = net.ReadVector() + + tbl.Damping = net.ReadUInt(10)/1000 + tbl.LevitationHeight = net.ReadInt(14) + + tbl.Continuous = net.ReadBool() + tbl.AccountMass = net.ReadBool() + tbl.Falloff = net.ReadBool() + tbl.ReverseFalloff = net.ReadBool() + tbl.DampingFalloff = net.ReadBool() + tbl.DampingReverseFalloff = net.ReadBool() + tbl.Levitation = net.ReadBool() + tbl.AffectSelf = net.ReadBool() + tbl.Players = net.ReadBool() + tbl.PhysicsProps = net.ReadBool() + tbl.NPC = net.ReadBool() + + --server limits + tbl.Radius = math.Clamp(tbl.Radius,-force_max_radius:GetInt(),force_max_radius:GetInt()) + tbl.Length = math.Clamp(tbl.Length,-force_max_length:GetInt(),force_max_length:GetInt()) + tbl.BaseForce = math.Clamp(tbl.BaseForce1,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.AddedVectorForce1.x = math.Clamp(tbl.AddedVectorForce1.x,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.AddedVectorForce1.y = math.Clamp(tbl.AddedVectorForce1.y,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.AddedVectorForce1.z = math.Clamp(tbl.AddedVectorForce1.z,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.Torque1.x = math.Clamp(tbl.Torque1.x,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.Torque1.y = math.Clamp(tbl.Torque1.y,-force_max_amount:GetInt(),force_max_amount:GetInt()) + tbl.Torque1.z = math.Clamp(tbl.Torque1.z,-force_max_amount:GetInt(),force_max_amount:GetInt()) + + if on then + if tbl.Continuous then + hook.Add("Tick", "pac_force_hold"..tbl.UniqueID, function() + ImpulseForce(tbl, pos, ang, ply) + end) + + active_force_ids[tbl.UniqueID] = CurTime() + else + active_force_ids[tbl.UniqueID] = nil + end + ImpulseForce(tbl, pos, ang, ply) + else + hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) + active_force_ids[tbl.UniqueID] = nil + end + + --check bad or inactive hooks + for i,v in pairs(active_force_ids) do + if not v then + hook.Remove("Tick", "pac_force_hold"..i) + --print("invalid force") + elseif v + 0.1 < CurTime() then + hook.Remove("Tick", "pac_force_hold"..i) + --print("outdated force") + end + end + + end) + + hook.Add("Tick", "pac_check_force_hooks", function() + if nextcheckforce > SysTime() then return else nextcheckforce = SysTime() + 0.2 end + for i,v in pairs(active_force_ids) do + if not v then + hook.Remove("Tick", "pac_force_hold"..i) + --print("removed an invalid force") + elseif v + 0.1 < CurTime() then + hook.Remove("Tick", "pac_force_hold"..i) + --print("removed an outdated force") + end + end + + end) + end + + function DeclareDamageZoneReceivers() + util.AddNetworkString("pac_request_zone_damage") + util.AddNetworkString("pac_hit_results") + net.Receive("pac_request_zone_damage", function(len,ply) + --server allow + if not damagezone_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Damage zone: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end + + local pos = net.ReadVector() + if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then return end + local ang = net.ReadAngle() + local tbl = {} + + tbl.Damage = net.ReadUInt(28) + tbl.MaxHpScaling = net.ReadUInt(10) / 1000 + tbl.Length = net.ReadInt(16) + tbl.Radius = net.ReadInt(16) + + tbl.AffectSelf = net.ReadBool() + tbl.NPC = net.ReadBool() + tbl.Players = net.ReadBool() + tbl.PointEntities = net.ReadBool() + + tbl.HitboxMode = table.KeyFromValue(hitbox_ids, net.ReadUInt(5)) + tbl.DamageType = table.KeyFromValue(damage_ids, net.ReadUInt(7)) + + tbl.Detail = net.ReadInt(6) + tbl.ExtraSteps = net.ReadInt(4) + tbl.RadialRandomize = net.ReadInt(7) / 8 + tbl.PhaseRandomize = net.ReadInt(7) / 8 + tbl.DamageFalloff = net.ReadBool() + tbl.DamageFalloffPower = net.ReadInt(12) / 8 + tbl.Bullet = net.ReadBool() + tbl.DoNotKill = net.ReadBool() + tbl.CriticalHealth = net.ReadUInt(16) + tbl.RemoveNPCWeaponsOnKill = net.ReadBool() + + local dmg_info = DamageInfo() + + --server limits + tbl.Radius = math.Clamp(tbl.Radius,-damagezone_max_radius:GetInt(),damagezone_max_radius:GetInt()) + tbl.Length = math.Clamp(tbl.Length,-damagezone_max_length:GetInt(),damagezone_max_length:GetInt()) + tbl.Damage = math.Clamp(tbl.Damage,-damagezone_max_damage:GetInt(),damagezone_max_damage:GetInt()) + + dmg_info:SetDamage(tbl.Damage) + dmg_info:IsBulletDamage(tbl.Bullet) + dmg_info:SetDamageForce(Vector(0,0,0)) + dmg_info:SetAttacker(ply) + dmg_info:SetInflictor(ply) + + local ents_hits + local kill = false + local hit = false + + dmg_info:SetDamageType(damage_types[tbl.DamageType]) + + local ratio + if tbl.Radius == 0 then ratio = tbl.Length + else ratio = math.abs(tbl.Length / tbl.Radius) end + + if tbl.HitboxMode == "Sphere" then + ents_hits = ents.FindInSphere(pos, tbl.Radius) + + elseif tbl.HitboxMode == "Box" or tbl.HitboxMode == "Cube" then + local mins + local maxs + if tbl.HitboxMode == "Box" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) + elseif tbl.HitboxMode == "Cube" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Radius) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius) + end + + ents_hits = ents.FindInBox(mins, maxs) + + elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then + ents_hits = {} + if tbl.Radius ~= 0 then + local sides = tbl.Detail + if tbl.Detail < 1 then sides = 1 end + local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + end + steps = math.max(steps + math.abs(tbl.ExtraSteps),1) + + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier + phase = math.random() + local ray_thickness = math.Clamp(0.5*math.log(tbl.Radius) + 0.05*tbl.Radius,0,10)*(1 - 0.7*ringnumber) + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + local startpos = pos + x + y + local endpos = pos + ang:Forward()*tbl.Length + x + y + MergeTargetsByID(ents_hits, ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness))) + end + end + if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then + --fast sphere check on the wide end + if tbl.Length/tbl.Radius >= 2 then + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius), tbl.Radius)) + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Radius, tbl.Radius)) + if tbl.Radius ~= 0 then + local counter = 0 + for i=math.floor(tbl.Length / tbl.Radius) - 1,1,-1 do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Radius*i), tbl.Radius)) + if counter == 100 then break end + counter = counter + 1 + end + end + end + end + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + + elseif tbl.HitboxMode == "CylinderSpheres" then + ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + + elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then + ents_hits = {} + if tbl.Radius ~= 0 then + local sides = tbl.Detail + if tbl.Detail < 1 then sides = 1 end + local startpos = pos-- + Vector(0, self.Radius,self.Radius) + local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection + local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then + area_factor = 0.15*area_factor + steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) + end + steps = math.max(steps + math.abs(tbl.ExtraSteps),1) + local timestart = SysTime() + local casts = 0 + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the ringnumber multiplier + phase = math.random() + local ray_thickness = 5 * (2 - ringnumber) + + for i=1,0,-1/sides do + if ringnumber == 0 then i = 0 end + x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) + local endpos = pos + ang:Forward()*tbl.Length + x + y + MergeTargetsByID(ents_hits,ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness))) + casts = casts + 1 + end + end + if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then + --fast sphere check on the wide end + local radius_multiplier = math.atan(math.abs(ratio)) / (1.5 + 0.1*math.sqrt(ratio)) + if ratio > 0.5 then + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius * radius_multiplier), tbl.Radius * radius_multiplier)) + end + end + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + + elseif tbl.HitboxMode == "ConeSpheres" then + ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + + if tbl.Bullet then + local bullet = {} + bullet.Src = pos + ang:Forward() + bullet.Dir = ang:Forward()*50000 + bullet.Damage = -1 + bullet.Force = 0 + bullet.Entity = dmg_info:GetAttacker() + dmg_info:GetInflictor():FireBullets(bullet) + end + end + hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply) + highest_dmg = highest_dmg or 0 + net.Start("pac_hit_results", true) + net.WriteBool(hit) + net.WriteBool(kill) + net.WriteFloat(highest_dmg) + net.WriteTable(successful_hit_ents) + net.WriteTable(successful_kill_ents) + net.Broadcast() + end) + end + + local nextchecklock = CurTime() + function DeclareLockReceivers() + util.AddNetworkString("pac_request_position_override_on_entity_teleport") + util.AddNetworkString("pac_request_position_override_on_entity_grab") + util.AddNetworkString("pac_request_angle_reset_on_entity") + util.AddNetworkString("pac_lock_imposecalcview") + util.AddNetworkString("pac_signal_stop_lock") + util.AddNetworkString("pac_request_lock_break") + util.AddNetworkString("pac_mark_grabbed_ent") + util.AddNetworkString("pac_notify_grabbed_player") + --The lock part grab request net message + net.Receive("pac_request_position_override_on_entity_grab", function(len, ply) + --server allow + if not lock_allow:GetBool() then return end + if not lock_allow_grab:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Lock grab: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end + + local did_grab = true + local need_breakup = false + local breakup_condition = "" + --monstrous net message + local is_first_time = net.ReadBool() + local lockpart_UID = net.ReadString() + local pos = net.ReadVector() + local ang = net.ReadAngle() + local override_ang = net.ReadBool() + local override_eyeang = net.ReadBool() + local targ_ent = net.ReadEntity() + local auth_ent = net.ReadEntity() + local override_viewposition = net.ReadBool() + local alt_pos = net.ReadVector() + local alt_ang = net.ReadAngle() + local ask_drawviewer = net.ReadBool() + + if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then + ApplyLockState(targ_ent, false) + if ply.grabbed_ents then + net.Start("pac_request_lock_break") + net.WriteEntity(targ_ent) + net.WriteString(lockpart_UID) + net.WriteString(breakup_condition) + net.Send(ply) + end + return + end + + local prop_protected, reason = IsPropProtected(targ_ent, ply) + + local unconsenting_owner = targ_ent:GetCreator() ~= ply and (grab_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) + local calcview_unconsenting = targ_ent:GetCreator() ~= ply and (calcview_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and calcview_consents[targ_ent] == false)) + + if unconsenting_owner or (global_combat_prop_protection:GetBool() and prop_protected) then return end + + local targ_ent_owner = targ_ent:GetCreator() or targ_ent + local auth_ent_owner = ply + + auth_ent_owner.grabbed_ents = auth_ent_owner.grabbed_ents or {} + + local consent_break_condition = false + + if grab_consents[targ_ent_owner] == false then --if the target player is non-consenting + if targ_ent_owner == auth_ent_owner then consent_break_condition = false --player can still grab his owned entities + elseif targ_ent:IsPlayer() then --if not the same player, we cannot grab + consent_break_condition = true + breakup_condition = breakup_condition .. "cannot grab another player if they don't consent to grabs, " + elseif global_combat_prop_protection:GetBool() and targ_ent:GetCreator() ~= ply then + --if entity not owned by grabbing player, he cannot do it to other players' entities in the prop-protected mode + consent_break_condition = true + breakup_condition = breakup_condition .. "cannot grab another player's owned entities if they don't consent to grabs, " + end + end + + if not IsValid(targ_ent) then --invalid entity? + did_grab = false + return --nothing else matters, get out + end + if consent_break_condition then --any of the non-consenting conditions + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "non-consenting, " + end + + --dead ent = break + --but don't exclude about physics props + if targ_ent:Health() == 0 and not (physics_point_ent_classes[targ_ent:GetClass()] or string.find(targ_ent:GetClass(),"item_") or string.find(targ_ent:GetClass(),"ammo_") or targ_ent:IsWeapon()) then + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "dead, " + end + + if is_first_time then + if (auth_ent_owner ~= targ_ent and auth_ent_owner.grabbed_ents[targ_ent] == true) then + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "mutual grab prevention, " + end + end + + if did_grab then + + + if targ_ent:IsPlayer() and targ_ent:InVehicle() then --yank player out of vehicle + print("Kicking " .. targ_ent:Nick() .. " out of vehicle to be grabbed!") + targ_ent:ExitVehicle() + end + + if override_ang then + if not targ_ent:IsPlayer() then --non-players work with angles + targ_ent:SetAngles(ang) + else --players work with eyeangles + if override_eyeang then + + if PlayerAllowsCalcView(targ_ent) and override_viewposition then + targ_ent.nextcalcviewTick = targ_ent.nextcalcviewTick or CurTime() + if targ_ent.nextcalcviewTick < CurTime() then + net.Start("pac_lock_imposecalcview") + net.WriteBool(true) + net.WriteVector(alt_pos) + net.WriteAngle(alt_ang) + net.WriteBool(ask_drawviewer) + net.Send(targ_ent) + targ_ent.nextcalcviewTick = CurTime() + 0.1 + targ_ent.has_calcview = true + else print("skipping") end + targ_ent:SetEyeAngles(alt_ang) + targ_ent:SetAngles(alt_ang) + else + targ_ent:SetEyeAngles(ang) + targ_ent:SetAngles(ang) + end + elseif not override_eyeang or not override_viewposition or not PlayerAllowsCalcView(targ_ent) then --break any calcviews if we can't do that + if targ_ent.has_calcview then + net.Start("pac_lock_imposecalcview") + net.WriteBool(false) + net.WriteVector(Vector(0,0,0)) + net.WriteAngle(Angle(0,0,0)) + net.Send(targ_ent) + targ_ent.has_calcview = false + end + end + + end + end + + targ_ent:SetPos(pos) + + ApplyLockState(targ_ent, true) + if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end + + --@@note lock assignation! IMPORTANT + if is_first_time then --successful, first + auth_ent_owner.grabbed_ents[targ_ent] = true + targ_ent.grabbed_by = auth_ent_owner + targ_ent.grabbed_by_uid = lockpart_UID + print(auth_ent, "grabbed", targ_ent, "owner grabber is", auth_ent_owner) + end + targ_ent.grabbed_by_time = CurTime() + else + auth_ent_owner.grabbed_ents[targ_ent] = nil + targ_ent.grabbed_by_uid = nil + targ_ent.grabbed_by = nil + end + + if need_breakup then + print("stop this now! reason: " .. breakup_condition) + net.Start("pac_request_lock_break") + net.WriteEntity(targ_ent) + net.WriteString(lockpart_UID) + net.WriteString(breakup_condition) + net.Send(auth_ent_owner) + + else + if is_first_time and did_grab then + net.Start("pac_mark_grabbed_ent") + net.WriteEntity(targ_ent) + net.WriteBool(did_grab) + net.WriteString(lockpart_UID) + net.Broadcast() + + if targ_ent:IsPlayer() then + net.Start("pac_notify_grabbed_player") + net.WriteEntity(ply) + net.Send(targ_ent) + end + end + end + end) + --the lockpart teleport request net message + net.Receive("pac_request_position_override_on_entity_teleport", function(len, ply) + --server allow + if not lock_allow:GetBool() then return end + if not lock_allow_teleport:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Lock teleport: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end + + local lockpart_UID = net.ReadString() + local pos = net.ReadVector() + local ang = net.ReadAngle() + local override_ang = net.ReadBool() + + if IsValid(ply) then + if override_ang then + ply:SetEyeAngles(ang) + end + ply:SetPos(pos) + end + + end) + --the lockpart grab end request net message + net.Receive("pac_request_angle_reset_on_entity", function(len, ply) + + if not PlayerIsCombatAllowed(ply) then return end + + local ang = net.ReadAngle() + local delay = net.ReadFloat() + local targ_ent = net.ReadEntity() + local auth_ent = net.ReadEntity() + + targ_ent:SetAngles(ang) + ApplyLockState(targ_ent, false) + + end) + + + hook.Add("Tick", "pac_checklocks", function() + if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.2 end + --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check + for ent,bool in pairs(active_grabbed_ents) do + if not IsValid(ent) then + active_grabbed_ents[ent] = nil + elseif (ent.grabbed_by or bool) then + if ent.grabbed_by_time + 0.5 < CurTime() then --restore the movetype + local grabber = ent.grabbed_by + ent.grabbed_by_uid = nil + ent.grabbed_by = nil + if grabber then + grabber.grabbed_ents[ent] = false + end + + ApplyLockState(ent, false) + active_grabbed_ents[ent] = nil + end + end + end + end) + end + + function DeclareHitscanReceivers() + util.AddNetworkString("pac_hitscan") + net.Receive("pac_hitscan", function(len,ply) + + if not hitscan_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Hitscan: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end + + local bulletinfo = {} + local affect_self = net.ReadBool() + bulletinfo.Src = net.ReadVector() + local dir = net.ReadAngle() + bulletinfo.Dir = dir:Forward() + + bulletinfo.dmgtype_str = table.KeyFromValue(damage_ids, net.ReadUInt(7)) + bulletinfo.dmgtype = damage_types[bulletinfo.dmgtype_str] + bulletinfo.Spread = net.ReadVector() + bulletinfo.Damage = net.ReadUInt(28) + bulletinfo.Tracer = net.ReadUInt(8) + bulletinfo.Force = net.ReadUInt(16) + + bulletinfo.Distance = net.ReadUInt(16) + bulletinfo.Num = net.ReadUInt(9) + bulletinfo.TracerName = table.KeyFromValue(tracer_ids, net.ReadUInt(4)) + bulletinfo.DistributeDamage = net.ReadBool() + + bulletinfo.DamageFalloff = net.ReadBool() + bulletinfo.DamageFalloffDistance = net.ReadUInt(16) + bulletinfo.DamageFalloffFraction = net.ReadUInt(10) / 1000 + + local part_uid = ply:Nick() .. net.ReadString() + + bulletinfo.Num = math.Clamp(bulletinfo.Num, 1, hitscan_max_bullets:GetInt()) + bulletinfo.Damage = math.Clamp(bulletinfo.Damage, 0, hitscan_max_damage:GetInt()) + bulletinfo.DamageFalloffFraction = math.Clamp(bulletinfo.DamageFalloffFraction,0,1) + + if hitscan_spreadout_dmg:GetBool() or bulletinfo.DistributeDamage then + bulletinfo.Damage = bulletinfo.Damage / bulletinfo.Num + end + + if not affect_self then bulletinfo.IgnoreEntity = ply end + ply.pac_bullet_emitters = ply.pac_bullet_emitters or {} + ply.pac_bullet_emitters[part_uid] = ply.pac_bullet_emitters[part_uid] or ents.Create("pac_bullet_emitter") + + bulletinfo.Callback = function(atk, trc, dmg) + dmg:SetDamageType(bulletinfo.dmgtype) + if trc.Hit and IsValid(trc.Entity) then + local distance = (trc.HitPos):Distance(trc.StartPos) + local fraction = math.Clamp(1 - (1-bulletinfo.DamageFalloffFraction)*(distance / bulletinfo.DamageFalloffDistance),bulletinfo.DamageFalloffFraction,1) + local ent = trc.Entity + + if bulletinfo.dmgtype_str == "heal" then + dmg:SetDamageType(0) + ent:SetHealth(math.min(ent:Health() + fraction * dmg:GetDamage(), math.max(ent:Health(), ent:GetMaxHealth()))) + dmg:SetDamage(0) + return + elseif bulletinfo.dmgtype_str == "armor" then + dmg:SetDamageType(0) + ent:SetArmor(math.min(ent:Armor() + fraction * dmg:GetDamage(), math.max(ent:Armor(), ent:GetMaxArmor()))) + dmg:SetDamage(0) + return + end + if bulletinfo.DamageFalloff and trc.Hit and IsValid(trc.Entity) then + if not bulletinfo.dmgtype_str == "heal" and not bulletinfo.dmgtype_str == "armor" then + dmg:SetDamage(fraction * dmg:GetDamage()) + end + end + end + end + + if IsValid(ply.pac_bullet_emitters[part_uid]) then + ply.pac_bullet_emitters[part_uid]:FireBullets(bulletinfo) + else + ply.pac_bullet_emitters[part_uid] = ents.Create("pac_bullet_emitter") + end + + end) + end + + function DeclareHealthModifierReceivers() + util.AddNetworkString("pac_request_healthmod") + util.AddNetworkString("pac_update_healthbars") + net.Receive("pac_request_healthmod", function(len,ply) + if not healthmod_allow:GetBool() then return end + + --netrate enforce + if not CountNetMessage(ply) then + if netrate_enforcement_sv_monitoring:GetBool() then + MsgC(Color(255,255,0), "[PAC3] Health modifier: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + end + return + end + + local part_uid = net.ReadString() + local mod_id = net.ReadString() + local action = net.ReadString() + + if action == "MaxHealth" then + if not healthmod_allow:GetBool() then return end + local num = net.ReadUInt(32) + local follow = net.ReadBool() + if not healthmod_allow_change_maxhp:GetBool() then return end + if ply:Health() == ply:GetMaxHealth() and follow then + ply:SetHealth(num) + elseif num < ply:Health() then + ply:SetHealth(num) + end + ply:SetMaxHealth(num) + ply.pac_healthmods = ply.pac_healthmods or {} + ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} + ply.pac_healthmods[part_uid].maxhealth = num + + elseif action == "MaxArmor" then + if not healthmod_allow:GetBool() then return end + local num = net.ReadUInt(32) + local follow = net.ReadBool() + if not healthmod_allow_change_maxhp:GetBool() then return end + if ply:Armor() == ply:GetMaxArmor() and follow then + ply:SetArmor(num) + elseif num < ply:Armor() then + ply:SetArmor(num) + end + ply:SetMaxArmor(num) + ply.pac_healthmods = ply.pac_healthmods or {} + ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} + ply.pac_healthmods[part_uid].maxarmor = num + + elseif action == "DamageMultiplier" then + local scale = net.ReadFloat() + AddDamageScale(ply, mod_id, scale, part_uid) + + elseif action == "HealthBars" then + if not healthmod_allowed_extra_bars:GetBool() then return end + local num = net.ReadUInt(32) + local barsize = net.ReadUInt(32) + local layer = net.ReadUInt(4) + local absorbfactor = net.ReadFloat() + local follow = net.ReadBool() + + UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) + + elseif action == "OnRemove" then + if ply.pac_damage_scalings then + if ply.pac_damage_scalings[part_uid] then + ply.pac_damage_scalings[part_uid] = nil + end + end + if ply.pac_healthmods then + ply.pac_healthmods[part_uid] = nil + end + + FixMaxHealths(ply) + UpdateHealthBars(ply, 0, 0, 0, 0, part_uid, follow) + end + SendUpdateHealthBars(ply) + end) + end + + --[[util.AddNetworkString("pac_hitscan") + util.AddNetworkString("pac_request_position_override_on_entity_teleport") + util.AddNetworkString("pac_request_position_override_on_entity_grab") + util.AddNetworkString("pac_request_angle_reset_on_entity") + util.AddNetworkString("pac_request_zone_damage") + util.AddNetworkString("pac_hit_results") + util.AddNetworkString("pac_request_force") + + util.AddNetworkString("pac_signal_stop_lock") + util.AddNetworkString("pac_request_lock_break") + util.AddNetworkString("pac_lock_imposecalcview") + util.AddNetworkString("pac_mark_grabbed_ent") + util.AddNetworkString("pac_notify_grabbed_player") + util.AddNetworkString("pac_request_healthmod") + util.AddNetworkString("pac_update_healthbars")]] + + if master_init_featureblocker:GetInt() == 0 then + FINAL_BLOCKED_COMBAT_FEATURES = { + hitscan = false, + damage_zone = false, + lock = false, + force = false, + health_modifier = false, + } + + + elseif master_init_featureblocker:GetInt() == 1 then + FINAL_BLOCKED_COMBAT_FEATURES = { + hitscan = not hitscan_allow:GetBool(), + damage_zone = not damagezone_allow:GetBool(), + lock = not lock_allow:GetBool(), + force = not force_allow:GetBool(), + health_modifier = not healthmod_allow:GetBool(), + } + + else -- if it's not 0 or 1, all net combat features will be removed! + FINAL_BLOCKED_COMBAT_FEATURES = { + hitscan = true, + damage_zone = true, + lock = true, + force = true, + health_modifier = true, + } + end + + if not FINAL_BLOCKED_COMBAT_FEATURES["force"] then DeclareForceReceivers() end + if not FINAL_BLOCKED_COMBAT_FEATURES["damage_zone"] then DeclareDamageZoneReceivers() end + if not FINAL_BLOCKED_COMBAT_FEATURES["lock"] then DeclareLockReceivers() end + if not FINAL_BLOCKED_COMBAT_FEATURES["hitscan"] then DeclareHitscanReceivers() end + if not FINAL_BLOCKED_COMBAT_FEATURES["health_modifier"] then DeclareHealthModifierReceivers() end + + concommand.Add("pac_sv_combat_reinitialize_missing_receivers", function() + for name,blocked in pairs(FINAL_BLOCKED_COMBAT_FEATURES) do + local update = blocked and (blocked == GetConVar("pac_sv_"..name):GetBool()) + local new_bool = not (blocked or not GetConVar("pac_sv_"..name):GetBool()) + + if update then + FINAL_BLOCKED_COMBAT_FEATURES[name] = new_bool + if name == "force" then DeclareForceReceivers() print("reinitialized " .. name) + elseif name == "damage_zone" then DeclareDamageZoneReceivers() print("reinitialized " .. name) + elseif name == "lock" then DeclareLockReceivers() print("reinitialized " .. name) + elseif name == "hitscan" then DeclareHitscanReceivers() print("reinitialized " .. name) + elseif name == "health_modifier" then DeclareHealthModifierReceivers() print("reinitialized " .. name) + end + end + + end + + net.Start("pac_inform_blocked_parts") + net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES) + net.Broadcast() + end) + + net.Receive("pac_request_blocked_parts", function(len, ply) + net.Start("pac_inform_blocked_parts") + net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES) + net.Send(ply) + end) +end + +if CLIENT then + CreateConVar("pac_client_grab_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part") + CreateConVar("pac_client_lock_camera_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to having lock parts override your view") + CreateConVar("pac_client_damage_zone_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") + CreateConVar("pac_client_force_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to pac3 physics forces") + CreateConVar("pac_client_hitscan_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") + + function pac.CountNetMessage() + local ply = LocalPlayer() + + local stime = SysTime() + local ms_basis = GetConVar("pac_sv_combat_enforce_netrate"):GetInt()/1000 + local base_allowance = GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt() + + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance or base_allowance + ply.pac_netmessage_allowance_time = ply.pac_netmessage_allowance_time or 0 --initialize fields + + local timedelta = stime - ply.pac_netmessage_allowance_time --in seconds + ply.pac_netmessage_allowance_time = stime + local regen_rate = math.Clamp(ms_basis,0.01,10) / 20 --delay (converted from milliseconds) -> frequency (1/seconds) + local regens = timedelta / regen_rate + --print(timedelta .. " s, " .. 1/regen_rate .. "/s, " .. regens .. " regens") + if base_allowance == 0 then --limiting only by time, with no reserves + return timedelta > ms_basis + elseif ms_basis == 0 then --allowance with 0 time means ??? I guess automatic pass + return true + else + if timedelta > ms_basis then --good, count up + --print("good time: +"..regens .. "->" .. math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance)) + ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance) + else --earlier than base delay, so count down the allowance + --print("bad time: -1") + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance - 1 + end + ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance,-1,base_allowance) + ply.pac_netmessage_allowance_time = stime + return ply.pac_netmessage_allowance ~= -1 + end + + end + + local function SendConsents() + net.Start("pac_signal_player_combat_consent") + net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) + net.WriteBool(GetConVar("pac_client_damage_zone_consent"):GetBool()) + net.WriteBool(GetConVar("pac_client_lock_camera_consent"):GetBool()) + net.WriteBool(GetConVar("pac_client_force_consent"):GetBool()) + net.WriteBool(GetConVar("pac_client_hitscan_consent"):GetBool()) + net.SendToServer() + end + + + local function RequestBlockedParts() + + net.Start("pac_request_blocked_parts") + net.SendToServer() + end + concommand.Add("pac_inform_about_blocked_parts", RequestBlockedParts) + + net.Receive("pac_inform_blocked_parts", function() + pac.Blocked_Combat_Parts = net.ReadTable() + print("Are these pac combat parts blocked?") + + for name,b in pairs(pac.Blocked_Combat_Parts) do + local blocked = b + local disabled = not GetConVar("pac_sv_"..name):GetBool() + + local bool_str + + if disabled and blocked then bool_str = "disabled and blocked -> unavailable" + elseif disabled and not blocked then bool_str = "disabled -> unavailable" + elseif not disabled and blocked then bool_str = "blocked - > unavailable" + elseif not disabled and not blocked then bool_str = "available" + else bool_str = "??" end + + print(name .. " is " .. bool_str) + end + end) + + local consent_cvars = {"pac_client_grab_consent", "pac_client_lock_camera_consent", "pac_client_damage_zone_consent", "pac_client_force_consent", "pac_client_hitscan_consent"} + for _,cmd in ipairs(consent_cvars) do + cvars.AddChangeCallback(cmd, SendConsents) + end + + CreateConVar("pac_break_lock_verbosity", "3", FCVAR_ARCHIVE, "How much info you want for the PAC3 lock notifications\n3:full information\n2:grabbing player + basic reminder of the lock break command\n1:grabbing player\n0:suppress the notifications") + + + concommand.Add( "pac_stop_lock", function() + net.Start("pac_signal_stop_lock") + net.SendToServer() + end, "asks the server to breakup any lockpart hold on your player") + + concommand.Add( "pac_break_lock", function() + net.Start("pac_signal_stop_lock") + net.SendToServer() + end, "asks the server to breakup any lockpart hold on your player") + + net.Receive("pac_lock_imposecalcview", function() + local authority_to_calcview = net.ReadBool() and GetConVar("pac_client_lock_camera_consent"):GetBool() + + local alt_pos = net.ReadVector() + local alt_ang = net.ReadAngle() + local ask_drawviewer = net.ReadBool() + + if authority_to_calcview then + LocalPlayer().last_calcview = CurTime() + LocalPlayer().has_calcview = true + hook.Add("CalcView", "PAC_lockpart_calcview", function(ply, pos, angles, fov) + if LocalPlayer().last_calcview + 0.5 < CurTime() then + hook.Remove("CalcView", "PAC_lockpart_calcview") + LocalPlayer().has_calcview = false + return nil + + end + local view = { + origin = alt_pos, + angles = alt_ang, + fov = fov, + drawviewer = ask_drawviewer + } + return view + end) + hook.Add("Tick", "pac_checkcalcview", function() + if LocalPlayer().has_calcview and LocalPlayer().last_calcview + 0.5 < CurTime() then + hook.Remove("CalcView", "PAC_lockpart_calcview") + LocalPlayer().has_calcview = false + --print("killed a calcview due to expiry") + end + if LocalPlayer().last_calcview + 0.5 < CurTime() then + hook.Remove("CalcView", "PAC_lockpart_calcview") + LocalPlayer().has_calcview = false + --print("killed a calcview again due to expiry") + end + end) + else --if LocalPlayer().has_calcview then + hook.Remove("CalcView", "PAC_lockpart_calcview") + --print("killed a calcview due to lack of authority") + end + end) + + net.Receive("pac_request_player_combat_consent_update", function() + SendConsents() + end) + + net.Receive("pac_notify_grabbed_player", function() + local grabber = net.ReadEntity() + local verbosity = GetConVar("pac_break_lock_verbosity"):GetInt() + local str + if verbosity == 3 then + str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "! You can break free with pac_break_lock or pac_stop_lock. You can suppress these messages with pac_break_lock_verbosity 0" + notification.AddLegacy( str, NOTIFY_HINT, 10 ) + elseif verbosity == 2 then + str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "! pac_break_lock to break free" + notification.AddLegacy( str, NOTIFY_HINT, 7 ) + elseif verbosity == 1 then + str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "!" + notification.AddLegacy( str, NOTIFY_HINT, 7 ) + end + + pac.Message("You've been grabbed by " .. grabber:Nick() .. "!") + + end) + + hook.Add("InitPostEntity", "PAC_Send_Consents_On_Join", SendConsents) + hook.Add("InitPostEntity", "PAC_Request_BlockedParts_On_Join", RequestBlockedParts) + pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} +end + diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index 7189ad6f1..4dac3dedf 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -1,6 +1,10 @@ -local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}) -local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}) -local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}) +local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow physical projectiles serverside') +local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum attract radius for physical projectiles') +local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum damage radius for physical projectiles') +local pac_sv_projectile_max_phys_radius = CreateConVar("pac_sv_projectile_max_phys_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum physical radius for physical projectiles') +local pac_sv_projectile_max_speed = CreateConVar("pac_sv_projectile_max_speed", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum speed for physical projectiles') +local pac_sv_projectile_max_damage = CreateConVar("pac_sv_projectile_max_damage", 100000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum damage for physical projectiles') +local pac_sv_projectile_max_mass = CreateConVar("pac_sv_projectile_max_mass", 50000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum speed for physical projectiles') do -- projectile entity local ENT = {} @@ -60,17 +64,49 @@ do -- projectile entity self.projectile_owner = ply - local radius = math.Clamp(part.Radius, 1, pac_sv_projectile_max_damage_radius:GetFloat()) + local radius = math.Clamp(part.Radius, 1, pac_sv_projectile_max_phys_radius:GetFloat()) if part.Sphere then - self:PhysicsInitSphere(radius) + self:PhysicsInitSphere(radius, part.SurfaceProperties) else - self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius) + local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel) + --print("valid fallback? " .. part.FallbackSurfpropModel , valid_fallback) + self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties) + + if part.OverridePhysMesh and valid_fallback then + self:SetModel(part.FallbackSurfpropModel) + self:PhysicsInit(SOLID_VPHYSICS) + self:PhysicsInitMultiConvex(self:GetPhysicsObject():GetMeshConvexes(), part.SurfaceProperties) + end + + if part.RescalePhysMesh then + local physmesh = self:GetPhysicsObject():GetMeshConvexes() + --hack from prop resizer + for convexkey, convex in pairs( physmesh ) do + for poskey, postab in pairs( convex ) do + convex[ poskey ] = postab.pos * radius + end + end + + self:PhysicsInitMultiConvex( physmesh, part.SurfaceProperties) + self:EnableCustomCollisions( true ) + elseif not valid_fallback then + self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties) + end + end + local phys = self:GetPhysicsObject() + phys:SetMaterial(part.SurfaceProperties) + + phys:EnableGravity(part.Gravity) phys:AddVelocity((ang:Forward() + (VectorRand():Angle():Forward() * part.Spread)) * part.Speed * 1000) + phys:AddAngleVelocity(Vector(part.RandomAngleVelocity.x * math.Rand(-1,1), part.RandomAngleVelocity.y * math.Rand(-1,1), part.RandomAngleVelocity.z * math.Rand(-1,1))) + + phys:AddAngleVelocity(part.LocalAngleVelocity) + if part.AddOwnerSpeed then phys:AddVelocity(ply:GetVelocity()) end @@ -84,12 +120,14 @@ do -- projectile entity else phys:EnableCollisions(false) end + - phys:SetMass(math.Clamp(part.Mass, 0.001, 50000)) + phys:SetMass(math.Clamp(part.Mass, 0.001, pac_sv_projectile_max_mass:GetFloat())) phys:SetDamping(0, 0) + self.phys = phys self:SetAimDir(part.AimDir) - + self:DrawShadow(part.DrawShadow) self.part_data = part end @@ -314,7 +352,7 @@ do -- projectile entity end end - local damage_radius = math.Clamp(self.part_data.DamageRadius, 0, 300) + local damage_radius = math.Clamp(self.part_data.DamageRadius, 0, pac_sv_projectile_max_damage_radius:GetFloat()) if self.part_data.Damage > 0 then if self.part_data.DamageType == "heal" then @@ -359,7 +397,7 @@ do -- projectile entity end elseif self.part_data.DamageType == "explosion" then info:SetDamageType(damage_types.blast) - info:SetDamage(math.Clamp(self.part_data.Damage, 0, 100000)) + info:SetDamage(math.Clamp(self.part_data.Damage, 0, pac_sv_projectile_max_damage:GetFloat())) util.BlastDamageInfo(info, data.HitPos, damage_radius) else info:SetDamageForce(data.OurOldVelocity) @@ -399,6 +437,59 @@ do -- projectile entity scripted_ents.Register(ENT, ENT.ClassName) end + +local damage_ids = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 3, --sharp objects, such as manhacks or other npcs attacks + burn = 4, --damage from fire + vehicle = 5, --hit by a vehicle + fall = 6, --fall damage + blast = 7, --explosion damage + club = 8, --crowbar damage + shock = 9, --electrical damage, shows smoke at the damage position + sonic = 10, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 11, --laser + nevergib = 12, --don't create gibs + alwaysgib = 13, --always create gibs + drown = 14, --drown damage + paralyze = 15, --same as dmg_poison + nervegas = 16, --neurotoxin damage + poison = 17, --poison damage + acid = 18, -- + airboat = 19, --airboat gun damage + blast_surface = 20, --this won't hurt the player underwater + buckshot = 21, --the pellets fired from a shotgun + direct = 22, -- + dissolve = 23, --forces the entity to dissolve on death + drownrecover = 24, --damage applied to the player to restore health after drowning + physgun = 25, --damage done by the gravity gun + plasma = 26, -- + prevent_physics_force = 27, -- + radiation = 28, --radiation + removenoragdoll = 29, --don't create a ragdoll on death + slowburn = 30, -- + + explosion = 31, -- ent:Ignite(5) + fire = 32, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 33, + dissolve_heavy_electrical = 34, + dissolve_light_electrical = 35, + dissolve_core_effect = 36, + + heal = 37, + armor = 38, +} +local attract_ids = { + hitpos = 0, + hitpos_radius = 1, + closest_to_projectile = 2, + closest_to_hitpos = 3, +} + if SERVER then for key, ent in pairs(ents.FindByClass("pac_projectile")) do ent:Remove() @@ -406,20 +497,65 @@ if SERVER then util.AddNetworkString("pac_projectile") util.AddNetworkString("pac_projectile_attach") + util.AddNetworkString("pac_projectile_remove") + --REWORKED NET MESSAGE STRUCTURE MEANS THERE'S A LIMITED AMOUNT OF RECEIVED TABLE FIELDS net.Receive("pac_projectile", function(len, ply) if not enable:GetBool() then return end pace.suppress_prop_spawn = true - if hook.Run("PlayerSpawnProp", ply, "models/props_junk/popcan01a.mdl") == false then + if hook.Run("PlayerSpawnProp", ply, "models/props_junk/PopCan01a.mdl") == false then pace.suppress_prop_spawn = nil return end pace.suppress_prop_spawn = nil + local multi_projectile_count = net.ReadUInt(7) local pos = net.ReadVector() local ang = net.ReadAngle() - local part = net.ReadTable() + local part = {} + + --bools + part.Sphere = net.ReadBool() + part.RemoveOnCollide = net.ReadBool() + part.CollideWithOwner = net.ReadBool() + part.RemoveOnHide = net.ReadBool() + part.OverridePhysMesh = net.ReadBool() + part.Gravity = net.ReadBool() + part.AddOwnerSpeed = net.ReadBool() + part.Collisions = net.ReadBool() + part.CollideWithSelf = net.ReadBool() + part.AimDir = net.ReadBool() + part.DrawShadow = net.ReadBool() + part.Sticky = net.ReadBool() + part.BulletImpact = net.ReadBool() + + --vectors + part.RandomAngleVelocity = net.ReadVector() + part.LocalAngleVelocity = net.ReadVector() + + --strings + part.FallbackSurfpropModel = "models/" .. net.ReadString() + + part.UniqueID = net.ReadString() + part.SurfaceProperties = util.GetSurfacePropName(net.ReadUInt(10)) + part.DamageType = table.KeyFromValue(damage_ids, net.ReadUInt(7)) + part.AttractMode = table.KeyFromValue(attract_ids, net.ReadUInt(3)) + + --numbers + part.Radius = net.ReadUInt(8) + part.DamageRadius = net.ReadUInt(10) + part.Damage = net.ReadUInt(24) + part.Speed = net.ReadUInt(16) / 1000 + part.Maximum = net.ReadUInt(7) + part.LifeTime = net.ReadUInt(14) / 100 + part.Delay = net.ReadUInt(9) / 100 + part.Mass = net.ReadUInt(18) + part.Spread = net.ReadInt(10) / 100 + part.Damping = net.ReadInt(20) / 100 + part.Attract = net.ReadInt(14) + part.AttractRadius = net.ReadUInt(10) + part.Bounce = net.ReadInt(8) / 100 local radius_limit = 2000 @@ -453,7 +589,7 @@ if SERVER then end if projectile_count > 50 then - pac.Message("Player ", ply, " has more than 50 projectiles spawned!") + pac.Message("Player ", ply, " has more than 50 projectiles spawned! No more will be spawned until some expire.") return end @@ -464,10 +600,14 @@ if SERVER then local ent = ents.Create("pac_projectile") SafeRemoveEntityDelayed(ent,math.Clamp(part.LifeTime, 0, 50)) - ent:SetModel("models/props_junk/popcan01a.mdl") + local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel) + if not valid_fallback or part.FallbackSurfpropModel == "models/" or not part.OverridePhysMesh then part.FallbackSurfpropModel = "models/props_junk/PopCan01a.mdl" end + + ent:SetModel(part.FallbackSurfpropModel) ent:SetPos(pos) ent:SetAngles(ang) ent:Spawn() + if not part.CollideWithOwner then ent:SetOwner(ply) @@ -485,6 +625,7 @@ if SERVER then net.WriteEntity(ply) net.WriteInt(ent:EntIndex(), 16) net.WriteString(part.UniqueID) + net.WriteString(part.SurfaceProperties) net.Broadcast() ent.pac_projectile_uid = part.UniqueID @@ -494,10 +635,68 @@ if SERVER then ent.pac_projectile_owner = ply end - if part.Delay == 0 then - spawn() + local function multispawn() + if not ply:IsValid() then return end + + ply.pac_projectiles = ply.pac_projectiles or {} + + local projectile_count = 0 + for ent in pairs(ply.pac_projectiles) do + if ent:IsValid() then + projectile_count = projectile_count + 1 + else + ply.pac_projectiles[ent] = nil + end + end + + local remaining_projectile_slots = math.max(50 - projectile_count,0) + + if (multi_projectile_count > remaining_projectile_slots) then + if remaining_projectile_slots == 0 then + --block the spawns + pac.Message("Player ", ply, " has 50 projectiles spawned! No more will be spawned until some expire.") + goto CONTINUE + else + --adjust the spawn to just the limit + pac.Message("Player ", ply, " will spawn only ",remaining_projectile_slots," projectiles to prevent going over-limit") + multi_projectile_count = remaining_projectile_slots + end + + end + if part.Maximum > 0 and projectile_count >= part.Maximum then + return + end + + for i = multi_projectile_count - 1, 0, -1 do + spawn() + end + + ::CONTINUE:: + end + + if multi_projectile_count == 1 then + if part.Delay == 0 then + spawn() + else + timer.Simple(part.Delay, spawn) + end else - timer.Simple(part.Delay, spawn) + if part.Delay == 0 then + multispawn() + else + timer.Simple(part.Delay, multispawn) + end + end + end) + + net.Receive("pac_projectile_remove", function() + local id = net.ReadInt(16) + local ent = ents.GetByIndex(id) + + if ent.part_data.RemoveOnHide then + SafeRemoveEntity(ent) end + end) + end From 528c6543955dd43a55c100be6680527ca82604d1 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 15:01:47 -0400 Subject: [PATCH 054/300] put legacy scrolling back --- lua/pac3/editor/client/panels/tree.lua | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index ba24b1056..9be74632f 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -57,6 +57,69 @@ do function PANEL:Think(...) if not pace.current_part:IsValid() then return end + if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then + if + pace.current_part.pace_tree_node and + pace.current_part.pace_tree_node:IsValid() and not + ( + pace.BusyWithProperties:IsValid() or + pace.ActiveSpecialPanel:IsValid() or + pace.editing_viewmodel or + pace.editing_hands or + pace.properties.search:HasFocus() + ) and + not gui.IsConsoleVisible() + then + if input.IsKeyDown(KEY_LEFT) then + pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) + elseif input.IsKeyDown(KEY_RIGHT) then + pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) + end + + if input.IsKeyDown(KEY_UP) or input.IsKeyDown(KEY_PAGEUP) then + local added_nodes = get_added_nodes(self) + local offset = input.IsKeyDown(KEY_PAGEUP) and 10 or 1 + if not self.scrolled_up or self.scrolled_up < os.clock() then + for i,v in ipairs(added_nodes) do + if v == pace.current_part.pace_tree_node then + local node = added_nodes[i - offset] or added_nodes[1] + if node then + node:DoClick() + scroll_to_node(self, node) + break + end + end + end + + self.scrolled_up = self.scrolled_up or os.clock() + 0.4 + end + else + self.scrolled_up = nil + end + + if input.IsKeyDown(KEY_DOWN) or input.IsKeyDown(KEY_PAGEDOWN) then + local added_nodes = get_added_nodes(self) + local offset = input.IsKeyDown(KEY_PAGEDOWN) and 10 or 1 + if not self.scrolled_down or self.scrolled_down < os.clock() then + for i,v in ipairs(added_nodes) do + if v == pace.current_part.pace_tree_node then + local node = added_nodes[i + offset] or added_nodes[#added_nodes] + if node then + node:DoClick() + --scroll_to_node(self, node) + break + end + end + end + + self.scrolled_down = self.scrolled_down or os.clock() + 0.4 + end + else + self.scrolled_down = nil + end + end + end + for _, part in pairs(pac.GetLocalParts()) do local node = part.pace_tree_node if not node or not node:IsValid() then continue end From cd95bdc58ed45c8803f6fea583004d446c70c3ad Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 15:30:46 -0400 Subject: [PATCH 055/300] fix variable name rookie mistake! --- 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 ec92f2e36..dd7401452 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2506,7 +2506,7 @@ function pace.UltraCleanup(obj) --first pass: absolute unsafes: hidden parts for i,v in pairs(root:GetChildrenList()) do if v:IsHidden() or v.Hide then - if not FoundImportantMarkedParent(part) then + if not FoundImportantMarkedParent(v) then v:Remove() end From 57caadc517276faf230aaebe99a2947b32c13ac8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 15:36:47 -0400 Subject: [PATCH 056/300] load changes for the editor branch look at my repository's main readme for more information might still be related files. these separate branches and pull requests are not for separating functions, but for review! --- lua/pac3/core/client/parts/camera.lua | 132 +- lua/pac3/core/client/parts/command.lua | 5 +- lua/pac3/core/client/parts/group.lua | 4 + lua/pac3/editor/client/asset_browser.lua | 211 +- lua/pac3/editor/client/fonts.lua | 4 +- lua/pac3/editor/client/init.lua | 10 + lua/pac3/editor/client/logic.lua | 1 + lua/pac3/editor/client/menu_bar.lua | 84 + lua/pac3/editor/client/panels/editor.lua | 41 +- .../editor/client/panels/extra_properties.lua | 105 +- lua/pac3/editor/client/panels/pac_tree.lua | 2 +- lua/pac3/editor/client/panels/properties.lua | 822 +++++- lua/pac3/editor/client/panels/tree.lua | 228 +- lua/pac3/editor/client/parts.lua | 2390 ++++++++++++++- .../editor/client/popups_part_tutorials.lua | 1173 ++++++++ lua/pac3/editor/client/saved_parts.lua | 139 +- lua/pac3/editor/client/settings.lua | 2560 ++++++++++++++++- lua/pac3/editor/client/shortcuts.lua | 797 ++++- lua/pac3/editor/client/spawnmenu.lua | 30 +- lua/pac3/editor/client/tools.lua | 53 + lua/pac3/editor/client/view.lua | 116 +- 21 files changed, 8612 insertions(+), 295 deletions(-) create mode 100644 lua/pac3/editor/client/popups_part_tutorials.lua diff --git a/lua/pac3/core/client/parts/camera.lua b/lua/pac3/core/client/parts/camera.lua index 01319bf1b..fbd40609f 100644 --- a/lua/pac3/core/client/parts/camera.lua +++ b/lua/pac3/core/client/parts/camera.lua @@ -20,13 +20,47 @@ for i, ply in ipairs(player.GetAll()) do end function PART:OnShow() - local owner = self:GetRootPart():GetOwner() + local owner = self:GetPlayerOwner() if not owner:IsValid() then return end + self.inactive = false owner.pac_cameras = owner.pac_cameras or {} owner.pac_cameras[self] = self + + --the policy is that a shown camera takes priority over all others + for _, part in pairs(owner.pac_cameras) do + if part ~= self then + part.priority = false + end + end + self.priority = true + timer.Simple(0.02, function() + self.priority = true + end) end +function PART:OnHide() + local owner = self:GetPlayerOwner() + if not owner:IsValid() then return end + + owner.pac_cameras = owner.pac_cameras or {} + + --this camera cedes priority to others that may be active + for _, part in pairs(owner.pac_cameras) do + if part ~= self and not part:IsHidden() then + part.priority = true + end + end + self.inactive = true + self.priority = false + owner.pac_cameras[self] = nil +end + +--[[function PART:OnHide() + --only stop the part if explicitly set to hidden. + if not self.Hide and not self:IsHidden() then return end +end]] + function PART:CalcView(_, _, eyeang, fov, nearz, farz) local pos, ang = self:GetDrawPosition(nil, true) @@ -49,28 +83,92 @@ end BUILDER:Register() + local temp = {} +local remaining_camera = false +local remaining_camera_time_buffer = CurTime() + +local function CheckCamerasAgain(ply) + local cams = ply.pac_cameras or {} + local fpos, fang, ffov, fnearz, ffarz + + for _, part in pairs(cams) do + if (not part.inactive or part.priority) and not part:IsHidden() then + return true + end + end +end + +local function RebuildCameras(ply) + ply.pac_cameras = {} + for _,part in pairs(pac.GetLocalParts()) do + if part:IsValid() then + if part.ClassName == "camera" and (not part.inactive or not part:IsHidden() or part.priority) then + if part:GetPlayerOwner() == ply then + ply.pac_cameras[part] = part + end + end + end + end +end pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) - if not ply.pac_cameras then return end + + local fpos, fang, ffov, fnearz, ffarz + local warning_state = not ply.pac_cameras + if not warning_state then warning_state = table.IsEmpty(ply.pac_cameras) end if ply:GetViewEntity() ~= ply then return end - for _, part in pairs(ply.pac_cameras) do - if part:IsValid() then - part:CalcShowHide() - - if not part:IsHidden() then - pos, ang, fov, nearz, farz = part:CalcView(ply, pos, ang, fov, nearz, farz) - temp.origin = pos - temp.angles = ang - temp.fov = fov - temp.znear = nearz - temp.zfar = farz - temp.drawviewer = not part.DrawViewModel - return temp + remaining_camera = false + remaining_camera_time_buffer = remaining_camera_time_buffer or CurTime() + if warning_state then + RebuildCameras(ply) + else + for _, part in pairs(ply.pac_cameras) do + if part.ClassName ~= "camera" then + ply.pac_cameras[part] = nil + end + + if part.ClassName == "camera" and part:IsValid() then + if not part:IsHidden() then + remaining_camera = true + remaining_camera_time_buffer = CurTime() + 0.1 + end + + part:CalcShowHide() + if not part.inactive then + --calculate values ahead of the return, used as a fallback just in case + fpos, fang, ffov, fnearz, ffarz = part:CalcView(ply, pos, ang, fov, nearz, farz) + temp.origin = fpos + temp.angles = fang + temp.fov = ffov + temp.znear = fnearz + temp.zfar = ffarz + temp.drawviewer = false + + if not part:IsHidden() and not part.inactive and part.priority then + temp.drawviewer = not part.DrawViewModel + return temp + end + end + else + ply.pac_cameras[part] = nil end - else - ply.pac_cameras[part] = nil end end + + if remaining_camera or CurTime() < remaining_camera_time_buffer then + return temp + end + + --final fallback, just give us any valid pac camera to preserve the view! priority will be handled elsewhere + if CheckCamerasAgain(ply) then + return temp + else + return + end + + return + --only time to return to first person is if all camera parts are hidden AFTER we pass the buffer time filter + --until we make reversible first person a thing, letting some non-drawable parts think, this is the best solution I could come up with end) diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index f902b6475..8e320a7b3 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -25,7 +25,10 @@ end function PART:OnShow(from_rendering) if not from_rendering and self:GetExecuteOnShow() then - self:Execute() + timer.Simple(0, function() + if self.Hide or self:IsHidden() then return end + self:Execute() + end) end end diff --git a/lua/pac3/core/client/parts/group.lua b/lua/pac3/core/client/parts/group.lua index 2c0d2170a..ba6b30070 100644 --- a/lua/pac3/core/client/parts/group.lua +++ b/lua/pac3/core/client/parts/group.lua @@ -8,6 +8,8 @@ PART.Description = "right click to add parts" BUILDER:StartStorableVars() BUILDER:GetSet("Duplicate", false) BUILDER:GetSet("OwnerName", "self") + BUILDER:GetSet("ModelTracker", "", {hide_in_editor = true}) + BUILDER:GetSet("ClassTracker", "", {hide_in_editor = true}) BUILDER:EndStorableVars() local init_list = {} @@ -50,6 +52,8 @@ function PART:SetOwner(ent) if not pac.HookEntityRender(owner, self) then self:ShowFromRendering() end + self.ModelTracker = owner:GetModel() + self.ClassTracker = owner:GetClass() end end end diff --git a/lua/pac3/editor/client/asset_browser.lua b/lua/pac3/editor/client/asset_browser.lua index c4ea9969c..57e3a784d 100644 --- a/lua/pac3/editor/client/asset_browser.lua +++ b/lua/pac3/editor/client/asset_browser.lua @@ -1,6 +1,87 @@ -- based on starfall CreateClientConVar("pac_asset_browser_close_on_select", "1") CreateClientConVar("pac_asset_browser_remember_layout", "1") +CreateClientConVar("pac_asset_browser_extra_options", "1") +CreateClientConVar("pac_favorites_try_to_get_asset_series", "1") +CreateClientConVar("pac_favorites_try_to_build_asset_series", "0") + +local function rebuild_bookmarks() + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + + --here's some default favorites + if not pace.bookmarked_ressources["models"] or table.IsEmpty(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 + + if not pace.bookmarked_ressources["sound"] or table.IsEmpty(pace.bookmarked_ressources["sound"]) then + pace.bookmarked_ressources["sound"] = { + "music/hl1_song11.mp3", + "npc/combine_gunship/dropship_engine_near_loop1.wav", + "ambient/alarms/warningbell1.wav", + "phx/epicmetal_hard7.wav", + "phx/explode02.wav" + } + end + + if not pace.bookmarked_ressources["materials"] or table.IsEmpty(pace.bookmarked_ressources["materials"]) then + pace.bookmarked_ressources["materials"] = { + "models/debug/debugwhite", + "vgui/null", + "debug/env_cubemap_model", + "models/wireframe", + "cable/physbeam", + "cable/cable2", + "effects/tool_tracer", + "effects/flashlight/logo", + "particles/flamelet[1,5]", + "sprites/key_[0,9]", + "vgui/spawnmenu/generating", + "vgui/spawnmenu/hover" + } + end +end + +local function encode_table_to_file(str) + local data = {} + if not file.Exists("pac3_config", "DATA") then + file.CreateDir("pac3_config") + + end + + + if str == "pac_editor_shortcuts" then + data = pace.PACActionShortcut + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "pac_editor_partmenu_layouts" then + data = pace.operations_order + file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data)) + elseif str == "pac_part_categories" then + data = pace.partgroups + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "bookmarked_ressources" then + rebuild_bookmarks() + for category, tbl in pairs(pace.bookmarked_ressources) do + data = tbl + str = category + file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data)) + end + + end + +end + +function pace.SaveRessourceBookmarks() + encode_table_to_file("bookmarked_ressources") +end + + local function table_tolist(tbl, sort) local list = {} @@ -51,6 +132,7 @@ end local L = pace.LanguageString +--icon is the item's panel, path is the item's full file path, on_menu is a function that can extend the function local function install_click(icon, path, pattern, on_menu, pathid) local old = icon.OnMouseReleased icon.OnMouseReleased = function(_, code) @@ -70,6 +152,42 @@ local function install_click(icon, path, pattern, on_menu, pathid) end SetClipboardText(path) end) + + if string.match(path, "^materials/(.+)%.vmt$") or string.match(path, "^materials/(.+%.png)$") then resource_type = "materials" + elseif string.match(path, "^models/") then resource_type = "models" end + + if not pace.bookmarked_ressources then + pace.SaveRessourceBookmarks() + elseif not pace.bookmarked_ressources[resource_type] then + pace.SaveRessourceBookmarks() + end + if GetConVar("pac_asset_browser_extra_options"):GetBool() then + if GetConVar("pac_favorites_try_to_get_asset_series"):GetBool() then + if not table.HasValue(pace.bookmarked_ressources[resource_type], path) then + menu:AddOption(L"add series to favorites", function() + table.insert(pace.bookmarked_ressources[resource_type], path) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove series from favorites", function() + table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], path )) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/cross.png") + end + end + if not table.HasValue(pace.bookmarked_ressources[resource_type], path) then + menu:AddOption(L"add to favorites", function() + table.insert(pace.bookmarked_ressources[resource_type], path) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove from favorites", function() + table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], path )) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/cross.png") + end + end + if on_menu then on_menu(menu) end menu:Open() end @@ -93,6 +211,10 @@ local function get_unlit_mat(path) return CreateMaterial(path .. "_pac_asset_browser", "UnlitGeneric", {["$basetexture"] = path:match("materials/(.+)%.vtf")}) end +function pace.get_unlit_mat(path) + return get_unlit_mat(path) +end + local next_generate_icon = 0 local max_generating = 5 @@ -215,25 +337,25 @@ local function create_material_icon(path, grid_panel) --[[ - local old = icon.OnCursorEntered - function icon:OnCursorEntered(...) - if pace.current_part:IsValid() and pace.current_part.Materialm then - pace.asset_browser_old_mat = pace.asset_browser_old_mat or pace.current_part.Materialm - pace.current_part.Materialm = mat - end + local old = icon.OnCursorEntered + function icon:OnCursorEntered(...) + if pace.current_part:IsValid() and pace.current_part.Materialm then + pace.asset_browser_old_mat = pace.asset_browser_old_mat or pace.current_part.Materialm + pace.current_part.Materialm = mat + end - old(self, ...) - end + old(self, ...) + end - local old = icon.OnCursorExited - function icon:OnCursorExited(...) - if pace.current_part:IsValid() and pace.current_part.Materialm then - pace.current_part.Materialm = pace.asset_browser_old_mat + local old = icon.OnCursorExited + function icon:OnCursorExited(...) + if pace.current_part:IsValid() and pace.current_part.Materialm then + pace.current_part.Materialm = pace.asset_browser_old_mat + end + old(self, ...) end - old(self, ...) - end -]] + ]] local unlit_mat = get_unlit_mat(path) @@ -343,6 +465,7 @@ local function create_material_icon(path, grid_panel) create_text_view(str) end) + end) grid_panel:Add(icon) @@ -606,6 +729,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) local frame = vgui.Create("DFrame") frame.title = L"asset browser" .. " - " .. (browse_types_str:gsub(";", " ")) + if GetConVar("pac_asset_browser_remember_layout"):GetBool() then frame:SetCookieName("pac_asset_browser") end @@ -681,6 +805,9 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) options_menu:SetDeleteSelf(false) options_menu:AddCVar(L"close browser on select", "pac_asset_browser_close_on_select", "1", "0") options_menu:AddCVar(L"remember layout", "pac_asset_browser_remember_layout", "1", "0") + options_menu:AddCVar(L"additional right click options", "pac_asset_browser_extra_options", "1", "0") + options_menu:AddCVar(L"try to find asset series for saving favorites", "pac_favorites_try_to_get_asset_series", "1", "0") + options_menu:AddCVar(L"try to build asset series in the editor", "pac_favorites_try_to_build_asset_series", "1", "0") local zoom_controls = vgui.Create("pac_AssetBrowser_ZoomControls", menu_bar) @@ -813,6 +940,60 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) if code == MOUSE_RIGHT then play:Start() + local menu = DermaMenu() + menu:SetPos(input.GetCursorPos()) + menu:AddOption(L"copy path", function() + SetClipboardText(sound) + end) + if GetConVar("pac_asset_browser_extra_options"):GetBool() then + pace.bookmarked_ressources["sound"] = pace.bookmarked_ressources["sound"] or {} + local resource_type = "sound" + if GetConVar("pac_favorites_try_to_get_asset_series"):GetBool() then + --print(sound) + local extension = string.GetExtensionFromFilename(sound) + local base_name = string.gsub(sound, "%d+."..extension.."$", "") + base_name = string.gsub(base_name, "^sound/", "") + --print(resource_type, base_name, extension) + + local series_results = pace.FindAssetSeriesBounds(resource_type, base_name, extension) + PrintTable(series_results) + if not series_results.start_index then + goto CONTINUE + end + + local series_str = base_name .. "[" .. series_results.start_index .. "," .. series_results.end_index .. "]." .. extension + + if not table.HasValue(pace.bookmarked_ressources[resource_type], series_str) then + + menu:AddOption(L"add series to favorites", function() + table.insert(pace.bookmarked_ressources[resource_type], series_str) + pace.SaveRessourceBookmarks() + + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove series from favorites", function() + table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], series_str )) + pace.SaveRessourceBookmarks() + + end):SetImage("icon16/cross.png") + end + + ::CONTINUE:: + end + + if not table.HasValue(pace.bookmarked_ressources["sound"], sound) then + menu:AddOption(L"add to favorites", function() + table.insert(pace.bookmarked_ressources["sound"], sound) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove from favorites", function() + table.remove(pace.bookmarked_ressources["sound"], table.KeyFromValue( pace.bookmarked_ressources["sound"], sound )) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/cross.png") + end + end + menu:MakePopup() menu:RequestFocus() else pace.model_browser_callback(sound, "GAME") end diff --git a/lua/pac3/editor/client/fonts.lua b/lua/pac3/editor/client/fonts.lua index 4a70c106c..59b75c50f 100644 --- a/lua/pac3/editor/client/fonts.lua +++ b/lua/pac3/editor/client/fonts.lua @@ -2,7 +2,7 @@ local L = pace.LanguageString pace.Fonts = {} -for i = 1, 5 do +for i = 1, 34 do surface.CreateFont("pac_font_"..i, { font = "Arial", @@ -14,7 +14,7 @@ for i = 1, 5 do table.insert(pace.Fonts, "pac_font_"..i) end -for i = 1, 5 do +for i = 1, 34 do surface.CreateFont("pac_font_bold"..i, { font = "Arial", diff --git a/lua/pac3/editor/client/init.lua b/lua/pac3/editor/client/init.lua index 933d58a9b..eff163823 100644 --- a/lua/pac3/editor/client/init.lua +++ b/lua/pac3/editor/client/init.lua @@ -86,6 +86,7 @@ pace.ActivePanels = pace.ActivePanels or {} pace.Editor = NULL local remember = CreateConVar("pac_editor_remember_position", "1", {FCVAR_ARCHIVE}, "Remember PAC3 editor position on screen") +local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's vertical divider position") local positionMode = CreateConVar("pac_editor_position_mode", "0", {FCVAR_ARCHIVE}, "Editor position mode. 0 - Left, 1 - middle, 2 - Right. Has no effect if pac_editor_remember_position is true") local showCameras = CreateConVar("pac_show_cameras", "1", {FCVAR_ARCHIVE}, "Show the PAC cameras of players using the editor") local showInEditor = CreateConVar("pac_show_in_editor", "1", {FCVAR_ARCHIVE}, "Show the 'In PAC3 Editor' text above players using the editor") @@ -134,6 +135,15 @@ function pace.OpenEditor() editor:SetPos(0, 0) end end + + if remember_divider:GetBool() then + pace.vertical_div_height = pace.vertical_div_height or ScrH()/1.4 + + timer.Simple(0, function() + editor.div:SetTopHeight(pace.vertical_div_height) + end) + + end if ctp and ctp.Disable then ctp:Disable() diff --git a/lua/pac3/editor/client/logic.lua b/lua/pac3/editor/client/logic.lua index 329928faa..92026aca8 100644 --- a/lua/pac3/editor/client/logic.lua +++ b/lua/pac3/editor/client/logic.lua @@ -70,6 +70,7 @@ function pace.OnOpenEditor() end function pace.OnCloseEditor() + pace.FlushInfoPopups() pace.EnableView(false) pace.StopSelect() pace.SafeRemoveSpecialPanel() diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 15f8f5588..40347b728 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -55,6 +55,11 @@ local function populate_pac(menu) function() pace.ShowWiki(pace.WikiURL .. "Beginners-FAQ") end ):SetImage(pace.MiscIcons.info) + help:AddOption( + L"PAC3 Wiki", + function() pace.ShowWiki("https://wiki.pac3.info/start") end + ):SetImage(pace.MiscIcons.info) + do local chat_pnl = help:AddOption( L"Discord / PAC3 Chat", @@ -75,8 +80,12 @@ local function populate_pac(menu) version_pnl:SetImage(pace.MiscIcons.info) version:AddOption(version_string) + + version:AddOption("update news", function() pac.OpenMOTD(false) end) end + + help:AddOption( L"about", function() pace.ShowAbout() end @@ -86,6 +95,8 @@ local function populate_pac(menu) do menu:AddOption(L"exit", function() pace.CloseEditor() end):SetImage(pace.MiscIcons.exit) end + + end local function populate_view(menu) @@ -101,10 +112,64 @@ end local function populate_options(menu) menu:AddOption(L"settings", function() pace.OpenSettings() end) + menu:AddCVar(L"Keyboard shortcuts: Legacy mode", "pac_editor_shortcuts_legacy_mode", "1", "0") menu:AddCVar(L"inverse collapse/expand controls", "pac_reverse_collapse", "1", "0") menu:AddCVar(L"enable shift+move/rotate clone", "pac_grab_clone", "1", "0") menu:AddCVar(L"remember editor position", "pac_editor_remember_position", "1", "0") + menu:AddCVar(L"remember divider position", "pac_editor_remember_divider_height", "1", "0") + menu:AddCVar(L"ask before loading autoload", "pac_prompt_for_autoload", "1", "0") + + local prop_pac_load_mode, pnlpplm = menu:AddSubMenu("(singleplayer only) How to handle prop/npc outfits", function() end) + prop_pac_load_mode.GetDeleteSelf = function() return false end + pnlpplm:SetImage("icon16/transmit.png") + prop_pac_load_mode:AddOption(L"Load without queuing", function() RunConsoleCommand("pac_autoload_preferred_prop", "0") end) + prop_pac_load_mode:AddOption(L"Queue parts if there's only one group", function() RunConsoleCommand("pac_autoload_preferred_prop", "1") end) + prop_pac_load_mode:AddOption(L"Queue parts if there's one or more groups", function() RunConsoleCommand("pac_autoload_preferred_prop", "2") end) + menu:AddCVar(L"show parts IDs", "pac_show_uniqueid", "1", "0") + local popups, pnlp = menu:AddSubMenu("configure editor popups", function() end) + popups.GetDeleteSelf = function() return false end + pnlp:SetImage("icon16/comment.png") + popups:AddCVar(L"enable editor popups", "pac_popups_enable", "1", "0") + popups:AddCVar(L"don't kill popups on autofade", "pac_popups_preserve_on_autofade", "1", "0") + popups:AddOption("Configure popups appearance", function() pace.OpenPopupConfig() end):SetImage('icon16/color_wheel.png') + local popup_pref_mode, pnlppm = popups:AddSubMenu("prefered location", function() end) + pnlppm:SetImage("icon16/layout_header.png") + popup_pref_mode.GetDeleteSelf = function() return false end + popup_pref_mode:AddOption(L"parts on viewport", function() RunConsoleCommand("pac_popups_preferred_location", "part world") end):SetImage('icon16/camera.png') + popup_pref_mode:AddOption(L"part label on tree", function() RunConsoleCommand("pac_popups_preferred_location", "pac tree label") end):SetImage('icon16/layout_content.png') + popup_pref_mode:AddOption(L"menu bar", function() RunConsoleCommand("pac_popups_preferred_location", "menu bar") end):SetImage('icon16/layout_header.png') + popup_pref_mode:AddOption(L"cursor", function() RunConsoleCommand("pac_popups_preferred_location", "cursor") end):SetImage('icon16/mouse.png') + popup_pref_mode:AddOption(L"screen", function() RunConsoleCommand("pac_popups_preferred_location", "screen") end):SetImage('icon16/monitor.png') + + menu:AddOption(L"configure event wheel", pace.ConfigureEventWheelMenu):SetImage("icon16/color_wheel.png") + + local copilot, pnlc = menu:AddSubMenu("configure editor copilot", function() end) + copilot.GetDeleteSelf = function() return false end + pnlc:SetImage("icon16/award_star_gold_3.png") + copilot:AddCVar(L"show info popup when changing an event's type", "pac_copilot_make_popup_when_selecting_event", "1", "0") + copilot:AddCVar(L"auto-focus on the main property when creating some parts", "pac_copilot_auto_focus_main_property_when_creating_part","1","0") + copilot:AddCVar(L"auto-setup a command event when entering a name as an event type", "pac_copilot_auto_setup_command_events", "1", "0") + copilot:AddCVar(L"open asset browser when creating some parts", "pac_copilot_open_asset_browser_when_creating_part", "1", "0") + copilot:AddCVar(L"disable the editor view when creating a camera part", "pac_copilot_force_preview_cameras", "1", "0") + local copilot_add_part_search_menu, pnlaps = copilot:AddSubMenu("configure the searchable add part menu", function() end) + pnlaps:SetImage("icon16/add.png") + copilot_add_part_search_menu.GetDeleteSelf = function() return false end + copilot_add_part_search_menu:AddOption(L"No copilot", function() RunConsoleCommand("pac_copilot_partsearch_depth", "-1") end):SetImage('icon16/page_white.png') + copilot_add_part_search_menu:AddOption(L"automatically select a text field after creating the part (e.g. event type)", function() RunConsoleCommand("pac_copilot_partsearch_depth", "0") end):SetImage('icon16/layout_edit.png') + copilot_add_part_search_menu:AddOption(L"open another quick list menu (event types, favorite models...)", function() RunConsoleCommand("pac_copilot_partsearch_depth", "1") end):SetImage('icon16/application_view_list.png') + + local combat_consents, pnlcc = menu:AddSubMenu("pac combat consents", function() end) + combat_consents.GetDeleteSelf = function() return false end + pnlcc:SetImage("icon16/joystick.png") + + combat_consents:AddCVar(L"damage_zone part (area damage)", "pac_client_damage_zone_consent", "1", "0") + combat_consents:AddCVar(L"hitscan part (bullets)", "pac_client_hitscan_consent", "1", "0") + combat_consents:AddCVar(L"force part (physics forces)", "pac_client_force_consent", "1", "0") + combat_consents:AddCVar(L"lock part's grab (can take control of your position)", "pac_client_grab_consent", "1", "0") + combat_consents:AddCVar(L"lock part's grab calcview (can take control of your view)", "pac_client_lock_camera_consent", "1", "0") + + menu:AddSpacer() menu:AddOption(L"position grid size", function() Derma_StringRequest(L"position grid size", L"size in units:", GetConVarNumber("pac_grid_pos_size"), function(val) @@ -123,6 +188,7 @@ local function populate_options(menu) menu:AddCVar(L"enable language identifier in text fields", "pac_editor_languageid", "1", "0") pace.AddLanguagesToMenu(menu) pace.AddFontsToMenu(menu) + menu:AddCVar(L"Use the new PAC4.5 icon", "pac_icon", "1", "0") menu:AddSpacer() @@ -149,6 +215,19 @@ local function populate_player(menu) end end +function pace.PopulateMenuBarTab(menu, tab) + if tab == "pac" then + populate_pac(menu) + elseif tab == "player" then + populate_player(menu) + elseif tab == "options" then + populate_options(menu) + elseif tab == "view" then + populate_view(menu) + end + --timer.Simple(0.3, function() menu:RequestFocus() end) +end + function pace.OnMenuBarPopulate(bar) for k,v in pairs(bar.Menus) do v:Remove() @@ -161,6 +240,11 @@ function pace.OnMenuBarPopulate(bar) pace.AddToolsToMenu(bar:AddMenu(L"tools")) bar:RequestFocus(true) + --[[timer.Simple(0.2, function() + if IsValid(bar) then + bar:RequestFocus(true) + end + end)]] end function pace.OnOpenMenu() diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index dab095bdb..36c086daa 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -17,6 +17,7 @@ local use_tabs = CreateClientConVar("pac_property_tabs", 1, true) local zoom_persistent = CreateClientConVar("pac_zoom_persistent", 0, true, false, 'Keep zoom between sessions.') local zoom_mousewheel = CreateClientConVar("pac_zoom_mousewheel", 0, true, false, 'Enable zooming with mouse wheel.') local zoom_smooth = CreateClientConVar("pac_zoom_smooth", 0, true, false, 'Enable smooth zooming.') +local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's vertical divider position") function PANEL:Init() self:SetTitle("") @@ -172,6 +173,10 @@ function PANEL:MakeBar() end function PANEL:OnRemove() + if remember_divider:GetBool() then + pace.vertical_div_height = self.div:GetTopHeight() + end + if self.menu_bar:IsValid() then self.menu_bar:Remove() end @@ -185,6 +190,10 @@ function PANEL:OnRemove() end end +function PANEL:IsLeft() --which side the editor is on. + return self:GetPos() + self:GetWide() / 2 < ScrW() / 2 +end + function PANEL:Think(...) if not self.okay then return end DFrame.Think(self, ...) @@ -200,10 +209,11 @@ function PANEL:Think(...) local bar = self.menu_bar - self:SetTall(ScrH()) + + self:SetTall(ScrH() - (self.y_offset or 0)) local w = math.max(self:GetWide(), 200) self:SetWide(w) - self:SetPos(math.Clamp(self:GetPos(), 0, ScrW() - w), 0) + self:SetPos(math.Clamp(self:GetPos(), 0, ScrW() - w), (self.y_offset or 0)) if x ~= self.last_x then self:SetCookie("x", x) @@ -212,7 +222,7 @@ function PANEL:Think(...) if self.exit_button:IsValid() then - if self:GetPos() + self:GetWide() / 2 < ScrW() / 2 then + if self:IsLeft() then self.exit_button:SetPos(ScrW() - self.exit_button:GetWide() + 4, -4) else self.exit_button:SetPos(-4, -4) @@ -244,6 +254,9 @@ function PANEL:Think(...) self.zoomslider:SetValue(75) pace.zoom_reset = nil end + if pace.OverridingFOVSlider then + self.zoomslider:SetValue(pace.ViewFOV) + end if zoom_smooth:GetInt() == 1 then pace.SetZoom(self.zoomslider:GetValue(),true) @@ -261,6 +274,8 @@ function PANEL:Think(...) else self.zoomsettings:SetVisible(false) end + + end end @@ -278,6 +293,9 @@ function PANEL:PerformLayout() end if self.old_part ~= pace.current_part then + if remember_divider:GetBool() then + pace.vertical_div_height = self.div:GetTopHeight() + end self.div:InvalidateLayout() self.bottom:PerformLayout() pace.properties:PerformLayout() @@ -292,10 +310,23 @@ function PANEL:PerformLayout() local oldh = self.div:GetTopHeight() if newh= 1 then - self.div:SetTopHeight(newh) + + if remember_divider:GetBool() then + if remember_divider:GetBool() then + self.div:SetTopHeight(pace.vertical_div_height) + else + self.div:SetTopHeight(newh) + end + else + self.div:SetTopHeight(newh) + end end end end diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index fb2283d5a..f2eefa773 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -714,9 +714,16 @@ do -- event is_touching if part ~= last_part then stop() return end if not part:IsValid() then stop() return end if part.ClassName ~= "event" then stop() return end - if part:GetEvent() ~= "is_touching" then stop() return end + if not (part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable" or part:GetEvent() == "is_touching_filter" or part:GetEvent() == "is_touching_life") then stop() return end local extra_radius = part:GetProperty("extra_radius") or 0 + local nearest_model = part:GetProperty("nearest_model") or false + local no_npc = part:GetProperty("no_npc") or false + local no_players = part:GetProperty("no_players") or false + local x_stretch = part:GetProperty("x_stretch") or 1 + local y_stretch = part:GetProperty("y_stretch") or 1 + local z_stretch = part:GetProperty("z_stretch") or 1 + local ent if part.RootOwner then ent = part:GetRootPart():GetOwner() @@ -724,34 +731,102 @@ do -- event is_touching ent = part:GetOwner() end + if nearest_model then ent = part:GetOwner() end + if not IsValid(ent) then stop() return end - local radius = ent:BoundingRadius() + local radius if radius == 0 and IsValid(ent.pac_projectile) then radius = ent.pac_projectile:GetRadius() end - radius = math.max(radius + extra_radius + 1, 1) + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) - local mins = Vector(-1,-1,-1) - local maxs = Vector(1,1,1) - local startpos = ent:WorldSpaceCenter() + radius = math.max(ent:BoundingRadius() + extra_radius + 1, 1) mins = mins * radius maxs = maxs * radius + + local startpos = ent:WorldSpaceCenter() + local b = false + if part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable" then + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {part:GetRootPart():GetOwner(),ent} + } ) + b = tr.Hit + elseif part:GetEvent() == "is_touching_life" then + local found = false + local ents_hits = ents.FindInBox(startpos + mins, startpos + maxs) + for _,ent2 in pairs(ents_hits) do + + if IsValid(ent2) and (ent2 ~= ent and ent2 ~= part:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) + then + found = true + if ent2:IsNPC() and no_npc then + found = false + elseif ent2:IsPlayer() and no_players then + found = false + end + if found then b = true end + end + end + elseif part:GetEvent() == "is_touching_filter" then + local ents_hits = ents.FindInBox(startpos + mins, startpos + maxs) + for _,ent2 in pairs(ents_hits) do + if (ent2 ~= ent and ent2 ~= part:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) and + not ( (no_npc and ent2:IsNPC()) or (no_players and ent2:IsPlayer()) ) + then b = true end + end + end + + if self.udata then + render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, b and Color(255,0,0) or Color(255,255,255), true ) + end + end) + end + + pace.RegisterPanel(PANEL) +end - local tr = util.TraceHull( { - start = startpos, - endpos = startpos, - maxs = maxs, - mins = mins, - filter = ent - } ) +do --projectile radius + local PANEL = {} + + PANEL.ClassName = "properties_projectile_radii" + PANEL.Base = "pace_properties_number" + + function PANEL:OnValueSet() + time = os.clock() + 6 + local function stop() + hook.Remove("PostDrawOpaqueRenderables", "pace_draw_projectile_radii") + end + local last_part = pace.current_part + hook.Add("PostDrawOpaqueRenderables", "pace_draw_projectile_radii", function() + if time < os.clock() then + stop() + end + if not pace.current_part:IsValid() then stop() return end + if pace.current_part.ClassName ~= "projectile" then stop() return end if self.udata then - render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, tr.Hit and Color(255,0,0) or Color(255,255,255), true ) + if last_part.Sphere then + render.DrawWireframeSphere( last_part:GetWorldPosition(), last_part.Radius, 10, 10, Color(255,255,255), true ) + render.DrawWireframeSphere( last_part:GetWorldPosition(), last_part.DamageRadius, 10, 10, Color(255,0,0), true ) + else + local mins_ph = Vector(last_part.Radius,last_part.Radius,last_part.Radius) + local mins_dm = Vector(last_part.DamageRadius,last_part.DamageRadius,last_part.DamageRadius) + render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_ph, mins_ph, Color(255,255,255), true ) + render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_dm, mins_dm, Color(255,0,0), true ) + end + end end) end pace.RegisterPanel(PANEL) -end +end \ No newline at end of file diff --git a/lua/pac3/editor/client/panels/pac_tree.lua b/lua/pac3/editor/client/panels/pac_tree.lua index 5389e0756..994497cc0 100644 --- a/lua/pac3/editor/client/panels/pac_tree.lua +++ b/lua/pac3/editor/client/panels/pac_tree.lua @@ -30,7 +30,7 @@ AccessorFunc(PANEL, "m_bClickOnDragHover", "ClickOnDragHover") function PANEL:Init() self:SetShowIcons(true) self:SetIndentSize(14) - self:SetLineHeight(17) + self:SetLineHeight(17 * GetConVar("pac_editor_scale"):GetFloat()) self.RootNode = self:GetCanvas():Add("pac_dtree_node") self.RootNode:SetRoot(self) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index b062fdd96..101ff769e 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -1,6 +1,10 @@ local L = pace.LanguageString local languageID = CreateClientConVar("pac_editor_languageid", 1, true, false, "Whether we should show the language indicator inside of editable text entries.") +local favorites_menu_expansion = CreateClientConVar("pac_favorites_try_to_build_asset_series", "0", true, false) + + +local searched_cache_series_results = {} function pace.ShowSpecial(pnl, parent, size) size = size or 150 @@ -16,6 +20,157 @@ function pace.FixMenu(menu) menu:SetPos(pace.Editor:GetPos() + pace.Editor:GetWide(), gui.MouseY() - (menu:GetTall() * 0.5)) end +---returns table +--start_index is the first known index +--continuous is whether it's continuous (some series have holes) +--end_index is the last known +function pace.FindAssetSeriesBounds(base_directory, base_file, extension) + + --LEADING ZEROES FIX NOT YET IMPLEMENTED + local function leading_zeros(str) + str = string.StripExtension(str) + + local untilzero_pattern = "%f[1-9][0-9]+$" + local afterzero_pattern = "0+%f[1-9+]" + local beforenumbers_pattern = "%f[%f[1-9][0-9]+$]" + --string.gsub(str, "%f[1-9][0-9]+$", "") --get the start until the zeros stop + + --string.gsub(str, "0+%f[1-9+]", "") --leave start + + if string.find(str, afterzero_pattern) then + return string.gsub(str, untilzero_pattern, string.match(str, afterzero_pattern)) + end + end + --print(base_file .. "leading zeros?" , leading_zeros(base_file)) + if searched_cache_series_results[base_directory .. "/" .. base_file] then return searched_cache_series_results[base_directory .. "/" .. base_file] end + local tbl = {} + local i = 0 --try with 0 at first + local keep_looking = true + local file_n + local lookaheads_left = 15 + local next_exists + tbl.start_index = nil + tbl.all_paths = {} + local index_compressed = 1 --increasing ID number of valid files + + while keep_looking do + + file_n = base_directory .. "/" .. base_file .. i .. "." .. extension + --print(file_n , "file" , file.Exists(file_n, "GAME") and "exists" or "doesn't exist") + --print("checking" , file_n) print("\tThe file" , file.Exists(file_n, "GAME") and "exists" or "doesn't exist") + if file.Exists(file_n, "GAME") then + if not tbl.start_index then tbl.start_index = i end + tbl.end_index = i + tbl.all_paths[index_compressed] = file_n + index_compressed = index_compressed + 1 + end + + + i = i + 1 + file_n = base_directory .. "/" .. base_file .. i .. "." .. extension + next_exists = file.Exists(file_n, "GAME") + if not next_exists then + if tbl.start_index then tbl.continuous = false end + lookaheads_left = lookaheads_left - 1 + else + lookaheads_left = 15 + end + keep_looking = next_exists or lookaheads_left > 0 + end + if not tbl.start_index then tbl.continuous = false end + --print("result of search:") + --PrintTable(tbl) + searched_cache_series_results[base_directory .. "/" .. base_file] = tbl + return tbl +end + + +function pace.AddSubmenuWithBracketExpansion(pnl, func, base_file, extension, base_directory) + if extension == "vmt" then base_directory = "materials" end --prescribed format: short + if extension == "mdl" then base_directory = "models" end --prescribed format: full + if extension == "wav" or extension == "mp3" or extension == "ogg" then base_directory = "sound" end --prescribed format: no trunk + + local base_file_original = base_file + if string.find(base_file, "%[%d+,%d+%]") then --find the bracket notation + base_file = string.gsub(base_file, "%[%d+,%d+%]$", "") + elseif string.find(base_file, "%d+") then + base_file = string.gsub(base_file, "%d+$", "") + end + + local tbl = pace.FindAssetSeriesBounds(base_directory, base_file, extension) + + local icon = "icon16/sound.png" + + if string.find(base_file, "music") or string.find(base_file, "theme") then + icon = "icon16/music.png" + elseif string.find(base_file, "loop") then + icon = "icon16/arrow_rotate_clockwise.png" + end + + if base_directory == "materials" then + icon = "icon16/paint_can.png" + elseif base_directory == "models" then + icon = "materials/spawnicons/"..string.gsub(base_file, ".mdl", "")..".png" + end + + local pnl2 + local menu2 + --print(base_file , #tbl.all_paths) + if #tbl.all_paths > 1 then + pnl2, menu2 = pnl:AddSubMenu(base_file .. " series", function() + func(base_file_original .. "." .. extension) + end) + + if base_directory == "materials" then + menu2:SetImage("icon16/table_multiple.png") + --local mat = string.gsub(base_file_original, "." .. string.GetExtensionFromFilename(base_file_original), "") + --pnl2:AddOption(mat, function() func(base_file_original) end):SetImage("icon16/paint_can.png") + elseif base_directory == "models" then + menu2:SetImage(icon) + elseif base_directory == "sound" then + --print("\t" .. icon) + menu2:SetImage(icon) + end + + else + if base_directory == "materials" then + --local mat = string.gsub(base_file_original, "." .. string.GetExtensionFromFilename(base_file_original), "") + --pnl2:AddOption(mat, function() func(base_file_original) end):SetImage("icon16/paint_can.png") + elseif base_directory == "models" then + + elseif base_directory == "sound" then + local snd = base_file_original + menu2 = pnl:AddOption(snd, function() func(snd) end):SetImage(icon) + end + end + + + --print(tbl) + --PrintTable(tbl.all_paths) + if not tbl then return end + if #tbl.all_paths > 1 then + for _,path in ipairs(tbl.all_paths) do + path_no_trunk = string.gsub(path, base_directory .. "/", "") + if base_directory == "materials" then + local mat = string.gsub(path_no_trunk, "." .. string.GetExtensionFromFilename(path_no_trunk), "") + pnl2:AddOption(mat, function() func(mat) end):SetMaterial(pace.get_unlit_mat(path)) + + elseif base_directory == "models" then + local mdl = path + pnl2:AddOption(string.GetFileFromFilename(mdl), function() func(mdl) end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") + + elseif base_directory == "sound" then + local snd = path_no_trunk + pnl2:AddOption(snd, function() func(snd) end):SetImage(icon) + end + end + end + + + +end + + local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight) local btn = vgui.Create("DButton", self) btn:SetSize(16, 16) @@ -134,6 +289,25 @@ pac.AddHook("GUIMousePressed", "pace_SafeRemoveSpecialPanel", function() end end) +pac.AddHook("PostRenderVGUI", "flash_properties", function() + if not pace.flashes then return end + for pnl, tbl in pairs(pace.flashes) do + if IsValid(pnl) then + --print(pnl:LocalToScreen(0,0)) + local x,y = pnl:LocalToScreen(0,0) + local flash_alpha = 255*math.pow(math.Clamp((tbl.flash_end - CurTime()) / 2.5,0,1), 0.6) + surface.SetDrawColor(Color(tbl.color.r, tbl.color.g, tbl.color.b, flash_alpha)) + local flash_size = 300*math.pow(math.Clamp((tbl.flash_end - 1.8 - CurTime()) / 0.7,0,1), 8) + 5 + if pnl:GetY() > 4 then + surface.DrawOutlinedRect(-flash_size + x,-flash_size + y,pnl:GetWide() + 2*flash_size,pnl:GetTall() + 2*flash_size,5) + surface.SetDrawColor(Color(tbl.color.r, tbl.color.g, tbl.color.b, flash_alpha/2)) + surface.DrawOutlinedRect(-flash_size + x - 3,-flash_size + y - 3,pnl:GetWide() + 2*flash_size + 6,pnl:GetTall() + 2*flash_size + 6,2) + end + if tbl.flash_end < CurTime() then pace.flashes[pnl] = nil end + end + end +end) + do -- container local PANEL = {} @@ -159,6 +333,28 @@ do -- container derma.SkinHook( "Paint", "CategoryButton", self, w, h ) end + function PANEL:Flash() + pace.flashes = pace.flashes or {} + pace.flashes[self] = {start = CurTime(), flash_end = CurTime() + 2.5, color = Color(255,0,0)} + + do --scroll to the property + local _,y = self:LocalToScreen(0,0) + local _,py = pace.properties:LocalToScreen(0,0) + local scry = pace.properties.scr:GetScroll() + + if y > ScrH() then + pace.properties.scr:SetScroll(scry - py + y) + elseif y < py - 200 then + pace.properties.scr:SetScroll(scry + (y - py) - 100) + end + end + + do --scroll to the tree node + pace.tree:ScrollToChild(self:GetChildren()[1].part.pace_tree_node) + end + + end + function PANEL:SetContent(pnl) pnl:SetParent(self) self.content = pnl @@ -726,11 +922,17 @@ end do -- base editable local PANEL = {} + PANEL.ClassName = "properties_base_type" PANEL.Base = "DLabel" PANEL.SingleClick = true + function PANEL:Flash() + --redirect to the parent (container) + self:GetParent():Flash() + end + function PANEL:OnCursorMoved() self:SetCursor("hand") end @@ -833,6 +1035,446 @@ do -- base editable self.OnValueChanged(self:GetValue()) end):SetImage(pace.MiscIcons.paste) + --command's String variable + if self.CurrentKey == "String" then + + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + pace.bookmarked_ressources["command"] = + { + --[[["user"] = { + + },]] + ["basic lua"] = { + { + lua = true, + nicename = "if alive then say I\'m alive", + expression = "if LocalPlayer():Health() > 0 then print(\"I\'m alive\") RunConsoleCommand(\"say\", \"I\'m alive\") else RunConsoleCommand(\"say\", \"I\'m DEAD\") end", + explanation = "To showcase a basic if/else statement, this will make you say \"I'm alive\" or \"I\'m DEAD\" depending on whether you have more than 0 health." + }, + { + lua = true, + nicename = "print 100 first numbers", + expression = "for i=0,100,1 do print(\"number\" .. i) end", + explanation = "To showcase a basic for loop (with the number setup), this will print the first 100 numbers in the console." + }, + { + lua = true, + nicename = "print all entities' health", + expression = "for _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end", + explanation = "To showcase a basic for loop (using a table iterator), this will print the list of all entities\' health" + }, + { + lua = true, + nicename = "print all entities' health", + expression = "local random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)", + explanation = "To showcase basic number handling and variables, this will run a pac_event command for \"event_1\" to \"event_5\"" + } + }, + ["movement"] ={ + { + lua = false, + nicename = "dash", + expression = "+forward;+speed", + explanation = "go forward. WARNING. It holds forever until you release it with -forward;-speed" + }, + }, + ["weapons"] = { + { + lua = false, + nicename = "go unarmed (using console)", + expression = "give none; use none", + explanation = "use the hands swep (\"none\"). In truth, we need to split the command and run the second one after a delay, or run the full thing twice. the console doesn't let us switch to a weapon we don't yet have" + }, + { + lua = true, + nicename = "go unarmed (using lua)", + expression = "RunConsoleCommand(\"give\", \"none\") timer.Simple(0.1, function() RunConsoleCommand(\"use\", \"none\") end)", + explanation = "use the hands swep (\"none\"). we need lua because the console doesn't let us switch to a weapon we don't yet have" + } + }, + ["events logic"] = { + { + lua = true, + nicename = "random command event activation", + expression = "RunConsoleCommand(\"pac_event\", \"COMMAND\" .. math.ceil(math.random()*4))", + explanation = "randomly pick between commands COMMAND1 to COMMAND4.\nReplace 4 to another whole number if you need more or less" + }, + { + lua = true, + nicename = "command series (held down)", + expression = "local i = LocalPlayer()[\"COMMAND\"] RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i, \"1\") RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i-1, \"0\") if i > 5 then i = 0 end LocalPlayer()[\"COMMAND\"] = i + 1", + explanation = "goes in the series of COMMAND1 to COMMAND5 activating the current number and deactivating the previous.\nYou can replace COMMAND for another name, and replace the i > 5 for another limit to loop back around\nAlthough now you can use pac_event_sequenced to control event series" + }, + { + lua = true, + nicename = "command series (impulse)", + expression = "local i = LocalPlayer()[\"COMMAND\"] RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i) if i >= 5 then i = 0 end LocalPlayer()[\"COMMAND\"] = i + 1", + explanation = "goes in the series of COMMAND1 to COMMAND5 activating one command instantaneously.\nYou can replace COMMAND for another name, and replace the i >= 5 for another limit to loop back around" + }, + { + lua = nil, + nicename = "save current events to a single command", + explanation = "this hardcoded preset should build a list of all your active command events and save it as a single command string for you" + } + }, + --[[["experimental things"] = { + { + nicename = "", + expression = "", + explanation = "" + }, + }]] + } + + local menu1, pnl1 = menu:AddSubMenu(L"example commands", function() + end) + pnl1:SetIcon("icon16/cart_go.png") + for group, tbl in pairs(pace.bookmarked_ressources["command"]) do + local icon = "icon16/bullet_white.png" + if group == "user" then icon = "icon16/user.png" + elseif group == "movement" then icon = "icon16/user_go.png" + elseif group == "weapons" then icon = "icon16/bomb.png" + elseif group == "events logic" then icon = "icon16/clock.png" + elseif group == "spatial" then icon = "icon16/world.png" + elseif group == "experimental things" then icon = "icon16/ruby.png" + end + local menu2, pnl2 = menu1:AddSubMenu(group) + pnl2:SetIcon(icon) + + if not table.IsEmpty(tbl) then + for i,tbl2 in pairs(tbl) do + --print(tbl2.nicename) + local str = tbl2.nicename or "invalid name" + local pnl3 = menu2:AddOption(str, function() + if pace.current_part.ClassName == "command" then + local expression = pace.current_part.String + local hardcode = tbl2.lua == nil + local new_expression = "" + if hardcode then + + if tbl2.nicename == "save current events to a single command" then + local tbl3 = {} + for i,v in pairs(LocalPlayer().pac_command_events) do tbl3[i] = v.on end + for i,v in pairs(LocalPlayer().pac_command_events) do RunConsoleCommand("pac_event", i, "0") end + new_expression = "" + + for i,v in pairs(tbl3) do new_expression = new_expression .. "pac_event " .. i .. " " .. v .. ";" end + pace.current_part:SetUseLua(false) + end + + end + if expression == "" then --blank: bare insert + expression = tbl2.expression + pace.current_part:SetUseLua(tbl2.lua) + elseif pace.current_part.UseLua == tbl2.lua then --something present: concatenate the existing bit but only if we're on the same mode + expression = expression .. ";" .. tbl2.expression + pace.current_part:SetUseLua(tbl2.lua) + end + + if not hardcode then + pace.current_part:SetString(expression) + self:SetValue(expression) + else + pace.current_part:SetString(new_expression) + self:SetValue(new_expression) + end + end + + end) + pnl3:SetIcon(icon) + pnl3:SetTooltip(tbl2.explanation) + end + + end + end + end + + --proxy expression + if self.CurrentKey == "Expression" then + + + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + pace.bookmarked_ressources["proxy"] = pace.bookmarked_ressources["proxy"] + local menu1, pnl1 = menu:AddSubMenu(L"Proxy template bits", function() + end) + pnl1:SetIcon("icon16/cart_go.png") + for group, tbl in pairs(pace.bookmarked_ressources["proxy"]) do + local icon = "icon16/bullet_white.png" + if group == "user" then icon = "icon16/user.png" + elseif group == "fades and transitions" then icon = "icon16/shading.png" + elseif group == "pulses" then icon = "icon16/transmit_blue.png" + elseif group == "facial expressions" then icon = "icon16/emoticon_smile.png" + elseif group == "spatial" then icon = "icon16/world.png" + elseif group == "experimental things" then icon = "icon16/ruby.png" + end + local menu2, pnl2 = menu1:AddSubMenu(group) + pnl2:SetIcon(icon) + + if not table.IsEmpty(tbl) then + for i,tbl2 in pairs(tbl) do + --print(tbl2.nicename) + local str = tbl2.nicename or "invalid name" + local pnl3 = menu2:AddOption(str, function() + if pace.current_part.ClassName == "proxy" then + local expression = pace.current_part.Expression + if expression == "" then --blank: bare insert + expression = tbl2.expression + elseif true then --something present: multiply the existing bit? + expression = expression .. " * " .. tbl2.expression + end + + pace.current_part:SetExpression(expression) + self:SetValue(expression) + end + + end) + pnl3:SetIcon(icon) + pnl3:SetTooltip(tbl2.explanation) + end + + end + end + end + + if self.CurrentKey == "LoadVmt" then + local owner = pace.current_part:GetOwner() + local name = string.GetFileFromFilename( owner:GetModel() ) + local mats = owner:GetMaterials() + + local pnl, menu2 = menu:AddSubMenu(L"Load " .. name .. "'s material", function() + end) + menu2:SetImage("icon16/paintcan.png") + + for id,mat in ipairs(mats) do + pnl:AddOption(string.GetFileFromFilename(mat), function() + pace.current_part:SetLoadVmt(mat) + end) + end + end + + if self.CurrentKey == "SurfaceProperties" and pace.current_part.GetSurfacePropsTable then + local tbl = pace.current_part:GetSurfacePropsTable() + menu:AddOption(L"See physics info", function() + local pnl2 = vgui.Create("DFrame") + local txt_zone = vgui.Create("DTextEntry", pnl2) + local str = "" + for i,v in pairs(tbl) do + str = str .. i .. " = " .. v .."\n" + end + txt_zone:SetMultiline(true) + txt_zone:SetText(str) + txt_zone:Dock(FILL) + pnl2:SetTitle("SurfaceProp info : " .. pace.current_part.SurfaceProperties) + pnl2:SetSize(500, 500) + pnl2:SetPos(ScrW()/2, ScrH()/2) + pnl2:MakePopup() + + end):SetImage("icon16/table.png") + + 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/cedrics_models/basic_shapes/plane.mdl", + "models/cedrics_models/basic_shapes/circle.mdl", + "models/hunter/blocks/cube025x025x025.mdl", + "models/editor/axis_helper.mdl", + "models/editor/axis_helper_thick.mdl" + } + end + + local pnl, menu2 = menu:AddSubMenu(L"Load favourite models", function() + end) + menu2:SetImage("icon16/cart_go.png") + + local pm = pace.current_part:GetPlayerOwner():GetModel() + + pnl:AddOption("Current 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") + + for id,mdl in ipairs(pace.bookmarked_ressources["models"]) do + pnl: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 + + 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 pnl, menu2 = menu:AddSubMenu(L"Load favourite materials", function() + end) + menu2: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.find(mat, "%[%d+,%d+%]") then --find the bracket notation + mat_no_ext = string.gsub(mat_no_ext, "%[%d+,%d+%]", "") + pace.AddSubmenuWithBracketExpansion(pnl, 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 + pnl: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 + 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 pnl, menu2 = menu:AddSubMenu(L"Load favourite sounds", function() + end) + menu2: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.find(snd_no_ext, "%[%d+,%d+%]") then --find the bracket notation + pace.AddSubmenuWithBracketExpansion(pnl, 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(pnl, 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 + + pnl: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 + + --long string menu to bypass the DLabel's limits, only applicable for sound2 for urls and base part's notes + if (pace.current_part.ClassName == "sound2" and self.CurrentKey == "Path") or self.CurrentKey == "Notes" then + + menu:AddOption(L"Insert long text", function() + local pnl = vgui.Create("DFrame") + local DText = vgui.Create("DTextEntry", pnl) + local DButtonOK = vgui.Create("DButton", pnl) + DText:SetMaximumCharCount(50000) + + pnl:SetSize(1200,800) + pnl:SetTitle("Long text 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) + DText:SetPos(5,25) + DText:SetSize(1190,700) + DText:SetMultiline(true) + DText:SetContentAlignment(7) + pnl:MakePopup() + DText:RequestFocus() + + DButtonOK.DoClick = function() + local str = DText:GetText() + if self.CurrentKey == "Notes" then + pace.current_part.Notes = str + elseif pace.current_part.ClassName == "sound2" then + pace.current_part.AllPaths = str + pace.current_part:UpdateSoundsFromAll() + end + pnl:Remove() + end + end):SetImage('icon16/text_letter_omega.png') + end + --left right swap available on strings (and parts) if type(self:GetValue()) == 'string' then menu:AddSpacer() @@ -1178,7 +1820,7 @@ do -- vector val = ctor(val.p, val.y, val.r) end - if _G.type(val):lower() == type or type == "color" then + if _G.type(val):lower() == type or type == "color" or type == "color2" then self:SetValue(val) self.OnValueChanged(self.vector * 1) @@ -1404,6 +2046,7 @@ do -- vector end, 0.25 ) + end do -- number @@ -1567,3 +2210,180 @@ do -- boolean pace.RegisterPanel(PANEL) end + + +local tree_search_excluded_vars = { + ["ParentUID"] = true, + ["UniqueID"] = true, + ["ModelTracker"] = true, + ["ClassTracker"] = true, + ["LoadVmt"] = true +} + +function pace.OpenTreeSearch() + if pace.tree_search_open then return end + pace.Editor.y_offset = 24 + pace.tree_search_open = true + pace.tree_search_match_index = 0 + pace.tree_search_matches = {} + local resulting_part + local search_term = "friend" + local matched_property + local matches = {} + + local base = vgui.Create("DFrame") + pace.tree_searcher = base + local edit = vgui.Create("DTextEntry", base) + local search_button = vgui.Create("DButton", base) + local range_label = vgui.Create("DLabel", base) + local close_button = vgui.Create("DButton", base) + local case_box = vgui.Create("DButton", base) + + case_box:SetText("Aa") + case_box:SetPos(325,2) + case_box:SetSize(25,20) + case_box:SetTooltip("case sensitive") + case_box:SetColor(Color(150,150,150)) + case_box:SetFont("DermaDefaultBold") + function case_box:DoClick() + self.on = not self.on + if self.on then + self:SetColor(Color(0,0,0)) + else + self:SetColor(Color(150,150,150)) + end + end + + + local function select_match() + if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end + if not pace.tree_search_matches[pace.tree_search_match_index] then return end + + resulting_part = pace.tree_search_matches[pace.tree_search_match_index].part_matched + matched_property = pace.tree_search_matches[pace.tree_search_match_index].key_matched + if resulting_part ~= pace.current_part then pace.OnPartSelected(resulting_part, true) end + local parent = resulting_part:GetParent() + while IsValid(parent) and (parent:GetParent() ~= parent) do + parent.pace_tree_node:SetExpanded(true) + parent = parent:GetParent() + if parent:IsValid() then + parent.pace_tree_node:SetExpanded(true) + end + end + --pace.RefreshTree() + pace.FlashProperty(resulting_part, matched_property, false) + end + + function base.OnRemove() + pace.tree_search_open = false + if not IsValid(pace.Editor) then return end + pace.Editor.y_offset = 0 + end + + function base.Think() + if not IsValid(pace.Editor) then base:Remove() return end + if not pace.Focused then base:Remove() end + base:SetX(pace.Editor:GetX()) + end + function base.Paint(_,w,h) + surface.SetDrawColor(Color(255,255,255)) + surface.DrawRect(0,0,w,h) + end + base:SetDraggable(false) + base:SetX(pace.Editor:GetX()) + base:ShowCloseButton(false) + + close_button:SetSize(40,20) + close_button:SetPos(450,2) + close_button:SetText("close") + function close_button.DoClick() + base:Remove() + end + + local fwd = vgui.Create("DButton", base) + local bck = vgui.Create("DButton", base) + + local function perform_search() + local case_sensitive = case_box.on + matches = {} + pace.tree_search_matches = {} + search_term = edit:GetText() + if not case_sensitive then search_term = string.lower(search_term) end + for _,part in pairs(pac.GetLocalParts()) do + + for k,v in pairs(part:GetProperties()) do + local value = v.get(part) + + if (type(value) ~= "number" and type(value) ~= "string") or tree_search_excluded_vars[v.key] then continue end + + value = tostring(value) + if not case_sensitive then value = string.lower(value) end + + if string.find(case_sensitive and v.key or string.lower(v.key), search_term) or (string.find(value, search_term)) then + if v.key == "Name" and part.Name == "" then continue end + table.insert(matches, #matches + 1, {part_matched = part, key_matched = v.key}) + table.insert(pace.tree_search_matches, #matches, {part_matched = part, key_matched = v.key}) + end + end + end + table.sort(pace.tree_search_matches, function(a, b) return select(2, a.part_matched.pace_tree_node:LocalToScreen()) < select(2, b.part_matched.pace_tree_node:LocalToScreen()) end) + if table.IsEmpty(matches) then range_label:SetText("0 / 0") else pace.tree_search_match_index = 1 end + range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) + end + + base:SetSize(492,24) + edit:SetSize(290,20) + edit:SetPos(0,2) + base:MakePopup() + edit:RequestFocus() + edit:SetUpdateOnType(true) + edit.previous_search = "" + + range_label:SetSize(50,20) + range_label:SetPos(295,2) + range_label:SetText("0 / 0") + range_label:SetTextColor(Color(0,0,0)) + + fwd:SetSize(25,20) + fwd:SetPos(375,2) + fwd:SetText(">") + function fwd.DoClick() + if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end + pace.tree_search_match_index = (pace.tree_search_match_index % math.max(#matches,1)) + 1 + range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) + select_match() + end + + search_button:SetSize(50,20) + search_button:SetPos(400,2) + search_button:SetText("search") + function search_button.DoClick() + perform_search() + select_match() + end + + bck:SetSize(25,20) + bck:SetPos(350,2) + bck:SetText("<") + function bck.DoClick() + if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end + pace.tree_search_match_index = ((pace.tree_search_match_index - 2 + #matches) % math.max(#matches,1)) + 1 + range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) + select_match() + end + + function edit.OnEnter() + if self.previous_search ~= edit:GetText() then + perform_search() + self.previous_search = edit:GetText() + elseif not table.IsEmpty(pace.tree_search_matches) then + fwd:DoClick() + else + perform_search() + end + select_match() + + timer.Simple(0.1,function() edit:RequestFocus() end) + end + +end \ No newline at end of file diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index eb3d91eab..9be74632f 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -1,3 +1,4 @@ +CreateClientConVar("pac_editor_scale","1", true, false) local L = pace.LanguageString local PANEL = {} @@ -8,7 +9,7 @@ PANEL.Base = "pac_dtree" function PANEL:Init() pace.pac_dtree.Init(self) - self:SetLineHeight(18) + self:SetLineHeight(18 * GetConVar("pac_editor_scale"):GetFloat()) self:SetIndentSize(10) self.parts = {} @@ -19,6 +20,7 @@ function PANEL:Init() end do + local function get_added_nodes(self) local added_nodes = {} for i,v in ipairs(self.added_nodes) do @@ -55,64 +57,66 @@ do function PANEL:Think(...) if not pace.current_part:IsValid() then return end - if - pace.current_part.pace_tree_node and - pace.current_part.pace_tree_node:IsValid() and not - ( - pace.BusyWithProperties:IsValid() or - pace.ActiveSpecialPanel:IsValid() or - pace.editing_viewmodel or - pace.editing_hands or - pace.properties.search:HasFocus() - ) and - not gui.IsConsoleVisible() - then - if input.IsKeyDown(KEY_LEFT) then - pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) - elseif input.IsKeyDown(KEY_RIGHT) then - pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) - end + if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then + if + pace.current_part.pace_tree_node and + pace.current_part.pace_tree_node:IsValid() and not + ( + pace.BusyWithProperties:IsValid() or + pace.ActiveSpecialPanel:IsValid() or + pace.editing_viewmodel or + pace.editing_hands or + pace.properties.search:HasFocus() + ) and + not gui.IsConsoleVisible() + then + if input.IsKeyDown(KEY_LEFT) then + pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) + elseif input.IsKeyDown(KEY_RIGHT) then + pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) + end - if input.IsKeyDown(KEY_UP) or input.IsKeyDown(KEY_PAGEUP) then - local added_nodes = get_added_nodes(self) - local offset = input.IsKeyDown(KEY_PAGEUP) and 10 or 1 - if not self.scrolled_up or self.scrolled_up < os.clock() then - for i,v in ipairs(added_nodes) do - if v == pace.current_part.pace_tree_node then - local node = added_nodes[i - offset] or added_nodes[1] - if node then - node:DoClick() - scroll_to_node(self, node) - break + if input.IsKeyDown(KEY_UP) or input.IsKeyDown(KEY_PAGEUP) then + local added_nodes = get_added_nodes(self) + local offset = input.IsKeyDown(KEY_PAGEUP) and 10 or 1 + if not self.scrolled_up or self.scrolled_up < os.clock() then + for i,v in ipairs(added_nodes) do + if v == pace.current_part.pace_tree_node then + local node = added_nodes[i - offset] or added_nodes[1] + if node then + node:DoClick() + scroll_to_node(self, node) + break + end end end - end - self.scrolled_up = self.scrolled_up or os.clock() + 0.4 + self.scrolled_up = self.scrolled_up or os.clock() + 0.4 + end + else + self.scrolled_up = nil end - else - self.scrolled_up = nil - end - if input.IsKeyDown(KEY_DOWN) or input.IsKeyDown(KEY_PAGEDOWN) then - local added_nodes = get_added_nodes(self) - local offset = input.IsKeyDown(KEY_PAGEDOWN) and 10 or 1 - if not self.scrolled_down or self.scrolled_down < os.clock() then - for i,v in ipairs(added_nodes) do - if v == pace.current_part.pace_tree_node then - local node = added_nodes[i + offset] or added_nodes[#added_nodes] - if node then - node:DoClick() - --scroll_to_node(self, node) - break + if input.IsKeyDown(KEY_DOWN) or input.IsKeyDown(KEY_PAGEDOWN) then + local added_nodes = get_added_nodes(self) + local offset = input.IsKeyDown(KEY_PAGEDOWN) and 10 or 1 + if not self.scrolled_down or self.scrolled_down < os.clock() then + for i,v in ipairs(added_nodes) do + if v == pace.current_part.pace_tree_node then + local node = added_nodes[i + offset] or added_nodes[#added_nodes] + if node then + node:DoClick() + --scroll_to_node(self, node) + break + end end end - end - self.scrolled_down = self.scrolled_down or os.clock() + 0.4 + self.scrolled_down = self.scrolled_down or os.clock() + 0.4 + end + else + self.scrolled_down = nil end - else - self.scrolled_down = nil end end @@ -156,8 +160,8 @@ do if not node.Icon.event_icon then local pnl = vgui.Create("DImage", node.Icon) pnl:SetImage("icon16/clock_red.png") - pnl:SetSize(8, 8) - pnl:SetPos(8, 8) + pnl:SetSize(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1))) + pnl:SetPos(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1))) pnl:SetVisible(false) node.Icon.event_icon = pnl end @@ -183,8 +187,83 @@ do end end end + + function DoScrollControl(self, action) + pace.BulkSelectKey = input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString()) + if + pace.current_part.pace_tree_node and + pace.current_part.pace_tree_node:IsValid() and not + ( + pace.BusyWithProperties:IsValid() or + pace.ActiveSpecialPanel:IsValid() or + pace.editing_viewmodel or + pace.editing_hands or + pace.properties.search:HasFocus() + ) and + not gui.IsConsoleVisible() + then + + if action == "editor_node_collapse" then + pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) + elseif action == "editor_node_expand" then + pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) + end + + if action == "editor_up" or action == "editor_pageup" then + local added_nodes = get_added_nodes(self) + local offset = action == "editor_pageup" and 10 or 1 + if not self.scrolled_up or self.scrolled_up < os.clock() then + for i,v in ipairs(added_nodes) do + if v == pace.current_part.pace_tree_node then + local node = added_nodes[i - offset] or added_nodes[1] + if node then + node:DoClick() + scroll_to_node(self, node) + if input.IsKeyDown(pace.BulkSelectKey) then pace.DoBulkSelect(node.part, true) end + break + end + end + end + + self.scrolled_up = self.scrolled_up or os.clock() + 0.4 + end + else + self.scrolled_up = nil + end + + if action == "editor_down" or action == "editor_pagedown" then + local added_nodes = get_added_nodes(self) + local offset = action == "editor_pagedown" and 10 or 1 + if not self.scrolled_down or self.scrolled_down < os.clock() then + for i,v in ipairs(added_nodes) do + if v == pace.current_part.pace_tree_node then + local node = added_nodes[i + offset] or added_nodes[#added_nodes] + if node then + node:DoClick() + if input.IsKeyDown(pace.BulkSelectKey) then pace.DoBulkSelect(node.part, true) end + --scroll_to_node(self, node) + break + end + end + end + + self.scrolled_down = self.scrolled_down or os.clock() + 0.4 + end + else + self.scrolled_down = nil + end + end + end + + function pace.DoScrollControls(action) + DoScrollControl(pace.tree, action) + end + end + + + function PANEL:OnMouseReleased(mc) if mc == MOUSE_RIGHT then pace.Call("PartMenu") @@ -192,7 +271,7 @@ function PANEL:OnMouseReleased(mc) end function PANEL:SetModel(path) - if not file.Exists(path, "GAME") then + if not file.Exists(path or "", "GAME") then path = player_manager.TranslatePlayerModel(path) if not file.Exists(path, "GAME") then print(path, "is invalid") @@ -352,7 +431,7 @@ function PANEL:AddNode(...) local add_button = node:Add("DImageButton") add_button:SetImage(pace.MiscIcons.new) - add_button:SetSize(16, 16) + add_button:SetSize(16*GetConVar("pac_editor_scale"):GetFloat(), 16*GetConVar("pac_editor_scale"):GetFloat()) add_button:SetVisible(false) add_button.DoClick = function() add_parts_menu(node) pace.Call("PartSelected", node.part) end add_button.DoRightClick = function() node:DoRightClick() end @@ -454,6 +533,7 @@ function PANEL:PopulateParts(node, parts, children) elseif isstring(part.Icon) then part_node.Icon:SetImage(part.Icon) end + part_node.Icon:SetSize(16 * GetConVar("pac_editor_scale"):GetFloat(),16 * GetConVar("pac_editor_scale"):GetFloat()) self:PopulateParts(part_node, part:GetChildren(), true) @@ -499,8 +579,8 @@ end function PANEL:Populate(reset) - self:SetLineHeight(18) - self:SetIndentSize(2) + self:SetLineHeight(18 * (1 + (GetConVar("pac_editor_scale"):GetFloat()-1))) + self:SetIndentSize(10) for key, node in pairs(self.parts) do if reset or (not node.part or not node.part:IsValid()) then @@ -530,6 +610,7 @@ local function remove_node(part) part.pace_tree_node:GetRoot().m_pSelectedItem = nil part.pace_tree_node:Remove() pace.RefreshTree() + end end @@ -564,7 +645,42 @@ pac.AddHook("pace_OnVariableChanged", "pace_create_tree_nodes", function(part, k end end) +local function refresh_events_gated() + pace.final_scheduled_event_refresh = pace.final_scheduled_event_refresh or CurTime() + 0.08 + pace.event_refresh_spam_time = CurTime() + hook.Add("Tick", "pace_refresh_events", function() + if CurTime() < pace.event_refresh_spam_time + 0.08 then return end + if CurTime() > pace.final_scheduled_event_refresh then + pace.RefreshEvents() + pace.final_scheduled_event_refresh = nil + hook.Remove("Tick", "pace_refresh_events") + end + end) +end + +function pace.RefreshEvents() + local events = {} + for _, part in pairs(pac.GetLocalParts()) do + if part.ClassName == "event" then + events[part] = part + end + end + local no_events = table.Count(events) == 0 + + for _, child in pairs(pac.GetLocalParts()) do + child.active_events = {} + child.active_events_ref_count = 0 + if not no_events then + for _,event in pairs(events) do + event:OnThink() + end + end + child:CallRecursive("CalcShowHide", false) + end +end + function pace.RefreshTree(reset) + --print("pace.RefreshTree("..tostring(reset)..")") if pace.tree:IsValid() then timer.Create("pace_refresh_tree", 0.01, 1, function() if pace.tree:IsValid() then @@ -573,8 +689,10 @@ function pace.RefreshTree(reset) pace.TrySelectPart() end + refresh_events_gated() end) end + end if Entity(1):IsPlayer() and not PAC_RESTART and not VLL2_FILEDEF then diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 65fad2a3d..ec92f2e36 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1,4 +1,49 @@ +--include("pac3/editor/client/panels/properties.lua") +include("popups_part_tutorials.lua") + local L = pace.LanguageString +pace.BulkSelectList = {} +pace.BulkSelectUIDs = {} +pace.BulkSelectClipboard = {} +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"} + +pace.operations_default = {"wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "spacer", "save", "load", "spacer", "remove"} + +pace.operations_experimental = {"help_part_info", "wear", "copy", "paste", "cut", "paste_properties", "clone", "bulk_select", "spacer", "registered_parts", "spacer", "bulk_apply_properties", "partsize_info", "copy_uid", "spacer", "save", "load", "spacer", "remove"} +pace.operations_bulk_poweruser = {"bulk_select","clone", "registered_parts", "spacer", "copy", "paste", "cut", "spacer", "wear", "save", "load", "partsize_info"} + +pace.operations_order = pace.operations_experimental + +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") + +CreateConVar( "pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") +CreateConVar( "pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") + +CreateConVar("pac_copilot_partsearch_depth", -1, FCVAR_ARCHIVE, "amount of copiloting in the searchable part menu\n-1:none\n0:auto-focus on the text edit for events\n1:bring up a list of clickable event types\nother parts aren't supported yet") +CreateConVar("pac_copilot_make_popup_when_selecting_event", 1, FCVAR_ARCHIVE, "whether to create a popup so you can read what an event does") +CreateConVar("pac_copilot_open_asset_browser_when_creating_part", 0, FCVAR_ARCHIVE, "whether to open the asset browser for models, materials, or sounds") +CreateConVar("pac_copilot_force_preview_cameras", 1, FCVAR_ARCHIVE, "whether to force the editor camera off when creating a camera part") +CreateConVar("pac_copilot_auto_setup_command_events", 0, FCVAR_ARCHIVE, "whether to automatically setup a command event if the name you type doesn't match an existing event. we'll assume you want a command event name.\nif this is set to 0, it will only auto-setup in such a case if you already have such a command event actively present in your events or waiting to be activated by your command parts") +CreateConVar("pac_copilot_auto_focus_main_property_when_creating_part", 1, FCVAR_ARCHIVE, "whether to automatically focus on the main property that defines a part, such as the event's event type, the text's text, the proxy's expression or the command's string.") + +--the necessary properties we always edit for certain parts +--others might be opened with the asset browser so this is not the full list +--should be a minimal list because we don't want to get too much in the way of routine editing +local star_properties = { + ["event"] = "Event", + ["proxy"] = "Expression", + ["text"] = "Text", + ["command"] = "String", + ["animation"] = "SequenceName", + ["flex"] = "Flex", + ["bone3"] = "Bone", + ["poseparameter"] = "PoseParameter", + ["damage_zone"] = "Damage", + ["hitscan"] = "Damage" +} -- load only when hovered above local function add_expensive_submenu_load(pnl, callback) @@ -11,12 +56,16 @@ local function add_expensive_submenu_load(pnl, callback) end function pace.WearParts(temp_wear_filter) + pace.still_loading_wearing = true + local allowed, reason = pac.CallHook("CanWearParts", pac.LocalPlayer) if allowed == false then pac.Message(reason or "the server doesn't want you to wear parts for some reason") return end + + pace.still_loading_wearing = false return pace.WearOnServer(temp_wear_filter) end @@ -98,13 +147,154 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) end pace.RefreshTree() + timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) + + if GetConVar("pac_copilot_open_asset_browser_when_creating_part"):GetBool() then + timer.Simple(0.5, function() + local self = nil + if class_name == "model2" then + self = pace.current_part.pace_properties["Model"] + + pace.AssetBrowser(function(path) + if not part:IsValid() then return end + -- because we refresh the properties + + if IsValid(self) and self.OnValueChanged then + self.OnValueChanged(path) + end + + if pace.current_part.SetMaterials then + local model = pace.current_part:GetModel() + local part = pace.current_part + if part.pace_last_model and part.pace_last_model ~= model then + part:SetMaterials("") + end + part.pace_last_model = model + end + + pace.PopulateProperties(pace.current_part) + + for k,v in ipairs(pace.properties.List) do + if v.panel and v.panel.part == part and v.key == key then + self = v.panel + break + end + end + + end, "models") + elseif class_name == "sound" or class_name == "sound2" then + if class_name == "sound"then + self = pace.current_part.pace_properties["Sound"] + elseif class_name == "sound2" then + self = pace.current_part.pace_properties["Path"] + end + + pace.AssetBrowser(function(path) + if not self:IsValid() then return end + + self:SetValue(path) + self.OnValueChanged(path) + + end, "sound") + elseif pace.current_part.pace_properties["LoadVmt"] then + self = pace.current_part.pace_properties["LoadVmt"] + pace.AssetBrowser(function(path) + if not self:IsValid() then return end + path = string.gsub(string.StripExtension(path), "^materials/", "") or "error" + self:SetValue(path) + self.OnValueChanged(path) + pace.current_part:SetLoadVmt(path) + end, "materials") + + end + end) + + end + if class_name == "camera" and GetConVar("pac_copilot_force_preview_cameras"):GetBool() then + RunConsoleCommand("pac_enable_editor_view", "0") + pace.EnableView(true) + pac.RemoveHook("CalcView", "editor") + pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) + part:CalcShowHide() + pos, ang, fov, nearz, farz = part:CalcView(_,_,ply:EyeAngles()) + local temp = {} + temp.origin = pos + temp.angles = ang + temp.fov = fov + temp.znear = nearz + temp.zfar = farz + temp.drawviewer = not part.DrawViewModel + return temp + + end) + end + if GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool() then + if star_properties[part.ClassName] then + timer.Simple(0.2, function() + + pace.FlashProperty(part, star_properties[part.ClassName], true) + + end) + end + end return part end +local last_span_select_part +local last_select_was_span = false +local last_direction + function pace.OnPartSelected(part, is_selecting) - local parent = part:GetRootPart() + pace.delaybulkselect = pace.delaybulkselect or 0 --a time updated in shortcuts.lua to prevent common pac operations from triggering bulk selection + local bulk_key_pressed = input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) + if RealTime() > pace.delaybulkselect and bulk_key_pressed and not input.IsKeyDown(input.GetKeyCode("v")) and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then + --jumping multi-select if holding shift + ctrl + if bulk_key_pressed and input.IsShiftDown() then + --ripped some local functions from tree.lua + local added_nodes = {} + for i,v in ipairs(pace.tree.added_nodes) do + if v.part and v:IsVisible() and v:IsExpanded() then + table.insert(added_nodes, v) + end + end + + local startnodenumber = table.KeyFromValue( added_nodes, pace.current_part.pace_tree_node) + local endnodenumber = table.KeyFromValue( added_nodes, part.pace_tree_node) + + table.sort(added_nodes, function(a, b) return select(2, a:LocalToScreen()) < select(2, b:LocalToScreen()) end) + + local i = startnodenumber + + direction = math.Clamp(endnodenumber - startnodenumber,-1,1) + if direction == 0 then last_direction = direction return end + last_direction = last_direction or 0 + if last_span_select_part == nil then last_span_select_part = part end + + if last_select_was_span then + if last_direction == -direction then + pace.DoBulkSelect(pace.current_part, true) + end + if last_span_select_part == pace.current_part then + pace.DoBulkSelect(pace.current_part, true) + end + end + while (i ~= endnodenumber) do + pace.DoBulkSelect(added_nodes[i].part, true) + i = i + direction + end + pace.DoBulkSelect(part) + last_direction = direction + last_select_was_span = true + else + pace.DoBulkSelect(part) + last_select_was_span = false + end + else last_select_was_span = false end + last_span_select_part = part + + local parent = part:GetRootPart() if parent:IsValid() and (parent.OwnerName == "viewmodel" or parent.OwnerName == "hands") then pace.editing_viewmodel = parent.OwnerName == "viewmodel" pace.editing_hands = parent.OwnerName == "hands" @@ -118,11 +308,9 @@ function pace.OnPartSelected(part, is_selecting) pace.mctrl.SetTarget(part) pace.SetViewPart(part) - if pace.Editor:IsValid() then pace.Editor:InvalidateLayout() end - pace.SafeRemoveSpecialPanel() if pace.tree:IsValid() then @@ -134,6 +322,29 @@ function pace.OnPartSelected(part, is_selecting) if not is_selecting then pace.StopSelect() end + +end + +pace.suppress_flashing_property = false + +function pace.FlashProperty(obj, key, edit) + if pace.suppress_flashing_property then return end + if not obj.flashing_property then + obj.flashing_property = true + timer.Simple(0.1, function() + if not obj.pace_properties[key] then return end + obj.pace_properties[key]:Flash() + pace.current_flashed_property = key + if edit then + obj.pace_properties[key]:RequestFocus() + if obj.pace_properties[key].EditText then + obj.pace_properties[key]:EditText() + end + end + end) + timer.Simple(0.3, function() obj.flashing_property = false end) + end + end function pace.OnVariableChanged(obj, key, val, not_from_editor) @@ -241,7 +452,12 @@ function pace.GetRegisteredParts() end do -- menu + local trap + if not pace.Active or refresh_halo_hook then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + end +//@note registered parts function pace.AddRegisteredPartsToMenu(menu, parent) local partsToShow = {} local clicked = false @@ -286,16 +502,39 @@ do -- menu end end end + return newMenuEntry end local sortedTree = {} - + local PartStructure = {} + local Groups = {} + local Parts = pac.GetRegisteredParts() for _, part in pairs(pace.GetRegisteredParts()) do + local class = part.ClassName + local groupname = "other" + local group = part.Group or part.Groups or "other" - + --print(group) if isstring(group) then + --MsgC(Color(0,255,0), "\t" .. group .. "\n") + groupname = group group = {group} + else + --PrintTable(group) + Groups[groupname] = Groups[groupname] or {} + for i,v in ipairs(group) do + Groups[v] = Groups[v] or {} + Groups[v][class] = Groups[v][class] or class + end end + + Groups[groupname] = Groups[groupname] or group + + Groups[groupname][class] = Groups[groupname][class] or class + + --[[if isstring(group) then + group = {group} + end]] for i, name in ipairs(group) do if not sortedTree[name] then @@ -314,52 +553,117 @@ do -- menu end end end - + + --file.Write("pac_partgroups.txt", util.TableToKeyValues(Groups)) + local other = sortedTree.other sortedTree.other = nil - for group, groupData in pairs(sortedTree) do - local sub, pnl = menu:AddSubMenu(groupData.name, function() - if groupData.group_class_name then - pace.RecordUndoHistory() - pace.Call("CreatePart", groupData.group_class_name, nil, nil, parent) - pace.RecordUndoHistory() + if not file.Exists("pac3_config/pac_part_categories.txt", "DATA") then + for group, groupData in pairs(sortedTree) do + local sub, pnl = menu:AddSubMenu(groupData.name, function() + if groupData.group_class_name then + pace.RecordUndoHistory() + pace.Call("CreatePart", groupData.group_class_name, nil, nil, parent) + pace.RecordUndoHistory() + end + end) + + sub.GetDeleteSelf = function() return false end + + if groupData.icon then + pnl:SetImage(groupData.icon) end - end) - sub.GetDeleteSelf = function() return false end + trap = false + table.sort(groupData.parts, function(a, b) return a.ClassName < b.ClassName end) + for i, part in ipairs(groupData.parts) do + add_part(sub, part) + end - if groupData.icon then - pnl:SetImage(groupData.icon) - end + hook.Add('Think', sub, function() + local ctrl = input.IsControlDown() - trap = false - table.sort(groupData.parts, function(a, b) return a.ClassName < b.ClassName end) - for i, part in ipairs(groupData.parts) do - add_part(sub, part) - end + if clicked and not ctrl then + sub:SetDeleteSelf(true) + RegisterDermaMenuForClose(sub) + CloseDermaMenus() + return + end - hook.Add('Think', sub, function() - local ctrl = input.IsControlDown() + sub:SetDeleteSelf(not ctrl) + end) + + hook.Add('CloseDermaMenus', sub, function() + if input.IsControlDown() and trap then + trap = false + sub:SetVisible(true) + end - if clicked and not ctrl then - sub:SetDeleteSelf(true) RegisterDermaMenuForClose(sub) - CloseDermaMenus() - return + end) + end + else --custom part categories + pace.partgroups = pace.partgroups or util.KeyValuesToTable(file.Read("pac3_config/pac_part_categories.txt", "DATA")) + Groups = pace.partgroups + --group is the group name + --tbl is a shallow table with part class names + --PrintTable(Groups) + for group, tbl in pairs(Groups) do + + local sub, pnl = menu:AddSubMenu(group, function() + if Parts[group] then + if group == "entity" then + pace.RecordUndoHistory() + pace.Call("CreatePart", "entity2", nil, nil, parent) + pace.RecordUndoHistory() + elseif group == "model" then + pace.RecordUndoHistory() + pace.Call("CreatePart", "model2", nil, nil, parent) + pace.RecordUndoHistory() + else + pace.RecordUndoHistory() + pace.Call("CreatePart", group, nil, nil, parent) + pace.RecordUndoHistory() + end + end + end) + +--@note partmenu definer + sub.GetDeleteSelf = function() return false end + + if tbl["icon"] then + --print(tbl["icon"]) + if pace.MiscIcons[string.gsub(tbl["icon"], "pace.MiscIcons.", "")] then + pnl:SetImage(pace.MiscIcons[string.gsub(tbl["icon"], "pace.MiscIcons.", "")]) + else + local img = string.gsub(tbl["icon"], ".png", "") --remove the png extension + img = string.gsub(img, "icon16/", "") --remove the icon16 base path + img = "icon16/" .. img .. ".png" --why do this? to be able to write any form and let the program fix the form + pnl:SetImage(img) + end + elseif Parts[group] then + pnl:SetImage(Parts[group].Icon) + else + pnl:SetImage("icon16/page_white.png") end - - sub:SetDeleteSelf(not ctrl) - end) - - hook.Add('CloseDermaMenus', sub, function() - if input.IsControlDown() and trap then - trap = false - sub:SetVisible(true) + if tbl["tooltip"] then + pnl:SetTooltip(tbl["tooltip"]) end - - RegisterDermaMenuForClose(sub) - end) + --trap = false + table.sort(tbl, function(a, b) return a < b end) + for i, class in pairs(tbl) do + if isstring(i) and Parts[class] then + local tooltip = pace.TUTORIALS.PartInfos[class].tooltip + + if not tooltip or tooltip == "" then tooltip = "no information available" end + if #i > 2 then + local part_submenu = add_part(sub, Parts[class]) + part_submenu:SetTooltip(tooltip) + end + end + end + end end for i,v in ipairs(other.parts) do @@ -380,9 +684,19 @@ do -- menu end function pace.OnAddPartMenu(obj) + local event_part_template + for _, part in ipairs(pace.GetRegisteredParts()) do + if part.ClassName == "event" then + event_part_template = part + end + end + local mode = GetConVar("pac_copilot_partsearch_depth"):GetInt() + pace.suppress_flashing_property = false + local base = vgui.Create("EditablePanel") base:SetPos(input.GetCursorPos()) base:SetSize(200, 300) + base:MakePopup() function base:OnRemove() @@ -395,38 +709,212 @@ do -- menu edit:RequestFocus() edit:SetUpdateOnType(true) - local result = base:Add("DPanel") + local result = base:Add("DScrollPanel") result:Dock(FILL) + base.search_mode = "classes" + + local function populate_with_sounds(base,result,filter) + base.search_mode = "sounds" + for _,snd in ipairs(pace.bookmarked_ressources["sound"]) do + if filter ~= nil and filter ~= "" then + if snd:find(filter, nil, true) then + table.insert(result.found, snd) + end + else + table.insert(result.found, snd) + end + end + for _,snd in ipairs(result.found) do + if not isstring(snd) then continue end + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + local icon = "icon16/sound.png" - function edit:OnEnter() - if result.found[1] then - pace.RecordUndoHistory() - pace.Call("CreatePart", result.found[1].ClassName) - pace.RecordUndoHistory() + 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 + + btn:SetIcon(icon) + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText(snd) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() + + line.DoClick = function() + if pace.current_part.ClassName == "sound" then + pace.current_part:SetSound(snd) + elseif pace.current_part.ClassName == "sound2" then + pace.current_part:SetPath(snd) + end + pace.PopulateProperties(pace.current_part) + base:Remove() + end + + line:Dock(TOP) end - base:Remove() end - edit.OnValueChange = function(_, str) - result:Clear() - result.found = {} + local function populate_with_models(base,result,filter) + base.search_mode = "models" + for _,mdl in ipairs(pace.bookmarked_ressources["models"]) do + if filter ~= nil and filter ~= "" then + if mdl:find(filter, nil, true) then + table.insert(result.found, mdl) + end + else + table.insert(result.found, mdl) + end + end + for _,mdl in ipairs(result.found) do + if not isstring(mdl) then continue end + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + btn:SetIcon("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText(mdl) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() - for _, part in ipairs(pace.GetRegisteredParts()) do - if (part.FriendlyName or part.ClassName):find(str, nil, true) then - table.insert(result.found, part) + line.DoClick = function() + if pace.current_part.Model then + pace.current_part:SetModel(mdl) + pace.PopulateProperties(pace.current_part) + end + base:Remove() end + + line:Dock(TOP) end + end - table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end) + local function populate_with_events(base,result,filter) + base.search_mode = "events" + for e,tbl in pairs(event_part_template.Events) do + if filter ~= nil and filter ~= "" then + if e:find(filter, nil, true) then + table.insert(result.found, e) + end + else + table.insert(result.found, e) + end + end + for _,e in ipairs(result.found) do + if not isstring(e) then continue end + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + btn:SetIcon("icon16/clock.png") + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText(e) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() + + line.DoClick = function() + if pace.current_part.Event then + pace.current_part:SetEvent(e) + pace.PopulateProperties(pace.current_part) + end + base:Remove() + end + + line:Dock(TOP) + end + end + local function populate_with_classes(base, result, filter) + for i, part in ipairs(pace.GetRegisteredParts()) do + if filter then + if (part.FriendlyName or part.ClassName):find(filter, nil, true) then + table.insert(result.found, part) + end + else table.insert(result.found, part) end + end + table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end) for _, part in ipairs(result.found) do local line = result:Add("DButton") line:SetText("") line:SetTall(20) + local remove_now = false line.DoClick = function() pace.RecordUndoHistory() pace.Call("CreatePart", part.ClassName) - base:Remove() + + if part.ClassName == "event" then + remove_now = false + result:Clear() + result.found = {} + + if mode == 0 then --auto-focus mode + remove_now = true + timer.Simple(0.1, function() + pace.FlashProperty(pace.current_part, "Event", true) + end) + elseif mode == 1 then --event partsearch + pace.suppress_flashing_property = true + populate_with_events(base,result,"") + edit:SetText("") + edit:RequestFocus() + else + remove_now = true + end + elseif part.ClassName == "model2" and mode == 1 then --model partsearch + remove_now = false + result:Clear() + result.found = {} + populate_with_models(base,result,"") + pace.suppress_flashing_property = true + edit:SetText("") + edit:RequestFocus() + elseif (part.ClassName == "sound" or part.ClassName == "sound2") and mode == 1 then + remove_now = false + result:Clear() + result.found = {} + populate_with_sounds(base,result,"") + pace.suppress_flashing_property = true + edit:SetText("") + edit:RequestFocus() + elseif star_properties[result.found[1].ClassName] and (mode == 0 or GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool()) then + pace.suppress_flashing_property = false + local classname = part.ClassName + timer.Simple(0.1, function() + pace.FlashProperty(pace.current_part, star_properties[classname], true) + end) + remove_now = true + else + remove_now = true + end + timer.Simple(0.4, function() + pace.suppress_flashing_property = false + end) + if remove_now then base:Remove() end pace.RecordUndoHistory() end @@ -457,8 +945,109 @@ do -- menu line:Dock(TOP) end + end + + function edit:OnEnter() + local remove_now = true + if result.found[1] then + if base.search_mode == "classes" then + pace.RecordUndoHistory() + local part = pace.Call("CreatePart", result.found[1].ClassName) + pace.RecordUndoHistory() + + if mode == 1 then + if result.found[1].ClassName == "event" then + result:Clear() + populate_with_events(base,result,"") + elseif result.found[1].ClassName == "model2" then + result:Clear() + populate_with_models(base,result,"") + end + + else + base:Remove() + end + + elseif base.search_mode == "events" then + if pace.current_part.Event then + pace.current_part:SetEvent() + pace.PopulateProperties(pace.current_part) + end + base:Remove() + + elseif base.search_mode == "models" then + if mode == 1 then + result:Clear() + pace.current_part:SetModel(result.found[1]) + pace.PopulateProperties(pace.current_part) + else + base:Remove() + end + elseif base.search_mode == "sounds" then + if mode == 1 then + result:Clear() + if pace.current_part.ClassName == "sound" then + pace.current_part:SetSound(result.found[1]) + elseif pace.current_part.ClassName == "sound2" then + pace.current_part:SetPath(result.found[1]) + end + pace.PopulateProperties(pace.current_part) + else + base:Remove() + end + + end + + if result.found[1].ClassName == "event" then + remove_now = false + result:Clear() + result.found = {} + if mode == 0 then + remove_now = true + timer.Simple(0.1, function() + pace.FlashProperty(pace.current_part, "Event", true) + end) + elseif mode == 1 then + pace.suppress_flashing_property = true + populate_with_events(base,result,"") + edit:SetText("") + edit:RequestFocus() + else + remove_now = true + end + elseif star_properties[result.found[1].ClassName] and (mode == 0 or GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool()) then + local classname = result.found[1].ClassName + timer.Simple(0.1, function() + pace.FlashProperty(pace.current_part, star_properties[classname], true) + end) + end + end + timer.Simple(0.4, function() + pace.suppress_flashing_property = false + end) + if remove_now then base:Remove() end + end - base:SetHeight(20 * #result.found + edit:GetTall()) + edit.OnValueChange = function(_, str) + result:Clear() + result.found = {} + local remove_now = true + + if base.search_mode == "classes" then + populate_with_classes(base, result, str) + + elseif base.search_mode == "events" then + populate_with_events(base,result,str,event_template) + + elseif base.search_mode == "models" then + populate_with_models(base,result,str) + elseif base.search_mode == "sounds" then + populate_with_sounds(base,result,str) + end + + --base:SetHeight(20 * #result.found + edit:GetTall()) + base:SetHeight(600 + edit:GetTall()) + end edit:OnValueChange("") @@ -470,6 +1059,12 @@ do -- menu end end end) + + timer.Simple(0.1, function() + base:MoveToFront() + base:RequestFocus() + end) + end function pace.Copy(obj) @@ -511,6 +1106,8 @@ do -- menu end function pace.RemovePart(obj) + if table.HasValue(pace.BulkSelectList,obj) then table.RemoveByValue(pace.BulkSelectList,obj) end + pace.RecordUndoHistory() obj:Remove() pace.RecordUndoHistory() @@ -520,93 +1117,1676 @@ do -- menu if not obj:HasParent() and obj.ClassName == "group" then pace.RemovePartOnServer(obj:GetUniqueID(), false, true) end + if obj.ClassName == "camera" and GetConVar("pac_copilot_force_preview_cameras"):GetBool() then + local no_camera_part = true + for i,v in ipairs(pac.GetLocalParts()) do + if v.ClassName == "camera" then no_camera_part = false end + end + if no_camera_part then + RunConsoleCommand("pac_enable_editor_view", "1") + pac.RemoveHook("CalcView", "camera_part") + pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil) + end + end end - function pace.OnPartMenu(obj) - local menu = DermaMenu() - menu:SetPos(input.GetCursorPos()) - - if obj then - if not obj:HasParent() then - menu:AddOption(L"wear", function() pace.SendPartToServer(obj) end):SetImage(pace.MiscIcons.wear) + function pace.SwapBaseMovables(obj1, obj2, promote) + if not obj1 or not obj2 then return end + if not obj1.Position or not obj1.Angles or not obj2.Position or not obj2.Angles then return end + local base_movable_fields = { + "Position", "PositionOffset", "Angles", "AngleOffset", "EyeAngles", "AimPart", "AimPartName" + } + local a_part = obj2 + local b_part = obj1 + + if promote then --obj1 takes place of obj2 up or down the hierarchy + if obj1.Parent == obj2 then + a_part = obj2 + b_part = obj1 + elseif obj2.Parent == obj1 then + a_part = obj1 + b_part = obj2 end - - menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy) - menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste) - menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png') - menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) - menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) - - menu:AddSpacer() end - pace.AddRegisteredPartsToMenu(menu, not obj) + for i,field in ipairs(base_movable_fields) do + local a_val = a_part["Get"..field](a_part) + local b_val = b_part["Get"..field](b_part) + a_part["Set"..field](a_part, b_val) + b_part["Set"..field](b_part, a_val) + end - menu:AddSpacer() + if promote then + b_part:SetParent(a_part.Parent) b_part:SetEditorExpand(true) + a_part:SetParent(b_part) a_part:SetEditorExpand(true) + else + local a_parent = a_part.Parent + local b_parent = b_part.Parent - if obj then - local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end) - pnl:SetImage(pace.MiscIcons.save) - add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end) + a_part:SetParent(b_parent) + b_part:SetParent(a_parent) end + pace.RefreshTree() + end - local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) - add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) + function pace.SubstituteBaseMovable(obj,action) + if action == "create_parent" then + Derma_StringRequest("Create substitute parent", "Select a class name to create a parent", "model2", + function(str) + if str == "model" then str = "model2" end --I don't care, stop using legacy + local newObj = pac.CreatePart(str) + if not IsValid(newObj) then return end - pnl:SetImage(pace.MiscIcons.load) + newObj:SetParent(obj.Parent) + obj:SetParent(newObj) - if obj then - menu:AddSpacer() - menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear) - end + for i,v in pairs(obj:GetChildren()) do + v:SetParent(newObj) + end - menu:Open() - menu:MakePopup() + newObj:SetPosition(obj.Position) + newObj:SetPositionOffset(obj.PositionOffset) + newObj:SetAngles(obj.Angles) + newObj:SetAngleOffset(obj.AngleOffset) + newObj:SetEyeAngles(obj.EyeAngles) + newObj:SetAimPart(obj.AimPart) + newObj:SetAimPartName(obj.AimPartName) + newObj:SetBone(obj.Bone) + newObj:SetEditorExpand(true) + + obj:SetPosition(Vector(0,0,0)) + obj:SetPositionOffset(Vector(0,0,0)) + obj:SetAngles(Angle(0,0,0)) + obj:SetAngleOffset(Angle(0,0,0)) + obj:SetEyeAngles(false) + obj:SetAimPart(nil) + obj:SetAimPartName("") + obj:SetBone("head") + + pace.RefreshTree() + end) + elseif action == "reorder_child" then + if obj.Parent then + if obj.Parent.Position and obj.Parent.Angles then + pace.SwapBaseMovables(obj, obj.Parent, true) + end + end + pace.RefreshTree() + elseif action == "cast" then + Derma_StringRequest("Cast", "Select a class name to convert to. Make sure you know what you\'re doing! It will do a pac_restart after!", "model2", + function(str) + if str == obj.ClassName then return end + if str == "model" then str = "model2" end --I don't care, stop using legacy + local uid = obj.UniqueID + + if pace.Editor:IsValid() then + pace.RefreshTree() + pace.Editor:InvalidateLayout() + pace.RefreshTree() + end + + + obj.ClassName = str + + timer.Simple(0, function() + _G.pac_Restart() + if str == "model2" then + obj = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid) + obj:SetModel("models/pac/default.mdl") + end + + end) + end) + end end - function pace.OnNewPartMenu() - pace.current_part = NULL - local menu = DermaMenu() - menu:MakePopup() - menu:SetPos(input.GetCursorPos()) + function pace.ClearBulkList() + for _,v in ipairs(pace.BulkSelectList) do + if v.pace_tree_node ~= nil then v.pace_tree_node:SetAlpha( 255 ) end + v:SetInfo() + end + pace.BulkSelectList = {} + pac.Message("Bulk list deleted!") + --surface.PlaySound('buttons/button16.wav') + end +//@note pace.DoBulkSelect + function pace.DoBulkSelect(obj, silent) + obj = obj or pace.current_part + refresh_halo_hook = false + --print(obj.pace_tree_node, "color", obj.pace_tree_node:GetFGColor().r .. " " .. obj.pace_tree_node:GetFGColor().g .. " " .. obj.pace_tree_node:GetFGColor().b) + if obj.ClassName == "timeline_dummy_bone" then return end + local selected_part_added = false --to decide the sound to play afterward + + pace.BulkSelectList = pace.BulkSelectList or {} + if (table.HasValue(pace.BulkSelectList, obj)) then + pace.RemoveFromBulkSelect(obj) + selected_part_added = false + elseif (pace.BulkSelectList[obj] == nil) then + pace.AddToBulkSelect(obj) + selected_part_added = true + for _,v in ipairs(obj:GetChildrenList()) do + pace.RemoveFromBulkSelect(v) + end + end - pace.AddRegisteredPartsToMenu(menu) + --check parents and children + for _,v in ipairs(pace.BulkSelectList) do + if table.HasValue(v:GetChildrenList(), obj) then + --print("selected part is already child to a bulk-selected part!") + pace.RemoveFromBulkSelect(obj) + selected_part_added = false + elseif table.HasValue(obj:GetChildrenList(), v) then + --print("selected part is already parent to a bulk-selected part!") + pace.RemoveFromBulkSelect(v) + selected_part_added = false + end + end - menu:AddSpacer() + RebuildBulkHighlight() + if not silent then + if selected_part_added then + surface.PlaySound('buttons/button1.wav') + + else surface.PlaySound('buttons/button16.wav') end + end - local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) - pnl:SetImage(pace.MiscIcons.load) + if table.IsEmpty(pace.BulkSelectList) then + --remove halo hook + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + else + --start halo hook + hook.Add('PreDrawHalos', "BulkSelectHighlights", function() + local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + if mode == 0 then return + elseif mode == 1 then ThinkBulkHighlight() + elseif mode == 2 then if input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) then ThinkBulkHighlight() end + elseif mode == 3 then if input.IsControlDown() then ThinkBulkHighlight() end + elseif mode == 4 then if input.IsShiftDown() then ThinkBulkHighlight() end + end + end) + end + + for _,v in ipairs(pace.BulkSelectList) do + --v.pace_tree_node:SetAlpha( 150 ) + end + end + + function pace.RemoveFromBulkSelect(obj) + table.RemoveByValue(pace.BulkSelectList, obj) + obj.pace_tree_node:SetAlpha( 255 ) + obj:SetInfo() + --RebuildBulkHighlight() + end + + function pace.AddToBulkSelect(obj) + table.insert(pace.BulkSelectList, obj) + if obj.pace_tree_node == nil then return end + obj:SetInfo("selected in bulk select") + obj.pace_tree_node:SetAlpha( 150 ) + --RebuildBulkHighlight() + end + function pace.BulkHide() + if #pace.BulkSelectList == 0 then return end + local first_bool = pace.BulkSelectList[1]:GetHide() + for _,v in ipairs(pace.BulkSelectList) do + v:SetHide(not first_bool) + end + end +//@note apply properties + function pace.BulkApplyProperties(obj, policy) + local basepart = obj + --[[if not table.HasValue(pace.BulkSelectList,obj) then + basepart = pace.BulkSelectList[1] + end]] + + local Panel = vgui.Create( "DFrame" ) + Panel:SetSize( 500, 600 ) + Panel:Center() + Panel:SetTitle("BULK SELECT PROPERTY EDIT - WARNING! EXPERIMENTAL FEATURE!") + + Panel:MakePopup() + surface.CreateFont("Font", { + font = "Arial", + extended = true, + weight = 700, + size = 15 + }) + + local scroll_panel = vgui.Create("DScrollPanel", Panel) + scroll_panel:SetSize( 500, 540 ) + scroll_panel:SetPos( 0, 60 ) + local thoroughness_tickbox = vgui.Create("DCheckBox", Panel) + thoroughness_tickbox:SetSize(20,20) + thoroughness_tickbox:SetPos( 5, 30 ) + local thoroughness_tickbox_label = vgui.Create("DLabel", Panel) + thoroughness_tickbox_label:SetSize(150,30) + thoroughness_tickbox_label:SetPos( 30, 25 ) + thoroughness_tickbox_label:SetText("Affect children?") + thoroughness_tickbox_label:SetFont("Font") + local basepart_label = vgui.Create("DLabel", Panel) + basepart_label:SetSize(340,30) + basepart_label:SetPos( 160, 25 ) + local partinfo = basepart.ClassName + if basepart.ClassName == "event" then partinfo = basepart.Event .. " " .. partinfo end + local partinfo_icon = vgui.Create("DImage",basepart_label) + partinfo_icon:SetSize(30,30) + partinfo_icon:SetPos( 300, 0 ) + partinfo_icon:SetImage(basepart.Icon) + + basepart_label:SetText("base part: "..partinfo) + basepart_label:SetFont("Font") + + local excluded_vars = { + ["Duplicate"] = true, + ["OwnerName"] = true, + ["ParentUID"] = true, + ["UniqueID"] = true, + ["TargetEntityUID"] = true + } + + local shared_properties = {} + local shared_udata_properties = {} + + for _,prop in pairs(basepart:GetProperties()) do + + local shared = true + for _,part2 in pairs(pace.BulkSelectList) do + if basepart ~= part2 and basepart.ClassName ~= part2.ClassName then + if part2["Get" .. prop["key"]] == nil then + if policy == "harsh" then shared = false end + end + end + end + if shared and not prop.udata.editor_friendly and basepart["Get" .. prop["key"]] ~= nil then + shared_properties[#shared_properties + 1] = prop["key"] + elseif shared and prop.udata.editor_friendly and basepart["Get" .. prop["key"]] == nil then + if not table.HasValue(shared_udata_properties, "event_udata_"..prop["key"]) then + shared_udata_properties[#shared_udata_properties + 1] = "event_udata_"..prop["key"] + end + end + end + + if policy == "lenient" then + local initial_shared_properties = table.Copy(shared_properties) + local initial_shared_udata_properties = table.Copy(shared_udata_properties) + for _,part2 in pairs(pace.BulkSelectList) do + for _,prop in ipairs(part2:GetProperties()) do + if not (table.HasValue(shared_properties, prop["key"]) or table.HasValue(shared_udata_properties, "event_udata_"..prop["key"])) then + if part2["Get" .. prop["key"]] ~= nil then + initial_shared_properties[#initial_shared_properties + 1] = prop["key"] + elseif part2["Get" .. prop["key"]] == nil then + if not table.HasValue(initial_shared_udata_properties, "event_udata_"..prop["key"]) then + initial_shared_udata_properties[#initial_shared_udata_properties + 1] = "event_udata_"..prop["key"] + end + end + end + end + end + shared_properties = initial_shared_properties + shared_udata_properties = initial_shared_udata_properties + end + + for i,v in ipairs(shared_properties) do + if excluded_vars[v] then table.remove(shared_properties,i) end + end + + --populate panels for standard GetSet part properties + for i,v in pairs(shared_properties) do + local VAR_PANEL = vgui.Create("DFrame") + VAR_PANEL:SetSize(500,30) + VAR_PANEL:SetPos(0,0) + VAR_PANEL:ShowCloseButton( false ) + local VAR_PANEL_BUTTON = VAR_PANEL:Add("DButton") + VAR_PANEL_BUTTON:SetSize(80,30) + VAR_PANEL_BUTTON:SetPos(400,0) + local VAR_PANEL_EDITZONE + local var_type + for _,testpart in ipairs(pace.BulkSelectList) do + if + testpart["Get" .. v] ~= nil + then + var_type = type(testpart["Get" .. v](testpart)) + end + end + if basepart["Get" .. v] ~= nil then var_type = type(basepart["Get" .. v](basepart)) end + + if var_type == "number" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "boolean" then + VAR_PANEL_EDITZONE = vgui.Create("DCheckBox", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(30,30) + elseif var_type == "string" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "Vector" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "Angle" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + else + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + end + VAR_PANEL_EDITZONE:SetPos(200,0) + + VAR_PANEL_BUTTON:SetText("APPLY") + + VAR_PANEL:SetTitle("[" .. i .. "] "..v.." "..var_type) + + VAR_PANEL:Dock( TOP ) + VAR_PANEL:DockMargin( 5, 0, 0, 5 ) + VAR_PANEL_BUTTON.DoClick = function() + for i,part in pairs(pace.BulkSelectList) do + local sent_var + if var_type == "number" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if not tonumber(sent_var) then + local ok, res = pac.CompileExpression(sent_var) + if ok then + sent_var = res() or 0 + end + end + elseif var_type == "boolean" then + sent_var = VAR_PANEL_EDITZONE:GetChecked() + elseif var_type == "string" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if v == "Name" and sent_var ~= "" then + sent_var = sent_var..i + end + elseif var_type == "Vector" then + local str = string.Split(VAR_PANEL_EDITZONE:GetValue(), ",") + sent_var = Vector() + sent_var.x = tonumber(str[1]) or 1 + sent_var.y = tonumber(str[2]) or 1 + sent_var.z = tonumber(str[3]) or 1 + if v == "Color" and not part.ProperColorRange then sent_var = sent_var*255 end + elseif var_type == "Angle" then + local str = string.Split(VAR_PANEL_EDITZONE:GetValue(), ",") + sent_var = Angle() + sent_var.r = tonumber(str[1]) or 1 + sent_var.g = tonumber(str[2]) or 1 + sent_var.b = tonumber(str[3]) or 1 + else sent_var = VAR_PANEL_EDITZONE:GetValue() end + + + if policy == "harsh" then part["Set" .. v](part, sent_var) + elseif policy == "lenient" then + if part["Get" .. v] ~= nil then part["Set" .. v](part, sent_var) end + end + if thoroughness_tickbox:GetChecked() then + for _,child in pairs(part:GetChildrenList()) do + if part["Get" .. v] ~= nil then child["Set" .. v](child, sent_var) end + end + end + end + + pace.RefreshTree(true) + timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) + end + scroll_panel:AddItem( VAR_PANEL ) + end + + --populate panels for event "userdata" packaged into arguments + if #shared_udata_properties > 0 then + local fallback_event_types = {} + local fallback_event + for i,v in ipairs(pace.BulkSelectList) do + if v.ClassName == "event" then + table.Add(fallback_event_types,v.Event) + fallback_event = v + end + end + + --[[example udata arg from part.Events[part.Event].__registeredArguments + 1: + 1 = button + 2 = string + 3: + default = mouse_left + enums = function: 0xa88929ea + group = arguments + ]] + + local function GetEventArgType(part, str) + + for argn,arg in ipairs(part.Events[part.Event].__registeredArguments) do + if arg[1] == str then + return arg[2] + end + end + if fallback_event then + for i,e in ipairs(fallback_event_types) do + for argn,arg in ipairs(fallback_event.Events[e].__registeredArguments) do + if arg[1] == str then + return arg[2] + end + end + end + end + return "string" + end + + local function GetEventArgIndex(part,str) + str = string.gsub(str, "event_udata_", "") + + for argn,arg in ipairs(part.Events[part.Event].__registeredArguments) do + if arg[1] == str then + return argn + end + end + return 1 + end + + local function ApplyArgToIndex(args_str, str, index) + local args_tbl = string.Split(args_str,"@@") + args_tbl[index] = str + return table.concat(args_tbl,"@@") + end + + for i,v in ipairs(shared_udata_properties) do + + local udata_val_name = string.gsub(v, "event_udata_", "") + + local var_type = GetEventArgType(obj, udata_val_name) + if var_type == nil then var_type = "string" end + + local VAR_PANEL = vgui.Create("DFrame") + + VAR_PANEL:SetSize(500,30) + VAR_PANEL:SetPos(0,0) + VAR_PANEL:ShowCloseButton( false ) + local VAR_PANEL_BUTTON = VAR_PANEL:Add("DButton") + VAR_PANEL_BUTTON:SetSize(80,30) + VAR_PANEL_BUTTON:SetPos(400,0) + local VAR_PANEL_EDITZONE + if var_type == "number" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "boolean" then + VAR_PANEL_EDITZONE = vgui.Create("DCheckBox", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(30,30) + elseif var_type == "string" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "Vector" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + elseif var_type == "Angle" then + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + else + VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) + VAR_PANEL_EDITZONE:SetSize(200,30) + end + + VAR_PANEL_EDITZONE:SetPos(200,0) + VAR_PANEL:SetTitle("[" .. i .. "] "..udata_val_name.." "..var_type) + VAR_PANEL_BUTTON:SetText("APPLY") + + + VAR_PANEL:Dock( TOP ) + VAR_PANEL:DockMargin( 5, 0, 0, 5 ) + VAR_PANEL_BUTTON.DoClick = function() + + for i,part in ipairs(pace.BulkSelectList) do + --PrintTable(part.Events[part.Event].__registeredArguments) + if part.ClassName == "event" and part.Event == basepart.Event then + local sent_var + if var_type == "number" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if not tonumber(sent_var) then + local ok, res = pac.CompileExpression(sent_var) + if ok then + sent_var = res() or 0 + end + end + elseif var_type == "boolean" then + sent_var = VAR_PANEL_EDITZONE:GetChecked() + if sent_var == true then sent_var = "1" + else sent_var = "0" end + elseif var_type == "string" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if v == "Name" and sent_var ~= "" then + sent_var = sent_var..i + end + else sent_var = VAR_PANEL_EDITZONE:GetValue() end + + part:SetArguments(ApplyArgToIndex(part:GetArguments(), sent_var, GetEventArgIndex(part,v))) + end + + if thoroughness_tickbox:GetChecked() then + for _,child in pairs(part:GetChildrenList()) do + if child.ClassName == "event" and child.Event == basepart.Event then + local sent_var + if var_type == "number" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if not tonumber(sent_var) then + local ok, res = pac.CompileExpression(sent_var) + if ok then + sent_var = res() or 0 + end + end + elseif var_type == "boolean" then + sent_var = VAR_PANEL_EDITZONE:GetChecked() + if sent_var == true then sent_var = "1" + else sent_var = "0" end + elseif var_type == "string" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if v == "Name" and sent_var ~= "" then + sent_var = sent_var..i + end + else sent_var = VAR_PANEL_EDITZONE:GetValue() end + + child:SetArguments(ApplyArgToIndex(child:GetArguments(), sent_var, GetEventArgIndex(child,v))) + end + end + end + end + + pace.RefreshTree(true) + timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) + end + scroll_panel:AddItem( VAR_PANEL ) + end + end + end + + function pace.BulkCutPaste(obj) + pace.RecordUndoHistory() + for _,v in ipairs(pace.BulkSelectList) do + --if a part is inserted onto itself, it should instead serve as a parent + if v ~= obj then v:SetParent(obj) end + end + pace.RecordUndoHistory() + pace.RefreshTree() + end + + function pace.BulkCutPasteOrdered() --two-state operation + --first to define an ordered list of parts to move, from bulk select + --second to transfer these parts to bulk select list + if not pace.ordered_operation_readystate then + pace.temp_bulkselect_orderedlist = {} + for i,v in ipairs(pace.BulkSelectList) do + pace.temp_bulkselect_orderedlist[i] = v + end + pace.ordered_operation_readystate = true + pace.ClearBulkList() + pace.FlashNotification("Selected " .. #pace.temp_bulkselect_orderedlist .. " parts for Ordered Insert. Now select " .. #pace.temp_bulkselect_orderedlist .. " parts destinations.") + surface.PlaySound("buttons/button4.wav") + else + if #pace.temp_bulkselect_orderedlist == #pace.BulkSelectList then + pace.RecordUndoHistory() + for i,v in ipairs(pace.BulkSelectList) do + pace.temp_bulkselect_orderedlist[i]:SetParent(v) + end + pace.RecordUndoHistory() + pace.RefreshTree() + surface.PlaySound("buttons/button6.wav") + end + pace.ordered_operation_readystate = false + end + end + + function pace.BulkCopy(obj) + if #pace.BulkSelectList == 1 then pace.Copy(obj) end --at least if there's one selected, we can take it that we want to copy that part + pace.BulkSelectClipboard = table.Copy(pace.BulkSelectList) --if multiple parts are selected, copy it to a new bulk clipboard + print("copied: ") + TestPrintTable(pace.BulkSelectClipboard,"pace.BulkSelectClipboard") + end + + function pace.BulkPasteFromBulkClipboard(obj) --paste bulk clipboard into one part + pace.RecordUndoHistory() + if not table.IsEmpty(pace.BulkSelectClipboard) then + for _,v in ipairs(pace.BulkSelectClipboard) do + local newObj = pac.CreatePart(v.ClassName) + newObj:SetTable(v:ToTable(), true) + newObj:SetParent(obj) + end + end + pace.RecordUndoHistory() + --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end) + end + + function pace.BulkPasteFromBulkSelectToSinglePart(obj) --paste bulk selection into one part + pace.RecordUndoHistory() + if not table.IsEmpty(pace.BulkSelectList) then + for _,v in ipairs(pace.BulkSelectList) do + local newObj = pac.CreatePart(v.ClassName) + newObj:SetTable(v:ToTable(), true) + newObj:SetParent(obj) + end + end + pace.RecordUndoHistory() + end + + function pace.BulkPasteFromSingleClipboard() --paste the normal clipboard into each bulk select item + pace.RecordUndoHistory() + if not table.IsEmpty(pace.BulkSelectList) then + for _,v in ipairs(pace.BulkSelectList) do + local newObj = pac.CreatePart(pace.Clipboard.self.ClassName) + newObj:SetTable(pace.Clipboard, true) + newObj:SetParent(v) + end + end + pace.RecordUndoHistory() + --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end) + end + + function pace.BulkPasteFromBulkClipboardToBulkSelect() + for _,v in ipairs(pace.BulkSelectList) do + pace.BulkPasteFromBulkClipboard(v) + end + end + + function pace.BulkRemovePart() + pace.RecordUndoHistory() + if not table.IsEmpty(pace.BulkSelectList) then + for _,v in ipairs(pace.BulkSelectList) do + v:Remove() + + if not v:HasParent() and v.ClassName == "group" then + pace.RemovePartOnServer(v:GetUniqueID(), false, true) + end + end + end + pace.RefreshTree() + pace.RecordUndoHistory() + pace.ClearBulkList() + --timer.Simple(0.1, function BulkSelectRefreshFadedNodes() end) + end + + function pace.CopyUID(obj) + pace.Clipboard = obj.UniqueID + SetClipboardText("\"" .. obj.UniqueID .. "\"") + pace.FlashNotification(tostring(obj) .. " UID " .. obj.UniqueID .. " has been copied") + end +//@note part menu + function pace.OnPartMenu(obj) + local menu = DermaMenu() + menu:SetPos(input.GetCursorPos()) + --new_operations_order + --default_operations_order + --if not obj then obj = pace.current_part end + for _,option_name in ipairs(pace.operations_order) do + pace.addPartMenuComponent(menu, obj, option_name) + end + + --[[if obj then + if not obj:HasParent() then + menu:AddOption(L"wear", function() + pace.SendPartToServer(obj) + pace.BulkSelectList = {} + end):SetImage(pace.MiscIcons.wear) + end + + menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy) + menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste) + menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png') + menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) + menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) + + local part_size_info, psi_icon = menu:AddSubMenu(L"get part size information", function() + local function GetTableSizeInfo(obj_arg) + if not IsValid(obj_arg) then return { + raw_bytes = 0, + info = "" + } end + local charsize = #util.TableToJSON(obj_arg:ToTable()) + + local kilo_range = -1 + local remainder = charsize*2 + while remainder / 1000 > 1 do + kilo_range = kilo_range + 1 + remainder = remainder / 1000 + end + local unit = "" + if kilo_range == -1 then + unit = "B" + elseif kilo_range == 0 then + unit = "KB" + elseif (kilo_range == 1) then + unit = "MB" + elseif (kilo_range == 2) then + unit = "GB" + end + return { + raw_bytes = charsize*2, + info = "raw JSON table size: " .. charsize*2 .. " bytes (" .. remainder .. " " .. unit .. ")" + } + end + + local part_size_info = GetTableSizeInfo(obj) + local part_size_info_root = GetTableSizeInfo(obj:GetRootPart()) + + local part_size_info_root_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_root.raw_bytes,1) .. "% share of root " + + local part_size_info_parent + local part_size_info_parent_processed + if IsValid(obj.Parent) then + part_size_info_parent = GetTableSizeInfo(obj.Parent) + part_size_info_parent_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_parent.raw_bytes,1) .. "% share of parent " + pac.Message( + obj, " " .. + part_size_info.info.."\n".. + part_size_info_parent_processed,obj.Parent,"\n".. + part_size_info_root_processed,obj:GetRootPart() + ) + else + pac.Message( + obj, " " .. + part_size_info.info.."\n".. + part_size_info_root_processed,obj:GetRootPart() + ) + end + + end) + psi_icon:SetImage('icon16/drive.png') + + part_size_info:AddOption(L"from bulk select", function() + local cumulative_bytes = 0 + for _,v in pairs(pace.BulkSelectList) do + cumulative_bytes = cumulative_bytes + 2*#util.TableToJSON(v:ToTable()) + end + local kilo_range = -1 + local remainder = cumulative_bytes + while remainder / 1000 > 1 do + kilo_range = kilo_range + 1 + remainder = remainder / 1000 + end + local unit = "" + if kilo_range == -1 then + unit = "B" + elseif kilo_range == 0 then + unit = "KB" + elseif (kilo_range == 1) then + unit = "MB" + elseif (kilo_range == 2) then + unit = "GB" + end + pac.Message("Bulk selected parts total " .. remainder .. unit) + end + ) + + local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end) + bap_icon:SetImage('icon16/table_multiple.png') + bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end) + bulk_apply_properties:AddOption("Policy: lenient filtering", function() pace.BulkApplyProperties(obj, "lenient") end) + + --bulk select + bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#pace.BulkSelectList..")", function() pace.DoBulkSelect(obj) end) + bs_icon:SetImage('icon16/table_multiple.png') + bulk_menu.GetDeleteSelf = function() return false end + + local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + local info + if mode == 0 then info = "not halo-highlighted" + elseif mode == 1 then info = "automatically halo-highlighted" + elseif mode == 2 then info = "halo-highlighted on custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString() + elseif mode == 3 then info = "halo-highlighted on preset keypress: control" + elseif mode == 4 then info = "halo-highlighted on preset keypress: shift" end + + bulk_menu:AddOption(L"Bulk select info: "..info, function() end):SetImage(pace.MiscIcons.info) + bulk_menu:AddOption(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts", function() end):SetImage(pace.MiscIcons.info) + + bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() + pace.BulkCutPaste(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Copy to Bulk Clipboard", function() + pace.BulkCopy(obj) + end):SetImage(pace.MiscIcons.copy) + + bulk_menu:AddSpacer() + + --bulk paste modes + bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function() + pace.BulkPasteFromBulkSelectToSinglePart(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function() + if not pace.Clipboard then pace.Copy(obj) end + pace.BulkPasteFromSingleClipboard() + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function() + pace.BulkPasteFromBulkClipboard(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function() + for _,v in ipairs(pace.BulkSelectList) do + pace.BulkPasteFromBulkClipboard(v) + end + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk paste properties from selected part", function() + pace.Copy(obj) + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddOption(L"Bulk paste properties from clipboard", function() + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk Delete", function() + pace.BulkRemovePart() + end):SetImage(pace.MiscIcons.clear) + + bulk_menu:AddOption(L"Clear Bulk List", function() + pace.ClearBulkList() + end):SetImage('icon16/table_delete.png') + + menu:AddSpacer() + end]] + + --pace.AddRegisteredPartsToMenu(menu, not obj) + + --menu:AddSpacer() + + --[[if obj then + local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end) + pnl:SetImage(pace.MiscIcons.save) + add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end) + end]] + + --[[local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) + pnl:SetImage(pace.MiscIcons.load)]] + + --[[if obj then + menu:AddSpacer() + menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear) + end]] + + menu:Open() + menu:MakePopup() + end + function pace.OnNewPartMenu() + pace.current_part = NULL + local menu = DermaMenu() + + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + + pace.AddRegisteredPartsToMenu(menu) + + menu:AddSpacer() + + local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) + pnl:SetImage(pace.MiscIcons.load) + add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) menu:AddOption(L"clear", function() + pace.ClearBulkList() pace.ClearParts() end):SetImage(pace.MiscIcons.clear) end + + +end + +function pace.GetPartSizeInformation(obj) + if not IsValid(obj) then return { raw_bytes = 0, info = "" } end + local charsize = #util.TableToJSON(obj:ToTable()) + local root_charsize = #util.TableToJSON(obj:GetRootPart():ToTable()) + + local roots = {} + local all_charsize = 0 + for i,v in pairs(pac.GetLocalParts()) do + roots[v:GetRootPart()] = v:GetRootPart() + end + for i,v in pairs(roots) do + all_charsize = all_charsize + #util.TableToJSON(v:ToTable()) + end + + return { + raw_bytes = charsize*2, + info = "raw JSON table size: " .. charsize*2 .. " bytes (" .. string.NiceSize(charsize*2) .. ")", + root_share_fraction = math.Round(charsize / root_charsize, 1), + root_share_percent = math.Round(100 * charsize / root_charsize, 1), + all_share_fraction = math.Round(charsize / all_charsize, 1), + all_share_percent = math.Round(100 * charsize / all_charsize, 1), + all_size_raw_bytes = all_charsize*2, + all_size_nice = string.NiceSize(all_charsize*2) + } end -do +function pace.addPartMenuComponent(menu, obj, option_name) + + if option_name == "save" and obj then + local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end) + pnl:SetImage(pace.MiscIcons.save) + add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end) + elseif option_name == "load" then + local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end) + add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end) + pnl:SetImage(pace.MiscIcons.load) + elseif option_name == "wear" and obj then + if not obj:HasParent() then + menu:AddOption(L"wear", function() + pace.SendPartToServer(obj) + pace.BulkSelectList = {} + end):SetImage(pace.MiscIcons.wear) + end + elseif option_name == "remove" and obj then + menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear) + elseif option_name == "copy" and obj then + local menu2, pnl = menu:AddSubMenu(L"copy", function() pace.Copy(obj) end) + pnl:SetIcon(pace.MiscIcons.copy) + --menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy) + menu2:AddOption(L"Copy part UniqueID", function() pace.CopyUID(obj) end):SetImage(pace.MiscIcons.uniqueid) + elseif option_name == "paste" and obj then + menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste) + elseif option_name == "cut" and obj then + menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png') + elseif option_name == "paste_properties" and obj then + menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) + elseif option_name == "clone" and obj then + menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) + elseif option_name == "partsize_info" and obj then + local function GetTableSizeInfo(obj_arg) + return pace.GetPartSizeInformation(obj_arg) + end + local part_size_info, psi_icon = menu:AddSubMenu(L"get part size information", function() + local part_size_info = GetTableSizeInfo(obj) + local part_size_info_root = GetTableSizeInfo(obj:GetRootPart()) + + local part_size_info_root_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_root.raw_bytes,1) .. "% share of root " + + local part_size_info_parent + local part_size_info_parent_processed + if IsValid(obj.Parent) then + part_size_info_parent = GetTableSizeInfo(obj.Parent) + part_size_info_parent_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_parent.raw_bytes,1) .. "% share of parent " + pac.Message( + obj, " " .. + part_size_info.info.."\n".. + part_size_info_parent_processed,obj.Parent,"\n".. + part_size_info_root_processed,obj:GetRootPart() + ) + else + pac.Message( + obj, " " .. + part_size_info.info.."\n".. + part_size_info_root_processed,obj:GetRootPart() + ) + end + + end) + psi_icon:SetImage('icon16/drive.png') + part_size_info:AddOption(L"from bulk select", function() + local cumulative_bytes = 0 + for _,v in pairs(pace.BulkSelectList) do + v.partsize_info = pace.GetPartSizeInformation(v) + cumulative_bytes = cumulative_bytes + 2*#util.TableToJSON(v:ToTable()) + end + + pac.Message("Bulk selected parts total " .. string.NiceSize(cumulative_bytes) .. "\nhere's the breakdown:") + for _,v in pairs(pace.BulkSelectList) do + local partsize_info = pace.GetPartSizeInformation(v) + MsgC(Color(100,255,100), string.NiceSize(partsize_info.raw_bytes)) MsgC(Color(200,200,200), " - ", v, "\n\t ") + MsgC(Color(0,255,255), math.Round(100 * partsize_info.raw_bytes/cumulative_bytes,1) .. "%") + MsgC(Color(200,200,200), " of bulk select total\n\t ") + MsgC(Color(0,255,255), math.Round(100 * partsize_info.raw_bytes/partsize_info.all_size_raw_bytes,1) .. "%") + MsgC(Color(200,200,200), " of total local parts)\n") + end + end) + elseif option_name == "bulk_apply_properties" then + local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end) + bap_icon:SetImage('icon16/application_form.png') + bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end) + bulk_apply_properties:AddOption("Policy: lenient filtering", function() pace.BulkApplyProperties(obj, "lenient") end) + elseif option_name == "bulk_select" then + bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#pace.BulkSelectList..")", function() pace.DoBulkSelect(obj) end) + bs_icon:SetImage('icon16/table_multiple.png') + bulk_menu.GetDeleteSelf = function() return false end + + local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + local info + if mode == 0 then info = "not halo-highlighted" + elseif mode == 1 then info = "automatically halo-highlighted" + elseif mode == 2 then info = "halo-highlighted on custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString() + elseif mode == 3 then info = "halo-highlighted on preset keypress: control" + elseif mode == 4 then info = "halo-highlighted on preset keypress: shift" end + + bulk_menu:AddOption(L"Bulk select info: "..info, function() end):SetImage(pace.MiscIcons.info) + bulk_menu:AddOption(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts", function() end):SetImage(pace.MiscIcons.info) + + bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() + pace.BulkCutPaste(obj) + end):SetImage('icon16/arrow_join.png') + + if not pace.ordered_operation_readystate then + bulk_menu:AddOption(L"prepare Ordered Insert (please select parts in order beforehand)", function() + pace.BulkCutPasteOrdered() + end):SetImage('icon16/text_list_numbers.png') + else + bulk_menu:AddOption(L"do Ordered Insert (select destinations in order)", function() + pace.BulkCutPasteOrdered() + end):SetImage('icon16/arrow_switch.png') + end + + + bulk_menu:AddOption(L"Copy to Bulk Clipboard", function() + pace.BulkCopy(obj) + end):SetImage(pace.MiscIcons.copy) + + bulk_menu:AddSpacer() + + --bulk paste modes + bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function() + pace.BulkPasteFromBulkSelectToSinglePart(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function() + if not pace.Clipboard then pace.Copy(obj) end + pace.BulkPasteFromSingleClipboard() + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function() + pace.BulkPasteFromBulkClipboard(obj) + end):SetImage('icon16/arrow_join.png') + + bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function() + pace.BulkPasteFromBulkClipboardToBulkSelect() + end):SetImage('icon16/arrow_divide.png') + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk paste properties from selected part", function() + pace.Copy(obj) + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddOption(L"Bulk paste properties from clipboard", function() + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end):SetImage(pace.MiscIcons.replace) + + bulk_menu:AddOption(L"Deploy a numbered command event series ("..#pace.BulkSelectList..")", function() + Derma_StringRequest(L"command series", L"input the base name", "", function(str) + str = string.gsub(str, " ", "") + for i,v in ipairs(pace.BulkSelectList) do + part = pac.CreatePart("event") + part:SetParent(v) + part.Event = "command" + part.Arguments = str..i.."@@0@@0" + end + end) + end):SetImage('icon16/clock.png') + + bulk_menu:AddOption(L"Pack into a new root group", function() + root = pac.CreatePart("group") + for i,v in ipairs(pace.BulkSelectList) do + v:SetParent(root) + end + end):SetImage('icon16/world.png') + + bulk_menu:AddSpacer() + + bulk_menu:AddOption(L"Bulk Delete", function() + pace.BulkRemovePart() + end):SetImage(pace.MiscIcons.clear) + + bulk_menu:AddOption(L"Clear Bulk List", function() + pace.ClearBulkList() + end):SetImage('icon16/table_delete.png') + elseif option_name == "spacer" then + menu:AddSpacer() + elseif option_name == "registered_parts" then + pace.AddRegisteredPartsToMenu(menu, not obj) + elseif option_name == "hide_editor" then + menu:AddOption(L"hide editor / toggle focus", function() pace.Call("ToggleFocus") end):SetImage('icon16/zoom.png') + elseif option_name == "expand_all" and obj then + menu:AddOption(L"expand all", function() + obj:CallRecursive('SetEditorExpand', true) + pace.RefreshTree(true) end):SetImage('icon16/arrow_down.png') + elseif option_name == "collapse_all" and obj then + menu:AddOption(L"collapse all", function() + obj:CallRecursive('SetEditorExpand', false) + pace.RefreshTree(true) end):SetImage('icon16/arrow_in.png') + elseif option_name == "copy_uid" and obj then + local menu2, pnl = menu:AddSubMenu(L"Copy part UniqueID", function() pace.CopyUID(obj) end) + pnl:SetIcon(pace.MiscIcons.uniqueid) + elseif option_name == "help_part_info" and obj then + menu:AddOption(L"View help or info about this part", function() pac.AttachInfoPopupToPart(obj, nil, { + obj_type = GetConVar("pac_popups_preferred_location"):GetString(), + hoverfunc = "open", + pac_part = pace.current_part, + panel_exp_width = 900, panel_exp_height = 400 + }) end):SetImage('icon16/information.png') + elseif option_name == "reorder_movables" and obj then + if (obj.Position and obj.Angles and obj.PositionOffset) then + local substitute, pnl = menu:AddSubMenu("Reorder / replace base movable") + pnl:SetImage('icon16/application_double.png') + substitute:AddOption("Create a parent for position substitution", function() pace.SubstituteBaseMovable(obj, "create_parent") end) + if obj.Parent then + if obj.Parent.Position and obj.Parent.Angles then + substitute:AddOption("Switch with parent", function() pace.SubstituteBaseMovable(obj, "reorder_child") end) + end + end + substitute:AddOption("Switch with another (select two parts with bulk select)", function() pace.SwapBaseMovables(pace.BulkSelectList[1], pace.BulkSelectList[2], false) end) + substitute:AddOption("Recast into new class (warning!)", function() pace.SubstituteBaseMovable(obj, "cast") end) + end + end + +end + +function pace.addPartGroupMenuComponent(menu, obj, group_name) + +end + +--destructive tool +function pace.UltraCleanup(obj) + if not obj then return end + + local root = obj:GetRootPart() + local safe_parts = {} + local parts_have_saved_parts = {} + local marked_for_deletion = {} + + local function IsImportantMarked(part) + if not IsValid(part) then return false end + if part.Notes == "important" then return true end + return false + end + + local function FoundImportantMarkedParent(part) + if not IsValid(part) then return false end + if IsImportantMarked(part) then return true end + local root = part:GetRootPart() + local parent = part + while parent ~= root do + if parent.Notes and parent.Notes == "important" then return true end + parent = parent:GetParent() + end + return false + end + + local function Important(part) + if not IsValid(part) then return false end + return IsImportantMarked(part) or FoundImportantMarkedParent(part) + end + + local function CheckPartWithLinkedParts(part) + local found_parts = false + local part_roles = { + ["AimPart"] = nil, --base_movable + ["OutfitPart"] = nil, --projectile bullets + ["EndPoint"] = nil --beams + } + + if part.ClassName == "projectile" then + if part.OutfitPart then + if part.OutfitPart:IsValid() then + part_roles["OutfitPart"] = part.OutfitPart + found_parts = true + end + end + end + + if part.AimPart then + if part.AimPart:IsValid() then + part_roles["AimPart"] = part.AimPart + found_parts = true + end + end + + if part.ClassName == "beam" then + if part.EndPoint then + if part.EndPoint:IsValid() then + part_roles["EndPoint"] = part.EndPoint + found_parts = true + end + end + end + + parts_have_saved_parts[part] = found_parts + if found_parts then + safe_parts[part] = part + for i2,v2 in pairs(part_roles) do + if v2 then + safe_parts[v2] = v2 + end + end + end + end + + local function IsSafe(part) + local safe = true + + if part.Notes then + if #(part.Notes) > 20 then return true end --assume if we write 20 characters in the notes then it's fine to keep it... + end + + if part.ClassName == "event" or part.ClassName == "proxy" or part.ClassName == "command" then + return false + end + + if not part:IsHidden() and not part.Hide then + else + safe = false + if string.find(part.ClassName,"material") then + safe = true + end + end + + return safe + end + + local function IsMildlyRisky(part) + if part.ClassName == "event" or part.ClassName == "proxy" or part.ClassName == "command" then + if not part:IsHidden() and not part.Hide then + return true + end + return false + end + return false + end + + local function IsHangingPart(part) + if IsImportantMarked(part) then return false end + local c = part.ClassName + + --unlooped sounds or 0 volume should be wiped + if c == "sound" or c == "ogg" or c == "webaudio" or c == "sound2" then + if part.Volume == 0 then return true end + if part.Loop ~= nil then + if not part.Loop then return true end + end + if part.PlayCount then + if part.PlayCount == 0 then return true end + end + end + + --fireonce particles should be wiped + if c == "particle" then + if part.NumberParticles == 0 or part.FireOnce then return true end + end + + --0 weight flexes have to be removed + if c == "flex" then + if part.Weight < 0.1 then return true end + end + + if c == "sunbeams" then + if math.abs(part.Multiplier) == 0 then return true end + end + + --other parts to leave forever + if c == "shake" or c == "gesture" then + return true + end + end + + local function FindNearestSafeParent(part) + if not part then return end + local root = part:GetRootPart() + local parent = part:GetParent() + local child = part + local i = 0 + while parent ~= root do + if i > 10 then return parent end + i = i + 1 + if IsSafe(parent) then + return parent + elseif not IsMildlyRisky(parent) then + return parent + elseif not parent:IsHidden() and parent.Hide then + return parent + end + child = parent + parent = parent:GetParent() + end + return parent + end + + + local function SafeRemove(part) + if IsValid(part) then + if IsSafe(part) or Important(part) then + return + elseif IsMildlyRisky(part) then + if table.Count(part:GetChildren()) == 0 then + part:Remove() + end + end + end + end + + --does algorithm needs to be recursive? + --delete absolute unsafes: hiddens. + --now there are safe events. + --extract children into nearest safe parent BUT ONLY DO IT VIA ITS DOMINANT and IF IT'S SAFE + --delete remaining unsafes: events, hiddens, commands ... + --but we still need to check for children to extract! + + local function Move_contents_up(part) --this will be the powerhouse recursor + local parent = FindNearestSafeParent(part) + --print(part, "nearest parent is", parent) + for _,child in pairs(part:GetChildren()) do + if child:IsHidden() or child.Hide then --hidden = delete + marked_for_deletion[child] = child + else --visible = possible container = check + if table.Count(child:GetChildren()) == 0 then --dead end = immediate action + if IsSafe(child) then --safe = keep but now extract it + child:SetParent(parent) + --print(child, "moved to", parent) + safe_parts[child] = child + elseif child:IsHidden() or child.Hide then --hidden = delete + marked_for_deletion[child] = child + end + else --parent = process the children? done by the recursion + --the parent still needs to be moved up + child:SetParent(parent) + + safe_parts[child] = child + Move_contents_up(child) --recurse + end + + end + + end + end + + --find parts to delete + --first pass: absolute unsafes: hidden parts + for i,v in pairs(root:GetChildrenList()) do + if v:IsHidden() or v.Hide then + if not FoundImportantMarkedParent(part) then + v:Remove() + end + + end + end + + --second pass: + --A: mark safe parts + --B: extract children in remaining unsafes (i.e. break the chain of an event) + for i,v in pairs(root:GetChildrenList()) do + if IsSafe(v) then + safe_parts[v] = v + CheckPartWithLinkedParts(v) + if IsMildlyRisky(v:GetParent()) then + v:SetParent(v:GetParent():GetParent()) + end + elseif IsMildlyRisky(v) then + Move_contents_up(v) + marked_for_deletion[v] = v + end + + end + --after that, the remaining events etc are marked + for i,v in pairs(root:GetChildrenList()) do + if IsMildlyRisky(v) then + marked_for_deletion[v] = v + end + end + + pace.RefreshTree() + --go through delete tables except when marked as important or those protected by these + for i,v in pairs(marked_for_deletion) do + + local delete = false + + if not safe_parts[v] then + + if v:IsValid() then + delete = true + end + if FoundImportantMarkedParent(v) then + delete = false + end + end + + if delete then SafeRemove(v) end + end + + --third pass: cleanup the last remaining unwanted parts + for i,v in pairs(root:GetChildrenList()) do + --remove remaining events after their children have been freed, and delete parts that don't have durable use, like sounds that aren't looping + if IsMildlyRisky(v) or IsHangingPart(v) then + if not Important(v) then + v:Remove() + end + end + end + + --fourth pass: delete bare containing nothing left + for i,v in pairs(root:GetChildrenList()) do + if v.ClassName == "group" then + local bare = true + for i2,v2 in pairs(v:GetChildrenList()) do + if v2.ClassName ~= "group" then + bare = false + end + end + if bare then v:Remove() end + end + end + pace.RefreshTree() + +end + +do --hover highlight halo pac.haloex = include("pac3/libraries/haloex.lua") + local hover_halo_limit + local warn = false + local post_warn_next_allowed_check_time = 0 + local post_warn_next_allowed_warn_time = 0 + local last_culprit_UID = 0 + local last_checked_partUID = 0 + local last_tbl = {} + local last_bulk_select_tbl = nil + local last_root_ent = {} + local last_time_checked = 0 + function pace.OnHoverPart(self) + local skip = false + if GetConVar("pac_hover_color"):GetString() == "none" then return end + hover_halo_limit = GetConVar("pac_hover_halo_limit"):GetInt() + local tbl = {} local ent = self:GetOwner() - - if ent:IsValid() then - table.insert(tbl, ent) + local is_root = ent == self:GetRootPart():GetOwner() + + --decide whether to skip + --it will skip the part-search loop if we already checked the part recently + if self.UniqueID == last_checked_partUID then + skip = true + if is_root and last_root_ent ~= self:GetRootPart():GetOwner() then + table.RemoveByValue(last_tbl, last_root_ent) + table.insert(last_tbl, self:GetRootPart():GetOwner()) + end + tbl = last_tbl end - for _, child in ipairs(self:GetChildrenList()) do - local ent = self:GetOwner() - if ent:IsValid() then + --operations : search the part and look for entity-candidates to halo + if not skip then + --start with entity, which could be part or entity + if (is_root and ent:IsValid()) then table.insert(tbl, ent) + else + if not ((self.ClassName == "group" or self.ClassName == "jiggle") or (self.Hide == true) or (self.Size == 0) or (self.Alpha == 0)) then + table.insert(tbl, ent) + end + end + + --get the children if any + if self:HasChildren() then + for _,v in ipairs(self:GetChildrenList()) do + local can_add = false + local ent = v:GetOwner() + + --we're not gonna add parts that don't have a specific reason to be haloed or that don't at least group up some haloable models + --because the table.insert function has a processing load on the memory, and so is halo-drawing + if (v.ClassName == "model" or v.ClassName == "model2" or v.ClassName == "jiggle") then + can_add = true + else can_add = false end + if (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then + can_add = false + end + if can_add then table.insert(tbl, ent) end + end + end + end + + last_tbl = tbl + last_root_ent = self:GetRootPart():GetOwner() + last_checked_partUID = self.UniqueID + + DrawHaloHighlight(tbl) + + --also refresh the bulk-selected nodes' labels because pace.RefreshTree() resets their alphas, but I want to keep the fade because it indicates what's being bulk-selected + if not skip then timer.Simple(0.3, function() BulkSelectRefreshFadedNodes(self) end) end + end + + function BulkSelectRefreshFadedNodes(part_trace) + if refresh_halo_hook then return end + if part_trace then + for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do + if IsValid(v.pace_tree_node) then + v.pace_tree_node:SetAlpha( 255 ) + end + end end - if #tbl > 0 then - local pulse = math.sin(pac.RealTime * 20) * 0.5 + 0.5 - pulse = pulse * 255 - pac.haloex.Add(tbl, Color(pulse, pulse, pulse, 255), 1, 1, 1, true, true, 5, 1, 1) + for _,v in ipairs(pace.BulkSelectList) do + if not v:IsValid() then table.RemoveByValue(pace.BulkSelectList, v) + elseif v.pace_tree_node then + v.pace_tree_node:SetAlpha( 150 ) + end end end + + function RebuildBulkHighlight() + local parts_tbl = {} + local ents_tbl = {} + local hover_tbl = {} + local ent = {} + + --get potential entities and part-children from each parent in the bulk list + for _,v in pairs(pace.BulkSelectList) do --this will get parts + + if (v == v:GetRootPart()) then --if this is the root part, send the entity + table.insert(ents_tbl,v:GetRootPart():GetOwner()) + table.insert(parts_tbl,v) + else + table.insert(parts_tbl,v) + end + + for _,child in ipairs(v:GetChildrenList()) do --now do its children + table.insert(parts_tbl,child) + end + end + + --check what parts are candidates we can give to halo + for _,v in ipairs(parts_tbl) do + local can_add = false + if (v.ClassName == "model" or v.ClassName == "model2") then + can_add = true + end + if (v.ClassName == "group") or (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then + can_add = false + end + if can_add then + table.insert(hover_tbl, v:GetOwner()) + end + end + + table.Add(hover_tbl,ents_tbl) + --TestPrintTable(hover_tbl, "hover_tbl") + + last_bulk_select_tbl = hover_tbl + end + + function TestPrintTable(tbl, tbl_name) + MsgC(Color(200,255,200), "TABLE CONTENTS:" .. tbl_name .. " = {\n") + for _,v in pairs(tbl) do + MsgC(Color(200,255,200), "\t", tostring(v), ", \n") + end + MsgC(Color(200,255,200), "}\n") + end + + function ThinkBulkHighlight() + if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + return + end + DrawHaloHighlight(last_bulk_select_tbl) + end + + function DrawHaloHighlight(tbl) + if (type(tbl) ~= "table") then return end + if not pace.Active then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + end + + --Find out the color and apply the halo + local color_string = GetConVar("pac_hover_color"):GetString() + local pulse_rate = math.min(math.abs(GetConVar("pac_hover_pulserate"):GetFloat()), 100) + local pulse = math.sin(SysTime() * pulse_rate) * 0.5 + 0.5 + if pulse_rate == 0 then pulse = 1 end + local pulseamount + + local halo_color = Color(255,255,255) + + if color_string == "rave" then + halo_color = Color(255*((0.33 + SysTime() * pulse_rate/20)%1), 255*((0.66 + SysTime() * pulse_rate/20)%1), 255*((SysTime() * pulse_rate/20)%1), 255) + pulseamount = 8 + elseif color_string == "funky" then + halo_color = Color(255*((0.33 + SysTime() * pulse_rate/10)%1), 255*((0.2 + SysTime() * pulse_rate/15)%1), 255*((SysTime() * pulse_rate/15)%1), 255) + pulseamount = 5 + elseif color_string == "ocean" then + halo_color = Color(0, 80 + 30*(pulse), 200 + 50*(pulse) * 0.5 + 0.5, 255) + pulseamount = 4 + elseif color_string == "rainbow" then + --halo_color = Color(255*(0.5 + 0.5*math.sin(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*-math.cos(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*math.sin(1 + pac.RealTime * pulse_rate/20)), 255) + halo_color = HSVToColor(SysTime() * 360 * pulse_rate/20, 1, 1) + pulseamount = 4 + elseif #string.Split(color_string, " ") == 3 then + halo_color_tbl = string.Split( color_string, " " ) + for i,v in ipairs(halo_color_tbl) do + if not isnumber(tonumber(halo_color_tbl[i])) then halo_color_tbl[i] = 0 end + end + halo_color = Color(pulse*halo_color_tbl[1],pulse*halo_color_tbl[2],pulse*halo_color_tbl[3],255) + pulseamount = 4 + else + halo_color = Color(255,255,255,255) + pulseamount = 2 + end + --print("using", halo_color, "blurs=" .. 2, "amount=" .. pulseamount) + + pac.haloex.Add(tbl, halo_color, 2, 2, pulseamount, true, true, pulseamount, 1, 1) + --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) + end end + +--custom info panel +--[[args +tbl = { + obj = part.Label, --the associated object, could be a tree label, mouse, part etc. + pac_part = part --a pac part reference, if applicable + obj_type = "pac tree label", + hoverfunc = function() end, + doclickfunc = function() end, + panel_exp_width = 300, panel_exp_height = 200 +} + +]] + diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua new file mode 100644 index 000000000..6d51bb62d --- /dev/null +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -0,0 +1,1173 @@ +--[[ + This is the framework for popups. This should be expandable for various use cases. + It uses DFrame as a base, overrides the Paint function for a basic fade effect. + + Tutorials will be written here +]] + + +CreateConVar("pac_popups_enable", 1, FCVAR_ARCHIVE, "Enables PAC editor popups. They provide some information but can be annoying") +CreateConVar("pac_popups_preserve_on_autofade", 1, FCVAR_ARCHIVE, "If set to 0, PAC editor popups appear only once and don't reappear when hovering over the part label or pressing F1") +CreateConVar("pac_popups_base_color", "215 230 255", FCVAR_ARCHIVE, "The color of the base filler rectangle for editor popups") +CreateConVar("pac_popups_base_color_pulse", "0", FCVAR_ARCHIVE, "Amount of pulse of the base filler rectangle for editor popups") + +CreateConVar("pac_popups_base_alpha", "0.5", FCVAR_ARCHIVE, "The alpha opacity of the base filler rectangle for editor popups") +CreateConVar("pac_popups_fade_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") +CreateConVar("pac_popups_fade_alpha", "1", FCVAR_ARCHIVE, "The alpha opacity of the fading effect for editor popups") +CreateConVar("pac_popups_text_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") +CreateConVar("pac_popups_verbosity", "beginner tutorial", FCVAR_ARCHIVE, "Sets the amount of information added to PAC editor popups. While in development, there will be limited contextual support. If no special information is defined, it will indicate the part size information. Here are the planned modes: \nbeginner tutorial : Basic tutorials about pac parts, for beginners or casual users looking for a quick reference for what a part does\nReference tutorial : doesn't give part tutorials, but still keeps events' tutorial explanations.\n") +CreateConVar("pac_popups_preferred_location", "pac tree label", FCVAR_ARCHIVE, "Sets the preferred method of PAC editor popups.\n".. + "pac tree label : the part label on the pac tree\n".. + "part world : if part is base_movable, place it next to the part in the viewport\n".. + "screen : static x,y on screen no matter what. That would be at the center\n".. + "cursor : right on the cursor\n".. + "editor bar : next to the toolbar") + + +function pace.OpenPopupConfig() + local master_pnl = vgui.Create("DFrame") + master_pnl:SetTitle("Configure PAC3 popups appearance") + master_pnl:SetSize(400,800) + master_pnl:Center() + + local list_pnl = vgui.Create("DListLayout", master_pnl) + list_pnl:Dock(FILL) + + local basecolor = vgui.Create("DColorMixer") + basecolor:SetSize(400,150) + local col_args = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + basecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + function basecolor:ValueChanged(col) + GetConVar("pac_popups_base_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_popups_base_alpha"):SetString(col.a) + end + local basecolor_pulse = vgui.Create("DNumSlider") + basecolor_pulse:SetMax(255) + basecolor_pulse:SetMin(0) + + if isnumber(GetConVar("pac_popups_base_color_pulse"):GetInt()) then + basecolor_pulse:SetValue(GetConVar("pac_popups_base_color_pulse"):GetInt()) + else + basecolor_pulse:SetValue(0) + end + + basecolor_pulse:SetText("base pulse") + function basecolor_pulse:OnValueChanged(val) + val = math.Round(tonumber(val),0) + GetConVar("pac_popups_base_color_pulse"):SetInt(val) + end + + local fadecolor = vgui.Create("DColorMixer") + fadecolor:SetSize(400,150) + col_args = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + fadecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + function fadecolor:ValueChanged(col) + GetConVar("pac_popups_fade_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_popups_fade_alpha"):SetString(col.a) + end + + local textcolor = vgui.Create("DColorMixer") + textcolor:SetSize(400,150) + col_args = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + + if isnumber(col_args[1]) then + textcolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + end + + textcolor:SetAlphaBar(false) + function textcolor:ValueChanged(col) + GetConVar("pac_popups_text_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + end + + local invertcolor_btn = vgui.Create("DButton") + invertcolor_btn:SetSize(400,30) + invertcolor_btn:SetText("Use text invert color (experimental)") + function invertcolor_btn:DoClick() + GetConVar("pac_popups_text_color"):SetString("invert") + end + + local preview_pnl = vgui.Create("DLabel") + preview_pnl:SetSize(400,170) + preview_pnl:SetText("") + local label_text = "Popup preview! The text will look like this." + + local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + local a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + + local preview_refresh_btn = vgui.Create("DButton") + preview_refresh_btn:SetSize(400,30) + preview_refresh_btn:SetText("Refresh") + local oldpaintfunc = master_pnl.Paint + local invis_frame = false + function preview_refresh_btn:DoClick() + invis_frame = not invis_frame + if invis_frame then master_pnl.Paint = nil else master_pnl.Paint = oldpaintfunc end + rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + end + + function preview_pnl:Paint( w, h ) + --base layer + local sine = 0.5 + 0.5*math.sin(CurTime()*2) + draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) ) + for band=0,w,1 do + --per-pixel fade + fade = 1 - (1/w * band * 1) + fade = math.pow(fade,2) + draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) + end + draw.DrawText(label_text, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,255)) + end + + list_pnl:Add(Label("Base color")) + list_pnl:Add(basecolor) + list_pnl:Add(basecolor_pulse) + list_pnl:Add(Label("Gradient color")) + list_pnl:Add(fadecolor) + list_pnl:Add(Label("Text color")) + list_pnl:Add(textcolor) + list_pnl:Add(invertcolor_btn) + list_pnl:Add(preview_refresh_btn) + list_pnl:Add(preview_pnl) + master_pnl:MakePopup() + + +end + +concommand.Add("pac_popups_settings", function() pace.OpenPopupConfig() end) + +--[[ + info_string, main string + { info about where to position the label + pac_part = part, that would be the pac part if applicable + obj = self.Label, that would be the positioning target + obj_type = "pac tree label", what type of thing is the target, for positioning + pac tree label = on the editor, needs to realign when scrolling + part world = if base_movable, place it next to the part in the view, if not, owner entity + screen = static x,y on screen no matter what, needs the further x,y args specified outside + cursor = right on the cursor + editor bar = next to the toolbar + hoverfunc = function() end, a function to run when hovering. + doclickfunc = function() end, a function to run when clicking + panel_exp_width = 900, panel_exp_height = 200 prescribed dimensions to expand to + }, + self:LocalToScreen() x,y +]] + + + +--[[ +we generally have two routes to create a popup: part and direct +at the part level we can tell pac to try to create a popup +pac.AttachInfoPopupToPart(part : obj, string : str, table : tbl) --naming scheme close to a general pac function + PART:AttachEditorPopup(string : str, bool : flash, table : tbl) --calls the generic base setup in base_part, shouldn't be overridden + PART:SetupEditorPopup(str, force_open, tbl) --calls the specific setup, can be overridden for different classes + pac.InfoPopup(str, tbl, x, y) --creates the vgui element + +we can directly create an independent editor popup +pac.InfoPopup(str, tbl, x, y) +]] + + +function pac.InfoPopup(str, tbl, x, y) + if not GetConVar("pac_popups_enable"):GetBool() then return end + local x = x + local y = y + if not x or not y then + x = ScrW()/2 + math.Rand(-300,300) + y = ScrH()/2 + math.Rand(-300,0) + end + tbl = tbl or {} + if not tbl.obj then + if tbl.obj_type == "pac tree label" then + tbl.obj = tbl.pac_part.pace_tree_node + elseif tbl.obj_type == "part world" then + tbl.obj = tbl.pac_part + end + end + + str = str or "" + local verbosity = GetConVar("pac_popups_verbosity"):GetString() + + local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + local a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + + local pnl = vgui.Create("DFrame") + local txt_zone = vgui.Create("RichText", pnl) + + --function pnl:PerformLayout() end + pnl:SetTitle("") pnl:SetText("") pnl:ShowCloseButton( false ) + txt_zone:SetPos(5,25) + txt_zone:SetContentAlignment( 7 ) --top left + + if tbl.pac_part then + if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + if pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName] then + str = str .. "\n\n" .. pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName].popup_tutorial .. "\n" + end + end + end + + + pnl.hoverfunc = function() end + pnl.doclickfunc = function() end + pnl.titletext = "Click for more information! (or F1)" + pnl.alternativetitle = "Right click / Alt+P to kill popups. \"pac_popups_preserve_on_autofade\" is set to " .. GetConVar("pac_popups_preserve_on_autofade"):GetInt() .. ", " .. (GetConVar("pac_popups_preserve_on_autofade"):GetBool() and "If it fades away, the popup is allowed to reappear on hover or F1" or "If it fades away, the popup will not reappear") + + --pnl:SetPos(ScrW()/2 + math.Rand(-100,100), ScrH()/2 + math.Rand(-100,100)) + + function pnl:FixPartReference(tbl) + if not tbl or table.IsEmpty(tbl) then self:Remove() end + if tbl.pac_part then tbl.obj = tbl.pac_part.pace_tree_node end + + end + + function pnl:MoveToObj(tbl) + --self:MakePopup() + if tbl.obj_type == "pac tree label" then + if not IsValid(tbl.obj) then + self:FixPartReference(tbl) + self:SetPos(x,y) + else + local x,y = tbl.obj:LocalToScreen() + x = pace.Editor:GetWide() + --print(pace.Editor:GetWide(), input.GetCursorPos()) + self:SetPos(x,y) + end + if pace then + if pace.Editor then + if pace.Editor.IsLeft then + if not pace.Editor:IsLeft() then + self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) + else + self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) + end + end + end + end + + elseif tbl.obj_type == "part world" then + if tbl.pac_part then + local global_position = tbl.pac_part:GetRootPart():GetOwner():GetPos() + tbl.pac_part:GetRootPart():GetOwner():OBBCenter()*1.5 + if tbl.pac_part.GetWorldPosition then + global_position = tbl.pac_part:GetWorldPosition() --if part is a base_movable, we'll get its position right away + elseif tbl.pac_part:GetParent().GetWorldPosition then + global_position = tbl.pac_part:GetParent():GetWorldPosition() --if part isn't but has a base_movable parent, get that + end + local scr_tbl = global_position:ToScreen() + self:SetPos(scr_tbl.x, scr_tbl.y) + end + + elseif tbl.obj_type == "screen" then + self:SetPos(x,y) + + elseif tbl.obj_type == "cursor" then + self:SetPos(input.GetCursorPos()) + + elseif tbl.obj_type == "editor bar" then + if not pace.Editor:IsLeft() then + self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) + else + self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) + end + end + + end + + + if tbl then + pnl.tbl = tbl + pnl:MoveToObj(tbl) + if tbl.hoverfunc then + if tbl.hoverfunc == "open" then + pnl.hoverfunc = function() + pnl.hovering = true + pnl:keep_alive(3) + if not pnl.hovering and not pnl.expand then + pnl.resizing = true + pnl.expand = true + + pnl.ResizeStartTime = CurTime() + pnl.ResizeEndTime = CurTime() + 0.3 + end + end + else + pnl.hoverfunc = tbl.hoverfunc + end + end + pnl.exp_height = tbl.panel_exp_height or 400 + pnl.exp_width = tbl.panel_exp_width or 800 + end + + pnl.exp_height = pnl.exp_height or 400 + pnl.exp_width = pnl.exp_width or 800 + pnl:SetSize(200,20) + + pnl.DeathTimeAdd = 0 + if GetConVar("pac_popups_preserve_on_autofade"):GetBool() then + pnl.DeathTimeAdd = 240 + end + pnl.DeathTime = CurTime() + 13 + pnl.FadeTime = CurTime() + 10 + pnl.FadeDuration = pnl.DeathTime - pnl.FadeTime + pnl.ResizeEndTime = 0 + pnl.ResizeStartTime = 0 + pnl.resizing = false + + function pnl:keep_alive(extra_time) + pnl.DeathTime = math.max(pnl.DeathTime, CurTime() + extra_time + pnl.FadeDuration) + pnl.FadeTime = math.max(pnl.FadeTime, CurTime() + extra_time) + pnl:SetAlpha(255) + end + + --the header needs a label to click on to open the popup + function pnl:DoClick() + + if input.IsKeyDown(KEY_F1) or (self:IsHovered() and not txt_zone:IsHovered()) then + pnl.expand = not pnl.expand + pnl.ResizeStartTime = CurTime() + pnl.ResizeEndTime = CurTime() + 0.3 + pnl.resizing = true + end + + pnl:keep_alive(3) + pnl.doclickfunc() + end + + --handle positioning, expanding and termination + function pnl:Think() + self:MoveToObj(tbl) + if input.IsButtonDown(KEY_P) and input.IsButtonDown(KEY_LALT) then --auto-kill if alt-p + tbl.pac_part.killpopup = true + self:Remove() + end + + if input.IsMouseDown(MOUSE_RIGHT) then + if self:IsHovered() and not txt_zone:IsHovered() then + self:Remove() + end + end + + self.F1_doclick_possible_at = self.F1_doclick_possible_at or 0 + self.mouse_doclick_possible_at = self.mouse_doclick_possible_at or 0 + + if input.IsButtonDown(KEY_F1) then --expand if press F1, but only after a delay + if self.F1_doclick_possible_at == 0 then + self.F1_doclick_possible_at = CurTime() + 0.3 + end + if CurTime() > self.F1_doclick_possible_at then + self.F1_doclick_possible_at = 0 + self:DoClick() + end + end + if input.IsMouseDown(MOUSE_LEFT) and self:IsHovered() or self:IsChildHovered() then --expand if press mouse left + if self.mouse_doclick_possible_at == 0 then + self.mouse_doclick_possible_at = CurTime() + 1 + end + if CurTime() > self.mouse_doclick_possible_at then + self.mouse_doclick_possible_at = 0 + self:DoClick() + end + end + if not input.IsMouseDown(MOUSE_LEFT) then + self.mouse_doclick_possible_at = CurTime() + end + + if not IsValid(tbl.pac_part) and tbl.pac_part ~= false then self:Remove() end + self.exp_width = self.exp_width or 800 + self.exp_height = self.exp_height or 500 + --resizing code, initially the label should start small + if self.resizing then + local expand_frac_w = math.Clamp((self.ResizeEndTime - CurTime()) / 0.3,0,1) + local expand_frac_h = math.Clamp((self.ResizeEndTime - (CurTime() - 0.5)) / 0.5,0,1) + local width,height + if not self.expand then + width = 200 + (self.exp_width - 200)*(expand_frac_w) + height = 20 + (self.exp_height - 20)*(expand_frac_h) + if self.hovering and not self:IsHovered() then self.hovering = false end + else + width = 200 + (self.exp_width - 200)*(1 - expand_frac_h) + height = 20 + (self.exp_height - 20)*(1 - expand_frac_w) + + end + self:SetSize(width,height) + txt_zone:SetSize(width-10,height-30) + end + + + self.fade_factor = math.Clamp((self.DeathTime - CurTime()) / self.FadeDuration,0,1) + self.fade_factor = math.pow(self.fade_factor, 3) + + if CurTime() > self.DeathTime + self.DeathTimeAdd then + self:Remove() + end + if pace.Focused then self:SetAlpha(255*self.fade_factor) end + if self:IsHovered() then + self:keep_alive(1) + self.hoverfunc() + if input.IsMouseDown(MOUSE_RIGHT) then + if tbl.pac_part then + tbl.pac_part.killpopup = true + end + self:Remove() + end + + end + + if not pace.Focused then + self:AlphaTo(0, 0.1, 0) + self:KillFocus() + self:SetMouseInputEnabled(false) + self:SetKeyBoardInputEnabled(false) + gui.EnableScreenClicker(false) + end + + function pnl:OnRemove() + if not GetConVar("pac_popups_preserve_on_autofade"):GetBool() then + tbl.pac_part.killpopup = true + end + end + end + + pnl.doclickfunc = tbl.doclickfunc or function() end + + + + pnl.exp_height = tbl.panel_exp_height + pnl.exp_width = tbl.panel_exp_width + + --cast the convars values + r1 = tonumber(r1) + g1 = tonumber(g1) + b1 = tonumber(b1) + a1 = tonumber(a1) + r2 = tonumber(r2) + g2 = tonumber(g2) + b2 = tonumber(b2) + a2 = tonumber(a2) + r3 = tonumber(r3) + g3 = tonumber(g3) + b3 = tonumber(b3) + + local col = Color(r3,g3,b3,255) + + --txt_zone:SetFont("DermaDefaultBold") + function txt_zone:PerformLayout() + txt_zone:SetBGColor(0,0,0,0) + txt_zone:SetFGColor(col) + end + + function txt_zone:Think() + if self:IsHovered() then + pnl:keep_alive(3) + end + end + + txt_zone:SetText("") + txt_zone:AppendText(str) + + txt_zone:SetVerticalScrollbarEnabled(true) + + function pnl:Paint( w, h ) + + + self.fade_factor = self.fade_factor or 1 + --base layer + local sine = 0.5 + 0.5*math.sin(CurTime()*2) + draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) ) + --draw.RoundedBox( 0, 0, 0, 1, h, Color( 88, 179, 255, 255)) + for band=0,w,1 do + --per-pixel fade + fade = 1 - (1/w * band * self.fade_factor) + fade2 = math.pow(fade,3) + fade = math.pow(fade,2) + --draw.RoundedBox( c, x, y, w, h, color ) + draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) + --draw.RoundedBox( 0, band, 0, 1, 1, Color( 88, 179, 255, 255)) + --draw.RoundedBox( 0, band, h-1, 1, 1, Color( 0, 0, 0, 255)) + end + + if self.expand then + draw.DrawText(self.alternativetitle, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) + else + draw.DrawText(self.titletext, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) + end + end + + pnl:MakePopup() + return pnl +end + +function pac.AttachInfoPopupToPart(obj, str, tbl) + if not obj then return end + obj:AttachEditorPopup(str, true, tbl) +end + +function pace.FlushInfoPopups() + for _,part in pairs(pac.GetLocalParts()) do + local node = part.pace_tree_node + if not node or not node:IsValid() then continue end + if node.popupinfopnl then + node.popupinfopnl:Remove() + node.popupinfopnl = nil + end + end + +end + +--[[ + part classes info + +ideally we should have: +1-a tooltip form (7 words max) + e.g. projectile: throws missiles into the world +2-a fuller form for the popups (4-5 sentences or more if needed) + e.g. projectile: the projectile part creates physical entities and launches them forward.\n + the entity has physics but it can be clientside (visual) or serverside (physical)\n + by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n + the entity can do damage but servers can restrict that. + +but then again we should probably look for better ways for the full-length explanations, + maybe grab some of them from the wiki or have a web browser for the wiki +]] + +do + + pace.TUTORIALS = pace.TUTORIALS or {} + pace.TUTORIALS.PartInfos = { + + ["trail"] = { + tooltip = "leaves a trail behind", + popup_tutorial = + "the trail part creates beams along its path to make a trail\n".. + "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. + "you can set how it looks, how big it becomes etc." + }, + + ["trail2"] = { + tooltip = "leaves a trail behind", + popup_tutorial = + "the trail part creates beams along its path to make a trail\n".. + "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. + "you can set how it looks, how big it becomes etc." + }, + + ["sound"] = { + tooltip = "plays sounds", + popup_tutorial = "plays sounds in wav, mp3, ogg formats.\n".. + "for random sounds, paste each path separated by semicolons e.g. sound1.wav;sound3.wav;sound8.wav\n".. + "we have a special bracket notation for sound lists: sound[1,50].wav\n\n".. + "some of the parameters to know:\n".. + "sound level affects the falloff along with volume; a good starting point is 70 level, 0.6 volume\n".. + "overlapping means it doesn't get cut off if hidden\n".. + "sequential plays sounds in a list in order once you have the semicolon or bracket notation;\n".. + "\tthe steps is how much you progress by each activation. it can go one by one (1), every other sound (2+), stay there (0) or go back (negative values)" + }, + + ["sound2"] = { + tooltip = "plays web sounds", + popup_tutorial = "plays sounds in wav, mp3, ogg formats, with the option to download sound files from the internet\n".. + "people usually use dropbox, google drive, other cloud hosts or their own server host to store and distribute their files. each has its limitations.\n\n".. + "WARNING! Downloading and using these sounds is only possible in the chromium branch of garry's mod!\n\n".. + "to randomize sounds, we still have the same notations as legacy sound:\n".. + "\tsemicolon notation e.g. path1.wav;https://url1.wav;https://url2.wav\n".. + "\tbracket notation e.g. sound[1,50].wav\n\n".. + "some of the parameters to know, you'll already know some of them from legacy sound:\n".. + "-radius affects the falloff distance\n".. + "-overlapping means it doesn't get cut off if hidden\n".. + "-sequential plays sounds in a list in order" + }, + + ["ogg"] = { + tooltip = "plays ogg sounds (broken)", + popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." + }, + + ["webaudio"] = { + tooltip = "plays web sounds (legacy)", + popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." + }, + + + ["halo"] = { + tooltip = "makes models glow", + popup_tutorial = + "This part creates a halo around a model entity.\n".. + "That could be your playermodel or a pac3 model, but for some reason it doesn't work on your player if you have an entity part.\n".. + "passes is the thickness of the halo, amount is the brightness, blur x and y spread out the shape" + }, + + ["bodygroup"] = { + tooltip = "changes body parts on supported models", + popup_tutorial = + "Bodygroups are a Source engine model feature which allows to easily show or hide different pieces of a model\n".. + "those are often used for accessories and styles. But it won't work unless your model has bodygroups.\n".. + "this part does exactly that. but you might do that directly with the model part or entity part" + }, + + ["holdtype"] = { + tooltip = "changes your animation set", + popup_tutorial = + "this part allows you to change animations played in individual movement slots, so you can mix and match from the available animations in your playermodel\n".. + "a holdtype is a set of animations for holding one kind of weapon, such as one-handed pistols vs two-handed revolvers, rifles, melee weapons etc.\n".. + "The option is also in the normal animation part, but this part goes in more detail in choosing different animations" + }, + + ["clip"] = { + tooltip = "cuts a model in a plane (legacy)", + popup_tutorial = + "This part cuts off one side of the model in rendering.\n".. + "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." + }, + + ["clip2"] = { + tooltip = "cuts a model in a plane", + popup_tutorial = + "This part cuts off one side of the model in rendering.\n".. + "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." + }, + + ["model"] = { + tooltip = "places a model (legacy)", + popup_tutorial = "The old model part still does the basic things you need a model to do" + }, + + ["model2"] = { + tooltip = "places a model", + popup_tutorial = + "The model part creates a clientside entity to draw a model locally.\n".. + "Being a base_movable, parts inside it will be physically arented to it.\n".. + "therefore, it can act as a regrouper, rail or anchoring point for your pac structuring needs, although you probably shouldn't abuse it.\n".. + "It can accept most modifiers and play animations, if present or referenced in the model.\n\n".. + "It can load MDL zips or OBJ files from a direct link to a server host or cloud provider, allowing you to use pretty much any model as long as it's the right format for Source. And on that subject, you would do well to install Crowbar, as well as Blender with Blender Source Tools, if you want to extract and edit models. Consult the valve developer community wiki for more information about QC; I view this as common knowledge rather than the purview of pac3 so you have to do some research." + }, + + ["material"] = { + tooltip = "defines a material (legacy)", + popup_tutorial = + "the old material still works as it says. it lets you define some VMT parameters for a material" + }, + + ["material_3d"] = { + tooltip = "defines a material for models", + popup_tutorial = + "This part creates a VMT material of the shader type VertexLitGeneric.\n".. + "If you have experience in Source engine things, you probably should know what some of these do, I won't expound fully but here's the essential summary anyway:\n\n".. + "\tbase texture is the base image. It's basically just color pixels.\n".. + "\tbump map / normal map is a relief that gives a texture on the surface. It uses a distinctly purple pixel format; it's not color but directional information\n".. + "\tdetail is a second base image added on top to modify the pixels. It's usually grayscale because we don't need to add color to give more grit to an image\n".. + "\tself illumination and emissive blend are glowing layers. emissive blend is more complex and needs three necessary components before it starts to work properly.\n".. + "\tenvironment map is a layer for pre-baked room reflection, by default env_cubemap tries to get the nearest cubemap but you can choose another texture, although cubemaps are a very specific format\n".. + "\tphong is a layer of dynamic light reflections\n\n".. + "If you want to edit a material, you can load its VMT with a right click on \"load vmt\", then select the right material override\n".. + "Reminder that transparent textures may need additive or some form of translucent setting on the model and on the material.\n\n".. + "For more information, search the Valve developer community site or elsewhere. Many material features are standard, and if you want to push this part to the limit, the extra research will be worth it." + }, + + ["material_2d"] = { + tooltip = "defines a material for sprites", + popup_tutorial = + "This part creates a VMT material of the shader type UnlitGeneric. This is used by particles and sprites.\n".. + "For transparent textures, use additive or vertex alpha/vertex color (for particles and decals). Some VTF or PNG textures have an alpha channel, but many just have a black background meant for additive rendering.\n\n".. + "For more information, search the Valve developer community site" + }, + + ["material_refract"] = { + tooltip = "defines a refracting material", + popup_tutorial = + "This part creates a VMT material of the shader type Refract. As with other material parts, you would find it useful to name the material to use that in multiple models' \"material\" fields\n".. + "In a way, it doesn't work by surface, but by silhouette. But the surface does determine how the refraction occurs. Setting a base texture creates a flat wall behind it that can distort in interesting ways but it'll replace the view behind.\n".. + "The normal section does most of the heavy lifting. This is where the image behind the material gets refracted according to the surface. You can blend between two normal maps in greater detail.\n".. + "Your model needs to be set to \"translucent\" rendering mode for this to work because the shader is in a multi-step rendering process.\n\n".. + "For more information, search the Valve developer community site" + }, + + ["material_eye refract"] = { + tooltip = "defines a refracting eye material", + popup_tutorial = + "This part creates a VMT material of the shader type EyeRefract.\n".. + "It's tricky to use because of how it involves projections and entity eye position, but you can more easily get something working on premade HL2 or other Source games' characters with QC eyes." + }, + + ["submaterial"] = { + tooltip = "applies a material on a submaterial zone", + popup_tutorial = + "Models can be comprised of multiple materials in different areas. This part can replace the material applied to one of these zones.\n".. + "Depending on how the model was made, it might correspond to what you want, or it might not.\n".. + "As usual, as with other model modifiers your expectations should always line up with the quality of the model you're using." + }, + + ["bone"] = { + tooltip = "changes a bone (legacy)", + popup_tutorial = + "The legacy bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." + }, + + ["bone2"] = { + tooltip = "changes a bone (legacy)", + popup_tutorial = + "The legacy experimental bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." + }, + + ["bone3"] = { + tooltip = "changes a bone", + popup_tutorial = + "This part modifies a model's bone. It can move relative to the parent bone, scale, and rotate.\n".. + "Follow part forces the bone to relocate to a base_movable part. Might have issues if you successively follow part multiple related bones. You could try to fix that by changing draw orders of the follow parts and bones." + }, + + ["player_config"] = { + tooltip = "sets your player entity's behaviour", + popup_tutorial = + "This part has access to some of your player's behavior, like whether you will play footsteps, the chat animation etc.\n".. + "Some of these may or may not work as intended..." + }, + + ["light"] = { + tooltip = "lights up the world (legacy)", + popup_tutorial = + "This legacy part still does the basic thing you want from a light, but the new light part is more fully-featured, for the most part.\n".. + "There is one thing it does that the new part doesn't, and that's styles." + }, + + ["light2"] = { + tooltip = "lights up models or the world", + popup_tutorial = + "This part creates a dynamic light that can illuminate models or the world independently.\n".. + "There are some options for the light's falloff shape (inner and outer angles).\n".. + "Its brightness works by magnitude and size, not multiplication. Which means you can still have light at 0 or lower brightness." + }, + + ["event"] = { + tooltip = "activates or deactivates other parts", + popup_tutorial = + "This part hides or shows connected parts when certain conditions are met. We won't describe them in this tutorial, you'll have to read them individually. The essential behaviour remains common accross events.\n\n".. + "Domain, in other words, which parts get affected:\n".. + "\t1-Default: The event will command its direct parent. Because parts can contain other parts, this includes the event itself, and parts beside the event too. While this is not usually a problem, you have to be aware of that.\n".. + "\t2-ACO: Affect Children Only. The event will command parts inside it, not beside, not above. This is the first step to isolate your setup and have clean logic in your pac.\n".. + "\t3-Targeted: The event gets wired to a part directly, including its children of course. This is accessed when you select a part in the \"targeted part\" field, which has an unfortunate name because there's still the old \"target part\" parameter\n\n".. + "Some events, like is_touching, can select an external \"target\" to use as a point to gather information.\n\n".. + "Operators:\n".. + "Operators are just how the event asks the question to determine when to activate or deactivate. Just read the event the same way as it asks the question: is my source equal to the value? can I find this text in my source?\n".. + "\tnumber-related operators: equal, above, below, equal or above, equal or below\n".. + "\tstring-related operators: equal, find, find simple\n".. + "There's still a caveat. If you use the wrong type of operator for your event, it will NOT work. Please trust the editor autopilot when it automatically changes your operator to a good one. Do not change it unless you know what you're doing." + }, + + ["sprite"] = { + tooltip = "draws a 2D texture", + popup_tutorial = + "Sprites are another Source engine thing which are useful for some point effects. Most textures being for model surfaces will look like squares if drawn flat, but sprite and particle textures are made specially for this purpose.\n".. + "They should have a transparent background or black background. The difference is because of rendering modes or blend modes.\n".. + "Additive rendering adds pixels' values. So, bright pixels will be more visible, but dark pixels end up being faded or invisible because their amounts are low." + }, + + ["fog"] = { + tooltip = "colors a model with fog", + popup_tutorial = + "This strange modifier renders a fog-like color over a model. Not in the world, not inside the model, but over its surface.\n".. + "For that reason, you might do well to change rendering-related values like blend mode on the host model's side\n".. + "It requires to be attached to a base_drawable part, keep in mind the start and end values are multiplied by 100 in post for some reason.".. + "start is the distance where the fog starts to appear outside, end is where the fog is thickest." + }, + + ["force"] = { + tooltip = "provides physical force", + popup_tutorial = + "This part tries to tell the server to do a force impulse, or continually request small impulses for a continuous force. It should work for most physics props, some item and ammo entities, players and NPCs. But it may or may not be allowed on the server due to server settings: pac_sv_force.\n\n".. + "There's a base force and an added xyz vector force. You have options to choose how they're applied. Aside from that, the part's area is mainly for detection.\n\n".. + "For the Base force, Radial is from to self to each entity, Locus is from locus to each entity, Local is forward of self\n\n".. + "For the Vector force, Global is on world coordinates, Local is on self's coordinates, Radial is relative to the line from the self or locus toward the entity (Used in orbits/vortex/circular motion with centrifugal force)\n\n".. + "NPCs might have weird movement so don't expect much from pushing them." + }, + + ["faceposer"] = { + tooltip = "Adjusts multiple facial expression slots", + popup_tutorial = + "This part gives access to multiple facial expressions defined by your model's shape keys in one part.\n".. + "The flex multiplier affects the whole model, so you should avoid stacking faceposers if they have different multipliers." + }, + + ["command"] = { + tooltip = "Runs a console command or lua code", + popup_tutorial = "This part attempts to run a command or Lua code on your client. It may or may not work depending on the command and some servers don't allow you to run clientside lua, because of sv_allowcslua 0.\n\n".. + "Some example lua bits:\n".. + "\tif LocalPlayer():Health() > 0 then print(\"I'm alive\") RunConsoleCommand(\"say\", \"I\'m alive\") end\n".. + "\tfor i=0,100,1 do print(\"number\" .. i) end\n".. + "\tfor _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end\n".. + "\tlocal random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)" + + }, + + ["weapon"] = { + tooltip = "configures your weapon entity", + popup_tutorial = "This part is like an entity part, but for weapons. It can change your weapon's position and appearance, for all or one weapon class." + }, + + ["woohoo"] = { + tooltip = "applies a censor square", + popup_tutorial = + "This part draws a pixelated square with what's behind it, with a possible blur filter and adjustable resolution.\n".. + "It requires a lot of resources to set up and needs to refresh in specific circumstances, which is why you can't change its resolution or blur filtering state with proxies." + }, + + ["flex"] = { + tooltip = "Adjusts one facial expression slot", + popup_tutorial = + "This part gives access to one facial expression defined by your model's shape keys." + }, + + ["particles"] = { + tooltip = "Emits particles", + popup_tutorial = + "Throws particles into the world. They are quite configurable, can be flat 3D or 2D sprites, can be stretched with start/end length.\n".. + "To start with, you may want to set zero angle to false and particle angle velocity to (0, 0, 0)\n".. + "You can use a web texture but you might still need to work around material limitations for transparent images\n".. + "They are not PCF effects though. But I think that with a wise choice and layered particles, you can recreate something that looks like an effect." + }, + + ["custom_animation"] = { + tooltip = "sets up an editable bone animation", + popup_tutorial = + "This part creates a custom animation with a separate editor menu. It is not a sequence, but it moves bones on top of your base animations. It morphs between keyframes which correspond to bones' positions and angles. This is what creates movement.\n\n".. + "Custom animation types:\n".. + "\tsequence: loopable. plays the A-pose animation as a base, layers bone movements on top.".. + "\tstance: loopable. layers bone movements on top.".. + "\tgesture: not loopable. layers bone movements on top. ideally you should start with duplicating your initial frame once for smoothly going back to 0.".. + "\tposture: only applies one non-moving frame. this is like a set of bones.\n\n".. + "There are interesting Easing styles available when you select the linear interpolation mode. They're useful in many ways, if you want to have more control over the dynamics and ultimately give character to your animation.\n".. + "While this is not the place to write a full tutorial for how to animate, or explaining animation principles in depth, I editorialize a bit and say those are two I try to aim for:\n".. + "\tinertia: trying to carry some movement over from a previous frame, because real physics take time to decelerate and accelerate between positions.\n".. + "\texaggeration: animations often use unnatural movement dynamics (e.g. different speeds at different times) to make movements look more pleasing by giving it more character. This goes in hand with anticipation." + }, + + ["beam"] = { + tooltip = "draws a rope or beam", + popup_tutorial = + "This part renders a rope or beam between itself and the end point. It can bend relative to the two endpoints' angles.\n".. + "frequency determines how many half-cycles it goes through. 1 is half a cycle (1 bump), 2 is one cycle(2 bumps)\n".. + "resolution is how many segments it tries to draw for that.\n\n".. + "And here's another reminder that while it can load url images, there are limitations so you may have to do something with a material part or blend mode if you want a custom transparent texture." + }, + + ["animation"] = { + tooltip = "plays a sequence animation", + popup_tutorial = + "This part plays a sequence animation defined in your model via the model's inherent animation definitions, included animations and active addons. Cannot load custom animations, not even .ani, .mdl or .smd\n".. + "If you want to import an animation from somewhere else, you need to know some decompiling/recompiling QC knowledge" + }, + + ["player_movement"] = { + tooltip = "edits your player movement", + popup_tutorial = "This part tells the server to handle your movement manually with a Move hook.\n".. + "Z-Velocity means you can move in the air relative to your eye angles, with WASD and jump almost like noclip. It is however still subject to air friction (needs friction to move, but friction also decelerates you) and uses ground friction as a driver.\n".. + "Friction generally cuts your movement as a percentage every tick. This is why it's very sensitive because its effect is exponential. Horizontal air friction tries to mitigate that a bit\n".. + "Reverse pitch is probably buggy. " + }, + + ["group"] = { + tooltip = "organizes parts", + popup_tutorial = + "This part groups parts. That's all it does. It bypasses parenting, which means it has no side effect, aside from when modifiers act on their direct parent, in which case the group can get in the way.\n".. + "But with a root group, (a group at the root/top level, \"my outfit\"), you can choose an owner name to select another entity to bear the pac outfit." + }, + + ["lock"] = { + tooltip = "grabs or teleports", + popup_tutorial = + "This part allows you to grab things or teleport yourself.\n\n".. + "Warning in advance: It has the most barriers because it probably has the most potential for abuse out of all parts.\n".. + "\tClients need to give consent explicitly (pac_client_grab_consent 1), otherwise you can't grab them.\n".. + "\tThis is doubly true for players' view position. That's another consent (pac_client_lock_camera_consent 1) layered on top of the existing grab consent.\n".. + "\tOn top of that, grabbed players will get a notification if you grab them, and they will know how to break the lock. Clients have multiple commands (pac_break_lock, pac_stop_lock) to request the server to force you to release them. It is mildly punitive.\n".. + "\tThere are multiple server-level settings to limit it. Some servers may even wholesale disable the new combat parts for all players by default until they're trusted/whitelisted.\n\n".. + "Now, here's business. How it works, and how to use this part:\n".. + "\tThe part searches for entities around a sphere, selects the closest one and locks onto it. You should plan ahead for the fact that it only picks up entities by their origin position, which for NPCs and players is between their feet. offset down amount compensates for this, but only for where the detection radius begins.\n".. + "\tIt will then start communicating with the server and the server may reposition the entity if it's allowed. If rejected, you may get a warning in the console, and the part will be stopped for a while.".. + "\tOverrideEyeAngles works for players only, and as stated previously, is subject to consent restrictions.\n" + }, + + ["physics"] = { + tooltip = "creates a clientside physics object", + popup_tutorial = + "This part creates a physics object clientside which can be a box or a sphere. It will relocate the model and pac parts contained and put them in the object.\n".. + "It's not compatible with the force part, unfortunately, because it's clientside. There are other reasonably fun things it can do though.\n".. + "It only works as a direct modifier on a model." + }, + + ["jiggle"] = { + tooltip = "wobbles around", + popup_tutorial = + "This part creates a subpoint that carries base_movables, and moves around with a certain type of dynamics that can lag behind and then catch up, or wiggle back and forth for a while. Strain is how much it will wobble. The children parts will be contained within that subpoint.\n".. + "There is immense utility to control movement and have some physicality to your parts' movement. To name a few examples:\n".. + "\tThe jiggle 0 speed trick: Having your jiggle set at 0 speed will freeze what's inside. You can easily control that with two proxies: one for moving (not 0), one for stopping (0)\n".. + "\tPets and drones: Fun things that are semi-independent. Easy to do with jiggle.\n".. + "\tSmoother transitions with multiple static proxies: If you have position proxies that snap to different positions, making a model teleport too fast, using these proxies on a jiggle instead will let the jiggle do the work of smoothing things out with the movement.\n".. + "\tForward velocity indicator via a counter-lagger: jiggle lags behind an origin, model points to origin with aim part, other model is forward relative to the pointer. Result: a model that goes in the direction of your movement.\n\n".. + "The part, however, has issues when crossing certain angles (up and down)." + }, + + ["projected_texture"] = { + tooltip = "creates a lamp", + popup_tutorial = + "This part creates a dynamic light / projected texture that can project onto models or the world. That's pretty much it. It's useful for lamps, flashlights and the like.\n".. + "But if you're expecting a proper light, its directed lighting method gives mediocre results alone. With another light, and a sprite maybe, it'll look nicer. We won't have point_spotlights though.\n".. + "Its brightness works by multiplication, not magnitude. 0 is a proper 0 amount.\n\n".. + "Because it uses ITexture / VTF, it doesn't link up with pac materials. Animated textures must be done by frames instead of proxies. Although you can still set a custom image. But it's additive so the transparency can't be done with alpha, but on a black background\t".. + "fov on one hand, and horizontal/vertical fovs on the other hand, compete; so you should touch only one and leave the other." + }, + + ["hitscan"] = { + tooltip = "fires bullets", + popup_tutorial = + "This part tries to fire bullets. There are damaging serverside bullets and non-damaging clientside bullets. Both could be useful in their own scenarios.\n".. + "For serverside bullets, the server might restrict that. For example, it can force you to spread your damage among all your bullets, to notably prevent you from stacking tons of bullets to multiply your damage beyond the limit.\n".. + "Damage falloff works with a fractional floor on individual bullets, which means each bullet is lowered to a percentage of its max damage." + }, + + ["motion_blur"] = { + tooltip = "makes a trail of after-images", + popup_tutorial = + "This part continually renders a series of ghost copies of your model along its path to simulate a motion blur-like effect.\n".. + "It has limited options because of how models' clientside entity is set up, allegedly." + }, + + ["link"] = { + tooltip = "transfers variables between parts", + popup_tutorial = + "This part tries to copy variables between two parts and update them when their values change.\n".. + "It doesn't work for all variables especially booleans! Also, \"link\" is a strong word. Whatever you think it means, it's not doing that.".. + "Might require a rewear to work properly." + }, + + ["effect"] = { + tooltip = "runs a PCF effect", + popup_tutorial = + "This part uses an existing PCF effect on your game installation, from your mounted games or addons. No importable PCFs from the web.\n".. + "It apparently can use control points and make tracers work. It may or may not be supported by different effects; start by putting the effect in a basic model to position the effect.\n".. + "And PCF effects can be a gigantic pain, with for example looping issues, permanence issues (BEWARE OF TF2 UNUSUAL EFFECTS!), wrong positions etc.".. + "You should probably look into particles and think about how to layer them if you're looking for something more configurable." + }, + + ["text"] = { + tooltip = "draws 3D2D or 2D text", + popup_tutorial = + "This part renders text on a flat surface (3D2D with the DrawTextOutlined mode) or on the screen (2D with the SurfaceText mode). Due to technical limitations, some features in one may not be present in the other, such as the outline and the size scaling\n\n".. + "You can use a combination of data and text to build up your text. Combined text tells the part to use both, and text position tells you whether the text is before or after the data.\n".. + "What's this data? text override. There are a handful of presets, like your health, name, position. If you want more control, you can use Proxy, and it will use the dynamic text value (a simple number variable) which you can control with proxies.\n\n".. + "If you want to raise the resolution of the text, you should try making a bigger font. But creating fonts is expensive so it's throttled. You can only make one every 3 seconds.\n".. + "Although you can use any of gmod's or try to use your operating system's font names, there are still limits to fonts' features, both in their definitions and in the lua code. Not everything will work. But it will create a unique ID for the font it creates, and you can reuse that font in other text parts." + }, + + ["camera"] = { + tooltip = "changes your view", + popup_tutorial = + "This part runs a CalcView hook to allow you to go into a third person mode and change your view accordingly. Some parts on your player may get in the way.\n".. + "eye angle lerp determines how much you mix the original eye angles into the view. Otherwise at 0 it will fully use the part's local angles.\n".. + "Remember a right hand rule! Point forward, thumb up, middle finger perpendicular to the palm. This is how the camera will look with 0 lerp.\n".. + "\tX = Red = index finger = forward\n".. + "\tY = Green= middle finger = left\n".. + "\tZ = Blue = thumb finger = up\n".. + "As an example, if you apply this, you will learn that, on the head, you can simply take a 0,-90,-90 angle value and be done with it.\n\n".. + "Because of how pac3 works, you should be careful when toggling between cameras. I've made some fixes to prevent part of that, but if you hide with events, and lose your final camera, you can't come back unless you go back to third person (to restart the cameras) and then back into first person (to put priority back on an active camera)." + }, + + ["decal"] = { + tooltip = "applies decals", + popup_tutorial = + "Decals are a Source engine thing for bullet holes, sprays and other such flat details as manholes and posters. This part when shown emits one by tracing a line forward and applying it at the hit surface.\n".. + "It can use web images and pac materials, but still subject to rendering quirks depending on transparency and others.\n".. + "Decals are semi-permanent, you can only remove them with r_cleardecals" + }, + + ["projectile"] = { + tooltip = "throws missiles into the world", + popup_tutorial = + "the projectile part creates physical entities and launches them forward.\n".. + "the entity has physics but it can be clientside (visual) or serverside (physical)\n".. + "by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n".. + "the entity can do damage but servers can restrict that.\n".. + "For visual reference, a 1x1x1 cubeex is around radius 24, and a default pac sphere is around 8." + }, + + ["poseparameter"] = { + tooltip = "sets a pose parameter", + popup_tutorial = + "pose parameters are a Source engine thing that helps models animate using a \"blend sequence\". For instance, this is how the body and head are rotated, and how 8-way walks are blended.\n".. + "It goes without saying that not all models have those, and some have fewer supported pose parameters because of how they were made." + }, + + ["entity"] = { + tooltip = "edits an entity (legacy)", + popup_tutorial = "The legacy entity part still does the usual things you need to edit your entity. Color, model, size, no draw etc. But better use the new entity part." + }, + + ["entity2"] = { + tooltip = "edits an entity", + popup_tutorial = + "This part can edit some properties of an entity. This can be your playermodel, a pac3 model (it's a clientside entity) or a prop (you give a prop a pac outfit by selecting it with the owner name on a root group).\n".. + "It supports web models. See the model part's tutorial or read the MDL zips page on our wiki for further info.\n\n".. + "As with other bone-related things, it might not work properly if you use it on a ragdoll or some similar entities.\n\n".. + "Another warning in advance, if you wonder why your playermodel won't change, there are some addons, such as Enhanced Playermodel Selector, known to cause issues because they override your entity, thus conflicting with pac3. This one can be fixed if you disable \"enforce playermodel\"\n".. + "Other than that, the server setting pac_modifier_model and pac_modifier_size can forbid you from changing your playermodel and size respectively." + }, + + ["interpolated_multibone"] = { + tooltip = "morphs position between nodes", + popup_tutorial = + "A node-based path/morpher. This part allows you to move its contents by blending positions and angles between different points. Obviously enough, the nodes you select need to be base_movable parts.\n".. + "The first (Zeroth) node is the interpolated_multibone itself. From then on, the next node is reached when lerp reaches the corresponding number, and when you're at the end, i.e. an invalid or missing node, it morphs back to the origin.\n".. + "For example, 0.5 lerp will be halfway between the first node and the origin.\n".. + "While this part finally breaks through one of pac3's fundamental limitations (that of base_movables being limited to specific bones as anchoring points), there are still known issues, namely because of how angles are morphed. Roll angles might break.\n\n".. + "Suggested use cases: multi-position cutscene camera, returning hitpos pseudo-projectile, joints." + }, + + ["proxy"] = { + tooltip = "applies math to parts", + popup_tutorial = + "This part computes math and applies the numbers it gives to a parameter on a part, for number (x), vector (x,y,z) or boolean(true (1) or false (0)) types. It can send to the parent, to all its children, or to an external target part.\n".. + "Easy setup can help you make a rough idea quickly, but writing math yourself in the expression gives supremely superior control over what the math does.\n\n".. + "Here's a quick crash course in the syntax with basic examples showing the rules to observe:\n\n".. + "Basic numbers /math operators : 4^0.5 - 2*(0.2 / 5) + timeex()%4\n".. + "The only basic operators are: + - * / % ^\n".. + "Functions:\n".. + "\tFunctions are like variables that gather data from the world or that process math.\n".. + "\tMost functions are nullary, which means they have no argument: timeex(), time(), owner_health(), owner_armor_fraction()\n".. + "\tOthers have arguments, which can be required or optional: clamp(x,min,max), random(), random(min,max), random_once(seed,min,max), etc.\n".. + "\tAll Lua functions are declared by a set of parentheses containing arguments, possibly separated by commas.\n".. + "Arguments and tokens:\n".. + "\tMost arguments\' type is numbers, but some might be strings with some requirements; Most of the time it\'s a name or a part UID, for example:\n".. + "\tValid number arguments are numbers, functions or well-formed expressions. It\'s the same type because at the end of the day it gives you a number.\n".. + "\t\tNeedless to say, if you compose an expression, you need a coherent link between the tokens (i.e. math operators or functions). 2 + 2 is valid, 2 2 is not.\n".. + "\tValid string arguments are text declared by double quotes. Lua\'s string concatenation operator works. command(\"name\"..2) is the same as command(\"name2\")\n".. + "\t\tWithout the string declaration, Lua tries to look for a global variable. command(\"name\") is valid, command(name) is not.\n\n".. + "Nested functions (composition) : clamp(1 - timeex()^0.5,0,1)\n".. + "XYZ / Vectors (comma notation) : 100,0,50\n".. + "nil (skipping an axis) : 100,nil,0\n\n".. + "You can write pretty much any math using the existing functions as long as you observe the syntax\'s rules: the most common ones being to close your brackets properly, don't misspell your functions\' names and give them all their necessary arguments.\n\n".. + "There are lots of technical things to learn, but you can consult my example proxy bank by right clicking the expression field, and go consult our wiki for reference. https://wiki.pac3.info/part/proxy\n\n".. + "As a conclusion, I\'m gonna editorialize and give my recommendations:\n".. + "\t-Write with purpose. Avoid unnecessary math.\n".. + "\t\t->But still, write in a way that lets you understand the concept better.\n".. + "\t-More to the point, please have patience and deliberation. Make sure every piece works BEFORE moving on and making it more complex.\n".. + "\t-The fundamental mechanism of developing and applying new ideas is composition / compounding.\n".. + "\t\t->Multiplying different expression bits together tends to combine the concepts\n".. + "\t\t->e.g. clamp(0.2*timeex(),0,1)*sin(10*time()) is a fadein and a sine wave. What do you get? sine wave fading to full power.\n".. + "\t-Please read the debug messages in the console or in chat, they help the correction process if we make mistakes." + + }, + + ["sunbeams"] = { + tooltip = "shines like rays of light", + popup_tutorial = + "This part applies a sunbeam effect centered around the part.\n".. + "Multiplier is the strength of the effect. It can also be negative for engulfing darkness.\n".. + "Darken is how much to darken or brighten the whole effect. It helps control the contrast in conjunction with multiplier. With enough darken, only the brightest rays will go through, otherwise with unchecked multipliers or negative darken there's a whole blob of white that just overpowers everything\n".. + "Size affects the after-images projected around the center, which serve as a base for the effect." + }, + + ["shake"] = { + tooltip = "shakes nearby viewers\' camera", + popup_tutorial = + "This part applies a shake that uses the camera\'s position to take effect. For that reason, it may be nullified by certain third person addons, as well as the pac3 editor camera. You can still temporarily disable it to preview your shakes." + }, + + ["gesture"] = { + tooltip = "plays a gesture", + popup_tutorial = + "Gestures are a type of animation usually added as a layer on top of other animations, this part tries to play one but not all animations listed are gestures, so it might not work for most." + }, + + ["damage_zone"] = { + tooltip = "deals damage in a zone", + popup_tutorial = + "This part tries to deal hitbox damage via the server. It may or may not be allowed because of server settings (pac_sv_damage_zone) and client consents (pac_client_damage_zone_consent), etc. Server owners can add or remove entity classes that can be damaged with pac_damage_zone_blacklist_entity_class, pac_damage_zone_whitelist_entity_class commands.\n".. + "Among NPCs it should include VJ and DRG base NPCs, but only if they have npc_ or drg_ in their name\n".. + "Most shapes should be self-explanatory but you can use the preview function to see what it should cover. There are some settings for raycasts which could come in handy for some niche use cases even if the basic ones you'll use most of the time (box, sphere, cone from spheres, ray) will not really use these.".. + "There are certain special damage types. the dissolves can disintegrate entities but can be restricted in the server, prevent_physics_force suppresses the corpse force, removenoragdoll removes the corpse.\n".. + "" + }, + + ["health_modifier"] = { + tooltip = "modifies your health, armor", + popup_tutorial = + "This part allows you to quickly change your max health, max armor, damage multiplier taken, and has the possibility to give you extra health bars that absorb damage before the main health gets damaged.\n".. + "For the extra bars, you need to set a layer priority to pick which ones get damaged first. Outer ones are higher layer values. But they're still invisible for now... events and proxies will come later...\n".. + "The part's usage may or may not be allowed by the server." + } + + } + --print("we have defined the pace.TUTORIALS.PartInfos", pace.TUTORIALS.PartInfos) + + for i,v in pairs(pace.TUTORIALS.PartInfos) do + --print(i,v) + if pace.PartTemplates then + if pace.PartTemplates[i] then + pace.PartTemplates[i].TutorialInfo = v + end + end + end +end + +local motd_cvar = CreateConVar("pac_show_message_on_startup", "2", {FCVAR_ARCHIVE}, "Whether to show the update MOTD when you load in the game") + + +function pac.OpenMOTD(from_initial_startup) + local pnl = vgui.Create("DFrame") + pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) + + local url = "https://github.com/pingu7867/pac3#readme" + + function pnl:OnClose() + if not from_initial_startup then return end + local function exit_message() + if LocalPlayer():IsAdmin() then + notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat!", NOTIFY_GENERIC, 10) + end + notification.AddLegacy("Before you go, once you're in the PAC editor, please go to the options tab and consider choosing which parts of the combat update you want to consent to.", NOTIFY_GENERIC, 10) + end + Derma_Query("Did you read the update news?", "update news", + "Yes, go away", function() motd_cvar:SetInt(0) exit_message() end, + "Bring it up next update", function() motd_cvar:SetInt(1) exit_message() end, + "No, I'll read later", function() motd_cvar:SetInt(2) exit_message() end + ) + + end + + pnl:SetTitle("Welcome to a new update!") + local html = vgui.Create("DHTML", pnl) + html:Dock(FILL) + html:OpenURL( url ) + + + pnl:Center() + pnl:MakePopup() + pace.motd_opened = true +end + + +timer.Simple(10, function() if motd_cvar:GetInt() ~= 0 and not pace.motd_opened then pac.OpenMOTD(true) end end) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 6bee23138..38589542f 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -1,10 +1,11 @@ local L = pace.LanguageString -- load only when hovered above -local function add_expensive_submenu_load(pnl, callback) +local function add_expensive_submenu_load(pnl, callback, subdir) + local old = pnl.OnCursorEntered pnl.OnCursorEntered = function(...) - callback() + callback(subdir) pnl.OnCursorEntered = old return old(...) end @@ -95,6 +96,10 @@ end local last_backup local maxBackups = CreateConVar("pac_backup_limit", "100", {FCVAR_ARCHIVE}, "Maximal amount of backups") +local autoload_prompt = CreateConVar("pac_prompt_for_autoload", "1", {FCVAR_ARCHIVE}, "Whether to ask before loading autoload. The prompt can let you choose to not load, pick autoload or the newest backup") +local auto_spawn_prop = CreateConVar("pac_autoload_preferred_prop", "2", {FCVAR_ARCHIVE}, "When loading a pac with an owner name suggesting a prop, notify you and then wait before auto-applying the outfit next time you spawn a prop.\n".. + "0 : do not check\n1 : check if only 1 such group is present\n2 : check if multiple such groups are present and queue one group at a time") + function pace.Backup(data, name) name = name or "" @@ -138,7 +143,28 @@ function pace.Backup(data, name) end end +local latestprop +local latest_uid +if game.SinglePlayer() then + hook.Add("OnEntityCreated", "PAC_queue_proppacs", function( ent ) + if ( ent:GetClass() == "prop_physics" or ent:IsNPC()) and not ent:CreatedByMap() and LocalPlayer().pac_propload_queuedparts then + if not table.IsEmpty(LocalPlayer().pac_propload_queuedparts) then + ent:EmitSound( "buttons/button4.wav" ) + local root = LocalPlayer().pac_propload_queuedparts[next(LocalPlayer().pac_propload_queuedparts)] + root.self.OwnerName = ent:EntIndex() + latest_uid = root.self.UniqueID + pace.LoadPartsFromTable(root, false, false) + LocalPlayer().pac_propload_queuedparts[next(LocalPlayer().pac_propload_queuedparts)] = nil + latestprop = ent + end + + end + end) +end + + function pace.LoadParts(name, clear, override_part) + if not name then local frm = vgui.Create("DFrame") frm:SetTitle(L"parts") @@ -175,6 +201,7 @@ function pace.LoadParts(name, clear, override_part) end else + if name ~= "autoload.txt" and not string.find(name, "pac3/__backup") then cookie.Set( "pac_last_loaded_outfit", name ) end if hook.Run("PrePACLoadOutfit", name) == false then return end @@ -190,6 +217,7 @@ function pace.LoadParts(name, clear, override_part) local data, err = pace.luadata.Decode(str) if not data then + ErrorNoHalt(("URL fail: %s : %s\n"):format(name,err)) local message = string.format("URL fail: %s : %s\n", name, err) pace.MessagePrompt(message, "URL Failed", "OK") return @@ -205,22 +233,62 @@ function pace.LoadParts(name, clear, override_part) name = name:gsub("%.txt", "") local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") + local has_possible_prop_pacs = false - if name == "autoload" and (not data or not next(data)) then - local err - data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true) - if not data then - if err then - ErrorNoHalt(("Autoload failed: %s\n"):format(err)) + if data and istable(data) then + for i,part in pairs(data) do + if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end + end + end + + --queue up prop pacs for the next prop or npc you spawn when in singleplayer + if (auto_spawn_prop:GetInt() == 2 or (auto_spawn_prop:GetInt() == 1 and #data == 1)) and game.SinglePlayer() and has_possible_prop_pacs then + if clear then pace.ClearParts() end + LocalPlayer().pac_propload_queuedparts = LocalPlayer().pac_propload_queuedparts or {} + + --check all root parts from data. format: each data member is a {self, children} table of the part and the list of children + for i,part in pairs(data) do + local possible_prop_pac = isnumber(tonumber(part.self.OwnerName)) + if part.self.ClassName == "group" and possible_prop_pac then + + part.self.ModelTracker = part.self.ModelTracker or "" + part.self.ClassTracker = part.self.ClassTracker or "" + local str = "" + if part.self.ClassTracker == "" or part.self.ClassTracker == "" then + str = "But the class or model is unknown" + else + str = part.self.ClassTracker .. " : " .. part.self.ModelTracker + end + --notify which model / entity should be spawned with the class tracker + notification.AddLegacy( "You have queued a pac part (" .. i .. ":" .. part.self.Name .. ") for a prop or NPC! " .. str, NOTIFY_HINT, 10 ) + LocalPlayer().pac_propload_queuedparts[i] = part + + else + pace.LoadPartsFromTable(part, false, false) end + end + + else + + if name == "autoload" and (not data or not next(data)) then + local err + data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true) + if not data then + if err then + ErrorNoHalt(("Autoload failed: %s\n"):format(err)) + end + return + end + elseif not data then + ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err)) return end - elseif not data then - ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err)) - return - end - pace.LoadPartsFromTable(data, clear, override_part) + + pace.LoadPartsFromTable(data, clear, override_part) + + end + end end end @@ -425,6 +493,39 @@ local function populate_parts(menu, tbl, override_part, clear) end end +function pace.AddOneDirectorySavedPartsToMenu(menu, subdir, nicename) + if not subdir then return end + local subdir_head = subdir .. "/" + + local exp_submenu, pnl = menu:AddSubMenu(L""..subdir) + pnl:SetImage(pace.MiscIcons.load) + exp_submenu.GetDeleteSelf = function() return false end + subdir = "pac3/" .. subdir + if nicename then exp_submenu:SetText(nicename) end + + add_expensive_submenu_load(pnl, function(subdir) + local files = file.Find(subdir.."/*", "DATA") + local files2 = {} + --PrintTable(files) + for i, filename in ipairs(files) do + table.insert(files2, {filename, file.Time(subdir .. filename, "DATA")}) + end + + table.sort(files2, function(a, b) + return a[2] > b[2] + end) + + for _, data in pairs(files2) do + local name = data[1] + local full_path = subdir .. "/" .. name + --print(full_path) + local friendly_name = name .. " " .. string.NiceSize(file.Size(full_path, "DATA")) + exp_submenu:AddOption(friendly_name, function() pace.LoadParts(subdir_head .. name, true) end) + :SetImage(pace.MiscIcons.outfit) + end + end, subdir) +end + function pace.AddSavedPartsToMenu(menu, clear, override_part) menu.GetDeleteSelf = function() return false end @@ -481,7 +582,10 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) pnl:SetImage(pace.MiscIcons.clone) backups.GetDeleteSelf = function() return false end - add_expensive_submenu_load(pnl, function() + local subdir = "pac3/__backup/*" + + add_expensive_submenu_load(pnl, function(subdir) + local files = file.Find("pac3/__backup/*", "DATA") local files2 = {} @@ -500,14 +604,15 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) backups:AddOption(friendly_name, function() pace.LoadParts("__backup/" .. name, true) end) :SetImage(pace.MiscIcons.outfit) end - end) + end, subdir) local backups, pnl = menu:AddSubMenu(L"outfit backups") pnl:SetImage(pace.MiscIcons.clone) backups.GetDeleteSelf = function() return false end + subdir = "pac3/__backup_save/*" add_expensive_submenu_load(pnl, function() - local files = file.Find("pac3/__backup_save/*", "DATA") + local files = file.Find(subdir, "DATA") local files2 = {} for i, filename in ipairs(files) do @@ -534,7 +639,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) end) :SetImage(pace.MiscIcons.outfit) end - end) + end, subdir) end local function populate_parts(menu, tbl, dir, override_part) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 574c99876..2e753054f 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1,14 +1,633 @@ +include("parts.lua") +include("shortcuts.lua") + +local pac_submit_spam = CreateConVar('pac_submit_spam', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'Prevent users from spamming pac_submit') +local pac_submit_limit = CreateConVar('pac_submit_limit', '30', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'pac_submit spam limit') +local hitscan_allow = CreateConVar("pac_sv_hitscan", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside") +local hitscan_max_bullets = CreateConVar("pac_sv_hitscan_max_bullets", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum number of bullets") +local hitscan_max_damage = CreateConVar("pac_sv_hitscan_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum damage") +local hitscan_spreadout_dmg = CreateConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not force hitscans to divide their damage among the number of bullets fired") + +local damagezone_allow = CreateConVar("pac_sv_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside") +local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum damage") +local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length") +local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius") +local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone") + +local lock_allow = CreateConVar("pac_sv_lock", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") +local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") +local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside") +local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius") +local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing players with lock part") +local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part") +local lock_allow_grab_ent = CreateConVar("pac_sv_lock_allow_grab_ent", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing other entities with lock part") + +local force_allow = CreateConVar("pac_sv_force", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside") +local force_max_length = CreateConVar("pac_sv_force_max_length", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum length") +local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum radius") +local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force") + +local healthmod_allow = CreateConVar("pac_sv_health_modifier", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") +local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") +local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") +local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") + +local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") + +local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter") +local netrate_allowance = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 60, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "the budgeted allowance to limit how many pac combat net messages can be sent in bursts. 0 to disable, otherwise a number of net messages of allowance.") +local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_netrate_monitor_serverside", 0, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not to let clients enforce their net message rates.\nSet this to 1 to get serverside prints telling you whenever someone is going over their allowance, but it'll still take the network bandwidth.\nSet this to 0 to let clients enforce their net rate and save some bandwidth but the server won't know who's spamming net messages.") +local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") +local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") +local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") +local enforce_distance = CreateConVar("pac_sv_combat_distance_enforced", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enforce a limit on how far a pac combat action can originate.\nIf set to a distance, it will prevent actions that are too far from the acting player.\n0 to disable.") + + +local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") +local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") + + +include("pac3/editor/server/combat_bans.lua") + + +pace = pace + +pace.partmenu_categories_cedrics = +{ + ["new!"] = + { + ["icon"] = "icon16/new.png", + ["interpolated_multibone"]= "interpolated_multibone", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + }, + ["logic"] = + { + ["icon"] = "icon16/server_chart.png", + ["proxy"] = "proxy", + ["command"] = "command", + ["event"] = "event", + ["text"] = "text", + ["link"] = "link", + }, + ["scaffolds"] = + { + ["tooltip"] = "useful to build up structures with specific positioning rules", + ["icon"] = "map", + ["jiggle"] = "jiggle", + ["model2"] = "model2", + ["projectile"] = "projectile", + ["interpolated_multibone"]= "interpolated_multibone", + }, + ["combat"] = + { + ["icon"] = "icon16/joystick.png", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["projectile"] = "projectile", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + ["player_movement"] = "player_movement", + }, + ["animation"]= + { + ["icon"] = "icon16/world.png", + ["group"] = "group", + ["event"] = "event", + ["custom_animation"] = "custom_animation", + ["proxy"] = "proxy", + ["sprite"] = "sprite", + ["particle"] = "particle", + }, + ["materials"]= + { + ["icon"] = "pace.MiscIcons.appearance", + ["material_3d"] = "material_3d", + ["material_2d"] = "material_2d", + ["material_refract"] = "material_refract", + ["material_eye refract"]= "material_eye refract", + ["submaterial"] = "submaterial", + }, + ["entity"] = + { + ["icon"] = "icon16/cd_go.png", + ["bone3"] = "bone3", + ["custom_animation"] = "custom_animation", + ["gesture"] = "gesture", + ["entity2"] = "entity2", + ["poseparameter"] = "poseparameter", + ["camera"] = "camera", + ["holdtype"] = "holdtype", + ["effect"] = "effect", + ["player_config"] = "player_config", + ["player_movement"] = "player_movement", + ["animation"] = "animation", + ["submaterial"] = "submaterial", + ["faceposer"] = "faceposer", + ["flex"] = "flex", + ["material_3d"] = "material_3d", + ["weapon"] = "weapon", + }, + ["model"] = + { + ["icon"] = "icon16/bricks.png", + ["jiggle"] = "jiggle", + ["physics"] = "physics", + ["animation"] = "animation", + ["bone3"] = "bone3", + ["effect"] = "effect", + ["submaterial"] = "submaterial", + ["clip2"] = "clip2", + ["halo"] = "halo", + ["material_3d"] = "material_3d", + ["model2"] = "model2", + }, + ["modifiers"] = + { + ["icon"] = "icon16/connect.png", + ["fog"] = "fog", + ["motion_blur"] = "motion_blur", + ["halo"] = "halo", + ["clip2"] = "clip2", + ["bone3"] = "bone3", + ["poseparameter"] = "poseparameter", + ["material_3d"] = "material_3d", + ["proxy"]= "proxy", + }, + ["effects"] = + { + ["icon"] = "icon16/wand.png", + ["sprite"] = "sprite", + ["sound2"] = "sound2", + ["effect"] = "effect", + ["halo"] = "halo", + ["particles"]= "particles", + ["sunbeams"]= "sunbeams", + ["beam"]= "beam", + ["projected_texture"]= "projected_texture", + ["decal"]= "decal", + ["text"]= "text", + ["trail2"]= "trail2", + ["sound"]= "sound", + ["woohoo"]= "woohoo", + ["light2"]= "light2", + ["shake"]= "shake", + } +} + +pace.partmenu_categories_default = +{ + ["legacy"]= + { + ["icon"] = pace.GroupsIcons.legacy, + ["trail"]= "trail", + ["bone2"]= "bone2", + ["model"]= "model", + ["bodygroup"]= "bodygroup", + ["material"]= "material", + ["light"]= "light", + ["entity"]= "entity", + ["clip"]= "clip", + ["bone"]= "bone", + ["webaudio"]= "webaudio", + ["ogg"] = "ogg", + }, + ["advanced"]= + { + ["icon"] = pace.GroupsIcons.advanced, + ["lock"]= "lock", + ["force"]= "force", + ["custom_animation"]= "custom_animation", + ["material_refract"]= "material_refract", + ["projectile"]= "projectile", + ["link"] = "link", + ["damage_zone"] = "damage_zone", + ["interpolated_multibone"] = "interpolated_multibone", + ["material_2d"] = "material_2d", + ["material_eye refract"] = "material_eye refract", + ["hitscan"] = "hitscan", + ["health_modifier"] = "health_modifier", + ["command"] ="command", + }, + ["entity"]= + { + ["icon"] = pace.GroupsIcons.entity, + ["bone3"] = "bone3", + ["gesture"] = "gesture", + ["entity2"] ="entity2", + ["poseparameter"] = "poseparameter", + ["camera"] = "camera", + ["holdtype"]= "holdtype", + ["effect"] = "effect", + ["player_config"] = "player_config", + ["player_movement"] = "player_movement", + ["animation"] = "animation", + ["submaterial"] = "submaterial", + ["faceposer"] = "faceposer", + ["flex"] = "flex", + ["material_3d"] = "material_3d", + ["weapon"]= "weapon", + }, + ["model"]= + { + ["icon"] = pace.GroupsIcons.model, + ["jiggle"] = "jiggle", + ["physics"] = "physics", + ["animation"]= "animation", + ["bone3"] = "bone3", + ["effect"] = "effect", + ["submaterial"] ="submaterial", + ["clip2"] = "clip2", + ["halo"] = "halo", + ["material_3d"] = "material_3d", + ["model2"]= "model2", + }, + ["modifiers"]= + { + ["icon"] = pace.GroupsIcons.modifiers, + ["animation"] = "animation", + ["fog"] = "fog", + ["motion_blur"] = "motion_blur", + ["clip2"]= "clip2", + ["poseparameter"] = "poseparameter", + ["material_3d"] = "material_3d", + ["proxy"] = "proxy", + }, + ["effects"]= + { + ["icon"] = pace.GroupsIcons.effects, + ["sprite"] = "sprite", + ["sound2"] = "sound2", + ["effect"] = "effect", + ["halo"] = "halo", + ["particles"]= "particles", + ["sunbeams"] = "sunbeams", + ["beam"] = "beam", + ["projected_texture"]= "projected_texture", + ["decal"] = "decal", + ["text"] = "text", + ["trail2"] = "trail2", + ["sound"] = "sound", + ["woohoo"] = "woohoo", + ["light2"] = "light2", + ["shake"] = "shake" + } +} + + +local function rebuild_bookmarks() + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + + --here's some default favorites + if not pace.bookmarked_ressources["models"] or table.IsEmpty(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 + + if not pace.bookmarked_ressources["sound"] or table.IsEmpty(pace.bookmarked_ressources["sound"]) then + pace.bookmarked_ressources["sound"] = { + "music/hl1_song11.mp3", + "npc/combine_gunship/dropship_engine_near_loop1.wav", + "ambient/alarms/warningbell1.wav", + "phx/epicmetal_hard7.wav", + "phx/explode02.wav" + } + end + + if not pace.bookmarked_ressources["materials"] or table.IsEmpty(pace.bookmarked_ressources["materials"]) then + pace.bookmarked_ressources["materials"] = { + "models/debug/debugwhite", + "vgui/null", + "debug/env_cubemap_model", + "models/wireframe", + "cable/physbeam", + "cable/cable2", + "effects/tool_tracer", + "effects/flashlight/logo", + "particles/flamelet[1,5]", + "sprites/key_[0,9]", + "vgui/spawnmenu/generating", + "vgui/spawnmenu/hover" + } + end + + if not pace.bookmarked_ressources["proxy"] or table.IsEmpty(pace.bookmarked_ressources["proxy"]) then + pace.bookmarked_ressources["proxy"] = { + --[[["user"] = { + + },]] + ["fades and transitions"] ={ + { + nicename = "standard clamp fade (in)", + expression = "clamp(timeex(),0,1)", + explanation = "the simplest fade.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 0, moves gradually to 1 and stops progressing at 1 due to the clamp" + }, + { + nicename = "standard clamp fade (out)", + expression = "clamp(1 - timeex(),0,1)", + explanation = "the simplest fade's reverse.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 1, moves gradually to 0 and stops progressing at 0 due to the clamp" + }, + { + nicename = "standard clamp fade (delayed in)", + expression = "clamp(-1 + timeex(),0,1)", + explanation = "the basic fade is delayed by the fact that the clamp makes sure the negative values are pulled back to 0 until the first argument crosses 0 into the clamp's range." + }, + { + nicename = "standard clamp fade (delayed out)", + expression = "clamp(2 - timeex(),0,1)", + explanation = "the reverse fade is delayed by the fact that the clamp makes sure the values beyond 1 are pulled back to 1 until the first argument crosses 1 into the clamp's range." + }, + { + nicename = "standard clamp fade (in and out)", + expression = "clamp(timeex(),0,1)*clamp(3 - timeex(),0,1)", + explanation = "this is just compounding both fades. the second clamp's 3 is comprised of 1 (the clamp max) + 1 (the delay BEFORE the fade) + 1 (the delay BETWEEN the fades)" + }, + { + nicename = "quick ease setup", + expression = "easeInBack(clamp(timeex(),0,1))", + explanation = "get started quickly with the new easing functions.\nsearch \"ease\" in the proxy's input list to see how to write them in pac3, or look at the gmod wiki to see previews of each" + }, + }, + ["pulses"] = { + { + nicename = "bell pulse", + expression = "(0 + 1*sin(PI*timeex())^16)", + explanation = "a basic normalized pulse, using a sine power." + }, + { + nicename = "square-like throb", + expression = "(0 + 1 * (cos(PI*timeex())^16) ^0.3)", + explanation = "a throbbing-like pulse, made by combining a sine power with a fractionnal power.\nthis is better explained visually, so either test it right here in game or go look at a graph to see how x, and cos or sin behave with powers.\ntry x^pow and sin(x)^pow, and try different pows" + }, + { + nicename = "binary pulse", + expression = "floor(1 + sin(PI*timeex()))", + explanation = "an on-off pulse, in other words a square wave.\nthis one completes one cycle every 2 seconds.\nfloor rounds down between 1 and 0 with nothing in-between." + }, + { + nicename = "saw wave (up)", + expression = "(timeex()%1)", + explanation = "a sawtooth wave. it can repeat a 0-1 transition." + }, + { + nicename = "saw wave (down)", + expression = "(1 - timeex()%1)", + explanation = "a sawtooth wave. it can repeat a 1-0 transition." + }, + { + nicename = "triangle wave", + expression = "(clamp(-1+timeex()%2,0,1) + clamp(1 - timeex()%2,0,1))", + explanation = "a triangle wave. it goes back and forth linearly like a saw up and down." + } + }, + ["facial expressions"] = { + { + nicename = "normal slow blink", + expression = "3*clamp(sin(timeex())^100,0,1)", + explanation = "a normal slow blink.\nwhile flexes usually have a range of 0-1, the 3 outside of the clamp is there to trick the value into going faster in case they're too slow to reach their target" + }, + { + nicename = "normal fast blink", + expression = "8*clamp(sin(timeex())^600,0,1)", + explanation = "a normal slow blink.\nwhile flexes usually have a range of 0-1, the 8 outside of the clamp is there to trick the value into going faster in case they're too slow to reach their target\nif it's still not enough, use another flex with less blinking amount to provide the additionnal distance for the blink" + }, + { + nicename = "babble", + expression = "sin(12*timeex())^2", + explanation = "a basic piece to move the mouth semi-convincingly for voicelines.\nthere'll never be dynamic lipsync in pac3, but this is a start." + }, + { + nicename = "voice smoothener", + expression = "clamp(feedback() + 70*voice_volume()*ftime() - 15*ftime(),0,2)", + explanation = "uses a feedback() setup to raise the mouth's value gradually against a constantly lowering value, which should be more smoothly than a direct input" + }, + { + nicename = "look side (legacy symmetrical look)", + expression = "3*(-1 + 2*pose_parameter(\"head_yaw\"))", + explanation = "an expression to mimic the head's yaw" + }, + { + nicename = "look side (new)", + expression = "pose_parameter_true(\"head_yaw\")", + explanation = "an expression to mimic the head's yaw, but it requires your model to have this standard pose parameter" + }, + { + nicename = "look up", + expression = "(-1 + 2*owner_eye_angle_pitch())", + explanation = "an expression to mimic the head's pitch on a [-1,1] range" + }, + { + nicename = "single eyeflex direction (up)", + expression = "-0.03*pose_parameter_true(\"head_pitch\")", + explanation = "plug into an eye_look_up flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (down)", + expression = "0.03*pose_parameter_true(\"head_pitch\")", + explanation = "plug into an eye_look_down flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (left)", + expression = "0.03*pose_parameter_true(\"head_yaw\")", + explanation = "plug into an eye_look_left flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (right)", + expression = "-0.03*pose_parameter_true(\"head_yaw\")", + explanation = "plug into an eye_look_right flex or an eye bone with a higher multiplier" + }, + }, + ["spatial"] = { + { + nicename = "random position (cloud)", + expression = "150*random(-1,1),150*random(-1,1),150*random(-1,1)", + explanation = "position a part randomly across X,Y,Z\nbut constantly blinking everywhere, because random generates a new number every frame.\nyou should only use this for parts that emit things into the world" + }, + { + nicename = "random position (once)", + expression = "150*random_once(0,-1,1),150*random_once(1,-1,1),150*random_once(2,-1,1)", + explanation = "position a part randomly across X,Y,Z\nbut once, because random_once only generates a number once.\nit, however, needs distinct numbers in the first arguments to distinguish them every time you write the function." + }, + { + nicename = "distance-based fade", + expression = "clamp((250/500) + 1 - (eye_position_distance() / 500),0,1)", + explanation = "a fading based on the viewer's distance. 250 and 500 are the example distances, 250 is where the expression starts diminishing, and 750 is where we reach 0." + }, + { + nicename = "distance between two points", + expression = "part_distance(uid1,uid2)", + explanation = "Trick question! You have some homework! You need to find out your parts' UIDs first.\ntry tools -> copy global id, then paste those in place of uid1 and uid2" + }, + { + nicename = "revolution (orbit)", + expression = "150*sin(time()),150*cos(time()),0", + explanation = "Trick question! You might need to rearrange the expression depending on which coordinate system we're at. For a thing on a pos_noang bone, it works as is. But for something on your head, you would need to swap x and z\n0,150*cos(time()),150*sin(time())" + }, + { + nicename = "spin", + expression = "0,360*time(),0", + explanation = "a simple spinner on Y" + } + }, + ["experimental things"] = { + { + nicename = "control a boolean directly with an event", + expression = "event_alternative(uid1,0,1)", + explanation = "trick question! you need to find out your event's part UID first and substitute uid1\n" + }, + { + nicename = "feedback system controlled with 2 events", + expression = "feedback() + ftime()*(event_alternative(uid1,0,1) + event_alternative(uid2,0,-1))", + explanation = "trick question! you need to find out your event parts' UIDs first and substitute uid1 and uid2.\nthe new event_alternative function gets an event's state\nwe can inject that into our feedback system to act as a positive or negative speed" + }, + { + nicename = "basic if-else statement", + expression = "number_operator_alternative(1,\">\",0,100,50)", + explanation = "might be niche but here's a basic alternator thing, you can compare the 1st and 3rd args with numeric operators like \"above\", \"<\", \"=\", \"~=\" etc. to choose between the 4th and 5th args\nit goes like this\nnumber_operator_alternative(1,\">\",0,100,50)\nif 1>0, return 100, else return 50" + }, + { + nicename = "pick from 3 random colors", + expression = "number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.75)),number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 0.8, 0.65)),number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.58))", + explanation = + "using a shared random source, you can nest number_operator_alternative functions to get a 3-way branching random choice\n0.333 and 0.666 correspond to the chance slices where each choice gets decided so you can change the probabilities by editing these numbers\nBecause of the fact we're going deep, it's not easily readable so I'll lay out each component.\n\n" .. + "R: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.75))\n".. + "G: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 0.8, 0.65))\n".. + "B: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.58))\n\n".. + "The first choice is white (1,1,1), the second choice is light pink (1,0.8,1) like a strawberry milk, the third choice is light creamy brown (0.75,0.65,0.58) like chocolate milk" + }, + { + nicename = "feedback command attractor", + expression = "feedback() + ftime()*(command(\"destination\") - feedback())", + explanation = + "This thing uses a principle of iteration similar to exponential functions to attract the feedback toward any target\n".. + "The delta bit will get smaller and smaller as the gap between destination and feedback closes, stabilizing at 0, thereby stopping.\n".. + "You will utilize pac_proxy commands to set the destination target: \"pac_proxy destination 2\" will make the expression tend toward 2." + } + } + } + + end + +end local PANEL = {} +local player_ban_list = {} +local player_combat_ban_list = {} + +local function encode_table_to_file(str) + local data = {} + if not file.Exists("pac3_config", "DATA") then + file.CreateDir("pac3_config") + + end + + + if str == "pac_editor_shortcuts" then + data = pace.PACActionShortcut + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "pac_editor_partmenu_layouts" then + data = pace.operations_order + file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data)) + elseif str == "pac_part_categories" then + data = pace.partgroups + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "bookmarked_ressources" then + rebuild_bookmarks() + for category, tbl in pairs(pace.bookmarked_ressources) do + data = tbl + str = category + file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data)) + end + elseif str == "eventwheel_colors" then + data = pace.command_colors or {} + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + end + +end + +local function decode_table_from_file(str) + if str == "bookmarked_ressources" then + rebuild_bookmarks() + local ressource_types = {"models", "sound", "materials", "sprites"} + for _, category in pairs(ressource_types) do + data = file.Read("pac3_config/bookmarked_" .. category ..".txt", "DATA") + if data then pace.bookmarked_ressources[category] = util.KeyValuesToTable(data) end + end + return + end + + local data = file.Read("pac3_config/" .. str..".txt", "DATA") + if not data then return end + + if str == "pac_editor_shortcuts" then + pace.PACActionShortcut = util.KeyValuesToTable(data) + + elseif str == "pac_editor_partmenu_layouts" then + pace.operations_order = util.JSONToTable(data) + + elseif str == "pac_part_categories" then + pace.partgroups = util.KeyValuesToTable(data) + + elseif str == "eventwheel_colors" then + pace.command_colors = util.KeyValuesToTable(data) + end + + +end + +decode_table_from_file("bookmarked_ressources") +pace.bookmarked_ressources = pace.bookmarked_ressources or {} + +function pace.SaveRessourceBookmarks() + encode_table_to_file("bookmarked_ressources") +end + function PANEL:Init() - local pnl = vgui.Create("DPropertySheet", self) - pnl:Dock(FILL) + local master_pnl = vgui.Create("DPropertySheet", self) + master_pnl:Dock(FILL) + + local properties_filter = pace.FillWearSettings(master_pnl) + master_pnl:AddSheet("Wear / Ignore", properties_filter) + + local editor_settings = pace.FillEditorSettings(master_pnl) + master_pnl:AddSheet("Editor menu Settings", editor_settings) - local props = pace.FillWearSettings(pnl) + local editor_settings2 = pace.FillEditorSettings2(master_pnl) + master_pnl:AddSheet("Editor menu Settings 2", editor_settings2) - pnl:AddSheet("Wear / Ignore", props) - self.sheet = pnl + + if game.SinglePlayer() or LocalPlayer():IsAdmin() then + + local general_sv_settings = pace.FillServerSettings(master_pnl) + master_pnl:AddSheet("General Settings (SV)", general_sv_settings) + + local combat_sv_settings = pace.FillCombatSettings(master_pnl) + master_pnl:AddSheet("Combat Settings (SV)", combat_sv_settings) + + local ban_settings = pace.FillBanPanel(master_pnl) + master_pnl:AddSheet("Bans (SV)", ban_settings) + + local combat_ban_settings = pace.FillCombatBanPanel(master_pnl) + master_pnl:AddSheet("Combat Bans (SV)", combat_ban_settings) + + end + + + self.sheet = master_pnl + + --local properties_shortcuts = pace.FillShortcutSettings(pnl) + --pnl:AddSheet("Editor Shortcuts", properties_shortcuts) end vgui.Register( "pace_settings", PANEL, "DPanel" ) @@ -20,7 +639,7 @@ function pace.OpenSettings() local pnl = vgui.Create("DFrame") pnl:SetTitle("pac settings") pace.settings_panel = pnl - pnl:SetSize(600,600) + pnl:SetSize(800,600) pnl:MakePopup() pnl:Center() pnl:SetSizable(true) @@ -31,4 +650,1931 @@ end concommand.Add("pace_settings", function() pace.OpenSettings() -end) \ No newline at end of file +end) + + +function pace.FillBanPanel(pnl) + local pnl = pnl + local BAN = vgui.Create("DPanel", pnl) + local ply_state_list = player_ban_list or {} + + local ban_list = vgui.Create("DListView", BAN) + ban_list:SetText("ban list") + ban_list:SetSize(400,400) + ban_list:SetPos(10,10) + + ban_list:AddColumn("Player name") + ban_list:AddColumn("SteamID") + ban_list:AddColumn("State") + ban_list:SetSortable(false) + for _,ply in pairs(player.GetAll()) do + --print(ply, pace.IsBanned(ply)) + ban_list:AddLine(ply:Name(),ply:SteamID(),player_ban_list[ply] or "Allowed") + end + + function ban_list:DoDoubleClick( lineID, line ) + --MsgN( "Line " .. lineID .. " was double clicked!" ) + local state = line:GetColumnText( 3 ) + + if state == "Banned" then state = "Allowed" + elseif state == "Allowed" then state = "Banned" end + line:SetColumnText(3,state) + ply_state_list[player.GetBySteamID(line:GetColumnText( 2 ))] = state + PrintTable(ply_state_list) + end + + local ban_confirm_list_button = vgui.Create("DButton", BAN) + ban_confirm_list_button:SetText("Send ban list update to server") + + ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!") + ban_confirm_list_button:SetColor(Color(255,0,0)) + ban_confirm_list_button:SetSize(200, 40) + ban_confirm_list_button:SetPos(450, 10) + function ban_confirm_list_button:DoClick() + net.Start("pac.BanUpdate") + net.WriteTable(ply_state_list) + net.SendToServer() + end + local ban_request_list_button = vgui.Create("DButton", BAN) + ban_request_list_button:SetText("Request ban list from server") + --ban_request_list_button:SetColor(Color(255,0,0)) + ban_request_list_button:SetSize(200, 40) + ban_request_list_button:SetPos(450, 60) + + function ban_request_list_button:DoClick() + net.Start("pac.RequestBanStates") + net.SendToServer() + end + + net.Receive("pac.SendBanStates", function() + local players = net.ReadTable() + player_ban_list = players + PrintTable(players) + end) + + + return BAN +end + +function pace.FillCombatBanPanel(pnl) + local pnl = pnl + local BAN = vgui.Create("DPanel", pnl) + pac.global_combat_whitelist = pac.global_combat_whitelist or {} + + + local ban_list = vgui.Create("DListView", BAN) + ban_list:SetText("Combat ban list") + ban_list:SetSize(400,400) + ban_list:SetPos(10,10) + + ban_list:AddColumn("Player name") + ban_list:AddColumn("SteamID") + ban_list:AddColumn("State") + ban_list:SetSortable(false) + if GetConVar('pac_sv_combat_whitelisting'):GetBool() then + ban_list:SetTooltip( "Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed" ) + else + ban_list:SetTooltip( "Blacklist mode: Default players are allowed to use the combat features" ) + end + + local combat_bans_temp_merger = {} + + for _,ply in pairs(player.GetAll()) do + combat_bans_temp_merger[ply:SteamID()] = pac.global_combat_whitelist[ply:SteamID()]-- or {nick = ply:Nick(), steamid = ply:SteamID(), permission = "Default"} + end + + for id,data in pairs(pac.global_combat_whitelist) do + combat_bans_temp_merger[id] = data + end + + for id,data in pairs(combat_bans_temp_merger) do + ban_list:AddLine(data.nick,data.steamid,data.permission) + end + + function ban_list:DoDoubleClick( lineID, line ) + --MsgN( "Line " .. lineID .. " was double clicked!" ) + local state = line:GetColumnText( 3 ) + + if state == "Banned" then state = "Default" + elseif state == "Default" then state = "Allowed" + elseif state == "Allowed" then state = "Banned" end + line:SetColumnText(3,state) + pac.global_combat_whitelist[string.lower(line:GetColumnText( 2 ))].permission = state + PrintTable(pac.global_combat_whitelist) + end + + local ban_confirm_list_button = vgui.Create("DButton", BAN) + ban_confirm_list_button:SetText("Send combat ban list update to server") + + ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!") + ban_confirm_list_button:SetColor(Color(255,0,0)) + ban_confirm_list_button:SetSize(200, 40) + ban_confirm_list_button:SetPos(450, 10) + function ban_confirm_list_button:DoClick() + net.Start("pac.CombatBanUpdate") + net.WriteTable(pac.global_combat_whitelist) + net.WriteBool(true) + net.SendToServer() + end + local ban_request_list_button = vgui.Create("DButton", BAN) + ban_request_list_button:SetText("Request ban list from server") + --ban_request_list_button:SetColor(Color(255,0,0)) + ban_request_list_button:SetSize(200, 40) + ban_request_list_button:SetPos(450, 60) + + function ban_request_list_button:DoClick() + net.Start("pac.RequestCombatBanStates") + net.SendToServer() + end + + net.Receive("pac.SendCombatBanStates", function() + pac.global_combat_whitelist = net.ReadTable() + ban_list:Clear() + local combat_bans_temp_merger = {} + + for _,ply in pairs(player.GetAll()) do + combat_bans_temp_merger[ply:SteamID()] = pac.global_combat_whitelist[ply:SteamID()]-- or {nick = ply:Nick(), steamid = ply:SteamID(), permission = "Default"} + end + + for id,data in pairs(pac.global_combat_whitelist) do + combat_bans_temp_merger[id] = data + end + + for id,data in pairs(combat_bans_temp_merger) do + ban_list:AddLine(data.nick,data.steamid,data.permission) + end + end) + + + return BAN +end + +function pace.FillCombatSettings(pnl) + local pnl = pnl + + local master_list = vgui.Create("DCategoryList", pnl) + master_list:Dock(FILL) + --general + do + local general_list = master_list:Add("General (Global policy and Network protection)") + general_list.Header:SetSize(40,40) + general_list.Header:SetFont("DermaLarge") + local general_list_list = vgui.Create("DListLayout") + general_list_list:DockPadding(20,0,20,20) + general_list:SetContents(general_list_list) + + local sv_prop_protection_props_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_prop_protection_props_box:SetText("Enforce generic prop protection for player-owned props and physics entities.\nRelated to client consents, but the policies for each part are not uniform.") + sv_prop_protection_props_box:SetSize(400,30) + sv_prop_protection_props_box:SetConVar("pac_sv_prop_protection") + + + local sv_combat_whitelisting_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_combat_whitelisting_box:SetText("Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.") + sv_combat_whitelisting_box:SetSize(400,30) + sv_combat_whitelisting_box:SetConVar("pac_sv_combat_whitelisting") + sv_combat_whitelisting_box:SetTooltip("off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed") + + local sv_master_break_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_master_break_box:SetText("Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts") + sv_master_break_box:SetSize(400,30) + sv_master_break_box:SetConVar("pac_sv_block_combat_features_on_next_restart") + sv_master_break_box:SetTooltip("You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers") + + local sv_netrate_monitoring_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_netrate_monitoring_box:SetText("Enable serverside monitoring prints for allowance and rate limiters") + sv_netrate_monitoring_box:SetSize(400,30) + sv_netrate_monitoring_box:SetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") + sv_netrate_monitoring_box:SetTooltip("Enable serverside monitoring prints.\n0=let clients enforce their netrate allowance before sending messages\n1=the server will receive net messages and print the outcome.") + + local sv_netrate_time_numbox = vgui.Create("DNumSlider", general_list_list) + sv_netrate_time_numbox:SetText("Rate limiter (milliseconds)") + sv_netrate_time_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate"):GetInt()) + sv_netrate_time_numbox:SetMin(0) sv_netrate_time_numbox:SetDecimals(0) sv_netrate_time_numbox:SetMax(1000) + sv_netrate_time_numbox:SetSize(400,30) + sv_netrate_time_numbox:SetConVar("pac_sv_combat_enforce_netrate") + sv_netrate_time_numbox:SetTooltip("The milliseconds delay between net messages.\nIf this is 0, the allowance won't matter, otherwise early net messages use up the player's allowance.\nThe allowance regenerates gradually when unused, and one unit gets spent if the message is earlier than the rate limiter's delay.") + + local sv_netrate_buffer_numbox = vgui.Create("DNumSlider", general_list_list) + sv_netrate_buffer_numbox:SetText("Allowance, in number of messages") + sv_netrate_buffer_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt()) + sv_netrate_buffer_numbox:SetMin(0) sv_netrate_buffer_numbox:SetDecimals(0) sv_netrate_buffer_numbox:SetMax(400) + sv_netrate_buffer_numbox:SetSize(400,30) + sv_netrate_buffer_numbox:SetConVar("pac_sv_combat_enforce_netrate_buffersize") + sv_netrate_buffer_numbox:SetTooltip("Allowance:\nIf this is 0, only the time limiter will stop pac combat messages if they're too fast.\nOtherwise, players trying to use a pac combat message earlier will deduct 1 from the player's allowance, and only stop the messages if the allowance reaches 0.") + + local sv_hard_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) + sv_hard_ent_limit_numbox:SetText("Hard entity limit to cutoff damage zones and force parts") + sv_hard_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_combat_operation"):GetInt()) + sv_hard_ent_limit_numbox:SetMin(0) sv_hard_ent_limit_numbox:SetDecimals(0) sv_hard_ent_limit_numbox:SetMax(1000) + sv_hard_ent_limit_numbox:SetSize(400,30) + sv_hard_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_combat_operation") + sv_hard_ent_limit_numbox:SetTooltip("If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") + + local sv_per_player_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) + sv_per_player_ent_limit_numbox:SetText("Entity limit per player to cutoff damage zones and force parts") + sv_per_player_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_player_per_combat_operation"):GetInt()) + sv_per_player_ent_limit_numbox:SetMin(0) sv_per_player_ent_limit_numbox:SetDecimals(0) sv_per_player_ent_limit_numbox:SetMax(500) + sv_per_player_ent_limit_numbox:SetSize(400,30) + sv_per_player_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_player_per_combat_operation") + sv_per_player_ent_limit_numbox:SetTooltip("When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") + + local sv_player_fraction_slider = vgui.Create("DNumSlider", general_list_list) + sv_player_fraction_slider:SetText("block damage zones targeting this fraction of players") + sv_player_fraction_slider:SetValue(GetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone"):GetFloat()) + sv_player_fraction_slider:SetMin(0) sv_player_fraction_slider:SetDecimals(2) sv_player_fraction_slider:SetMax(1) + sv_player_fraction_slider:SetSize(400,30) + sv_player_fraction_slider:SetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone") + sv_player_fraction_slider:SetTooltip("This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.") + + local sv_distance_slider = vgui.Create("DNumSlider", general_list_list) + sv_distance_slider:SetText("distance to block combat actions that are too far") + sv_distance_slider:SetValue(GetConVar("pac_sv_combat_distance_enforced"):GetFloat()) + sv_distance_slider:SetMin(0) sv_distance_slider:SetDecimals(0) sv_distance_slider:SetMax(64000) + sv_distance_slider:SetSize(400,30) + sv_distance_slider:SetConVar("pac_sv_combat_distance_enforced") + sv_distance_slider:SetTooltip("The distance is compared between the action's origin and the player's position.\n0 to ignore.") + + end + + do --hitscan + --[[ + pac_sv_hitscan + pac_sv_hitscan_max_bullets + pac_sv_hitscan_max_damage + pac_sv_hitscan_divide_max_damage_by_max_bullets + ]] + + local hitscans_list = master_list:Add("Hitscans") + hitscans_list.Header:SetSize(40,40) + hitscans_list.Header:SetFont("DermaLarge") + local hitscans_list_list = vgui.Create("DListLayout") + hitscans_list_list:DockPadding(20,0,20,20) + hitscans_list:SetContents(hitscans_list_list) + + local sv_hitscans_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) + sv_hitscans_box:SetText("allow serverside bullets") + sv_hitscans_box:SetSize(400,30) + sv_hitscans_box:SetConVar("pac_sv_hitscan") + + local hitscans_max_dmg_numbox = vgui.Create("DNumSlider", hitscans_list_list) + hitscans_max_dmg_numbox:SetText("Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)") + hitscans_max_dmg_numbox:SetValue(GetConVar("pac_sv_hitscan_max_damage"):GetInt()) + hitscans_max_dmg_numbox:SetMin(0) hitscans_max_dmg_numbox:SetDecimals(0) hitscans_max_dmg_numbox:SetMax(268435455) + hitscans_max_dmg_numbox:SetSize(400,30) + hitscans_max_dmg_numbox:SetConVar("pac_sv_hitscan_max_damage") + + local sv_hitscans_distribute_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) + sv_hitscans_distribute_box:SetText("force hitscans to distribute their total damage accross bullets. if off, every bullet does full damage; if on, adding more bullets doesn't do more damage") + sv_hitscans_distribute_box:SetSize(400,30) + sv_hitscans_distribute_box:SetConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets") + + local hitscans_max_numbullets_numbox = vgui.Create("DNumSlider", hitscans_list_list) + hitscans_max_numbullets_numbox:SetText("Maximum number of bullets for hitscan multishots") + hitscans_max_numbullets_numbox:SetValue(GetConVar("pac_sv_hitscan_max_bullets"):GetInt()) + hitscans_max_numbullets_numbox:SetMin(1) hitscans_max_numbullets_numbox:SetDecimals(0) hitscans_max_numbullets_numbox:SetMax(500) + hitscans_max_numbullets_numbox:SetSize(400,30) + hitscans_max_numbullets_numbox:SetConVar("pac_sv_hitscan_max_bullets") + end + + do --projectiles + local projectiles_list = master_list:Add("Projectiles") + projectiles_list.Header:SetSize(40,40) + projectiles_list.Header:SetFont("DermaLarge") + local projectiles_list_list = vgui.Create("DListLayout") + projectiles_list_list:DockPadding(20,0,20,20) + projectiles_list:SetContents(projectiles_list_list) + + local sv_projectiles_box = vgui.Create("DCheckBoxLabel", projectiles_list_list) + sv_projectiles_box:SetText("allow serverside physical projectiles") + sv_projectiles_box:SetSize(400,30) + sv_projectiles_box:SetConVar("pac_sv_projectiles") + + local projectile_max_phys_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_phys_radius_numbox:SetText("Max projectile physical radius") + projectile_max_phys_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_phys_radius"):GetInt()) + projectile_max_phys_radius_numbox:SetMin(0) projectile_max_phys_radius_numbox:SetDecimals(0) projectile_max_phys_radius_numbox:SetMax(1000) + projectile_max_phys_radius_numbox:SetSize(400,30) + projectile_max_phys_radius_numbox:SetConVar("pac_sv_projectile_max_phys_radius") + + local projectile_max_dmg_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_dmg_radius_numbox:SetText("Max projectile damage radius") + projectile_max_dmg_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage_radius"):GetInt()) + projectile_max_dmg_radius_numbox:SetMin(0) projectile_max_dmg_radius_numbox:SetDecimals(0) projectile_max_dmg_radius_numbox:SetMax(5000) + projectile_max_dmg_radius_numbox:SetSize(400,30) + projectile_max_dmg_radius_numbox:SetConVar("pac_sv_projectile_max_damage_radius") + + local projectile_max_attract_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_attract_radius_numbox:SetText("Max projectile attract radius") + projectile_max_attract_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_attract_radius"):GetInt()) + projectile_max_attract_radius_numbox:SetMin(0) projectile_max_attract_radius_numbox:SetDecimals(0) projectile_max_attract_radius_numbox:SetMax(100000000) + projectile_max_attract_radius_numbox:SetSize(400,30) + projectile_max_attract_radius_numbox:SetConVar("pac_sv_projectile_max_attract_radius") + + local projectile_max_dmg_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_dmg_numbox:SetText("Max projectile damage") + projectile_max_dmg_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage"):GetInt()) + projectile_max_dmg_numbox:SetMin(0) projectile_max_dmg_numbox:SetDecimals(0) projectile_max_dmg_numbox:SetMax(100000000) + projectile_max_dmg_numbox:SetSize(400,30) + projectile_max_dmg_numbox:SetConVar("pac_sv_projectile_max_damage") + + local projectile_max_speed_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_speed_numbox:SetText("Max projectile speed") + projectile_max_speed_numbox:SetValue(GetConVar("pac_sv_projectile_max_speed"):GetInt()) + projectile_max_speed_numbox:SetMin(0) projectile_max_speed_numbox:SetDecimals(0) projectile_max_speed_numbox:SetMax(50000) + projectile_max_speed_numbox:SetSize(400,30) + projectile_max_speed_numbox:SetConVar("pac_sv_projectile_max_speed") + + local projectile_max_mass_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_mass_numbox:SetText("Max projectile mass") + projectile_max_mass_numbox:SetValue(GetConVar("pac_sv_projectile_max_mass"):GetInt()) + projectile_max_mass_numbox:SetMin(0) projectile_max_mass_numbox:SetDecimals(0) projectile_max_mass_numbox:SetMax(500000) + projectile_max_mass_numbox:SetSize(400,30) + projectile_max_mass_numbox:SetConVar("pac_sv_projectile_max_mass") + end + + do --damage zone + local damagezone_list = master_list:Add("Damage Zone") + damagezone_list.Header:SetSize(40,40) + damagezone_list.Header:SetFont("DermaLarge") + local damagezone_list_list = vgui.Create("DListLayout") + damagezone_list_list:DockPadding(20,0,20,20) + damagezone_list:SetContents(damagezone_list_list) + + local sv_dmgzone_box = vgui.Create("DCheckBoxLabel", damagezone_list_list) + sv_dmgzone_box:SetText("Allow damage zone") + sv_dmgzone_box:SetSize(400,30) + sv_dmgzone_box:SetConVar("pac_sv_damage_zone") + + local max_dmgzone_radius_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_radius_numbox:SetText("Max damage zone radius") + max_dmgzone_radius_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_radius"):GetInt()) + max_dmgzone_radius_numbox:SetMin(0) max_dmgzone_radius_numbox:SetDecimals(0) max_dmgzone_radius_numbox:SetMax(32767) + max_dmgzone_radius_numbox:SetSize(400,30) + max_dmgzone_radius_numbox:SetConVar("pac_sv_damage_zone_max_radius") + + local max_dmgzone_length_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_length_numbox:SetText("Max damage zone length") + max_dmgzone_length_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_length"):GetInt()) + max_dmgzone_length_numbox:SetMin(0) max_dmgzone_length_numbox:SetDecimals(0) max_dmgzone_length_numbox:SetMax(32767) + max_dmgzone_length_numbox:SetSize(400,30) + max_dmgzone_length_numbox:SetConVar("pac_sv_damage_zone_max_length") + + local max_dmgzone_damage_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_damage_numbox:SetText("Max damage zone damage") + max_dmgzone_damage_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_damage"):GetInt()) + max_dmgzone_damage_numbox:SetMin(0) max_dmgzone_damage_numbox:SetDecimals(0) max_dmgzone_damage_numbox:SetMax(268435455) + max_dmgzone_damage_numbox:SetSize(400,30) + max_dmgzone_damage_numbox:SetConVar("pac_sv_damage_zone_max_damage") + + local sv_dmgzone_allow_dissolve_box = vgui.Create("DCheckBoxLabel", damagezone_list_list) + sv_dmgzone_allow_dissolve_box:SetText("Allow damage entity dissolvers") + sv_dmgzone_allow_dissolve_box:SetSize(400,30) + sv_dmgzone_allow_dissolve_box:SetConVar("pac_sv_damage_zone_allow_dissolve") + + end + + do --lock part + local lock_list = master_list:Add("Lock part") + lock_list.Header:SetSize(40,40) + lock_list.Header:SetFont("DermaLarge") + local lock_list_list = vgui.Create("DListLayout") + lock_list_list:DockPadding(20,0,20,20) + lock_list:SetContents(lock_list_list) + + local sv_lock_allow_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_allow_box:SetText("Allow lock part") + sv_lock_allow_box:SetSize(400,30) + sv_lock_allow_box:SetConVar("pac_sv_lock") + + local sv_lock_grab_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_box:SetText("Allow lock part grabbing") + sv_lock_grab_box:SetSize(400,30) + sv_lock_grab_box:SetConVar("pac_sv_lock_grab") + + local sv_lock_grab_ply_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_ply_box:SetText("Allow grabbing players") + sv_lock_grab_ply_box:SetSize(400,30) + sv_lock_grab_ply_box:SetConVar("pac_sv_lock_allow_grab_ply") + + local sv_lock_grab_npc_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_npc_box:SetText("Allow grabbing NPCs") + sv_lock_grab_npc_box:SetSize(400,30) + sv_lock_grab_npc_box:SetConVar("pac_sv_lock_allow_grab_npc") + + local sv_lock_grab_ents_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_ents_box:SetText("Allow grabbing other entities") + sv_lock_grab_ents_box:SetSize(400,30) + sv_lock_grab_ents_box:SetConVar("pac_sv_lock_allow_grab_ent") + + local sv_lock_teleport_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_teleport_box:SetText("Allow lock part teleportation") + sv_lock_teleport_box:SetSize(400,30) + sv_lock_teleport_box:SetConVar("pac_sv_lock_teleport") + + local max_lock_radius_numbox = vgui.Create("DNumSlider", lock_list_list) + max_lock_radius_numbox:SetText("Max lock part grab range") + max_lock_radius_numbox:SetValue(GetConVar("pac_sv_lock_max_grab_radius"):GetInt()) + max_lock_radius_numbox:SetMin(0) max_lock_radius_numbox:SetDecimals(0) max_lock_radius_numbox:SetMax(5000) + max_lock_radius_numbox:SetSize(400,30) + max_lock_radius_numbox:SetConVar("pac_sv_lock_max_grab_radius") + end + + do --force + local force_list = master_list:Add("Force part") + force_list.Header:SetSize(40,40) + force_list.Header:SetFont("DermaLarge") + local force_list_list = vgui.Create("DListLayout") + force_list_list:DockPadding(20,0,20,20) + force_list:SetContents(force_list_list) + + local sv_force_box = vgui.Create("DCheckBoxLabel", force_list_list) + sv_force_box:SetText("Allow force part") + sv_force_box:SetSize(400,30) + sv_force_box:SetConVar("pac_sv_force") + + local max_force_radius_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_radius_numbox:SetText("Max force part radius") + max_force_radius_numbox:SetValue(GetConVar("pac_sv_force_max_radius"):GetInt()) + max_force_radius_numbox:SetMin(0) max_force_radius_numbox:SetDecimals(0) max_force_radius_numbox:SetMax(32767) + max_force_radius_numbox:SetSize(400,30) + max_force_radius_numbox:SetConVar("pac_sv_force_max_radius") + + local max_force_length_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_length_numbox:SetText("Max force part length") + max_force_length_numbox:SetValue(GetConVar("pac_sv_force_max_length"):GetInt()) + max_force_length_numbox:SetMin(0) max_force_length_numbox:SetDecimals(0) max_force_length_numbox:SetMax(32767) + max_force_length_numbox:SetSize(400,30) + max_force_length_numbox:SetConVar("pac_sv_force_max_length") + + local max_force_amount_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_amount_numbox:SetText("Max force part amount") + max_force_amount_numbox:SetValue(GetConVar("pac_sv_force_max_amount"):GetInt()) + max_force_amount_numbox:SetMin(0) max_force_amount_numbox:SetDecimals(0) max_force_amount_numbox:SetMax(10000000) + max_force_amount_numbox:SetSize(400,30) + max_force_amount_numbox:SetConVar("pac_sv_force_max_amount") + end + + do --health_modifier + local healthmod_list = master_list:Add("Health modifier part") + healthmod_list.Header:SetSize(40,40) + healthmod_list.Header:SetFont("DermaLarge") + local healthmod_list_list = vgui.Create("DListLayout") + healthmod_list_list:DockPadding(20,0,20,20) + healthmod_list:SetContents(healthmod_list_list) + + local sv_healthmod_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + sv_healthmod_box:SetText("Allow health modifier part") + sv_healthmod_box:SetSize(400,30) + sv_healthmod_box:SetConVar("pac_sv_health_modifier") + + local healthmod_extrabars_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + healthmod_extrabars_box:SetText("Allow changing max health and max armor") + healthmod_extrabars_box:SetSize(400,30) + healthmod_extrabars_box:SetConVar("pac_sv_health_modifier_allow_maxhp") + + local min_healthmod_dmgmult_box = vgui.Create("DNumSlider", healthmod_list_list) + min_healthmod_dmgmult_box:SetText("Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.") + min_healthmod_dmgmult_box:SetValue(GetConVar("pac_sv_health_modifier_min_damagescaling"):GetInt()) + min_healthmod_dmgmult_box:SetMin(-10) min_healthmod_dmgmult_box:SetDecimals(2) min_healthmod_dmgmult_box:SetMax(1) + min_healthmod_dmgmult_box:SetSize(400,30) + min_healthmod_dmgmult_box:SetConVar("pac_sv_health_modifier_min_damagescaling") + + local healthmod_extrabars_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + healthmod_extrabars_box:SetText("Allow extra healthbars") + healthmod_extrabars_box:SetSize(400,30) + healthmod_extrabars_box:SetConVar("pac_sv_health_modifier_extra_bars") + healthmod_extrabars_box:SetToolTip("What are those? It's like an armor layer that takes damage before it gets applied to the entity.") + end + return master_list +end + +function pace.FillServerSettings(pnl) + local pnl = pnl + + local master_list = vgui.Create("DCategoryList", pnl) + master_list:Dock(FILL) + + --models/entity + --[[ + pac_allow_blood_color + pac_allow_mdl + pac_allow_mdl_entity + pac_modifier_model + pac_modifier_size + ]] + + local model_category = master_list:Add("Allowed Playermodel Mutations") + model_category.Header:SetSize(40,40) + model_category.Header:SetFont("DermaLarge") + local model_category_list = vgui.Create("DListLayout") + model_category_list:DockPadding(20,0,20,20) + model_category:SetContents(model_category_list) + + local pac_allow_blood_color_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_blood_color_box:SetText("Blood") + pac_allow_blood_color_box:SetSize(400,30) + pac_allow_blood_color_box:SetConVar("pac_allow_blood_color") + model_category_list:Add(pac_allow_blood_color_box) + local pac_allow_mdl_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_mdl_box:SetText("MDL") + pac_allow_mdl_box:SetSize(400,30) + pac_allow_mdl_box:SetConVar("pac_allow_mdl") + model_category_list:Add(pac_allow_mdl_box) + local pac_allow_mdl_entity_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_mdl_entity_box:SetText("Entity MDL") + pac_allow_mdl_entity_box:SetSize(400,30) + pac_allow_mdl_entity_box:SetConVar("pac_allow_mdl_entity") + model_category_list:Add(pac_allow_mdl_entity_box) + local pac_modifier_model_box = vgui.Create("DCheckBoxLabel", master_list) + pac_modifier_model_box:SetText("Entity model") + pac_modifier_model_box:SetSize(400,30) + pac_modifier_model_box:SetConVar("pac_modifier_model") + model_category_list:Add(pac_modifier_model_box) + local pac_modifier_size_box = vgui.Create("DCheckBoxLabel", master_list) + pac_modifier_size_box:SetText("Entity size") + pac_modifier_size_box:SetSize(400,30) + pac_modifier_size_box:SetConVar("pac_modifier_size") + model_category_list:Add(pac_modifier_size_box) + + --movement and mass + --[[ + pac_free_movement + ]] + + local movement_category = master_list:Add("Player Movement") + movement_category.Header:SetSize(40,40) + movement_category.Header:SetFont("DermaLarge") + local movement_category_list = vgui.Create("DListLayout") + movement_category_list:DockPadding(20,20,20,20) + movement_category:SetContents(movement_category_list) + + local pac_allow_movement_form = vgui.Create("DComboBox", movement_category_list) + pac_allow_movement_form:SetText("Allow PAC player movement") + --pac_allow_movement_form:SetSize(400,20) + pac_allow_movement_form:SetSortItems(false) + + pac_allow_movement_form:AddChoice("disabled") + pac_allow_movement_form:AddChoice("disabled if noclip not allowed") + pac_allow_movement_form:AddChoice("enabled") + + pac_allow_movement_form.OnSelect = function(_, _, value) + if value == "disabled" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("0") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is disabled.") + elseif value == "disabled if noclip not allowed" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("-1") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is disabled if noclip is not allowed.") + elseif value == "enabled" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("1") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is enabled.") + end + end + + --mode:ChooseOption(mode_str) + + local pac_player_movement_allow_mass_box = vgui.Create("DCheckBoxLabel", movement_category_list) + pac_player_movement_allow_mass_box:SetText("Allow Modify Mass") + pac_player_movement_allow_mass_box:SetSize(400,30) + movement_category_list:Add(pac_player_movement_allow_mass_box) + pac_player_movement_allow_mass_box:SetConVar("pac_player_movement_allow_mass") + + local playermovement_min_mass_numbox = vgui.Create("DNumSlider", movement_category_list) + playermovement_min_mass_numbox:SetText("Mimnimum mass players can set for themselves") + playermovement_min_mass_numbox:SetValue(GetConVar("pac_player_movement_min_mass"):GetFloat()) + playermovement_min_mass_numbox:SetMin(0.01) playermovement_min_mass_numbox:SetDecimals(0) playermovement_min_mass_numbox:SetMax(1000000) + playermovement_min_mass_numbox:SetSize(400,30) + movement_category_list:Add(playermovement_min_mass_numbox) + playermovement_min_mass_numbox:SetConVar("pac_player_movement_min_mass") + + + local playermovement_max_mass_numbox = vgui.Create("DNumSlider", movement_category_list) + playermovement_max_mass_numbox:SetText("Maximum mass players can set for themselves") + playermovement_max_mass_numbox:SetValue(GetConVar("pac_player_movement_max_mass"):GetFloat()) + playermovement_max_mass_numbox:SetMin(0.01) playermovement_max_mass_numbox:SetDecimals(0) playermovement_max_mass_numbox:SetMax(1000000) + playermovement_max_mass_numbox:SetSize(400,30) + movement_category_list:Add(playermovement_max_mass_numbox) + playermovement_max_mass_numbox:SetConVar("pac_player_movement_max_mass") + + + local pac_player_movement_allow_mass_dmgscaling_box = vgui.Create("DCheckBoxLabel", movement_category_list) + pac_player_movement_allow_mass_dmgscaling_box:SetText("Allow damage scaling of physics damage based on player's mass") + pac_player_movement_allow_mass_dmgscaling_box:SetSize(400,30) + movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) + pac_player_movement_allow_mass_dmgscaling_box:SetConVar("pac_player_movement_physics_damage_scaling") + movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) + + + --wear limits and bans + --[[ + pac_sv_draw_distance + pac_sv_hide_outfit_on_death WORKSHOP DEPRECATED + pac_submit_limit + pac_submit_spam + pac_ban + pac_unban + ]] + + local wear_list = master_list:Add("Server wearing/drawing") + wear_list.Header:SetSize(40,40) + wear_list.Header:SetFont("DermaLarge") + local draw_distance_list = vgui.Create("DListLayout") + draw_distance_list:DockPadding(20,0,20,20) + wear_list:SetContents(draw_distance_list) + + local draw_dist_numbox = vgui.Create("DNumSlider", draw_distance_list) + draw_dist_numbox:SetText("Server draw distance") + draw_dist_numbox:SetValue(GetConVar("pac_sv_draw_distance"):GetInt()) + draw_dist_numbox:SetMin(0) draw_dist_numbox:SetDecimals(0) draw_dist_numbox:SetMax(50000) + draw_dist_numbox:SetSize(400,30) + draw_dist_numbox:SetConVar("pac_sv_draw_distance") + + local pac_submit_limit_numbox = vgui.Create("DNumSlider", draw_distance_list) + pac_submit_limit_numbox:SetText("pac_submit limit") + pac_submit_limit_numbox:SetValue(GetConVar("pac_submit_limit"):GetInt()) + pac_submit_limit_numbox:SetMin(0) pac_submit_limit_numbox:SetDecimals(0) pac_submit_limit_numbox:SetMax(100) + pac_submit_limit_numbox:SetSize(400,30) + pac_submit_limit_numbox:SetConVar("pac_submit_limit") + + local pac_submit_spam_box = vgui.Create("DCheckBoxLabel", draw_distance_list) + pac_submit_spam_box:SetText("prevent pac_submit spam") + pac_submit_spam_box:SetSize(400,30) + pac_submit_spam_box:SetConVar("pac_submit_spam") + + + + --misc + --[[ + sv_pac_webcontent_allow_no_content_length + sv_pac_webcontent_limit + pac_to_contraption_allow + pac_max_contraption_entities + pac_restrictions + ]] + local misc_list = master_list:Add("Misc") + misc_list.Header:SetSize(40,40) + misc_list.Header:SetFont("DermaLarge") + local misc_list_list = vgui.Create("DListLayout") + misc_list_list:DockPadding(20,0,20,20) + misc_list:SetContents(misc_list_list) + local webcontent_no_content_box = vgui.Create("DCheckBoxLabel", misc_list_list) + webcontent_no_content_box:SetText("allow downloads with no content length") + webcontent_no_content_box:SetSize(400,30) + webcontent_no_content_box:SetConVar("sv_pac_webcontent_allow_no_content_length") + + local contraption_box = vgui.Create("DCheckBoxLabel", misc_list_list) + contraption_box:SetText("allow contraptions") + contraption_box:SetSize(400,30) + contraption_box:SetConVar("pac_to_contraption_allow") + + local contraption_entities_numbox = vgui.Create("DNumSlider", misc_list_list) + contraption_entities_numbox:SetText("PAC3 contraption entities limit") + contraption_entities_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + contraption_entities_numbox:SetMin(0) contraption_entities_numbox:SetDecimals(0) contraption_entities_numbox:SetMax(200) + contraption_entities_numbox:SetSize(400,30) + contraption_entities_numbox:SetConVar("pac_max_contraption_entities") + + local cam_restrict_box = vgui.Create("DCheckBoxLabel", misc_list_list) + cam_restrict_box:SetText("restrict PAC editor camera movement") + cam_restrict_box:SetSize(400,30) + cam_restrict_box:SetConVar("pac_restrictions") + + + return master_list +end + + +--part order, shortcuts +function pace.FillEditorSettings(pnl) + + local buildlist_partmenu = {} + local f = vgui.Create( "DPanel", pnl ) + f:SetSize(800) + f:Center() + + local LeftPanel = vgui.Create( "DPanel", f ) -- Can be any panel, it will be stretched + + local partmenu_order_presets = vgui.Create("DComboBox",LeftPanel) + partmenu_order_presets:SetText("Select a part menu preset") + partmenu_order_presets:AddChoice("factory preset") + partmenu_order_presets:AddChoice("expanded PAC4.5 preset") + partmenu_order_presets:AddChoice("bulk select poweruser") + partmenu_order_presets:AddChoice("user preset") + partmenu_order_presets:SetX(10) partmenu_order_presets:SetY(10) + partmenu_order_presets:SetWidth(200) + partmenu_order_presets:SetHeight(20) + + local partmenu_apply_button = vgui.Create("DButton", LeftPanel) + partmenu_apply_button:SetText("Apply") + partmenu_apply_button:SetX(220) + partmenu_apply_button:SetY(10) + partmenu_apply_button:SetWidth(65) + partmenu_apply_button:SetImage('icon16/accept.png') + + local partmenu_clearlist_button = vgui.Create("DButton", LeftPanel) + partmenu_clearlist_button:SetText("Clear") + partmenu_clearlist_button:SetX(285) + partmenu_clearlist_button:SetY(10) + partmenu_clearlist_button:SetWidth(65) + partmenu_clearlist_button:SetImage('icon16/application_delete.png') + + local partmenu_savelist_button = vgui.Create("DButton", LeftPanel) + partmenu_savelist_button:SetText("Save") + partmenu_savelist_button:SetX(350) + partmenu_savelist_button:SetY(10) + partmenu_savelist_button:SetWidth(70) + partmenu_savelist_button:SetImage('icon16/disk.png') + + + + local partmenu_choices = vgui.Create("DScrollPanel", LeftPanel) + local partmenu_choices_textAdd = vgui.Create("DLabel", LeftPanel) + partmenu_choices_textAdd:SetText("ADD MENU COMPONENTS") + partmenu_choices_textAdd:SetFont("DermaDefaultBold") + partmenu_choices_textAdd:SetColor(Color(0,200,0)) + partmenu_choices_textAdd:SetWidth(200) + partmenu_choices_textAdd:SetX(10) + partmenu_choices_textAdd:SetY(30) + + local partmenu_choices_textRemove = vgui.Create("DLabel", LeftPanel) + partmenu_choices_textRemove:SetText("DOUBLE CLICK TO REMOVE") + partmenu_choices_textRemove:SetColor(Color(200,0,0)) + partmenu_choices_textRemove:SetFont("DermaDefaultBold") + partmenu_choices_textRemove:SetWidth(200) + partmenu_choices_textRemove:SetX(220) + partmenu_choices_textRemove:SetY(30) + + local partmenu_previews = vgui.Create("DListView", LeftPanel) + partmenu_previews:AddColumn("index") + partmenu_previews:AddColumn("control name") + partmenu_previews:SetSortable(false) + partmenu_previews:SetX(220) + partmenu_previews:SetY(50) + partmenu_previews:SetHeight(320) + partmenu_previews:SetWidth(200) + + + + local shortcutaction_choices = vgui.Create("DComboBox", LeftPanel) + shortcutaction_choices:SetText("Select a PAC action") + for _,name in ipairs(pace.PACActionShortcut_Dictionary) do + shortcutaction_choices:AddChoice(name) + end + shortcutaction_choices:SetX(10) shortcutaction_choices:SetY(400) + shortcutaction_choices:SetWidth(170) + shortcutaction_choices:SetHeight(20) + shortcutaction_choices:ChooseOptionID(1) + + function shortcutaction_choices:Think() + self.next = self.next or 0 + self.found = self.found or false + if self.next < RealTime() then self.found = false end + if self:IsHovered() then + if input.IsKeyDown(KEY_UP) then + if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() + 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end + elseif input.IsKeyDown(KEY_DOWN) then + if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() - 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end + else self.found = false end + else self.found = false + end + end + + local shortcuts_description_text = vgui.Create("DLabel", LeftPanel) + shortcuts_description_text:SetFont("DermaDefaultBold") + shortcuts_description_text:SetText("Edit keyboard shortcuts") + shortcuts_description_text:SetColor(Color(0,0,0)) + shortcuts_description_text:SetWidth(200) + shortcuts_description_text:SetX(10) + shortcuts_description_text:SetY(380) + + local shortcutaction_presets = vgui.Create("DComboBox", LeftPanel) + shortcutaction_presets:SetText("Select a shortcut preset") + shortcutaction_presets:AddChoice("factory preset", pace.PACActionShortcut_Default) + shortcutaction_presets:AddChoice("no CTRL preset", pace.PACActionShortcut_NoCTRL) + shortcutaction_presets:AddChoice("Cedric's preset", pace.PACActionShortcut_Cedric) + + for i,filename in ipairs(file.Find("pac3_config/pac_editor_shortcuts*.txt","DATA")) do + local data = file.Read("pac3_config/" .. filename, "DATA") + shortcutaction_presets:AddChoice(string.GetFileFromFilename(filename), util.KeyValuesToTable(data)) + end + + shortcutaction_presets:SetX(10) shortcutaction_presets:SetY(420) + shortcutaction_presets:SetWidth(170) + shortcutaction_presets:SetHeight(20) + function shortcutaction_presets:OnSelect(num, name, data) + pace.PACActionShortcut = data + pace.FlashNotification("Selected shortcut preset: " .. name .. ". View console for more info") + pac.Message("Selected shortcut preset: " .. name) + for i,v in pairs(data) do + if #v > 0 then MsgC(Color(50,250,50), i .. "\n") end + for i2,v2 in pairs(v) do + MsgC(Color(0,250,250), "\t" .. table.concat(v2, "+") .. "\n") + end + end + end + + + local shortcutaction_choices_textCurrentShortcut = vgui.Create("DLabel", LeftPanel) + shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit:") + shortcutaction_choices_textCurrentShortcut:SetColor(Color(0,60,160)) + shortcutaction_choices_textCurrentShortcut:SetWidth(200) + shortcutaction_choices_textCurrentShortcut:SetX(200) + shortcutaction_choices_textCurrentShortcut:SetY(420) + + + local shortcutaction_index = vgui.Create("DNumberWang", LeftPanel) + shortcutaction_index:SetToolTip("index") + shortcutaction_index:SetValue(1) + shortcutaction_index:SetMin(1) + shortcutaction_index:SetMax(10) + shortcutaction_index:SetWidth(30) + shortcutaction_index:SetHeight(20) + shortcutaction_index:SetX(180) + shortcutaction_index:SetY(400) + + local function update_shortcutaction_choices_textCurrentShortcut(num) + shortcutaction_choices_textCurrentShortcut:SetText("") + num = tonumber(num) + local action, val = shortcutaction_choices:GetSelected() + local strs = {} + + if action and action ~= "" then + if pace.PACActionShortcut[action] and pace.PACActionShortcut[action][num] then + for i,v in ipairs(pace.PACActionShortcut[action][num]) do + strs[i] = v + end + shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit: " .. table.concat(strs, " + ")) + else + shortcutaction_choices_textCurrentShortcut:SetText("") + end + end + end + update_shortcutaction_choices_textCurrentShortcut(1) + + function shortcutaction_index:OnValueChanged(num) + update_shortcutaction_choices_textCurrentShortcut(num) + end + + function shortcutaction_choices:OnSelect(i, action) + shortcutaction_index:OnValueChanged(shortcutaction_index:GetValue()) + end + + local binder1 = vgui.Create("DBinder", LeftPanel) + binder1:SetX(10) + binder1:SetY(440) + binder1:SetHeight(30) + binder1:SetWidth(90) + function binder1:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 1: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 1: "..input.GetKeyName( num )) + end + + local binder2 = vgui.Create("DBinder", LeftPanel) + binder2:SetX(105) + binder2:SetY(440) + binder2:SetHeight(30) + binder2:SetWidth(90) + function binder2:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 2: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 2: "..input.GetKeyName( num )) + end + + local binder3 = vgui.Create("DBinder", LeftPanel) + binder3:SetX(200) + binder3:SetY(440) + binder3:SetHeight(30) + binder3:SetWidth(90) + function binder3:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 3: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 3: "..input.GetKeyName( num )) + end + + local function send_active_shortcut_to_assign(tbl) + local action = shortcutaction_choices:GetValue() + local index = shortcutaction_index:GetValue() + + if not tbl then + pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {} + pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {} + + if table.IsEmpty(pace.PACActionShortcut[action][index]) then + pace.PACActionShortcut[action][index] = nil + if table.IsEmpty(pace.PACActionShortcut[action]) then + pace.PACActionShortcut[action] = nil + end + else + pace.PACActionShortcut[action][index] = nil + end + elseif not table.IsEmpty(tbl) then + pace.AssignEditorShortcut(shortcutaction_choices:GetValue(), tbl, shortcutaction_index:GetValue()) + end + encode_table_to_file("pac_editor_shortcuts") + end + + local bindclear = vgui.Create("DButton", LeftPanel) + bindclear:SetText("clear") + bindclear:SetTooltip("deletes the current shortcut at the current index") + bindclear:SetX(10) + bindclear:SetY(480) + bindclear:SetHeight(30) + bindclear:SetWidth(90) + bindclear:SetColor(Color(200,0,0)) + bindclear:SetIcon("icon16/keyboard_delete.png") + function bindclear:DoClick() + binder1:SetSelectedNumber(0) + binder2:SetSelectedNumber(0) + binder3:SetSelectedNumber(0) + send_active_shortcut_to_assign() + update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue()) + end + + local bindoverwrite = vgui.Create("DButton", LeftPanel) + bindoverwrite:SetText("confirm") + bindoverwrite:SetTooltip("applies the current shortcut combination at the current index") + bindoverwrite:SetX(105) + bindoverwrite:SetY(480) + bindoverwrite:SetHeight(30) + bindoverwrite:SetWidth(90) + bindoverwrite:SetColor(Color(0,200,0)) + bindoverwrite:SetIcon("icon16/disk.png") + function bindoverwrite:DoClick() + local tbl = {} + local i = 1 + --print(binder1:GetValue(), binder2:GetValue(), binder3:GetValue()) + if binder1:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder1:GetValue()) i = i + 1 end + if binder2:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder2:GetValue()) i = i + 1 end + if binder3:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder3:GetValue()) end + if not table.IsEmpty(tbl) then + pace.FlashNotification("Combo " .. shortcutaction_index:GetValue() .. " committed: " .. table.concat(tbl," ")) + if not pace.PACActionShortcut[shortcutaction_choices:GetValue()] then + pace.PACActionShortcut[shortcutaction_choices:GetValue()] = {} + end + send_active_shortcut_to_assign(tbl) + update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue()) + end + encode_table_to_file("pac_editor_shortcuts") + end + + function bindoverwrite:DoRightClick() + Derma_StringRequest("Save preset", "Save a keyboard shortcuts preset?", "pac_editor_shortcuts", + function(name) file.Write("pac3_config/"..name..".txt", util.TableToKeyValues(pace.PACActionShortcut)) + shortcutaction_presets:AddChoice(name..".txt") + end + ) + end + + local bindcapture_text = vgui.Create("DLabel", LeftPanel) + bindcapture_text:SetFont("DermaDefaultBold") + bindcapture_text:SetText("") + bindcapture_text:SetColor(Color(0,0,0)) + bindcapture_text:SetX(300) + bindcapture_text:SetY(480) + bindcapture_text:SetSize(300, 30) + + function bindcapture_text:Think() + self:SetText(pace.bindcapturelabel_text) + end + local bindcapture = vgui.Create("DButton", LeftPanel) + bindcapture:SetText("capture input") + bindcapture:SetX(200) + bindcapture:SetY(480) + bindcapture:SetHeight(30) + bindcapture:SetWidth(90) + pace.bindcapturelabel_text = "" + function bindcapture:DoClick() + pace.delayshortcuts = RealTime() + 5 + local input_active = {} + local no_input = true + local inputs_str = "" + local previous_inputs_str = "" + pace.FlashNotification("Recording input... Release one key when you're done") + + hook.Add("Tick", "pace_buttoncapture_countdown", function() + pace.delayshortcuts = RealTime() + 5 + local inputs_tbl = {} + inputs_str = "" + for i=1,172,1 do --build bool list of all current keys + if input.IsKeyDown(i) then + input_active[i] = true + inputs_tbl[i] = true + no_input = false + inputs_str = inputs_str .. input.GetKeyName(i) .. " " + else + input_active[i] = false + end + end + pace.bindcapturelabel_text = "Recording input:\n" .. inputs_str + + if previous_inputs_tbl and table.Count(previous_inputs_tbl) > 0 then + if table.Count(inputs_tbl) < table.Count(previous_inputs_tbl) then + pace.FlashNotification("ending input!" .. previous_inputs_str) + + local tbl = {} + local i = 1 + for key,bool in pairs(previous_inputs_tbl) do + tbl[i] = input.GetKeyName(key) + i = i + 1 + end + --print(shortcutaction_choices:GetValue(), shortcutaction_index:GetValue()) + pace.AssignEditorShortcut(shortcutaction_choices:GetValue(), tbl, shortcutaction_index:GetValue()) + --pace.PACActionShortcut[shortcutaction_choices:GetValue()][shortcutaction_index:GetValue()] = tbl + pace.delayshortcuts = RealTime() + 5 + pace.bindcapturelabel_text = "Recorded input:\n" .. previous_inputs_str + hook.Remove("Tick", "pace_buttoncapture_countdown") + end + end + previous_inputs_str = inputs_str + previous_inputs_tbl = inputs_tbl + end) + + end + + local bulkbinder = vgui.Create("DBinder", LeftPanel) + function bulkbinder:OnChange( num ) + GetConVar("pac_bulk_select_key"):SetString(input.GetKeyName( num )) + end + bulkbinder:SetX(210) + bulkbinder:SetY(400) + bulkbinder:SetSize(80,20) + bulkbinder:SetText("bulk select key") + + local function ClearPartMenuPreviewList() + local i = 0 + while (partmenu_previews:GetLine(i + 1) ~= nil) do + i = i+1 + end + for v=i,0,-1 do + if partmenu_previews:GetLine(v) ~= nil then partmenu_previews:RemoveLine(v) end + v = v - 1 + 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' + 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)) + + function pnl:DoClick() + table.insert(buildlist_partmenu,v) + partmenu_previews:AddLine(#buildlist_partmenu,v) + end + partmenu_choices:AddItem(pnl) + pnl:SetHeight(18) + pnl:SetWidth(200) + pnl:SetY(20*(i-1)) + end + + partmenu_choices:SetWidth(200) + partmenu_choices:SetHeight(320) + partmenu_choices:SetVerticalScrollbarEnabled(true) + + + local RightPanel = vgui.Create( "DTree", f ) + Test_Node = RightPanel:AddNode( "Test", "icon16/world.png" ) + test_part = pac.CreatePart("base") //the menu needs a part to get its full version in preview + function RightPanel:DoRightClick() + temp_list = pace.operations_order + pace.operations_order = buildlist_partmenu + pace.OnPartMenu(test_part) + temp_list = pace.operations_order + pace.operations_order = temp_list + end + function RightPanel:DoClick() + temp_list = pace.operations_order + pace.operations_order = buildlist_partmenu + pace.OnPartMenu(test_part) + temp_list = pace.operations_order + pace.operations_order = temp_list + end + test_part:Remove() //dumb workaround but it works + + + local div = vgui.Create( "DHorizontalDivider", f ) + div:Dock( FILL ) + div:SetLeft( LeftPanel ) + div:SetRight( RightPanel ) + + div:SetDividerWidth( 8 ) + div:SetLeftMin( 50 ) + div:SetRightMin( 50 ) + div:SetLeftWidth( 450 ) + partmenu_order_presets.OnSelect = function( self, index, value ) + local temp_list = {"wear","save","load"} + if value == "factory preset" then + temp_list = table.Copy(pace.operations_default) + elseif value == "expanded PAC4.5 preset" then + temp_list = table.Copy(pace.operations_experimental) + elseif value == "bulk select poweruser" then + temp_list = table.Copy(pace.operations_bulk_poweruser) + elseif value == "user preset" then + temp_list = pace.operations_order + end + ClearPartMenuPreviewList() + for i,v in ipairs(temp_list) do + partmenu_previews:AddLine(i,v) + end + buildlist_partmenu = temp_list + end + + function partmenu_apply_button:DoClick() + pace.operations_order = buildlist_partmenu + end + + function partmenu_clearlist_button:DoClick() + ClearPartMenuPreviewList() + buildlist_partmenu = {} + end + + function partmenu_savelist_button:DoClick() + encode_table_to_file("pac_editor_partmenu_layouts") + end + + function partmenu_previews:DoDoubleClick(id, line) + table.remove(buildlist_partmenu,id) + + ClearPartMenuPreviewList() + for i,v in ipairs(buildlist_partmenu) do + partmenu_previews:AddLine(i,v) + end + + PrintTable(buildlist_partmenu) + end + + + if pace.operations_order then + for i,v in pairs(pace.operations_order) do + table.insert(buildlist_partmenu,v) + partmenu_previews:AddLine(#buildlist_partmenu,v) + end + end + + return f +end + +--camera movement +function pace.FillEditorSettings2(pnl) + local panel = vgui.Create( "DPanel", pnl ) + --[[ movement binds + CreateConVar("pac_editor_camera_forward_bind", "w") + + CreateConVar("pac_editor_camera_back_bind", "s") + + CreateConVar("pac_editor_camera_moveleft_bind", "a") + + CreateConVar("pac_editor_camera_moveright_bind", "d") + + CreateConVar("pac_editor_camera_up_bind", "space") + + CreateConVar("pac_editor_camera_down_bind", "") + + ]] + + --[[pace.camera_movement_binds = { + ["forward"] = pace.camera_forward_bind, + ["back"] = pace.camera_back_bind, + ["moveleft"] = pace.camera_moveleft_bind, + ["moveright"] = pace.camera_moveright_bind, + ["up"] = pace.camera_up_bind, + ["down"] = pace.camera_down_bind, + ["slow"] = pace.camera_slow_bind, + ["speed"] = pace.camera_speed_bind + } + ]] + + local LeftPanel = vgui.Create( "DPanel", panel ) -- Can be any panel, it will be stretched + local RightPanel = vgui.Create( "DPanel", panel ) -- Can be any panel, it will be stretched + LeftPanel:SetSize(300,600) + RightPanel:SetSize(300,600) + local div = vgui.Create( "DHorizontalDivider", panel ) + div:Dock( FILL ) + div:SetLeft( LeftPanel ) + div:SetRight( RightPanel ) + + div:SetDividerWidth( 8 ) + div:SetLeftMin( 50 ) + div:SetRightMin( 50 ) + div:SetLeftWidth( 400 ) + + local movement_binders_label = vgui.Create("DLabel", LeftPanel) + movement_binders_label:SetText("PAC editor camera movement") + movement_binders_label:SetFont("DermaDefaultBold") + movement_binders_label:SetColor(Color(0,0,0)) + movement_binders_label:SetSize(200,40) + movement_binders_label:SetPos(30,5) + + local forward_binder = vgui.Create("DBinder", LeftPanel) + forward_binder:SetSize(40,40) + forward_binder:SetPos(100,40) + forward_binder:SetTooltip("move forward") + forward_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["forward"]:GetString())) + function forward_binder:OnChange(num) + pace.camera_movement_binds["forward"]:SetString(input.GetKeyName( num )) + end + + local back_binder = vgui.Create("DBinder", LeftPanel) + back_binder:SetSize(40,40) + back_binder:SetPos(100,80) + back_binder:SetTooltip("move back") + back_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["back"]:GetString())) + function back_binder:OnChange(num) + pace.camera_movement_binds["back"]:SetString(input.GetKeyName( num )) + end + + local moveleft_binder = vgui.Create("DBinder", LeftPanel) + moveleft_binder:SetSize(40,40) + moveleft_binder:SetPos(60,80) + moveleft_binder:SetTooltip("move left") + moveleft_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveleft"]:GetString())) + function moveleft_binder:OnChange(num) + pace.camera_movement_binds["moveleft"]:SetString(input.GetKeyName( num )) + end + + local moveright_binder = vgui.Create("DBinder", LeftPanel) + moveright_binder:SetSize(40,40) + moveright_binder:SetPos(140,80) + moveright_binder:SetTooltip("move right") + moveright_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveright"]:GetString())) + function moveright_binder:OnChange(num) + pace.camera_movement_binds["moveright"]:SetString(input.GetKeyName( num )) + end + + local up_binder = vgui.Create("DBinder", LeftPanel) + up_binder:SetSize(40,40) + up_binder:SetPos(180,40) + up_binder:SetTooltip("move up") + up_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["up"]:GetString())) + function up_binder:OnChange(num) + pace.camera_movement_binds["up"]:SetString(input.GetKeyName( num )) + end + + local down_binder = vgui.Create("DBinder", LeftPanel) + down_binder:SetSize(40,40) + down_binder:SetPos(180,80) + down_binder:SetTooltip("move down") + down_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["down"]:GetString())) + function down_binder:OnChange(num) + print(num, input.GetKeyName( num )) + pace.camera_movement_binds["down"]:SetString(input.GetKeyName( num )) + end + + local slow_binder = vgui.Create("DBinder", LeftPanel) + slow_binder:SetSize(40,40) + slow_binder:SetPos(20,80) + slow_binder:SetTooltip("go slow") + slow_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["slow"]:GetString())) + function slow_binder:OnChange(num) + pace.camera_movement_binds["slow"]:SetString(input.GetKeyName( num )) + end + + local speed_binder = vgui.Create("DBinder", LeftPanel) + speed_binder:SetSize(40,40) + speed_binder:SetPos(20,40) + speed_binder:SetTooltip("go fast") + speed_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["speed"]:GetString())) + function speed_binder:OnChange(num) + pace.camera_movement_binds["speed"]:SetString(input.GetKeyName( num )) + end + + --[[pace.partmenu_categories_cedrics = + { + ["new!"] = + { + ["icon"] = "icon16/new.png", + ["interpolated_multibone"]= "interpolated_multibone", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + }, + ["logic"] = + { + ["icon"] = "icon16/server_chart.png", + ["proxy"] = "proxy", + ["command"] = "command", + ["event"] = "event", + ["text"] = "text", + ["link"] = "link", + },]] + local Parts = pac.GetRegisteredParts() + local function get_icon(str, fallback) + if str then + if pace.MiscIcons[string.gsub(str, "pace.MiscIcons.", "")] then + return pace.MiscIcons[string.gsub(str, "pace.MiscIcons.", "")] + else + local img = string.gsub(str, ".png", "") --remove the png extension + img = string.gsub(img, "icon16/", "") --remove the icon16 base path + img = "icon16/" .. img .. ".png" --why do this? to be able to write any form and let the program fix the form + return img + end + elseif Parts[fallback] then + return Parts[fallback].Icon + else + return "icon16/page_white.png" + end + + end + + local categorytree = vgui.Create("DTree", RightPanel) + categorytree:SetY(30) + categorytree:SetSize(360,400) + + local function class_partnode_add(parentnode, class) + if Parts[class] then + for i,v in ipairs(parentnode:GetChildNodes()) do --can't make duplicates so remove to place it at the end + if v:GetText() == class then v:Remove() end + end + + local part_node = parentnode:AddNode(class) + part_node:SetIcon(get_icon(nil, class)) + part_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("remove", function() part_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + end + end + local function bring_up_partclass_list(cat_node) + + --function from pace.OnAddPartMenu(obj) + local base = vgui.Create("EditablePanel") + base:SetPos(input.GetCursorPos()) + base:SetSize(200, 300) + + base:MakePopup() + + function base:OnRemove() + pac.RemoveHook("VGUIMousePressed", "search_part_menu") + end + + local edit = base:Add("DTextEntry") + edit:SetTall(20) + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + + local result = base:Add("DScrollPanel") + result:Dock(FILL) + + function edit:OnEnter() + if result.found[1] then + class_partnode_add(cat_node, result.found[1].ClassName) + end + base:Remove() + end + + edit.OnValueChange = function(_, str) + result:Clear() + result.found = {} + + for _, part in ipairs(pace.GetRegisteredParts()) do + if (part.FriendlyName or part.ClassName):find(str, nil, true) then + table.insert(result.found, part) + end + end + + table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end) + + for _, part in ipairs(result.found) do + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + line.DoClick = function() + class_partnode_add(cat_node, part.ClassName) + end + + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + if part.Icon then + btn:SetImage(part.Icon) + end + + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText((part.FriendlyName or part.ClassName):Replace('_', ' ')) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() + + line:Dock(TOP) + end + + --base:SetHeight(20 * #result.found + edit:GetTall()) + base:SetHeight(600 + edit:GetTall()) + + end + + edit:OnValueChange("") + + pac.AddHook("VGUIMousePressed", "search_part_menu", function(pnl, code) + if code == MOUSE_LEFT or code == MOUSE_RIGHT then + if not base:IsOurChild(pnl) then + base:Remove() + end + end + end) + end + local function bring_up_category_icon_browser(category_node) + local master_frame = vgui.Create("DFrame") + master_frame:SetPos(input.GetCursorPos()) + master_frame:SetSize(400,400) + + local browser = vgui.Create("DIconBrowser", master_frame) + function browser:OnChange() + category_node:SetIcon(self:GetSelectedIcon()) + end + browser:SetSize(400,380) + browser:SetPos(0,40) + + local frame = vgui.Create("EditablePanel", master_frame) + local edit = vgui.Create("DTextEntry", frame) + frame:SetSize(300,20) + function browser:Think() + if not IsValid(category_node) then master_frame:Remove() end + x = master_frame:GetX() + y = master_frame:GetY() + frame:SetPos(x,y+20) + frame:MakePopup() + end + + function edit:OnValueChange(value) + browser:FilterByText( value ) + end + master_frame:MakePopup() + frame:MakePopup() + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + local function bring_up_tooltip_edit(category_node) + local frame = vgui.Create("EditablePanel") + local edit = vgui.Create("DTextEntry", frame) + function edit:OnEnter(value) + category_node:SetTooltip(value) + frame:Remove() + end + function frame:Think() + if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end + end + frame:MakePopup() + + frame:SetSize(300,30) + frame:SetPos(input.GetCursorPos()) + + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + local function bring_up_name_edit(category_node) + local frame = vgui.Create("EditablePanel") + local edit = vgui.Create("DTextEntry", frame) + edit:SetText(category_node:GetText()) + function edit:OnEnter(value) + category_node:SetText(value) + frame:Remove() + end + function frame:Think() + if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end + end + frame:MakePopup() + + frame:SetSize(300,30) + frame:SetPos(category_node.Label:LocalToScreen(category_node.Label:GetPos())) + + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + + local function load_partgroup_template_into_tree(categorytree, tbl) + tbl = tbl or pace.partgroups or pace.partmenu_categories_default + categorytree:Clear() + for category,category_contents in pairs(tbl) do + + local category_node = categorytree:AddNode(category) + category_node:SetIcon(get_icon(category_contents.icon, category)) + + category_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("insert part in category", function() bring_up_partclass_list(category_node) end):SetImage("icon16/add.png") + menu:AddOption("select icon", function() bring_up_category_icon_browser(category_node) end):SetImage("icon16/picture.png") + menu:AddOption("write a tooltip", function() bring_up_tooltip_edit(category_node) end):SetImage("icon16/comment.png") + menu:AddOption("rename this category", function() bring_up_name_edit(category_node) end):SetImage("icon16/textfield_rename.png") + menu:AddOption("remove this category", function() category_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + + if category_contents["tooltip"] then + category_node:SetTooltip(category_contents["tooltip"]) + end + + for field,value in pairs(category_contents) do + if Parts[field] then + class_partnode_add(category_node, field) + end + end + end + end + + local function extract_partgroup_template_from_tree(categorytree) + local tbl = {} + for i,category_node in ipairs(categorytree:Root():GetChildNodes()) do + tbl[category_node:GetText()] = {} + --print(i,category_node:GetText(),category_node.Label:GetTooltip(), category_node:GetIcon()) + if category_node:GetTooltip() ~= nil and category_node:GetTooltip() ~= "" then tbl[category_node:GetText()]["tooltip"] = category_node:GetTooltip() end + tbl[category_node:GetText()]["icon"] = category_node:GetIcon() + + for i2,part_node in ipairs(category_node:GetChildNodes()) do + tbl[category_node:GetText()][part_node:GetText()] = part_node:GetText() + --print("\t",part_node:GetText()) + end + end + return tbl + end + + load_partgroup_template_into_tree(categorytree, pace.partgroups) + + local part_categories_presets = vgui.Create("DComboBox", RightPanel) + part_categories_presets:SetText("Select a part category preset") + part_categories_presets:AddChoice("active preset") + part_categories_presets:AddChoice("factory preset") + part_categories_presets:AddChoice("Cedric's preset") + local default_partgroup_presets = { + ["pac_part_categories.txt"] = true, + ["pac_part_categories_cedrics.txt"] = true, + ["pac_part_categories_default.txt"] = true + } + for i,filename in ipairs(file.Find("pac3_config/pac_part_categories*.txt","DATA")) do + if not default_partgroup_presets[string.GetFileFromFilename(filename)] then + part_categories_presets:AddChoice(string.GetFileFromFilename(filename)) + end + end + + part_categories_presets:SetX(10) part_categories_presets:SetY(10) + part_categories_presets:SetWidth(170) + part_categories_presets:SetHeight(20) + + part_categories_presets.OnSelect = function( self, index, value ) + if value == "factory preset" then + pace.partgroups = pace.partmenu_categories_default + elseif value == "Cedric's preset" then + pace.partgroups = pace.partmenu_categories_cedrics + elseif string.find(value, ".txt") then + pace.partgroups = util.KeyValuesToTable(file.Read("pac3_config/"..value)) + elseif value == "active preset" then + decode_table_from_file("pac_part_categories") + if not pace.partgroups_user then pace.partgroups_user = pace.partgroups end + file.Write("pac3_config/pac_part_categories_user.txt", util.TableToKeyValues(pace.partgroups_user)) + end + load_partgroup_template_into_tree(categorytree, pace.partgroups) + end + + local part_categories_save = vgui.Create("DButton", RightPanel) + part_categories_save:SetText("Save") + part_categories_save:SetImage("icon16/disk.png") + part_categories_save:SetX(180) part_categories_save:SetY(10) + part_categories_save:SetWidth(80) + part_categories_save:SetHeight(20) + part_categories_save:SetTooltip("Left click to save preset to the active slot\nRight click to save to a new file") + part_categories_save.DoClick = function() + pace.partgroups = extract_partgroup_template_from_tree(categorytree) + file.Write("pac3_config/pac_part_categories.txt", util.TableToKeyValues(extract_partgroup_template_from_tree(categorytree))) + end + part_categories_save.DoRightClick = function() + Derma_StringRequest("Save preset", "Save a part category preset?", "pac_part_categories", + function(name) file.Write("pac3_config/"..name..".txt", util.TableToKeyValues(extract_partgroup_template_from_tree(categorytree))) + part_categories_presets:AddChoice(name..".txt") + end + ) + end + + local part_categories_add_cat = vgui.Create("DButton", RightPanel) + part_categories_add_cat:SetText("Add category") + part_categories_add_cat:SetImage("icon16/add.png") + part_categories_add_cat:SetX(260) part_categories_add_cat:SetY(10) + part_categories_add_cat:SetWidth(100) + part_categories_add_cat:SetHeight(20) + part_categories_add_cat.DoClick = function() + local category_node = categorytree:AddNode("Category " .. categorytree:Root():GetChildNodeCount() + 1) + category_node:SetIcon("icon16/page_white.png") + + category_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("insert part in category", function() bring_up_partclass_list(category_node) end):SetImage("icon16/add.png") + menu:AddOption("select icon", function() bring_up_category_icon_browser(category_node) end):SetImage("icon16/picture.png") + menu:AddOption("write a tooltip", function() bring_up_tooltip_edit(category_node) end):SetImage("icon16/comment.png") + menu:AddOption("rename this category", function() bring_up_name_edit(category_node) end):SetImage("icon16/textfield_rename.png") + menu:AddOption("remove this category", function() category_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + end + + return panel +end + +function pace.GetPartMenuComponentPreviewForMenuEdit(menu, option_name) + local pnl = vgui.Create("DButton", menu) + pnl:SetText(string.Replace(string.upper(option_name),"_"," ")) + return pnl +end + +function pace.ConfigureEventWheelMenu() + pace.command_colors = pace.command_colors or {} + local master_panel = vgui.Create("DFrame") + master_panel:SetTitle("event wheel config") + master_panel:SetSize(500,800) + master_panel:Center() + local mid_panel = vgui.Create("DPanel", master_panel) + mid_panel:Dock(FILL) + + local scr_pnl = vgui.Create("DScrollPanel", mid_panel) + scr_pnl:SetSize(490,800) + scr_pnl:SetPos(0,45) + local list = vgui.Create("DListLayout", scr_pnl) list:Dock(FILL) + + local first_panel = vgui.Create("DPanel", mid_panel) + first_panel:SetSize(500,40) + first_panel:Dock(TOP) + + local circle_style_listmenu = vgui.Create("DComboBox",first_panel) + circle_style_listmenu:SetText("Choose eventwheel style") + circle_style_listmenu:SetSize(200,20) + circle_style_listmenu:AddChoice("legacy") + circle_style_listmenu:AddChoice("concentric") + circle_style_listmenu:AddChoice("alternative") + function circle_style_listmenu:OnSelect( index, value ) + if value == "legacy" then + GetConVar("pac_eventwheel_style"):SetString("0") + elseif value == "concentric" then + GetConVar("pac_eventwheel_style"):SetString("1") + elseif value == "alternative" then + GetConVar("pac_eventwheel_style"):SetString("2") + end + end + + local circle_clickmode = vgui.Create("DComboBox",first_panel) + circle_clickmode:SetText("Choose eventwheel clickmode") + circle_clickmode:SetSize(200,20) + circle_clickmode:SetPos(200,0) + circle_clickmode:AddChoice("clickable and activates on close") + circle_clickmode:AddChoice("not clickable, but activate on close") + circle_clickmode:AddChoice("clickable, but do not activate on close") + function circle_clickmode:OnSelect( index, value ) + if value == "clickable and activates on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("0") + elseif value == "not clickable, but activate on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("-1") + elseif value == "clickable, but do not activate on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("1") + end + end + + + local rectangle_style_listmenu = vgui.Create("DComboBox",first_panel) + rectangle_style_listmenu:SetText("Choose eventlist style") + rectangle_style_listmenu:SetSize(200,20) + rectangle_style_listmenu:SetPos(0,20) + rectangle_style_listmenu:AddChoice("legacy-like") + rectangle_style_listmenu:AddChoice("concentric") + rectangle_style_listmenu:AddChoice("alternative") + + function rectangle_style_listmenu:OnSelect( index, value ) + if value == "legacy-like" then + GetConVar("pac_eventlist_style"):SetString("0") + elseif value == "concentric" then + GetConVar("pac_eventlist_style"):SetString("1") + elseif value == "alternative" then + GetConVar("pac_eventlist_style"):SetString("2") + end + end + + local rectangle_clickmode = vgui.Create("DComboBox",first_panel) + rectangle_clickmode:SetText("Choose eventlist clickmode") + rectangle_clickmode:SetSize(200,20) + rectangle_clickmode:SetPos(200,20) + rectangle_clickmode:AddChoice("clickable and activates on close") + rectangle_clickmode:AddChoice("not clickable, but activate on close") + rectangle_clickmode:AddChoice("clickable, but do not activate on close") + function rectangle_clickmode:OnSelect( index, value ) + if value == "clickable and activates on close" then + GetConVar("pac_eventlist_clickmode"):SetString("0") + elseif value == "not clickable, but activate on close" then + GetConVar("pac_eventlist_clickmode"):SetString("-1") + elseif value == "clickable, but do not activate on close" then + GetConVar("pac_eventlist_clickmode"):SetString("1") + end + end + + local events = {} + for i,v in pairs(pac.GetLocalParts()) do + if v.ClassName == "event" then + local e = v:GetEvent() + if e == "command" then + local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) + local this_event_hidden = v:IsHiddenBySomethingElse(false) + events[cmd] = cmd + end + end + + end + + local names = table.GetKeys( events ) + table.sort(names, function(a, b) return a < b end) + + local copied_color = nil + local lanes = {} + local colorpanel + if LocalPlayer().pac_command_events then + if table.Count(names) == 0 then + local error_label = vgui.Create("DLabel", list) + error_label:SetText("Uh oh, nothing to see here! Looks like you don't have any command events in your outfit!\nPlease go back to the editor.") + error_label:SetPos(100,200) + error_label:SetFont("DermaDefaultBold") + error_label:SetSize(450,50) + error_label:SetColor(Color(150,0,0)) + end + for _, name in ipairs(names) do + local pnl = vgui.Create("DPanel") list:Add(pnl) pnl:SetSize(400,20) + local btn = vgui.Create("DButton", pnl) + + btn:SetSize(200,25) + btn:SetText(name) + btn:SetTooltip(name) + + + if pace.command_colors[name] then + local tbl = string.Split(pace.command_colors[name], " ") + btn:SetColor(Color(tonumber(tbl[1]),tonumber(tbl[2]),tonumber(tbl[3]))) + end + local colorbutton = vgui.Create("DButton", pnl) + colorbutton:SetText("Color") + colorbutton:SetIcon("icon16/color_wheel.png") + colorbutton:SetPos(200,0) colorbutton:SetSize(65,20) + function colorbutton:DoClick() + if IsValid(colorpanel) then colorpanel:Remove() end + local clr_frame = vgui.Create("DPanel") + colorpanel = clr_frame + function clr_frame:Think() + if not pace.command_event_menu_opened and not IsValid(master_panel) then self:Remove() end + end + + local clr_pnl = vgui.Create("DColorMixer", clr_frame) + if pace.command_colors[name] then + local str_tbl = string.Split(pace.command_colors[name], " ") + clr_pnl:SetBaseColor(Color(tonumber(str_tbl[1]),tonumber(str_tbl[2]),tonumber(str_tbl[3]))) + end + + clr_frame:SetSize(300,200) clr_pnl:Dock(FILL) + clr_frame:SetPos(self:LocalToScreen(0,0)) + clr_frame:RequestFocus() + function clr_pnl:Think() + if input.IsMouseDown(MOUSE_LEFT) then + + if not IsValid(vgui.GetHoveredPanel()) then + self:Remove() clr_frame:Remove() + else + if vgui.GetHoveredPanel():GetClassName() == "CGModBase" and not self.clicking then + self:Remove() clr_frame:Remove() + end + end + self.clicking = true + else + self.clicking = false + end + end + function clr_pnl:ValueChanged(col) + pace.command_colors = pace.command_colors or {} + pace.command_colors[name] = col.r .. " " .. col.g .. " " .. col.b + btn:SetColor(col) + end + + end + + local copypastebutton = vgui.Create("DButton", pnl) + copypastebutton:SetText("Copy/Paste") + copypastebutton:SetToolTip("right click to copy\nleft click to paste") + copypastebutton:SetIcon("icon16/page_copy.png") + copypastebutton:SetPos(265,0) copypastebutton:SetSize(150,20) + function copypastebutton:DoClick() + if not copied_color then return end + pace.command_colors[name] = copied_color + btn:SetColor(Color(tonumber(string.Split(copied_color, " ")[1]), tonumber(string.Split(copied_color, " ")[2]), tonumber(string.Split(copied_color, " ")[3]))) + end + function copypastebutton:DoRightClick() + for _,tbl in pairs(lanes) do + if tbl.cmd ~= name then + tbl.copypaste:SetText("Copy/Paste") + end + end + copied_color = pace.command_colors[name] + if copied_color then + self:SetText("copied: " .. pace.command_colors[name]) + else + self:SetText("no color to copy!") + end + end + + local clearbutton = vgui.Create("DButton", pnl) + clearbutton:SetText("Clear") + clearbutton:SetIcon("icon16/cross.png") + clearbutton:SetPos(415,0) clearbutton:SetSize(60,20) + function clearbutton:DoClick() + btn:SetColor(Color(0,0,0)) + pace.command_colors[name] = nil + end + + lanes[name] = {cmd = name, main_btn = btn, color_btn = colorbutton, copypaste = copypastebutton, clear = clearbutton} + end + end + + function master_panel:OnRemove() + gui.EnableScreenClicker(false) + pace.command_event_menu_opened = nil + encode_table_to_file("eventwheel_colors", pace.command_colors) + end + + master_panel:RequestFocus() + gui.EnableScreenClicker(true) + pace.command_event_menu_opened = master_panel +end + + +decode_table_from_file("pac_editor_shortcuts") +decode_table_from_file("pac_editor_partmenu_layouts") +decode_table_from_file("eventwheel_colors") + +if not file.Exists("pac_part_categories_cedrics.txt", "DATA") then + file.Write("pac3_config/pac_part_categories_cedrics.txt", util.TableToKeyValues(pace.partmenu_categories_cedrics)) +end +if not file.Exists("pac_part_categories_default.txt", "DATA") then + file.Write("pac3_config/pac_part_categories_default.txt", util.TableToKeyValues(pace.partmenu_categories_default)) +end +decode_table_from_file("pac_part_categories") +pace.partgroups = pace.partgroups or pace.partmenu_categories_default diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 40efcff14..19355f393 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -1,3 +1,265 @@ +include("parts.lua") +include("popups_part_tutorials.lua") + +local L = pace.LanguageString + +concommand.Add( "pac_toggle_focus", function() pace.Call("ToggleFocus") end) +concommand.Add( "pac_focus", function() pace.Call("ToggleFocus") end) + +local legacy_input = CreateConVar("pac_editor_shortcuts_legacy_mode", "1", FCVAR_ARCHIVE, "Reverts the editor to hardcoded key checks ignoring customizable keys. Some keys are hidden and held down causing serious editor usage problems.") + +local last_recorded_combination + +pace.PACActionShortcut_Dictionary = { + "wear", + "save", + "load", + "hide_editor", + "hide_editor_visible", + "copy", + "paste", + "cut", + "clone", + "delete", + "expand_all", + "collapse_all", + "editor_up", + "editor_down", + "editor_pageup", + "editor_pagedown", + "editor_node_collapse", + "editor_node_expand", + "undo", + "redo", + "hide", + "panic", + "restart", + "partmenu", + "add_part", + "property_search_current_part", + "property_search_in_tree", + "toolbar_pac", + "toolbar_tools", + "toolbar_player", + "toolbar_view", + "toolbar_options", + "zoom_panel", + "T_Pose", + "clear_bulkselect", + "copy_bulkselect", + "bulk_insert", + "bulk_delete", + "bulk_pack", + "bulk_paste_1", + "bulk_paste_2", + "bulk_paste_3", + "bulk_paste_4", + "bulk_paste_properties_1", + "bulk_paste_properties_2", + "bulk_hide", + "help_info_popup", + "ultra_cleanup" +} + +pace.PACActionShortcut_Default = { + ["wear"] = { + [1] = {"CTRL", "n"} + }, + + ["save"] = { + [1] = {"CTRL", "s"} + }, + + ["hide_editor"] = { + [1] = {"CTRL", "e"} + }, + + ["help_info_popup"] = { + [1] = {"F1"} + }, + + ["hide_editor_visible"] = { + [1] = {"ALT", "e"} + }, + + ["copy"] = { + [1] = {"CTRL", "c"} + }, + + ["paste"] = { + [1] = {"CTRL", "v"} + }, + ["cut"] = { + [1] = {"CTRL", "x"} + }, + ["delete"] = { + [1] = {"DEL"} + }, + ["expand_all"] = { + + }, + ["collapse_all"] = { + + }, + ["undo"] = { + [1] = {"CTRL", "z"} + }, + ["redo"] = { + [1] = {"CTRL", "y"} + }, + ["T_Pose"] = { + [1] = {"CTRL", "t"} + }, + ["property_search_current_part"] = { + [1] = {"CTRL", "f"} + }, + ["property_search_in_tree"] = { + [1] = {"CTRL", "SHIFT", "f"} + }, + ["editor_up"] = { + [1] = {"UPARROW"} + }, + ["editor_down"] = { + [1] = {"DOWNARROW"} + }, + ["editor_pageup"] = { + [1] = {"PGUP"} + }, + ["editor_pagedown"] = { + [1] = {"PGDN"} + }, + ["editor_node_collapse"] = { + [1] = {"LEFTARROW"} + }, + ["editor_node_expand"] = { + [1] = {"RIGHTARROW"} + } +} + +pace.PACActionShortcut_NoCTRL = { + ["wear"] = { + [1] = {"n"} + }, + + ["save"] = { + [1] = {"m"} + }, + + ["hide_editor"] = { + [1] = {"q"} + }, + + ["copy"] = { + [1] = {"c"} + }, + + ["paste"] = { + [1] = {"v"} + }, + + ["cut"] = { + [1] = {"x"} + }, + + ["delete"] = { + [1] = {"DEL"} + }, + + ["undo"] = { + [1] = {"z"} + }, + + ["redo"] = { + [1] = {"y"} + }, + + ["T_Pose"] = { + [1] = {"t"} + } +} + +pace.PACActionShortcut_Cedric = { + ["wear"] = { + [1] = {"CTRL", "n"} + }, + + ["save"] = { + [1] = {"CTRL", "m"} + }, + + ["hide_editor"] = { + [1] = {"CTRL", "e"}, + [2] = {"INS"}, + [3] = {"TAB"} + }, + + ["hide_editor_visible"] = { + [1] = {"LALT", "e"} + }, + + ["copy"] = { + [1] = {"CTRL", "c"} + }, + + ["paste"] = { + [1] = {"CTRL", "v"} + }, + ["cut"] = { + [1] = {"CTRL", "x"} + }, + ["delete"] = { + [1] = {"DEL"} + }, + ["expand_all"] = { + [1] = {"x"} + }, + ["collapse_all"] = { + [1] = {"c"} + }, + ["undo"] = { + [1] = {"CTRL", "z"}, + [2] = {"u"} + }, + ["redo"] = { + [1] = {"CTRL", "y"}, + [2] = {"i"} + }, + ["tpose"] = { + [1] = {"CTRL", "t"} + }, + ["editor_up"] = { + [1] = {"UPARROW"} + }, + ["editor_down"] = { + [1] = {"DOWNARROW"} + }, + ["editor_pageup"] = { + [1] = {"PGUP"} + }, + ["editor_pagedown"] = { + [1] = {"PGDN"} + }, + ["editor_node_collapse"] = { + [1] = {"LEFTARROW"} + }, + ["editor_node_expand"] = { + [1] = {"RIGHTARROW"} + } +} + +pace.PACActionShortcut = pace.PACActionShortcut or pace.PACActionShortcut_Cedric + +--pace.PACActionShortcut = pace.PACActionShortcuts_NoCTRL + + + --[[thinkUndo() + thinkCopy() + thinkPaste() + thinkCut() + thinkDelete() + thinkExpandAll() + thinkCollapseAll()]]-- + function pace.OnShortcutSave() if not IsValid(pace.current_part) then return end @@ -17,54 +279,518 @@ function pace.OnShortcutWear() end local last = 0 +pace.passthrough_keys = { + [KEY_LWIN] = true, + [KEY_RWIN] = true, + [KEY_CAPSLOCK] = true, + [KEY_CAPSLOCKTOGGLE] = true +} + + +function pace.LookupShortcutsForAction(action, provided_inputs, do_it) + pace.BulkSelectKey = input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString()) + + --combo is the table of key names for one combo slot + local function input_contains_one_match(combo, action, inputs) + --if pace.shortcut_inputs_count ~= #combo then return false end --if input has too much or too little keys, we already know it doesn't match + for _,key in ipairs(combo) do --check the combo's keys for a match + --[[if not (input.IsKeyDown(input.GetKeyCode(key)) and inputs[input.GetKeyCode(key)]) then --all keys must be there + return false + end]] + + if not input.IsKeyDown(input.GetKeyCode(key)) then --all keys must be there + return false + end + end + return true + end + + local function shortcut_contains_counterexample(combo, action, inputs) + + local counterexample = false + for key,bool in ipairs(inputs) do --check the input for counter-examples + if input.IsKeyDown(key) then + if not table.HasValue(combo, input.GetKeyName(key)) then --any keypress that is not in the combo invalidates the combo + --some keys don't count as counterexamples?? + --random windows or capslocktoggle keys being pressed screw up the input + --bulk select should allow rolling select with the scrolling options + + if key == pace.BulkSelectKey and not action == "editor_up" and not action == "editor_down" and not action == "editor_pageup" and not action == "editor_pagedown" then + counterexample = true + elseif not pace.passthrough_keys[key] and key ~= pace.BulkSelectKey then + counterexample = true + end + + if pace.passthrough_keys[key] or key == pace.BulkSelectKey then + counterexample = false + end + end + + end + end + return counterexample + end + + if not pace.PACActionShortcut[action] then return false end + local final_success = false -function pace.CheckShortcuts() - if gui.IsConsoleVisible() then return end - if not pace.Editor or not pace.Editor:IsValid() then return end - if last > RealTime() or input.IsMouseDown(MOUSE_LEFT) then return end + local keynames_str = "" + for key,bool in ipairs(provided_inputs) do + if bool then keynames_str = keynames_str .. input.GetKeyName(key) .. "," end + end - if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_E) then - pace.Call("ToggleFocus", true) - last = RealTime() + 0.2 + for i=1,10,1 do --go through each combo slot + if pace.PACActionShortcut[action][i] then --is there a combo in that slot + combo = pace.PACActionShortcut[action][i] + local keynames_str = "" + + local single_match = false + if input_contains_one_match(combo, action, provided_inputs) then + single_match = true + if shortcut_contains_counterexample(combo, action, provided_inputs) then + single_match = false + end + end + + if single_match and do_it then + pace.DoShortcutFunc(action) + final_success = true + --MsgC(Color(50,255,100),"-------------------------\n\n\n\nrun yes " .. action .. "\n" .. keynames_str .. "\n\n\n\n-------------------------") + end + end end + + return final_success +end - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_E) then - pace.Call("ToggleFocus") - last = RealTime() + 0.2 +function pace.AssignEditorShortcut(action, tbl, index) + print("received a new shortcut assignation") + + pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {} + pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {} + + if table.IsEmpty(tbl) or not tbl then + pace.PACActionShortcut[action][index] = nil + print("wiped shortcut " .. action .. " off index " .. index) + return + end + --validate tbl argument + for i,key in pairs(tbl) do + print(i,key) + if not isnumber(i) then print("passed a wrong table") return end + if not isstring(key) then print("passed a wrong table") return end end + pace.PACActionShortcut[action][index] = tbl +end + +function pace.DoShortcutFunc(action) + + pace.delaybulkselect = RealTime() + 0.5 + pace.delayshortcuts = RealTime() + 0.2 + pace.delaymovement = RealTime() + 1 - if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then - RunConsoleCommand("pac_restart") + if action == "editor_up" then pace.DoScrollControls(action) + elseif action == "editor_down" then pace.DoScrollControls(action) + elseif action == "editor_pageup" then pace.DoScrollControls(action) + elseif action == "editor_pagedown" then pace.DoScrollControls(action) end + if action == "editor_node_expand" then pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) + elseif action == "editor_node_collapse" then pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) end + + if action == "redo" then pace.Redo(pace.current_part) pace.delayshortcuts = RealTime() end + if action == "undo" then pace.Undo(pace.current_part) pace.delayshortcuts = RealTime() end + if action == "delete" then pace.RemovePart(pace.current_part) end + if action == "hide" then pace.current_part:SetHide(not pace.current_part:GetHide()) end + + if action == "copy" then pace.Copy(pace.current_part) end + if action == "cut" then pace.Cut(pace.current_part) end + if action == "paste" then pace.Paste(pace.current_part) end + if action == "clone" then pace.Clone(pace.current_part) end + if action == "save" then pace.Call("ShortcutSave") end + if action == "load" then + local function add_expensive_submenu_load(pnl, callback) + local old = pnl.OnCursorEntered + pnl.OnCursorEntered = function(...) + callback() + pnl.OnCursorEntered = old + return old(...) + end + end + local menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + + menu.GetDeleteSelf = function() return false end + + menu:AddOption(L"load from url", function() + Derma_StringRequest( + L"load parts", + L"Some indirect urls from on pastebin, dropbox, github, etc are handled automatically. Pasting the outfit's file contents into the input field will also work.", + "", + + function(name) + pace.LoadParts(name, clear, override_part) + end + ) + end):SetImage(pace.MiscIcons.url) + + menu:AddOption(L"load from clipboard", function() + pace.MultilineStringRequest( + L"load parts from clipboard", + L"Paste the outfits content here.", + "", + + function(name) + local data,err = pace.luadata.Decode(name) + if data then + pace.LoadPartsFromTable(data, clear, override_part) + end + end + ) + end):SetImage(pace.MiscIcons.paste) + + if not override_part and pace.example_outfits then + local examples, pnl = menu:AddSubMenu(L"examples") + pnl:SetImage(pace.MiscIcons.help) + examples.GetDeleteSelf = function() return false end + + local sorted = {} + for k,v in pairs(pace.example_outfits) do sorted[#sorted + 1] = {k = k, v = v} end + table.sort(sorted, function(a, b) return a.k < b.k end) + + for _, data in pairs(sorted) do + examples:AddOption(data.k, function() pace.LoadPartsFromTable(data.v) end) + :SetImage(pace.MiscIcons.outfit) + end + end - -- Only if the editor is in the foreground - if pace.IsFocused() then - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_S) then - pace.Call("ShortcutSave") - last = RealTime() + 0.2 + menu:AddSpacer() + + pace.AddOneDirectorySavedPartsToMenu(menu, "templates", "templates") + pace.AddOneDirectorySavedPartsToMenu(menu, "__backup_save", "backups") + + menu:AddSpacer() + do + local menu, icon = menu:AddSubMenu(L"load (expensive)", function() pace.LoadParts(nil, true) end) + menu:SetDeleteSelf(false) + icon:SetImage(pace.MiscIcons.load) + add_expensive_submenu_load(icon, function() pace.AddSavedPartsToMenu(menu, true) end) end + + menu:SetMaxHeight(ScrH() - y) + menu:MakePopup() + + end + if action == "wear" then pace.Call("ShortcutWear") end + + if action == "hide_editor" then pace.Call("ToggleFocus") pace.delaymovement = RealTime() pace.delaybulkselect = RealTime() end + if action == "hide_editor_visible" then pace.Call("ToggleFocus", true) end + if action == "panic" then pac.Panic() end + if action == "restart" then RunConsoleCommand("pac_restart") end + if action == "collapse_all" then + + local part = pace.current_part - -- CTRL + (W)ear? - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_N) then - pace.Call("ShortcutWear") - last = RealTime() + 0.2 + if not part or not part:IsValid() then + pace.FlashNotification('No part to collapse') + else + end + part:CallRecursive('SetEditorExpand', GetConVar("pac_reverse_collapse"):GetBool()) + pace.RefreshTree(true) + end + if action == "expand_all" then + + local part = pace.current_part - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_T) then - pace.SetTPose(not pace.GetTPose()) - last = RealTime() + 0.2 + if not part or not part:IsValid() then + pace.FlashNotification('No part to collapse') + else + end + part:CallRecursive('SetEditorExpand', not GetConVar("pac_reverse_collapse"):GetBool()) + pace.RefreshTree(true) + end - if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_F) then + if action == "partmenu" then pace.OnPartMenu(pace.current_part) end + if action == "property_search_current_part" then + if pace.properties.search:IsVisible() then + pace.properties.search:SetVisible(false) + pace.properties.search:SetEnabled(false) + pace.property_searching = false + else pace.properties.search:SetVisible(true) pace.properties.search:RequestFocus() pace.properties.search:SetEnabled(true) pace.property_searching = true + end + + end + if action == "property_search_in_tree" then + if pace.tree_search_open then + pace.tree_searcher:Remove() + else + pace.OpenTreeSearch() + end + end + if action == "add_part" then pace.OnAddPartMenu(pace.current_part) end + if action == "toolbar_tools" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + pace.AddToolsToMenu(menu) + end + if action == "toolbar_pac" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:AddOption("pac") + menu:SetPos(x,y) + pace.PopulateMenuBarTab(menu, "pac") + end + if action == "toolbar_options" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + pace.PopulateMenuBarTab(menu, "options") + end + if action == "toolbar_player" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + pace.PopulateMenuBarTab(menu, "player") + + end + if action == "toolbar_view" then + menu = DermaMenu() + local x,y = input.GetCursorPos() + menu:SetPos(x,y) + pace.PopulateMenuBarTab(menu, "view") + end + + if action == "zoom_panel" then + pace.PopupMiniFOVSlider() + end + + if action == "T_Pose" then pace.SetTPose(not pace.GetTPose()) end + + if action == "clear_bulkselect" then pace.ClearBulkList() end + if action == "copy_bulkselect" then pace.BulkCopy(pace.current_part) end + if action == "bulk_insert" then pace.BulkCutPaste(pace.current_part) end + if action == "bulk_delete" then pace.BulkRemovePart() end + if action == "bulk_pack" then + root = pac.CreatePart("group") + for i,v in ipairs(pace.BulkSelectList) do + v:SetParent(root) + end + end + if action == "bulk_paste_1" then pace.BulkPasteFromBulkSelectToSinglePart(pace.current_part) end + if action == "bulk_paste_2" then pace.BulkPasteFromSingleClipboard(pace.current_part) end + if action == "bulk_paste_3" then pace.BulkPasteFromBulkClipboard(pace.current_part) end + if action == "bulk_paste_4" then pace.BulkPasteFromBulkClipboardToBulkSelect(pace.current_part) end + + if action == "bulk_paste_properties_1" then + pace.Copy(pace.current_part) + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end + if action == "bulk_paste_properties_2" then + for _,v in ipairs(pace.BulkSelectList) do + pace.PasteProperties(v) + end + end + if action == "bulk_hide" then pace.BulkHide() end + + if action == "help_info_popup" then + if pace.floating_popup_reserved then + pace.floating_popup_reserved:Remove() + end + + --[[pac.InfoPopup("Looks like you don't have an active part. You should right click and go make one to get started", { + obj_type = "screen", + clickfunc = function() pace.OnAddPartMenu(pace.current_part) end, + hoverfunc = "open", + pac_part = false + }, ScrW()/2, ScrH()/2)]] + + + local popup_setup_tbl = { + obj_type = "", + clickfunc = function() pace.OnAddPartMenu(pace.current_part) end, + hoverfunc = "open", + pac_part = pace.current_part, + panel_exp_width = 900, panel_exp_height = 400 + } + + --obj_type types + local popup_prefered_type = GetConVar("pac_popups_preferred_location"):GetString() + popup_setup_tbl.obj_type = popup_prefered_type + + if popup_prefered_type == "pac tree label" then + popup_setup_tbl.obj = pace.current_part.pace_tree_node + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) + + elseif popup_prefered_type == "part world" then + popup_setup_tbl.obj = pace.current_part + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) + + elseif popup_prefered_type == "screen" then + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, ScrW()/2, ScrH()/2) + + elseif popup_prefered_type == "cursor" then + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, input.GetCursorPos()) + + elseif popup_prefered_type == "editor bar" then + popup_setup_tbl.obj = pace.Editor + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) + + end + + + + + --[[if IsValid(pace.current_part) then + pac.AttachInfoPopupToPart(pace.current_part) + else + pac.InfoPopup("Looks like you don't have an active part. You should right click and go make one to get started", { + obj_type = "screen", + --hoverfunc = function() pace.OnAddPartMenu(pace.current_part) end, + pac_part = false + }, ScrW()/2, ScrH()/2) + end]] + end + + if action == "ultra_cleanup" then + pace.UltraCleanup(pace.current_part) + end + + +end + +pace.delaybulkselect = 0 +pace.delayshortcuts = 0 +pace.delaymovement = 0 + +--only check once. what does this mean? +--if a shortcut is SUCCESSFULLY run (check_input = false), stop checking until inputs are reset (if no_input then check_input = true end) +--always refresh the inputs, but check if we stay the same before checking the shortcuts! +-- + +skip = false +no_input_override = false +has_run_something = false +previous_inputs_str = "" + +function pace.CheckShortcuts() + if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then + if gui.IsConsoleVisible() then return end + if not pace.Editor or not pace.Editor:IsValid() then return end + if last > RealTime() or input.IsMouseDown(MOUSE_LEFT) then return end + + if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_E) then + pace.Call("ToggleFocus", true) + last = RealTime() + 0.2 + end + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_E) then + pace.Call("ToggleFocus") last = RealTime() + 0.2 end + if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then + RunConsoleCommand("pac_restart") + end + + -- Only if the editor is in the foreground + if pace.IsFocused() then + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_S) then + pace.Call("ShortcutSave") + last = RealTime() + 0.2 + end + + -- CTRL + (W)ear? + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_N) then + pace.Call("ShortcutWear") + last = RealTime() + 0.2 + end + + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_T) then + pace.SetTPose(not pace.GetTPose()) + last = RealTime() + 0.2 + end + + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_F) then + if not input.IsKeyDown(KEY_LSHIFT)then + pace.properties.search:SetVisible(true) + pace.properties.search:RequestFocus() + pace.properties.search:SetEnabled(true) + pace.property_searching = true + + last = RealTime() + 0.2 + else + pace.OpenTreeSearch() + end + end + + end + return end + + local input_active = {} + local no_input = true + no_input_override = false + local inputs_str = "" + pace.shortcut_inputs_count = 0 + for i=1,172,1 do --build bool list of all current keys + if input.IsKeyDown(i) then + if pace.passthrough_keys[i] or i == pace.BulkSelectKey then no_input_override = true end + input_active[i] = true + pace.shortcut_inputs_count = pace.shortcut_inputs_count + 1 + no_input = false + inputs_str = inputs_str .. input.GetKeyName(i) .. " " + else input_active[i] = false end + end + + if previous_inputs_str ~= inputs_str then + if last + 0.2 > RealTime() and has_run_something then + skip = true + else + has_run_something = false + end + + end + if no_input then + skip = false + end + previous_inputs_str = inputs_str + + + if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end + if gui.IsConsoleVisible() then return end + if not pace.Editor or not pace.Editor:IsValid() then return end + + + if skip and not no_input_override then return end + + local starttime = SysTime() + + for action,list_of_lists in pairs(pace.PACActionShortcut) do + if not has_run_something then + if action == "hide_editor" and pace.LookupShortcutsForAction(action, input_active, true) then --we can focus back if editor is not focused + --pace.DoShortcutFunc(action) + last = RealTime() + has_run_something = true + check_input = false + elseif pace.Focused and pace.LookupShortcutsForAction(action, input_active, true) then --we can't do anything else if not focused + --pace.DoShortcutFunc(action) + pace.FlashNotification(action) + last = RealTime() + has_run_something = true + check_input = false + end + end + end + end pac.AddHook("Think", "pace_shortcuts", pace.CheckShortcuts) @@ -254,16 +980,21 @@ do end pac.AddHook("Think", "pace_keyboard_shortcuts", function() + if not pace.IsActive() then return end if not pace.Focused then return end if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end if gui.IsConsoleVisible() then return end - thinkUndo() - thinkCopy() - thinkPaste() - thinkCut() - thinkDelete() - thinkExpandAll() - thinkCollapseAll() + if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then + thinkUndo() + thinkCopy() + thinkPaste() + thinkCut() + thinkDelete() + thinkExpandAll() + thinkCollapseAll() + end + end) -end \ No newline at end of file +end + diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index d18234abe..406eaed61 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -110,9 +110,37 @@ function pace.ClientSettingsMenu(self) end -local icon = "icon64/pac3.png" + +local icon_cvar = CreateConVar("pac_icon", "1", {FCVAR_ARCHIVE}, "Use the new PAC4.5 icon or the old PAC icon.\n0 = use the old one\n1 = use the new one") +local icon = icon_cvar:GetBool() and "icon64/new pac icon.png" or "icon64/pac3.png" + icon = file.Exists("materials/"..icon,'GAME') and icon or "icon64/playermodel.png" +local function ResetPACIcon() + if icon_cvar:GetBool() then icon = "icon64/new pac icon.png" else icon = "icon64/pac3.png" end + list.Set( + "DesktopWindows", + "PACEditor", + { + title = "PAC Editor", + icon = icon, + width = 960, + height = 700, + onewindow = true, + init = function(icn, pnl) + pnl:Remove() + RunConsoleCommand("pac_editor") + end + } + ) + RunConsoleCommand("spawnmenu_reload") +end + +cvars.AddChangeCallback("pac_icon", ResetPACIcon) + +concommand.Add("pac_change_icon", function() RunConsoleCommand("pac_icon", (not icon_cvar:GetBool()) and "1" or "0") ResetPACIcon() end) + + list.Set( "DesktopWindows", "PACEditor", diff --git a/lua/pac3/editor/client/tools.lua b/lua/pac3/editor/client/tools.lua index b94b3dceb..330eea2e3 100644 --- a/lua/pac3/editor/client/tools.lua +++ b/lua/pac3/editor/client/tools.lua @@ -1,5 +1,6 @@ local L = pace.LanguageString pace.Tools = {} +include("parts.lua") function pace.AddToolsToMenu(menu) menu.GetDeleteSelf = function() return false end @@ -782,6 +783,58 @@ pace.AddTool(L"dump player submaterials", function() end end) +pace.AddTool(L"dump model submaterials", function(part) + if part.ClassName == "model" or part.ClassName == "model2" then + for id,mat in pairs(part:GetOwner():GetMaterials()) do + chat.AddText(("%d %s"):format(id,tostring(mat))) + end + end +end) + +pace.AddTool(L"proxy/event: Engrave targets", function(part) + local function reassign(part) + if part.ClassName == "proxy" then + if not IsValid(part.TargetPart) then + if part.AffectChildren and table.Count(part:GetChildren()) == 1 then + part:SetTargetPart(part:GetChildren()[1]) + part:SetAffectChildren(nil) + else + part:SetTargetPart(part:GetParent()) + end + end + elseif part.ClassName == "event" then + if not IsValid(part.DestinationPart) then + if part.AffectChildrenOnly == true and table.Count(part:GetChildren()) == 1 then + part:SetDestinationPart(part:GetChildren()[1]) + elseif part.AffectChildrenOnly == false then + part:SetDestinationPart(part:GetParent()) + end + end + end + end + if part ~= part:GetRootPart() then + reassign(part) + else + for i,part2 in pairs(pac.GetLocalParts()) do + reassign(part2) + end + end +end) + +--aka pace.UltraCleanup +pace.AddTool(L"Destroy hidden parts, proxies and events", function(part) + + if not part then part = pace.current_part end + root = part:GetRootPart() + + pnl = Derma_Query("Only do this if you know what you're doing!\nMark parts as important in their notes to protect them.", "Warning", + "Destroy!", function() pace.UltraCleanup( root ) end, + "cancel", nil + ) + pnl:SetWidth(300) + +end) + pace.AddTool(L"stop all custom animations", function() pac.animations.StopAllEntityAnimations(pac.LocalPlayer) pac.animations.ResetEntityBoneMatrix(pac.LocalPlayer) diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 8f04a0905..3bc04a55f 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -12,6 +12,26 @@ acsfnc("Pos", Vector(5,5,5)) acsfnc("Angles", Angle(0,0,0)) acsfnc("FOV", 75) +pace.camera_forward_bind = CreateClientConVar("pac_editor_camera_forward_bind", "w", true) +pace.camera_back_bind = CreateClientConVar("pac_editor_camera_back_bind", "s", true) +pace.camera_moveleft_bind = CreateClientConVar("pac_editor_camera_moveleft_bind", "a", true) +pace.camera_moveright_bind = CreateClientConVar("pac_editor_camera_moveright_bind", "d", true) +pace.camera_up_bind = CreateClientConVar("pac_editor_camera_up_bind", "space", true) +pace.camera_down_bind = CreateClientConVar("pac_editor_camera_down_bind", "", true) +pace.camera_slow_bind = CreateClientConVar("pac_editor_camera_slow_bind", "ctrl", true) +pace.camera_speed_bind = CreateClientConVar("pac_editor_camera_speed_bind", "shift", true) + +pace.camera_movement_binds = { + ["forward"] = pace.camera_forward_bind, + ["back"] = pace.camera_back_bind, + ["moveleft"] = pace.camera_moveleft_bind, + ["moveright"] = pace.camera_moveright_bind, + ["up"] = pace.camera_up_bind, + ["down"] = pace.camera_down_bind, + ["slow"] = pace.camera_slow_bind, + ["speed"] = pace.camera_speed_bind +} + function pace.GetViewEntity() return pace.ViewEntity:IsValid() and pace.ViewEntity or pac.LocalPlayer end @@ -141,6 +161,10 @@ end local WORLD_ORIGIN = Vector(0, 0, 0) +local function MovementBindDown(name) + return input.IsButtonDown(input.GetKeyCode(pace.camera_movement_binds[name]:GetString())) +end + local function CalcDrag() if @@ -161,7 +185,7 @@ local function CalcDrag() local ftime = FrameTime() * 50 local mult = 5 - if input.IsKeyDown(KEY_LCONTROL) or input.IsKeyDown(KEY_RCONTROL) then + if MovementBindDown("slow") then mult = 0.1 end @@ -197,7 +221,7 @@ local function CalcDrag() mult = mult * math.min(origin:Distance(pace.ViewPos) / 200, 3) - if input.IsKeyDown(KEY_LSHIFT) then + if MovementBindDown("speed") then mult = mult + 5 end @@ -223,28 +247,30 @@ local function CalcDrag() end end - if input.IsKeyDown(KEY_W) then - pace.ViewPos = pace.ViewPos + pace.ViewAngles:Forward() * mult * ftime - elseif input.IsKeyDown(KEY_S) then - pace.ViewPos = pace.ViewPos - pace.ViewAngles:Forward() * mult * ftime - end - - if input.IsKeyDown(KEY_D) then - pace.ViewPos = pace.ViewPos + pace.ViewAngles:Right() * mult * ftime - elseif input.IsKeyDown(KEY_A) then - pace.ViewPos = pace.ViewPos - pace.ViewAngles:Right() * mult * ftime - end - if input.IsKeyDown(KEY_SPACE) then - if not IsValid(pace.timeline.frame) then - pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * mult * ftime + if pace.delaymovement < RealTime() then + if MovementBindDown("forward") then + pace.ViewPos = pace.ViewPos + pace.ViewAngles:Forward() * mult * ftime + elseif MovementBindDown("back") then + pace.ViewPos = pace.ViewPos - pace.ViewAngles:Forward() * mult * ftime end - end - --[[if input.IsKeyDown(KEY_LALT) then - pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * -mult * ftime - end]] + if MovementBindDown("moveright") then + pace.ViewPos = pace.ViewPos + pace.ViewAngles:Right() * mult * ftime + elseif MovementBindDown("moveleft") then + pace.ViewPos = pace.ViewPos - pace.ViewAngles:Right() * mult * ftime + end + if MovementBindDown("up") then + if not IsValid(pace.timeline.frame) then + pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * mult * ftime + end + elseif MovementBindDown("down") then + if not IsValid(pace.timeline.frame) then + pace.ViewPos = pace.ViewPos - pace.ViewAngles:Up() * mult * ftime + end + end + end end @@ -338,6 +364,7 @@ function pace.PostRenderVGUI() end function pace.EnableView(b) + if b then pac.AddHook("GUIMousePressed", "editor", pace.GUIMousePressed) pac.AddHook("GUIMouseReleased", "editor", pace.GUIMouseReleased) @@ -580,3 +607,52 @@ function pace.ResetEyeAngles() pac.SetupBones(ent) end end + +function pace.PopupMiniFOVSlider() + zoom_persistent = GetConVar("pac_zoom_persistent") + zoom_smooth = GetConVar("pac_zoom_smooth") + local zoomframe = vgui.Create( "DPanel" ) + local x,y = input.GetCursorPos() + zoomframe:SetPos(x - 90,y - 10) + zoomframe:SetSize( 180, 20 ) + + zoomframe.zoomslider = vgui.Create("DNumSlider", zoomframe) + zoomframe.zoomslider:DockPadding(4,0,0,0) + zoomframe.zoomslider:SetSize(200, 20) + zoomframe.zoomslider:SetMin( 0 ) + zoomframe.zoomslider:SetMax( 100 ) + zoomframe.zoomslider:SetDecimals( 0 ) + zoomframe.zoomslider:SetText("Camera FOV") + zoomframe.zoomslider:SetDark(true) + zoomframe.zoomslider:SetDefaultValue( 75 ) + + zoomframe.zoomslider:SetValue( pace.ViewFOV ) + + function zoomframe:Think(...) + pace.ViewFOV = zoomframe.zoomslider:GetValue() + if zoom_smooth:GetInt() == 1 then + pace.SetZoom(zoomframe.zoomslider:GetValue(),true) + else + pace.SetZoom(zoomframe.zoomslider:GetValue(),false) + end + end + + local hook_id = "pac_tools_menu"..util.SHA256(tostring(zoomframe)) + + pac.AddHook("VGUIMousePressed", hook_id, function(pnl, code) + pace.OverridingFOVSlider = true --to link the values with the original panel in the pac editor panel + if not IsValid(zoomframe) then + pac.RemoveHook("VGUIMousePressed", hook_id) + return + end + if code == MOUSE_LEFT or code == MOUSE_RIGHT then + if not zoomframe:IsOurChild(pnl) then + if zoomframe.zoomslider then zoomframe.zoomslider:Remove() end + zoomframe:Remove() + pac.RemoveHook("VGUIMousePressed", hook_id) + pace.OverridingFOVSlider = false + end + end + end) + +end From 3fffac5a7d17b905eff914c4c81f616bdc287677 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 15:38:50 -0400 Subject: [PATCH 057/300] fix from main branch --- lua/pac3/editor/client/parts.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index ec92f2e36..161fbd34d 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2506,7 +2506,7 @@ function pace.UltraCleanup(obj) --first pass: absolute unsafes: hidden parts for i,v in pairs(root:GetChildrenList()) do if v:IsHidden() or v.Hide then - if not FoundImportantMarkedParent(part) then + if not FoundImportantMarkedParent(v) then v:Remove() end @@ -2789,4 +2789,3 @@ tbl = { } ]] - From e6f5e056ab6930f052f25d4f9a3765731c8b32cd Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 15:53:03 -0400 Subject: [PATCH 058/300] load changes for remaining misc features read my readme from my repository for more information, but not all is documented. small things were added since then again, there are more related files in other branches but this is what didn't exactly fit in the combat or editor features --- lua/pac3/core/client/base_movable.lua | 67 +- lua/pac3/core/client/base_part.lua | 76 + lua/pac3/core/client/parts/beam.lua | 11 +- lua/pac3/core/client/parts/command.lua | 5 +- lua/pac3/core/client/parts/event.lua | 2453 +++++++++++++++- lua/pac3/core/client/parts/group.lua | 4 + .../client/parts/interpolated_multibone.lua | 230 ++ lua/pac3/core/client/parts/light.lua | 19 + lua/pac3/core/client/parts/physics.lua | 214 +- lua/pac3/core/client/parts/proxy.lua | 84 +- lua/pac3/core/client/parts/sound.lua | 22 +- lua/pac3/core/server/event.lua | 57 +- lua/pac3/core/server/net_messages.lua | 140 + .../editor/client/popups_part_tutorials.lua | 1173 ++++++++ lua/pac3/editor/client/saved_parts.lua | 139 +- lua/pac3/editor/client/settings.lua | 2560 ++++++++++++++++- lua/pac3/editor/client/wear.lua | 95 +- materials/icon64/new pac icon.png | Bin 0 -> 7621 bytes models/pac/circle.dx80.vtx | Bin 0 -> 1089 bytes models/pac/circle.dx90.vtx | Bin 0 -> 1089 bytes models/pac/circle.mdl | Bin 0 -> 1692 bytes models/pac/circle.sw.vtx | Bin 0 -> 1081 bytes models/pac/circle.vvd | Bin 0 -> 4160 bytes models/pac/plane.dx80.vtx | Bin 0 -> 189 bytes models/pac/plane.dx90.vtx | Bin 0 -> 189 bytes models/pac/plane.mdl | Bin 0 -> 1692 bytes models/pac/plane.sw.vtx | Bin 0 -> 181 bytes models/pac/plane.vvd | Bin 0 -> 320 bytes 28 files changed, 7174 insertions(+), 175 deletions(-) create mode 100644 lua/pac3/core/client/parts/interpolated_multibone.lua create mode 100644 lua/pac3/editor/client/popups_part_tutorials.lua create mode 100644 materials/icon64/new pac icon.png create mode 100644 models/pac/circle.dx80.vtx create mode 100644 models/pac/circle.dx90.vtx create mode 100644 models/pac/circle.mdl create mode 100644 models/pac/circle.sw.vtx create mode 100644 models/pac/circle.vvd create mode 100644 models/pac/plane.dx80.vtx create mode 100644 models/pac/plane.dx90.vtx create mode 100644 models/pac/plane.mdl create mode 100644 models/pac/plane.sw.vtx create mode 100644 models/pac/plane.vvd diff --git a/lua/pac3/core/client/base_movable.lua b/lua/pac3/core/client/base_movable.lua index f9886c30d..4175fe7aa 100644 --- a/lua/pac3/core/client/base_movable.lua +++ b/lua/pac3/core/client/base_movable.lua @@ -24,6 +24,9 @@ BUILDER ["player eyes"] = "PLAYEREYES", ["local eyes yaw"] = "LOCALEYES_YAW", ["local eyes pitch"] = "LOCALEYES_PITCH", + ["nearest npc or player (torso-level)"] = "NEAREST_LIFE", + ["nearest npc or player (entity position)"] = "NEAREST_LIFE_POS", + ["nearest npc or player (yaw only)"] = "NEAREST_LIFE_YAW" }}) :GetSetPart("Parent") :EndStorableVars() @@ -37,7 +40,7 @@ do -- bones function PART:GetBonePosition() local parent = self:GetParent() if parent:IsValid() then - if parent.ClassName == "jiggle" then + if parent.ClassName == "jiggle" or parent.ClassName == "interpolated_multibone" then return parent.pos, parent.ang elseif not parent.is_model_part and @@ -61,11 +64,13 @@ do -- bones function PART:GetBoneMatrix() local parent = self:GetParent() - if parent:IsValid() then - if parent.ClassName == "jiggle" then + if parent:IsValid() or IsValid(parent) then + if parent.ClassName == "jiggle" or parent.ClassName == "interpolated_multibone" then local bone_matrix = Matrix() - bone_matrix:SetTranslation(parent.pos) - bone_matrix:SetAngles(parent.ang) + if parent.pos then + bone_matrix:SetTranslation(parent.pos) + bone_matrix:SetAngles(parent.ang) + end return bone_matrix elseif not parent.is_model_part and @@ -184,6 +189,58 @@ function PART:CalcAngles(ang, wpos) return self.Angles + (pac.EyePos - wpos):Angle() end + + if pac.StringFind(self.AimPartName, "NEAREST_LIFE_YAW", true, true) then + local nearest_ent = self:GetRootPart():GetOwner() + local nearest_dist = math.huge + local owner_ent = self:GetRootPart():GetOwner() + local part_ent = self:GetOwner() + for _,ent in pairs(ents.GetAll()) do + if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then + local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() + if dist < nearest_dist then + nearest_ent = ent + nearest_dist = dist + end + end + end + local ang = (nearest_ent:GetPos() - part_ent:GetPos()):Angle() + return Angle(0,ang.y,0) + self.Angles + end + + if pac.StringFind(self.AimPartName, "NEAREST_LIFE_POS", true, true) then + local nearest_ent = self:GetRootPart():GetOwner() + local nearest_dist = math.huge + local owner_ent = self:GetRootPart():GetOwner() + local part_ent = self:GetOwner() + for _,ent in pairs(ents.GetAll()) do + if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then + local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() + if dist < nearest_dist then + nearest_ent = ent + nearest_dist = dist + end + end + end + return self.Angles + (nearest_ent:GetPos() - part_ent:GetPos()):Angle() + end + + if pac.StringFind(self.AimPartName, "NEAREST_LIFE", true, true) then + local nearest_ent = self:GetRootPart():GetOwner() + local nearest_dist = math.huge + local owner_ent = self:GetRootPart():GetOwner() + local part_ent = self:GetOwner() + for _,ent in pairs(ents.GetAll()) do + if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then + local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() + if dist < nearest_dist then + nearest_ent = ent + nearest_dist = dist + end + end + end + return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - part_ent:GetPos()):Angle() + end if self.AimPart:IsValid() and self.AimPart.GetWorldPosition then return self.Angles + (self.AimPart:GetWorldPosition() - wpos):Angle() diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 778678008..15196ba3d 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -31,6 +31,7 @@ BUILDER :StartStorableVars() :SetPropertyGroup("generic") :GetSet("Name", "") + :GetSet("Notes", "") :GetSet("Hide", false) :GetSet("EditorExpand", false, {hidden = true}) :GetSet("UniqueID", "", {hidden = true}) @@ -61,6 +62,7 @@ function PART:PreInitialize() self.hide_disturbing = false self.active_events = {} self.active_events_ref_count = 0 + if not GetConVar("pac_enable_" .. self.ClassName):GetBool() then self:SetWarning("This part class is disabled! Enable it with " .. "pac_enable_" .. self.ClassName .. " 1") end end function PART:Initialize() end @@ -200,10 +202,12 @@ do -- owner end function PART:GetParentOwner() + if self.TargetEntity:IsValid() and self.TargetEntity ~= self then return self.TargetEntity:GetOwner() end + for _, parent in ipairs(self:GetParentList()) do -- legacy behavior @@ -1147,4 +1151,76 @@ do function PART:AlwaysOnThink() end end +--the popup system +function PART:SetupEditorPopup(str, force_open, tbl) + + if not IsValid(self) then return end + + local popup_config_table = tbl or { + pac_part = self, obj_type = GetConVar("pac_popups_preferred_location"):GetString(), + hoverfunc = function() end, + doclickfunc = function() end, + panel_exp_width = 900, panel_exp_height = 400 + } + + local default_state = str == nil or str == "" + local info_string = str or self.ClassName .. "\nno special information available" + + if default_state and pace then + local partsize_tbl = pace.GetPartSizeInformation(self) + info_string = info_string .. "\n" .. partsize_tbl.info .. ", " .. partsize_tbl.all_share_percent .. "% of all parts" + end + + if self.Notes and self.Notes ~= "" then + info_string = info_string .. "\n\nNotes:\n\n" .. self.Notes + end + + local tree_node = self.pace_tree_node + local part = self + self.killpopup = false + local pnl + + --local pace = pace or {} + if tree_node then + tree_node.Label:SetTooltip(self.ClassName) + local part = self + + function tree_node:Think() + --if not part.killpopup and ((self.Label:IsHovered() and GetConVar("pac_popups_preferred_location"):GetString() == "pac tree label") or input.IsButtonDown(KEY_F1) or force_open) then + if not part.killpopup and ((self.Label:IsHovered() and GetConVar("pac_popups_preferred_location"):GetString() == "pac tree label") or force_open) then + if not self.popuppnl_is_up and not IsValid(self.popupinfopnl) and not part.killpopup then + self.popupinfopnl = pac.InfoPopup( + info_string, + popup_config_table + ) + self.popuppnl_is_up = true + end + + --if IsValid(self.popupinfopnl) then self.popupinfopnl:MakePopup() end + pnl = self.popupinfopnl + + end + if not IsValid(self.popupinfopnl) then self.popupinfopnl = nil self.popuppnl_is_up = false end + end + end + return pnl +end + +function PART:AttachEditorPopup(str, flash, tbl) + local pnl = self:SetupEditorPopup(str, flash, tbl) + if flash and pnl then + pnl:MakePopup() + end +end + +function PART:DetachEditorPopup() + local tree_node = self.pace_tree_node + if tree_node then + if tree_node.popupinfopnl then + tree_node.popupinfopnl:Remove() + end + if not IsValid(tree_node.popupinfopnl) then tree_node.popupinfopnl = nil end + end +end + BUILDER:Register() diff --git a/lua/pac3/core/client/parts/beam.lua b/lua/pac3/core/client/parts/beam.lua index ab83b816e..01ec42205 100644 --- a/lua/pac3/core/client/parts/beam.lua +++ b/lua/pac3/core/client/parts/beam.lua @@ -24,8 +24,8 @@ do local vector = Vector() local color = Color(255, 255, 255, 255) - function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size, width_start_mul, width_end_mul) - + function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size, width_start_mul, width_end_mul, width_pow) + if not veca or not vecb or not dira or not dirb then return end ax = veca.x; ay = veca.y; az = veca.z @@ -46,6 +46,7 @@ do tex_scroll = tex_scroll or 0 width_start_mul = width_start_mul or 1 width_end_mul = width_end_mul or 1 + width_pow = width_pow or 1 render_StartBeam(res + 1) @@ -66,7 +67,7 @@ do render_AddBeam( vector, - (width + ((math_sin(wave) ^ width_bend_size) * width_bend)) * Lerp(frac, width_start_mul, width_end_mul), + (width + ((math_sin(wave) ^ width_bend_size) * width_bend)) * Lerp(math.pow(frac,width_pow), width_start_mul, width_end_mul), (i / tex_stretch) + tex_scroll, color ) @@ -98,6 +99,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("WidthBendSize", 1) BUILDER:GetSet("StartWidthMultiplier", 1) BUILDER:GetSet("EndWidthMultiplier", 1) + BUILDER:GetSet("WidthMorphPower", 1) BUILDER:GetSet("TextureStretch", 1) BUILDER:GetSet("TextureScroll", 0) BUILDER:SetPropertyGroup("orientation") @@ -222,7 +224,8 @@ function PART:OnDraw() self.WidthBend, self.WidthBendSize, self.StartWidthMultiplier, - self.EndWidthMultiplier + self.EndWidthMultiplier, + self.WidthMorphPower ) end end diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index f902b6475..8e320a7b3 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -25,7 +25,10 @@ end function PART:OnShow(from_rendering) if not from_rendering and self:GetExecuteOnShow() then - self:Execute() + timer.Simple(0, function() + if self.Hide or self:IsHidden() then return end + self:Execute() + end) end end diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 4673a301a..3548a718b 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1,3 +1,6 @@ +include("pac3/core/client/part_pool.lua") +--include("pac3/editor/client/parts.lua") + local FrameTime = FrameTime local CurTime = CurTime local NULL = NULL @@ -5,6 +8,7 @@ local Vector = Vector local util = util local SysTime = SysTime + local BUILDER, PART = pac.PartTemplate("base") PART.ClassName = "event" @@ -26,21 +30,151 @@ BUILDER:StartStorableVars() return output end}) BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end}) - BUILDER:GetSet("Arguments", "", {hidden = true}) + BUILDER:GetSet("Arguments", "", {hidden = false}) BUILDER:GetSet("Invert", true) BUILDER:GetSet("RootOwner", true) BUILDER:GetSet("AffectChildrenOnly", false) BUILDER:GetSet("ZeroEyePitch", false) - BUILDER:GetSetPart("TargetPart") + BUILDER:GetSetPart("TargetPart", {editor_friendly = "ExternalOriginPart"}) + BUILDER:GetSetPart("DestinationPart", {editor_friendly = "TargetedPart"}) BUILDER:EndStorableVars() +local registered_command_event_series = {} + +function PART:register_command_event(str,b) + local ply = self:GetPlayerOwner() + + local event = str + local flush = b + + local num = tonumber(string.sub(event, string.find(event,"[%d]+$") or 0)) or 0 + + if string.find(event,"[%d]+$") then + event = string.gsub(event,"[%d]+$","") + end + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} + + if flush then + ply.pac_command_event_sequencebases[event] = nil + return + end + + if ply.pac_command_event_sequencebases[event] and string.find(str,"[%d]+$") then + ply.pac_command_event_sequencebases[event].max = math.max(ply.pac_command_event_sequencebases[event].max,num) + else + ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = num} + end + +end + +function PART:fix_event_operator() + --check if exists + --check class + --check current operator + --PrintTable(PART.Events[self.Event]) + if PART.Events[self.Event] then + local event_type = PART.Events[self.Event].operator_type + if event_type == "number" then + if self.Operator == "find" or self.Operator == "find simple" then + self.Operator = PART.Events[self.Event].preferred_operator --which depends, it's usually above but we'll have cases where it's best to have below, or equal + self:SetInfo("The operator was automatically changed to work with this event type, which handles numbers") + end + + elseif event_type == "string" then + if self.Operator ~= "find" and self.Operator ~= "find simple" and self.Operator ~= "equal" then + self.Operator = PART.Events[self.Event].preferred_operator --find simple + self:SetInfo("The operator was automatically changed to work with this event type, which handles strings (text)") + end + elseif event_type == "mixed" then + self:SetInfo("This event is mixed, which means it might have different behaviour with numeric operators or string operators. Some of these are that way because they're using different sources of data at once (e.g. addons' weapons can use different formats for fire modes), and we want to catch the most valid uses possible to fit what the event says") + --do nothing but still warn about it being a special complex event + elseif event_type == "none" then + --do nothing + end + end +end + +function PART:AttachEditorPopup(str) + + local info_string = str or "no information available" + local verbosity = "" + if self.Event ~= "" then + if PART.Events[self.Event] then + info_string = PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory" + else + info_string = "invalid event" + end + --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + + --end + + str = info_string or str + end + self:SetupEditorPopup(info_string, true) +end + function PART:SetEvent(event) local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) + + if not self.Events[event] then --invalid event? try a command event + local command_was_found_in_cmd_part = false + for i,v in ipairs(pac.GetLocalParts()) do + if v.ClassName == "command" then + if string.find(v.String, "pac_event " .. event) then + command_was_found_in_cmd_part = true + end + end + end + if self:GetPlayerOwner().pac_command_events or command_was_found_in_cmd_part then + self:GetPlayerOwner().pac_command_events = self:GetPlayerOwner().pac_command_events or {} + if self:GetPlayerOwner().pac_command_events[event] or command_was_found_in_cmd_part or GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then + timer.Simple(0.2, function() + --now we'll use event as a command name + self:SetEvent("command") + self.pace_properties["Event"]:SetValue("command") + + self:SetArguments(event .. "@@0") + self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") + pace.PopulateProperties(self) + + end) + return + end + end + end self.Event = event self:SetWarning() + self:SetInfo() + + --foolproofing: fix the operator to match the event's type, and fix arguments as needed + self:fix_event_operator() + self:fix_args() + + pace.changed_event = self --a reference to make it refresh the popup label panel + pace.changed_event_time = CurTime() + + if self == pace.current_part and GetConVar("pac_copilot_make_popup_when_selecting_event"):GetBool() then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit + self:GetDynamicProperties(reset) + if not GetConVar("pac_editor_remember_divider_height"):GetBool() then pace.Editor.div:SetTopHeight(ScrH() - 520) end + +end + +function PART:Initialize() + if self:GetPlayerOwner() == LocalPlayer() then + timer.Simple(0.2, function() + if self.Event == "command" then + local cmd, time, hide = self:GetParsedArgumentsForObject(self.Events.command) + self:register_command_event(cmd, true) + timer.Simple(0.2, function() + self:register_command_event(cmd, false) + end) + end + end) + end + end local function get_default(typ) @@ -149,9 +283,92 @@ for k,v in pairs(_G) do end end +local grounds_enums = { + ["MAT_ANTLION"] = "65", + ["MAT_BLOODYFLESH"] = "66", + ["MAT_CONCRETE"] = "67", + ["MAT_DIRT"] = "68", + ["MAT_EGGSHELL"] = "69", + ["MAT_FLESH"] = "70", + ["MAT_GRATE"] = "71", + ["MAT_ALIENFLESH"] = "72", + ["MAT_CLIP"] = "73", + ["MAT_SNOW"] = "74", + ["MAT_PLASTIC"] = "76", + ["MAT_METAL"] = "77", + ["MAT_SAND"] = "78", + ["MAT_FOLIAGE"] = "79", + ["MAT_COMPUTER"] = "80", + ["MAT_SLOSH"] = "83", + ["MAT_TILE"] = "84", + ["MAT_GRASS"] = "85", + ["MAT_VENT"] = "86", + ["MAT_WOOD"] = "87", + ["MAT_DEFAULT"] = "88", + ["MAT_GLASS"] = "89", + ["MAT_WARPSHIELD"] = "90" +} + +local grounds_enums_reverse = { + ["65"] = "antlion", + ["66"] = "bloody flesh", + ["67"] = "concrete", + ["68"] = "dirt", + ["69"] = "egg shell", + ["70"] = "flesh", + ["71"] = "grate", + ["72"] = "alien flesh", + ["73"] = "clip", + ["74"] = "snow", + ["76"] = "plastic", + ["77"] = "metal", + ["78"] = "sand", + ["79"] = "foliage", + ["80"] = "computer", + ["83"] = "slosh", + ["84"] = "tile", + ["85"] = "grass", + ["86"] = "vent", + ["87"] = "wood", + ["88"] = "default", + ["89"] = "glass", + ["90"] = "warp shield" +} + +local animation_event_enums = { + "attack primary", + "swim", + "flinch rightleg", + "flinch leftarm", + "flinch head", + "cancel", + "attack secondary", + "flinch rightarm", + "jump", + "snap yaw", + "attack grenade", + "custom", + "cancel reload", + "reload loop", + "custom gesture sequence", + "custom sequence", + "spawn", + "doublejump", + "flinch leftleg", + "flinch chest", + "die", + "reload end", + "reload", + "custom gesture" +} + + + PART.Events = {} PART.OldEvents = { + random = { + operator_type = "number", preferred_operator = "above", arguments = {{compare = "number"}}, callback = function(self, ent, compare) return self:NumberOperator(math.random(), compare) @@ -159,6 +376,7 @@ PART.OldEvents = { }, randint = { + operator_type = "number", preferred_operator = "above", arguments = {{compare = "number"}, {min = "number"}, {max = "number"}}, callback = function(self, ent, compare, min, max) min = min or 0 @@ -169,6 +387,8 @@ PART.OldEvents = { }, random_timer = { + operator_type = "none", + tutorial_explanation = "random_timer picks a number between min and max, waits this amount of seconds,\nthen activates for the amount of seconds from holdtime.\nafter this is over, it picks a new random number and starts waiting again", arguments = {{min = "number"}, {max = "number"}, {holdtime = "number"}}, callback = function(self, ent, min, max, holdtime) @@ -203,11 +423,18 @@ PART.OldEvents = { }, timerx = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "timerx is a stopwatch that counts time since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes", arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}}, + userdata = { + {default = 0, timerx_property = "seconds"}, + {default = true, timerx_property = "reset_on_hide"} + }, nice = function(self, ent, seconds) return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?" end, callback = function(self, ent, seconds, reset_on_hide, synced_time) + local time = synced_time and CurTime() or RealTime() self.time = self.time or time @@ -217,15 +444,18 @@ PART.OldEvents = { return false end self.number = time - self.time - + return self:NumberOperator(self.number, seconds) end, }, timersys = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "like timerx, timersys is a stopwatch that counts time (it uses SysTime()) since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes", arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}}, callback = function(self, ent, seconds, reset_on_hide) + local time = SysTime() self.time = self.time or time @@ -239,6 +469,7 @@ PART.OldEvents = { }, map_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(game.GetMap(), find) @@ -246,6 +477,7 @@ PART.OldEvents = { }, fov = { + operator_type = "number", preferred_operator = "above", arguments = {{fov = "number"}}, callback = function(self, ent, fov) ent = try_viewmodel(ent) @@ -258,6 +490,7 @@ PART.OldEvents = { end, }, health_lost = { + operator_type = "number", preferred_operator = "above", arguments = {{amount = "number"}}, callback = function(self, ent, amount) @@ -299,6 +532,7 @@ PART.OldEvents = { }, holdtype = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -307,9 +541,18 @@ PART.OldEvents = { return true end end, + nice = function(self, ent, find) + local str = "holdtype ["..self.Operator.. " " .. find .. "] | " + local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL + if wep:IsValid() then + str = str .. wep:GetHoldType() + end + return str + end }, is_crouching = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.Crouching and ent:Crouching() @@ -317,6 +560,7 @@ PART.OldEvents = { }, is_typing = { + operator_type = "none", callback = function(self, ent) ent = self:GetPlayerOwner() return ent.IsTyping and ent:IsTyping() @@ -324,6 +568,7 @@ PART.OldEvents = { }, using_physgun = { + operator_type = "none", callback = function(self, ent) ent = self:GetPlayerOwner() local pac_drawphysgun_event_part = ent.pac_drawphysgun_event_part @@ -336,11 +581,42 @@ PART.OldEvents = { end, }, + is_using_entity = { + operator_type = "none", + tutorial_explanation = "For when you're picking up props, clicking buttons etc.\nAlthough not all entities will do things if you +use them, the event tries to take the class of the entity you used,\nand compares it with the one written in class", + arguments = {{class = "string"}}, + callback = function(self, ent, class) + ent = self:GetPlayerOwner() + local b = false + + if not ent:IsPlayer() then return false + elseif ent == LocalPlayer() and self.singleactivatestate then + net.Start("pac.RequestPlayerObjUsed") + net.SendToServer() + self.singleactivatestate = false + self.nextactivationrefresh = CurTime() + 0.05 + end + if ent.entity_inuse or ent.entity_inuse_classname then + if ent.entity_inuse_classname == "player_pickup" then + b = true + end + if IsValid(ent.entity_inuse) and string.find(ent.entity_inuse_classname, class, 1, true) and string.find(ent.entity_inuse:GetClass(), class, 1, true) then + b = true + end + end + + return b + end + }, + eyetrace_entity_class = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "this compares the class of the entity you point to with the one(s) written in class", arguments = {{class = "string"}}, callback = function(self, ent, find) if ent.GetEyeTrace then ent = ent:GetEyeTrace().Entity + if not IsValid(ent) then return false end if self:StringOperator(ent:GetClass(), find) then return true end @@ -349,8 +625,10 @@ PART.OldEvents = { }, owner_health = { + operator_type = "number", preferred_operator = "above", arguments = {{health = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.Health then return self:NumberOperator(ent:Health(), num) @@ -360,8 +638,10 @@ PART.OldEvents = { end, }, owner_max_health = { + operator_type = "number", preferred_operator = "above", arguments = {{health = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.GetMaxHealth then return self:NumberOperator(ent:GetMaxHealth(), num) @@ -371,6 +651,7 @@ PART.OldEvents = { end, }, owner_alive = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) if ent.Alive then @@ -380,8 +661,10 @@ PART.OldEvents = { end, }, owner_armor = { + operator_type = "number", preferred_operator = "above", arguments = {{armor = "number"}}, callback = function(self, ent, num) + ent = try_viewmodel(ent) if ent.Armor then return self:NumberOperator(ent:Armor(), num) @@ -392,31 +675,36 @@ PART.OldEvents = { }, owner_scale_x = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, owner_scale_y = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, owner_scale_z = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) - ent = try_viewmodel(ent) - return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num) + ent = try_viewmodel(ent) + return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num) end, }, pose_parameter = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "pose parameters are values used in models for body movement and animation.\nthis event searches a pose parameter and compares its normalized (0-1 range) value with the number defined in num", arguments = {{name = "string"}, {num = "number"}}, callback = function(self, ent, name, num) ent = try_viewmodel(ent) @@ -424,7 +712,23 @@ PART.OldEvents = { end, }, + pose_parameter_true = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "pose parameters are values used in models for body movement and animation.\nthis event searches a pose parameter and compares its true (as opposed to normalized into the 0-1 range) value with number defined in num", + arguments = {{name = "string"}, {num = "number"}}, + callback = function(self, ent, name, num) + ent = try_viewmodel(ent) + if ent:IsValid() then + local min,max = ent:GetPoseParameterRange(ent:LookupPoseParameter(name)) + if not min or not max then return 0 end + local actual_value = min + (max - min)*(ent:GetPoseParameter(name)) + return self:NumberOperator(actual_value, num) + end + end, + }, + speed = { + operator_type = "number", preferred_operator = "equal", arguments = {{speed = "number"}}, callback = function(self, ent, num) ent = try_viewmodel(ent) @@ -433,6 +737,8 @@ PART.OldEvents = { }, 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", arguments = {{level = "number"}}, callback = function(self, ent, num) ent = try_viewmodel(ent) @@ -441,6 +747,7 @@ PART.OldEvents = { }, is_on_fire = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent:IsOnFire() @@ -448,17 +755,23 @@ PART.OldEvents = { }, client_spawned = { + operator_type = "number", preferred_operator = "below", + tutorial_explanation = "client_spawned supposedly activates for some time after you spawn", + arguments = {{time = "number"}}, callback = function(self, ent, time) time = time or 0.1 ent = try_viewmodel(ent) - if ent.pac_playerspawn and ent.pac_playerspawn + time > pac.RealTime then - return true + if ent.pac_playerspawn then + return self:NumberOperator(pac.RealTime, ent.pac_playerspawn + time) end + return false end, }, is_client = { + operator_type = "none", + tutorial_explanation = "is_client makes something visible only for you, or others (uninverted)", callback = function(self, ent) ent = try_viewmodel(ent) return self:GetPlayerOwner() == ent @@ -466,6 +779,7 @@ PART.OldEvents = { }, is_flashlight_on = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.FlashlightIsOn and ent:FlashlightIsOn() @@ -473,6 +787,7 @@ PART.OldEvents = { }, collide = { + operator_type = "none", callback = function(self, ent) ent.pac_event_collide_callback = ent.pac_event_collide_callback or ent:AddCallback("PhysicsCollide", function(ent, data) ent.pac_event_collision_data = data @@ -489,9 +804,15 @@ PART.OldEvents = { }, ranger = { + operator_type = "number", preferred_operator = "below", + tutorial_explanation = "ranger looks in a line to see if something is in front (red arrow) of its host's (parent) model;\ndetected things could be found before(below) or beyond(above) the distance defined in compare;\nthe event will only look as far as the distance defined in distance", arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}}, - userdata = {{editor_panel = "ranger", ranger_property = "distance"}, {editor_panel = "ranger", ranger_property = "compare"}}, + userdata = { + {default = 15, editor_panel = "ranger", ranger_property = "distance"}, + {default = 5, editor_panel = "ranger", ranger_property = "compare"} + }, callback = function(self, ent, distance, compare, npcs_and_players_only) + local parent = self:GetParentEx() if parent:IsValid() and parent.GetWorldPosition then @@ -508,7 +829,7 @@ PART.OldEvents = { if npcs_and_players_only and (not res.Entity:IsPlayer() and not res.Entity:IsNPC()) then return false end - + return self:NumberOperator(res.Fraction * distance, compare) else local classname = parent:GetNiceName() @@ -516,6 +837,100 @@ PART.OldEvents = { self:SetWarning(("ranger doesn't work on [%s] %s"):format(classname, classname ~= name and "(" .. name .. ")" or "")) end end, + nice = function(self, ent, distance, compare, npcs_and_players_only) + local str = "ranger: [" .. self.Operator .. " " .. compare .. "]" + return str + end + }, + + ground_surface = { + operator_type = "mixed", + tutorial_explanation = "ground_surface checks what ground you're standing on, and activates if it matches among the IDs written in surfaces.\nMatch multiple with ;", + arguments = {{exclude_noclip = "boolean"}, {surfaces = "string"}}, + userdata = {{}, {enums = function() + --grounds_enums = + return + { + ["MAT_ANTLION"] = "65", + ["MAT_BLOODYFLESH"] = "66", + ["MAT_CONCRETE"] = "67", + ["MAT_DIRT"] = "68", + ["MAT_EGGSHELL"] = "69", + ["MAT_FLESH"] = "70", + ["MAT_GRATE"] = "71", + ["MAT_ALIENFLESH"] = "72", + ["MAT_CLIP"] = "73", + ["MAT_SNOW"] = "74", + ["MAT_PLASTIC"] = "76", + ["MAT_METAL"] = "77", + ["MAT_SAND"] = "78", + ["MAT_FOLIAGE"] = "79", + ["MAT_COMPUTER"] = "80", + ["MAT_SLOSH"] = "83", + ["MAT_TILE"] = "84", + ["MAT_GRASS"] = "85", + ["MAT_VENT"] = "86", + ["MAT_WOOD"] = "87", + ["MAT_DEFAULT"] = "88", + ["MAT_GLASS"] = "89", + ["MAT_WARPSHIELD"] = "90" + } end}}, + nice = function(self, ent, exclude_noclip, surfaces) + local grounds_enums_reverse = { + ["65"] = "antlion", + ["66"] = "bloody flesh", + ["67"] = "concrete", + ["68"] = "dirt", + ["69"] = "egg shell", + ["70"] = "flesh", + ["71"] = "grate", + ["72"] = "alien flesh", + ["73"] = "clip", + ["74"] = "snow", + ["76"] = "plastic", + ["77"] = "metal", + ["78"] = "sand", + ["79"] = "foliage", + ["80"] = "computer", + ["83"] = "slosh", + ["84"] = "tile", + ["85"] = "grass", + ["86"] = "vent", + ["87"] = "wood", + ["88"] = "default", + ["89"] = "glass", + ["90"] = "warp shield" + } + surfaces = surfaces or "" + local str = "ground surface: " + for i,v in ipairs(string.Split(surfaces,";")) do + local element = grounds_enums_reverse[v] or "" + str = str .. element + if i ~= #string.Split(surfaces,";") then + str = str .. ", " + end + end + return str + end, + callback = function(self, ent, exclude_noclip, surfaces, down) + surfaces = surfaces or "" + if exclude_noclip and ent:GetMoveType() == MOVETYPE_NOCLIP then return false end + local trace = util.TraceLine( { + start = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, 10), + endpos = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, -30 ), + filter = function(ent) + if ent == self:GetRootPart():GetOwner() or ent == self:GetPlayerOwner() then return false end + end + }) + local found = false + if trace.Hit then + local surfs = string.Split(surfaces,";") + for _,surf in pairs(surfs) do + if surf == tostring(trace.MatType) then found = true end + end + end + return found + end }, is_on_ground = { @@ -546,12 +961,68 @@ PART.OldEvents = { return false end, }, - + + --this one uses util.TraceHull is_touching = { - arguments = {{extra_radius = "number"}}, - userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius"}}, - callback = function(self, ent, extra_radius) + operator_type = "none", + tutorial_explanation = "is_touching checks in a box (util.TraceHull) around the host model to see if there's something inside it.\nusually it's the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner,to override the old root owner setting,\nin case of issues when stacking this event inside other events", + arguments = {{extra_radius = "number"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = 0}}, + callback = function(self, ent, extra_radius, nearest_model) + if nearest_model then ent = self:GetOwner() end + extra_radius = extra_radius or 0 + + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.max(radius + extra_radius + 1, 1) + + local mins = Vector(-1,-1,-1) + local maxs = Vector(1,1,1) + local startpos = ent:WorldSpaceCenter() + mins = mins * radius + maxs = maxs * radius + + + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {ent, self:GetRootPart():GetOwner()} + }) + + return tr.Hit + end, + nice = function(self, ent, extra_radius, nearest_model) + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. "]" + return str + end, + }, + --this one uses ents.FindInBox + is_touching_filter = { + operator_type = "none", + tutorial_explanation = "is_touching_filter checks in a box (ents.FindInBox) around the host model to see if there's something inside it, but you can selectively exclude living things (NPCs or players) from being detected.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + arguments = {{extra_radius = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = false}, {default = false}, {default = false}}, + callback = function(self, ent, extra_radius, no_npc, no_players, nearest_model) + if nearest_model then ent = self:GetOwner() end extra_radius = extra_radius or 0 + no_npc = no_npc or false + no_players = no_players or false + nearest_model = nearest_model or false local radius = ent:BoundingRadius() @@ -564,6 +1035,126 @@ PART.OldEvents = { local mins = Vector(-1,-1,-1) local maxs = Vector(1,1,1) local startpos = ent:WorldSpaceCenter() + mins = startpos + mins * radius + maxs = startpos + maxs * radius + + local b = false + local ents_hits = ents.FindInBox(mins, maxs) + for _,ent2 in pairs(ents_hits) do + if (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) and + not ( (no_npc and ent2:IsNPC()) or (no_players and ent2:IsPlayer()) ) + then b = true end + end + + return b + end, + nice = function(self, ent, extra_radius, no_npc, no_players, nearest_model) + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. "]" + if no_npc or no_players then str = str .. " | " end + if no_npc then str = str .. "no_npc " end + if no_players then str = str .. "no_players " end + return str + end + }, + --this one uses ents.FindInBox + is_touching_life = { + operator_type = "none", + tutorial_explanation = "is_touching_life checks in a stretchable box (ents.FindInBox) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}, {default = false}, {default = false}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) + + if nearest_model then ent = self:GetOwner() end + extra_radius = extra_radius or 0 + no_npc = no_npc or false + no_players = no_players or false + x_stretch = x_stretch or 1 + y_stretch = y_stretch or 1 + z_stretch = z_stretch or 1 + nearest_model = nearest_model or false + + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.max(radius + extra_radius + 1, 1) + + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + local startpos = ent:WorldSpaceCenter() + mins = startpos + mins * radius + maxs = startpos + maxs * radius + + local ents_hits = ents.FindInBox(mins, maxs) + local b = false + for _,ent2 in pairs(ents_hits) do + if IsValid(ent2) and (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and + (ent2:IsNPC() or ent2:IsPlayer()) + + then + b = true + if ent2:IsNPC() and no_npc then + b = false + elseif ent2:IsPlayer() and no_players then + b = false + end + if b then return b end + end + end + + return b + end, + nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) + + if nearest_model then ent = self:GetOwner() end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]" + if no_npc or no_players then str = str .. " | " end + if no_npc then str = str .. "no_npc " end + if no_players then str = str .. "no_players " end + return str + end, + }, + --this one uses util.TraceHull + is_touching_scalable = { + operator_type = "none", + tutorial_explanation = "is_touching_life checks in a stretchable box (util.TraceHull) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", + + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {nearest_model = "boolean"}}, + userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) + if nearest_model then ent = self:GetOwner() end + extra_radius = extra_radius or 15 + x_stretch = x_stretch or 1 + y_stretch = y_stretch or 1 + z_stretch = z_stretch or 1 + nearest_model = nearest_model or false + + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) + local maxs = Vector(x_stretch,y_stretch,z_stretch) + local startpos = ent:WorldSpaceCenter() + + radius = math.max(extra_radius, 1) mins = mins * radius maxs = maxs * radius @@ -572,13 +1163,37 @@ PART.OldEvents = { endpos = startpos, maxs = maxs, mins = mins, - filter = ent + filter = {self:GetRootPart():GetOwner(),ent} } ) return tr.Hit end, + nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) + if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return false end + local radius = ent:BoundingRadius() + + if radius == 0 and IsValid(ent.pac_projectile) then + radius = ent.pac_projectile:GetRadius() + end + + radius = math.Round(math.max(radius + extra_radius + 1, 1)) + + local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]" + return str + end, + }, + + is_explicit = { + operator_type = "none", + tutorial_explanation = "is_explicit activates for viewers who want to hide explicit content with pac_hide_disturbing.\nyou can make special censoring effects for them, for example", + + callback = function(self, ent) + return GetConVar("pac_hide_disturbing"):GetBool() + end }, is_in_noclip = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent:GetMoveType() == MOVETYPE_NOCLIP and (not ent.GetVehicle or not ent:GetVehicle():IsValid()) @@ -586,6 +1201,7 @@ PART.OldEvents = { }, is_voice_chatting = { + operator_type = "none", callback = function(self, ent) ent = try_viewmodel(ent) return ent.IsSpeaking and ent:IsSpeaking() @@ -593,6 +1209,7 @@ PART.OldEvents = { }, ammo = { + operator_type = "number", preferred_operator = "above", arguments = {{primary = "boolean"}, {amount = "number"}}, userdata = {{editor_onchange = function(part, num) return math.Round(num) end}}, callback = function(self, ent, primary, amount) @@ -605,6 +1222,7 @@ PART.OldEvents = { end, }, total_ammo = { + operator_type = "number", preferred_operator = "above", arguments = {{ammo_id = "string"}, {amount = "number"}}, callback = function(self, ent, ammo_id, amount) if ent.GetAmmoCount then @@ -623,6 +1241,7 @@ PART.OldEvents = { }, clipsize = { + operator_type = "number", preferred_operator = "above", arguments = {{primary = "boolean"}, {amount = "number"}}, callback = function(self, ent, primary, amount) ent = try_viewmodel(ent) @@ -635,6 +1254,7 @@ PART.OldEvents = { }, vehicle_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -647,6 +1267,7 @@ PART.OldEvents = { }, vehicle_model = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -659,6 +1280,7 @@ PART.OldEvents = { }, driver_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = ent.GetDriver and ent:GetDriver() or NULL @@ -670,6 +1292,7 @@ PART.OldEvents = { }, entity_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(ent:GetClass(), find) @@ -677,6 +1300,7 @@ PART.OldEvents = { }, weapon_class = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}, {hide = "boolean"}}, callback = function(self, ent, find, hide) ent = try_viewmodel(ent) @@ -701,6 +1325,7 @@ PART.OldEvents = { }, has_weapon = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) ent = try_viewmodel(ent) @@ -717,6 +1342,7 @@ PART.OldEvents = { }, model_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) return self:StringOperator(ent:GetModel(), find) @@ -724,9 +1350,14 @@ PART.OldEvents = { }, sequence_name = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, - nice = function(self, ent) - return self.sequence_name or "invalid sequence" + nice = function(self, ent, find) + local anim = find + if find == "" then anim = "" end + local str = self.Event .. " ["..self.Operator.. " " .. anim .. "] | " + local seq = self.sequence_name or "invalid sequence" + return str .. seq end, callback = function(self, ent, find) ent = get_owner(self) @@ -738,6 +1369,7 @@ PART.OldEvents = { }, timer = { + operator_type = "none", arguments = {{interval = "number"}, {offset = "number"}}, callback = function(self, ent, interval, offset) interval = interval or 1 @@ -753,21 +1385,42 @@ PART.OldEvents = { }, animation_event = { - arguments = {{find = "string"}, {time = "number"}}, - nice = function(self) - return self.anim_name or "" + operator_type = "string", preferred_operator = "find simple", + arguments = {{find = "string"}, {time = "number"}, {try_stop_gesture = "boolean"}}, + userdata = {{default = "attack primary", enums = function() + local tbl = {} + for i,v in pairs(animation_event_enums) do + tbl[i] = v + end + return tbl + end}, {default = 0.5}}, + nice = function(self, ent, find, time) + find = find or "" + time = time or 0 + local anim = self.anim_name or "" + local str = self.Event .. " ["..self.Operator.. " \"" .. find .. "\" : " .. time .. " seconds] | " + return str .. anim end, - callback = function(self, ent, find, time) + callback = function(self, ent, find, time, try_stop_gesture) time = time or 0.1 ent = get_owner(self) local data = ent.pac_anim_event local b = false - + if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then data.reset = false b = true + if try_stop_gesture then + if string.find(find, "attack grenade") then + ent:AnimResetGestureSlot( GESTURE_SLOT_GRENADE ) + elseif string.find(find, "attack") or string.find(find, "reload") then + ent:AnimResetGestureSlot( GESTURE_SLOT_ATTACK_AND_RELOAD ) + elseif string.find(find, "flinch") then + ent:AnimResetGestureSlot( GESTURE_SLOT_FLINCH ) + end + end end if b then @@ -781,6 +1434,8 @@ PART.OldEvents = { }, fire_bullets = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "fire_bullets supposedly checks what types of bullets you're firing", arguments = {{find_ammo = "string"}, {time = "number"}}, callback = function(self, ent, find, time) time = time or 0.1 @@ -790,7 +1445,7 @@ PART.OldEvents = { local data = ent.pac_fire_bullets local b = false - if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then + if data and (self:StringOperator(data.name, find_ammo) and (time == 0 or data.time + time > pac.RealTime)) then data.reset = false b = true end @@ -800,6 +1455,7 @@ PART.OldEvents = { }, emit_sound = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find_sound = "string"}, {time = "number"}, {mute = "boolean"}}, callback = function(self, ent, find, time, mute) time = time or 0.1 @@ -822,19 +1478,22 @@ PART.OldEvents = { }, command = { + operator_type = "string", preferred_operator = "equal", + tutorial_explanation = "the command event reads your pac_event states.\nthe pac_event command can turn on (1), off (0) or toggle (2) a state that has a name.\nfor example, \"pac_event myhat 2\" can be used with a myhat command event to put the hat on or off\n\nwith this event, you read the states that contain this find name\n(equal being an exact match; find and find simple allowing to detect from different states having a part of the name)\n\nthe final result is to activate if:\n\tA) there's one active, or \n\tB) there's one recently turned off not too long ago", arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}}, userdata = { {default = "change_me", editor_friendly = "CommandName"}, - {default = 0.1, editor_friendly = "EventDuration"}, + {default = 0, editor_friendly = "EventDuration"}, {default = false, group = "event wheel", editor_friendly = "HideInEventWheel"} }, nice = function(self, ent, find, time) find = find or "?" time = time or "?" - return "command: " .. find .. " | " .. "duration: " .. time + return "command: [" .. self.Operator .. " " .. find .."] | " .. "duration: " .. time end, callback = function(self, ent, find, time) - time = time or 0.1 + + time = time or 0 local ply = self:GetPlayerOwner() @@ -857,6 +1516,8 @@ PART.OldEvents = { }, say = { + operator_type = "string", preferred_operator = "find simple", + tutorial_explanation = "say looks at the chat to find if a certain thing has been said some time ago", arguments = {{find = "string"}, {time = "number"}, {all_players = "boolean"}}, callback = function(self, ent, find, time, all_players) time = time or 0.1 @@ -886,6 +1547,7 @@ PART.OldEvents = { -- outfit owner owner_velocity_length = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -899,6 +1561,7 @@ PART.OldEvents = { end, }, owner_velocity_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -911,6 +1574,7 @@ PART.OldEvents = { end, }, owner_velocity_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -923,6 +1587,7 @@ PART.OldEvents = { end, }, owner_velocity_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -935,6 +1600,7 @@ PART.OldEvents = { end, }, owner_velocity_world_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -947,6 +1613,7 @@ PART.OldEvents = { end, }, owner_velocity_world_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -959,6 +1626,7 @@ PART.OldEvents = { end, }, owner_velocity_world_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) ent = try_viewmodel(ent) @@ -973,6 +1641,7 @@ PART.OldEvents = { -- parent part parent_velocity_length = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -989,6 +1658,7 @@ PART.OldEvents = { end, }, parent_velocity_forward = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1005,6 +1675,7 @@ PART.OldEvents = { end, }, parent_velocity_right = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1021,6 +1692,7 @@ PART.OldEvents = { end, }, parent_velocity_up = { + operator_type = "number", preferred_operator = "above", arguments = {{speed = "number"}}, callback = function(self, ent, speed) local parent = self:GetParentEx() @@ -1038,6 +1710,7 @@ PART.OldEvents = { }, parent_scale_x = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1054,6 +1727,7 @@ PART.OldEvents = { end, }, parent_scale_y = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1070,6 +1744,7 @@ PART.OldEvents = { end, }, parent_scale_z = { + operator_type = "number", preferred_operator = "above", arguments = {{scale = "number"}}, callback = function(self, ent, num) local parent = self:GetParentEx() @@ -1087,6 +1762,7 @@ PART.OldEvents = { }, gravitygun_punt = { + operator_type = "number", preferred_operator = "above", arguments = {{time = "number"}}, callback = function(self, ent, time) time = time or 0.1 @@ -1095,13 +1771,14 @@ PART.OldEvents = { local punted = ent.pac_gravgun_punt - if punted and punted + time > pac.RealTime then - return true + if punted then + return self:NumberOperator(pac.RealTime, punted + time) end end, }, movetype = { + operator_type = "string", preferred_operator = "find simple", arguments = {{find = "string"}}, callback = function(self, ent, find) local mt = ent:GetMoveType() @@ -1112,6 +1789,8 @@ PART.OldEvents = { }, dot_forward = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles", arguments = {{normal = "number"}}, callback = function(self, ent, normal) @@ -1128,6 +1807,9 @@ PART.OldEvents = { }, dot_right = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) @@ -1144,6 +1826,9 @@ PART.OldEvents = { }, flat_dot_forward = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) local owner = self:GetRootPart():GetOwner() @@ -1163,6 +1848,9 @@ PART.OldEvents = { }, flat_dot_right = { + operator_type = "number", preferred_operator = "above", + tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference", + arguments = {{normal = "number"}}, callback = function(self, ent, normal) local owner = self:GetRootPart():GetOwner() @@ -1179,48 +1867,838 @@ PART.OldEvents = { return 0 end - } -} + }, -do - local enums = {} - local enums2 = {} - for key, val in pairs(_G) do - if isstring(key) and isnumber(val) then - if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then - enums[val] = key:sub(5):lower() - enums2[enums[val]] = val - elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then - enums[val] = key:lower() - enums2[enums[val]] = val + is_sitting = { + operator_type = "none", + callback = function(self, ent) + if not ent:GetVehicle() then return false end + if ent.GetSitting then return (IsValid(ent:GetVehicle()) or ent:GetSitting()) and ent:GetVehicle():GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" end --sit anywhere script + return IsValid(ent:GetVehicle()) and ent:GetVehicle():GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" --no prison pod! + end + }, + + is_driving = { + operator_type = "none", + callback = function(self, ent) + if not ent:IsPlayer() then return false end + local vehicle = ent:GetVehicle() + + if IsValid(vehicle) then --vehicle entity exists + if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent + --print(vehicle:GetParent().PassengerSeats) + + if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys + return ent:IsDrivingSimfphys() and ent:GetVehicle() == ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat + elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too + --print(vehicle:GetParent().BaseClass.ClassName, #vehicle:GetParent().Seats) + --PrintTable(vehicle:GetParent().Seats[1]) + return vehicle == vehicle.wac_seatswitcher.seats[1] --first seat + end + elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we don't want bare seats or prisoner pod + if vehicle.HandleAnimation == true and not isfunction(vehicle.HandleAnimation) and vehicle:GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" then --exclude prisoner pod and narrow down to SCars + return true + end + return false + else --assume that most other classes than prop_vehicle_prisoner_pod are drivable vehicles + return true + end end + return false end - end + }, - pac.key_enums = enums + is_passenger = { + operator_type = "none", + callback = function(self, ent) + if not ent:IsPlayer() then return false end + local vehicle = ent:GetVehicle() + + if IsValid(vehicle) then --vehicle entity exists + if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent + if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys + return ent:IsDrivingSimfphys() and ent:GetVehicle() ~= ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat + elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too + return vehicle ~= vehicle.wac_seatswitcher.seats[1] --first seat + end + elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we can count bare seats and prisoner pods as passengers + return true + else --assume that most other classes than prop_vehicle_prisoner_pod are drivable vehicles, but they're also probably single seaters so... + return false + end + end + return false + end + }, - --TODO: Rate limit!!! - net.Receive("pac.BroadcastPlayerButton", function() - local ply = net.ReadEntity() + weapon_iron_sight = { + operator_type = "none", + callback = function(self, ent) + if not IsValid(ent) or ent:Health() < 1 then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + if wep.IsFAS2Weapon then + return wep.dt.Status == FAS_STAT_ADS + end - if not ply:IsValid() then return end + if wep.GetIronSights then return wep:GetIronSights() end + if wep.Sighted then return wep:GetActiveSights() end --arccw + return false + end + }, - if ply == pac.LocalPlayer and (pace and pace.IsFocused() or gui.IsConsoleVisible()) then return end + weapon_firemode = { + operator_type = "mixed", + arguments = {{name_or_id = "string"}}, + callback = function(self, ent, name_or_id) + name_or_id = string.lower(name_or_id) + if not IsValid(ent) or ent:Health() < 1 then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + + if wep.ArcCW then + if wep.Firemodes[wep:GetFireMode()] then --some use a Firemodes table + if wep.Firemodes[wep:GetFireMode()].PrintName then + return + self:StringOperator(name_or_id, wep.Firemodes[wep:GetFireMode()].PrintName) + or self:StringOperator(name_or_id, wep:GetFiremodeName()) + or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end + elseif wep.Primary then + if wep.Primary.Automatic ~= nil then + if wep.Primary.Automatic == true then + return name_or_id == "automatic" or name_or_id == "auto" + else + return name_or_id == "semi-automatic" or name_or_id == "semi-auto" or name_or_id == "single" + end + end + self:StringOperator(name_or_id, wep:GetFiremodeName()) + end + return self:StringOperator(name_or_id, wep:GetFiremodeName()) or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end - local key = net.ReadUInt(8) - local down = net.ReadBool() + if wep.IsFAS2Weapon then + if not wep.FireMode then return name_or_id == "" or name_or_id == "nil" or name_or_id == "null" or name_or_id == "none" + else return self:StringOperator(wep.FireMode, name_or_id) end + end - key = pac.key_enums[key] or key + if wep.GetFireModeName then --TFA base is an arbitrary number and name (language-specific) + return self:StringOperator(string.lower(wep:GetFireModeName()), name_or_id) or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id)) + end + + if wep.Primary then + if wep.Primary.Automatic ~= nil then --M9K is a boolean + if wep.Primary.Automatic == true then + return name_or_id == "automatic" or name_or_id == "auto" or name_or_id == "1" + else + return name_or_id == "semi-automatic" or name_or_id == "semi-auto" or name_or_id == "single" or name_or_id == "0" + end + end + end + + + return false + end, + nice = function(self, ent, name_or_id) + if not IsValid(ent) then return end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return "invalid weapon" end + wep = ent:GetActiveWeapon() + local str = "weapon_firemode ["..self.Operator.. " " .. name_or_id .. "] | " + + if wep.IsFAS2Weapon then + + if wep.FireMode then + str = str .. wep.FireMode .. " | options : " + for i,v in ipairs(wep.FireModes) do + str = str .. "(" .. v .. " = " .. i.. "), " + end + else str = str .. "" end + return str + end + + if wep.ArcCW then + if not IsValid(wep) then return "no active weapon" end + if wep.GetFiremodeName then + str = str .. wep:GetFiremodeName() .. " | options : " + for i,v in ipairs(wep.Firemodes) do + if v.PrintName then + str = str .. "(" .. i .. " = " .. v.PrintName .. "), " + end + end + if wep.Primary.Automatic then + str = str .. "(" .. "Automatic" .. "), " + end + end + return str + end + + if wep.GetFireModeName then --TFA base or arccw + if not IsValid(wep) then return "no active weapon" end + if wep.GetFireModeName then + str = str .. wep:GetFireModeName() .. " | options : " + for i,v in ipairs(wep:GetStatL("FireModes")) do + str = str .. "(" .. v .. " = " .. i.. "), " + end + end + return str + end + + if wep.Primary then --M9K + if wep.Primary.Automatic ~= nil then + if wep.Primary.Automatic then + str = str .. "automatic" + else + str = str .. "semi-auto" + end + end + str = str .. " | options : 1/auto/automatic, 0/single/semi-auto/semi-automatic" + return str + end + + + return str + end + }, + + weapon_safety = { + operator_type = "none", + callback = function(self, ent) + if not ent or not IsValid(ent) then return false end + if not ent.GetActiveWeapon then return false end + if not IsValid(ent:GetActiveWeapon()) then return false end + local wep = ent:GetActiveWeapon() + if wep.IsSafety then + return wep:IsSafety() + end + if wep.ArcCW then + return wep:GetFiremodeName() == "Safety" + end + + return false + end + }, +--@note take damage is like health_lost but 400% better + + take_damage = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {damage = "number"}, {attackers = "string"}, {inflictors = "string"}, {damage_type = "number"}}, + userdata = {{default = 1}, {default = 10}, + {default = "any", + enums = function() + local players = {} + for i,v in ipairs(player.GetAll()) do + players[v:Nick()] = v:SteamID() + end + return players + end + }, {default = "any"}, + {default = -1, + enums = function() + local damage_enums = {} + for k,v in pairs(_G) do + if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then + damage_enums[k] = tostring(v) + end + end + return damage_enums + end + }}, + callback = function(self, ent, time, damage, attackers, inflictors, damage_type) + local time = time or 0 + if not IsValid(ent) then return false end + local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" + local found_attacker = attackers == "" or attackers == "anyone" or attackers == "any" or attackers == "all" + + local unspec_inflictor = found_inflictor + local unspec_attacker = found_attacker + + local unspec_dmg = damage_type == -1 + local matching_dmg = unspec_dmg + + local lastest_attacker = nil + local latest_hit_time = 0 + + if not ent.pac_damage_attributions then + ent.pac_damage_attributions = {} + return false + elseif (table.Count(ent.pac_damage_attributions) < 1) then + return false + end + + ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 + + if CurTime() < ent.pac_damage_attributions.IngoingGraceTime + time then return true end + if not ent.pac_damage_attributions then return false end --the entity is a damage virgin, it's not hurt + + + for attacker,tbl in pairs(ent.pac_damage_attributions) do + if IsValid(attacker) and IsValid(tbl.inflictor) then + local found_inflictor_class = false + for i,v in ipairs(string.Split(inflictors, ";")) do + if v == tbl.inflictor:GetClass() then + found_inflictor = true + end + end + for i,v in ipairs(string.Split(attackers, ";")) do + if v == tbl.attacker:GetClass() then + found_attacker = true + elseif tbl.attacker:IsPlayer() then + if tbl.attacker:SteamID() == v or tbl.attacker:Nick() == v then + found_attacker = true + end + --print("tested attacker ", tbl.attacker:GetClass(), "it aint it.", v) + end + end + if tbl.hit_time > latest_hit_time and (bit.band(tbl.dmg_type, damage_type) ~= 0 or unspec_dmg) then + matching_dmg = true + latest_hit_time = tbl.hit_time + lastest_attacker = attacker + end + else --lost entity! i.e. grenades get removed so we can't use direct entity reference anymore + --so we give it a grace period for next time + if not type(attacker) == "table" then --exclude the grace fields... + ent.pac_damage_attributions.IngoingGraceTime = CurTime() + ent.pac_damage_attributions[attacker] = nil + end + end + + end + if not unspec_dmg and not matching_dmg then return false end + --print(ent.pac_damage_attributions.IngoingGraceTime) + ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 + + --print("CurTime:"..CurTime(), "Grace:" .. ent.pac_damage_attributions.IngoingGraceTime) + + + if found_attacker and found_inflictor then + if ent.pac_damage_attributions[lastest_attacker] then + if CurTime() < ent.pac_damage_attributions[lastest_attacker].hit_time + time then + return self:NumberOperator(ent.pac_damage_attributions[lastest_attacker].dmg_amount, damage) + end + elseif CurTime() < ent.pac_damage_attributions.IngoingGraceTime + time then + return true + end + + end + + if unspec_attacker and unspec_inflictor then + + if ent.pac_damage_attributions.latest then + + if (CurTime() < ent.pac_damage_attributions.latest.hit_time + time) then + return self:NumberOperator(ent.pac_damage_attributions.latest.dmg_amount, damage) + end + end + end + + return false + end, + nice = function(self, ent, time, damage, attackers, inflictors, damage_type) + time = time or 0 + damage = damage or 0 + attackers = attackers or "" + inflictors = inflictors or "" + damage_type = damage_type or -1 + local str = "take_damage : [" .. self.Operator .. " " .. damage .. "]" + if attackers == "" or attackers == "any" or attackers == "anyone" or attackers == "all" then + str = str .. " | from any attacker " + else + str = str .. " | from attackers: " + for i,v in ipairs(string.Split(attackers, ";")) do + str = str .. v .. " " + end + end + for i,v in ipairs(string.Split(attackers, ";")) do + str = str .. v .. " " + end + if inflictors == "" or inflictors == "any" or inflictors == "all" then + str = str .. " | from any inflictor" + else + str = str .. " | from inflictors: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + str = str .. " | with damage types : " .. damage_type + return str + end + }, + + inflicting_damage = { + operator_type = "mixed", preferred_operator = "above", + tutorial_explanation = "", + arguments = {{time = "number"}, {damage = "number"}, {targets = "string"}, {inflictors = "string"}, {damage_type = "number"}}, + userdata = {{default = 1}, {default = 10}, + {default = "any", + enums = function() + local players = {} + for i,v in ipairs(player.GetAll()) do + players[v:Nick()] = v:SteamID() + end + return players + end + }, {default = "any"}, + {default = -1, + enums = function() + local damage_enums = {} + for k,v in pairs(_G) do + if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then + damage_enums[k] = tostring(v) + end + end + return damage_enums + end + }}, + callback = function(self, ent, time, damage, targets, inflictors, damage_type) + local time = time or 0 + + if not IsValid(ent) then return false end + local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" + local found_target = targets == "" or targets == "anyone" or targets == "any" or targets == "all" + local unspec_dmg = damage_type == -1 + + ent.pac_damage_attributions = ent.pac_damage_attributions or {} + ent.pac_damage_attributions.OutgoingGraceTime = ent.pac_damage_attributions.OutgoingGraceTime or 0 + ent.pac_damage_attributions.OutgoingGraceTimeDMG = ent.pac_damage_attributions.OutgoingGraceTimeDMG or 0 + local latest_hit_time = ent.pac_damage_attributions.OutgoingGraceTime or 0 + + for _,target in pairs(ents.GetAll()) do --check ents we could hurt + if target.pac_damage_attributions then --skip the virgins + if target.pac_damage_attributions[ent] then --we're in. + + tbl = target.pac_damage_attributions[ent] + if not found_inflictor then + for i,v in ipairs(string.Split(inflictors, ";")) do + if v == tbl.inflictor:GetClass() then + found_inflictor = true + end + end + end + if not found_target then + for i,v in ipairs(string.Split(targets, ";")) do + if v == target:GetClass() then + found_target = true + elseif target:IsPlayer() then + if target:SteamID() == v or target:Nick() == v then + found_target = true + end + end + end + end + + if tbl.hit_time > latest_hit_time and (bit.band(tbl.dmg_type, damage_type) ~= 0 or unspec_dmg) then + latest_hit_time = CurTime() + ent.pac_damage_attributions.OutgoingGraceTime = CurTime() + ent.pac_damage_attributions.OutgoingGraceTimeDMG = tbl.dmg_amount + end + end + end + end + --WHAT ABOUT KILLS?? DONT WORRY ABOUT IT (TM) + --print("CurTime:" .. CurTime(), "out grace"..ent.pac_damage_attributions.OutgoingGraceTime) + if found_target and found_inflictor then + if CurTime() < ent.pac_damage_attributions.OutgoingGraceTime + time then + return self:NumberOperator(ent.pac_damage_attributions.OutgoingGraceTimeDMG, damage) + end + end + + return false + end, + nice = function(self, ent, time, damage, targets, inflictors, damage_type) + time = time or 0 + damage = damage or 0 + attackers = attackers or "" + targets = targets or "" + damage_type = damage_type or -1 + local str = "inflicting_damage : [" .. self.Operator .. " " .. damage .. "]" + if targets == "" or targets == "anyone" or targets == "any" or targets == "all" then + str = str .. " | to any target" + else + str = str .. " | to targets: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + if inflictors == "" or inflictors == "any" or inflictors == "all" then + str = str .. " | from any inflictor " + else + str = str .. " | from inflictors: " + for i,v in ipairs(string.Split(inflictors, ";")) do + str = str .. v .. " " + end + end + str = str .. " | with damage types : " .. damage_type + return str + end + }, + + damage_zone_hit = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {damage = "number"}, {uid = "string"}}, + userdata = {{default = 1}, {default = 0}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "damage_zone" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, time, damage, uid) + uid = uid or "" + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "damage_zone" then + if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then + if part.dmgzone_hit_done + time > CurTime() then + return true + end + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + if part.ClassName == "damage_zone" then + if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then + if part.dmgzone_hit_done + time > CurTime() then + return true + end + end + else + self:SetError("You set a UID that's not a damage zone!") + end + end + return false + end, + }, + + damage_zone_kill = { + operator_type = "mixed", preferred_operator = "above", + arguments = {{time = "number"}, {uid = "string"}}, + userdata = {{default = 1}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "damage_zone" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, time, uid) + uid = uid or "" + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "damage_zone" then + if part.dmgzone_kill_done then + if part.dmgzone_kill_done + time > CurTime() then + return true + end + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + if part.ClassName == "damage_zone" then + if part.dmgzone_kill_done then + if part.dmgzone_kill_done + time > CurTime() then + return true + end + end + else + self:SetError("You set a UID that's not a damage zone!") + end + end + return false + end, + }, + + lockpart_grabbed = { + operator_type = "none", + callback = function(self, ent) + return ent.IsGrabbed and ent.IsGrabbedByUID + end + }, + + lockpart_grabbing = { + operator_type = "none", + arguments = {{uid = "string"}}, + userdata = {{enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "lock" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, uid) + uid = uid or "" + local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) + if uid == "" then + for _,part in pairs(pac.GetLocalParts()) do + if part.ClassName == "lock" then + if part.grabbing then + return IsValid(part.target_ent) + end + end + end + elseif not valid_uid and err then + self:SetError("invalid part Unique ID\n"..err) + elseif valid_uid then + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + if part.ClassName == "lock" then + if part.grabbing then + return IsValid(part.target_ent) + end + else + self:SetError("You set a UID that's not a lock part!") + end + end + return false + end + }, + + --[[ + ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {} + ent.pac_healthbars_uidtotals = ent.pac_healthbars_uidtotals or {} + ent.pac_healthbars_total = 0 + ]] + healthmod_bar_total = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}}, + userdata = {{default = 0}}, + callback = function(self, ent, HpValue) + if ent.pac_healthbars and ent.pac_healthbars_total then + return self:NumberOperator(ent.pac_healthbars_total, HpValue) + end + return false + end, + nice = function(self, ent, HpValue) + local str = "healthmod_bar_total : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_total then + str = str .. " | " .. ent.pac_healthbars_total + end + return str + end + }, + + healthmod_bar_layertotal = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}, {layer = "number"}}, + userdata = {{default = 0}, {default = 0}}, + callback = function(self, ent, HpValue, layer) + if ent.pac_healthbars and ent.pac_healthbars_layertotals then + if ent.pac_healthbars_layertotals[layer] then + return self:NumberOperator(ent.pac_healthbars_layertotals[layer], HpValue) + end + + end + return false + end, + nice = function(self, ent, HpValue, layer) + local str = "healthmod_layer_total at layer " .. layer .. " : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_layertotals then + if ent.pac_healthbars_layertotals[layer] then + str = str .. " | " .. ent.pac_healthbars_layertotals[layer] + else + str = str .. " | not found" + end + + else + str = str .. " | not found" + end + return str + end + }, + + healthmod_bar_uidvalue = { + operator_type = "number", preferred_operator = "above", + arguments = {{HpValue = "number"}, {part_uid = "string"}}, + userdata = {{default = 0}, {enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "health_modifier" then + output[i] = part + end + end + + return output + end}}, + callback = function(self, ent, HpValue, part_uid) + if ent.pac_healthbars and ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals[part_uid] then + return self:NumberOperator(ent.pac_healthbars_uidtotals[part_uid], HpValue) + end + end + return false + end, + nice = function(self, ent, HpValue, part_uid) + local str = "healthmod_bar_uidvalue : [" .. self.Operator .. " " .. HpValue .. "]" + if ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals[part_uid] then + str = str .. " | " .. ent.pac_healthbars_uidtotals[part_uid] + else + str = str .. " | nothing for UID "..part_uid + end + else + str = str .. " | nothing for UID "..part_uid + end + return str + end + }, + +} + + +do + + --[[local base_input_enums_names = { + ["IN_ATTACK"] = 1, + ["IN_JUMP"] = 2, + ["IN_DUCK"] = 4, + ["IN_FORWARD"] = 8, + ["IN_BACK"] = 16, + ["IN_USE"] = 32, + ["IN_CANCEL"] = 64, + ["IN_LEFT"] = 128, + ["IN_RIGHT"] = 256, + ["IN_MOVELEFT"] = 512, + ["IN_MOVERIGHT"] = 1024, + ["IN_ATTACK2"] = 2048, + ["IN_RUN"] = 4096, + ["IN_RELOAD"] = 8192, + ["IN_ALT1"] = 16384, + ["IN_ALT2"] = 32768, + ["IN_SCORE"] = 65536, + ["IN_SPEED"] = 131072, + ["IN_WALK"] = 262144, + ["IN_ZOOM"] = 524288, + ["IN_WEAPON1"] = 1048576, + ["IN_WEAPON2"] = 2097152, + ["IN_BULLRUSH"] = 4194304, + ["IN_GRENADE1"] = 8388608, + ["IN_GRENADE2"] = 16777216 + } + local input_aliases = {} + + for name,value in pairs(base_input_enums_names) do + local alternative0 = string.lower(name) + local alternative1 = string.Replace(string.lower(name),"in_","") + local alternative2 = "+"..alternative1 + input_aliases[name] = value + input_aliases[alternative0] = value + input_aliases[alternative1] = value + input_aliases[alternative2] = value + end]] + + + local enums = {} + local enums2 = {} + for key, val in pairs(_G) do + if isstring(key) and isnumber(val) then + if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:sub(5):lower() + enums2[enums[val]] = val + elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:lower() + enums2[enums[val]] = val + end + end + end + + pac.key_enums = enums + +--@note button broadcast + + --TODO: Rate limit!!! + net.Receive("pac.BroadcastPlayerButton", function() + local ply = net.ReadEntity() + + if not ply:IsValid() then return end + + if ply == pac.LocalPlayer and (pace and pace.IsFocused() or gui.IsConsoleVisible()) then return end + + local key = net.ReadUInt(8) + local down = net.ReadBool() + + if not pac.key_enums then --rebuild the enums + local enums = {} + local enums2 = {} + for key, val in pairs(_G) do + if isstring(key) and isnumber(val) then + if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:sub(5):lower() + enums2[enums[val]] = val + elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then + enums[val] = key:lower() + enums2[enums[val]] = val + end + end + end + + pac.key_enums = enums + end + + key = pac.key_enums[key] or key ply.pac_buttons = ply.pac_buttons or {} ply.pac_buttons[key] = down + + + ply.pac_broadcasted_buttons_lastpressed = ply.pac_broadcasted_buttons_lastpressed or {} + if down then + ply.pac_broadcasted_buttons_lastpressed[key] = SysTime() + end + + for _,part in pairs(pac.getallparts()) do --locate the corresponding parts among the part pool + if part:GetPlayerOwner() == ply and part.ClassName == "event" and part.Event == "button" then + part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {} + part.holdtime = part.holdtime or 0 + part.toggleimpulsekey = part.toggleimpulsekey or {} + part.toggleimpulsekey[key] = down + part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 + ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0 + part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime + end + end + end) + pac.player_inputs = {} + pac.player_inputs_update_times = {} + + net.Receive("pac.BroadcastPlayerInputs", function() + pac.player_inputs = net.ReadTable() + pac.player_inputs_update_times = net.ReadTable() + end) + + PART.OldEvents.button = { - arguments = {{button = "string"}}, + operator_type = "none", + arguments = {{button = "string"}, {holdtime = "number"}, {toggle = "boolean"}}, userdata = {{enums = function() return enums - end}}, + end, default = "mouse_left"}, {default = 0}, {default = false}}, nice = function(self, ent, button) local ply = self:GetPlayerOwner() @@ -1240,11 +2718,31 @@ do return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")" end, - callback = function(self, ent, button) + callback = function(self, ent, button, holdtime, toggle) + + local holdtime = holdtime or 0 + local toggle = toggle or false + + self.togglestate = self.togglestate or false + self.holdtime = holdtime + self.toggle = toggle + + self.toggleimpulsekey = self.toggleimpulsekey or {} + + if self.toggleimpulsekey[button] then + self.togglestate = not self.togglestate + self.toggleimpulsekey[button] = false + end + + --print(button, "hold" ,self.holdtime) local ply = self:GetPlayerOwner() + self.pac_broadcasted_buttons_holduntil = self.pac_broadcasted_buttons_holduntil or {} + if ply == pac.LocalPlayer then + ply.pac_broadcast_buttons = ply.pac_broadcast_buttons or {} + if not ply.pac_broadcast_buttons[button] then local val = enums2[button:lower()] if val then @@ -1254,15 +2752,103 @@ do end ply.pac_broadcast_buttons[button] = true end + + --print(button, ply.pac_broadcasted_buttons_holduntil[button], ply.pac_broadcast_buttons[button]) + --PrintTable(ply.pac_broadcast_buttons) + --PrintTable(self.pac_broadcasted_buttons_holduntil) end local buttons = ply.pac_buttons + self.pac_broadcasted_buttons_holduntil[button] = self.pac_broadcasted_buttons_holduntil[button] or SysTime() + --print(button, self.toggle, self.togglestate) + --print(button,"until",self.pac_broadcasted_buttons_holduntil[button]) if buttons then - return buttons[button] + --print("trying to compare " .. SysTime() .. " > " .. self.pac_broadcasted_buttons_holduntil[button] - 0.05) + if self.toggle then + return self.togglestate + elseif self.holdtime > 0 then + return SysTime() < self.pac_broadcasted_buttons_holduntil[button] + else + return buttons[button] + end + end end, } + + --PART.OldEvents.input = { + -- operator_type = "none", + -- arguments = {{UserInputs = "string"}, {RequireAllInputs = "boolean"}}, + -- userdata = {{enums = function() + -- return base_input_enums_names + -- end}}, + -- callback = function(self, ent, UserInputs, RequireAllInputs) + -- local ply = self:GetPlayerOwner() + -- UserInputs = UserInputs or "" + -- pac.player_inputs[ply] = pac.player_inputs[ply] or {} + -- local detect = false + -- local fulldetect = true + -- local input_list = string.Split(UserInputs, ";") + -- + -- for i,v in pairs(pac.player_inputs[ply]) do + -- for _,v2 in pairs(input_list) do + -- if pac.player_inputs[ply][input_aliases[v2]] then detect = true + -- else fulldetect = false end + -- end + -- end + -- if RequireAllInputs then return fulldetect + -- else return detect end + -- end + --} + + --[[PART.OldEvents.is_moving = { + operator_type = "none", + callback = function(self) + local ply = self:GetPlayerOwner() + pac.player_inputs = pac.player_inputs or {} + pac.player_inputs[ply] = pac.player_inputs[ply] or {} + return pac.player_inputs[ply][IN_FORWARD] or + pac.player_inputs[ply][IN_BACK] or + pac.player_inputs[ply][IN_MOVELEFT] or + pac.player_inputs[ply][IN_MOVERIGHT] or + pac.player_inputs[ply][IN_JUMP] + end + }]] + + --[[PART.OldEvents.afk = { + operator_type = "none", + arguments = {{time = "number"}, {IncludeEyeAngles = "boolean"}}, + callback = function(self, ent, time, IncludeEyeAngles) + local time = time or 0 + local IncludeEyeAngles = IncludeEyeAngles + local ply = self:GetPlayerOwner() + local time_bool = false + local eyes_bool = false + + if pac.player_inputs_update_times then + pac.player_inputs_update_times[ply] = pac.player_inputs_update_times[ply] or 0 + time_bool = pac.player_inputs_update_times[ply] + time > CurTime() + end + + ply.last_eyeang = ply.last_eyeang or ply:EyeAngles() + ply.eyeang_update_time = ply.eyeang_update_time or CurTime() + + if ply.last_eyeang ~= ply:EyeAngles() then + ply.eyeang_update_time = CurTime() + end + + eyes_bool = (ply.last_eyeang ~= ply:EyeAngles()) or (ply.eyeang_update_time + time > CurTime()) + ply.last_eyeang = ply:EyeAngles() + if IncludeEyeAngles then + return not (time_bool or eyes_bool) + else + return not time_bool + end + return true + end + }]] + end do @@ -1417,7 +3003,7 @@ do local arguments = data.arguments local think = data.callback local eventObject = pac.CreateEvent(classname) - + if arguments then for i, data2 in ipairs(arguments) do local key, Type = next(data2) @@ -1427,6 +3013,13 @@ do eventObject.extra_nice_name = data.nice + local operator_type = data.operator_type + local preferred_operator = data.preferred_operator + local tutorial_explanation = data.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + function eventObject:Think(event, ent, ...) return think(event, ent, ...) end @@ -1443,6 +3036,8 @@ end do local animations = pac.animations local event = { + operator_type = "none", + tutorial_explanation = "selecting a custom animation part via UID,\nthis event activates whenever the linked custom animation is currently playing somewhere between the frames specified", name = "custom_animation_frame", nice = function(self, ent, animation) if animation == "" then self:SetWarning("no animation selected") return "no animation" end @@ -1517,6 +3112,15 @@ do eventObject.IsAvailable = event.available eventObject.extra_nice_name = event.nice + data = event + + local operator_type = data.operator_type + local preferred_operator = data.preferred_operator + local tutorial_explanation = data.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + pac.RegisterEvent(eventObject) end @@ -1617,6 +3221,13 @@ do return isDarkRP() and available() end + local operator_type = v.operator_type + local preferred_operator = v.preferred_operator + local tutorial_explanation = v.tutorial_explanation + eventObject.operator_type = operator_type + eventObject.preferred_operator = preferred_operator + eventObject.tutorial_explanation = tutorial_explanation + pac.RegisterEvent(eventObject) end end @@ -1643,7 +3254,7 @@ local function is_hidden_by_something_else(part, ignored_part) if part.active_events_ref_count > 0 and not part.active_events[ignored_part] then return true end - + return part.Hide end @@ -1690,13 +3301,50 @@ end PART.last_event_triggered = false +function PART:fix_args() + local args = string.Split(self.Arguments, "@@") + if self.Events[self.Event] then + if self.Events[self.Event].__registeredArguments then + --PrintTable(self.Events[self.Event].__registeredArguments) + if #self.Events[self.Event].__registeredArguments ~= #args then + for argn,arg in ipairs(self.Events[self.Event].__registeredArguments) do + if not args[argn] or args[argn] == "" then + local added_arg = "0" + if arg[2] == "boolean" then + if arg[3] then + if arg[3].default then added_arg = "1" + else added_arg = "0" end + end + else + if arg[3] then + if arg[3].default then + added_arg = tostring(arg[3].default) + end + end + end + args[argn] = added_arg + end + end + self.Arguments = table.concat(args, "@@") + end + end + end +end + function PART:OnThink() + self.nextactivationrefresh = self.nextactivationrefresh or CurTime() + if not self.singleactivatestate and self.nextactivationrefresh < CurTime() then + self.singleactivatestate = true + end + local ent = get_owner(self) if not ent:IsValid() then return end local data = PART.Events[self.Event] + if not data then return end + self:fix_args() self:TriggerEvent(should_trigger(self, ent, data)) if pace and pace.IsActive() and self.Name == "" then @@ -1707,6 +3355,51 @@ function PART:OnThink() end +function PART:SetAffectChildrenOnly(b) + if b == nil then return end + + if self.AffectChildrenOnly ~= nil and self.AffectChildrenOnly ~= b then + --print("changing") + local ent = get_owner(self) + local data = PART.Events[self.Event] + + if ent:IsValid() and data then + local b = should_trigger(self, ent, data) + if self.AffectChildrenOnly then + local parent = self:GetParent() + if parent:IsValid() then + parent:SetEventTrigger(self, b) + + for _, child in ipairs(self:GetChildren()) do + if child.active_events[self] then + child.active_events[self] = nil + child.active_events_ref_count = child.active_events_ref_count - 1 + child:CallRecursive("CalcShowHide", false) + end + end + end + + else + for _, child in ipairs(self:GetChildren()) do + child:SetEventTrigger(self, b) + end + if self:GetParent():IsValid() then + local parent = self:GetParent() + if parent.active_events[self] then + parent.active_events[self] = nil + parent.active_events_ref_count = parent.active_events_ref_count - 1 + parent:CallRecursive("CalcShowHide", false) + end + end + + end + end + end + self.AffectChildrenOnly = b + +end + + function PART:TriggerEvent(b) self.event_triggered = b -- event_triggered is just used for the editor @@ -1720,6 +3413,16 @@ function PART:TriggerEvent(b) parent:SetEventTrigger(self, b) end end + if IsValid(self.DestinationPart) then --target part. the proper one. + if IsValid(self.previousdestinationpart) then + if self.DestinationPart ~= self.previousdestinationpart then --once we change the destination part we need to reset the old one + self.previousdestinationpart:SetEventTrigger(self, false) + end + end + + (self.DestinationPart):SetEventTrigger(self, b) + self.previousdestinationpart = (self.DestinationPart) + end end PART.Operators = { @@ -1866,6 +3569,8 @@ function PART:NumberOperator(a, b) end end + + function PART:OnHide() if self.timerx_reset then self.time = nil @@ -1889,6 +3594,8 @@ function PART:OnShow() self.time = nil self.number = 0 end + self.showtime = CurTime() + self.singleactivatestate = true end function PART:OnAnimationEvent(ent) @@ -1963,6 +3670,7 @@ pac.AddHook("EntityFireBullets", "firebullets", function(ent, data) end end) + net.Receive("pac_event", function(umr) local ply = net.ReadEntity() local str = net.ReadString() @@ -1979,6 +3687,95 @@ net.Receive("pac_event", function(umr) end end) +concommand.Add("pac_wipe_events", function(ply) + ply.pac_command_events = nil + ply.pac_command_event_sequencebases = nil +end) +concommand.Add("pac_print_events", function(ply) + ply.pac_command_events = ply.pac_command_events or {} + PrintTable(ply.pac_command_events) +end) + +concommand.Add("pac_event_sequenced", function(ply, cmd, args) + + if not args[1] then return end + + local event = args[1] + local action = args[2] or "+" + local sequence_number = 0 + local set_target = args[3] or 1 + local found = false + + ply.pac_command_events = ply.pac_command_events or {} + ply.pac_command_events[event..1] = ply.pac_command_events[event..1] or {name = event..1, time = 0, on = 1} + + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} + + if not ply.pac_command_event_sequencebases[event] then + ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = 1} + end + + local target_number = 1 + local min = 1 + local max = ply.pac_command_event_sequencebases[event].max + + for i=1,100,1 do + if ply.pac_command_events[event..i] then + if ply.pac_command_events[event..i].on == 1 then + if sequence_number == 0 then sequence_number = i end + found = true + end + --elseif ply.pac_command_events[event..i] == nil then + ply.pac_command_events[event..i] = {name = event..i, time = 0, on = 0} + end + end + + if found then + if action == "+" or action == "forward" or action == "add" or action == "sequence+" or action == "advance" then + + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 0} + if sequence_number == max then + target_number = min + else target_number = sequence_number + 1 end + + pac.Message("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) + ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} + + RunConsoleCommand("pac_event", event..sequence_number, "0") + RunConsoleCommand("pac_event", event..target_number, "1") + + + elseif action == "-" or action == "backward" or action == "sub" or action == "sequence-" then + + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 0} + if sequence_number == min then + target_number = max + else target_number = sequence_number - 1 end + + print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) + ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} + + RunConsoleCommand("pac_event", event..sequence_number, "0") + RunConsoleCommand("pac_event", event..target_number, "1") + + elseif action == "set" then + print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. set_target .. " / " .. max) + + sequence_number = set_target or 1 + for i=1,100,1 do + ply.pac_command_events[event..i] = nil + end + ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 1} + target_number = set_target + net.Start("pac_event_set_sequence") + net.WriteString(event) + net.WriteUInt(sequence_number,8) + net.SendToServer() + end + end +end) + + pac.AddHook("OnPlayerChat", "say_event", function(ply, str) if ply:IsValid() then ply.pac_say_event = {str = str, time = pac.RealTime} @@ -2032,39 +3829,217 @@ reload custom gesture --]] +local eventwheel_visibility_rule = CreateConVar("pac_eventwheel_visibility_rule" , "0", FCVAR_ARCHIVE, +"Different ways to filter your command events for the wheel.\n".. +"-1 ignores hide flags completely\n".. +"0 will hide a command if at least one event of one name has the \"hide in event wheel\" flag\n".. +"1 will hide a command only if ALL events of one name have the \"hide in event wheel\" flag\n".. +"2 will hide a command as soon as one event of a name is being hidden\n".. +"3 will hide a command only if ALL events of a name are being hidden\n".. +"4 will only show commands containing the following substrings, separated by spaces\n".. +"-4 will hide commands containing the following substrings, separated by spaces") + +local eventwheel_style = CreateConVar("pac_eventwheel_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel.\n0 is the default legacy style with one circle\n1 is the new style with colors, using one circle for the color and one circle for the activation indicator\n2 is an alternative style using a smaller indicator circle on the corner of the circle") +local eventlist_style = CreateConVar("pac_eventlist_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel list alternative.\n0 is like the default eventwheel legacy style with one indicator for the activation\n1 is the new style with colors, using one rectangle for the color and one rectangle for the activation indicator\n2 is an alternative style using a smaller indicator on the corner") + +local eventwheel_font = CreateConVar("pac_eventwheel_font", "DermaDefault", FCVAR_ARCHIVE, "pac3 eventwheel font. try pac_font_ such as pac_font_20 or pac_font_bold30. the pac fonts go up to 34") +local eventwheel_clickable = CreateConVar("pac_eventwheel_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel.\n-1 : not clickable, but activate on menu close\n0 : clickable, and activate on menu close\n1 : clickable, but doesn't activate on menu close") +local eventlist_clickable = CreateConVar("pac_eventlist_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel list alternative.\n-1 : not clickable, but activate a hovered event on menu close\n0 : clickable, and activate a hovered event on menu close\n1 : clickable, but doesn't do anything on menu close") + +local event_list_font_size = CreateConVar("pac_eventlist_fontsize", "12", FCVAR_ARCHIVE, "How big the font should be for the eventwheel's rectangle list counterpart.\nMight not work if the corresponding pac_font is missing") -- Custom event selector wheel do + local function get_events() + pace.command_colors = pace.command_colors or {} local available = {} + local names = {} + local args = string.Split(eventwheel_visibility_rule:GetString(), " ") + local uncolored_events = {} for k,v in pairs(pac.GetLocalParts()) do if v.ClassName == "event" then local e = v:GetEvent() if e == "command" then local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) - if hide then continue end + local this_event_hidden = v:IsHiddenBySomethingElse(false) + + + if not names[cmd] then + --wheel_hidden is the hide_in_eventwheel box + --possible_hidden is part hidden + names[cmd] = { + name = cmd, event = v, + + wheel_hidden = hide, + all_wheel_hidden = hide, + + possible_hidden = this_event_hidden, + all_possible_hidden = this_event_hidden, + } + else + --if already exists, we need to check counter examples for whether all members are hidden or hide_in_eventwheel + + if not hide then + names[cmd].all_wheel_hidden = false + end + + if not this_event_hidden then + names[cmd].all_possible_hidden = false + end - available[cmd] = {type = e, time = time} + if not names[cmd].wheel_hidden and hide then + names[cmd].wheel_hidden = true + end + + if not names[cmd].possible_hidden and this_event_hidden then + names[cmd].possible_hidden = true + end + + + end + + available[cmd] = {type = e, time = time, trigger = cmd} end end end + for cmd,v in pairs(names) do + uncolored_events[cmd] = not pace.command_colors[cmd] + local remove = false + + if args[1] == "-1" then --skip + remove = false + elseif args[1] == "0" then --one hide_in_eventwheel + if v.wheel_hidden then + remove = true + end + elseif args[1] == "1" then --all hide_in_eventwheel + if v.all_wheel_hidden then + remove = true + end + elseif args[1] == "2" then --one hidden + if v.possible_hidden then + remove = true + end + elseif args[1] == "3" then --all hidden + if v.all_possible_hidden then + remove = true + end + elseif args[2] then + if #args > 1 then --args contains many strings + local match = false + + for i=2, #args, 1 do + local str = args[i] + if string.find(cmd, str) then + match = true + end + end - local list = {} - for k,v in pairs(available) do - v.trigger = k - table.insert(list, v) + if args[1] == "4" and not match then + remove = true + elseif args[1] == "-4" and match then + remove = true + end + + else --why would you use the 4 or -4 mode if you didn't set keywords?? + remove = false + end + end + + if remove then + available[cmd] = nil + end end + + local list = {} + + if true then + local colors = {} + + for name,colstr in pairs(pace.command_colors) do + colors[colstr] = colors[colstr] or {} + colors[colstr][name] = available[name] + end + + + for col,tbl in pairs(colors) do + + local sublist = {} + for k,v in pairs(tbl) do + table.insert(sublist,available[k]) + end + + table.sort(sublist, function(a, b) return a.trigger < b.trigger end) + + for i,v in pairs(sublist) do + table.insert(list,v) + end + end + + local uncolored_sublist = {} - table.sort(list, function(a, b) return a.trigger > b.trigger end) + for k,v in pairs(available) do + if uncolored_events[k] then + table.insert(uncolored_sublist,available[k]) + end + end + + table.sort(uncolored_sublist, function(a, b) return a.trigger < b.trigger end) + for k,v in ipairs(uncolored_sublist) do + table.insert(list, v) + end + else + + for k,v in pairs(available) do + if k == names[k].name then + v.trigger = k + table.insert(list, v) + end + end + + table.sort(list, function(a, b) return a.trigger > b.trigger end) + end + return list end local selectorBg = Material("sgm/playercircle") local selected + local clicking = false + local open_btn + + + local clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1 + local close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0 + + local clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1 + local close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0 function pac.openEventSelectionWheel() + if not IsValid(open_btn) then open_btn = vgui.Create("DButton") end + open_btn:SetSize(80,30) + open_btn:SetText("Customize") + open_btn:SetPos(ScrW() - 80,0) + pace.command_event_menu_opened = nil + + function open_btn:DoClick() + + if (pace.command_event_menu_opened == nil) then + pace.ConfigureEventWheelMenu() + elseif IsValid(pace.command_event_menu_opened) then + pace.command_event_menu_opened:Remove() + end + + end + + open_btn:Show() + pace.command_colors = pace.command_colors or {} + clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1 + close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0 + gui.EnableScreenClicker(true) local scrw, scrh = ScrW(), ScrH() @@ -2129,6 +4104,13 @@ do local ply = pac.LocalPlayer local data = ply.pac_command_events and ply.pac_command_events[self.event.trigger] and ply.pac_command_events[self.event.trigger] + + + local d1 = 64 --indicator + + + local d2 = 50 --color + local indicator_color if data then local is_oneshot = self.event.time and self.event.time > 0 @@ -2136,28 +4118,84 @@ do local f = (pac.RealTime - data.time) / self.event.time local s = Lerp(math.Clamp(f,0,1), 1, 0) local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15) - surface.SetDrawColor(HSVToColor(210,s,v)) + indicator_color = HSVToColor(210,s,v) + else if data.on == 1 then - surface.SetDrawColor(HSVToColor(210,1,0.55)) + indicator_color = HSVToColor(210,1,0.55) else - surface.SetDrawColor(HSVToColor(210,0,0.15)) + indicator_color = HSVToColor(210,0,0.15) end end else - surface.SetDrawColor(HSVToColor(210,0,0.15)) + indicator_color = HSVToColor(210,0,0.15) end - surface.DrawTexturedRect(x-48, y-48, 96, 96) - draw.SimpleText(self.name, "DermaDefault", x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) + if eventwheel_style:GetInt() == 0 then + d2 = 96 + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2) + elseif eventwheel_style:GetInt() == 1 then + if pace.command_colors[self.name] then + local col_str_tbl = string.Split(pace.command_colors[self.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + + d1 = 100 --color + d2 = 50 --indicator + + surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1) + + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2) + + draw.RoundedBox(0,x-40,y-8,80,16,Color(0,0,0)) + + elseif eventwheel_style:GetInt() == 2 then + if pace.command_colors[self.name] then + local col_str_tbl = string.Split(pace.command_colors[self.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + + d1 = 96 --color + d2 = 40 --indicator + surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1) + surface.SetDrawColor(indicator_color) + surface.DrawTexturedRect(x-1.2*d2, y-1.2*d2, d2, d2) + end + + draw.SimpleText(self.name, eventwheel_font:GetString(), x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) + cam.PopModelMatrix() end pac.AddHook("HUDPaint","custom_event_selector",function() -- Right clicking cancels if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionWheel(true) return end - + if input.IsButtonDown(MOUSE_LEFT) and not pace.command_event_menu_opened and not open_btn:IsHovered() and clickable then + + if not clicking and selected then + if not selected.event.time then + RunConsoleCommand("pac_event", selected.event.trigger, "toggle") + elseif selected.event.time > 0 then + RunConsoleCommand("pac_event", selected.event.trigger) + else + local ply = pac.LocalPlayer + + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then + RunConsoleCommand("pac_event", selected.event.trigger, "0") + else + RunConsoleCommand("pac_event", selected.event.trigger, "1") + end + end + end + clicking = true + else clicking = false end -- Normalize mouse vector from center of screen local x, y = input.GetCursorPos() x = x - scrw2 @@ -2184,10 +4222,11 @@ do end function pac.closeEventSelectionWheel(cancel) + open_btn:Hide() gui.EnableScreenClicker(false) pac.RemoveHook("HUDPaint","custom_event_selector") - if selected and cancel ~= true then + if selected and cancel ~= true and close_click then if not selected.event.time then RunConsoleCommand("pac_event", selected.event.trigger, "toggle") elseif selected.event.time > 0 then @@ -2205,6 +4244,270 @@ do selected = nil end + local panels = {} + function pac.openEventSelectionList() + if not IsValid(open_btn) then open_btn = vgui.Create("DButton") end + open_btn:SetSize(80,30) + open_btn:SetText("Customize") + open_btn:SetPos(ScrW() - 80,0) + pace.command_event_menu_opened = nil + + function open_btn:DoClick() + + if (pace.command_event_menu_opened == nil) then + pace.ConfigureEventWheelMenu() + elseif IsValid(pace.command_event_menu_opened) then + pace.command_event_menu_opened:Remove() + end + + end + + open_btn:Show() + pace.command_colors = pace.command_colors or {} + clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1 + close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0 + + local height = 2*event_list_font_size:GetInt() + 8 + panels = panels or {} + if not table.IsEmpty(panels) then + for i, v in pairs(panels) do + v:Remove() + end + end + local selections = {} + local events = get_events() + for i, v in ipairs(events) do + + + local list_element = vgui.Create("DPanel") + + panels[i] = list_element + list_element:SetSize(250,height) + list_element.event = v + + selections[i] = { + grow = 0, + name = v.trigger, + event = v, + pnl = list_element + } + function list_element:Paint() end + function list_element:DoCommand() + if not selected.event.time then + RunConsoleCommand("pac_event", selected.event.trigger, "toggle") + elseif selected.event.time > 0 then + RunConsoleCommand("pac_event", selected.event.trigger) + else + local ply = pac.LocalPlayer + + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then + RunConsoleCommand("pac_event", selected.event.trigger, "0") + else + RunConsoleCommand("pac_event", selected.event.trigger, "1") + end + end + end + function list_element:Think() + if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_G) then + self:Remove() + end + if self:IsHovered() then + selected = self + if input.IsMouseDown(MOUSE_LEFT) and not self.was_clicked and not IsValid(pace.command_event_menu_opened) and not open_btn:IsHovered() and clickable2 then + self.was_clicked = true + self:DoCommand() + elseif not input.IsMouseDown(MOUSE_LEFT) then self.was_clicked = false end + end + end + + end + + gui.EnableScreenClicker(true) + + pac.AddHook("HUDPaint","custom_event_selector_list",function() + local height = 2*event_list_font_size:GetInt() + 8 + -- Right clicking cancels + if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionList(true) return end + + DisableClipping(true) + render.PushFilterMag(TEXFILTER.ANISOTROPIC) + render.PushFilterMin(TEXFILTER.ANISOTROPIC) + draw.SimpleText("Right click to cancel", "DermaDefault", ScrW()/2, ScrH()/2, color_red, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) + local x = 0 + local y = 0 + for i, v in ipairs(selections) do + if IsValid(v.pnl) then + + if y + height > ScrH() then + y = 0 + x = x + 200 + end + local list_element = v.pnl + list_element:SetPos(x,y) + list_element:SetSize(250,height) + + local ply = pac.LocalPlayer + local data = ply.pac_command_events and ply.pac_command_events[list_element.event.trigger] + local indicator_color + + if data then + local is_oneshot = list_element.event.time and list_element.event.time > 0 + + if is_oneshot then + local f = (pac.RealTime - data.time) / list_element.event.time + local s = Lerp(math.Clamp(f,0,1), 1, 0) + local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15) + + indicator_color = HSVToColor(210,s,v) + else + if data.on == 1 then + indicator_color = HSVToColor(210,1,0.55) + else + indicator_color = HSVToColor(210,0,0.15) + end + end + else + indicator_color = HSVToColor(210,0,0.15) + end + + local main_color = HSVToColor(210,0,0.15) + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + main_color = Color(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + end + + local hue, sat, lightness_value = ColorToHSL(main_color) + + + if eventlist_style:GetInt() == 0 then + surface.SetDrawColor(indicator_color) + surface.DrawRect(x,y,200,height) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x,y,200,height,2) + elseif eventlist_style:GetInt() == 1 then + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + surface.DrawRect(x,y,200,height) + + surface.SetDrawColor(indicator_color) + surface.DrawRect(x + 200/6,y + height/6,200 * 0.666,height * 0.666,2) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x,y,200,height,2) + + elseif eventlist_style:GetInt() == 2 then + surface.DrawOutlinedRect(x,y,200,height,2) + if pace.command_colors[v.name] then + local col_str_tbl = string.Split(pace.command_colors[v.name]," ") + surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) + else + surface.SetDrawColor(HSVToColor(210,0,0.15)) + end + surface.DrawRect(x,y,200,height) + + surface.SetDrawColor(indicator_color) + surface.DrawRect(x + 150,y,50,height/2,2) + surface.SetDrawColor(0,0,0) + surface.DrawOutlinedRect(x + 150,y,50,height/2,2) + surface.DrawOutlinedRect(x,y,200,height,2) + end + + local text_color = Color(255,255,255) + if lightness_value > 0.5 and eventlist_style:GetInt() ~= 0 then + text_color = Color(0,0,0) + end + draw.SimpleText(v.name,"pac_font_" .. event_list_font_size:GetString(),x + 4,y + 4, text_color, TEXT_ALIGN_LEFT) + y = y + height + + end + + end + + render.PopFilterMag() + render.PopFilterMin() + DisableClipping(false) + + end) + end + + function pac.closeEventSelectionList(cancel) + open_btn:Hide() + gui.EnableScreenClicker(false) + pac.RemoveHook("HUDPaint","custom_event_selector_list") + + if IsValid(selected) and close_click2 then + if selected:IsHovered() then + selected:DoCommand() + end + end + for i,v in pairs(panels) do v:Remove() end + selected = nil + end + + concommand.Add("+pac_events", pac.openEventSelectionWheel) concommand.Add("-pac_events", pac.closeEventSelectionWheel) + + concommand.Add("+pac_events_list", pac.openEventSelectionList) + concommand.Add("-pac_events_list", pac.closeEventSelectionList) + end + +net.Receive("pac.SendPlayerObjUsed", function() + + local ply = net.ReadEntity() + local ent = net.ReadEntity() + local class = net.ReadString() + local override = net.ReadBool() + + if ply then + if ply:IsPlayer() and override then + ply.entity_inuse = ent + ply.entity_inuse_classname = class + end + end +end) + +net.Receive("pac.BroadcastDamageAttributions", function() + local ent = net.ReadEntity() + local tbl = net.ReadTable() + local kill = net.ReadBool() + if IsValid(ent) and tbl and IsValid(tbl.inflictor) then + ent.pac_damage_attributions = ent.pac_damage_attributions or {} + ent.pac_damage_attributions[tbl.attacker] = tbl + ent.pac_damage_attributions.latest = tbl + ent.pac_damage_attributions.is_kill = kill + if tbl.inflictor:GetClass() == "npc_grenade_frag" then + ent.pac_damage_attributions.IngoingGraceTime = CurTime() + end + end + +end) + +net.Receive("pac_update_healthbars", function() + local ent = net.ReadEntity() + local tbl = net.ReadTable() + + ent.pac_healthbars = tbl + + ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {} + ent.pac_healthbars_uidtotals = ent.pac_healthbars_uidtotals or {} + ent.pac_healthbars_total = 0 + + for layer=15,0,-1 do --go progressively inward in the layers + ent.pac_healthbars_layertotals[layer] = 0 + if tbl[layer] then + for uid,value in pairs(tbl[layer]) do --check the healthbars by uid + ent.pac_healthbars_uidtotals[uid] = value + ent.pac_healthbars_layertotals[layer] = ent.pac_healthbars_layertotals[layer] + value + ent.pac_healthbars_total = ent.pac_healthbars_total + value + end + else + ent.pac_healthbars_layertotals[layer] = nil + end + end + +end) diff --git a/lua/pac3/core/client/parts/group.lua b/lua/pac3/core/client/parts/group.lua index 2c0d2170a..ba6b30070 100644 --- a/lua/pac3/core/client/parts/group.lua +++ b/lua/pac3/core/client/parts/group.lua @@ -8,6 +8,8 @@ PART.Description = "right click to add parts" BUILDER:StartStorableVars() BUILDER:GetSet("Duplicate", false) BUILDER:GetSet("OwnerName", "self") + BUILDER:GetSet("ModelTracker", "", {hide_in_editor = true}) + BUILDER:GetSet("ClassTracker", "", {hide_in_editor = true}) BUILDER:EndStorableVars() local init_list = {} @@ -50,6 +52,8 @@ function PART:SetOwner(ent) if not pac.HookEntityRender(owner, self) then self:ShowFromRendering() end + self.ModelTracker = owner:GetModel() + self.ClassTracker = owner:GetClass() end end end diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua new file mode 100644 index 000000000..c3da33ff3 --- /dev/null +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -0,0 +1,230 @@ +local BUILDER, PART = pac.PartTemplate("base_drawable") + +PART.ClassName = "interpolated_multibone" +PART.Group = 'advanced' +PART.Icon = 'icon16/table_multiple.png' +PART.is_model_part = false + +PART.ManualDraw = true +PART.HandleModifiersManually = false + +BUILDER:StartStorableVars() + :SetPropertyGroup("test") + :GetSet("Preview", false) + :SetPropertyGroup("Interpolation") + :GetSet("LerpValue",0) + :GetSet("Power",1) + :GetSet("InterpolatePosition", true) + :GetSet("InterpolateAngles", true) + :SetPropertyGroup("Nodes") + :GetSetPart("Node1") + :GetSetPart("Node2") + :GetSetPart("Node3") + :GetSetPart("Node4") + :GetSetPart("Node5") + :GetSetPart("Node6") + :GetSetPart("Node7") + :GetSetPart("Node8") + :GetSetPart("Node9") + :GetSetPart("Node10") + :GetSetPart("Node11") + :GetSetPart("Node12") + :GetSetPart("Node13") + :GetSetPart("Node14") + :GetSetPart("Node15") + :GetSetPart("Node16") + :GetSetPart("Node17") + :GetSetPart("Node18") + :GetSetPart("Node19") + :GetSetPart("Node20") +:EndStorableVars() + +function PART:OnRemove() + SafeRemoveEntityDelayed(self.Owner,0.1) +end + +function PART:Initialize() + self.nodes = {} + self.Owner = pac.CreateEntity("models/pac/default.mdl") + self.Owner:SetNoDraw(true) + self.valid_time = CurTime() + 1 +end + +function PART:OnShow() + self.valid_time = CurTime() +end + +function PART:OnHide() + hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) + +end + +function PART:OnRemove() + hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) +end +--NODES self 1 2 3 +--STAGE 0 1 2 3 +--PROPORTION 0 0.5 0 0.5 0 0.5 3 +function PART:OnDraw() + self:UpdateNodes() + if self.valid_time > CurTime() then return end + + self.pos = self.pos or self:GetWorldPosition() + self.ang = self.ang or self:GetWorldAngles() + + if not self.Preview then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end + + local stage = math.max(0,math.floor(self.LerpValue)) + local proportion = math.max(0,self.LerpValue) % 1 + + if self.Preview then + hook.Add("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID, function() + render.DrawLine(self.pos,self.pos + self.ang:Forward()*50, Color(255,0,0)) + render.DrawLine(self.pos,self.pos - self.ang:Right()*50, Color(0,255,0)) + render.DrawLine(self.pos,self.pos + self.ang:Up()*50, Color(0,0,255)) + render.DrawWireframeSphere(self.pos, 8 + 2*math.sin(5*RealTime()), 15, 15, Color(255,255,255), true) + end) + end + self:Interpolate(stage,proportion) + +end + +function PART:UpdateNodes() + for i=1,10,1 do + self.nodes["Node"..i] = self["Node"..i] + end +end + +function PART:SetWorldPos(x,y,z) + self.pos.x = x + self.pos.y = y + self.pos.z = z +end + +function PART:Interpolate(stage, proportion) + --print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1)) + local firstnode + if stage <= 0 then + firstnode = self + else + firstnode = self.nodes["Node"..stage] or self + end + + + local secondnode = self.nodes["Node"..stage+1] + if firstnode == nil or firstnode == NULL or not firstnode.GetWorldPosition then firstnode = self end + if secondnode == nil or secondnode == NULL or not secondnode.GetWorldPosition then secondnode = self end + + proportion = math.pow(proportion,self.Power) + if secondnode ~= nil and secondnode ~= NULL then + self.pos = (1-proportion)*(firstnode:GetWorldPosition()) + (secondnode:GetWorldPosition())*proportion + self.ang = GetClosestAngleMidpoint(firstnode:GetWorldAngles(), secondnode:GetWorldAngles(), proportion) + --self.ang = (1-proportion)*(firstnode:GetWorldAngles() + Angle(360,360,360)) + (secondnode:GetWorldAngles() + Angle(360,360,360))*proportion + elseif proportion == 0 then + self.pos = firstnode:GetWorldPosition() + self.ang = firstnode:GetWorldAngles() + else + if self.InterpolatePosition then self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion end + if self.InterpolateAngles then self.ang = GetClosestAngleMidpoint(self:GetWorldAngles(), self:GetWorldAngles(), proportion) end + --self.ang = (1-proportion)*(self:GetWorldAngles() + Angle(360,360,360)) + (self:GetWorldAngles() + Angle(360,360,360))*proportion + end + + if not self.InterpolatePosition then + self.pos = self:GetWorldPosition() + end + if not self.InterpolateAngles then + self.ang = self:GetWorldAngles() + end + self.Owner:SetPos(self.pos) + self.Owner:SetAngles(self.ang) +end + +function GetClosestAngleMidpoint(a1, a2, proportion) + --print(a1) + --print(a2) + local axes = {"p","y","r"} + local ang_delta_candidate1 + local ang_delta_candidate2 + local ang_delta_candidate3 + local ang_delta_final + local final_ang = Angle() + for _,ax in pairs(axes) do + ang_delta_candidate1 = a2[ax] - a1[ax] + ang_delta_candidate2 = (a2[ax] + 360) - a1[ax] + ang_delta_candidate3 = (a2[ax] - 360) - a1[ax] + ang_delta_final = 180 + if math.abs(ang_delta_candidate1) < math.abs(ang_delta_final) then + ang_delta_final = ang_delta_candidate1 + end + if math.abs(ang_delta_candidate2) < math.abs(ang_delta_final) then + ang_delta_final = ang_delta_candidate2 + end + if math.abs(ang_delta_candidate3) < math.abs(ang_delta_final) then + ang_delta_final = ang_delta_candidate3 + end + --print("at "..ax.." 1:"..ang_delta_candidate1.." 2:"..ang_delta_candidate2.." 3:"..ang_delta_candidate3.." pick "..ang_delta_final) + final_ang[ax] = a1[ax] + proportion * ang_delta_final + end + + return final_ang +end + +function PART:GoTo(part) + self.pos = part:GetWorldPosition() or self:GetWorldPosition() + self.ang = part:GetWorldAngles() or self:GetWorldAngles() +end + +--we need to know the stage and proportion (progress) +--e.g. lerp 0.5 is stage 0, proportion 0.5 because it's 50% toward Node 1 +--e.g. lerp 2.2 is stage 2, proportion 0.2 because it's 20% toward Node 3 +function PART:GetInterpolationParameters() + stage = math.max(0,math.floor(self.LerpValue)) + proportion = math.max(0,self.LerpValue) % 1 + --print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1)) + --print("proportion is " .. proportion) + return stage, proportion +end + +function PART:GetNodeAngle(nodenumber) + --print("node" .. nodenumber .. " angle " .. self.__['Node'..nodenumber].Angles) + --print("node" .. nodenumber .. " world angle " .. self.__['Node'..nodenumber]:GetWorldAngles()) + + --return self.Node1:GetWorldAngles() +end + +function PART:GetNodePosition(nodenumber) + --print("node" .. nodenumber .. " position " .. self.__['Node'..nodenumber].Position) + --print("node" .. nodenumber .. " world position " .. self.__['Node'..nodenumber]:GetWorldPosition()) + --return self.Node1:GetWorldPosition() +end + +function PART:InterpolateFromLerp(lerp) +end + +function PART:InterpolateFromNodes(firstnode, secondnode, proportion) + --position_interpolated = InterpolateFromStage("position", stage, self.Lerp) +end + +function PART:InterpolateFromStage(stage, proportion) + self:InterpolateFromNodes(stage, stage + 1) +end + +function PART:InterpolateAngle() + +end + + +function PART:SetInterpolatePosition(b) + --print(type(b).." "..b) + self.InterpolatePosition = b +end + +function PART:SetInterpolateAngles(b) + --print(type(b).." "..b) + self.InterpolateAngles = b +end + + + + +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/light.lua b/lua/pac3/core/client/parts/light.lua index 36249ad3f..8c3d3d15c 100644 --- a/lua/pac3/core/client/parts/light.lua +++ b/lua/pac3/core/client/parts/light.lua @@ -18,6 +18,21 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Brightness", 8) BUILDER:GetSet("Size", 100, {editor_sensitivity = 0.25}) BUILDER:GetSet("Color", Vector(1, 1, 1), {editor_panel = "color2"}) + BUILDER:GetSet("Style", 0, {editor_clamp = {0, 12}, enums = { + ["Normal"] = "0", + ["Flicker A"] = "1", + ["Slow, strong pulse"] = "2", + ["Candle A"] = "3", + ["Fast strobe"] = "4", + ["Gentle pulse"] = "5", + ["Flicker B"] = "6", + ["Candle B"] = "7", + ["Candle C"] = "8", + ["Slow strobe"] = "9", + ["Fluorescent flicker"] = "10", + ["Slow pulse, noblack"] = "11", + ["Underwater light mutation"] = "12" + }}) BUILDER:EndStorableVars() function PART:GetLight() @@ -68,6 +83,10 @@ function PART:OnDraw() self:GetLight().pos = pos self:GetLight().dir = ang:Forward() end +function PART:SetStyle(val) + self.Style = val + self:GetLight().Style = self.Style +end function PART:SetSize(val) self.Size = val diff --git a/lua/pac3/core/client/parts/physics.lua b/lua/pac3/core/client/parts/physics.lua index cc616301e..3cc8fb9ab 100644 --- a/lua/pac3/core/client/parts/physics.lua +++ b/lua/pac3/core/client/parts/physics.lua @@ -1,3 +1,15 @@ +local physprop_enums = {} +local physprop_indices = {} +for i=0,500,1 do + local name = util.GetSurfacePropName(i) + if name ~= "" then + physprop_enums[name] = name + physprop_indices[name] = i + end +end + + + local BUILDER, PART = pac.PartTemplate("base") PART.ThinkTime = 0 @@ -7,25 +19,44 @@ PART.Group = 'model' PART.Icon = 'icon16/shape_handles.png' BUILDER:StartStorableVars() - BUILDER:GetSet("Box", true) - BUILDER:GetSet("Radius", 1) - BUILDER:GetSet("SelfCollision", false) - BUILDER:GetSet("Gravity", true) - BUILDER:GetSet("Collisions", true) - BUILDER:GetSet("Mass", 100) - - BUILDER:GetSet("Follow", false) - BUILDER:GetSet("SecondsToArrive", 0.1) - - BUILDER:GetSet("MaxSpeed", 10000) - BUILDER:GetSet("MaxAngular", 3600) - - BUILDER:GetSet("MaxSpeedDamp", 1000) - BUILDER:GetSet("MaxAngularDamp", 1000) - BUILDER:GetSet("DampFactor", 1) - - BUILDER:GetSet("ConstrainSphere", 0) + BUILDER:SetPropertyGroup("Behavior") + :GetSet("SelfCollision", false) + :GetSet("Gravity", true) + :GetSet("Collisions", true) + :GetSet("ConstrainSphere", 0) + :GetSet("Pushable", false, {description = "Whether the physics object should be pushed back by nearby players and props within its radius."}) + :GetSet("ThinkDelay", 1) + + BUILDER:SetPropertyGroup("Follow") + :GetSet("Follow", false, {description = "Whether the physics object should follow via SetPos. But it might clip in the world! seconds to arrive will be used for deciding the speed"}) + :GetSet("PushFollow", false, {description = "Whether the physics object should try to follow via AddVelocity, to prevent phasing through walls. But it might get stuck in a corner!\n".. + "seconds to arrive, along with the extra distance if it's beyond the constrain sphere, will be used for deciding the speed"}) + :GetSet("SecondsToArrive", 0.1) + :GetSet("MaxSpeed", 10000) + :GetSet("MaxAngular", 3600) + :GetSet("MaxSpeedDamp", 1000) + :GetSet("MaxAngularDamp", 1000) + :GetSet("DampFactor", 1) + BUILDER:SetPropertyGroup("Speeds") + + :GetSet("ConstantVelocity", Vector(0,0,0)) + + BUILDER:SetPropertyGroup("Shape") + :GetSet("BoxScale",Vector(1,1,1)) + :GetSet("Box", true) + :GetSet("Radius", 1) + :GetSet("SurfaceProperties", "default", {enums = physprop_enums}) + :GetSet("Preview", false) + :GetSet("Mass", 100) + + BUILDER:SetPropertyGroup("InitialVelocity") + :GetSet("AddOwnerSpeed", false) + :GetSet("InitialVelocityVector", Vector(0,0,0)) + :GetSetPart("InitialVelocityPart") + :GetSet("OverrideInitialPosition", false, {description = "Whether the initial velocity part should be used as an initial position, otherwise it'll just be for the initial velocity's angle"}) + + BUILDER:EndStorableVars() local function IsInvalidParent(self) @@ -55,6 +86,38 @@ function PART:SetMass(n) end end +function PART:MeshDraw() + if not IsValid(self.phys) then return end + + local mesh = (self.phys):GetMesh() + local drawmesh = Mesh() + + if mesh == nil or not self.Box then + render.DrawWireframeSphere( self.phys:GetPos(), self.Radius, 10, 10, Color( 255, 255, 255 ) ) + else + drawmesh:BuildFromTriangles(mesh) + + render.SetMaterial( Material( "models/wireframe" ) ) + local mat = Matrix() + mat:Translate(self.phys:GetPos()) + mat:Rotate(self.phys:GetAngles()) + cam.PushModelMatrix( mat ) + drawmesh:Draw() + cam.PopModelMatrix() + end +end + +function PART:SetPreview(b) + self.Preview = b + if self.Preview then + hook.Add("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID, function() + self:MeshDraw() + end) + else + hook.Remove("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID) + end +end + function PART:SetRadius(n) self.Radius = n @@ -67,9 +130,9 @@ function PART:SetRadius(n) ent:SetNoDraw(false) if self.Box then - ent:PhysicsInitBox(Vector(1,1,1) * -n, Vector(1,1,1) * n) + ent:PhysicsInitBox(self.BoxScale * -n, self.BoxScale * n, self.SurfaceProperties) else - ent:PhysicsInitSphere(n) + ent:PhysicsInitSphere(n, self.SurfaceProperties) end self.phys = ent:GetPhysicsObject() @@ -79,6 +142,20 @@ function PART:SetRadius(n) end end +function PART:SetSurfaceProperties(str) + self.SurfaceProperties = str + self:SetRadius(self.Radius) --refresh the physics +end + +function PART:GetSurfacePropsTable() --to view info over in the properties + return util.GetSurfaceData(physprop_indices[self.SurfaceProperties]) +end + +function PART:SetBoxScale(vec) + self.BoxScale = vec + self:SetRadius(self.Radius) --refresh the physics +end + function PART:SetGravity(b) self.Gravity = b @@ -121,25 +198,34 @@ function PART:OnThink() if phys:IsValid() then phys:Wake() - if self.Follow then - params.pos = self.Parent:GetWorldPosition() - params.angle = self.Parent:GetWorldAngles() - - params.secondstoarrive = math.max(self.SecondsToArrive, 0.0001) - params.maxangular = self.MaxAngular - params.maxangulardamp = self.MaxAngularDamp - params.maxspeed = self.MaxSpeed - params.maxspeeddamp = self.MaxSpeedDamp - params.dampfactor = self.DampFactor + if self.Follow or self.PushFollow then + if not self.PushFollow then + params.pos = self.Parent:GetWorldPosition() + params.angle = self.Parent:GetWorldAngles() - params.teleportdistance = 0 - - phys:ComputeShadowControl(params) + params.secondstoarrive = math.max(self.SecondsToArrive, 0.0001) + params.maxangular = self.MaxAngular + params.maxangulardamp = self.MaxAngularDamp + params.maxspeed = self.MaxSpeed + params.maxspeeddamp = self.MaxSpeedDamp + params.dampfactor = self.DampFactor + params.teleportdistance = 0 + phys:ComputeShadowControl(params) + end + -- this is nicer i think if self.ConstrainSphere ~= 0 and phys:GetPos():Distance(self.Parent:GetWorldPosition()) > self.ConstrainSphere then - phys:SetPos(self.Parent:GetWorldPosition() + (self.Parent:GetWorldPosition() - phys:GetPos()):GetNormalized() * -self.ConstrainSphere) + if not self.PushFollow then + phys:SetPos(self.Parent:GetWorldPosition() + (self.Parent:GetWorldPosition() - phys:GetPos()):GetNormalized() * -self.ConstrainSphere) + --new push mode + else + local vec = (self.Parent:GetWorldPosition() - phys:GetPos()) + local current_dist = vec:Length() + local extra_dist = current_dist - self.ConstrainSphere + phys:AddVelocity(0.5 * vec:GetNormalized() * extra_dist / math.Clamp(self.SecondsToArrive,0.05,10)) + end end else if self.ConstrainSphere ~= 0 then @@ -162,13 +248,25 @@ function PART:OnUnParent(part) timer.Simple(0, function() self:Disable() end) end - function PART:OnShow() timer.Simple(0, function() self:Enable() end) end function PART:OnHide() + if not self.Hide and not self:IsHidden() then + timer.Simple(0.4, function() + self:GetRootPart():OnShow() + self.Parent:OnShow() + for _,part in pairs(self:GetParent():GetChildrenList()) do + part:OnShow() + + end + end) + return + end timer.Simple(0, function() self:Disable() end) + + hook.Remove("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID) end function PART:Enable() @@ -190,14 +288,60 @@ function PART:Enable() end self.disabled = false + + if IsValid(self.InitialVelocityPart) then + if self.InitialVelocityPart.GetWorldPosition then + local local_vec, local_ang = self.InitialVelocityPart:GetDrawPosition() + local local_vec2 = self.InitialVelocityVector.x * local_ang:Forward() + + self.InitialVelocityVector.y * local_ang:Right() + + self.InitialVelocityVector.z * local_ang:Up() + self.phys:AddVelocity(local_vec2) + if self.OverrideInitialPosition then + self.phys:SetPos(local_vec) + end + else + self.phys:AddVelocity(self.InitialVelocityVector) + end + else + self.phys:AddVelocity(self.InitialVelocityVector) + end + + if self.AddOwnerSpeed then + self.phys:AddVelocity(self:GetRootPart():GetOwner():GetVelocity()) + end + + timer.Simple(self.ThinkDelay, function() hook.Add("Tick", "pac_phys_repulsionthink"..self.UniqueID, function() + if not IsValid(self.phys) then hook.Remove("Tick", "pac_phys_repulsionthink"..self.UniqueID) return end + self.phys:AddVelocity(self.ConstantVelocity * RealFrameTime()) + + if self.Pushable then + local pushvec = Vector(0,0,0) + local pos = self.phys:GetPos() + local ents_tbl = ents.FindInSphere(pos, self.Radius) + local valid_phys_pushers = 0 + for i,ent in pairs(ents_tbl) do + if ent.GetPhysicsObject or ent:IsPlayer() then + if ent:IsPlayer() or ent:GetClass() == "prop_physics" or ent:GetClass() == "prop_ragdoll" then + valid_phys_pushers = valid_phys_pushers + 1 + pushvec = pushvec + (pos - ent:GetPos()):GetNormalized() * 20 + end + end + end + if valid_phys_pushers > 0 then self.phys:AddVelocity(pushvec / valid_phys_pushers) end + end + + + end) end) end function PART:Disable() + hook.Remove("Tick", "pac_phys_repulsionthink"..self.UniqueID) if IsInvalidParent(self) then return end local part = self:GetParent() local ent = part:GetOwner() + if ent:IsValid() then -- SetNoDraw does not care of validity but PhysicsInit does? ent:SetNoDraw(true) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index b111bd342..885f83a08 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -44,10 +44,10 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Pow", 1) BUILDER:SetPropertyGroup("behavior") - BUILDER:GetSet("Additive", false) - BUILDER:GetSet("PlayerAngles", false) - BUILDER:GetSet("ZeroEyePitch", false) - BUILDER:GetSet("ResetVelocitiesOnHide", true) + BUILDER:GetSet("Additive", false, {description = "This means that every computation frame, the proxy will add its output to its current stored memory. This can quickly get out of control if you don't know what you're doing! This is like using the feedback() function"}) + BUILDER:GetSet("PlayerAngles", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will choose between the owner entity's Angles or EyeAngles. Unsure of whether this makes a difference."}) + BUILDER:GetSet("ZeroEyePitch", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will force the angle to be horizon level."}) + BUILDER:GetSet("ResetVelocitiesOnHide", true, {description = "Because velocity calculators use smoothing that makes the output converge toward a crude rolling average, it might matter whether you want to get a clean slate readout.\n(VelocityRoughness is how close to the snapshots it will be. Lower means smoother but delayed. Higher means less smoothing but it might overshoot and be inaccurate because of frame time works and varies)"}) BUILDER:GetSet("VelocityRoughness", 10) BUILDER:EndStorableVars() @@ -219,6 +219,20 @@ PART.Inputs.property = function(self, property_name, field) return 0 end +PART.Inputs.polynomial = function(self, x, ...) + x = x or 1 + local total = 0 + local args = { ... } + + pow = 0 + for _, coefficient in ipairs(args) do + total = total + coefficient*math.pow(x, pow) + pow = pow + 1 + end + return total + +end + PART.Inputs.owner_position = function(self) local owner = get_owner(self) @@ -307,7 +321,7 @@ PART.Inputs.random_once = function(self, seed, min, max) self.rand_id = self.rand_id or {} if seed then self.rand_id[seed] = self.rand_id[seed] or min + math.random()*(max-min) - else + else self.rand = self.rand or min + math.random()*(max-min) end @@ -343,22 +357,25 @@ end PART.Inputs.part_distance = function(self, uid1, uid2) if not uid1 or not uid2 then return 0 end + local owner = self:GetPlayerOwner() - local PartA = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid1, self) end + local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) + if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end - local PartB = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid2) - if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid2, self) end + local PartB = pac.GetPartFromUniqueID(pac.Hash(owner), uid2) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid2) + if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(owner), uid2, self) end if not PartA:IsValid() or not PartB:IsValid() then return 0 end + if not PartA.Position or not PartB.Position then return 0 end return (PartB:GetWorldPosition() - PartA:GetWorldPosition()):Length() end PART.Inputs.event_alternative = function(self, uid1, num1, num2) if not uid1 then return 0 end + local owner = self:GetPlayerOwner() - local PartA = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid1, self) end + local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) + if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end if PartA.ClassName == "event" then if PartA.event_triggered then return num1 or 0 @@ -929,6 +946,32 @@ PART.Inputs.flat_dot_right = function(self) return 0 end + +PART.Inputs.pac_healthbars_total = function(self) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars then + return ent.pac_healthbars_total or 0 + end + return 0 +end + +PART.Inputs.pac_healthbars_layertotal = function(self, layer) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars and ent.pac_healthbars_layertotals then + return ent.pac_healthbars_layertotals[layer] or 0 + end + return 0 +end + +PART.Inputs.pac_healthbar_uidvalue = function(self, uid) + local ent = self:GetPlayerOwner() + if ent.pac_healthbars and ent.pac_healthbars_uidtotals then + return ent.pac_healthbars_uidtotals[uid] or 0 + end + return 0 +end + + net.Receive("pac_proxy", function() local ply = net.ReadEntity() local str = net.ReadString() @@ -940,6 +983,9 @@ net.Receive("pac_proxy", function() if ply:IsValid() then ply.pac_proxy_events = ply.pac_proxy_events or {} ply.pac_proxy_events[str] = {name = str, x = x, y = y, z = z} + if LocalPlayer() == ply then + pac.Message("pac_proxy -> command(\""..str.."\") is " .. x .. "," .. y .. "," .. z) + end end end) @@ -1093,7 +1139,21 @@ end function PART:OnThink() local part = self:GetTarget() if not part:IsValid() then return end - if part.ClassName == 'woohoo' then return end + if part.ClassName == 'woohoo' then --why a part hardcode exclusion?? + --ok fine I guess it's because it's super expensive, but at least we can be selective about it, the other parameters are safe + if self.VariableName == "Resolution" or self.VariableName == "BlurFiltering" and self.touched then + return + end + end + + --foolproofing: scream at the user if they didn't set a variable name + if self == pace.current_part then self.touched = true end + if self ~= pace.current_part and self.VariableName == "" and self.touched then + self:AttachEditorPopup("You forgot to set a variable name! The proxy won't work until it knows where to send the math!", true) + pace.FlashNotification("An edited proxy still has no variable name! The proxy won't work until it knows where to send the math!") + self:SetWarning("You forgot to set a variable name! The proxy won't work until it knows where to send the math!") + self.touched = false + elseif self.VariableName ~= "" then self:SetWarning() end self:CalcVelocity() diff --git a/lua/pac3/core/client/parts/sound.lua b/lua/pac3/core/client/parts/sound.lua index da1f90d84..51afcb415 100644 --- a/lua/pac3/core/client/parts/sound.lua +++ b/lua/pac3/core/client/parts/sound.lua @@ -11,6 +11,7 @@ PART.Group = 'effects' BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") BUILDER:GetSet("Path", "", {editor_panel = "sound"}) + BUILDER:GetSet("AllPaths", "", {hide_in_editor = true}) BUILDER:GetSet("Volume", 1, {editor_sensitivity = 0.25}) BUILDER:GetSet("Pitch", 1, {editor_sensitivity = 0.125}) BUILDER:GetSet("Radius", 1500) @@ -166,6 +167,15 @@ function PART:OnThink() end function PART:SetPath(path) + if #path > 1024 then + self:AttachEditorPopup("This part has more sounds than the 1024-letter limit! Please do not touch the path field now!") + self:SetInfo("This part has more sounds than the 1024-letter limit! Please do not touch the path field now!") + if self.Name == "" then + self:SetName("big sound list") + pace.RefreshTree() + end + end + self.seq_index = 1 self.Path = path @@ -180,10 +190,13 @@ function PART:SetPath(path) if min and max then for i = min, max do table.insert(paths, (path:gsub("%[.-%]", i))) + self.AllPaths = self.AllPaths .. ";" .. path end else table.insert(paths, path) + self.AllPaths = self.AllPaths .. ";" .. path end + end for _, stream in pairs(self.streams) do @@ -245,10 +258,15 @@ function PART:SetPath(path) end end self.paths = paths + end PART.last_stream = NULL +function PART:UpdateSoundsFromAll() + self:SetPath(self.AllPaths) +end + function PART:PlaySound(_, additiveVolumeFraction) --PrintTable(self.streams) additiveVolumeFraction = additiveVolumeFraction or 0 @@ -271,7 +289,7 @@ function PART:PlaySound(_, additiveVolumeFraction) if self.streams[snd]:IsValid() then stream = self.streams[snd] - print(snd,self.seq_index) + --print(snd,self.seq_index) end self.seq_index = self.seq_index + self.SequentialStep if self.seq_index > #self.paths then @@ -333,4 +351,4 @@ function PART:OnRemove() end end -BUILDER:Register() +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/server/event.lua b/lua/pac3/core/server/event.lua index 84cc0db15..ba156128a 100644 --- a/lua/pac3/core/server/event.lua +++ b/lua/pac3/core/server/event.lua @@ -1,6 +1,17 @@ util.AddNetworkString("pac_proxy") util.AddNetworkString("pac_event") +util.AddNetworkString("pac_event_set_sequence") + +net.Receive("pac_event_set_sequence", function(len, ply) + local event = net.ReadString() + local num = net.ReadUInt(8) + ply.pac_command_events = ply.pac_command_events or {} + for i=1,100,1 do + ply.pac_command_events[event..i] = nil + end + ply.pac_command_events[event..num] = {name = event, time = pac.RealTime, on = 1} +end) -- event concommand.Add("pac_event", function(ply, _, args) @@ -21,6 +32,9 @@ concommand.Add("pac_event", function(ply, _, args) net.WriteString(event) net.WriteInt(extra, 8) net.Broadcast() + ply.pac_command_events = ply.pac_command_events or {} + ply.pac_command_events[event] = ply.pac_command_events[event] or {} + ply.pac_command_events[event] = {name = event, time = pac.RealTime, on = extra} end) concommand.Add("+pac_event", function(ply, _, args) @@ -66,12 +80,49 @@ end) -- proxy concommand.Add("pac_proxy", function(ply, _, args) + str = args[1] + + if ply:IsValid() then + ply.pac_proxy_events = ply.pac_proxy_events or {} + end + local x + local y + local z + if ply.pac_proxy_events[str] ~= nil then + if args[2] then + if string.sub(args[2],1,2) == "++" or string.sub(args[2],1,2) == "--" then + x = ply.pac_proxy_events[str].x + tonumber(string.sub(args[2],2,#args[2])) + else x = tonumber(args[2]) or ply.pac_proxy_events[str].x or 0 end + end + + if args[3] then + if string.sub(args[3],1,2) == "++" or string.sub(args[3],1,2) == "--" then + y = ply.pac_proxy_events[str].y + tonumber(string.sub(args[3],2,#args[3])) + else y = tonumber(args[3]) or ply.pac_proxy_events[str].y or 0 end + end + if not args[3] then y = 0 end + + if args[4] then + if string.sub(args[4],1,2) == "++" or string.sub(args[4],1,2) == "--" then + z = ply.pac_proxy_events[str].z + tonumber(string.sub(args[4],2,#args[4])) + else z = tonumber(args[4]) or ply.pac_proxy_events[str].z or 0 end + end + if not args[4] then z = 0 end + else + x = tonumber(args[2]) or 0 + y = tonumber(args[3]) or 0 + z = tonumber(args[4]) or 0 + end + ply.pac_proxy_events[str] = {name = str, x = x, y = y, z = z} + net.Start("pac_proxy", true) net.WriteEntity(ply) net.WriteString(args[1]) - net.WriteFloat(tonumber(args[2]) or 0) - net.WriteFloat(tonumber(args[3]) or 0) - net.WriteFloat(tonumber(args[4]) or 0) + net.WriteFloat(x or 0) + net.WriteFloat(y or 0) + net.WriteFloat(z or 0) net.Broadcast() + + --PrintTable(ply.pac_proxy_events[str]) end) diff --git a/lua/pac3/core/server/net_messages.lua b/lua/pac3/core/server/net_messages.lua index b0bfd379e..235b07ea0 100644 --- a/lua/pac3/core/server/net_messages.lua +++ b/lua/pac3/core/server/net_messages.lua @@ -2,6 +2,13 @@ util.AddNetworkString("pac.AllowPlayerButtons") util.AddNetworkString("pac.BroadcastPlayerButton") +util.AddNetworkString("pac.BroadcastPlayerInputs") + +util.AddNetworkString("pac.RequestPlayerObjUsed") +util.AddNetworkString("pac.SendPlayerObjUsed") + +util.AddNetworkString("pac.BroadcastDamageAttributions") + do -- button event net.Receive("pac.AllowPlayerButtons", function(length, client) local key = net.ReadUInt(8) @@ -28,4 +35,137 @@ do -- button event pac.AddHook("PlayerButtonUp", "event", function(ply, key) broadcast_key(ply, key, false) end) +end + +--[[do -- input event + local input_enums = { + IN_ATTACK, --1 + IN_JUMP, --2 + IN_DUCK, --4 + IN_FORWARD, --8 + IN_BACK, --16 + IN_USE, --32 + IN_CANCEL, --64 + IN_LEFT, --128 + IN_RIGHT, --256 + IN_MOVELEFT, --512 + IN_MOVERIGHT, --1024 + IN_ATTACK2, --2048 + IN_RUN, --4096 + IN_RELOAD, --8192 + IN_ALT1, --16384 + IN_ALT2, --32768 + IN_SCORE, --65536 + IN_SPEED, --131072 + IN_WALK, --262144 + IN_ZOOM, --524288 + IN_WEAPON1, --1048576 + IN_WEAPON2, --2097152 + IN_BULLRUSH, --4194304 + IN_GRENADE1, --8388608 + IN_GRENADE2 --16777216 + } + + local pac_broadcast_inputs = {} + local last_input_broadcast = 0 + local player_last_input_broadcast_times = {} + + local function broadcast_inputs(update) + + if not update or not (last_input_broadcast + 0.05 < CurTime()) then return + elseif update + net.Start("pac.BroadcastPlayerInputs") + net.WriteTable(pac_broadcast_inputs) + net.WriteTable(player_last_input_broadcast_times) + net.Broadcast() + last_input_broadcast = CurTime() + end + end + + pac.AddHook("Tick", "PACBroadcastPlayerInputs", function() + + local update = false + local updated_players = {} + pac_broadcast_inputs = pac_broadcast_inputs or {} + local last_broadcast_inputs = table.Copy(pac_broadcast_inputs) + local time = CurTime() + for _,ply in pairs(player.GetAll()) do + if not pac_broadcast_inputs[ply] then pac_broadcast_inputs[ply] = {} end + for _,v in pairs(input_enums) do + + if ply:KeyDown( v ) then + pac_broadcast_inputs[ply][v] = true + elseif ply:KeyDownLast( v ) then + pac_broadcast_inputs[ply][v] = false + end + + if last_broadcast_inputs[ply] and pac_broadcast_inputs[ply] then + if last_broadcast_inputs[ply][v] ~= pac_broadcast_inputs[ply][v] then + update = true + player_last_input_broadcast_times[ply] = CurTime() + end + end + + + end + end + broadcast_inputs(update) + + end) +end]] + +do --is_using_entity + local function send_player_used_object(client, ent, class, b, from_client) + net.Start("pac.SendPlayerObjUsed") + net.WriteEntity(client) + net.WriteEntity(ent) + net.WriteString(class) + net.WriteBool(b) + + --print("BROADCAST", client, ent, class, b) + net.Send(player.GetAll()) + end + + net.Receive("pac.RequestPlayerObjUsed", function(length, client) + local from_client = true + local override = true + local ent_used = client:GetEntityInUse() + local class = "nil" + if ent_used and IsValid(ent_used) then + if ent_used.GetClass ~= nil then + class = ent_used:GetClass() + end + end + if class == "player_pickup" then + override = false + end + + send_player_used_object(client, ent_used, class, override, from_client) + end) + + hook.Add("PlayerUse", "pac.PlayerUse", function( client, ent ) + local class = ent:GetClass() + --print("USE", client,ent, class) + if ent:GetClass() ~= "player_pickup" then + send_player_used_object(client, ent, class, true, false) + end + end) +end + +do --damage attribution + timer.Simple(1, function()--call it regularly in case a new hook overrides the damage, we want the final one + pac.AddHook("EntityTakeDamage", "pac.AttributeDamage", function(ent, dmg) + local time = CurTime() + if IsValid(dmg:GetAttacker()) then + local tbl = {hit_time = time, attacker = dmg:GetAttacker(), dmg_amount = dmg:GetDamage(), dmg_type = dmg:GetDamageType(), inflictor = dmg:GetInflictor()} + ent[dmg:GetInflictor()] = tbl + net.Start("pac.BroadcastDamageAttributions") + net.WriteEntity(ent) + net.WriteTable(tbl) + net.WriteBool(ent:Health() < dmg:GetDamage()) + net.Broadcast() + end + + end) + end) end \ No newline at end of file diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua new file mode 100644 index 000000000..6d51bb62d --- /dev/null +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -0,0 +1,1173 @@ +--[[ + This is the framework for popups. This should be expandable for various use cases. + It uses DFrame as a base, overrides the Paint function for a basic fade effect. + + Tutorials will be written here +]] + + +CreateConVar("pac_popups_enable", 1, FCVAR_ARCHIVE, "Enables PAC editor popups. They provide some information but can be annoying") +CreateConVar("pac_popups_preserve_on_autofade", 1, FCVAR_ARCHIVE, "If set to 0, PAC editor popups appear only once and don't reappear when hovering over the part label or pressing F1") +CreateConVar("pac_popups_base_color", "215 230 255", FCVAR_ARCHIVE, "The color of the base filler rectangle for editor popups") +CreateConVar("pac_popups_base_color_pulse", "0", FCVAR_ARCHIVE, "Amount of pulse of the base filler rectangle for editor popups") + +CreateConVar("pac_popups_base_alpha", "0.5", FCVAR_ARCHIVE, "The alpha opacity of the base filler rectangle for editor popups") +CreateConVar("pac_popups_fade_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") +CreateConVar("pac_popups_fade_alpha", "1", FCVAR_ARCHIVE, "The alpha opacity of the fading effect for editor popups") +CreateConVar("pac_popups_text_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") +CreateConVar("pac_popups_verbosity", "beginner tutorial", FCVAR_ARCHIVE, "Sets the amount of information added to PAC editor popups. While in development, there will be limited contextual support. If no special information is defined, it will indicate the part size information. Here are the planned modes: \nbeginner tutorial : Basic tutorials about pac parts, for beginners or casual users looking for a quick reference for what a part does\nReference tutorial : doesn't give part tutorials, but still keeps events' tutorial explanations.\n") +CreateConVar("pac_popups_preferred_location", "pac tree label", FCVAR_ARCHIVE, "Sets the preferred method of PAC editor popups.\n".. + "pac tree label : the part label on the pac tree\n".. + "part world : if part is base_movable, place it next to the part in the viewport\n".. + "screen : static x,y on screen no matter what. That would be at the center\n".. + "cursor : right on the cursor\n".. + "editor bar : next to the toolbar") + + +function pace.OpenPopupConfig() + local master_pnl = vgui.Create("DFrame") + master_pnl:SetTitle("Configure PAC3 popups appearance") + master_pnl:SetSize(400,800) + master_pnl:Center() + + local list_pnl = vgui.Create("DListLayout", master_pnl) + list_pnl:Dock(FILL) + + local basecolor = vgui.Create("DColorMixer") + basecolor:SetSize(400,150) + local col_args = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + basecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + function basecolor:ValueChanged(col) + GetConVar("pac_popups_base_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_popups_base_alpha"):SetString(col.a) + end + local basecolor_pulse = vgui.Create("DNumSlider") + basecolor_pulse:SetMax(255) + basecolor_pulse:SetMin(0) + + if isnumber(GetConVar("pac_popups_base_color_pulse"):GetInt()) then + basecolor_pulse:SetValue(GetConVar("pac_popups_base_color_pulse"):GetInt()) + else + basecolor_pulse:SetValue(0) + end + + basecolor_pulse:SetText("base pulse") + function basecolor_pulse:OnValueChanged(val) + val = math.Round(tonumber(val),0) + GetConVar("pac_popups_base_color_pulse"):SetInt(val) + end + + local fadecolor = vgui.Create("DColorMixer") + fadecolor:SetSize(400,150) + col_args = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + fadecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + function fadecolor:ValueChanged(col) + GetConVar("pac_popups_fade_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_popups_fade_alpha"):SetString(col.a) + end + + local textcolor = vgui.Create("DColorMixer") + textcolor:SetSize(400,150) + col_args = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + + if isnumber(col_args[1]) then + textcolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + end + + textcolor:SetAlphaBar(false) + function textcolor:ValueChanged(col) + GetConVar("pac_popups_text_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + end + + local invertcolor_btn = vgui.Create("DButton") + invertcolor_btn:SetSize(400,30) + invertcolor_btn:SetText("Use text invert color (experimental)") + function invertcolor_btn:DoClick() + GetConVar("pac_popups_text_color"):SetString("invert") + end + + local preview_pnl = vgui.Create("DLabel") + preview_pnl:SetSize(400,170) + preview_pnl:SetText("") + local label_text = "Popup preview! The text will look like this." + + local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + local a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + + local preview_refresh_btn = vgui.Create("DButton") + preview_refresh_btn:SetSize(400,30) + preview_refresh_btn:SetText("Refresh") + local oldpaintfunc = master_pnl.Paint + local invis_frame = false + function preview_refresh_btn:DoClick() + invis_frame = not invis_frame + if invis_frame then master_pnl.Paint = nil else master_pnl.Paint = oldpaintfunc end + rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + end + + function preview_pnl:Paint( w, h ) + --base layer + local sine = 0.5 + 0.5*math.sin(CurTime()*2) + draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) ) + for band=0,w,1 do + --per-pixel fade + fade = 1 - (1/w * band * 1) + fade = math.pow(fade,2) + draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) + end + draw.DrawText(label_text, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,255)) + end + + list_pnl:Add(Label("Base color")) + list_pnl:Add(basecolor) + list_pnl:Add(basecolor_pulse) + list_pnl:Add(Label("Gradient color")) + list_pnl:Add(fadecolor) + list_pnl:Add(Label("Text color")) + list_pnl:Add(textcolor) + list_pnl:Add(invertcolor_btn) + list_pnl:Add(preview_refresh_btn) + list_pnl:Add(preview_pnl) + master_pnl:MakePopup() + + +end + +concommand.Add("pac_popups_settings", function() pace.OpenPopupConfig() end) + +--[[ + info_string, main string + { info about where to position the label + pac_part = part, that would be the pac part if applicable + obj = self.Label, that would be the positioning target + obj_type = "pac tree label", what type of thing is the target, for positioning + pac tree label = on the editor, needs to realign when scrolling + part world = if base_movable, place it next to the part in the view, if not, owner entity + screen = static x,y on screen no matter what, needs the further x,y args specified outside + cursor = right on the cursor + editor bar = next to the toolbar + hoverfunc = function() end, a function to run when hovering. + doclickfunc = function() end, a function to run when clicking + panel_exp_width = 900, panel_exp_height = 200 prescribed dimensions to expand to + }, + self:LocalToScreen() x,y +]] + + + +--[[ +we generally have two routes to create a popup: part and direct +at the part level we can tell pac to try to create a popup +pac.AttachInfoPopupToPart(part : obj, string : str, table : tbl) --naming scheme close to a general pac function + PART:AttachEditorPopup(string : str, bool : flash, table : tbl) --calls the generic base setup in base_part, shouldn't be overridden + PART:SetupEditorPopup(str, force_open, tbl) --calls the specific setup, can be overridden for different classes + pac.InfoPopup(str, tbl, x, y) --creates the vgui element + +we can directly create an independent editor popup +pac.InfoPopup(str, tbl, x, y) +]] + + +function pac.InfoPopup(str, tbl, x, y) + if not GetConVar("pac_popups_enable"):GetBool() then return end + local x = x + local y = y + if not x or not y then + x = ScrW()/2 + math.Rand(-300,300) + y = ScrH()/2 + math.Rand(-300,0) + end + tbl = tbl or {} + if not tbl.obj then + if tbl.obj_type == "pac tree label" then + tbl.obj = tbl.pac_part.pace_tree_node + elseif tbl.obj_type == "part world" then + tbl.obj = tbl.pac_part + end + end + + str = str or "" + local verbosity = GetConVar("pac_popups_verbosity"):GetString() + + local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + local a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + + local pnl = vgui.Create("DFrame") + local txt_zone = vgui.Create("RichText", pnl) + + --function pnl:PerformLayout() end + pnl:SetTitle("") pnl:SetText("") pnl:ShowCloseButton( false ) + txt_zone:SetPos(5,25) + txt_zone:SetContentAlignment( 7 ) --top left + + if tbl.pac_part then + if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + if pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName] then + str = str .. "\n\n" .. pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName].popup_tutorial .. "\n" + end + end + end + + + pnl.hoverfunc = function() end + pnl.doclickfunc = function() end + pnl.titletext = "Click for more information! (or F1)" + pnl.alternativetitle = "Right click / Alt+P to kill popups. \"pac_popups_preserve_on_autofade\" is set to " .. GetConVar("pac_popups_preserve_on_autofade"):GetInt() .. ", " .. (GetConVar("pac_popups_preserve_on_autofade"):GetBool() and "If it fades away, the popup is allowed to reappear on hover or F1" or "If it fades away, the popup will not reappear") + + --pnl:SetPos(ScrW()/2 + math.Rand(-100,100), ScrH()/2 + math.Rand(-100,100)) + + function pnl:FixPartReference(tbl) + if not tbl or table.IsEmpty(tbl) then self:Remove() end + if tbl.pac_part then tbl.obj = tbl.pac_part.pace_tree_node end + + end + + function pnl:MoveToObj(tbl) + --self:MakePopup() + if tbl.obj_type == "pac tree label" then + if not IsValid(tbl.obj) then + self:FixPartReference(tbl) + self:SetPos(x,y) + else + local x,y = tbl.obj:LocalToScreen() + x = pace.Editor:GetWide() + --print(pace.Editor:GetWide(), input.GetCursorPos()) + self:SetPos(x,y) + end + if pace then + if pace.Editor then + if pace.Editor.IsLeft then + if not pace.Editor:IsLeft() then + self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) + else + self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) + end + end + end + end + + elseif tbl.obj_type == "part world" then + if tbl.pac_part then + local global_position = tbl.pac_part:GetRootPart():GetOwner():GetPos() + tbl.pac_part:GetRootPart():GetOwner():OBBCenter()*1.5 + if tbl.pac_part.GetWorldPosition then + global_position = tbl.pac_part:GetWorldPosition() --if part is a base_movable, we'll get its position right away + elseif tbl.pac_part:GetParent().GetWorldPosition then + global_position = tbl.pac_part:GetParent():GetWorldPosition() --if part isn't but has a base_movable parent, get that + end + local scr_tbl = global_position:ToScreen() + self:SetPos(scr_tbl.x, scr_tbl.y) + end + + elseif tbl.obj_type == "screen" then + self:SetPos(x,y) + + elseif tbl.obj_type == "cursor" then + self:SetPos(input.GetCursorPos()) + + elseif tbl.obj_type == "editor bar" then + if not pace.Editor:IsLeft() then + self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) + else + self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) + end + end + + end + + + if tbl then + pnl.tbl = tbl + pnl:MoveToObj(tbl) + if tbl.hoverfunc then + if tbl.hoverfunc == "open" then + pnl.hoverfunc = function() + pnl.hovering = true + pnl:keep_alive(3) + if not pnl.hovering and not pnl.expand then + pnl.resizing = true + pnl.expand = true + + pnl.ResizeStartTime = CurTime() + pnl.ResizeEndTime = CurTime() + 0.3 + end + end + else + pnl.hoverfunc = tbl.hoverfunc + end + end + pnl.exp_height = tbl.panel_exp_height or 400 + pnl.exp_width = tbl.panel_exp_width or 800 + end + + pnl.exp_height = pnl.exp_height or 400 + pnl.exp_width = pnl.exp_width or 800 + pnl:SetSize(200,20) + + pnl.DeathTimeAdd = 0 + if GetConVar("pac_popups_preserve_on_autofade"):GetBool() then + pnl.DeathTimeAdd = 240 + end + pnl.DeathTime = CurTime() + 13 + pnl.FadeTime = CurTime() + 10 + pnl.FadeDuration = pnl.DeathTime - pnl.FadeTime + pnl.ResizeEndTime = 0 + pnl.ResizeStartTime = 0 + pnl.resizing = false + + function pnl:keep_alive(extra_time) + pnl.DeathTime = math.max(pnl.DeathTime, CurTime() + extra_time + pnl.FadeDuration) + pnl.FadeTime = math.max(pnl.FadeTime, CurTime() + extra_time) + pnl:SetAlpha(255) + end + + --the header needs a label to click on to open the popup + function pnl:DoClick() + + if input.IsKeyDown(KEY_F1) or (self:IsHovered() and not txt_zone:IsHovered()) then + pnl.expand = not pnl.expand + pnl.ResizeStartTime = CurTime() + pnl.ResizeEndTime = CurTime() + 0.3 + pnl.resizing = true + end + + pnl:keep_alive(3) + pnl.doclickfunc() + end + + --handle positioning, expanding and termination + function pnl:Think() + self:MoveToObj(tbl) + if input.IsButtonDown(KEY_P) and input.IsButtonDown(KEY_LALT) then --auto-kill if alt-p + tbl.pac_part.killpopup = true + self:Remove() + end + + if input.IsMouseDown(MOUSE_RIGHT) then + if self:IsHovered() and not txt_zone:IsHovered() then + self:Remove() + end + end + + self.F1_doclick_possible_at = self.F1_doclick_possible_at or 0 + self.mouse_doclick_possible_at = self.mouse_doclick_possible_at or 0 + + if input.IsButtonDown(KEY_F1) then --expand if press F1, but only after a delay + if self.F1_doclick_possible_at == 0 then + self.F1_doclick_possible_at = CurTime() + 0.3 + end + if CurTime() > self.F1_doclick_possible_at then + self.F1_doclick_possible_at = 0 + self:DoClick() + end + end + if input.IsMouseDown(MOUSE_LEFT) and self:IsHovered() or self:IsChildHovered() then --expand if press mouse left + if self.mouse_doclick_possible_at == 0 then + self.mouse_doclick_possible_at = CurTime() + 1 + end + if CurTime() > self.mouse_doclick_possible_at then + self.mouse_doclick_possible_at = 0 + self:DoClick() + end + end + if not input.IsMouseDown(MOUSE_LEFT) then + self.mouse_doclick_possible_at = CurTime() + end + + if not IsValid(tbl.pac_part) and tbl.pac_part ~= false then self:Remove() end + self.exp_width = self.exp_width or 800 + self.exp_height = self.exp_height or 500 + --resizing code, initially the label should start small + if self.resizing then + local expand_frac_w = math.Clamp((self.ResizeEndTime - CurTime()) / 0.3,0,1) + local expand_frac_h = math.Clamp((self.ResizeEndTime - (CurTime() - 0.5)) / 0.5,0,1) + local width,height + if not self.expand then + width = 200 + (self.exp_width - 200)*(expand_frac_w) + height = 20 + (self.exp_height - 20)*(expand_frac_h) + if self.hovering and not self:IsHovered() then self.hovering = false end + else + width = 200 + (self.exp_width - 200)*(1 - expand_frac_h) + height = 20 + (self.exp_height - 20)*(1 - expand_frac_w) + + end + self:SetSize(width,height) + txt_zone:SetSize(width-10,height-30) + end + + + self.fade_factor = math.Clamp((self.DeathTime - CurTime()) / self.FadeDuration,0,1) + self.fade_factor = math.pow(self.fade_factor, 3) + + if CurTime() > self.DeathTime + self.DeathTimeAdd then + self:Remove() + end + if pace.Focused then self:SetAlpha(255*self.fade_factor) end + if self:IsHovered() then + self:keep_alive(1) + self.hoverfunc() + if input.IsMouseDown(MOUSE_RIGHT) then + if tbl.pac_part then + tbl.pac_part.killpopup = true + end + self:Remove() + end + + end + + if not pace.Focused then + self:AlphaTo(0, 0.1, 0) + self:KillFocus() + self:SetMouseInputEnabled(false) + self:SetKeyBoardInputEnabled(false) + gui.EnableScreenClicker(false) + end + + function pnl:OnRemove() + if not GetConVar("pac_popups_preserve_on_autofade"):GetBool() then + tbl.pac_part.killpopup = true + end + end + end + + pnl.doclickfunc = tbl.doclickfunc or function() end + + + + pnl.exp_height = tbl.panel_exp_height + pnl.exp_width = tbl.panel_exp_width + + --cast the convars values + r1 = tonumber(r1) + g1 = tonumber(g1) + b1 = tonumber(b1) + a1 = tonumber(a1) + r2 = tonumber(r2) + g2 = tonumber(g2) + b2 = tonumber(b2) + a2 = tonumber(a2) + r3 = tonumber(r3) + g3 = tonumber(g3) + b3 = tonumber(b3) + + local col = Color(r3,g3,b3,255) + + --txt_zone:SetFont("DermaDefaultBold") + function txt_zone:PerformLayout() + txt_zone:SetBGColor(0,0,0,0) + txt_zone:SetFGColor(col) + end + + function txt_zone:Think() + if self:IsHovered() then + pnl:keep_alive(3) + end + end + + txt_zone:SetText("") + txt_zone:AppendText(str) + + txt_zone:SetVerticalScrollbarEnabled(true) + + function pnl:Paint( w, h ) + + + self.fade_factor = self.fade_factor or 1 + --base layer + local sine = 0.5 + 0.5*math.sin(CurTime()*2) + draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) ) + --draw.RoundedBox( 0, 0, 0, 1, h, Color( 88, 179, 255, 255)) + for band=0,w,1 do + --per-pixel fade + fade = 1 - (1/w * band * self.fade_factor) + fade2 = math.pow(fade,3) + fade = math.pow(fade,2) + --draw.RoundedBox( c, x, y, w, h, color ) + draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) + --draw.RoundedBox( 0, band, 0, 1, 1, Color( 88, 179, 255, 255)) + --draw.RoundedBox( 0, band, h-1, 1, 1, Color( 0, 0, 0, 255)) + end + + if self.expand then + draw.DrawText(self.alternativetitle, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) + else + draw.DrawText(self.titletext, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) + end + end + + pnl:MakePopup() + return pnl +end + +function pac.AttachInfoPopupToPart(obj, str, tbl) + if not obj then return end + obj:AttachEditorPopup(str, true, tbl) +end + +function pace.FlushInfoPopups() + for _,part in pairs(pac.GetLocalParts()) do + local node = part.pace_tree_node + if not node or not node:IsValid() then continue end + if node.popupinfopnl then + node.popupinfopnl:Remove() + node.popupinfopnl = nil + end + end + +end + +--[[ + part classes info + +ideally we should have: +1-a tooltip form (7 words max) + e.g. projectile: throws missiles into the world +2-a fuller form for the popups (4-5 sentences or more if needed) + e.g. projectile: the projectile part creates physical entities and launches them forward.\n + the entity has physics but it can be clientside (visual) or serverside (physical)\n + by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n + the entity can do damage but servers can restrict that. + +but then again we should probably look for better ways for the full-length explanations, + maybe grab some of them from the wiki or have a web browser for the wiki +]] + +do + + pace.TUTORIALS = pace.TUTORIALS or {} + pace.TUTORIALS.PartInfos = { + + ["trail"] = { + tooltip = "leaves a trail behind", + popup_tutorial = + "the trail part creates beams along its path to make a trail\n".. + "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. + "you can set how it looks, how big it becomes etc." + }, + + ["trail2"] = { + tooltip = "leaves a trail behind", + popup_tutorial = + "the trail part creates beams along its path to make a trail\n".. + "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. + "you can set how it looks, how big it becomes etc." + }, + + ["sound"] = { + tooltip = "plays sounds", + popup_tutorial = "plays sounds in wav, mp3, ogg formats.\n".. + "for random sounds, paste each path separated by semicolons e.g. sound1.wav;sound3.wav;sound8.wav\n".. + "we have a special bracket notation for sound lists: sound[1,50].wav\n\n".. + "some of the parameters to know:\n".. + "sound level affects the falloff along with volume; a good starting point is 70 level, 0.6 volume\n".. + "overlapping means it doesn't get cut off if hidden\n".. + "sequential plays sounds in a list in order once you have the semicolon or bracket notation;\n".. + "\tthe steps is how much you progress by each activation. it can go one by one (1), every other sound (2+), stay there (0) or go back (negative values)" + }, + + ["sound2"] = { + tooltip = "plays web sounds", + popup_tutorial = "plays sounds in wav, mp3, ogg formats, with the option to download sound files from the internet\n".. + "people usually use dropbox, google drive, other cloud hosts or their own server host to store and distribute their files. each has its limitations.\n\n".. + "WARNING! Downloading and using these sounds is only possible in the chromium branch of garry's mod!\n\n".. + "to randomize sounds, we still have the same notations as legacy sound:\n".. + "\tsemicolon notation e.g. path1.wav;https://url1.wav;https://url2.wav\n".. + "\tbracket notation e.g. sound[1,50].wav\n\n".. + "some of the parameters to know, you'll already know some of them from legacy sound:\n".. + "-radius affects the falloff distance\n".. + "-overlapping means it doesn't get cut off if hidden\n".. + "-sequential plays sounds in a list in order" + }, + + ["ogg"] = { + tooltip = "plays ogg sounds (broken)", + popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." + }, + + ["webaudio"] = { + tooltip = "plays web sounds (legacy)", + popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." + }, + + + ["halo"] = { + tooltip = "makes models glow", + popup_tutorial = + "This part creates a halo around a model entity.\n".. + "That could be your playermodel or a pac3 model, but for some reason it doesn't work on your player if you have an entity part.\n".. + "passes is the thickness of the halo, amount is the brightness, blur x and y spread out the shape" + }, + + ["bodygroup"] = { + tooltip = "changes body parts on supported models", + popup_tutorial = + "Bodygroups are a Source engine model feature which allows to easily show or hide different pieces of a model\n".. + "those are often used for accessories and styles. But it won't work unless your model has bodygroups.\n".. + "this part does exactly that. but you might do that directly with the model part or entity part" + }, + + ["holdtype"] = { + tooltip = "changes your animation set", + popup_tutorial = + "this part allows you to change animations played in individual movement slots, so you can mix and match from the available animations in your playermodel\n".. + "a holdtype is a set of animations for holding one kind of weapon, such as one-handed pistols vs two-handed revolvers, rifles, melee weapons etc.\n".. + "The option is also in the normal animation part, but this part goes in more detail in choosing different animations" + }, + + ["clip"] = { + tooltip = "cuts a model in a plane (legacy)", + popup_tutorial = + "This part cuts off one side of the model in rendering.\n".. + "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." + }, + + ["clip2"] = { + tooltip = "cuts a model in a plane", + popup_tutorial = + "This part cuts off one side of the model in rendering.\n".. + "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." + }, + + ["model"] = { + tooltip = "places a model (legacy)", + popup_tutorial = "The old model part still does the basic things you need a model to do" + }, + + ["model2"] = { + tooltip = "places a model", + popup_tutorial = + "The model part creates a clientside entity to draw a model locally.\n".. + "Being a base_movable, parts inside it will be physically arented to it.\n".. + "therefore, it can act as a regrouper, rail or anchoring point for your pac structuring needs, although you probably shouldn't abuse it.\n".. + "It can accept most modifiers and play animations, if present or referenced in the model.\n\n".. + "It can load MDL zips or OBJ files from a direct link to a server host or cloud provider, allowing you to use pretty much any model as long as it's the right format for Source. And on that subject, you would do well to install Crowbar, as well as Blender with Blender Source Tools, if you want to extract and edit models. Consult the valve developer community wiki for more information about QC; I view this as common knowledge rather than the purview of pac3 so you have to do some research." + }, + + ["material"] = { + tooltip = "defines a material (legacy)", + popup_tutorial = + "the old material still works as it says. it lets you define some VMT parameters for a material" + }, + + ["material_3d"] = { + tooltip = "defines a material for models", + popup_tutorial = + "This part creates a VMT material of the shader type VertexLitGeneric.\n".. + "If you have experience in Source engine things, you probably should know what some of these do, I won't expound fully but here's the essential summary anyway:\n\n".. + "\tbase texture is the base image. It's basically just color pixels.\n".. + "\tbump map / normal map is a relief that gives a texture on the surface. It uses a distinctly purple pixel format; it's not color but directional information\n".. + "\tdetail is a second base image added on top to modify the pixels. It's usually grayscale because we don't need to add color to give more grit to an image\n".. + "\tself illumination and emissive blend are glowing layers. emissive blend is more complex and needs three necessary components before it starts to work properly.\n".. + "\tenvironment map is a layer for pre-baked room reflection, by default env_cubemap tries to get the nearest cubemap but you can choose another texture, although cubemaps are a very specific format\n".. + "\tphong is a layer of dynamic light reflections\n\n".. + "If you want to edit a material, you can load its VMT with a right click on \"load vmt\", then select the right material override\n".. + "Reminder that transparent textures may need additive or some form of translucent setting on the model and on the material.\n\n".. + "For more information, search the Valve developer community site or elsewhere. Many material features are standard, and if you want to push this part to the limit, the extra research will be worth it." + }, + + ["material_2d"] = { + tooltip = "defines a material for sprites", + popup_tutorial = + "This part creates a VMT material of the shader type UnlitGeneric. This is used by particles and sprites.\n".. + "For transparent textures, use additive or vertex alpha/vertex color (for particles and decals). Some VTF or PNG textures have an alpha channel, but many just have a black background meant for additive rendering.\n\n".. + "For more information, search the Valve developer community site" + }, + + ["material_refract"] = { + tooltip = "defines a refracting material", + popup_tutorial = + "This part creates a VMT material of the shader type Refract. As with other material parts, you would find it useful to name the material to use that in multiple models' \"material\" fields\n".. + "In a way, it doesn't work by surface, but by silhouette. But the surface does determine how the refraction occurs. Setting a base texture creates a flat wall behind it that can distort in interesting ways but it'll replace the view behind.\n".. + "The normal section does most of the heavy lifting. This is where the image behind the material gets refracted according to the surface. You can blend between two normal maps in greater detail.\n".. + "Your model needs to be set to \"translucent\" rendering mode for this to work because the shader is in a multi-step rendering process.\n\n".. + "For more information, search the Valve developer community site" + }, + + ["material_eye refract"] = { + tooltip = "defines a refracting eye material", + popup_tutorial = + "This part creates a VMT material of the shader type EyeRefract.\n".. + "It's tricky to use because of how it involves projections and entity eye position, but you can more easily get something working on premade HL2 or other Source games' characters with QC eyes." + }, + + ["submaterial"] = { + tooltip = "applies a material on a submaterial zone", + popup_tutorial = + "Models can be comprised of multiple materials in different areas. This part can replace the material applied to one of these zones.\n".. + "Depending on how the model was made, it might correspond to what you want, or it might not.\n".. + "As usual, as with other model modifiers your expectations should always line up with the quality of the model you're using." + }, + + ["bone"] = { + tooltip = "changes a bone (legacy)", + popup_tutorial = + "The legacy bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." + }, + + ["bone2"] = { + tooltip = "changes a bone (legacy)", + popup_tutorial = + "The legacy experimental bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." + }, + + ["bone3"] = { + tooltip = "changes a bone", + popup_tutorial = + "This part modifies a model's bone. It can move relative to the parent bone, scale, and rotate.\n".. + "Follow part forces the bone to relocate to a base_movable part. Might have issues if you successively follow part multiple related bones. You could try to fix that by changing draw orders of the follow parts and bones." + }, + + ["player_config"] = { + tooltip = "sets your player entity's behaviour", + popup_tutorial = + "This part has access to some of your player's behavior, like whether you will play footsteps, the chat animation etc.\n".. + "Some of these may or may not work as intended..." + }, + + ["light"] = { + tooltip = "lights up the world (legacy)", + popup_tutorial = + "This legacy part still does the basic thing you want from a light, but the new light part is more fully-featured, for the most part.\n".. + "There is one thing it does that the new part doesn't, and that's styles." + }, + + ["light2"] = { + tooltip = "lights up models or the world", + popup_tutorial = + "This part creates a dynamic light that can illuminate models or the world independently.\n".. + "There are some options for the light's falloff shape (inner and outer angles).\n".. + "Its brightness works by magnitude and size, not multiplication. Which means you can still have light at 0 or lower brightness." + }, + + ["event"] = { + tooltip = "activates or deactivates other parts", + popup_tutorial = + "This part hides or shows connected parts when certain conditions are met. We won't describe them in this tutorial, you'll have to read them individually. The essential behaviour remains common accross events.\n\n".. + "Domain, in other words, which parts get affected:\n".. + "\t1-Default: The event will command its direct parent. Because parts can contain other parts, this includes the event itself, and parts beside the event too. While this is not usually a problem, you have to be aware of that.\n".. + "\t2-ACO: Affect Children Only. The event will command parts inside it, not beside, not above. This is the first step to isolate your setup and have clean logic in your pac.\n".. + "\t3-Targeted: The event gets wired to a part directly, including its children of course. This is accessed when you select a part in the \"targeted part\" field, which has an unfortunate name because there's still the old \"target part\" parameter\n\n".. + "Some events, like is_touching, can select an external \"target\" to use as a point to gather information.\n\n".. + "Operators:\n".. + "Operators are just how the event asks the question to determine when to activate or deactivate. Just read the event the same way as it asks the question: is my source equal to the value? can I find this text in my source?\n".. + "\tnumber-related operators: equal, above, below, equal or above, equal or below\n".. + "\tstring-related operators: equal, find, find simple\n".. + "There's still a caveat. If you use the wrong type of operator for your event, it will NOT work. Please trust the editor autopilot when it automatically changes your operator to a good one. Do not change it unless you know what you're doing." + }, + + ["sprite"] = { + tooltip = "draws a 2D texture", + popup_tutorial = + "Sprites are another Source engine thing which are useful for some point effects. Most textures being for model surfaces will look like squares if drawn flat, but sprite and particle textures are made specially for this purpose.\n".. + "They should have a transparent background or black background. The difference is because of rendering modes or blend modes.\n".. + "Additive rendering adds pixels' values. So, bright pixels will be more visible, but dark pixels end up being faded or invisible because their amounts are low." + }, + + ["fog"] = { + tooltip = "colors a model with fog", + popup_tutorial = + "This strange modifier renders a fog-like color over a model. Not in the world, not inside the model, but over its surface.\n".. + "For that reason, you might do well to change rendering-related values like blend mode on the host model's side\n".. + "It requires to be attached to a base_drawable part, keep in mind the start and end values are multiplied by 100 in post for some reason.".. + "start is the distance where the fog starts to appear outside, end is where the fog is thickest." + }, + + ["force"] = { + tooltip = "provides physical force", + popup_tutorial = + "This part tries to tell the server to do a force impulse, or continually request small impulses for a continuous force. It should work for most physics props, some item and ammo entities, players and NPCs. But it may or may not be allowed on the server due to server settings: pac_sv_force.\n\n".. + "There's a base force and an added xyz vector force. You have options to choose how they're applied. Aside from that, the part's area is mainly for detection.\n\n".. + "For the Base force, Radial is from to self to each entity, Locus is from locus to each entity, Local is forward of self\n\n".. + "For the Vector force, Global is on world coordinates, Local is on self's coordinates, Radial is relative to the line from the self or locus toward the entity (Used in orbits/vortex/circular motion with centrifugal force)\n\n".. + "NPCs might have weird movement so don't expect much from pushing them." + }, + + ["faceposer"] = { + tooltip = "Adjusts multiple facial expression slots", + popup_tutorial = + "This part gives access to multiple facial expressions defined by your model's shape keys in one part.\n".. + "The flex multiplier affects the whole model, so you should avoid stacking faceposers if they have different multipliers." + }, + + ["command"] = { + tooltip = "Runs a console command or lua code", + popup_tutorial = "This part attempts to run a command or Lua code on your client. It may or may not work depending on the command and some servers don't allow you to run clientside lua, because of sv_allowcslua 0.\n\n".. + "Some example lua bits:\n".. + "\tif LocalPlayer():Health() > 0 then print(\"I'm alive\") RunConsoleCommand(\"say\", \"I\'m alive\") end\n".. + "\tfor i=0,100,1 do print(\"number\" .. i) end\n".. + "\tfor _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end\n".. + "\tlocal random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)" + + }, + + ["weapon"] = { + tooltip = "configures your weapon entity", + popup_tutorial = "This part is like an entity part, but for weapons. It can change your weapon's position and appearance, for all or one weapon class." + }, + + ["woohoo"] = { + tooltip = "applies a censor square", + popup_tutorial = + "This part draws a pixelated square with what's behind it, with a possible blur filter and adjustable resolution.\n".. + "It requires a lot of resources to set up and needs to refresh in specific circumstances, which is why you can't change its resolution or blur filtering state with proxies." + }, + + ["flex"] = { + tooltip = "Adjusts one facial expression slot", + popup_tutorial = + "This part gives access to one facial expression defined by your model's shape keys." + }, + + ["particles"] = { + tooltip = "Emits particles", + popup_tutorial = + "Throws particles into the world. They are quite configurable, can be flat 3D or 2D sprites, can be stretched with start/end length.\n".. + "To start with, you may want to set zero angle to false and particle angle velocity to (0, 0, 0)\n".. + "You can use a web texture but you might still need to work around material limitations for transparent images\n".. + "They are not PCF effects though. But I think that with a wise choice and layered particles, you can recreate something that looks like an effect." + }, + + ["custom_animation"] = { + tooltip = "sets up an editable bone animation", + popup_tutorial = + "This part creates a custom animation with a separate editor menu. It is not a sequence, but it moves bones on top of your base animations. It morphs between keyframes which correspond to bones' positions and angles. This is what creates movement.\n\n".. + "Custom animation types:\n".. + "\tsequence: loopable. plays the A-pose animation as a base, layers bone movements on top.".. + "\tstance: loopable. layers bone movements on top.".. + "\tgesture: not loopable. layers bone movements on top. ideally you should start with duplicating your initial frame once for smoothly going back to 0.".. + "\tposture: only applies one non-moving frame. this is like a set of bones.\n\n".. + "There are interesting Easing styles available when you select the linear interpolation mode. They're useful in many ways, if you want to have more control over the dynamics and ultimately give character to your animation.\n".. + "While this is not the place to write a full tutorial for how to animate, or explaining animation principles in depth, I editorialize a bit and say those are two I try to aim for:\n".. + "\tinertia: trying to carry some movement over from a previous frame, because real physics take time to decelerate and accelerate between positions.\n".. + "\texaggeration: animations often use unnatural movement dynamics (e.g. different speeds at different times) to make movements look more pleasing by giving it more character. This goes in hand with anticipation." + }, + + ["beam"] = { + tooltip = "draws a rope or beam", + popup_tutorial = + "This part renders a rope or beam between itself and the end point. It can bend relative to the two endpoints' angles.\n".. + "frequency determines how many half-cycles it goes through. 1 is half a cycle (1 bump), 2 is one cycle(2 bumps)\n".. + "resolution is how many segments it tries to draw for that.\n\n".. + "And here's another reminder that while it can load url images, there are limitations so you may have to do something with a material part or blend mode if you want a custom transparent texture." + }, + + ["animation"] = { + tooltip = "plays a sequence animation", + popup_tutorial = + "This part plays a sequence animation defined in your model via the model's inherent animation definitions, included animations and active addons. Cannot load custom animations, not even .ani, .mdl or .smd\n".. + "If you want to import an animation from somewhere else, you need to know some decompiling/recompiling QC knowledge" + }, + + ["player_movement"] = { + tooltip = "edits your player movement", + popup_tutorial = "This part tells the server to handle your movement manually with a Move hook.\n".. + "Z-Velocity means you can move in the air relative to your eye angles, with WASD and jump almost like noclip. It is however still subject to air friction (needs friction to move, but friction also decelerates you) and uses ground friction as a driver.\n".. + "Friction generally cuts your movement as a percentage every tick. This is why it's very sensitive because its effect is exponential. Horizontal air friction tries to mitigate that a bit\n".. + "Reverse pitch is probably buggy. " + }, + + ["group"] = { + tooltip = "organizes parts", + popup_tutorial = + "This part groups parts. That's all it does. It bypasses parenting, which means it has no side effect, aside from when modifiers act on their direct parent, in which case the group can get in the way.\n".. + "But with a root group, (a group at the root/top level, \"my outfit\"), you can choose an owner name to select another entity to bear the pac outfit." + }, + + ["lock"] = { + tooltip = "grabs or teleports", + popup_tutorial = + "This part allows you to grab things or teleport yourself.\n\n".. + "Warning in advance: It has the most barriers because it probably has the most potential for abuse out of all parts.\n".. + "\tClients need to give consent explicitly (pac_client_grab_consent 1), otherwise you can't grab them.\n".. + "\tThis is doubly true for players' view position. That's another consent (pac_client_lock_camera_consent 1) layered on top of the existing grab consent.\n".. + "\tOn top of that, grabbed players will get a notification if you grab them, and they will know how to break the lock. Clients have multiple commands (pac_break_lock, pac_stop_lock) to request the server to force you to release them. It is mildly punitive.\n".. + "\tThere are multiple server-level settings to limit it. Some servers may even wholesale disable the new combat parts for all players by default until they're trusted/whitelisted.\n\n".. + "Now, here's business. How it works, and how to use this part:\n".. + "\tThe part searches for entities around a sphere, selects the closest one and locks onto it. You should plan ahead for the fact that it only picks up entities by their origin position, which for NPCs and players is between their feet. offset down amount compensates for this, but only for where the detection radius begins.\n".. + "\tIt will then start communicating with the server and the server may reposition the entity if it's allowed. If rejected, you may get a warning in the console, and the part will be stopped for a while.".. + "\tOverrideEyeAngles works for players only, and as stated previously, is subject to consent restrictions.\n" + }, + + ["physics"] = { + tooltip = "creates a clientside physics object", + popup_tutorial = + "This part creates a physics object clientside which can be a box or a sphere. It will relocate the model and pac parts contained and put them in the object.\n".. + "It's not compatible with the force part, unfortunately, because it's clientside. There are other reasonably fun things it can do though.\n".. + "It only works as a direct modifier on a model." + }, + + ["jiggle"] = { + tooltip = "wobbles around", + popup_tutorial = + "This part creates a subpoint that carries base_movables, and moves around with a certain type of dynamics that can lag behind and then catch up, or wiggle back and forth for a while. Strain is how much it will wobble. The children parts will be contained within that subpoint.\n".. + "There is immense utility to control movement and have some physicality to your parts' movement. To name a few examples:\n".. + "\tThe jiggle 0 speed trick: Having your jiggle set at 0 speed will freeze what's inside. You can easily control that with two proxies: one for moving (not 0), one for stopping (0)\n".. + "\tPets and drones: Fun things that are semi-independent. Easy to do with jiggle.\n".. + "\tSmoother transitions with multiple static proxies: If you have position proxies that snap to different positions, making a model teleport too fast, using these proxies on a jiggle instead will let the jiggle do the work of smoothing things out with the movement.\n".. + "\tForward velocity indicator via a counter-lagger: jiggle lags behind an origin, model points to origin with aim part, other model is forward relative to the pointer. Result: a model that goes in the direction of your movement.\n\n".. + "The part, however, has issues when crossing certain angles (up and down)." + }, + + ["projected_texture"] = { + tooltip = "creates a lamp", + popup_tutorial = + "This part creates a dynamic light / projected texture that can project onto models or the world. That's pretty much it. It's useful for lamps, flashlights and the like.\n".. + "But if you're expecting a proper light, its directed lighting method gives mediocre results alone. With another light, and a sprite maybe, it'll look nicer. We won't have point_spotlights though.\n".. + "Its brightness works by multiplication, not magnitude. 0 is a proper 0 amount.\n\n".. + "Because it uses ITexture / VTF, it doesn't link up with pac materials. Animated textures must be done by frames instead of proxies. Although you can still set a custom image. But it's additive so the transparency can't be done with alpha, but on a black background\t".. + "fov on one hand, and horizontal/vertical fovs on the other hand, compete; so you should touch only one and leave the other." + }, + + ["hitscan"] = { + tooltip = "fires bullets", + popup_tutorial = + "This part tries to fire bullets. There are damaging serverside bullets and non-damaging clientside bullets. Both could be useful in their own scenarios.\n".. + "For serverside bullets, the server might restrict that. For example, it can force you to spread your damage among all your bullets, to notably prevent you from stacking tons of bullets to multiply your damage beyond the limit.\n".. + "Damage falloff works with a fractional floor on individual bullets, which means each bullet is lowered to a percentage of its max damage." + }, + + ["motion_blur"] = { + tooltip = "makes a trail of after-images", + popup_tutorial = + "This part continually renders a series of ghost copies of your model along its path to simulate a motion blur-like effect.\n".. + "It has limited options because of how models' clientside entity is set up, allegedly." + }, + + ["link"] = { + tooltip = "transfers variables between parts", + popup_tutorial = + "This part tries to copy variables between two parts and update them when their values change.\n".. + "It doesn't work for all variables especially booleans! Also, \"link\" is a strong word. Whatever you think it means, it's not doing that.".. + "Might require a rewear to work properly." + }, + + ["effect"] = { + tooltip = "runs a PCF effect", + popup_tutorial = + "This part uses an existing PCF effect on your game installation, from your mounted games or addons. No importable PCFs from the web.\n".. + "It apparently can use control points and make tracers work. It may or may not be supported by different effects; start by putting the effect in a basic model to position the effect.\n".. + "And PCF effects can be a gigantic pain, with for example looping issues, permanence issues (BEWARE OF TF2 UNUSUAL EFFECTS!), wrong positions etc.".. + "You should probably look into particles and think about how to layer them if you're looking for something more configurable." + }, + + ["text"] = { + tooltip = "draws 3D2D or 2D text", + popup_tutorial = + "This part renders text on a flat surface (3D2D with the DrawTextOutlined mode) or on the screen (2D with the SurfaceText mode). Due to technical limitations, some features in one may not be present in the other, such as the outline and the size scaling\n\n".. + "You can use a combination of data and text to build up your text. Combined text tells the part to use both, and text position tells you whether the text is before or after the data.\n".. + "What's this data? text override. There are a handful of presets, like your health, name, position. If you want more control, you can use Proxy, and it will use the dynamic text value (a simple number variable) which you can control with proxies.\n\n".. + "If you want to raise the resolution of the text, you should try making a bigger font. But creating fonts is expensive so it's throttled. You can only make one every 3 seconds.\n".. + "Although you can use any of gmod's or try to use your operating system's font names, there are still limits to fonts' features, both in their definitions and in the lua code. Not everything will work. But it will create a unique ID for the font it creates, and you can reuse that font in other text parts." + }, + + ["camera"] = { + tooltip = "changes your view", + popup_tutorial = + "This part runs a CalcView hook to allow you to go into a third person mode and change your view accordingly. Some parts on your player may get in the way.\n".. + "eye angle lerp determines how much you mix the original eye angles into the view. Otherwise at 0 it will fully use the part's local angles.\n".. + "Remember a right hand rule! Point forward, thumb up, middle finger perpendicular to the palm. This is how the camera will look with 0 lerp.\n".. + "\tX = Red = index finger = forward\n".. + "\tY = Green= middle finger = left\n".. + "\tZ = Blue = thumb finger = up\n".. + "As an example, if you apply this, you will learn that, on the head, you can simply take a 0,-90,-90 angle value and be done with it.\n\n".. + "Because of how pac3 works, you should be careful when toggling between cameras. I've made some fixes to prevent part of that, but if you hide with events, and lose your final camera, you can't come back unless you go back to third person (to restart the cameras) and then back into first person (to put priority back on an active camera)." + }, + + ["decal"] = { + tooltip = "applies decals", + popup_tutorial = + "Decals are a Source engine thing for bullet holes, sprays and other such flat details as manholes and posters. This part when shown emits one by tracing a line forward and applying it at the hit surface.\n".. + "It can use web images and pac materials, but still subject to rendering quirks depending on transparency and others.\n".. + "Decals are semi-permanent, you can only remove them with r_cleardecals" + }, + + ["projectile"] = { + tooltip = "throws missiles into the world", + popup_tutorial = + "the projectile part creates physical entities and launches them forward.\n".. + "the entity has physics but it can be clientside (visual) or serverside (physical)\n".. + "by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n".. + "the entity can do damage but servers can restrict that.\n".. + "For visual reference, a 1x1x1 cubeex is around radius 24, and a default pac sphere is around 8." + }, + + ["poseparameter"] = { + tooltip = "sets a pose parameter", + popup_tutorial = + "pose parameters are a Source engine thing that helps models animate using a \"blend sequence\". For instance, this is how the body and head are rotated, and how 8-way walks are blended.\n".. + "It goes without saying that not all models have those, and some have fewer supported pose parameters because of how they were made." + }, + + ["entity"] = { + tooltip = "edits an entity (legacy)", + popup_tutorial = "The legacy entity part still does the usual things you need to edit your entity. Color, model, size, no draw etc. But better use the new entity part." + }, + + ["entity2"] = { + tooltip = "edits an entity", + popup_tutorial = + "This part can edit some properties of an entity. This can be your playermodel, a pac3 model (it's a clientside entity) or a prop (you give a prop a pac outfit by selecting it with the owner name on a root group).\n".. + "It supports web models. See the model part's tutorial or read the MDL zips page on our wiki for further info.\n\n".. + "As with other bone-related things, it might not work properly if you use it on a ragdoll or some similar entities.\n\n".. + "Another warning in advance, if you wonder why your playermodel won't change, there are some addons, such as Enhanced Playermodel Selector, known to cause issues because they override your entity, thus conflicting with pac3. This one can be fixed if you disable \"enforce playermodel\"\n".. + "Other than that, the server setting pac_modifier_model and pac_modifier_size can forbid you from changing your playermodel and size respectively." + }, + + ["interpolated_multibone"] = { + tooltip = "morphs position between nodes", + popup_tutorial = + "A node-based path/morpher. This part allows you to move its contents by blending positions and angles between different points. Obviously enough, the nodes you select need to be base_movable parts.\n".. + "The first (Zeroth) node is the interpolated_multibone itself. From then on, the next node is reached when lerp reaches the corresponding number, and when you're at the end, i.e. an invalid or missing node, it morphs back to the origin.\n".. + "For example, 0.5 lerp will be halfway between the first node and the origin.\n".. + "While this part finally breaks through one of pac3's fundamental limitations (that of base_movables being limited to specific bones as anchoring points), there are still known issues, namely because of how angles are morphed. Roll angles might break.\n\n".. + "Suggested use cases: multi-position cutscene camera, returning hitpos pseudo-projectile, joints." + }, + + ["proxy"] = { + tooltip = "applies math to parts", + popup_tutorial = + "This part computes math and applies the numbers it gives to a parameter on a part, for number (x), vector (x,y,z) or boolean(true (1) or false (0)) types. It can send to the parent, to all its children, or to an external target part.\n".. + "Easy setup can help you make a rough idea quickly, but writing math yourself in the expression gives supremely superior control over what the math does.\n\n".. + "Here's a quick crash course in the syntax with basic examples showing the rules to observe:\n\n".. + "Basic numbers /math operators : 4^0.5 - 2*(0.2 / 5) + timeex()%4\n".. + "The only basic operators are: + - * / % ^\n".. + "Functions:\n".. + "\tFunctions are like variables that gather data from the world or that process math.\n".. + "\tMost functions are nullary, which means they have no argument: timeex(), time(), owner_health(), owner_armor_fraction()\n".. + "\tOthers have arguments, which can be required or optional: clamp(x,min,max), random(), random(min,max), random_once(seed,min,max), etc.\n".. + "\tAll Lua functions are declared by a set of parentheses containing arguments, possibly separated by commas.\n".. + "Arguments and tokens:\n".. + "\tMost arguments\' type is numbers, but some might be strings with some requirements; Most of the time it\'s a name or a part UID, for example:\n".. + "\tValid number arguments are numbers, functions or well-formed expressions. It\'s the same type because at the end of the day it gives you a number.\n".. + "\t\tNeedless to say, if you compose an expression, you need a coherent link between the tokens (i.e. math operators or functions). 2 + 2 is valid, 2 2 is not.\n".. + "\tValid string arguments are text declared by double quotes. Lua\'s string concatenation operator works. command(\"name\"..2) is the same as command(\"name2\")\n".. + "\t\tWithout the string declaration, Lua tries to look for a global variable. command(\"name\") is valid, command(name) is not.\n\n".. + "Nested functions (composition) : clamp(1 - timeex()^0.5,0,1)\n".. + "XYZ / Vectors (comma notation) : 100,0,50\n".. + "nil (skipping an axis) : 100,nil,0\n\n".. + "You can write pretty much any math using the existing functions as long as you observe the syntax\'s rules: the most common ones being to close your brackets properly, don't misspell your functions\' names and give them all their necessary arguments.\n\n".. + "There are lots of technical things to learn, but you can consult my example proxy bank by right clicking the expression field, and go consult our wiki for reference. https://wiki.pac3.info/part/proxy\n\n".. + "As a conclusion, I\'m gonna editorialize and give my recommendations:\n".. + "\t-Write with purpose. Avoid unnecessary math.\n".. + "\t\t->But still, write in a way that lets you understand the concept better.\n".. + "\t-More to the point, please have patience and deliberation. Make sure every piece works BEFORE moving on and making it more complex.\n".. + "\t-The fundamental mechanism of developing and applying new ideas is composition / compounding.\n".. + "\t\t->Multiplying different expression bits together tends to combine the concepts\n".. + "\t\t->e.g. clamp(0.2*timeex(),0,1)*sin(10*time()) is a fadein and a sine wave. What do you get? sine wave fading to full power.\n".. + "\t-Please read the debug messages in the console or in chat, they help the correction process if we make mistakes." + + }, + + ["sunbeams"] = { + tooltip = "shines like rays of light", + popup_tutorial = + "This part applies a sunbeam effect centered around the part.\n".. + "Multiplier is the strength of the effect. It can also be negative for engulfing darkness.\n".. + "Darken is how much to darken or brighten the whole effect. It helps control the contrast in conjunction with multiplier. With enough darken, only the brightest rays will go through, otherwise with unchecked multipliers or negative darken there's a whole blob of white that just overpowers everything\n".. + "Size affects the after-images projected around the center, which serve as a base for the effect." + }, + + ["shake"] = { + tooltip = "shakes nearby viewers\' camera", + popup_tutorial = + "This part applies a shake that uses the camera\'s position to take effect. For that reason, it may be nullified by certain third person addons, as well as the pac3 editor camera. You can still temporarily disable it to preview your shakes." + }, + + ["gesture"] = { + tooltip = "plays a gesture", + popup_tutorial = + "Gestures are a type of animation usually added as a layer on top of other animations, this part tries to play one but not all animations listed are gestures, so it might not work for most." + }, + + ["damage_zone"] = { + tooltip = "deals damage in a zone", + popup_tutorial = + "This part tries to deal hitbox damage via the server. It may or may not be allowed because of server settings (pac_sv_damage_zone) and client consents (pac_client_damage_zone_consent), etc. Server owners can add or remove entity classes that can be damaged with pac_damage_zone_blacklist_entity_class, pac_damage_zone_whitelist_entity_class commands.\n".. + "Among NPCs it should include VJ and DRG base NPCs, but only if they have npc_ or drg_ in their name\n".. + "Most shapes should be self-explanatory but you can use the preview function to see what it should cover. There are some settings for raycasts which could come in handy for some niche use cases even if the basic ones you'll use most of the time (box, sphere, cone from spheres, ray) will not really use these.".. + "There are certain special damage types. the dissolves can disintegrate entities but can be restricted in the server, prevent_physics_force suppresses the corpse force, removenoragdoll removes the corpse.\n".. + "" + }, + + ["health_modifier"] = { + tooltip = "modifies your health, armor", + popup_tutorial = + "This part allows you to quickly change your max health, max armor, damage multiplier taken, and has the possibility to give you extra health bars that absorb damage before the main health gets damaged.\n".. + "For the extra bars, you need to set a layer priority to pick which ones get damaged first. Outer ones are higher layer values. But they're still invisible for now... events and proxies will come later...\n".. + "The part's usage may or may not be allowed by the server." + } + + } + --print("we have defined the pace.TUTORIALS.PartInfos", pace.TUTORIALS.PartInfos) + + for i,v in pairs(pace.TUTORIALS.PartInfos) do + --print(i,v) + if pace.PartTemplates then + if pace.PartTemplates[i] then + pace.PartTemplates[i].TutorialInfo = v + end + end + end +end + +local motd_cvar = CreateConVar("pac_show_message_on_startup", "2", {FCVAR_ARCHIVE}, "Whether to show the update MOTD when you load in the game") + + +function pac.OpenMOTD(from_initial_startup) + local pnl = vgui.Create("DFrame") + pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) + + local url = "https://github.com/pingu7867/pac3#readme" + + function pnl:OnClose() + if not from_initial_startup then return end + local function exit_message() + if LocalPlayer():IsAdmin() then + notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat!", NOTIFY_GENERIC, 10) + end + notification.AddLegacy("Before you go, once you're in the PAC editor, please go to the options tab and consider choosing which parts of the combat update you want to consent to.", NOTIFY_GENERIC, 10) + end + Derma_Query("Did you read the update news?", "update news", + "Yes, go away", function() motd_cvar:SetInt(0) exit_message() end, + "Bring it up next update", function() motd_cvar:SetInt(1) exit_message() end, + "No, I'll read later", function() motd_cvar:SetInt(2) exit_message() end + ) + + end + + pnl:SetTitle("Welcome to a new update!") + local html = vgui.Create("DHTML", pnl) + html:Dock(FILL) + html:OpenURL( url ) + + + pnl:Center() + pnl:MakePopup() + pace.motd_opened = true +end + + +timer.Simple(10, function() if motd_cvar:GetInt() ~= 0 and not pace.motd_opened then pac.OpenMOTD(true) end end) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 6bee23138..38589542f 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -1,10 +1,11 @@ local L = pace.LanguageString -- load only when hovered above -local function add_expensive_submenu_load(pnl, callback) +local function add_expensive_submenu_load(pnl, callback, subdir) + local old = pnl.OnCursorEntered pnl.OnCursorEntered = function(...) - callback() + callback(subdir) pnl.OnCursorEntered = old return old(...) end @@ -95,6 +96,10 @@ end local last_backup local maxBackups = CreateConVar("pac_backup_limit", "100", {FCVAR_ARCHIVE}, "Maximal amount of backups") +local autoload_prompt = CreateConVar("pac_prompt_for_autoload", "1", {FCVAR_ARCHIVE}, "Whether to ask before loading autoload. The prompt can let you choose to not load, pick autoload or the newest backup") +local auto_spawn_prop = CreateConVar("pac_autoload_preferred_prop", "2", {FCVAR_ARCHIVE}, "When loading a pac with an owner name suggesting a prop, notify you and then wait before auto-applying the outfit next time you spawn a prop.\n".. + "0 : do not check\n1 : check if only 1 such group is present\n2 : check if multiple such groups are present and queue one group at a time") + function pace.Backup(data, name) name = name or "" @@ -138,7 +143,28 @@ function pace.Backup(data, name) end end +local latestprop +local latest_uid +if game.SinglePlayer() then + hook.Add("OnEntityCreated", "PAC_queue_proppacs", function( ent ) + if ( ent:GetClass() == "prop_physics" or ent:IsNPC()) and not ent:CreatedByMap() and LocalPlayer().pac_propload_queuedparts then + if not table.IsEmpty(LocalPlayer().pac_propload_queuedparts) then + ent:EmitSound( "buttons/button4.wav" ) + local root = LocalPlayer().pac_propload_queuedparts[next(LocalPlayer().pac_propload_queuedparts)] + root.self.OwnerName = ent:EntIndex() + latest_uid = root.self.UniqueID + pace.LoadPartsFromTable(root, false, false) + LocalPlayer().pac_propload_queuedparts[next(LocalPlayer().pac_propload_queuedparts)] = nil + latestprop = ent + end + + end + end) +end + + function pace.LoadParts(name, clear, override_part) + if not name then local frm = vgui.Create("DFrame") frm:SetTitle(L"parts") @@ -175,6 +201,7 @@ function pace.LoadParts(name, clear, override_part) end else + if name ~= "autoload.txt" and not string.find(name, "pac3/__backup") then cookie.Set( "pac_last_loaded_outfit", name ) end if hook.Run("PrePACLoadOutfit", name) == false then return end @@ -190,6 +217,7 @@ function pace.LoadParts(name, clear, override_part) local data, err = pace.luadata.Decode(str) if not data then + ErrorNoHalt(("URL fail: %s : %s\n"):format(name,err)) local message = string.format("URL fail: %s : %s\n", name, err) pace.MessagePrompt(message, "URL Failed", "OK") return @@ -205,22 +233,62 @@ function pace.LoadParts(name, clear, override_part) name = name:gsub("%.txt", "") local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") + local has_possible_prop_pacs = false - if name == "autoload" and (not data or not next(data)) then - local err - data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true) - if not data then - if err then - ErrorNoHalt(("Autoload failed: %s\n"):format(err)) + if data and istable(data) then + for i,part in pairs(data) do + if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end + end + end + + --queue up prop pacs for the next prop or npc you spawn when in singleplayer + if (auto_spawn_prop:GetInt() == 2 or (auto_spawn_prop:GetInt() == 1 and #data == 1)) and game.SinglePlayer() and has_possible_prop_pacs then + if clear then pace.ClearParts() end + LocalPlayer().pac_propload_queuedparts = LocalPlayer().pac_propload_queuedparts or {} + + --check all root parts from data. format: each data member is a {self, children} table of the part and the list of children + for i,part in pairs(data) do + local possible_prop_pac = isnumber(tonumber(part.self.OwnerName)) + if part.self.ClassName == "group" and possible_prop_pac then + + part.self.ModelTracker = part.self.ModelTracker or "" + part.self.ClassTracker = part.self.ClassTracker or "" + local str = "" + if part.self.ClassTracker == "" or part.self.ClassTracker == "" then + str = "But the class or model is unknown" + else + str = part.self.ClassTracker .. " : " .. part.self.ModelTracker + end + --notify which model / entity should be spawned with the class tracker + notification.AddLegacy( "You have queued a pac part (" .. i .. ":" .. part.self.Name .. ") for a prop or NPC! " .. str, NOTIFY_HINT, 10 ) + LocalPlayer().pac_propload_queuedparts[i] = part + + else + pace.LoadPartsFromTable(part, false, false) end + end + + else + + if name == "autoload" and (not data or not next(data)) then + local err + data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true) + if not data then + if err then + ErrorNoHalt(("Autoload failed: %s\n"):format(err)) + end + return + end + elseif not data then + ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err)) return end - elseif not data then - ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err)) - return - end - pace.LoadPartsFromTable(data, clear, override_part) + + pace.LoadPartsFromTable(data, clear, override_part) + + end + end end end @@ -425,6 +493,39 @@ local function populate_parts(menu, tbl, override_part, clear) end end +function pace.AddOneDirectorySavedPartsToMenu(menu, subdir, nicename) + if not subdir then return end + local subdir_head = subdir .. "/" + + local exp_submenu, pnl = menu:AddSubMenu(L""..subdir) + pnl:SetImage(pace.MiscIcons.load) + exp_submenu.GetDeleteSelf = function() return false end + subdir = "pac3/" .. subdir + if nicename then exp_submenu:SetText(nicename) end + + add_expensive_submenu_load(pnl, function(subdir) + local files = file.Find(subdir.."/*", "DATA") + local files2 = {} + --PrintTable(files) + for i, filename in ipairs(files) do + table.insert(files2, {filename, file.Time(subdir .. filename, "DATA")}) + end + + table.sort(files2, function(a, b) + return a[2] > b[2] + end) + + for _, data in pairs(files2) do + local name = data[1] + local full_path = subdir .. "/" .. name + --print(full_path) + local friendly_name = name .. " " .. string.NiceSize(file.Size(full_path, "DATA")) + exp_submenu:AddOption(friendly_name, function() pace.LoadParts(subdir_head .. name, true) end) + :SetImage(pace.MiscIcons.outfit) + end + end, subdir) +end + function pace.AddSavedPartsToMenu(menu, clear, override_part) menu.GetDeleteSelf = function() return false end @@ -481,7 +582,10 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) pnl:SetImage(pace.MiscIcons.clone) backups.GetDeleteSelf = function() return false end - add_expensive_submenu_load(pnl, function() + local subdir = "pac3/__backup/*" + + add_expensive_submenu_load(pnl, function(subdir) + local files = file.Find("pac3/__backup/*", "DATA") local files2 = {} @@ -500,14 +604,15 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) backups:AddOption(friendly_name, function() pace.LoadParts("__backup/" .. name, true) end) :SetImage(pace.MiscIcons.outfit) end - end) + end, subdir) local backups, pnl = menu:AddSubMenu(L"outfit backups") pnl:SetImage(pace.MiscIcons.clone) backups.GetDeleteSelf = function() return false end + subdir = "pac3/__backup_save/*" add_expensive_submenu_load(pnl, function() - local files = file.Find("pac3/__backup_save/*", "DATA") + local files = file.Find(subdir, "DATA") local files2 = {} for i, filename in ipairs(files) do @@ -534,7 +639,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) end) :SetImage(pace.MiscIcons.outfit) end - end) + end, subdir) end local function populate_parts(menu, tbl, dir, override_part) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 574c99876..2e753054f 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1,14 +1,633 @@ +include("parts.lua") +include("shortcuts.lua") + +local pac_submit_spam = CreateConVar('pac_submit_spam', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'Prevent users from spamming pac_submit') +local pac_submit_limit = CreateConVar('pac_submit_limit', '30', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'pac_submit spam limit') +local hitscan_allow = CreateConVar("pac_sv_hitscan", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside") +local hitscan_max_bullets = CreateConVar("pac_sv_hitscan_max_bullets", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum number of bullets") +local hitscan_max_damage = CreateConVar("pac_sv_hitscan_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum damage") +local hitscan_spreadout_dmg = CreateConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not force hitscans to divide their damage among the number of bullets fired") + +local damagezone_allow = CreateConVar("pac_sv_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside") +local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum damage") +local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length") +local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius") +local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone") + +local lock_allow = CreateConVar("pac_sv_lock", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") +local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") +local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside") +local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius") +local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing players with lock part") +local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part") +local lock_allow_grab_ent = CreateConVar("pac_sv_lock_allow_grab_ent", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing other entities with lock part") + +local force_allow = CreateConVar("pac_sv_force", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside") +local force_max_length = CreateConVar("pac_sv_force_max_length", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum length") +local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum radius") +local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force") + +local healthmod_allow = CreateConVar("pac_sv_health_modifier", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") +local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") +local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") +local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") + +local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") + +local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter") +local netrate_allowance = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 60, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "the budgeted allowance to limit how many pac combat net messages can be sent in bursts. 0 to disable, otherwise a number of net messages of allowance.") +local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_netrate_monitor_serverside", 0, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not to let clients enforce their net message rates.\nSet this to 1 to get serverside prints telling you whenever someone is going over their allowance, but it'll still take the network bandwidth.\nSet this to 0 to let clients enforce their net rate and save some bandwidth but the server won't know who's spamming net messages.") +local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") +local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") +local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") +local enforce_distance = CreateConVar("pac_sv_combat_distance_enforced", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enforce a limit on how far a pac combat action can originate.\nIf set to a distance, it will prevent actions that are too far from the acting player.\n0 to disable.") + + +local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") +local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") + + +include("pac3/editor/server/combat_bans.lua") + + +pace = pace + +pace.partmenu_categories_cedrics = +{ + ["new!"] = + { + ["icon"] = "icon16/new.png", + ["interpolated_multibone"]= "interpolated_multibone", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + }, + ["logic"] = + { + ["icon"] = "icon16/server_chart.png", + ["proxy"] = "proxy", + ["command"] = "command", + ["event"] = "event", + ["text"] = "text", + ["link"] = "link", + }, + ["scaffolds"] = + { + ["tooltip"] = "useful to build up structures with specific positioning rules", + ["icon"] = "map", + ["jiggle"] = "jiggle", + ["model2"] = "model2", + ["projectile"] = "projectile", + ["interpolated_multibone"]= "interpolated_multibone", + }, + ["combat"] = + { + ["icon"] = "icon16/joystick.png", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["projectile"] = "projectile", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + ["player_movement"] = "player_movement", + }, + ["animation"]= + { + ["icon"] = "icon16/world.png", + ["group"] = "group", + ["event"] = "event", + ["custom_animation"] = "custom_animation", + ["proxy"] = "proxy", + ["sprite"] = "sprite", + ["particle"] = "particle", + }, + ["materials"]= + { + ["icon"] = "pace.MiscIcons.appearance", + ["material_3d"] = "material_3d", + ["material_2d"] = "material_2d", + ["material_refract"] = "material_refract", + ["material_eye refract"]= "material_eye refract", + ["submaterial"] = "submaterial", + }, + ["entity"] = + { + ["icon"] = "icon16/cd_go.png", + ["bone3"] = "bone3", + ["custom_animation"] = "custom_animation", + ["gesture"] = "gesture", + ["entity2"] = "entity2", + ["poseparameter"] = "poseparameter", + ["camera"] = "camera", + ["holdtype"] = "holdtype", + ["effect"] = "effect", + ["player_config"] = "player_config", + ["player_movement"] = "player_movement", + ["animation"] = "animation", + ["submaterial"] = "submaterial", + ["faceposer"] = "faceposer", + ["flex"] = "flex", + ["material_3d"] = "material_3d", + ["weapon"] = "weapon", + }, + ["model"] = + { + ["icon"] = "icon16/bricks.png", + ["jiggle"] = "jiggle", + ["physics"] = "physics", + ["animation"] = "animation", + ["bone3"] = "bone3", + ["effect"] = "effect", + ["submaterial"] = "submaterial", + ["clip2"] = "clip2", + ["halo"] = "halo", + ["material_3d"] = "material_3d", + ["model2"] = "model2", + }, + ["modifiers"] = + { + ["icon"] = "icon16/connect.png", + ["fog"] = "fog", + ["motion_blur"] = "motion_blur", + ["halo"] = "halo", + ["clip2"] = "clip2", + ["bone3"] = "bone3", + ["poseparameter"] = "poseparameter", + ["material_3d"] = "material_3d", + ["proxy"]= "proxy", + }, + ["effects"] = + { + ["icon"] = "icon16/wand.png", + ["sprite"] = "sprite", + ["sound2"] = "sound2", + ["effect"] = "effect", + ["halo"] = "halo", + ["particles"]= "particles", + ["sunbeams"]= "sunbeams", + ["beam"]= "beam", + ["projected_texture"]= "projected_texture", + ["decal"]= "decal", + ["text"]= "text", + ["trail2"]= "trail2", + ["sound"]= "sound", + ["woohoo"]= "woohoo", + ["light2"]= "light2", + ["shake"]= "shake", + } +} + +pace.partmenu_categories_default = +{ + ["legacy"]= + { + ["icon"] = pace.GroupsIcons.legacy, + ["trail"]= "trail", + ["bone2"]= "bone2", + ["model"]= "model", + ["bodygroup"]= "bodygroup", + ["material"]= "material", + ["light"]= "light", + ["entity"]= "entity", + ["clip"]= "clip", + ["bone"]= "bone", + ["webaudio"]= "webaudio", + ["ogg"] = "ogg", + }, + ["advanced"]= + { + ["icon"] = pace.GroupsIcons.advanced, + ["lock"]= "lock", + ["force"]= "force", + ["custom_animation"]= "custom_animation", + ["material_refract"]= "material_refract", + ["projectile"]= "projectile", + ["link"] = "link", + ["damage_zone"] = "damage_zone", + ["interpolated_multibone"] = "interpolated_multibone", + ["material_2d"] = "material_2d", + ["material_eye refract"] = "material_eye refract", + ["hitscan"] = "hitscan", + ["health_modifier"] = "health_modifier", + ["command"] ="command", + }, + ["entity"]= + { + ["icon"] = pace.GroupsIcons.entity, + ["bone3"] = "bone3", + ["gesture"] = "gesture", + ["entity2"] ="entity2", + ["poseparameter"] = "poseparameter", + ["camera"] = "camera", + ["holdtype"]= "holdtype", + ["effect"] = "effect", + ["player_config"] = "player_config", + ["player_movement"] = "player_movement", + ["animation"] = "animation", + ["submaterial"] = "submaterial", + ["faceposer"] = "faceposer", + ["flex"] = "flex", + ["material_3d"] = "material_3d", + ["weapon"]= "weapon", + }, + ["model"]= + { + ["icon"] = pace.GroupsIcons.model, + ["jiggle"] = "jiggle", + ["physics"] = "physics", + ["animation"]= "animation", + ["bone3"] = "bone3", + ["effect"] = "effect", + ["submaterial"] ="submaterial", + ["clip2"] = "clip2", + ["halo"] = "halo", + ["material_3d"] = "material_3d", + ["model2"]= "model2", + }, + ["modifiers"]= + { + ["icon"] = pace.GroupsIcons.modifiers, + ["animation"] = "animation", + ["fog"] = "fog", + ["motion_blur"] = "motion_blur", + ["clip2"]= "clip2", + ["poseparameter"] = "poseparameter", + ["material_3d"] = "material_3d", + ["proxy"] = "proxy", + }, + ["effects"]= + { + ["icon"] = pace.GroupsIcons.effects, + ["sprite"] = "sprite", + ["sound2"] = "sound2", + ["effect"] = "effect", + ["halo"] = "halo", + ["particles"]= "particles", + ["sunbeams"] = "sunbeams", + ["beam"] = "beam", + ["projected_texture"]= "projected_texture", + ["decal"] = "decal", + ["text"] = "text", + ["trail2"] = "trail2", + ["sound"] = "sound", + ["woohoo"] = "woohoo", + ["light2"] = "light2", + ["shake"] = "shake" + } +} + + +local function rebuild_bookmarks() + pace.bookmarked_ressources = pace.bookmarked_ressources or {} + + --here's some default favorites + if not pace.bookmarked_ressources["models"] or table.IsEmpty(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 + + if not pace.bookmarked_ressources["sound"] or table.IsEmpty(pace.bookmarked_ressources["sound"]) then + pace.bookmarked_ressources["sound"] = { + "music/hl1_song11.mp3", + "npc/combine_gunship/dropship_engine_near_loop1.wav", + "ambient/alarms/warningbell1.wav", + "phx/epicmetal_hard7.wav", + "phx/explode02.wav" + } + end + + if not pace.bookmarked_ressources["materials"] or table.IsEmpty(pace.bookmarked_ressources["materials"]) then + pace.bookmarked_ressources["materials"] = { + "models/debug/debugwhite", + "vgui/null", + "debug/env_cubemap_model", + "models/wireframe", + "cable/physbeam", + "cable/cable2", + "effects/tool_tracer", + "effects/flashlight/logo", + "particles/flamelet[1,5]", + "sprites/key_[0,9]", + "vgui/spawnmenu/generating", + "vgui/spawnmenu/hover" + } + end + + if not pace.bookmarked_ressources["proxy"] or table.IsEmpty(pace.bookmarked_ressources["proxy"]) then + pace.bookmarked_ressources["proxy"] = { + --[[["user"] = { + + },]] + ["fades and transitions"] ={ + { + nicename = "standard clamp fade (in)", + expression = "clamp(timeex(),0,1)", + explanation = "the simplest fade.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 0, moves gradually to 1 and stops progressing at 1 due to the clamp" + }, + { + nicename = "standard clamp fade (out)", + expression = "clamp(1 - timeex(),0,1)", + explanation = "the simplest fade's reverse.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 1, moves gradually to 0 and stops progressing at 0 due to the clamp" + }, + { + nicename = "standard clamp fade (delayed in)", + expression = "clamp(-1 + timeex(),0,1)", + explanation = "the basic fade is delayed by the fact that the clamp makes sure the negative values are pulled back to 0 until the first argument crosses 0 into the clamp's range." + }, + { + nicename = "standard clamp fade (delayed out)", + expression = "clamp(2 - timeex(),0,1)", + explanation = "the reverse fade is delayed by the fact that the clamp makes sure the values beyond 1 are pulled back to 1 until the first argument crosses 1 into the clamp's range." + }, + { + nicename = "standard clamp fade (in and out)", + expression = "clamp(timeex(),0,1)*clamp(3 - timeex(),0,1)", + explanation = "this is just compounding both fades. the second clamp's 3 is comprised of 1 (the clamp max) + 1 (the delay BEFORE the fade) + 1 (the delay BETWEEN the fades)" + }, + { + nicename = "quick ease setup", + expression = "easeInBack(clamp(timeex(),0,1))", + explanation = "get started quickly with the new easing functions.\nsearch \"ease\" in the proxy's input list to see how to write them in pac3, or look at the gmod wiki to see previews of each" + }, + }, + ["pulses"] = { + { + nicename = "bell pulse", + expression = "(0 + 1*sin(PI*timeex())^16)", + explanation = "a basic normalized pulse, using a sine power." + }, + { + nicename = "square-like throb", + expression = "(0 + 1 * (cos(PI*timeex())^16) ^0.3)", + explanation = "a throbbing-like pulse, made by combining a sine power with a fractionnal power.\nthis is better explained visually, so either test it right here in game or go look at a graph to see how x, and cos or sin behave with powers.\ntry x^pow and sin(x)^pow, and try different pows" + }, + { + nicename = "binary pulse", + expression = "floor(1 + sin(PI*timeex()))", + explanation = "an on-off pulse, in other words a square wave.\nthis one completes one cycle every 2 seconds.\nfloor rounds down between 1 and 0 with nothing in-between." + }, + { + nicename = "saw wave (up)", + expression = "(timeex()%1)", + explanation = "a sawtooth wave. it can repeat a 0-1 transition." + }, + { + nicename = "saw wave (down)", + expression = "(1 - timeex()%1)", + explanation = "a sawtooth wave. it can repeat a 1-0 transition." + }, + { + nicename = "triangle wave", + expression = "(clamp(-1+timeex()%2,0,1) + clamp(1 - timeex()%2,0,1))", + explanation = "a triangle wave. it goes back and forth linearly like a saw up and down." + } + }, + ["facial expressions"] = { + { + nicename = "normal slow blink", + expression = "3*clamp(sin(timeex())^100,0,1)", + explanation = "a normal slow blink.\nwhile flexes usually have a range of 0-1, the 3 outside of the clamp is there to trick the value into going faster in case they're too slow to reach their target" + }, + { + nicename = "normal fast blink", + expression = "8*clamp(sin(timeex())^600,0,1)", + explanation = "a normal slow blink.\nwhile flexes usually have a range of 0-1, the 8 outside of the clamp is there to trick the value into going faster in case they're too slow to reach their target\nif it's still not enough, use another flex with less blinking amount to provide the additionnal distance for the blink" + }, + { + nicename = "babble", + expression = "sin(12*timeex())^2", + explanation = "a basic piece to move the mouth semi-convincingly for voicelines.\nthere'll never be dynamic lipsync in pac3, but this is a start." + }, + { + nicename = "voice smoothener", + expression = "clamp(feedback() + 70*voice_volume()*ftime() - 15*ftime(),0,2)", + explanation = "uses a feedback() setup to raise the mouth's value gradually against a constantly lowering value, which should be more smoothly than a direct input" + }, + { + nicename = "look side (legacy symmetrical look)", + expression = "3*(-1 + 2*pose_parameter(\"head_yaw\"))", + explanation = "an expression to mimic the head's yaw" + }, + { + nicename = "look side (new)", + expression = "pose_parameter_true(\"head_yaw\")", + explanation = "an expression to mimic the head's yaw, but it requires your model to have this standard pose parameter" + }, + { + nicename = "look up", + expression = "(-1 + 2*owner_eye_angle_pitch())", + explanation = "an expression to mimic the head's pitch on a [-1,1] range" + }, + { + nicename = "single eyeflex direction (up)", + expression = "-0.03*pose_parameter_true(\"head_pitch\")", + explanation = "plug into an eye_look_up flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (down)", + expression = "0.03*pose_parameter_true(\"head_pitch\")", + explanation = "plug into an eye_look_down flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (left)", + expression = "0.03*pose_parameter_true(\"head_yaw\")", + explanation = "plug into an eye_look_left flex or an eye bone with a higher multiplier" + }, + { + nicename = "single eyeflex direction (right)", + expression = "-0.03*pose_parameter_true(\"head_yaw\")", + explanation = "plug into an eye_look_right flex or an eye bone with a higher multiplier" + }, + }, + ["spatial"] = { + { + nicename = "random position (cloud)", + expression = "150*random(-1,1),150*random(-1,1),150*random(-1,1)", + explanation = "position a part randomly across X,Y,Z\nbut constantly blinking everywhere, because random generates a new number every frame.\nyou should only use this for parts that emit things into the world" + }, + { + nicename = "random position (once)", + expression = "150*random_once(0,-1,1),150*random_once(1,-1,1),150*random_once(2,-1,1)", + explanation = "position a part randomly across X,Y,Z\nbut once, because random_once only generates a number once.\nit, however, needs distinct numbers in the first arguments to distinguish them every time you write the function." + }, + { + nicename = "distance-based fade", + expression = "clamp((250/500) + 1 - (eye_position_distance() / 500),0,1)", + explanation = "a fading based on the viewer's distance. 250 and 500 are the example distances, 250 is where the expression starts diminishing, and 750 is where we reach 0." + }, + { + nicename = "distance between two points", + expression = "part_distance(uid1,uid2)", + explanation = "Trick question! You have some homework! You need to find out your parts' UIDs first.\ntry tools -> copy global id, then paste those in place of uid1 and uid2" + }, + { + nicename = "revolution (orbit)", + expression = "150*sin(time()),150*cos(time()),0", + explanation = "Trick question! You might need to rearrange the expression depending on which coordinate system we're at. For a thing on a pos_noang bone, it works as is. But for something on your head, you would need to swap x and z\n0,150*cos(time()),150*sin(time())" + }, + { + nicename = "spin", + expression = "0,360*time(),0", + explanation = "a simple spinner on Y" + } + }, + ["experimental things"] = { + { + nicename = "control a boolean directly with an event", + expression = "event_alternative(uid1,0,1)", + explanation = "trick question! you need to find out your event's part UID first and substitute uid1\n" + }, + { + nicename = "feedback system controlled with 2 events", + expression = "feedback() + ftime()*(event_alternative(uid1,0,1) + event_alternative(uid2,0,-1))", + explanation = "trick question! you need to find out your event parts' UIDs first and substitute uid1 and uid2.\nthe new event_alternative function gets an event's state\nwe can inject that into our feedback system to act as a positive or negative speed" + }, + { + nicename = "basic if-else statement", + expression = "number_operator_alternative(1,\">\",0,100,50)", + explanation = "might be niche but here's a basic alternator thing, you can compare the 1st and 3rd args with numeric operators like \"above\", \"<\", \"=\", \"~=\" etc. to choose between the 4th and 5th args\nit goes like this\nnumber_operator_alternative(1,\">\",0,100,50)\nif 1>0, return 100, else return 50" + }, + { + nicename = "pick from 3 random colors", + expression = "number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.75)),number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 0.8, 0.65)),number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.58))", + explanation = + "using a shared random source, you can nest number_operator_alternative functions to get a 3-way branching random choice\n0.333 and 0.666 correspond to the chance slices where each choice gets decided so you can change the probabilities by editing these numbers\nBecause of the fact we're going deep, it's not easily readable so I'll lay out each component.\n\n" .. + "R: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.75))\n".. + "G: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 0.8, 0.65))\n".. + "B: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.58))\n\n".. + "The first choice is white (1,1,1), the second choice is light pink (1,0.8,1) like a strawberry milk, the third choice is light creamy brown (0.75,0.65,0.58) like chocolate milk" + }, + { + nicename = "feedback command attractor", + expression = "feedback() + ftime()*(command(\"destination\") - feedback())", + explanation = + "This thing uses a principle of iteration similar to exponential functions to attract the feedback toward any target\n".. + "The delta bit will get smaller and smaller as the gap between destination and feedback closes, stabilizing at 0, thereby stopping.\n".. + "You will utilize pac_proxy commands to set the destination target: \"pac_proxy destination 2\" will make the expression tend toward 2." + } + } + } + + end + +end local PANEL = {} +local player_ban_list = {} +local player_combat_ban_list = {} + +local function encode_table_to_file(str) + local data = {} + if not file.Exists("pac3_config", "DATA") then + file.CreateDir("pac3_config") + + end + + + if str == "pac_editor_shortcuts" then + data = pace.PACActionShortcut + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "pac_editor_partmenu_layouts" then + data = pace.operations_order + file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data)) + elseif str == "pac_part_categories" then + data = pace.partgroups + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + elseif str == "bookmarked_ressources" then + rebuild_bookmarks() + for category, tbl in pairs(pace.bookmarked_ressources) do + data = tbl + str = category + file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data)) + end + elseif str == "eventwheel_colors" then + data = pace.command_colors or {} + file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + end + +end + +local function decode_table_from_file(str) + if str == "bookmarked_ressources" then + rebuild_bookmarks() + local ressource_types = {"models", "sound", "materials", "sprites"} + for _, category in pairs(ressource_types) do + data = file.Read("pac3_config/bookmarked_" .. category ..".txt", "DATA") + if data then pace.bookmarked_ressources[category] = util.KeyValuesToTable(data) end + end + return + end + + local data = file.Read("pac3_config/" .. str..".txt", "DATA") + if not data then return end + + if str == "pac_editor_shortcuts" then + pace.PACActionShortcut = util.KeyValuesToTable(data) + + elseif str == "pac_editor_partmenu_layouts" then + pace.operations_order = util.JSONToTable(data) + + elseif str == "pac_part_categories" then + pace.partgroups = util.KeyValuesToTable(data) + + elseif str == "eventwheel_colors" then + pace.command_colors = util.KeyValuesToTable(data) + end + + +end + +decode_table_from_file("bookmarked_ressources") +pace.bookmarked_ressources = pace.bookmarked_ressources or {} + +function pace.SaveRessourceBookmarks() + encode_table_to_file("bookmarked_ressources") +end + function PANEL:Init() - local pnl = vgui.Create("DPropertySheet", self) - pnl:Dock(FILL) + local master_pnl = vgui.Create("DPropertySheet", self) + master_pnl:Dock(FILL) + + local properties_filter = pace.FillWearSettings(master_pnl) + master_pnl:AddSheet("Wear / Ignore", properties_filter) + + local editor_settings = pace.FillEditorSettings(master_pnl) + master_pnl:AddSheet("Editor menu Settings", editor_settings) - local props = pace.FillWearSettings(pnl) + local editor_settings2 = pace.FillEditorSettings2(master_pnl) + master_pnl:AddSheet("Editor menu Settings 2", editor_settings2) - pnl:AddSheet("Wear / Ignore", props) - self.sheet = pnl + + if game.SinglePlayer() or LocalPlayer():IsAdmin() then + + local general_sv_settings = pace.FillServerSettings(master_pnl) + master_pnl:AddSheet("General Settings (SV)", general_sv_settings) + + local combat_sv_settings = pace.FillCombatSettings(master_pnl) + master_pnl:AddSheet("Combat Settings (SV)", combat_sv_settings) + + local ban_settings = pace.FillBanPanel(master_pnl) + master_pnl:AddSheet("Bans (SV)", ban_settings) + + local combat_ban_settings = pace.FillCombatBanPanel(master_pnl) + master_pnl:AddSheet("Combat Bans (SV)", combat_ban_settings) + + end + + + self.sheet = master_pnl + + --local properties_shortcuts = pace.FillShortcutSettings(pnl) + --pnl:AddSheet("Editor Shortcuts", properties_shortcuts) end vgui.Register( "pace_settings", PANEL, "DPanel" ) @@ -20,7 +639,7 @@ function pace.OpenSettings() local pnl = vgui.Create("DFrame") pnl:SetTitle("pac settings") pace.settings_panel = pnl - pnl:SetSize(600,600) + pnl:SetSize(800,600) pnl:MakePopup() pnl:Center() pnl:SetSizable(true) @@ -31,4 +650,1931 @@ end concommand.Add("pace_settings", function() pace.OpenSettings() -end) \ No newline at end of file +end) + + +function pace.FillBanPanel(pnl) + local pnl = pnl + local BAN = vgui.Create("DPanel", pnl) + local ply_state_list = player_ban_list or {} + + local ban_list = vgui.Create("DListView", BAN) + ban_list:SetText("ban list") + ban_list:SetSize(400,400) + ban_list:SetPos(10,10) + + ban_list:AddColumn("Player name") + ban_list:AddColumn("SteamID") + ban_list:AddColumn("State") + ban_list:SetSortable(false) + for _,ply in pairs(player.GetAll()) do + --print(ply, pace.IsBanned(ply)) + ban_list:AddLine(ply:Name(),ply:SteamID(),player_ban_list[ply] or "Allowed") + end + + function ban_list:DoDoubleClick( lineID, line ) + --MsgN( "Line " .. lineID .. " was double clicked!" ) + local state = line:GetColumnText( 3 ) + + if state == "Banned" then state = "Allowed" + elseif state == "Allowed" then state = "Banned" end + line:SetColumnText(3,state) + ply_state_list[player.GetBySteamID(line:GetColumnText( 2 ))] = state + PrintTable(ply_state_list) + end + + local ban_confirm_list_button = vgui.Create("DButton", BAN) + ban_confirm_list_button:SetText("Send ban list update to server") + + ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!") + ban_confirm_list_button:SetColor(Color(255,0,0)) + ban_confirm_list_button:SetSize(200, 40) + ban_confirm_list_button:SetPos(450, 10) + function ban_confirm_list_button:DoClick() + net.Start("pac.BanUpdate") + net.WriteTable(ply_state_list) + net.SendToServer() + end + local ban_request_list_button = vgui.Create("DButton", BAN) + ban_request_list_button:SetText("Request ban list from server") + --ban_request_list_button:SetColor(Color(255,0,0)) + ban_request_list_button:SetSize(200, 40) + ban_request_list_button:SetPos(450, 60) + + function ban_request_list_button:DoClick() + net.Start("pac.RequestBanStates") + net.SendToServer() + end + + net.Receive("pac.SendBanStates", function() + local players = net.ReadTable() + player_ban_list = players + PrintTable(players) + end) + + + return BAN +end + +function pace.FillCombatBanPanel(pnl) + local pnl = pnl + local BAN = vgui.Create("DPanel", pnl) + pac.global_combat_whitelist = pac.global_combat_whitelist or {} + + + local ban_list = vgui.Create("DListView", BAN) + ban_list:SetText("Combat ban list") + ban_list:SetSize(400,400) + ban_list:SetPos(10,10) + + ban_list:AddColumn("Player name") + ban_list:AddColumn("SteamID") + ban_list:AddColumn("State") + ban_list:SetSortable(false) + if GetConVar('pac_sv_combat_whitelisting'):GetBool() then + ban_list:SetTooltip( "Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed" ) + else + ban_list:SetTooltip( "Blacklist mode: Default players are allowed to use the combat features" ) + end + + local combat_bans_temp_merger = {} + + for _,ply in pairs(player.GetAll()) do + combat_bans_temp_merger[ply:SteamID()] = pac.global_combat_whitelist[ply:SteamID()]-- or {nick = ply:Nick(), steamid = ply:SteamID(), permission = "Default"} + end + + for id,data in pairs(pac.global_combat_whitelist) do + combat_bans_temp_merger[id] = data + end + + for id,data in pairs(combat_bans_temp_merger) do + ban_list:AddLine(data.nick,data.steamid,data.permission) + end + + function ban_list:DoDoubleClick( lineID, line ) + --MsgN( "Line " .. lineID .. " was double clicked!" ) + local state = line:GetColumnText( 3 ) + + if state == "Banned" then state = "Default" + elseif state == "Default" then state = "Allowed" + elseif state == "Allowed" then state = "Banned" end + line:SetColumnText(3,state) + pac.global_combat_whitelist[string.lower(line:GetColumnText( 2 ))].permission = state + PrintTable(pac.global_combat_whitelist) + end + + local ban_confirm_list_button = vgui.Create("DButton", BAN) + ban_confirm_list_button:SetText("Send combat ban list update to server") + + ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!") + ban_confirm_list_button:SetColor(Color(255,0,0)) + ban_confirm_list_button:SetSize(200, 40) + ban_confirm_list_button:SetPos(450, 10) + function ban_confirm_list_button:DoClick() + net.Start("pac.CombatBanUpdate") + net.WriteTable(pac.global_combat_whitelist) + net.WriteBool(true) + net.SendToServer() + end + local ban_request_list_button = vgui.Create("DButton", BAN) + ban_request_list_button:SetText("Request ban list from server") + --ban_request_list_button:SetColor(Color(255,0,0)) + ban_request_list_button:SetSize(200, 40) + ban_request_list_button:SetPos(450, 60) + + function ban_request_list_button:DoClick() + net.Start("pac.RequestCombatBanStates") + net.SendToServer() + end + + net.Receive("pac.SendCombatBanStates", function() + pac.global_combat_whitelist = net.ReadTable() + ban_list:Clear() + local combat_bans_temp_merger = {} + + for _,ply in pairs(player.GetAll()) do + combat_bans_temp_merger[ply:SteamID()] = pac.global_combat_whitelist[ply:SteamID()]-- or {nick = ply:Nick(), steamid = ply:SteamID(), permission = "Default"} + end + + for id,data in pairs(pac.global_combat_whitelist) do + combat_bans_temp_merger[id] = data + end + + for id,data in pairs(combat_bans_temp_merger) do + ban_list:AddLine(data.nick,data.steamid,data.permission) + end + end) + + + return BAN +end + +function pace.FillCombatSettings(pnl) + local pnl = pnl + + local master_list = vgui.Create("DCategoryList", pnl) + master_list:Dock(FILL) + --general + do + local general_list = master_list:Add("General (Global policy and Network protection)") + general_list.Header:SetSize(40,40) + general_list.Header:SetFont("DermaLarge") + local general_list_list = vgui.Create("DListLayout") + general_list_list:DockPadding(20,0,20,20) + general_list:SetContents(general_list_list) + + local sv_prop_protection_props_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_prop_protection_props_box:SetText("Enforce generic prop protection for player-owned props and physics entities.\nRelated to client consents, but the policies for each part are not uniform.") + sv_prop_protection_props_box:SetSize(400,30) + sv_prop_protection_props_box:SetConVar("pac_sv_prop_protection") + + + local sv_combat_whitelisting_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_combat_whitelisting_box:SetText("Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.") + sv_combat_whitelisting_box:SetSize(400,30) + sv_combat_whitelisting_box:SetConVar("pac_sv_combat_whitelisting") + sv_combat_whitelisting_box:SetTooltip("off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed") + + local sv_master_break_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_master_break_box:SetText("Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts") + sv_master_break_box:SetSize(400,30) + sv_master_break_box:SetConVar("pac_sv_block_combat_features_on_next_restart") + sv_master_break_box:SetTooltip("You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers") + + local sv_netrate_monitoring_box = vgui.Create("DCheckBoxLabel", general_list_list) + sv_netrate_monitoring_box:SetText("Enable serverside monitoring prints for allowance and rate limiters") + sv_netrate_monitoring_box:SetSize(400,30) + sv_netrate_monitoring_box:SetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") + sv_netrate_monitoring_box:SetTooltip("Enable serverside monitoring prints.\n0=let clients enforce their netrate allowance before sending messages\n1=the server will receive net messages and print the outcome.") + + local sv_netrate_time_numbox = vgui.Create("DNumSlider", general_list_list) + sv_netrate_time_numbox:SetText("Rate limiter (milliseconds)") + sv_netrate_time_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate"):GetInt()) + sv_netrate_time_numbox:SetMin(0) sv_netrate_time_numbox:SetDecimals(0) sv_netrate_time_numbox:SetMax(1000) + sv_netrate_time_numbox:SetSize(400,30) + sv_netrate_time_numbox:SetConVar("pac_sv_combat_enforce_netrate") + sv_netrate_time_numbox:SetTooltip("The milliseconds delay between net messages.\nIf this is 0, the allowance won't matter, otherwise early net messages use up the player's allowance.\nThe allowance regenerates gradually when unused, and one unit gets spent if the message is earlier than the rate limiter's delay.") + + local sv_netrate_buffer_numbox = vgui.Create("DNumSlider", general_list_list) + sv_netrate_buffer_numbox:SetText("Allowance, in number of messages") + sv_netrate_buffer_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt()) + sv_netrate_buffer_numbox:SetMin(0) sv_netrate_buffer_numbox:SetDecimals(0) sv_netrate_buffer_numbox:SetMax(400) + sv_netrate_buffer_numbox:SetSize(400,30) + sv_netrate_buffer_numbox:SetConVar("pac_sv_combat_enforce_netrate_buffersize") + sv_netrate_buffer_numbox:SetTooltip("Allowance:\nIf this is 0, only the time limiter will stop pac combat messages if they're too fast.\nOtherwise, players trying to use a pac combat message earlier will deduct 1 from the player's allowance, and only stop the messages if the allowance reaches 0.") + + local sv_hard_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) + sv_hard_ent_limit_numbox:SetText("Hard entity limit to cutoff damage zones and force parts") + sv_hard_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_combat_operation"):GetInt()) + sv_hard_ent_limit_numbox:SetMin(0) sv_hard_ent_limit_numbox:SetDecimals(0) sv_hard_ent_limit_numbox:SetMax(1000) + sv_hard_ent_limit_numbox:SetSize(400,30) + sv_hard_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_combat_operation") + sv_hard_ent_limit_numbox:SetTooltip("If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") + + local sv_per_player_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) + sv_per_player_ent_limit_numbox:SetText("Entity limit per player to cutoff damage zones and force parts") + sv_per_player_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_player_per_combat_operation"):GetInt()) + sv_per_player_ent_limit_numbox:SetMin(0) sv_per_player_ent_limit_numbox:SetDecimals(0) sv_per_player_ent_limit_numbox:SetMax(500) + sv_per_player_ent_limit_numbox:SetSize(400,30) + sv_per_player_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_player_per_combat_operation") + sv_per_player_ent_limit_numbox:SetTooltip("When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") + + local sv_player_fraction_slider = vgui.Create("DNumSlider", general_list_list) + sv_player_fraction_slider:SetText("block damage zones targeting this fraction of players") + sv_player_fraction_slider:SetValue(GetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone"):GetFloat()) + sv_player_fraction_slider:SetMin(0) sv_player_fraction_slider:SetDecimals(2) sv_player_fraction_slider:SetMax(1) + sv_player_fraction_slider:SetSize(400,30) + sv_player_fraction_slider:SetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone") + sv_player_fraction_slider:SetTooltip("This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.") + + local sv_distance_slider = vgui.Create("DNumSlider", general_list_list) + sv_distance_slider:SetText("distance to block combat actions that are too far") + sv_distance_slider:SetValue(GetConVar("pac_sv_combat_distance_enforced"):GetFloat()) + sv_distance_slider:SetMin(0) sv_distance_slider:SetDecimals(0) sv_distance_slider:SetMax(64000) + sv_distance_slider:SetSize(400,30) + sv_distance_slider:SetConVar("pac_sv_combat_distance_enforced") + sv_distance_slider:SetTooltip("The distance is compared between the action's origin and the player's position.\n0 to ignore.") + + end + + do --hitscan + --[[ + pac_sv_hitscan + pac_sv_hitscan_max_bullets + pac_sv_hitscan_max_damage + pac_sv_hitscan_divide_max_damage_by_max_bullets + ]] + + local hitscans_list = master_list:Add("Hitscans") + hitscans_list.Header:SetSize(40,40) + hitscans_list.Header:SetFont("DermaLarge") + local hitscans_list_list = vgui.Create("DListLayout") + hitscans_list_list:DockPadding(20,0,20,20) + hitscans_list:SetContents(hitscans_list_list) + + local sv_hitscans_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) + sv_hitscans_box:SetText("allow serverside bullets") + sv_hitscans_box:SetSize(400,30) + sv_hitscans_box:SetConVar("pac_sv_hitscan") + + local hitscans_max_dmg_numbox = vgui.Create("DNumSlider", hitscans_list_list) + hitscans_max_dmg_numbox:SetText("Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)") + hitscans_max_dmg_numbox:SetValue(GetConVar("pac_sv_hitscan_max_damage"):GetInt()) + hitscans_max_dmg_numbox:SetMin(0) hitscans_max_dmg_numbox:SetDecimals(0) hitscans_max_dmg_numbox:SetMax(268435455) + hitscans_max_dmg_numbox:SetSize(400,30) + hitscans_max_dmg_numbox:SetConVar("pac_sv_hitscan_max_damage") + + local sv_hitscans_distribute_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) + sv_hitscans_distribute_box:SetText("force hitscans to distribute their total damage accross bullets. if off, every bullet does full damage; if on, adding more bullets doesn't do more damage") + sv_hitscans_distribute_box:SetSize(400,30) + sv_hitscans_distribute_box:SetConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets") + + local hitscans_max_numbullets_numbox = vgui.Create("DNumSlider", hitscans_list_list) + hitscans_max_numbullets_numbox:SetText("Maximum number of bullets for hitscan multishots") + hitscans_max_numbullets_numbox:SetValue(GetConVar("pac_sv_hitscan_max_bullets"):GetInt()) + hitscans_max_numbullets_numbox:SetMin(1) hitscans_max_numbullets_numbox:SetDecimals(0) hitscans_max_numbullets_numbox:SetMax(500) + hitscans_max_numbullets_numbox:SetSize(400,30) + hitscans_max_numbullets_numbox:SetConVar("pac_sv_hitscan_max_bullets") + end + + do --projectiles + local projectiles_list = master_list:Add("Projectiles") + projectiles_list.Header:SetSize(40,40) + projectiles_list.Header:SetFont("DermaLarge") + local projectiles_list_list = vgui.Create("DListLayout") + projectiles_list_list:DockPadding(20,0,20,20) + projectiles_list:SetContents(projectiles_list_list) + + local sv_projectiles_box = vgui.Create("DCheckBoxLabel", projectiles_list_list) + sv_projectiles_box:SetText("allow serverside physical projectiles") + sv_projectiles_box:SetSize(400,30) + sv_projectiles_box:SetConVar("pac_sv_projectiles") + + local projectile_max_phys_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_phys_radius_numbox:SetText("Max projectile physical radius") + projectile_max_phys_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_phys_radius"):GetInt()) + projectile_max_phys_radius_numbox:SetMin(0) projectile_max_phys_radius_numbox:SetDecimals(0) projectile_max_phys_radius_numbox:SetMax(1000) + projectile_max_phys_radius_numbox:SetSize(400,30) + projectile_max_phys_radius_numbox:SetConVar("pac_sv_projectile_max_phys_radius") + + local projectile_max_dmg_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_dmg_radius_numbox:SetText("Max projectile damage radius") + projectile_max_dmg_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage_radius"):GetInt()) + projectile_max_dmg_radius_numbox:SetMin(0) projectile_max_dmg_radius_numbox:SetDecimals(0) projectile_max_dmg_radius_numbox:SetMax(5000) + projectile_max_dmg_radius_numbox:SetSize(400,30) + projectile_max_dmg_radius_numbox:SetConVar("pac_sv_projectile_max_damage_radius") + + local projectile_max_attract_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_attract_radius_numbox:SetText("Max projectile attract radius") + projectile_max_attract_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_attract_radius"):GetInt()) + projectile_max_attract_radius_numbox:SetMin(0) projectile_max_attract_radius_numbox:SetDecimals(0) projectile_max_attract_radius_numbox:SetMax(100000000) + projectile_max_attract_radius_numbox:SetSize(400,30) + projectile_max_attract_radius_numbox:SetConVar("pac_sv_projectile_max_attract_radius") + + local projectile_max_dmg_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_dmg_numbox:SetText("Max projectile damage") + projectile_max_dmg_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage"):GetInt()) + projectile_max_dmg_numbox:SetMin(0) projectile_max_dmg_numbox:SetDecimals(0) projectile_max_dmg_numbox:SetMax(100000000) + projectile_max_dmg_numbox:SetSize(400,30) + projectile_max_dmg_numbox:SetConVar("pac_sv_projectile_max_damage") + + local projectile_max_speed_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_speed_numbox:SetText("Max projectile speed") + projectile_max_speed_numbox:SetValue(GetConVar("pac_sv_projectile_max_speed"):GetInt()) + projectile_max_speed_numbox:SetMin(0) projectile_max_speed_numbox:SetDecimals(0) projectile_max_speed_numbox:SetMax(50000) + projectile_max_speed_numbox:SetSize(400,30) + projectile_max_speed_numbox:SetConVar("pac_sv_projectile_max_speed") + + local projectile_max_mass_numbox = vgui.Create("DNumSlider", projectiles_list_list) + projectile_max_mass_numbox:SetText("Max projectile mass") + projectile_max_mass_numbox:SetValue(GetConVar("pac_sv_projectile_max_mass"):GetInt()) + projectile_max_mass_numbox:SetMin(0) projectile_max_mass_numbox:SetDecimals(0) projectile_max_mass_numbox:SetMax(500000) + projectile_max_mass_numbox:SetSize(400,30) + projectile_max_mass_numbox:SetConVar("pac_sv_projectile_max_mass") + end + + do --damage zone + local damagezone_list = master_list:Add("Damage Zone") + damagezone_list.Header:SetSize(40,40) + damagezone_list.Header:SetFont("DermaLarge") + local damagezone_list_list = vgui.Create("DListLayout") + damagezone_list_list:DockPadding(20,0,20,20) + damagezone_list:SetContents(damagezone_list_list) + + local sv_dmgzone_box = vgui.Create("DCheckBoxLabel", damagezone_list_list) + sv_dmgzone_box:SetText("Allow damage zone") + sv_dmgzone_box:SetSize(400,30) + sv_dmgzone_box:SetConVar("pac_sv_damage_zone") + + local max_dmgzone_radius_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_radius_numbox:SetText("Max damage zone radius") + max_dmgzone_radius_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_radius"):GetInt()) + max_dmgzone_radius_numbox:SetMin(0) max_dmgzone_radius_numbox:SetDecimals(0) max_dmgzone_radius_numbox:SetMax(32767) + max_dmgzone_radius_numbox:SetSize(400,30) + max_dmgzone_radius_numbox:SetConVar("pac_sv_damage_zone_max_radius") + + local max_dmgzone_length_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_length_numbox:SetText("Max damage zone length") + max_dmgzone_length_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_length"):GetInt()) + max_dmgzone_length_numbox:SetMin(0) max_dmgzone_length_numbox:SetDecimals(0) max_dmgzone_length_numbox:SetMax(32767) + max_dmgzone_length_numbox:SetSize(400,30) + max_dmgzone_length_numbox:SetConVar("pac_sv_damage_zone_max_length") + + local max_dmgzone_damage_numbox = vgui.Create("DNumSlider", damagezone_list_list) + max_dmgzone_damage_numbox:SetText("Max damage zone damage") + max_dmgzone_damage_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_damage"):GetInt()) + max_dmgzone_damage_numbox:SetMin(0) max_dmgzone_damage_numbox:SetDecimals(0) max_dmgzone_damage_numbox:SetMax(268435455) + max_dmgzone_damage_numbox:SetSize(400,30) + max_dmgzone_damage_numbox:SetConVar("pac_sv_damage_zone_max_damage") + + local sv_dmgzone_allow_dissolve_box = vgui.Create("DCheckBoxLabel", damagezone_list_list) + sv_dmgzone_allow_dissolve_box:SetText("Allow damage entity dissolvers") + sv_dmgzone_allow_dissolve_box:SetSize(400,30) + sv_dmgzone_allow_dissolve_box:SetConVar("pac_sv_damage_zone_allow_dissolve") + + end + + do --lock part + local lock_list = master_list:Add("Lock part") + lock_list.Header:SetSize(40,40) + lock_list.Header:SetFont("DermaLarge") + local lock_list_list = vgui.Create("DListLayout") + lock_list_list:DockPadding(20,0,20,20) + lock_list:SetContents(lock_list_list) + + local sv_lock_allow_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_allow_box:SetText("Allow lock part") + sv_lock_allow_box:SetSize(400,30) + sv_lock_allow_box:SetConVar("pac_sv_lock") + + local sv_lock_grab_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_box:SetText("Allow lock part grabbing") + sv_lock_grab_box:SetSize(400,30) + sv_lock_grab_box:SetConVar("pac_sv_lock_grab") + + local sv_lock_grab_ply_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_ply_box:SetText("Allow grabbing players") + sv_lock_grab_ply_box:SetSize(400,30) + sv_lock_grab_ply_box:SetConVar("pac_sv_lock_allow_grab_ply") + + local sv_lock_grab_npc_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_npc_box:SetText("Allow grabbing NPCs") + sv_lock_grab_npc_box:SetSize(400,30) + sv_lock_grab_npc_box:SetConVar("pac_sv_lock_allow_grab_npc") + + local sv_lock_grab_ents_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_grab_ents_box:SetText("Allow grabbing other entities") + sv_lock_grab_ents_box:SetSize(400,30) + sv_lock_grab_ents_box:SetConVar("pac_sv_lock_allow_grab_ent") + + local sv_lock_teleport_box = vgui.Create("DCheckBoxLabel", lock_list_list) + sv_lock_teleport_box:SetText("Allow lock part teleportation") + sv_lock_teleport_box:SetSize(400,30) + sv_lock_teleport_box:SetConVar("pac_sv_lock_teleport") + + local max_lock_radius_numbox = vgui.Create("DNumSlider", lock_list_list) + max_lock_radius_numbox:SetText("Max lock part grab range") + max_lock_radius_numbox:SetValue(GetConVar("pac_sv_lock_max_grab_radius"):GetInt()) + max_lock_radius_numbox:SetMin(0) max_lock_radius_numbox:SetDecimals(0) max_lock_radius_numbox:SetMax(5000) + max_lock_radius_numbox:SetSize(400,30) + max_lock_radius_numbox:SetConVar("pac_sv_lock_max_grab_radius") + end + + do --force + local force_list = master_list:Add("Force part") + force_list.Header:SetSize(40,40) + force_list.Header:SetFont("DermaLarge") + local force_list_list = vgui.Create("DListLayout") + force_list_list:DockPadding(20,0,20,20) + force_list:SetContents(force_list_list) + + local sv_force_box = vgui.Create("DCheckBoxLabel", force_list_list) + sv_force_box:SetText("Allow force part") + sv_force_box:SetSize(400,30) + sv_force_box:SetConVar("pac_sv_force") + + local max_force_radius_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_radius_numbox:SetText("Max force part radius") + max_force_radius_numbox:SetValue(GetConVar("pac_sv_force_max_radius"):GetInt()) + max_force_radius_numbox:SetMin(0) max_force_radius_numbox:SetDecimals(0) max_force_radius_numbox:SetMax(32767) + max_force_radius_numbox:SetSize(400,30) + max_force_radius_numbox:SetConVar("pac_sv_force_max_radius") + + local max_force_length_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_length_numbox:SetText("Max force part length") + max_force_length_numbox:SetValue(GetConVar("pac_sv_force_max_length"):GetInt()) + max_force_length_numbox:SetMin(0) max_force_length_numbox:SetDecimals(0) max_force_length_numbox:SetMax(32767) + max_force_length_numbox:SetSize(400,30) + max_force_length_numbox:SetConVar("pac_sv_force_max_length") + + local max_force_amount_numbox = vgui.Create("DNumSlider", force_list_list) + max_force_amount_numbox:SetText("Max force part amount") + max_force_amount_numbox:SetValue(GetConVar("pac_sv_force_max_amount"):GetInt()) + max_force_amount_numbox:SetMin(0) max_force_amount_numbox:SetDecimals(0) max_force_amount_numbox:SetMax(10000000) + max_force_amount_numbox:SetSize(400,30) + max_force_amount_numbox:SetConVar("pac_sv_force_max_amount") + end + + do --health_modifier + local healthmod_list = master_list:Add("Health modifier part") + healthmod_list.Header:SetSize(40,40) + healthmod_list.Header:SetFont("DermaLarge") + local healthmod_list_list = vgui.Create("DListLayout") + healthmod_list_list:DockPadding(20,0,20,20) + healthmod_list:SetContents(healthmod_list_list) + + local sv_healthmod_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + sv_healthmod_box:SetText("Allow health modifier part") + sv_healthmod_box:SetSize(400,30) + sv_healthmod_box:SetConVar("pac_sv_health_modifier") + + local healthmod_extrabars_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + healthmod_extrabars_box:SetText("Allow changing max health and max armor") + healthmod_extrabars_box:SetSize(400,30) + healthmod_extrabars_box:SetConVar("pac_sv_health_modifier_allow_maxhp") + + local min_healthmod_dmgmult_box = vgui.Create("DNumSlider", healthmod_list_list) + min_healthmod_dmgmult_box:SetText("Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.") + min_healthmod_dmgmult_box:SetValue(GetConVar("pac_sv_health_modifier_min_damagescaling"):GetInt()) + min_healthmod_dmgmult_box:SetMin(-10) min_healthmod_dmgmult_box:SetDecimals(2) min_healthmod_dmgmult_box:SetMax(1) + min_healthmod_dmgmult_box:SetSize(400,30) + min_healthmod_dmgmult_box:SetConVar("pac_sv_health_modifier_min_damagescaling") + + local healthmod_extrabars_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) + healthmod_extrabars_box:SetText("Allow extra healthbars") + healthmod_extrabars_box:SetSize(400,30) + healthmod_extrabars_box:SetConVar("pac_sv_health_modifier_extra_bars") + healthmod_extrabars_box:SetToolTip("What are those? It's like an armor layer that takes damage before it gets applied to the entity.") + end + return master_list +end + +function pace.FillServerSettings(pnl) + local pnl = pnl + + local master_list = vgui.Create("DCategoryList", pnl) + master_list:Dock(FILL) + + --models/entity + --[[ + pac_allow_blood_color + pac_allow_mdl + pac_allow_mdl_entity + pac_modifier_model + pac_modifier_size + ]] + + local model_category = master_list:Add("Allowed Playermodel Mutations") + model_category.Header:SetSize(40,40) + model_category.Header:SetFont("DermaLarge") + local model_category_list = vgui.Create("DListLayout") + model_category_list:DockPadding(20,0,20,20) + model_category:SetContents(model_category_list) + + local pac_allow_blood_color_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_blood_color_box:SetText("Blood") + pac_allow_blood_color_box:SetSize(400,30) + pac_allow_blood_color_box:SetConVar("pac_allow_blood_color") + model_category_list:Add(pac_allow_blood_color_box) + local pac_allow_mdl_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_mdl_box:SetText("MDL") + pac_allow_mdl_box:SetSize(400,30) + pac_allow_mdl_box:SetConVar("pac_allow_mdl") + model_category_list:Add(pac_allow_mdl_box) + local pac_allow_mdl_entity_box = vgui.Create("DCheckBoxLabel", master_list) + pac_allow_mdl_entity_box:SetText("Entity MDL") + pac_allow_mdl_entity_box:SetSize(400,30) + pac_allow_mdl_entity_box:SetConVar("pac_allow_mdl_entity") + model_category_list:Add(pac_allow_mdl_entity_box) + local pac_modifier_model_box = vgui.Create("DCheckBoxLabel", master_list) + pac_modifier_model_box:SetText("Entity model") + pac_modifier_model_box:SetSize(400,30) + pac_modifier_model_box:SetConVar("pac_modifier_model") + model_category_list:Add(pac_modifier_model_box) + local pac_modifier_size_box = vgui.Create("DCheckBoxLabel", master_list) + pac_modifier_size_box:SetText("Entity size") + pac_modifier_size_box:SetSize(400,30) + pac_modifier_size_box:SetConVar("pac_modifier_size") + model_category_list:Add(pac_modifier_size_box) + + --movement and mass + --[[ + pac_free_movement + ]] + + local movement_category = master_list:Add("Player Movement") + movement_category.Header:SetSize(40,40) + movement_category.Header:SetFont("DermaLarge") + local movement_category_list = vgui.Create("DListLayout") + movement_category_list:DockPadding(20,20,20,20) + movement_category:SetContents(movement_category_list) + + local pac_allow_movement_form = vgui.Create("DComboBox", movement_category_list) + pac_allow_movement_form:SetText("Allow PAC player movement") + --pac_allow_movement_form:SetSize(400,20) + pac_allow_movement_form:SetSortItems(false) + + pac_allow_movement_form:AddChoice("disabled") + pac_allow_movement_form:AddChoice("disabled if noclip not allowed") + pac_allow_movement_form:AddChoice("enabled") + + pac_allow_movement_form.OnSelect = function(_, _, value) + if value == "disabled" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("0") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is disabled.") + elseif value == "disabled if noclip not allowed" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("-1") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is disabled if noclip is not allowed.") + elseif value == "enabled" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("1") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is enabled.") + end + end + + --mode:ChooseOption(mode_str) + + local pac_player_movement_allow_mass_box = vgui.Create("DCheckBoxLabel", movement_category_list) + pac_player_movement_allow_mass_box:SetText("Allow Modify Mass") + pac_player_movement_allow_mass_box:SetSize(400,30) + movement_category_list:Add(pac_player_movement_allow_mass_box) + pac_player_movement_allow_mass_box:SetConVar("pac_player_movement_allow_mass") + + local playermovement_min_mass_numbox = vgui.Create("DNumSlider", movement_category_list) + playermovement_min_mass_numbox:SetText("Mimnimum mass players can set for themselves") + playermovement_min_mass_numbox:SetValue(GetConVar("pac_player_movement_min_mass"):GetFloat()) + playermovement_min_mass_numbox:SetMin(0.01) playermovement_min_mass_numbox:SetDecimals(0) playermovement_min_mass_numbox:SetMax(1000000) + playermovement_min_mass_numbox:SetSize(400,30) + movement_category_list:Add(playermovement_min_mass_numbox) + playermovement_min_mass_numbox:SetConVar("pac_player_movement_min_mass") + + + local playermovement_max_mass_numbox = vgui.Create("DNumSlider", movement_category_list) + playermovement_max_mass_numbox:SetText("Maximum mass players can set for themselves") + playermovement_max_mass_numbox:SetValue(GetConVar("pac_player_movement_max_mass"):GetFloat()) + playermovement_max_mass_numbox:SetMin(0.01) playermovement_max_mass_numbox:SetDecimals(0) playermovement_max_mass_numbox:SetMax(1000000) + playermovement_max_mass_numbox:SetSize(400,30) + movement_category_list:Add(playermovement_max_mass_numbox) + playermovement_max_mass_numbox:SetConVar("pac_player_movement_max_mass") + + + local pac_player_movement_allow_mass_dmgscaling_box = vgui.Create("DCheckBoxLabel", movement_category_list) + pac_player_movement_allow_mass_dmgscaling_box:SetText("Allow damage scaling of physics damage based on player's mass") + pac_player_movement_allow_mass_dmgscaling_box:SetSize(400,30) + movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) + pac_player_movement_allow_mass_dmgscaling_box:SetConVar("pac_player_movement_physics_damage_scaling") + movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) + + + --wear limits and bans + --[[ + pac_sv_draw_distance + pac_sv_hide_outfit_on_death WORKSHOP DEPRECATED + pac_submit_limit + pac_submit_spam + pac_ban + pac_unban + ]] + + local wear_list = master_list:Add("Server wearing/drawing") + wear_list.Header:SetSize(40,40) + wear_list.Header:SetFont("DermaLarge") + local draw_distance_list = vgui.Create("DListLayout") + draw_distance_list:DockPadding(20,0,20,20) + wear_list:SetContents(draw_distance_list) + + local draw_dist_numbox = vgui.Create("DNumSlider", draw_distance_list) + draw_dist_numbox:SetText("Server draw distance") + draw_dist_numbox:SetValue(GetConVar("pac_sv_draw_distance"):GetInt()) + draw_dist_numbox:SetMin(0) draw_dist_numbox:SetDecimals(0) draw_dist_numbox:SetMax(50000) + draw_dist_numbox:SetSize(400,30) + draw_dist_numbox:SetConVar("pac_sv_draw_distance") + + local pac_submit_limit_numbox = vgui.Create("DNumSlider", draw_distance_list) + pac_submit_limit_numbox:SetText("pac_submit limit") + pac_submit_limit_numbox:SetValue(GetConVar("pac_submit_limit"):GetInt()) + pac_submit_limit_numbox:SetMin(0) pac_submit_limit_numbox:SetDecimals(0) pac_submit_limit_numbox:SetMax(100) + pac_submit_limit_numbox:SetSize(400,30) + pac_submit_limit_numbox:SetConVar("pac_submit_limit") + + local pac_submit_spam_box = vgui.Create("DCheckBoxLabel", draw_distance_list) + pac_submit_spam_box:SetText("prevent pac_submit spam") + pac_submit_spam_box:SetSize(400,30) + pac_submit_spam_box:SetConVar("pac_submit_spam") + + + + --misc + --[[ + sv_pac_webcontent_allow_no_content_length + sv_pac_webcontent_limit + pac_to_contraption_allow + pac_max_contraption_entities + pac_restrictions + ]] + local misc_list = master_list:Add("Misc") + misc_list.Header:SetSize(40,40) + misc_list.Header:SetFont("DermaLarge") + local misc_list_list = vgui.Create("DListLayout") + misc_list_list:DockPadding(20,0,20,20) + misc_list:SetContents(misc_list_list) + local webcontent_no_content_box = vgui.Create("DCheckBoxLabel", misc_list_list) + webcontent_no_content_box:SetText("allow downloads with no content length") + webcontent_no_content_box:SetSize(400,30) + webcontent_no_content_box:SetConVar("sv_pac_webcontent_allow_no_content_length") + + local contraption_box = vgui.Create("DCheckBoxLabel", misc_list_list) + contraption_box:SetText("allow contraptions") + contraption_box:SetSize(400,30) + contraption_box:SetConVar("pac_to_contraption_allow") + + local contraption_entities_numbox = vgui.Create("DNumSlider", misc_list_list) + contraption_entities_numbox:SetText("PAC3 contraption entities limit") + contraption_entities_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) + contraption_entities_numbox:SetMin(0) contraption_entities_numbox:SetDecimals(0) contraption_entities_numbox:SetMax(200) + contraption_entities_numbox:SetSize(400,30) + contraption_entities_numbox:SetConVar("pac_max_contraption_entities") + + local cam_restrict_box = vgui.Create("DCheckBoxLabel", misc_list_list) + cam_restrict_box:SetText("restrict PAC editor camera movement") + cam_restrict_box:SetSize(400,30) + cam_restrict_box:SetConVar("pac_restrictions") + + + return master_list +end + + +--part order, shortcuts +function pace.FillEditorSettings(pnl) + + local buildlist_partmenu = {} + local f = vgui.Create( "DPanel", pnl ) + f:SetSize(800) + f:Center() + + local LeftPanel = vgui.Create( "DPanel", f ) -- Can be any panel, it will be stretched + + local partmenu_order_presets = vgui.Create("DComboBox",LeftPanel) + partmenu_order_presets:SetText("Select a part menu preset") + partmenu_order_presets:AddChoice("factory preset") + partmenu_order_presets:AddChoice("expanded PAC4.5 preset") + partmenu_order_presets:AddChoice("bulk select poweruser") + partmenu_order_presets:AddChoice("user preset") + partmenu_order_presets:SetX(10) partmenu_order_presets:SetY(10) + partmenu_order_presets:SetWidth(200) + partmenu_order_presets:SetHeight(20) + + local partmenu_apply_button = vgui.Create("DButton", LeftPanel) + partmenu_apply_button:SetText("Apply") + partmenu_apply_button:SetX(220) + partmenu_apply_button:SetY(10) + partmenu_apply_button:SetWidth(65) + partmenu_apply_button:SetImage('icon16/accept.png') + + local partmenu_clearlist_button = vgui.Create("DButton", LeftPanel) + partmenu_clearlist_button:SetText("Clear") + partmenu_clearlist_button:SetX(285) + partmenu_clearlist_button:SetY(10) + partmenu_clearlist_button:SetWidth(65) + partmenu_clearlist_button:SetImage('icon16/application_delete.png') + + local partmenu_savelist_button = vgui.Create("DButton", LeftPanel) + partmenu_savelist_button:SetText("Save") + partmenu_savelist_button:SetX(350) + partmenu_savelist_button:SetY(10) + partmenu_savelist_button:SetWidth(70) + partmenu_savelist_button:SetImage('icon16/disk.png') + + + + local partmenu_choices = vgui.Create("DScrollPanel", LeftPanel) + local partmenu_choices_textAdd = vgui.Create("DLabel", LeftPanel) + partmenu_choices_textAdd:SetText("ADD MENU COMPONENTS") + partmenu_choices_textAdd:SetFont("DermaDefaultBold") + partmenu_choices_textAdd:SetColor(Color(0,200,0)) + partmenu_choices_textAdd:SetWidth(200) + partmenu_choices_textAdd:SetX(10) + partmenu_choices_textAdd:SetY(30) + + local partmenu_choices_textRemove = vgui.Create("DLabel", LeftPanel) + partmenu_choices_textRemove:SetText("DOUBLE CLICK TO REMOVE") + partmenu_choices_textRemove:SetColor(Color(200,0,0)) + partmenu_choices_textRemove:SetFont("DermaDefaultBold") + partmenu_choices_textRemove:SetWidth(200) + partmenu_choices_textRemove:SetX(220) + partmenu_choices_textRemove:SetY(30) + + local partmenu_previews = vgui.Create("DListView", LeftPanel) + partmenu_previews:AddColumn("index") + partmenu_previews:AddColumn("control name") + partmenu_previews:SetSortable(false) + partmenu_previews:SetX(220) + partmenu_previews:SetY(50) + partmenu_previews:SetHeight(320) + partmenu_previews:SetWidth(200) + + + + local shortcutaction_choices = vgui.Create("DComboBox", LeftPanel) + shortcutaction_choices:SetText("Select a PAC action") + for _,name in ipairs(pace.PACActionShortcut_Dictionary) do + shortcutaction_choices:AddChoice(name) + end + shortcutaction_choices:SetX(10) shortcutaction_choices:SetY(400) + shortcutaction_choices:SetWidth(170) + shortcutaction_choices:SetHeight(20) + shortcutaction_choices:ChooseOptionID(1) + + function shortcutaction_choices:Think() + self.next = self.next or 0 + self.found = self.found or false + if self.next < RealTime() then self.found = false end + if self:IsHovered() then + if input.IsKeyDown(KEY_UP) then + if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() + 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end + elseif input.IsKeyDown(KEY_DOWN) then + if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() - 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end + else self.found = false end + else self.found = false + end + end + + local shortcuts_description_text = vgui.Create("DLabel", LeftPanel) + shortcuts_description_text:SetFont("DermaDefaultBold") + shortcuts_description_text:SetText("Edit keyboard shortcuts") + shortcuts_description_text:SetColor(Color(0,0,0)) + shortcuts_description_text:SetWidth(200) + shortcuts_description_text:SetX(10) + shortcuts_description_text:SetY(380) + + local shortcutaction_presets = vgui.Create("DComboBox", LeftPanel) + shortcutaction_presets:SetText("Select a shortcut preset") + shortcutaction_presets:AddChoice("factory preset", pace.PACActionShortcut_Default) + shortcutaction_presets:AddChoice("no CTRL preset", pace.PACActionShortcut_NoCTRL) + shortcutaction_presets:AddChoice("Cedric's preset", pace.PACActionShortcut_Cedric) + + for i,filename in ipairs(file.Find("pac3_config/pac_editor_shortcuts*.txt","DATA")) do + local data = file.Read("pac3_config/" .. filename, "DATA") + shortcutaction_presets:AddChoice(string.GetFileFromFilename(filename), util.KeyValuesToTable(data)) + end + + shortcutaction_presets:SetX(10) shortcutaction_presets:SetY(420) + shortcutaction_presets:SetWidth(170) + shortcutaction_presets:SetHeight(20) + function shortcutaction_presets:OnSelect(num, name, data) + pace.PACActionShortcut = data + pace.FlashNotification("Selected shortcut preset: " .. name .. ". View console for more info") + pac.Message("Selected shortcut preset: " .. name) + for i,v in pairs(data) do + if #v > 0 then MsgC(Color(50,250,50), i .. "\n") end + for i2,v2 in pairs(v) do + MsgC(Color(0,250,250), "\t" .. table.concat(v2, "+") .. "\n") + end + end + end + + + local shortcutaction_choices_textCurrentShortcut = vgui.Create("DLabel", LeftPanel) + shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit:") + shortcutaction_choices_textCurrentShortcut:SetColor(Color(0,60,160)) + shortcutaction_choices_textCurrentShortcut:SetWidth(200) + shortcutaction_choices_textCurrentShortcut:SetX(200) + shortcutaction_choices_textCurrentShortcut:SetY(420) + + + local shortcutaction_index = vgui.Create("DNumberWang", LeftPanel) + shortcutaction_index:SetToolTip("index") + shortcutaction_index:SetValue(1) + shortcutaction_index:SetMin(1) + shortcutaction_index:SetMax(10) + shortcutaction_index:SetWidth(30) + shortcutaction_index:SetHeight(20) + shortcutaction_index:SetX(180) + shortcutaction_index:SetY(400) + + local function update_shortcutaction_choices_textCurrentShortcut(num) + shortcutaction_choices_textCurrentShortcut:SetText("") + num = tonumber(num) + local action, val = shortcutaction_choices:GetSelected() + local strs = {} + + if action and action ~= "" then + if pace.PACActionShortcut[action] and pace.PACActionShortcut[action][num] then + for i,v in ipairs(pace.PACActionShortcut[action][num]) do + strs[i] = v + end + shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit: " .. table.concat(strs, " + ")) + else + shortcutaction_choices_textCurrentShortcut:SetText("") + end + end + end + update_shortcutaction_choices_textCurrentShortcut(1) + + function shortcutaction_index:OnValueChanged(num) + update_shortcutaction_choices_textCurrentShortcut(num) + end + + function shortcutaction_choices:OnSelect(i, action) + shortcutaction_index:OnValueChanged(shortcutaction_index:GetValue()) + end + + local binder1 = vgui.Create("DBinder", LeftPanel) + binder1:SetX(10) + binder1:SetY(440) + binder1:SetHeight(30) + binder1:SetWidth(90) + function binder1:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 1: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 1: "..input.GetKeyName( num )) + end + + local binder2 = vgui.Create("DBinder", LeftPanel) + binder2:SetX(105) + binder2:SetY(440) + binder2:SetHeight(30) + binder2:SetWidth(90) + function binder2:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 2: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 2: "..input.GetKeyName( num )) + end + + local binder3 = vgui.Create("DBinder", LeftPanel) + binder3:SetX(200) + binder3:SetY(440) + binder3:SetHeight(30) + binder3:SetWidth(90) + function binder3:OnChange( num ) + if not num or num == 0 then return end + if not input.GetKeyName( num ) then return end + LocalPlayer():ChatPrint("New bound key 3: "..input.GetKeyName( num )) + pace.FlashNotification("New bound key 3: "..input.GetKeyName( num )) + end + + local function send_active_shortcut_to_assign(tbl) + local action = shortcutaction_choices:GetValue() + local index = shortcutaction_index:GetValue() + + if not tbl then + pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {} + pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {} + + if table.IsEmpty(pace.PACActionShortcut[action][index]) then + pace.PACActionShortcut[action][index] = nil + if table.IsEmpty(pace.PACActionShortcut[action]) then + pace.PACActionShortcut[action] = nil + end + else + pace.PACActionShortcut[action][index] = nil + end + elseif not table.IsEmpty(tbl) then + pace.AssignEditorShortcut(shortcutaction_choices:GetValue(), tbl, shortcutaction_index:GetValue()) + end + encode_table_to_file("pac_editor_shortcuts") + end + + local bindclear = vgui.Create("DButton", LeftPanel) + bindclear:SetText("clear") + bindclear:SetTooltip("deletes the current shortcut at the current index") + bindclear:SetX(10) + bindclear:SetY(480) + bindclear:SetHeight(30) + bindclear:SetWidth(90) + bindclear:SetColor(Color(200,0,0)) + bindclear:SetIcon("icon16/keyboard_delete.png") + function bindclear:DoClick() + binder1:SetSelectedNumber(0) + binder2:SetSelectedNumber(0) + binder3:SetSelectedNumber(0) + send_active_shortcut_to_assign() + update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue()) + end + + local bindoverwrite = vgui.Create("DButton", LeftPanel) + bindoverwrite:SetText("confirm") + bindoverwrite:SetTooltip("applies the current shortcut combination at the current index") + bindoverwrite:SetX(105) + bindoverwrite:SetY(480) + bindoverwrite:SetHeight(30) + bindoverwrite:SetWidth(90) + bindoverwrite:SetColor(Color(0,200,0)) + bindoverwrite:SetIcon("icon16/disk.png") + function bindoverwrite:DoClick() + local tbl = {} + local i = 1 + --print(binder1:GetValue(), binder2:GetValue(), binder3:GetValue()) + if binder1:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder1:GetValue()) i = i + 1 end + if binder2:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder2:GetValue()) i = i + 1 end + if binder3:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder3:GetValue()) end + if not table.IsEmpty(tbl) then + pace.FlashNotification("Combo " .. shortcutaction_index:GetValue() .. " committed: " .. table.concat(tbl," ")) + if not pace.PACActionShortcut[shortcutaction_choices:GetValue()] then + pace.PACActionShortcut[shortcutaction_choices:GetValue()] = {} + end + send_active_shortcut_to_assign(tbl) + update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue()) + end + encode_table_to_file("pac_editor_shortcuts") + end + + function bindoverwrite:DoRightClick() + Derma_StringRequest("Save preset", "Save a keyboard shortcuts preset?", "pac_editor_shortcuts", + function(name) file.Write("pac3_config/"..name..".txt", util.TableToKeyValues(pace.PACActionShortcut)) + shortcutaction_presets:AddChoice(name..".txt") + end + ) + end + + local bindcapture_text = vgui.Create("DLabel", LeftPanel) + bindcapture_text:SetFont("DermaDefaultBold") + bindcapture_text:SetText("") + bindcapture_text:SetColor(Color(0,0,0)) + bindcapture_text:SetX(300) + bindcapture_text:SetY(480) + bindcapture_text:SetSize(300, 30) + + function bindcapture_text:Think() + self:SetText(pace.bindcapturelabel_text) + end + local bindcapture = vgui.Create("DButton", LeftPanel) + bindcapture:SetText("capture input") + bindcapture:SetX(200) + bindcapture:SetY(480) + bindcapture:SetHeight(30) + bindcapture:SetWidth(90) + pace.bindcapturelabel_text = "" + function bindcapture:DoClick() + pace.delayshortcuts = RealTime() + 5 + local input_active = {} + local no_input = true + local inputs_str = "" + local previous_inputs_str = "" + pace.FlashNotification("Recording input... Release one key when you're done") + + hook.Add("Tick", "pace_buttoncapture_countdown", function() + pace.delayshortcuts = RealTime() + 5 + local inputs_tbl = {} + inputs_str = "" + for i=1,172,1 do --build bool list of all current keys + if input.IsKeyDown(i) then + input_active[i] = true + inputs_tbl[i] = true + no_input = false + inputs_str = inputs_str .. input.GetKeyName(i) .. " " + else + input_active[i] = false + end + end + pace.bindcapturelabel_text = "Recording input:\n" .. inputs_str + + if previous_inputs_tbl and table.Count(previous_inputs_tbl) > 0 then + if table.Count(inputs_tbl) < table.Count(previous_inputs_tbl) then + pace.FlashNotification("ending input!" .. previous_inputs_str) + + local tbl = {} + local i = 1 + for key,bool in pairs(previous_inputs_tbl) do + tbl[i] = input.GetKeyName(key) + i = i + 1 + end + --print(shortcutaction_choices:GetValue(), shortcutaction_index:GetValue()) + pace.AssignEditorShortcut(shortcutaction_choices:GetValue(), tbl, shortcutaction_index:GetValue()) + --pace.PACActionShortcut[shortcutaction_choices:GetValue()][shortcutaction_index:GetValue()] = tbl + pace.delayshortcuts = RealTime() + 5 + pace.bindcapturelabel_text = "Recorded input:\n" .. previous_inputs_str + hook.Remove("Tick", "pace_buttoncapture_countdown") + end + end + previous_inputs_str = inputs_str + previous_inputs_tbl = inputs_tbl + end) + + end + + local bulkbinder = vgui.Create("DBinder", LeftPanel) + function bulkbinder:OnChange( num ) + GetConVar("pac_bulk_select_key"):SetString(input.GetKeyName( num )) + end + bulkbinder:SetX(210) + bulkbinder:SetY(400) + bulkbinder:SetSize(80,20) + bulkbinder:SetText("bulk select key") + + local function ClearPartMenuPreviewList() + local i = 0 + while (partmenu_previews:GetLine(i + 1) ~= nil) do + i = i+1 + end + for v=i,0,-1 do + if partmenu_previews:GetLine(v) ~= nil then partmenu_previews:RemoveLine(v) end + v = v - 1 + 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' + 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)) + + function pnl:DoClick() + table.insert(buildlist_partmenu,v) + partmenu_previews:AddLine(#buildlist_partmenu,v) + end + partmenu_choices:AddItem(pnl) + pnl:SetHeight(18) + pnl:SetWidth(200) + pnl:SetY(20*(i-1)) + end + + partmenu_choices:SetWidth(200) + partmenu_choices:SetHeight(320) + partmenu_choices:SetVerticalScrollbarEnabled(true) + + + local RightPanel = vgui.Create( "DTree", f ) + Test_Node = RightPanel:AddNode( "Test", "icon16/world.png" ) + test_part = pac.CreatePart("base") //the menu needs a part to get its full version in preview + function RightPanel:DoRightClick() + temp_list = pace.operations_order + pace.operations_order = buildlist_partmenu + pace.OnPartMenu(test_part) + temp_list = pace.operations_order + pace.operations_order = temp_list + end + function RightPanel:DoClick() + temp_list = pace.operations_order + pace.operations_order = buildlist_partmenu + pace.OnPartMenu(test_part) + temp_list = pace.operations_order + pace.operations_order = temp_list + end + test_part:Remove() //dumb workaround but it works + + + local div = vgui.Create( "DHorizontalDivider", f ) + div:Dock( FILL ) + div:SetLeft( LeftPanel ) + div:SetRight( RightPanel ) + + div:SetDividerWidth( 8 ) + div:SetLeftMin( 50 ) + div:SetRightMin( 50 ) + div:SetLeftWidth( 450 ) + partmenu_order_presets.OnSelect = function( self, index, value ) + local temp_list = {"wear","save","load"} + if value == "factory preset" then + temp_list = table.Copy(pace.operations_default) + elseif value == "expanded PAC4.5 preset" then + temp_list = table.Copy(pace.operations_experimental) + elseif value == "bulk select poweruser" then + temp_list = table.Copy(pace.operations_bulk_poweruser) + elseif value == "user preset" then + temp_list = pace.operations_order + end + ClearPartMenuPreviewList() + for i,v in ipairs(temp_list) do + partmenu_previews:AddLine(i,v) + end + buildlist_partmenu = temp_list + end + + function partmenu_apply_button:DoClick() + pace.operations_order = buildlist_partmenu + end + + function partmenu_clearlist_button:DoClick() + ClearPartMenuPreviewList() + buildlist_partmenu = {} + end + + function partmenu_savelist_button:DoClick() + encode_table_to_file("pac_editor_partmenu_layouts") + end + + function partmenu_previews:DoDoubleClick(id, line) + table.remove(buildlist_partmenu,id) + + ClearPartMenuPreviewList() + for i,v in ipairs(buildlist_partmenu) do + partmenu_previews:AddLine(i,v) + end + + PrintTable(buildlist_partmenu) + end + + + if pace.operations_order then + for i,v in pairs(pace.operations_order) do + table.insert(buildlist_partmenu,v) + partmenu_previews:AddLine(#buildlist_partmenu,v) + end + end + + return f +end + +--camera movement +function pace.FillEditorSettings2(pnl) + local panel = vgui.Create( "DPanel", pnl ) + --[[ movement binds + CreateConVar("pac_editor_camera_forward_bind", "w") + + CreateConVar("pac_editor_camera_back_bind", "s") + + CreateConVar("pac_editor_camera_moveleft_bind", "a") + + CreateConVar("pac_editor_camera_moveright_bind", "d") + + CreateConVar("pac_editor_camera_up_bind", "space") + + CreateConVar("pac_editor_camera_down_bind", "") + + ]] + + --[[pace.camera_movement_binds = { + ["forward"] = pace.camera_forward_bind, + ["back"] = pace.camera_back_bind, + ["moveleft"] = pace.camera_moveleft_bind, + ["moveright"] = pace.camera_moveright_bind, + ["up"] = pace.camera_up_bind, + ["down"] = pace.camera_down_bind, + ["slow"] = pace.camera_slow_bind, + ["speed"] = pace.camera_speed_bind + } + ]] + + local LeftPanel = vgui.Create( "DPanel", panel ) -- Can be any panel, it will be stretched + local RightPanel = vgui.Create( "DPanel", panel ) -- Can be any panel, it will be stretched + LeftPanel:SetSize(300,600) + RightPanel:SetSize(300,600) + local div = vgui.Create( "DHorizontalDivider", panel ) + div:Dock( FILL ) + div:SetLeft( LeftPanel ) + div:SetRight( RightPanel ) + + div:SetDividerWidth( 8 ) + div:SetLeftMin( 50 ) + div:SetRightMin( 50 ) + div:SetLeftWidth( 400 ) + + local movement_binders_label = vgui.Create("DLabel", LeftPanel) + movement_binders_label:SetText("PAC editor camera movement") + movement_binders_label:SetFont("DermaDefaultBold") + movement_binders_label:SetColor(Color(0,0,0)) + movement_binders_label:SetSize(200,40) + movement_binders_label:SetPos(30,5) + + local forward_binder = vgui.Create("DBinder", LeftPanel) + forward_binder:SetSize(40,40) + forward_binder:SetPos(100,40) + forward_binder:SetTooltip("move forward") + forward_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["forward"]:GetString())) + function forward_binder:OnChange(num) + pace.camera_movement_binds["forward"]:SetString(input.GetKeyName( num )) + end + + local back_binder = vgui.Create("DBinder", LeftPanel) + back_binder:SetSize(40,40) + back_binder:SetPos(100,80) + back_binder:SetTooltip("move back") + back_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["back"]:GetString())) + function back_binder:OnChange(num) + pace.camera_movement_binds["back"]:SetString(input.GetKeyName( num )) + end + + local moveleft_binder = vgui.Create("DBinder", LeftPanel) + moveleft_binder:SetSize(40,40) + moveleft_binder:SetPos(60,80) + moveleft_binder:SetTooltip("move left") + moveleft_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveleft"]:GetString())) + function moveleft_binder:OnChange(num) + pace.camera_movement_binds["moveleft"]:SetString(input.GetKeyName( num )) + end + + local moveright_binder = vgui.Create("DBinder", LeftPanel) + moveright_binder:SetSize(40,40) + moveright_binder:SetPos(140,80) + moveright_binder:SetTooltip("move right") + moveright_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveright"]:GetString())) + function moveright_binder:OnChange(num) + pace.camera_movement_binds["moveright"]:SetString(input.GetKeyName( num )) + end + + local up_binder = vgui.Create("DBinder", LeftPanel) + up_binder:SetSize(40,40) + up_binder:SetPos(180,40) + up_binder:SetTooltip("move up") + up_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["up"]:GetString())) + function up_binder:OnChange(num) + pace.camera_movement_binds["up"]:SetString(input.GetKeyName( num )) + end + + local down_binder = vgui.Create("DBinder", LeftPanel) + down_binder:SetSize(40,40) + down_binder:SetPos(180,80) + down_binder:SetTooltip("move down") + down_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["down"]:GetString())) + function down_binder:OnChange(num) + print(num, input.GetKeyName( num )) + pace.camera_movement_binds["down"]:SetString(input.GetKeyName( num )) + end + + local slow_binder = vgui.Create("DBinder", LeftPanel) + slow_binder:SetSize(40,40) + slow_binder:SetPos(20,80) + slow_binder:SetTooltip("go slow") + slow_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["slow"]:GetString())) + function slow_binder:OnChange(num) + pace.camera_movement_binds["slow"]:SetString(input.GetKeyName( num )) + end + + local speed_binder = vgui.Create("DBinder", LeftPanel) + speed_binder:SetSize(40,40) + speed_binder:SetPos(20,40) + speed_binder:SetTooltip("go fast") + speed_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["speed"]:GetString())) + function speed_binder:OnChange(num) + pace.camera_movement_binds["speed"]:SetString(input.GetKeyName( num )) + end + + --[[pace.partmenu_categories_cedrics = + { + ["new!"] = + { + ["icon"] = "icon16/new.png", + ["interpolated_multibone"]= "interpolated_multibone", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["lock"] = "lock", + ["force"] = "force", + ["health_modifier"] = "health_modifier", + }, + ["logic"] = + { + ["icon"] = "icon16/server_chart.png", + ["proxy"] = "proxy", + ["command"] = "command", + ["event"] = "event", + ["text"] = "text", + ["link"] = "link", + },]] + local Parts = pac.GetRegisteredParts() + local function get_icon(str, fallback) + if str then + if pace.MiscIcons[string.gsub(str, "pace.MiscIcons.", "")] then + return pace.MiscIcons[string.gsub(str, "pace.MiscIcons.", "")] + else + local img = string.gsub(str, ".png", "") --remove the png extension + img = string.gsub(img, "icon16/", "") --remove the icon16 base path + img = "icon16/" .. img .. ".png" --why do this? to be able to write any form and let the program fix the form + return img + end + elseif Parts[fallback] then + return Parts[fallback].Icon + else + return "icon16/page_white.png" + end + + end + + local categorytree = vgui.Create("DTree", RightPanel) + categorytree:SetY(30) + categorytree:SetSize(360,400) + + local function class_partnode_add(parentnode, class) + if Parts[class] then + for i,v in ipairs(parentnode:GetChildNodes()) do --can't make duplicates so remove to place it at the end + if v:GetText() == class then v:Remove() end + end + + local part_node = parentnode:AddNode(class) + part_node:SetIcon(get_icon(nil, class)) + part_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("remove", function() part_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + end + end + local function bring_up_partclass_list(cat_node) + + --function from pace.OnAddPartMenu(obj) + local base = vgui.Create("EditablePanel") + base:SetPos(input.GetCursorPos()) + base:SetSize(200, 300) + + base:MakePopup() + + function base:OnRemove() + pac.RemoveHook("VGUIMousePressed", "search_part_menu") + end + + local edit = base:Add("DTextEntry") + edit:SetTall(20) + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + + local result = base:Add("DScrollPanel") + result:Dock(FILL) + + function edit:OnEnter() + if result.found[1] then + class_partnode_add(cat_node, result.found[1].ClassName) + end + base:Remove() + end + + edit.OnValueChange = function(_, str) + result:Clear() + result.found = {} + + for _, part in ipairs(pace.GetRegisteredParts()) do + if (part.FriendlyName or part.ClassName):find(str, nil, true) then + table.insert(result.found, part) + end + end + + table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end) + + for _, part in ipairs(result.found) do + local line = result:Add("DButton") + line:SetText("") + line:SetTall(20) + line.DoClick = function() + class_partnode_add(cat_node, part.ClassName) + end + + local btn = line:Add("DImageButton") + btn:SetSize(16, 16) + btn:SetPos(4,0) + btn:CenterVertical() + btn:SetMouseInputEnabled(false) + if part.Icon then + btn:SetImage(part.Icon) + end + + local label = line:Add("DLabel") + label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) + label:SetText((part.FriendlyName or part.ClassName):Replace('_', ' ')) + label:SizeToContents() + label:MoveRightOf(btn, 4) + label:SetMouseInputEnabled(false) + label:CenterVertical() + + line:Dock(TOP) + end + + --base:SetHeight(20 * #result.found + edit:GetTall()) + base:SetHeight(600 + edit:GetTall()) + + end + + edit:OnValueChange("") + + pac.AddHook("VGUIMousePressed", "search_part_menu", function(pnl, code) + if code == MOUSE_LEFT or code == MOUSE_RIGHT then + if not base:IsOurChild(pnl) then + base:Remove() + end + end + end) + end + local function bring_up_category_icon_browser(category_node) + local master_frame = vgui.Create("DFrame") + master_frame:SetPos(input.GetCursorPos()) + master_frame:SetSize(400,400) + + local browser = vgui.Create("DIconBrowser", master_frame) + function browser:OnChange() + category_node:SetIcon(self:GetSelectedIcon()) + end + browser:SetSize(400,380) + browser:SetPos(0,40) + + local frame = vgui.Create("EditablePanel", master_frame) + local edit = vgui.Create("DTextEntry", frame) + frame:SetSize(300,20) + function browser:Think() + if not IsValid(category_node) then master_frame:Remove() end + x = master_frame:GetX() + y = master_frame:GetY() + frame:SetPos(x,y+20) + frame:MakePopup() + end + + function edit:OnValueChange(value) + browser:FilterByText( value ) + end + master_frame:MakePopup() + frame:MakePopup() + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + local function bring_up_tooltip_edit(category_node) + local frame = vgui.Create("EditablePanel") + local edit = vgui.Create("DTextEntry", frame) + function edit:OnEnter(value) + category_node:SetTooltip(value) + frame:Remove() + end + function frame:Think() + if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end + end + frame:MakePopup() + + frame:SetSize(300,30) + frame:SetPos(input.GetCursorPos()) + + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + local function bring_up_name_edit(category_node) + local frame = vgui.Create("EditablePanel") + local edit = vgui.Create("DTextEntry", frame) + edit:SetText(category_node:GetText()) + function edit:OnEnter(value) + category_node:SetText(value) + frame:Remove() + end + function frame:Think() + if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end + end + frame:MakePopup() + + frame:SetSize(300,30) + frame:SetPos(category_node.Label:LocalToScreen(category_node.Label:GetPos())) + + edit:Dock(TOP) + edit:RequestFocus() + edit:SetUpdateOnType(true) + end + + local function load_partgroup_template_into_tree(categorytree, tbl) + tbl = tbl or pace.partgroups or pace.partmenu_categories_default + categorytree:Clear() + for category,category_contents in pairs(tbl) do + + local category_node = categorytree:AddNode(category) + category_node:SetIcon(get_icon(category_contents.icon, category)) + + category_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("insert part in category", function() bring_up_partclass_list(category_node) end):SetImage("icon16/add.png") + menu:AddOption("select icon", function() bring_up_category_icon_browser(category_node) end):SetImage("icon16/picture.png") + menu:AddOption("write a tooltip", function() bring_up_tooltip_edit(category_node) end):SetImage("icon16/comment.png") + menu:AddOption("rename this category", function() bring_up_name_edit(category_node) end):SetImage("icon16/textfield_rename.png") + menu:AddOption("remove this category", function() category_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + + if category_contents["tooltip"] then + category_node:SetTooltip(category_contents["tooltip"]) + end + + for field,value in pairs(category_contents) do + if Parts[field] then + class_partnode_add(category_node, field) + end + end + end + end + + local function extract_partgroup_template_from_tree(categorytree) + local tbl = {} + for i,category_node in ipairs(categorytree:Root():GetChildNodes()) do + tbl[category_node:GetText()] = {} + --print(i,category_node:GetText(),category_node.Label:GetTooltip(), category_node:GetIcon()) + if category_node:GetTooltip() ~= nil and category_node:GetTooltip() ~= "" then tbl[category_node:GetText()]["tooltip"] = category_node:GetTooltip() end + tbl[category_node:GetText()]["icon"] = category_node:GetIcon() + + for i2,part_node in ipairs(category_node:GetChildNodes()) do + tbl[category_node:GetText()][part_node:GetText()] = part_node:GetText() + --print("\t",part_node:GetText()) + end + end + return tbl + end + + load_partgroup_template_into_tree(categorytree, pace.partgroups) + + local part_categories_presets = vgui.Create("DComboBox", RightPanel) + part_categories_presets:SetText("Select a part category preset") + part_categories_presets:AddChoice("active preset") + part_categories_presets:AddChoice("factory preset") + part_categories_presets:AddChoice("Cedric's preset") + local default_partgroup_presets = { + ["pac_part_categories.txt"] = true, + ["pac_part_categories_cedrics.txt"] = true, + ["pac_part_categories_default.txt"] = true + } + for i,filename in ipairs(file.Find("pac3_config/pac_part_categories*.txt","DATA")) do + if not default_partgroup_presets[string.GetFileFromFilename(filename)] then + part_categories_presets:AddChoice(string.GetFileFromFilename(filename)) + end + end + + part_categories_presets:SetX(10) part_categories_presets:SetY(10) + part_categories_presets:SetWidth(170) + part_categories_presets:SetHeight(20) + + part_categories_presets.OnSelect = function( self, index, value ) + if value == "factory preset" then + pace.partgroups = pace.partmenu_categories_default + elseif value == "Cedric's preset" then + pace.partgroups = pace.partmenu_categories_cedrics + elseif string.find(value, ".txt") then + pace.partgroups = util.KeyValuesToTable(file.Read("pac3_config/"..value)) + elseif value == "active preset" then + decode_table_from_file("pac_part_categories") + if not pace.partgroups_user then pace.partgroups_user = pace.partgroups end + file.Write("pac3_config/pac_part_categories_user.txt", util.TableToKeyValues(pace.partgroups_user)) + end + load_partgroup_template_into_tree(categorytree, pace.partgroups) + end + + local part_categories_save = vgui.Create("DButton", RightPanel) + part_categories_save:SetText("Save") + part_categories_save:SetImage("icon16/disk.png") + part_categories_save:SetX(180) part_categories_save:SetY(10) + part_categories_save:SetWidth(80) + part_categories_save:SetHeight(20) + part_categories_save:SetTooltip("Left click to save preset to the active slot\nRight click to save to a new file") + part_categories_save.DoClick = function() + pace.partgroups = extract_partgroup_template_from_tree(categorytree) + file.Write("pac3_config/pac_part_categories.txt", util.TableToKeyValues(extract_partgroup_template_from_tree(categorytree))) + end + part_categories_save.DoRightClick = function() + Derma_StringRequest("Save preset", "Save a part category preset?", "pac_part_categories", + function(name) file.Write("pac3_config/"..name..".txt", util.TableToKeyValues(extract_partgroup_template_from_tree(categorytree))) + part_categories_presets:AddChoice(name..".txt") + end + ) + end + + local part_categories_add_cat = vgui.Create("DButton", RightPanel) + part_categories_add_cat:SetText("Add category") + part_categories_add_cat:SetImage("icon16/add.png") + part_categories_add_cat:SetX(260) part_categories_add_cat:SetY(10) + part_categories_add_cat:SetWidth(100) + part_categories_add_cat:SetHeight(20) + part_categories_add_cat.DoClick = function() + local category_node = categorytree:AddNode("Category " .. categorytree:Root():GetChildNodeCount() + 1) + category_node:SetIcon("icon16/page_white.png") + + category_node.DoRightClick = function() + local menu = DermaMenu() + menu:AddOption("insert part in category", function() bring_up_partclass_list(category_node) end):SetImage("icon16/add.png") + menu:AddOption("select icon", function() bring_up_category_icon_browser(category_node) end):SetImage("icon16/picture.png") + menu:AddOption("write a tooltip", function() bring_up_tooltip_edit(category_node) end):SetImage("icon16/comment.png") + menu:AddOption("rename this category", function() bring_up_name_edit(category_node) end):SetImage("icon16/textfield_rename.png") + menu:AddOption("remove this category", function() category_node:Remove() end):SetImage("icon16/cross.png") + menu:MakePopup() + menu:SetPos(input.GetCursorPos()) + end + end + + return panel +end + +function pace.GetPartMenuComponentPreviewForMenuEdit(menu, option_name) + local pnl = vgui.Create("DButton", menu) + pnl:SetText(string.Replace(string.upper(option_name),"_"," ")) + return pnl +end + +function pace.ConfigureEventWheelMenu() + pace.command_colors = pace.command_colors or {} + local master_panel = vgui.Create("DFrame") + master_panel:SetTitle("event wheel config") + master_panel:SetSize(500,800) + master_panel:Center() + local mid_panel = vgui.Create("DPanel", master_panel) + mid_panel:Dock(FILL) + + local scr_pnl = vgui.Create("DScrollPanel", mid_panel) + scr_pnl:SetSize(490,800) + scr_pnl:SetPos(0,45) + local list = vgui.Create("DListLayout", scr_pnl) list:Dock(FILL) + + local first_panel = vgui.Create("DPanel", mid_panel) + first_panel:SetSize(500,40) + first_panel:Dock(TOP) + + local circle_style_listmenu = vgui.Create("DComboBox",first_panel) + circle_style_listmenu:SetText("Choose eventwheel style") + circle_style_listmenu:SetSize(200,20) + circle_style_listmenu:AddChoice("legacy") + circle_style_listmenu:AddChoice("concentric") + circle_style_listmenu:AddChoice("alternative") + function circle_style_listmenu:OnSelect( index, value ) + if value == "legacy" then + GetConVar("pac_eventwheel_style"):SetString("0") + elseif value == "concentric" then + GetConVar("pac_eventwheel_style"):SetString("1") + elseif value == "alternative" then + GetConVar("pac_eventwheel_style"):SetString("2") + end + end + + local circle_clickmode = vgui.Create("DComboBox",first_panel) + circle_clickmode:SetText("Choose eventwheel clickmode") + circle_clickmode:SetSize(200,20) + circle_clickmode:SetPos(200,0) + circle_clickmode:AddChoice("clickable and activates on close") + circle_clickmode:AddChoice("not clickable, but activate on close") + circle_clickmode:AddChoice("clickable, but do not activate on close") + function circle_clickmode:OnSelect( index, value ) + if value == "clickable and activates on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("0") + elseif value == "not clickable, but activate on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("-1") + elseif value == "clickable, but do not activate on close" then + GetConVar("pac_eventwheel_clickmode"):SetString("1") + end + end + + + local rectangle_style_listmenu = vgui.Create("DComboBox",first_panel) + rectangle_style_listmenu:SetText("Choose eventlist style") + rectangle_style_listmenu:SetSize(200,20) + rectangle_style_listmenu:SetPos(0,20) + rectangle_style_listmenu:AddChoice("legacy-like") + rectangle_style_listmenu:AddChoice("concentric") + rectangle_style_listmenu:AddChoice("alternative") + + function rectangle_style_listmenu:OnSelect( index, value ) + if value == "legacy-like" then + GetConVar("pac_eventlist_style"):SetString("0") + elseif value == "concentric" then + GetConVar("pac_eventlist_style"):SetString("1") + elseif value == "alternative" then + GetConVar("pac_eventlist_style"):SetString("2") + end + end + + local rectangle_clickmode = vgui.Create("DComboBox",first_panel) + rectangle_clickmode:SetText("Choose eventlist clickmode") + rectangle_clickmode:SetSize(200,20) + rectangle_clickmode:SetPos(200,20) + rectangle_clickmode:AddChoice("clickable and activates on close") + rectangle_clickmode:AddChoice("not clickable, but activate on close") + rectangle_clickmode:AddChoice("clickable, but do not activate on close") + function rectangle_clickmode:OnSelect( index, value ) + if value == "clickable and activates on close" then + GetConVar("pac_eventlist_clickmode"):SetString("0") + elseif value == "not clickable, but activate on close" then + GetConVar("pac_eventlist_clickmode"):SetString("-1") + elseif value == "clickable, but do not activate on close" then + GetConVar("pac_eventlist_clickmode"):SetString("1") + end + end + + local events = {} + for i,v in pairs(pac.GetLocalParts()) do + if v.ClassName == "event" then + local e = v:GetEvent() + if e == "command" then + local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) + local this_event_hidden = v:IsHiddenBySomethingElse(false) + events[cmd] = cmd + end + end + + end + + local names = table.GetKeys( events ) + table.sort(names, function(a, b) return a < b end) + + local copied_color = nil + local lanes = {} + local colorpanel + if LocalPlayer().pac_command_events then + if table.Count(names) == 0 then + local error_label = vgui.Create("DLabel", list) + error_label:SetText("Uh oh, nothing to see here! Looks like you don't have any command events in your outfit!\nPlease go back to the editor.") + error_label:SetPos(100,200) + error_label:SetFont("DermaDefaultBold") + error_label:SetSize(450,50) + error_label:SetColor(Color(150,0,0)) + end + for _, name in ipairs(names) do + local pnl = vgui.Create("DPanel") list:Add(pnl) pnl:SetSize(400,20) + local btn = vgui.Create("DButton", pnl) + + btn:SetSize(200,25) + btn:SetText(name) + btn:SetTooltip(name) + + + if pace.command_colors[name] then + local tbl = string.Split(pace.command_colors[name], " ") + btn:SetColor(Color(tonumber(tbl[1]),tonumber(tbl[2]),tonumber(tbl[3]))) + end + local colorbutton = vgui.Create("DButton", pnl) + colorbutton:SetText("Color") + colorbutton:SetIcon("icon16/color_wheel.png") + colorbutton:SetPos(200,0) colorbutton:SetSize(65,20) + function colorbutton:DoClick() + if IsValid(colorpanel) then colorpanel:Remove() end + local clr_frame = vgui.Create("DPanel") + colorpanel = clr_frame + function clr_frame:Think() + if not pace.command_event_menu_opened and not IsValid(master_panel) then self:Remove() end + end + + local clr_pnl = vgui.Create("DColorMixer", clr_frame) + if pace.command_colors[name] then + local str_tbl = string.Split(pace.command_colors[name], " ") + clr_pnl:SetBaseColor(Color(tonumber(str_tbl[1]),tonumber(str_tbl[2]),tonumber(str_tbl[3]))) + end + + clr_frame:SetSize(300,200) clr_pnl:Dock(FILL) + clr_frame:SetPos(self:LocalToScreen(0,0)) + clr_frame:RequestFocus() + function clr_pnl:Think() + if input.IsMouseDown(MOUSE_LEFT) then + + if not IsValid(vgui.GetHoveredPanel()) then + self:Remove() clr_frame:Remove() + else + if vgui.GetHoveredPanel():GetClassName() == "CGModBase" and not self.clicking then + self:Remove() clr_frame:Remove() + end + end + self.clicking = true + else + self.clicking = false + end + end + function clr_pnl:ValueChanged(col) + pace.command_colors = pace.command_colors or {} + pace.command_colors[name] = col.r .. " " .. col.g .. " " .. col.b + btn:SetColor(col) + end + + end + + local copypastebutton = vgui.Create("DButton", pnl) + copypastebutton:SetText("Copy/Paste") + copypastebutton:SetToolTip("right click to copy\nleft click to paste") + copypastebutton:SetIcon("icon16/page_copy.png") + copypastebutton:SetPos(265,0) copypastebutton:SetSize(150,20) + function copypastebutton:DoClick() + if not copied_color then return end + pace.command_colors[name] = copied_color + btn:SetColor(Color(tonumber(string.Split(copied_color, " ")[1]), tonumber(string.Split(copied_color, " ")[2]), tonumber(string.Split(copied_color, " ")[3]))) + end + function copypastebutton:DoRightClick() + for _,tbl in pairs(lanes) do + if tbl.cmd ~= name then + tbl.copypaste:SetText("Copy/Paste") + end + end + copied_color = pace.command_colors[name] + if copied_color then + self:SetText("copied: " .. pace.command_colors[name]) + else + self:SetText("no color to copy!") + end + end + + local clearbutton = vgui.Create("DButton", pnl) + clearbutton:SetText("Clear") + clearbutton:SetIcon("icon16/cross.png") + clearbutton:SetPos(415,0) clearbutton:SetSize(60,20) + function clearbutton:DoClick() + btn:SetColor(Color(0,0,0)) + pace.command_colors[name] = nil + end + + lanes[name] = {cmd = name, main_btn = btn, color_btn = colorbutton, copypaste = copypastebutton, clear = clearbutton} + end + end + + function master_panel:OnRemove() + gui.EnableScreenClicker(false) + pace.command_event_menu_opened = nil + encode_table_to_file("eventwheel_colors", pace.command_colors) + end + + master_panel:RequestFocus() + gui.EnableScreenClicker(true) + pace.command_event_menu_opened = master_panel +end + + +decode_table_from_file("pac_editor_shortcuts") +decode_table_from_file("pac_editor_partmenu_layouts") +decode_table_from_file("eventwheel_colors") + +if not file.Exists("pac_part_categories_cedrics.txt", "DATA") then + file.Write("pac3_config/pac_part_categories_cedrics.txt", util.TableToKeyValues(pace.partmenu_categories_cedrics)) +end +if not file.Exists("pac_part_categories_default.txt", "DATA") then + file.Write("pac3_config/pac_part_categories_default.txt", util.TableToKeyValues(pace.partmenu_categories_default)) +end +decode_table_from_file("pac_part_categories") +pace.partgroups = pace.partgroups or pace.partmenu_categories_default diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index cb2c202d5..c6120482e 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -253,22 +253,89 @@ end) do local function LoadUpDefault() - if next(pac.GetLocalParts()) then - pac.Message("not wearing autoload outfit, already wearing something") - elseif pace.IsActive() then - pac.Message("not wearing autoload outfit, editor is open") + if not GetConVar("pac_prompt_for_autoload"):GetBool() then + --legacy behavior + if next(pac.GetLocalParts()) then + pac.Message("not wearing autoload outfit, already wearing something") + elseif pace.IsActive() then + pac.Message("not wearing autoload outfit, editor is open") + else + local autoload_file = "autoload" + local autoload_result = hook.Run("PAC3Autoload", autoload_file) + + if autoload_result ~= false then + if isstring(autoload_result) then + autoload_file = autoload_result + end + pac.Message("Wearing " .. autoload_file .. "...") + pace.LoadParts(autoload_file) + pace.WearParts() + end + end + else - local autoload_file = "autoload" - local autoload_result = hook.Run("PAC3Autoload", autoload_file) - - if autoload_result ~= false then - if isstring(autoload_result) then - autoload_file = autoload_result + --prompt + local backup_files, directories = file.Find( "pac3/__backup/*.txt", "DATA", "datedesc") + if not backup_files then + local pnl = Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)", + "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() + pac.Message("Wearing autoload...") + pace.LoadParts("autoload") + pace.WearParts() + end, + + "load latest outfit : pac3/" .. latest_outfit .. " " .. string.NiceSize(file.Size("pac3/" .. latest_outfit, "DATA")), function() + + if latest_outfit and file.Exists("pac3/" .. latest_outfit, "DATA") then + pac.Message("Wearing latest outfit...") + pace.LoadParts(latest_outfit, true) + pace.WearParts() + end + end, + + "cancel", function() pac.Message("Not loading autoload or backups...") end + ) + pnl.Think = function() if not pnl:HasFocus() or (input.IsMouseDown(MOUSE_LEFT) and not (pnl:IsHovered() or pnl:IsChildHovered())) then pnl:Remove() end end + else + if backup_files[1] then + local latest_autosave = "pac3/__backup/" .. backup_files[1] + local latest_outfit = cookie.GetString( "pac_last_loaded_outfit", "" ) + local pnl = Derma_Query("Do you want to load an outfit?", "PAC3 autoload (pac_prompt_for_autoload)", + "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() + pac.Message("Wearing autoload...") + pace.LoadParts("autoload") + pace.WearParts() + end, + + "load latest backup : " .. latest_autosave .. " " .. string.NiceSize(file.Size(latest_autosave, "DATA")), function() + pac.Message("Wearing latest backup outfit...") + pace.LoadParts("__backup/" .. backup_files[1], true) + pace.WearParts() + end, + + "load latest outfit : pac3/" .. latest_outfit .. " " .. string.NiceSize(file.Size("pac3/" .. latest_outfit, "DATA")), function() + if latest_outfit and file.Exists("pac3/" .. latest_outfit, "DATA") then + pac.Message("Wearing latest outfit...") + pace.LoadParts(latest_outfit, true) + pace.WearParts() + end + end, + + "cancel", function() pac.Message("Not loading autoload or backups...") end + ) + pnl.Think = function() if not pnl:HasFocus() or (input.IsMouseDown(MOUSE_LEFT) and not (pnl:IsHovered() or pnl:IsChildHovered())) then pnl:Remove() end end + else + local pnl = Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)", + "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() + pac.Message("Wearing autoload...") + pace.LoadParts("autoload") + pace.WearParts() + end, + + "cancel", function() pac.Message("Not loading autoload or backups...") end + ) + pnl.Think = function() if not pnl:HasFocus() or (input.IsMouseDown(MOUSE_LEFT) and not (pnl:IsHovered() or pnl:IsChildHovered())) then pnl:Remove() end end end - - pac.Message("Wearing " .. autoload_file .. "...") - pace.LoadParts(autoload_file) - pace.WearParts() end end diff --git a/materials/icon64/new pac icon.png b/materials/icon64/new pac icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7088ceab7d99d776791e99c6dac68fbae9bac46 GIT binary patch literal 7621 zcmV;$9XjHPP)EX>4Tx04R}tkv&MmKpe$iQ>9ue9V{Z^kfA!+MMWJ;6^me@v=v%)FuC*#nlvOW zE{=k0!NHHks)LKOt`4q(Aou~|>f)s6A|?JWDYS_3;J6>}?mh0_0YbCFRI?`vs9McP z#baVNw<`Kx(T^bnF$=%MOg)ia%)oVg-NVDzy9m#6KlkStQ1T`NJR)(F=|+usgLr1M zrgPpW4zZG?5T6rI7<576N3P2bzi}?wEbz>bkx9)Hhls^u7t3ADN`^{2O&nHKjq-)8 z!wTmu&T6I3+V|uy4CJ+yG}md4B90{_kc0>sb(B#-4G~&3QcR?1Kjz{evHeMM$>b`7 zkz)Z>sE}+w_#gc4)+|g-I!S>T(EDOpA7g-T7icvs>-*TUS|@<_8Mx9q{%RAL`y{>5 z(ZWYSa2vR|?r8EJaJd5vKk1So*^-y0P$&TJXY@@uVDJ|3uQ|Q7_i_3FWT>m<8{ps& z7%x)xy34!!x_kTgOuN4y(B5*)ktHM800006VoOIv05eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{02`f2L_t(|+P#~3oLyCw@4suG;f^&AsY)s{Nx)zNCI~_^ z$P^;Nqai4^s2_)RplP1^y0x|2h+sQCeeD>u(T9B0ZiWQWR!Ky~(11t?kU$bbNJ6SI zRZ^9ts_wmYhjaFNf1GoxZVd^+w%_@D&gYza?m2t!_1kN&d5d0}Zp9{UiSOy@nb3IrUm5u3 z>eZ|N{&<6^8M4++kjC_=jJ+Z!Zlo3z)yez3L}3`2z4w7HEoD4O&dT_ zkc|ahv0{bR5-%cno(Dh}hFELq>guWhyH>AWed>w0K;!<$F9Ty`WC>K4!^9;JvHuh0 z`{dd1RC)f^n*-E>+OZvdc1?1mgWAsG)3=s{4Ry)Ep74f`onsjSN;-n7=visGe&$=o zv|0;*=XvaLn@UDQ>RX?KH9i?_3EYL0Vr!i6d4(ltpz}9WI%kKKzs<_$!|6f zgfkna-*sDmrLhAw4q^kuT2SSQ6}MbGsSFs)NTI6~>ecqXt~lKHp{E`xo%@OE5*K8} ziWQId^z@ut{b2g^>0>~sa)9GF7-M2KRIIhNK;(gAt5>h?s3iI4EKPSx>PQ*Gew$3}Rj;M#fjf7VI%ZPI)PpLmgos);oO#jbCZ$-%+*ZX-$%8!& zYM)y=T-bH@MJ>fz0JNv42V)Gz7{V~5tE+1QHyC3`CX^PiW_B_l9LK@) zJd80TKvcOQ2!h&gwi`q5PwxH2MdP`k3c!;`?vo$%U6j}oq}q;R=9Eu32?$PVk^jTq+u;IY8fLYO+qm-UbEx5fs$kz6BonV#f7YPN;tI@FXlNVQw2$ zqL;46wscWy>7?YRu*NVoof`Rne_ZJhzvkM}`@j5;E{d&} zl50PMz^|(=g~M6&kuyrAryn@KDenSXrca;V4zwLOa3GTS#>KXDQJjAUr71J8o=*kX zsR{~hzDpK3Tn;b-uE-es#Sy5eA}HNJi{~j0HXn} z6mZIi@do-ZK>;+t1bLj{y*R~}F_mqgM~+MUPp_{b{(f*|KmI@miOe)YHx;RUsE@e7 z+kMa7PYg?qk@GSzx2LBElc|R@-%R1Or5wyO&3$E?q|B?}N% z=o>u5gZp;j4{pIJ?v7_Uh&Vy}kx2+&vm&})cR#GTezdzUJ_1k2Ok%zBD70V2Q2SDX z#6&J|^;ap0^zX0c-SCsQ+&Xi{^lQ{hF52ZK2waZ>P-iVkE#qnBqyZRX&7I(IG@zl} z%eMV5kUqQtQ+fV`vpgx=Uq6cTw;zLz{ZZEnNM##|!FdGP84SBAGGGL4@Rhm4#_Sed zn-r|XMo1*lzBKJc|A7Y{7%t9ROs}7Ua*>B-AEY_hhl#w1Nnw!JNW2i>**s3Uhp%pZ zf!&4eK;igK&Lq$8yKeOUOOL=a{S)7@-no?8uO{EVgdkDJv1v=c>!JIbtX=EkAY%p_ z+seTWH($cWM}Hgr2xt!WF|G6CfLVWz* zzh67{uYb4*-mxZnrxrU2UL-L%pP+6gxJi5zSMf78%(^($0n6Cdf!xvyZ)4L_D?u>? z7(I%kFT6UN*EV+&hil-Y^WdIt_+u{m_O?@i+u_>J!s$1Twsm2pg+8ak7Be_9fOoD@ zeiDOWCUK3?BbeeY{NX;rbUU7^x*(pa8w2p#*Y1D$sOh85hm3^}&V}W9X!4;o*2wq& zFZkFu99DF;a%1}xb~_bvp2eACc;^EHBW=8M&8u`5_TvNv92?+8PNhcfuv_(yU*oj> z83?aEj6l+XdG*zq6DYXMuhjF)8?z9mV7JHoQ-tnA@Yc#SrXHOez+`bBTNGbX;2w6-u&28d1=sQ>3AaMEvMLppSzoP}Hi3^qZ|Kv;?z zPdm_*hExh>D4Y_&E5p!dps5WSnjurq9B+VfI>oBFv-woRvQ_aKsP)4zs`<3F-JQk4 z;YbEl*yY;$h>kH5?lbl)-BH;ca0kZo~syVlJy_odk)N<`+7W8 z>eSMt0oncA=yG6Y#$ifRf@Dh)!yp_x25A@Cvrw0Xg$m6>&@V7|D%8)Ul4@nRyO+v9 znqn@^j|b~%Kf3j%L$hAD%BU7h6w6ctrL?u(GfKmo&fC4cFXjLpiMrMjpF*ijLgdyL zo}WnAWE?Pwc_Bc;fqGHg-J6)2fVHgva@1yc|X)I0~P>Vpi;swhXfT%SP3{qju0kVFkyi7A7t6S zSN=HX;NFs7DKTry#<24z->`_Ni#v|(1zK!g0Omcre&t3d#fHH=k1smNwD}L?=oT)0 z;V&lY@yNx+0IouUNLVLDNgK#IO0%Q1xR!QLY4nvkM@hLt(lI2A!FLUwYa*oT4Ai+G zDL8E!ENz8!GjJ~O4DeSV=L5e6%|WS*ltbLIrC#!!Edh!B(pU{X`;UK9nDhF#GB9WHtLy9ctWHH%?6)042 zO3|WCaEve;4a|yANF)_RM-vgzq`Q$xpiyp1;O39LE+i3qQ3z$EMhlGcyVbEv!2*Fu z>??v7LKY=~a!vXubzVr)1>*=f4!8~sII!7=v`1@N!IenWbvD3^W|-X!4V}OoU zt`l%}D*q?W9pJRwLiI{PsI!c8AoIK~Qy4CPT`%i|cp zPGU8O8qG@}a3mCR3dhu8zzRPcXr{vpX?8=hu99*rz5{Q_6u_x~I1m&e=RuH%U=YfE z5Y)pNz2Nr8Ny5v}T4#NX=qyT~x_>C}iA-LB8^Cs!- z6<{GS9a>)kyA^aFN*`JdP|Y&i7U`}P0}qkfHm z*l&f_&|z9&>GVR*$pXa$l+)m62)!0)Z-Y5A;jA;DsRJkhF8~K(4%i6X3tR&1fmilH z*@wIZ(%AYbNV!bKHq$;_f(TOX{+GYbzg>(sUfsL3Z2y`)I(-%OSaU5S^dTGzmA%%` z7`nW>y$r=P3}-;w3GH-(&JNsU8usjifx{q0IC=oi_%giww{XgQIFN(ie-CCahFl&F zcSEiSp+gWTroNtBLlXy05~pTZS9Y1oFZZc8DFEY-C$78Y;yiL8oB#OPllI_hX8w4< z1PD1G7Xry4@GWFgP^vJnw}f~2Fm65pM-A*OLSX=|kE0zweGFCz>>GgIqwva;&^#5& z0Spxh3k8aS;?J#Pz-wfyq<|tu6%*xuxpJCRetCcq5GGhUcl>Cp;R|b$Ct`s0pLp!V zZti$BIsU5f;72w`sS0NZyxox4Nk`jmj`j9YI@k+4yWyoh;5R{X32gaxPSmb{=msWy+s=n1c?MWF|RJ90r!rtY;$>8gkCe6z?<>^35+oy zXv7KrU?dlsXG2q6nqoX5PsS-lfk( zxsIoeK?TyPvs#Cw(XErb{5iusQLcQ!M|5nFReZXx&}b}IHk7$&MvAGkGx%*O+$W#G z-O-Kd&(WBv1AjR3x19=j0c1jGDMRNlWXoLj?Kff1s-0!U4@xHhphJ}xjbX?bDhNjm z3wwK<%+wCah{*73AaP0@!}mS#ozaPPYeN!7*uFb5m>0}-*}fyP-4`zOc>LJ{^c~{S zPhLaOOE7EEh3vU^6*HGy0}F;Z@aT7ugKm_lof&|sK&Aw3!;mda5)-chLMPYUF7s76 zD2iVV1yvn~*C^B!dVd7dV{?7yzKI6rf-W~s2F9IFCZWQB&`WUYH6Mm@nZZK`;mj6D zR>%_~L7BOWC7S(Y3F-=vDv#*1DpQTP=3zKytm9X<3nFx=ata}P#UR39GHvo=G>_HjxalL4y3+H)}s5Wrfk}PMnv@&N(CwssD75roUxC4jj z{@0J(;FamMy!^ z;UF;0h9GQg?J&p6q9+uBCn#;}`2j4rIqGHm?q~)U%#6_3-ZNUKws5Y;mY1R>uy~Qj z!_N&v81Sx3=J2Dt)(+#vUr{coC>NCfEV6(5x1-h<%ymY1b9;9U@v}U(?5qj6*_cCIk1Yd-Ad!M} znir(V)z?quIk9}^qd~;(iCFrZnxLwVD?^z?<&B>?p~e(myKarWV%Z9FqR6gu=^wz) z#+_Oy4MFcF*!o`3U2!1bVz~6zAP(ExzrupIe=-6=Y9r5v&AVbZSe50MpF37I_$?eg zHb_%bGw*ocw|L+5za8b<>ba~`TZ90`L0H>#0@W>;xNL9ivzlY=y47|B%}I`800#~n zVCBk{eC^9$VDsipF!KVK_pQ;FI^RVhUm$n%FmznN_HV^bhZmciV%V}}3wPgrHybu= z01>|SjqkH*Yh|Ra^yZtt%)mgD_j})2jB{ayul{sCh;Z+{_p);3N;coK68`x4QF9_* z7IxuTt0w0JsVS8ORHX)l`z>=ht}oE$m$>bm+X+JGejb7|%Qy|QM;Iq)dn0{&UuGyj z%)--O4-zeh8-MhdgrQ~s{{38g<>_qMQDo1aZk~PiSvosAx#}$!v%mW>+*Fo_AKuC4 z^=p_l>qATs;RsXOqaw|>Pu~KGFGk#WWSk5DTvcJog`el8XaA|T9m6QYO=>S*z3Zvs zt66Zaw}?4!ybIU!s0{3jTU&%_nTOZ6k>Q@FBU`@tGXVVflgn@%hn9hjB-5~DQHpD? zT)=g2UBU%tO=bVi$JqG4;jS-ym6WUO-TLsDz(s9QaU$Iu{Ws&SaOEIeb`aWcjmy;| z-(FO4Rq<+PcM{e&nM~cJG~RmX+22iSIkn7!D;2KT1gF1;T`z6JfBDl<#~mL;9G5AD z)l4sbkFZjPj;NZo;zViRG)LL=HQ4ljluciUU3bDu--C@`hjJivEx8(R?>aExCl;e)r?AqCpwX`Z&0YKn1A+5P#i%-A9%&j`gcD!Oi7zWV4ZA%jI%oi2dl` zp3i<-Yf@K4GT@Fo?!XC)v=%qB;tlJdF}7ja&czWyE?>)(ku|d@iYpEs!~;mQjAf-7 zLAKNXCJQoY*!$0*izfy%<73nF&vD69ce3E{&vfX25#|4&EcKtipQ zC9=Ce`OeQbeEGZY9hhhe$Ao9IS+dzIqRL>t5TS@tH_m`;^Gn=z_U)5!RTtWNDo;QC zG(Z3O&jGmXvdcLC{PUsi7LvI~2!fk&r!0jXHM4r^L(qCUl|Outh8asR$vU`f8#h$r zpQqmhySiZa+eU!+{JUO)!OuXDW7(O)#)8XyfFL*0t|%m}H6nsLT_T_AN;t|fe z;Br{{JnUZ&nHlKJOUNJE!;Y=L<8ya-BUR`f9UZJ(xsqVpeYm|pfYiHS!N=k71}N-? zg&%{SCz0+yvUt&Q$o0YC&p@&@6=>i{jSMj61opu=ATr4W8PB0bEv+X}oNT9DK&4zF zG4O+tXI<{s$qx-e`x$`H@ZvJouYHgpGZhwpm~i@=d12Et$de!9%o!OTe8+lLo&RH2 zUHC)h%hqUyDt%B{4{jT@oQ6%$;`VQTmweAgbo#j@F8CBqP=KdC3mlHO@EEKyR75mH zv?4|;p05~3AIzVfsNG0nuDJYd6F|u4@)U-Ph$@-*1m&K8I)$O%%$^j0ZNE5|4c|t; zKS?EUk+Z+Zkq0uEm#*a6^9uaUhc9Q@(mHRQy%pAVetM~1ZGVw5Ycc$-5HpdFN&GG1@k1EybyEvL+nI zDj&XjwE7O<^tte@|EXh4SX)iYsYb@TE4=qAA(SX+U_4>(y=Aglj}YP1+3L?m2psq%~gJOwFY1}Xta?6)5mxmsDNNZ~rVm2alJ&w9lL9Ssm z`SmJ3k{ri?ka2~xvC4|7zay=njzL9g6of!092zM@w5YfchLLqy6KoB|<(S*aA*Xe# zqbiDsZ!O7kS+I^^RnaoPJaV?Xy!pw%4Qi5L%a<<~y}Vf-`q;l&jguN(4Rvf8$Z^Q) zq~B{l`BGV_y^RyHV`5-h^I9}t)wVS**pM;a_~=T#Abkao7S_f4(oAgD=}3iSw>Nde zCV%SA7#&SmYqNzsXdzCql|cg@>RaHt;W&ci*GKg4RxU~WXI>xW+1u%NApd2P-7 zp~gkfcxsa4A{xaYM8*&XM#dIdKb-Ac<;IfPWz<;lvEqREVsStmQH+eY;-Xac1!eyF zuIyjjN!p9{5{A)cDx(&QN+kRcK|^14xGty^{Bi*}CQ)Hg5N9z%b)EH&lr9X$pzzhk z*5K?g*#FIjxmS7U!Ve7X`BIg8wU&XyQFk`wyQH15oL{VzN0KB}0InYFzG+OVt_Cm> z9kJDZSvT{a0cJU6Z&Q#eHArY?Ib{zr5nyim=!hE~gcut_1VB`-FJgVA&`_ytKV{%Z z@2uyav~UzOBAj?no0NZC?}S~dsf&ijdWtV-;_NVJIVz?z5HfYB3-Iy2=YIRD+%HNx znT&mxi7Hga1)7G7#gXULB@tQ&6NA%WhFFyTPo^a%-zW85f5-K4@e_ylXUZX>fk@!n zFgJbZ*uh0FZ|y(x`?n5bLyNT*wTe|xs}g*v!P73a?pts|A@GHLC-aOuTYwy=f%8R@uep|J$r z!?3#_j_ifpK`0(mDg{e8YzfO24~uIR8z@>4LJKx9*otu(#1$01Ows9TgO%B+i1?!1 zm?hybaW`xA{)#KA#+0yb-97fWIK(oo3WTa45~=1mUP>hQMuVJHPPzEv|7d}$fKSD( z9RePQhBBNMK=%M>s02wUHCoCEOW<2<(PB#$Td`1#hh2_1ThtZ3vA&4}fi`G`ES46v zygu5Jr5CD>#a~_}(I>T038@+&>wfc-U~)}aYpPTQpmNFVTN_Yg2#t_Nm^lMj3_K0Y zhXb1vbpDSbWZ=<9m19$&*HtzhPsH*4M;mI1V+93Y@{p)*1Tx?1a2Pk6gzqrG7w5V+be4!coiGGyUg(J8WiD+NRqmad< zKk>Azl#NmvxHhy6w}ySgz2TeTz;J138m3{^v6m>8Z6 zWdkcp<4a!+DMn`Q%+$aN(kR6;C5S~!FgC=Vq|DHeb|Pk7II}XG85)K?L+o+7&$gj% zI5+GX)*H_wo}G!~lcv8J;vA&Ri{aQ1M;nQ$x_%QSg{RAAQ657xvUM6O$ zq*zuJs&O^r`0fTrXs1dWEKXLkqoY$@_k2 ag)i~={wcn~SJW5yCt7O#->uhv?f(GC^(1-# literal 0 HcmV?d00001 diff --git a/models/pac/circle.dx90.vtx b/models/pac/circle.dx90.vtx new file mode 100644 index 0000000000000000000000000000000000000000..892ea62bb59de2b45600e8712b3fc07e64a067ca GIT binary patch literal 1089 zcmZ9L$yOpk5Jg|14B`L^3WS*Rv8kj*ErI@A!F=+|LhFFu785+_`#9e33EDa}yhGEALYn-05X{Z}c z4cms*!t;tpXJY%L+g}Z_4^rmYaA=4vkxE^|o8idtVK_Ft8+wL*0ya|#t!W)E6Ejs( zEUOCD*qRk`HF~BxJti@JZj(%1Ta4Y-4&ES^8QVZjyh+T}B8KN`<8AGtTziadXkP`@ z7T)3jb?^hYqeHS?9pMGBi}&b-`gmW*sL%=do=$nzna<}8DTisE)=XwMU(F5N%(IY-3PVgCh$|0YjXEHOrKvTVt6kp;?>T~=9Ewnm!tGQqKKV?BAp8x;= literal 0 HcmV?d00001 diff --git a/models/pac/circle.mdl b/models/pac/circle.mdl new file mode 100644 index 0000000000000000000000000000000000000000..352ae80e6e085fe6df7feb3a0bb26726b0fcdc84 GIT binary patch literal 1692 zcmds2J4*vW5Z?GMglJ`{zd$S$1BN7rHiDfgECM09Tg|~8iMfO{DV8)^X=9PXB8@-6 zS_MfJ!6Jo4O2JkkjUdFC+Z`?!a+laTFgG*be)HJddmJ7dr?(hm-|Yuo=SJDoovd9n zxaDN>ImgtqP9ay~PDVG&y2Uqb!vb=E+%5zx0G#F@_ZMsHPnV$J?|`p?uLWg9U;(u` zBN?Sx8Iu(%W^-Nz@n~yP>@j_Frum?$sbfg7+?X64lT1<* zPadeNLiO|Y`Ib?SXQWW~Ak=gJ>P7yCC-l{Wyuvknz&itmjsjvss9(bmPZqoM9cIh& z>_z(h4RUXpT1pFVrgv^EjGkI17N$%sV+#|cmP-q3N-gJ>s%2!EwVYULmZ62sNaGAF zY(Z*azmm*bx|RjYfrU*>FF#otmVL{TW!Z9QS+VpjP0NvG)pBfUSxzl$mNQG+a$#Aw zY*?-=n-)HQS`Obm$*pC}a%b7LJXm%tkCu+**|KZdi^5ULp`sb6Y8Gl7)fz`~9u~9+ z4K2a4R-ma>XlV`FT89m7LZL0#)(&*E3wzoJiI1hO4xy($40HrT9m9!EVWcxS*9GLd zgt4w*!hM+NMz>Jv4({~;Q$50yo*~lO(8oZN|aU~@F#sR4QUbTLt|8=)e88XJ9~HTo%v!nS%$lF z?%DI5@7yo5tvmK~mKuif_f)>Sgs#auyy>=!slY0gz~92Fr0-DO_K9#H6Un|QK3`ew z&*NU{FY|F@!@3A@H+xA0&_OY0VVy z)`g~lxN*HF=EbjTSV~TW(^j%)w&7tPp6V5NuHI8GM16SjQ{Xv255%AK;i+FX)f+7IOY~RvYhk6!ho?MD z0*^e5hXXP{A`hwGdwh7x8{q??Lf&M4oV*S7`S6ryf#>r4Q*ZzO;q&lS<_F$$ctww` z=no3-89h$Dfe_VetNDlbT)m2)q7PCJyyyHxzu*t`OXbf1%k>L+fd7<-Nwr>R;qrjI z0Z)0;@CMb&<&AvL15bI@@bHq$v+NUx=gtdq0NtD(?mXhW1JBjVop;O=;0u1rdp1FDcS9z^Ck6pWNC1{Qp z&)3~+zl+~E@|N$soNFl+cw_2w%x8hed4Fo8JN|3;%Rc-o%d;XsbWN)M+^VXIH}yU5 z!#jEcA=0DiKfF|v;Pj9V8@!}To44m@M`N#6wfOk=-mVn$p=987)&F3}&CQ9$&>s%_Y)%efusE?v%FTdia&?WN*FF8M8@S5nALJi-#@maBKd6(_!}$Xp&;wnH z{}#?yjxz1#hd`H4P(-|^pl{(=A8{89YOD|rYa%bcI&5Ag>=r~@z6AJh=J zTpe~lPvzHd@61ic)0Trv<%hFpa%IVMP_8evwda28be`ME+x$nBjpx<()l(>vO-C2Y`nllo41>i>r7SNHE6ZqEH_I=_41$1~GS&|mO5 zmA_N{?VkDxFY1?mk)N904=WoZ9Z~oDNRQ@+xBl_@4s#{y)-ya=t?_;K{bFW<{DF47 zHW{>K zeDWcoU#@Sgvo$_*-($#b@%xY0EriSIcc#SOb?gbz54lb+H)#9cxH_S&z4)FNH){KX zKitlacDVJQ@aXgEKe{cU>A&aFqq*joTmP<^4ch&yH!Czf|Nd;4gOnes|I7Ic=crzK xIsR{=eqbTjTO2=%_g}it4)2b*e(tr?;mfAG{^NnQ;nSw8=Zm>(TWND~|9_43;SK-* literal 0 HcmV?d00001 diff --git a/models/pac/plane.dx80.vtx b/models/pac/plane.dx80.vtx new file mode 100644 index 0000000000000000000000000000000000000000..05795dbd6ff731d56d43656caef0dee53ca17182 GIT binary patch literal 189 zcmZ9GyA6Oq3`3n90#2f!gq{)Df*vv2Ey5J6!64Y-Q()idoJ`O1qA>A literal 0 HcmV?d00001 diff --git a/models/pac/plane.dx90.vtx b/models/pac/plane.dx90.vtx new file mode 100644 index 0000000000000000000000000000000000000000..cc63afd750f6b3663a88a5a639f2d47bff00cb35 GIT binary patch literal 189 zcmZ9G%ME}q3`3n10hJg4?u-Caa7T>RMVNv$7z8_f4p@41YUhv!Ku>05B>Hid)#y8s xkB_%M@A#LrlQmRiA~o5Fih;NSNehy})GaZ)xg6%o?m=fn@=?7KVNR-5eN9ocsvx zlDMIRk->qH4?v=$Nxf^|Q3`Fr)l2T(-S7SG-M#nvC!0Aw*#dx0zjazvd zs79_J8RcT$C`r0vsmbK9ah_%t*K|hT?s=q`KCT-0* z^ZL9Kvo%Ew3vNE~zZV_eHul16#D(7zTv~&KV#Z9&Bbmh(alORIUY#5>MfFIi2NLFR z+!S@4S$E``gtT}O^f{lIk0X0!?7!L%JS!_ig7W zk^K!bG0yHb3TUuEG^7fsJVv&MPtM=9nkVI(;f!WzVTi|lah{gUUc_?@&Hw-a literal 0 HcmV?d00001 diff --git a/models/pac/plane.vvd b/models/pac/plane.vvd new file mode 100644 index 0000000000000000000000000000000000000000..4ac2e17682a6ad41de8d8ed75cba28b8d5027cbb GIT binary patch literal 320 zcmeZt2@YdnU|=}?WJVYxkOr~wVW2dF0~9lWG%z&SgGeY~{QqCxaj!YsHUI%!$b6W3EaqcX4|5L|^TFmeAna`bxfLrPC=8TC H$iw&mp_w`k literal 0 HcmV?d00001 From 2d9f738b0fa7feecf5705df4419c2e82daf59546 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Oct 2023 21:13:30 -0400 Subject: [PATCH 059/300] Update README.md --- README.md | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a4ec110d6..c4a6e9dc6 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ -# PAC3 +# PAC4.5 --- Welcome to my experimental combat update for PAC3. Here's the overview of the important bits to expect. -I have a major update coming soon, when I finish wrapping some things up, so some of these aren't in yet. - # New combat-related parts: @@ -28,14 +26,14 @@ The combat features work with the principle of consent. The lock part especially pac_client_hitscan_consent 0 pac_client_force_consent 0 pac_client_grab_consent 0 - pac_client_lock_camera_consent 1 + pac_client_lock_camera_consent 0 There are also commands for clients to free themselves if they're being grabbed. pac_break_lock pac_stop_lock -Multiple options exist for servers to prevent mass abuse. Although I might've had things to say about server owners being resistant to new disruptive features, I've come to a compromise in the form of cvars. size limits, damage limits, which combat parts are allowed... +Multiple options exist for servers to prevent mass abuse. Although I might've had things to say about server owners being resistant to new disruptive features, I've come to a compromise in the form of cvars, size limits, damage limits, which combat parts are allowed, as well as several net-protecting options to ease the load on the server's processing and on the network (reliable channel)... pac_sv_combat_whitelisting 0 pac_sv_damage_zone 1 @@ -43,6 +41,11 @@ Multiple options exist for servers to prevent mass abuse. Although I might've ha pac_sv_lock_grab 1 pac_sv_lock_teleport 1 pac_sv_lock_max_grab_radius 200 + pac_sv_combat_enforce_netrate 0 + pac_sv_entity_limit_per_combat_operation 500 + pac_sv_entity_limit_per_player_per_combat_operation 40 + pac_sv_player_limit_as_fraction_to_drop_damage_zone 1 + pac_sv_block_combat_features_on_next_restart 0 ... @@ -61,7 +64,9 @@ Customizable shortcuts for almost every action (in the pac settings menu). Reordering the part menu actions layout (in the pac settings menu). -Changing your part categories, with possible custom icons. (no menu, you'll have to edit the pac_part_categories.txt file directly) +Changing your part categories, with possible custom icons. + +Colors for the event wheel (with a menu) + a new grid style for command events that doesn't move too much. ## Expanded settings menu @@ -76,19 +81,22 @@ right click on assets in the pac asset browser to save it to your favorites. it select a part and press F1 to open information about it. limited support but it will be useful later on. It can be configured to be on a part in your viewport, on your cursor, next to the part's tree label ... -## Editor autopilot +## Editor copilot : Foolproofing and editor assist + +Selecting an event will pick an appropriate operator, and clicking away from a proxy without a variable name will notify you about how it won't work, telling you to go back and change it -An idea: correct common mistakes automatically or inform the user about it. +Writing a name into an event's type will create a command event with that name if the name isn't a recognized event type, so you can quickly setup command events. -For now, it's only two things: selecting an event will pick an appropriate operator, and clicking away from a proxy without a variable name will notify you about how it won't work, telling you to go back and change it +auto-disable editor camera to preview the camera part when creating a camera part +auto-focus on the relevant property when creating certain parts # Reference and help features proxy bank: some presets with tooltip explanations. right click on the expression field to look at them command bank: presets to use the command part. again, right click on the expression field to look at them - + built-in wiki written by me, for every part and most event types: short tooltips to tell you what a part does when you hover over the label when choosing which part to create, longer tutorials opened with F1 when you select an existing part. @@ -98,6 +106,14 @@ built-in wiki written by me, for every part and most event types: short tooltips a text field for the base_part, so you can write notes on any part. +## Prompt for autoload + +option to get a prompt to choose between your autoload file, your latest backup or latest loaded outfit when starting. + +## Queue prop/NPC outfits (singleplayer only) + +option so that, when loading an outfit for props/NPCs, instead of hanging in the editor and needing to reassign the owner name manually, pac will not wear yet, but wait for you to spawn an appropriate prop or entity that had the outfit. + ## pac_event_sequenced pac_event but with more options to control series of numbered events. From eca58c100bf752798d7a82a0d090d20c80030cb2 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 30 Oct 2023 19:17:41 -0400 Subject: [PATCH 060/300] default icon back to legacy I think the new icon should remain an easter egg --- lua/pac3/editor/client/spawnmenu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index 406eaed61..74467033e 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -111,7 +111,7 @@ end -local icon_cvar = CreateConVar("pac_icon", "1", {FCVAR_ARCHIVE}, "Use the new PAC4.5 icon or the old PAC icon.\n0 = use the old one\n1 = use the new one") +local icon_cvar = CreateConVar("pac_icon", "0", {FCVAR_ARCHIVE}, "Use the new PAC4.5 icon or the old PAC icon.\n0 = use the old one\n1 = use the new one") local icon = icon_cvar:GetBool() and "icon64/new pac icon.png" or "icon64/pac3.png" icon = file.Exists("materials/"..icon,'GAME') and icon or "icon64/playermodel.png" From 856df6dc20d7b89234fca1a782d39b25d4da3508 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 30 Oct 2023 19:23:16 -0400 Subject: [PATCH 061/300] revert part pool accessor functions outsourcing is a better solution, I used the allparts function for only one use case anyway --- lua/pac3/core/client/part_pool.lua | 17 ++++++++++++++--- lua/pac3/core/client/parts/event.lua | 14 +++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 7daa288ea..37105325f 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -44,9 +44,6 @@ local ent_parts = _G.pac_local_parts or {} local all_parts = _G.pac_all_parts or {} local uid_parts = _G.pac_uid_parts or {} -function pac.getentparts() return ent_parts or _G.pac_ent_parts or {} end -function pac.getallparts() return all_parts or _G.pac_all_parts or {} end -function pac.getuidparts() return uid_parts or _G.pac_uid_parts or {} end if game.SinglePlayer() or (player.GetCount() == 1 and LocalPlayer():IsSuperAdmin()) then _G.pac_local_parts = ent_parts @@ -648,6 +645,20 @@ function pac.EnablePartsByClass(classname, enable) end end +function pac.UpdateButtonEvents(ply, key, down) + for _,part in pairs(all_parts) do + if part:GetPlayerOwner() == ply and part.ClassName == "event" and part.Event == "button" then + part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {} + part.holdtime = part.holdtime or 0 + part.toggleimpulsekey = part.toggleimpulsekey or {} + part.toggleimpulsekey[key] = down + part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 + ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0 + part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime + end + end +end + cvars.AddChangeCallback("pac_hide_disturbing", function() for _, part in pairs(all_parts) do if part:IsValid() then diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 3548a718b..004c65366 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2670,17 +2670,9 @@ do ply.pac_broadcasted_buttons_lastpressed[key] = SysTime() end - for _,part in pairs(pac.getallparts()) do --locate the corresponding parts among the part pool - if part:GetPlayerOwner() == ply and part.ClassName == "event" and part.Event == "button" then - part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {} - part.holdtime = part.holdtime or 0 - part.toggleimpulsekey = part.toggleimpulsekey or {} - part.toggleimpulsekey[key] = down - part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 - ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0 - part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime - end - end + --outsource the part pool operations + pac.UpdateButtonEvents(ply, key, down) + end) From 3f73ccb3d5f55417f8e10d90aa3ef725a5565acb Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 30 Oct 2023 23:13:29 -0400 Subject: [PATCH 062/300] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c4a6e9dc6..34221b9be 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ There are also commands for clients to free themselves if they're being grabbed. pac_break_lock pac_stop_lock -Multiple options exist for servers to prevent mass abuse. Although I might've had things to say about server owners being resistant to new disruptive features, I've come to a compromise in the form of cvars, size limits, damage limits, which combat parts are allowed, as well as several net-protecting options to ease the load on the server's processing and on the network (reliable channel)... +Multiple options exist for servers to prevent mass abuse. Although I might've had things to say about server owners being resistant to new disruptive features, I've come to some compromises in the form of cvars, size limits, damage limits, which combat parts are allowed, as well as several net-protecting options to ease the load on the server's processing and on the network (reliable channel)... + +In sandbox, the default for the combat features will be 1 when creating the convars the first time. In other gamemodes, it will be 0. pac_sv_combat_whitelisting 0 pac_sv_damage_zone 1 From 9d1a72ba9e2850da6e7c8843673a4cf869d90da7 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 30 Oct 2023 23:28:45 -0400 Subject: [PATCH 063/300] basic defaults based on gamemode --- lua/pac3/extra/shared/net_combat.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 369c1242e..627eeb4d9 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -4,21 +4,26 @@ if SERVER then include("pac3/editor/server/bans.lua") end +local master_default = "0" + +if string.find(engine.ActiveGamemode(), "sandbox") then + master_default = "1" +end pac.global_combat_whitelist = pac.global_combat_whitelist or {} -local hitscan_allow = CreateConVar("pac_sv_hitscan", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside") +local hitscan_allow = CreateConVar("pac_sv_hitscan", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside") local hitscan_max_bullets = CreateConVar("pac_sv_hitscan_max_bullets", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum number of bullets") local hitscan_max_damage = CreateConVar("pac_sv_hitscan_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum damage") local hitscan_spreadout_dmg = CreateConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not force hitscans to divide their damage among the number of bullets fired") -local damagezone_allow = CreateConVar("pac_sv_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside") +local damagezone_allow = CreateConVar("pac_sv_damage_zone", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside") local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum damage") local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length") local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius") local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone") -local lock_allow = CreateConVar("pac_sv_lock", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") +local lock_allow = CreateConVar("pac_sv_lock", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside") local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius") @@ -26,12 +31,12 @@ local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part") local lock_allow_grab_ent = CreateConVar("pac_sv_lock_allow_grab_ent", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing other entities with lock part") -local force_allow = CreateConVar("pac_sv_force", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside") +local force_allow = CreateConVar("pac_sv_force", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside") local force_max_length = CreateConVar("pac_sv_force_max_length", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum length") local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum radius") local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force") -local healthmod_allow = CreateConVar("pac_sv_health_modifier", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") +local healthmod_allow = CreateConVar("pac_sv_health_modifier", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") From ae7e3d182a46eebf05f90249a73e63e1bb6af9bd Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 30 Oct 2023 23:35:28 -0400 Subject: [PATCH 064/300] isvalid 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 004c65366..91bef3555 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -158,7 +158,7 @@ function PART:SetEvent(event) if self == pace.current_part and GetConVar("pac_copilot_make_popup_when_selecting_event"):GetBool() then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit self:GetDynamicProperties(reset) - if not GetConVar("pac_editor_remember_divider_height"):GetBool() then pace.Editor.div:SetTopHeight(ScrH() - 520) end + if not GetConVar("pac_editor_remember_divider_height"):GetBool() and IsValid(pace.Editor) then pace.Editor.div:SetTopHeight(ScrH() - 520) end end From 6b8238947344ea90cdaa7e9e652bb9e4e19fa467 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 31 Oct 2023 17:15:51 -0400 Subject: [PATCH 065/300] fix cvar name --- lua/pac3/core/client/parts/projectile.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index deb107e86..ba7ccb371 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -431,7 +431,7 @@ end function PART:SetRadius(val) self.Radius = val - local sv_dist = GetConVar("pac_sv_projectile_max_radius"):GetInt() + local sv_dist = GetConVar("pac_sv_projectile_max_phys_radius"):GetInt() if self.Radius > sv_dist then self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) else From 505fe0f1f36526b1c0bcee27504c55f8ceadee18 Mon Sep 17 00:00:00 2001 From: Redox Date: Wed, 1 Nov 2023 12:42:00 +0100 Subject: [PATCH 066/300] Apply dynamic light error fix to legacy light --- lua/pac3/core/client/parts/legacy/light.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/legacy/light.lua b/lua/pac3/core/client/parts/legacy/light.lua index ba156c235..f6986ab64 100644 --- a/lua/pac3/core/client/parts/legacy/light.lua +++ b/lua/pac3/core/client/parts/legacy/light.lua @@ -22,7 +22,7 @@ local DynamicLight = DynamicLight function PART:OnDraw() local pos = self:GetDrawPosition() - local light = self.light or DynamicLight(tonumber(self.UniqueID)) + local light = self.light or DynamicLight(tonumber(self:GetPrintUniqueID(), 16)) light.Pos = pos From a2e8aa63f470aa24ddbfefca9de045740f36e46d Mon Sep 17 00:00:00 2001 From: Redox Date: Wed, 1 Nov 2023 16:20:31 +0100 Subject: [PATCH 067/300] Minor optimization --- lua/pac3/core/client/parts/legacy/entity.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lua/pac3/core/client/parts/legacy/entity.lua b/lua/pac3/core/client/parts/legacy/entity.lua index 5d3d240fc..f46778e9b 100644 --- a/lua/pac3/core/client/parts/legacy/entity.lua +++ b/lua/pac3/core/client/parts/legacy/entity.lua @@ -605,14 +605,15 @@ do end pac.AddHook("CreateMove", "legacy_entity_part_speed_modifier", function(cmd) + local plyTable = pac.LocalPlayer:GetTable() if cmd:KeyDown(IN_SPEED) then - mod_speed(cmd, pac.LocalPlayer.pac_sprint_speed) + mod_speed(cmd, plyTable.pac_sprint_speed) elseif cmd:KeyDown(IN_WALK) then - mod_speed(cmd, pac.LocalPlayer.pac_walk_speed) + mod_speed(cmd, plyTable.pac_walk_speed) elseif cmd:KeyDown(IN_DUCK) then - mod_speed(cmd, pac.LocalPlayer.pac_crouch_speed) + mod_speed(cmd, plyTable.pac_crouch_speed) else - mod_speed(cmd, pac.LocalPlayer.pac_run_speed) + mod_speed(cmd, plyTable.pac_run_speed) end end) end From 1a7edd1075aa38d5a20f63514b43b24d4e33a818 Mon Sep 17 00:00:00 2001 From: techbot Date: Thu, 2 Nov 2023 18:51:22 +0100 Subject: [PATCH 068/300] add PAC_VERSION via action (test) --- .github/workflows/update_workshop.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/update_workshop.yaml b/.github/workflows/update_workshop.yaml index 86bd29e1e..3ca9f66b1 100644 --- a/.github/workflows/update_workshop.yaml +++ b/.github/workflows/update_workshop.yaml @@ -19,6 +19,20 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Update PAC_VERSION + run: | + cat > lua/autorun/pac_version.lua << EOF + if SERVER then + SetGlobalString("pac_version", "${{ github.ref }}") + end + function _G.PAC_VERSION() + return GetGlobalString("pac_version") + end + concommand.Add("pac_version", function() + print(PAC_VERSION()) + end) + EOF + - name: Publish to Steam Workshop uses: vurv78/gmod-upload@v0.1.3 env: From 138fd251e31bfebf7bfc8f1d787bed6cb4192ba3 Mon Sep 17 00:00:00 2001 From: techbot Date: Thu, 2 Nov 2023 19:08:46 +0100 Subject: [PATCH 069/300] move "version tagging" to it's own action --- .github/workflows/update_workshop.yaml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/update_workshop.yaml b/.github/workflows/update_workshop.yaml index 3ca9f66b1..3f75fbbea 100644 --- a/.github/workflows/update_workshop.yaml +++ b/.github/workflows/update_workshop.yaml @@ -19,22 +19,8 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Update PAC_VERSION - run: | - cat > lua/autorun/pac_version.lua << EOF - if SERVER then - SetGlobalString("pac_version", "${{ github.ref }}") - end - function _G.PAC_VERSION() - return GetGlobalString("pac_version") - end - concommand.Add("pac_version", function() - print(PAC_VERSION()) - end) - EOF - - name: Publish to Steam Workshop - uses: vurv78/gmod-upload@v0.1.3 + uses: PAC3-Server/gmod-upload@v0.1.0 env: STEAM_USERNAME: ${{ secrets.STEAM_NAME }} STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }} From 18251af5eea6948f741e23789898351df0087044 Mon Sep 17 00:00:00 2001 From: techbot Date: Thu, 2 Nov 2023 19:26:50 +0100 Subject: [PATCH 070/300] use master --- .github/workflows/update_workshop.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update_workshop.yaml b/.github/workflows/update_workshop.yaml index 3f75fbbea..a4a781ac1 100644 --- a/.github/workflows/update_workshop.yaml +++ b/.github/workflows/update_workshop.yaml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v2 - name: Publish to Steam Workshop - uses: PAC3-Server/gmod-upload@v0.1.0 + uses: PAC3-Server/gmod-upload@master env: STEAM_USERNAME: ${{ secrets.STEAM_NAME }} STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }} From 6088966d51439941cc7321497c390cf383590187 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 12:31:08 -0400 Subject: [PATCH 071/300] small fix stop search if continuous search is grabbing --- lua/pac3/core/client/parts/lock.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index e51648a93..b17407cfe 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -87,7 +87,7 @@ function PART:OnThink() return end end - if self.ContinuousSearch then + if self.ContinuousSearch and not self.grabbing then self:DecideTarget() end self:CheckEntValidity() @@ -520,4 +520,4 @@ function PART:Initialize() end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From 1c5f3f0ef208d8943c3990b23683eb124df70596 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 12:35:37 -0400 Subject: [PATCH 072/300] Update net_combat.lua include distance excess warning in breaklock feedback message fix mutual grab prevention maybe --- lua/pac3/extra/shared/net_combat.lua | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 627eeb4d9..0283d2049 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1719,7 +1719,7 @@ if SERVER then net.Start("pac_request_lock_break") net.WriteEntity(targ_ent) net.WriteString(lockpart_UID) - net.WriteString(breakup_condition) + net.WriteString("too far!") net.Send(ply) end return @@ -1770,11 +1770,14 @@ if SERVER then end if is_first_time then - if (auth_ent_owner ~= targ_ent and auth_ent_owner.grabbed_ents[targ_ent] == true) then - did_grab = false - need_breakup = true - breakup_condition = breakup_condition .. "mutual grab prevention, " + if targ_ent.grabbed_ents then + if (auth_ent_owner ~= targ_ent and targ_ent.grabbed_ents[auth_ent_owner] == true) then + did_grab = false + need_breakup = true + breakup_condition = breakup_condition .. "mutual grab prevention, " + end end + end if did_grab then From 0500d9485b6e339b9d31d02a35e7a54778673c08 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 12:37:00 -0400 Subject: [PATCH 073/300] validity check --- lua/pac3/core/client/parts/lock.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index b17407cfe..3998a3199 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -211,7 +211,7 @@ do function PART:BreakLock(ent) self.forcebreak = true self.next_allowed_grab = CurTime() + 3 - self.target_ent.IsGrabbedID = nil + if self.target_ent then self.target_ent.IsGrabbedID = nil end self.target_ent = nil self.grabbing = false pac.Message(Color(255, 50, 50), "lock break result:") From dd09af23bfa8a764b64f85b729b3c356dc1dac9b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 12:38:18 -0400 Subject: [PATCH 074/300] specify more in the warning message --- lua/pac3/core/client/parts/lock.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 3998a3199..4f05c91b8 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -236,7 +236,7 @@ do local target_to_release = net.ReadEntity() local uid = net.ReadString() local reason = net.ReadString() - pac.Message(Color(255, 255, 255), "------------CEASE AND DESIST!------------") + pac.Message(Color(255, 255, 255), "------------ CEASE AND DESIST! / BREAK LOCK ------------") MsgC(Color(0,255,255), tostring(target_to_release)) MsgC(Color(255,50,50), " WANTS TO BREAK FREE!!\n") MsgC(Color(255,50,50), "reason:") MsgC(Color(0,255,255), reason .."\n") From 5de91ecf0d4d90666ab07d55900d01f4a7e56d72 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 12:50:06 -0400 Subject: [PATCH 075/300] legacy mode info popups should have at most one at a time --- lua/pac3/core/client/base_part.lua | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 15196ba3d..aee1e1ef6 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -1153,7 +1153,12 @@ end --the popup system function PART:SetupEditorPopup(str, force_open, tbl) - + local legacy_help_popup_hack = false + if not tbl then + legacy_help_popup_hack = false + elseif tbl.from_legacy then + legacy_help_popup_hack = true + end if not IsValid(self) then return end local popup_config_table = tbl or { @@ -1188,7 +1193,7 @@ function PART:SetupEditorPopup(str, force_open, tbl) function tree_node:Think() --if not part.killpopup and ((self.Label:IsHovered() and GetConVar("pac_popups_preferred_location"):GetString() == "pac tree label") or input.IsButtonDown(KEY_F1) or force_open) then if not part.killpopup and ((self.Label:IsHovered() and GetConVar("pac_popups_preferred_location"):GetString() == "pac tree label") or force_open) then - if not self.popuppnl_is_up and not IsValid(self.popupinfopnl) and not part.killpopup then + if not self.popuppnl_is_up and not IsValid(self.popupinfopnl) and not part.killpopup and not legacy_help_popup_hack then self.popupinfopnl = pac.InfoPopup( info_string, popup_config_table @@ -1202,7 +1207,14 @@ function PART:SetupEditorPopup(str, force_open, tbl) end if not IsValid(self.popupinfopnl) then self.popupinfopnl = nil self.popuppnl_is_up = false end end + tree_node:Think() end + if not pnl then + pnl = pac.InfoPopup(info_string,popup_config_table) + self.pace_tree_node.popupinfopnl = pnl + end + pace.legacy_floating_popup_reserved = pnl + return pnl end From 373306dd197cd04ab57fb03c7d377708e5663bc5 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 12:59:03 -0400 Subject: [PATCH 076/300] Update popups_part_tutorials.lua try to regain focus as needed replace indent spaces with tabs --- .../editor/client/popups_part_tutorials.lua | 1839 +++++++++-------- 1 file changed, 923 insertions(+), 916 deletions(-) diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index 6d51bb62d..3b550e345 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -1,8 +1,8 @@ --[[ - This is the framework for popups. This should be expandable for various use cases. - It uses DFrame as a base, overrides the Paint function for a basic fade effect. - - Tutorials will be written here + This is the framework for popups. This should be expandable for various use cases. + It uses DFrame as a base, overrides the Paint function for a basic fade effect. + + Tutorials will be written here ]] @@ -17,84 +17,84 @@ CreateConVar("pac_popups_fade_alpha", "1", FCVAR_ARCHIVE, "The alpha opacity of CreateConVar("pac_popups_text_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") CreateConVar("pac_popups_verbosity", "beginner tutorial", FCVAR_ARCHIVE, "Sets the amount of information added to PAC editor popups. While in development, there will be limited contextual support. If no special information is defined, it will indicate the part size information. Here are the planned modes: \nbeginner tutorial : Basic tutorials about pac parts, for beginners or casual users looking for a quick reference for what a part does\nReference tutorial : doesn't give part tutorials, but still keeps events' tutorial explanations.\n") CreateConVar("pac_popups_preferred_location", "pac tree label", FCVAR_ARCHIVE, "Sets the preferred method of PAC editor popups.\n".. - "pac tree label : the part label on the pac tree\n".. - "part world : if part is base_movable, place it next to the part in the viewport\n".. - "screen : static x,y on screen no matter what. That would be at the center\n".. - "cursor : right on the cursor\n".. - "editor bar : next to the toolbar") + "pac tree label : the part label on the pac tree\n".. + "part world : if part is base_movable, place it next to the part in the viewport\n".. + "screen : static x,y on screen no matter what. That would be at the center\n".. + "cursor : right on the cursor\n".. + "editor bar : next to the toolbar") function pace.OpenPopupConfig() - local master_pnl = vgui.Create("DFrame") - master_pnl:SetTitle("Configure PAC3 popups appearance") - master_pnl:SetSize(400,800) - master_pnl:Center() - - local list_pnl = vgui.Create("DListLayout", master_pnl) - list_pnl:Dock(FILL) - - local basecolor = vgui.Create("DColorMixer") - basecolor:SetSize(400,150) - local col_args = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") - basecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) - function basecolor:ValueChanged(col) - GetConVar("pac_popups_base_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) - GetConVar("pac_popups_base_alpha"):SetString(col.a) - end - local basecolor_pulse = vgui.Create("DNumSlider") - basecolor_pulse:SetMax(255) - basecolor_pulse:SetMin(0) - - if isnumber(GetConVar("pac_popups_base_color_pulse"):GetInt()) then - basecolor_pulse:SetValue(GetConVar("pac_popups_base_color_pulse"):GetInt()) - else - basecolor_pulse:SetValue(0) - end - - basecolor_pulse:SetText("base pulse") - function basecolor_pulse:OnValueChanged(val) - val = math.Round(tonumber(val),0) - GetConVar("pac_popups_base_color_pulse"):SetInt(val) - end - - local fadecolor = vgui.Create("DColorMixer") - fadecolor:SetSize(400,150) - col_args = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") - fadecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) - function fadecolor:ValueChanged(col) - GetConVar("pac_popups_fade_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) - GetConVar("pac_popups_fade_alpha"):SetString(col.a) - end - - local textcolor = vgui.Create("DColorMixer") - textcolor:SetSize(400,150) - col_args = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") - - if isnumber(col_args[1]) then - textcolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) - end - - textcolor:SetAlphaBar(false) - function textcolor:ValueChanged(col) - GetConVar("pac_popups_text_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) - end - - local invertcolor_btn = vgui.Create("DButton") - invertcolor_btn:SetSize(400,30) - invertcolor_btn:SetText("Use text invert color (experimental)") - function invertcolor_btn:DoClick() - GetConVar("pac_popups_text_color"):SetString("invert") - end - - local preview_pnl = vgui.Create("DLabel") - preview_pnl:SetSize(400,170) - preview_pnl:SetText("") - local label_text = "Popup preview! The text will look like this." - - local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + local master_pnl = vgui.Create("DFrame") + master_pnl:SetTitle("Configure PAC3 popups appearance") + master_pnl:SetSize(400,800) + master_pnl:Center() + + local list_pnl = vgui.Create("DListLayout", master_pnl) + list_pnl:Dock(FILL) + + local basecolor = vgui.Create("DColorMixer") + basecolor:SetSize(400,150) + local col_args = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + basecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + function basecolor:ValueChanged(col) + GetConVar("pac_popups_base_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_popups_base_alpha"):SetString(col.a) + end + local basecolor_pulse = vgui.Create("DNumSlider") + basecolor_pulse:SetMax(255) + basecolor_pulse:SetMin(0) + + if isnumber(GetConVar("pac_popups_base_color_pulse"):GetInt()) then + basecolor_pulse:SetValue(GetConVar("pac_popups_base_color_pulse"):GetInt()) + else + basecolor_pulse:SetValue(0) + end + + basecolor_pulse:SetText("base pulse") + function basecolor_pulse:OnValueChanged(val) + val = math.Round(tonumber(val),0) + GetConVar("pac_popups_base_color_pulse"):SetInt(val) + end + + local fadecolor = vgui.Create("DColorMixer") + fadecolor:SetSize(400,150) + col_args = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + fadecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + function fadecolor:ValueChanged(col) + GetConVar("pac_popups_fade_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_popups_fade_alpha"):SetString(col.a) + end + + local textcolor = vgui.Create("DColorMixer") + textcolor:SetSize(400,150) + col_args = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + + if isnumber(col_args[1]) then + textcolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255)) + end + + textcolor:SetAlphaBar(false) + function textcolor:ValueChanged(col) + GetConVar("pac_popups_text_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) + end + + local invertcolor_btn = vgui.Create("DButton") + invertcolor_btn:SetSize(400,30) + invertcolor_btn:SetText("Use text invert color (experimental)") + function invertcolor_btn:DoClick() + GetConVar("pac_popups_text_color"):SetString("invert") + end + + local preview_pnl = vgui.Create("DLabel") + preview_pnl:SetSize(400,170) + preview_pnl:SetText("") + local label_text = "Popup preview! The text will look like this." + + local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 local a1 = GetConVar("pac_popups_base_alpha"):GetFloat() - local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() @@ -102,50 +102,50 @@ function pace.OpenPopupConfig() if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) - local preview_refresh_btn = vgui.Create("DButton") - preview_refresh_btn:SetSize(400,30) - preview_refresh_btn:SetText("Refresh") - local oldpaintfunc = master_pnl.Paint - local invis_frame = false - function preview_refresh_btn:DoClick() - invis_frame = not invis_frame - if invis_frame then master_pnl.Paint = nil else master_pnl.Paint = oldpaintfunc end - rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") - r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 - a1 = GetConVar("pac_popups_base_alpha"):GetFloat() - pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() - rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") - r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 - a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() - rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") - if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end - r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) - end - - function preview_pnl:Paint( w, h ) + local preview_refresh_btn = vgui.Create("DButton") + preview_refresh_btn:SetSize(400,30) + preview_refresh_btn:SetText("Refresh") + local oldpaintfunc = master_pnl.Paint + local invis_frame = false + function preview_refresh_btn:DoClick() + invis_frame = not invis_frame + if invis_frame then master_pnl.Paint = nil else master_pnl.Paint = oldpaintfunc end + rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") + r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 + a1 = GetConVar("pac_popups_base_alpha"):GetFloat() + pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") + r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 + a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() + rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") + if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end + r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) + end + + function preview_pnl:Paint( w, h ) --base layer local sine = 0.5 + 0.5*math.sin(CurTime()*2) draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) ) for band=0,w,1 do --per-pixel fade fade = 1 - (1/w * band * 1) - fade = math.pow(fade,2) + fade = math.pow(fade,2) draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) end draw.DrawText(label_text, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,255)) end - list_pnl:Add(Label("Base color")) - list_pnl:Add(basecolor) - list_pnl:Add(basecolor_pulse) - list_pnl:Add(Label("Gradient color")) - list_pnl:Add(fadecolor) - list_pnl:Add(Label("Text color")) - list_pnl:Add(textcolor) - list_pnl:Add(invertcolor_btn) - list_pnl:Add(preview_refresh_btn) - list_pnl:Add(preview_pnl) - master_pnl:MakePopup() + list_pnl:Add(Label("Base color")) + list_pnl:Add(basecolor) + list_pnl:Add(basecolor_pulse) + list_pnl:Add(Label("Gradient color")) + list_pnl:Add(fadecolor) + list_pnl:Add(Label("Text color")) + list_pnl:Add(textcolor) + list_pnl:Add(invertcolor_btn) + list_pnl:Add(preview_refresh_btn) + list_pnl:Add(preview_pnl) + master_pnl:MakePopup() end @@ -153,21 +153,21 @@ end concommand.Add("pac_popups_settings", function() pace.OpenPopupConfig() end) --[[ - info_string, main string - { info about where to position the label - pac_part = part, that would be the pac part if applicable - obj = self.Label, that would be the positioning target - obj_type = "pac tree label", what type of thing is the target, for positioning - pac tree label = on the editor, needs to realign when scrolling - part world = if base_movable, place it next to the part in the view, if not, owner entity - screen = static x,y on screen no matter what, needs the further x,y args specified outside - cursor = right on the cursor - editor bar = next to the toolbar - hoverfunc = function() end, a function to run when hovering. - doclickfunc = function() end, a function to run when clicking - panel_exp_width = 900, panel_exp_height = 200 prescribed dimensions to expand to - }, - self:LocalToScreen() x,y + info_string, main string + { info about where to position the label + pac_part = part, that would be the pac part if applicable + obj = self.Label, that would be the positioning target + obj_type = "pac tree label", what type of thing is the target, for positioning + pac tree label = on the editor, needs to realign when scrolling + part world = if base_movable, place it next to the part in the view, if not, owner entity + screen = static x,y on screen no matter what, needs the further x,y args specified outside + cursor = right on the cursor + editor bar = next to the toolbar + hoverfunc = function() end, a function to run when hovering. + doclickfunc = function() end, a function to run when clicking + panel_exp_width = 900, panel_exp_height = 200 prescribed dimensions to expand to + }, + self:LocalToScreen() x,y ]] @@ -175,10 +175,10 @@ concommand.Add("pac_popups_settings", function() pace.OpenPopupConfig() end) --[[ we generally have two routes to create a popup: part and direct at the part level we can tell pac to try to create a popup -pac.AttachInfoPopupToPart(part : obj, string : str, table : tbl) --naming scheme close to a general pac function - PART:AttachEditorPopup(string : str, bool : flash, table : tbl) --calls the generic base setup in base_part, shouldn't be overridden - PART:SetupEditorPopup(str, force_open, tbl) --calls the specific setup, can be overridden for different classes - pac.InfoPopup(str, tbl, x, y) --creates the vgui element +pac.AttachInfoPopupToPart(part : obj, string : str, table : tbl) --naming scheme close to a general pac function + PART:AttachEditorPopup(string : str, bool : flash, table : tbl) --calls the generic base setup in base_part, shouldn't be overridden + PART:SetupEditorPopup(str, force_open, tbl) --calls the specific setup, can be overridden for different classes + pac.InfoPopup(str, tbl, x, y) --creates the vgui element we can directly create an independent editor popup pac.InfoPopup(str, tbl, x, y) @@ -186,53 +186,53 @@ pac.InfoPopup(str, tbl, x, y) function pac.InfoPopup(str, tbl, x, y) - if not GetConVar("pac_popups_enable"):GetBool() then return end - local x = x - local y = y - if not x or not y then - x = ScrW()/2 + math.Rand(-300,300) - y = ScrH()/2 + math.Rand(-300,0) - end - tbl = tbl or {} - if not tbl.obj then - if tbl.obj_type == "pac tree label" then - tbl.obj = tbl.pac_part.pace_tree_node - elseif tbl.obj_type == "part world" then - tbl.obj = tbl.pac_part - end - end - - str = str or "" - local verbosity = GetConVar("pac_popups_verbosity"):GetString() + if not GetConVar("pac_popups_enable"):GetBool() then return end + local x = x + local y = y + if not x or not y then + x = ScrW()/2 + math.Rand(-300,300) + y = ScrH()/2 + math.Rand(-300,0) + end + tbl = tbl or {} + if not tbl.obj then + if tbl.obj_type == "pac tree label" then + tbl.obj = tbl.pac_part.pace_tree_node + elseif tbl.obj_type == "part world" then + tbl.obj = tbl.pac_part + end + end + + str = str or "" + local verbosity = GetConVar("pac_popups_verbosity"):GetString() local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ") local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255 local a1 = GetConVar("pac_popups_base_alpha"):GetFloat() - local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() + local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt() local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ") local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255 local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat() local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) - + local pnl = vgui.Create("DFrame") - local txt_zone = vgui.Create("RichText", pnl) - - --function pnl:PerformLayout() end - pnl:SetTitle("") pnl:SetText("") pnl:ShowCloseButton( false ) - txt_zone:SetPos(5,25) - txt_zone:SetContentAlignment( 7 ) --top left - - if tbl.pac_part then - if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then - if pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName] then - str = str .. "\n\n" .. pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName].popup_tutorial .. "\n" - end - end - end - - + local txt_zone = vgui.Create("RichText", pnl) + + --function pnl:PerformLayout() end + pnl:SetTitle("") pnl:SetText("") pnl:ShowCloseButton( false ) + txt_zone:SetPos(5,25) + txt_zone:SetContentAlignment( 7 ) --top left + + if tbl.pac_part then + if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then + if pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName] then + str = str .. "\n\n" .. pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName].popup_tutorial .. "\n" + end + end + end + + pnl.hoverfunc = function() end pnl.doclickfunc = function() end pnl.titletext = "Click for more information! (or F1)" @@ -243,58 +243,58 @@ function pac.InfoPopup(str, tbl, x, y) function pnl:FixPartReference(tbl) if not tbl or table.IsEmpty(tbl) then self:Remove() end if tbl.pac_part then tbl.obj = tbl.pac_part.pace_tree_node end - + end function pnl:MoveToObj(tbl) - --self:MakePopup() - if tbl.obj_type == "pac tree label" then - if not IsValid(tbl.obj) then - self:FixPartReference(tbl) - self:SetPos(x,y) - else - local x,y = tbl.obj:LocalToScreen() - x = pace.Editor:GetWide() - --print(pace.Editor:GetWide(), input.GetCursorPos()) - self:SetPos(x,y) - end - if pace then - if pace.Editor then - if pace.Editor.IsLeft then - if not pace.Editor:IsLeft() then - self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) - else - self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) - end - end - end - end - - elseif tbl.obj_type == "part world" then - if tbl.pac_part then - local global_position = tbl.pac_part:GetRootPart():GetOwner():GetPos() + tbl.pac_part:GetRootPart():GetOwner():OBBCenter()*1.5 - if tbl.pac_part.GetWorldPosition then - global_position = tbl.pac_part:GetWorldPosition() --if part is a base_movable, we'll get its position right away - elseif tbl.pac_part:GetParent().GetWorldPosition then - global_position = tbl.pac_part:GetParent():GetWorldPosition() --if part isn't but has a base_movable parent, get that - end - local scr_tbl = global_position:ToScreen() - self:SetPos(scr_tbl.x, scr_tbl.y) - end - - elseif tbl.obj_type == "screen" then - self:SetPos(x,y) - - elseif tbl.obj_type == "cursor" then - self:SetPos(input.GetCursorPos()) - - elseif tbl.obj_type == "editor bar" then - if not pace.Editor:IsLeft() then - self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) - else - self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) - end - end + --self:MakePopup() + if tbl.obj_type == "pac tree label" then + if not IsValid(tbl.obj) then + self:FixPartReference(tbl) + self:SetPos(x,y) + else + local x,y = tbl.obj:LocalToScreen() + x = pace.Editor:GetWide() + --print(pace.Editor:GetWide(), input.GetCursorPos()) + self:SetPos(x,y) + end + if pace then + if pace.Editor then + if pace.Editor.IsLeft then + if not pace.Editor:IsLeft() then + self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) + else + self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) + end + end + end + end + + elseif tbl.obj_type == "part world" then + if tbl.pac_part then + local global_position = tbl.pac_part:GetRootPart():GetOwner():GetPos() + tbl.pac_part:GetRootPart():GetOwner():OBBCenter()*1.5 + if tbl.pac_part.GetWorldPosition then + global_position = tbl.pac_part:GetWorldPosition() --if part is a base_movable, we'll get its position right away + elseif tbl.pac_part:GetParent().GetWorldPosition then + global_position = tbl.pac_part:GetParent():GetWorldPosition() --if part isn't but has a base_movable parent, get that + end + local scr_tbl = global_position:ToScreen() + self:SetPos(scr_tbl.x, scr_tbl.y) + end + + elseif tbl.obj_type == "screen" then + self:SetPos(x,y) + + elseif tbl.obj_type == "cursor" then + self:SetPos(input.GetCursorPos()) + + elseif tbl.obj_type == "editor bar" then + if not pace.Editor:IsLeft() then + self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) + else + self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY()) + end + end end @@ -303,34 +303,34 @@ function pac.InfoPopup(str, tbl, x, y) pnl.tbl = tbl pnl:MoveToObj(tbl) if tbl.hoverfunc then - if tbl.hoverfunc == "open" then - pnl.hoverfunc = function() - pnl.hovering = true - pnl:keep_alive(3) - if not pnl.hovering and not pnl.expand then - pnl.resizing = true - pnl.expand = true - - pnl.ResizeStartTime = CurTime() - pnl.ResizeEndTime = CurTime() + 0.3 - end - end - else - pnl.hoverfunc = tbl.hoverfunc - end + if tbl.hoverfunc == "open" then + pnl.hoverfunc = function() + pnl.hovering = true + pnl:keep_alive(3) + if not pnl.hovering and not pnl.expand then + pnl.resizing = true + pnl.expand = true + + pnl.ResizeStartTime = CurTime() + pnl.ResizeEndTime = CurTime() + 0.3 + end + end + else + pnl.hoverfunc = tbl.hoverfunc + end end pnl.exp_height = tbl.panel_exp_height or 400 pnl.exp_width = tbl.panel_exp_width or 800 end - - pnl.exp_height = pnl.exp_height or 400 - pnl.exp_width = pnl.exp_width or 800 + + pnl.exp_height = pnl.exp_height or 400 + pnl.exp_width = pnl.exp_width or 800 pnl:SetSize(200,20) - pnl.DeathTimeAdd = 0 - if GetConVar("pac_popups_preserve_on_autofade"):GetBool() then - pnl.DeathTimeAdd = 240 - end + pnl.DeathTimeAdd = 0 + if GetConVar("pac_popups_preserve_on_autofade"):GetBool() then + pnl.DeathTimeAdd = 240 + end pnl.DeathTime = CurTime() + 13 pnl.FadeTime = CurTime() + 10 pnl.FadeDuration = pnl.DeathTime - pnl.FadeTime @@ -344,36 +344,36 @@ function pac.InfoPopup(str, tbl, x, y) pnl:SetAlpha(255) end - --the header needs a label to click on to open the popup - function pnl:DoClick() - - if input.IsKeyDown(KEY_F1) or (self:IsHovered() and not txt_zone:IsHovered()) then - pnl.expand = not pnl.expand - pnl.ResizeStartTime = CurTime() - pnl.ResizeEndTime = CurTime() + 0.3 - pnl.resizing = true - end - - pnl:keep_alive(3) - pnl.doclickfunc() - end - - --handle positioning, expanding and termination + --the header needs a label to click on to open the popup + function pnl:DoClick() + + if input.IsKeyDown(KEY_F1) or (self:IsHovered() and not txt_zone:IsHovered()) then + pnl.expand = not pnl.expand + pnl.ResizeStartTime = CurTime() + pnl.ResizeEndTime = CurTime() + 0.3 + pnl.resizing = true + end + + pnl:keep_alive(3) + pnl.doclickfunc() + end + + --handle positioning, expanding and termination function pnl:Think() self:MoveToObj(tbl) if input.IsButtonDown(KEY_P) and input.IsButtonDown(KEY_LALT) then --auto-kill if alt-p - tbl.pac_part.killpopup = true + tbl.pac_part.killpopup = true self:Remove() end - if input.IsMouseDown(MOUSE_RIGHT) then - if self:IsHovered() and not txt_zone:IsHovered() then - self:Remove() - end - end - + if input.IsMouseDown(MOUSE_RIGHT) then + if self:IsHovered() and not txt_zone:IsHovered() then + self:Remove() + end + end + self.F1_doclick_possible_at = self.F1_doclick_possible_at or 0 - self.mouse_doclick_possible_at = self.mouse_doclick_possible_at or 0 + self.mouse_doclick_possible_at = self.mouse_doclick_possible_at or 0 if input.IsButtonDown(KEY_F1) then --expand if press F1, but only after a delay if self.F1_doclick_possible_at == 0 then @@ -384,7 +384,7 @@ function pac.InfoPopup(str, tbl, x, y) self:DoClick() end end - if input.IsMouseDown(MOUSE_LEFT) and self:IsHovered() or self:IsChildHovered() then --expand if press mouse left + if input.IsMouseDown(MOUSE_LEFT) and self:IsHovered() or self:IsChildHovered() then --expand if press mouse left if self.mouse_doclick_possible_at == 0 then self.mouse_doclick_possible_at = CurTime() + 1 end @@ -393,29 +393,29 @@ function pac.InfoPopup(str, tbl, x, y) self:DoClick() end end - if not input.IsMouseDown(MOUSE_LEFT) then - self.mouse_doclick_possible_at = CurTime() - end + if not input.IsMouseDown(MOUSE_LEFT) then + self.mouse_doclick_possible_at = CurTime() + end - if not IsValid(tbl.pac_part) and tbl.pac_part ~= false then self:Remove() end - self.exp_width = self.exp_width or 800 - self.exp_height = self.exp_height or 500 + if not IsValid(tbl.pac_part) and tbl.pac_part ~= false then self:Remove() end + self.exp_width = self.exp_width or 800 + self.exp_height = self.exp_height or 500 --resizing code, initially the label should start small if self.resizing then local expand_frac_w = math.Clamp((self.ResizeEndTime - CurTime()) / 0.3,0,1) local expand_frac_h = math.Clamp((self.ResizeEndTime - (CurTime() - 0.5)) / 0.5,0,1) - local width,height + local width,height if not self.expand then - width = 200 + (self.exp_width - 200)*(expand_frac_w) - height = 20 + (self.exp_height - 20)*(expand_frac_h) + width = 200 + (self.exp_width - 200)*(expand_frac_w) + height = 20 + (self.exp_height - 20)*(expand_frac_h) if self.hovering and not self:IsHovered() then self.hovering = false end else - width = 200 + (self.exp_width - 200)*(1 - expand_frac_h) - height = 20 + (self.exp_height - 20)*(1 - expand_frac_w) - + width = 200 + (self.exp_width - 200)*(1 - expand_frac_h) + height = 20 + (self.exp_height - 20)*(1 - expand_frac_w) + end - self:SetSize(width,height) - txt_zone:SetSize(width-10,height-30) + self:SetSize(width,height) + txt_zone:SetSize(width-10,height-30) end @@ -423,27 +423,34 @@ function pac.InfoPopup(str, tbl, x, y) self.fade_factor = math.pow(self.fade_factor, 3) if CurTime() > self.DeathTime + self.DeathTimeAdd then - self:Remove() - end + self:Remove() + end if pace.Focused then self:SetAlpha(255*self.fade_factor) end if self:IsHovered() then self:keep_alive(1) self.hoverfunc() if input.IsMouseDown(MOUSE_RIGHT) then - if tbl.pac_part then - tbl.pac_part.killpopup = true - end + if tbl.pac_part then + tbl.pac_part.killpopup = true + end self:Remove() end end if not pace.Focused then + self.has_focus = false self:AlphaTo(0, 0.1, 0) self:KillFocus() self:SetMouseInputEnabled(false) self:SetKeyBoardInputEnabled(false) gui.EnableScreenClicker(false) + else + if not self.has_focus then + self:RequestFocus() + self:MakePopup() + self.has_focus = true + end end function pnl:OnRemove() @@ -453,11 +460,11 @@ function pac.InfoPopup(str, tbl, x, y) end end - pnl.doclickfunc = tbl.doclickfunc or function() end + pnl.doclickfunc = tbl.doclickfunc or function() end - + - pnl.exp_height = tbl.panel_exp_height + pnl.exp_height = tbl.panel_exp_height pnl.exp_width = tbl.panel_exp_width --cast the convars values @@ -473,24 +480,24 @@ function pac.InfoPopup(str, tbl, x, y) g3 = tonumber(g3) b3 = tonumber(b3) - local col = Color(r3,g3,b3,255) - - --txt_zone:SetFont("DermaDefaultBold") - function txt_zone:PerformLayout() - txt_zone:SetBGColor(0,0,0,0) - txt_zone:SetFGColor(col) - end - - function txt_zone:Think() - if self:IsHovered() then - pnl:keep_alive(3) - end - end - - txt_zone:SetText("") - txt_zone:AppendText(str) - - txt_zone:SetVerticalScrollbarEnabled(true) + local col = Color(r3,g3,b3,255) + + --txt_zone:SetFont("DermaDefaultBold") + function txt_zone:PerformLayout() + txt_zone:SetBGColor(0,0,0,0) + txt_zone:SetFGColor(col) + end + + function txt_zone:Think() + if self:IsHovered() then + pnl:keep_alive(3) + end + end + + txt_zone:SetText("") + txt_zone:AppendText(str) + + txt_zone:SetVerticalScrollbarEnabled(true) function pnl:Paint( w, h ) @@ -499,13 +506,13 @@ function pac.InfoPopup(str, tbl, x, y) --base layer local sine = 0.5 + 0.5*math.sin(CurTime()*2) draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) ) - --draw.RoundedBox( 0, 0, 0, 1, h, Color( 88, 179, 255, 255)) + --draw.RoundedBox( 0, 0, 0, 1, h, Color( 88, 179, 255, 255)) for band=0,w,1 do --per-pixel fade fade = 1 - (1/w * band * self.fade_factor) fade2 = math.pow(fade,3) - fade = math.pow(fade,2) - --draw.RoundedBox( c, x, y, w, h, color ) + fade = math.pow(fade,2) + --draw.RoundedBox( c, x, y, w, h, color ) draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) --draw.RoundedBox( 0, band, 0, 1, 1, Color( 88, 179, 255, 255)) --draw.RoundedBox( 0, band, h-1, 1, 1, Color( 0, 0, 0, 255)) @@ -523,7 +530,7 @@ function pac.InfoPopup(str, tbl, x, y) end function pac.AttachInfoPopupToPart(obj, str, tbl) - if not obj then return end + if not obj then return end obj:AttachEditorPopup(str, true, tbl) end @@ -540,633 +547,633 @@ function pace.FlushInfoPopups() end --[[ - part classes info + part classes info ideally we should have: 1-a tooltip form (7 words max) - e.g. projectile: throws missiles into the world + e.g. projectile: throws missiles into the world 2-a fuller form for the popups (4-5 sentences or more if needed) - e.g. projectile: the projectile part creates physical entities and launches them forward.\n - the entity has physics but it can be clientside (visual) or serverside (physical)\n - by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n - the entity can do damage but servers can restrict that. + e.g. projectile: the projectile part creates physical entities and launches them forward.\n + the entity has physics but it can be clientside (visual) or serverside (physical)\n + by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n + the entity can do damage but servers can restrict that. but then again we should probably look for better ways for the full-length explanations, - maybe grab some of them from the wiki or have a web browser for the wiki + maybe grab some of them from the wiki or have a web browser for the wiki ]] do - - pace.TUTORIALS = pace.TUTORIALS or {} - pace.TUTORIALS.PartInfos = { - - ["trail"] = { - tooltip = "leaves a trail behind", - popup_tutorial = - "the trail part creates beams along its path to make a trail\n".. - "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. - "you can set how it looks, how big it becomes etc." - }, - - ["trail2"] = { - tooltip = "leaves a trail behind", - popup_tutorial = - "the trail part creates beams along its path to make a trail\n".. - "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. - "you can set how it looks, how big it becomes etc." - }, - - ["sound"] = { - tooltip = "plays sounds", - popup_tutorial = "plays sounds in wav, mp3, ogg formats.\n".. - "for random sounds, paste each path separated by semicolons e.g. sound1.wav;sound3.wav;sound8.wav\n".. - "we have a special bracket notation for sound lists: sound[1,50].wav\n\n".. - "some of the parameters to know:\n".. - "sound level affects the falloff along with volume; a good starting point is 70 level, 0.6 volume\n".. - "overlapping means it doesn't get cut off if hidden\n".. - "sequential plays sounds in a list in order once you have the semicolon or bracket notation;\n".. - "\tthe steps is how much you progress by each activation. it can go one by one (1), every other sound (2+), stay there (0) or go back (negative values)" - }, - - ["sound2"] = { - tooltip = "plays web sounds", - popup_tutorial = "plays sounds in wav, mp3, ogg formats, with the option to download sound files from the internet\n".. - "people usually use dropbox, google drive, other cloud hosts or their own server host to store and distribute their files. each has its limitations.\n\n".. - "WARNING! Downloading and using these sounds is only possible in the chromium branch of garry's mod!\n\n".. - "to randomize sounds, we still have the same notations as legacy sound:\n".. - "\tsemicolon notation e.g. path1.wav;https://url1.wav;https://url2.wav\n".. - "\tbracket notation e.g. sound[1,50].wav\n\n".. - "some of the parameters to know, you'll already know some of them from legacy sound:\n".. - "-radius affects the falloff distance\n".. - "-overlapping means it doesn't get cut off if hidden\n".. - "-sequential plays sounds in a list in order" - }, - - ["ogg"] = { - tooltip = "plays ogg sounds (broken)", - popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." - }, - - ["webaudio"] = { - tooltip = "plays web sounds (legacy)", - popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." - }, - - - ["halo"] = { - tooltip = "makes models glow", - popup_tutorial = - "This part creates a halo around a model entity.\n".. - "That could be your playermodel or a pac3 model, but for some reason it doesn't work on your player if you have an entity part.\n".. - "passes is the thickness of the halo, amount is the brightness, blur x and y spread out the shape" - }, - - ["bodygroup"] = { - tooltip = "changes body parts on supported models", - popup_tutorial = - "Bodygroups are a Source engine model feature which allows to easily show or hide different pieces of a model\n".. - "those are often used for accessories and styles. But it won't work unless your model has bodygroups.\n".. - "this part does exactly that. but you might do that directly with the model part or entity part" - }, - - ["holdtype"] = { - tooltip = "changes your animation set", - popup_tutorial = - "this part allows you to change animations played in individual movement slots, so you can mix and match from the available animations in your playermodel\n".. - "a holdtype is a set of animations for holding one kind of weapon, such as one-handed pistols vs two-handed revolvers, rifles, melee weapons etc.\n".. - "The option is also in the normal animation part, but this part goes in more detail in choosing different animations" - }, - - ["clip"] = { - tooltip = "cuts a model in a plane (legacy)", - popup_tutorial = - "This part cuts off one side of the model in rendering.\n".. - "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." - }, - - ["clip2"] = { - tooltip = "cuts a model in a plane", - popup_tutorial = - "This part cuts off one side of the model in rendering.\n".. - "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." - }, - - ["model"] = { - tooltip = "places a model (legacy)", - popup_tutorial = "The old model part still does the basic things you need a model to do" - }, - - ["model2"] = { - tooltip = "places a model", - popup_tutorial = - "The model part creates a clientside entity to draw a model locally.\n".. - "Being a base_movable, parts inside it will be physically arented to it.\n".. - "therefore, it can act as a regrouper, rail or anchoring point for your pac structuring needs, although you probably shouldn't abuse it.\n".. - "It can accept most modifiers and play animations, if present or referenced in the model.\n\n".. - "It can load MDL zips or OBJ files from a direct link to a server host or cloud provider, allowing you to use pretty much any model as long as it's the right format for Source. And on that subject, you would do well to install Crowbar, as well as Blender with Blender Source Tools, if you want to extract and edit models. Consult the valve developer community wiki for more information about QC; I view this as common knowledge rather than the purview of pac3 so you have to do some research." - }, - - ["material"] = { - tooltip = "defines a material (legacy)", - popup_tutorial = - "the old material still works as it says. it lets you define some VMT parameters for a material" - }, - - ["material_3d"] = { - tooltip = "defines a material for models", - popup_tutorial = - "This part creates a VMT material of the shader type VertexLitGeneric.\n".. - "If you have experience in Source engine things, you probably should know what some of these do, I won't expound fully but here's the essential summary anyway:\n\n".. - "\tbase texture is the base image. It's basically just color pixels.\n".. - "\tbump map / normal map is a relief that gives a texture on the surface. It uses a distinctly purple pixel format; it's not color but directional information\n".. - "\tdetail is a second base image added on top to modify the pixels. It's usually grayscale because we don't need to add color to give more grit to an image\n".. - "\tself illumination and emissive blend are glowing layers. emissive blend is more complex and needs three necessary components before it starts to work properly.\n".. - "\tenvironment map is a layer for pre-baked room reflection, by default env_cubemap tries to get the nearest cubemap but you can choose another texture, although cubemaps are a very specific format\n".. - "\tphong is a layer of dynamic light reflections\n\n".. - "If you want to edit a material, you can load its VMT with a right click on \"load vmt\", then select the right material override\n".. - "Reminder that transparent textures may need additive or some form of translucent setting on the model and on the material.\n\n".. - "For more information, search the Valve developer community site or elsewhere. Many material features are standard, and if you want to push this part to the limit, the extra research will be worth it." - }, - - ["material_2d"] = { - tooltip = "defines a material for sprites", - popup_tutorial = - "This part creates a VMT material of the shader type UnlitGeneric. This is used by particles and sprites.\n".. - "For transparent textures, use additive or vertex alpha/vertex color (for particles and decals). Some VTF or PNG textures have an alpha channel, but many just have a black background meant for additive rendering.\n\n".. - "For more information, search the Valve developer community site" - }, - - ["material_refract"] = { - tooltip = "defines a refracting material", - popup_tutorial = - "This part creates a VMT material of the shader type Refract. As with other material parts, you would find it useful to name the material to use that in multiple models' \"material\" fields\n".. - "In a way, it doesn't work by surface, but by silhouette. But the surface does determine how the refraction occurs. Setting a base texture creates a flat wall behind it that can distort in interesting ways but it'll replace the view behind.\n".. - "The normal section does most of the heavy lifting. This is where the image behind the material gets refracted according to the surface. You can blend between two normal maps in greater detail.\n".. - "Your model needs to be set to \"translucent\" rendering mode for this to work because the shader is in a multi-step rendering process.\n\n".. - "For more information, search the Valve developer community site" - }, - - ["material_eye refract"] = { - tooltip = "defines a refracting eye material", - popup_tutorial = - "This part creates a VMT material of the shader type EyeRefract.\n".. - "It's tricky to use because of how it involves projections and entity eye position, but you can more easily get something working on premade HL2 or other Source games' characters with QC eyes." - }, - - ["submaterial"] = { - tooltip = "applies a material on a submaterial zone", - popup_tutorial = - "Models can be comprised of multiple materials in different areas. This part can replace the material applied to one of these zones.\n".. - "Depending on how the model was made, it might correspond to what you want, or it might not.\n".. - "As usual, as with other model modifiers your expectations should always line up with the quality of the model you're using." - }, - - ["bone"] = { - tooltip = "changes a bone (legacy)", - popup_tutorial = - "The legacy bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." - }, - - ["bone2"] = { - tooltip = "changes a bone (legacy)", - popup_tutorial = - "The legacy experimental bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." - }, - - ["bone3"] = { - tooltip = "changes a bone", - popup_tutorial = - "This part modifies a model's bone. It can move relative to the parent bone, scale, and rotate.\n".. - "Follow part forces the bone to relocate to a base_movable part. Might have issues if you successively follow part multiple related bones. You could try to fix that by changing draw orders of the follow parts and bones." - }, - - ["player_config"] = { - tooltip = "sets your player entity's behaviour", - popup_tutorial = - "This part has access to some of your player's behavior, like whether you will play footsteps, the chat animation etc.\n".. - "Some of these may or may not work as intended..." - }, - - ["light"] = { - tooltip = "lights up the world (legacy)", - popup_tutorial = - "This legacy part still does the basic thing you want from a light, but the new light part is more fully-featured, for the most part.\n".. - "There is one thing it does that the new part doesn't, and that's styles." - }, - - ["light2"] = { - tooltip = "lights up models or the world", - popup_tutorial = - "This part creates a dynamic light that can illuminate models or the world independently.\n".. - "There are some options for the light's falloff shape (inner and outer angles).\n".. - "Its brightness works by magnitude and size, not multiplication. Which means you can still have light at 0 or lower brightness." - }, - - ["event"] = { - tooltip = "activates or deactivates other parts", - popup_tutorial = - "This part hides or shows connected parts when certain conditions are met. We won't describe them in this tutorial, you'll have to read them individually. The essential behaviour remains common accross events.\n\n".. - "Domain, in other words, which parts get affected:\n".. - "\t1-Default: The event will command its direct parent. Because parts can contain other parts, this includes the event itself, and parts beside the event too. While this is not usually a problem, you have to be aware of that.\n".. - "\t2-ACO: Affect Children Only. The event will command parts inside it, not beside, not above. This is the first step to isolate your setup and have clean logic in your pac.\n".. - "\t3-Targeted: The event gets wired to a part directly, including its children of course. This is accessed when you select a part in the \"targeted part\" field, which has an unfortunate name because there's still the old \"target part\" parameter\n\n".. - "Some events, like is_touching, can select an external \"target\" to use as a point to gather information.\n\n".. - "Operators:\n".. - "Operators are just how the event asks the question to determine when to activate or deactivate. Just read the event the same way as it asks the question: is my source equal to the value? can I find this text in my source?\n".. - "\tnumber-related operators: equal, above, below, equal or above, equal or below\n".. - "\tstring-related operators: equal, find, find simple\n".. - "There's still a caveat. If you use the wrong type of operator for your event, it will NOT work. Please trust the editor autopilot when it automatically changes your operator to a good one. Do not change it unless you know what you're doing." - }, - - ["sprite"] = { - tooltip = "draws a 2D texture", - popup_tutorial = - "Sprites are another Source engine thing which are useful for some point effects. Most textures being for model surfaces will look like squares if drawn flat, but sprite and particle textures are made specially for this purpose.\n".. - "They should have a transparent background or black background. The difference is because of rendering modes or blend modes.\n".. - "Additive rendering adds pixels' values. So, bright pixels will be more visible, but dark pixels end up being faded or invisible because their amounts are low." - }, - - ["fog"] = { - tooltip = "colors a model with fog", - popup_tutorial = - "This strange modifier renders a fog-like color over a model. Not in the world, not inside the model, but over its surface.\n".. - "For that reason, you might do well to change rendering-related values like blend mode on the host model's side\n".. - "It requires to be attached to a base_drawable part, keep in mind the start and end values are multiplied by 100 in post for some reason.".. - "start is the distance where the fog starts to appear outside, end is where the fog is thickest." - }, - - ["force"] = { - tooltip = "provides physical force", - popup_tutorial = - "This part tries to tell the server to do a force impulse, or continually request small impulses for a continuous force. It should work for most physics props, some item and ammo entities, players and NPCs. But it may or may not be allowed on the server due to server settings: pac_sv_force.\n\n".. - "There's a base force and an added xyz vector force. You have options to choose how they're applied. Aside from that, the part's area is mainly for detection.\n\n".. - "For the Base force, Radial is from to self to each entity, Locus is from locus to each entity, Local is forward of self\n\n".. - "For the Vector force, Global is on world coordinates, Local is on self's coordinates, Radial is relative to the line from the self or locus toward the entity (Used in orbits/vortex/circular motion with centrifugal force)\n\n".. - "NPCs might have weird movement so don't expect much from pushing them." - }, - - ["faceposer"] = { - tooltip = "Adjusts multiple facial expression slots", - popup_tutorial = - "This part gives access to multiple facial expressions defined by your model's shape keys in one part.\n".. - "The flex multiplier affects the whole model, so you should avoid stacking faceposers if they have different multipliers." - }, - - ["command"] = { - tooltip = "Runs a console command or lua code", - popup_tutorial = "This part attempts to run a command or Lua code on your client. It may or may not work depending on the command and some servers don't allow you to run clientside lua, because of sv_allowcslua 0.\n\n".. - "Some example lua bits:\n".. - "\tif LocalPlayer():Health() > 0 then print(\"I'm alive\") RunConsoleCommand(\"say\", \"I\'m alive\") end\n".. - "\tfor i=0,100,1 do print(\"number\" .. i) end\n".. - "\tfor _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end\n".. - "\tlocal random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)" - - }, - - ["weapon"] = { - tooltip = "configures your weapon entity", - popup_tutorial = "This part is like an entity part, but for weapons. It can change your weapon's position and appearance, for all or one weapon class." - }, - - ["woohoo"] = { - tooltip = "applies a censor square", - popup_tutorial = - "This part draws a pixelated square with what's behind it, with a possible blur filter and adjustable resolution.\n".. - "It requires a lot of resources to set up and needs to refresh in specific circumstances, which is why you can't change its resolution or blur filtering state with proxies." - }, - - ["flex"] = { - tooltip = "Adjusts one facial expression slot", - popup_tutorial = - "This part gives access to one facial expression defined by your model's shape keys." - }, - - ["particles"] = { - tooltip = "Emits particles", - popup_tutorial = - "Throws particles into the world. They are quite configurable, can be flat 3D or 2D sprites, can be stretched with start/end length.\n".. - "To start with, you may want to set zero angle to false and particle angle velocity to (0, 0, 0)\n".. - "You can use a web texture but you might still need to work around material limitations for transparent images\n".. - "They are not PCF effects though. But I think that with a wise choice and layered particles, you can recreate something that looks like an effect." - }, - - ["custom_animation"] = { - tooltip = "sets up an editable bone animation", - popup_tutorial = - "This part creates a custom animation with a separate editor menu. It is not a sequence, but it moves bones on top of your base animations. It morphs between keyframes which correspond to bones' positions and angles. This is what creates movement.\n\n".. - "Custom animation types:\n".. - "\tsequence: loopable. plays the A-pose animation as a base, layers bone movements on top.".. - "\tstance: loopable. layers bone movements on top.".. - "\tgesture: not loopable. layers bone movements on top. ideally you should start with duplicating your initial frame once for smoothly going back to 0.".. - "\tposture: only applies one non-moving frame. this is like a set of bones.\n\n".. - "There are interesting Easing styles available when you select the linear interpolation mode. They're useful in many ways, if you want to have more control over the dynamics and ultimately give character to your animation.\n".. - "While this is not the place to write a full tutorial for how to animate, or explaining animation principles in depth, I editorialize a bit and say those are two I try to aim for:\n".. - "\tinertia: trying to carry some movement over from a previous frame, because real physics take time to decelerate and accelerate between positions.\n".. - "\texaggeration: animations often use unnatural movement dynamics (e.g. different speeds at different times) to make movements look more pleasing by giving it more character. This goes in hand with anticipation." - }, - - ["beam"] = { - tooltip = "draws a rope or beam", - popup_tutorial = - "This part renders a rope or beam between itself and the end point. It can bend relative to the two endpoints' angles.\n".. - "frequency determines how many half-cycles it goes through. 1 is half a cycle (1 bump), 2 is one cycle(2 bumps)\n".. - "resolution is how many segments it tries to draw for that.\n\n".. - "And here's another reminder that while it can load url images, there are limitations so you may have to do something with a material part or blend mode if you want a custom transparent texture." - }, - - ["animation"] = { - tooltip = "plays a sequence animation", - popup_tutorial = - "This part plays a sequence animation defined in your model via the model's inherent animation definitions, included animations and active addons. Cannot load custom animations, not even .ani, .mdl or .smd\n".. - "If you want to import an animation from somewhere else, you need to know some decompiling/recompiling QC knowledge" - }, - - ["player_movement"] = { - tooltip = "edits your player movement", - popup_tutorial = "This part tells the server to handle your movement manually with a Move hook.\n".. - "Z-Velocity means you can move in the air relative to your eye angles, with WASD and jump almost like noclip. It is however still subject to air friction (needs friction to move, but friction also decelerates you) and uses ground friction as a driver.\n".. - "Friction generally cuts your movement as a percentage every tick. This is why it's very sensitive because its effect is exponential. Horizontal air friction tries to mitigate that a bit\n".. - "Reverse pitch is probably buggy. " - }, - - ["group"] = { - tooltip = "organizes parts", - popup_tutorial = - "This part groups parts. That's all it does. It bypasses parenting, which means it has no side effect, aside from when modifiers act on their direct parent, in which case the group can get in the way.\n".. - "But with a root group, (a group at the root/top level, \"my outfit\"), you can choose an owner name to select another entity to bear the pac outfit." - }, - - ["lock"] = { - tooltip = "grabs or teleports", - popup_tutorial = - "This part allows you to grab things or teleport yourself.\n\n".. - "Warning in advance: It has the most barriers because it probably has the most potential for abuse out of all parts.\n".. - "\tClients need to give consent explicitly (pac_client_grab_consent 1), otherwise you can't grab them.\n".. - "\tThis is doubly true for players' view position. That's another consent (pac_client_lock_camera_consent 1) layered on top of the existing grab consent.\n".. - "\tOn top of that, grabbed players will get a notification if you grab them, and they will know how to break the lock. Clients have multiple commands (pac_break_lock, pac_stop_lock) to request the server to force you to release them. It is mildly punitive.\n".. - "\tThere are multiple server-level settings to limit it. Some servers may even wholesale disable the new combat parts for all players by default until they're trusted/whitelisted.\n\n".. - "Now, here's business. How it works, and how to use this part:\n".. - "\tThe part searches for entities around a sphere, selects the closest one and locks onto it. You should plan ahead for the fact that it only picks up entities by their origin position, which for NPCs and players is between their feet. offset down amount compensates for this, but only for where the detection radius begins.\n".. - "\tIt will then start communicating with the server and the server may reposition the entity if it's allowed. If rejected, you may get a warning in the console, and the part will be stopped for a while.".. - "\tOverrideEyeAngles works for players only, and as stated previously, is subject to consent restrictions.\n" - }, - - ["physics"] = { - tooltip = "creates a clientside physics object", - popup_tutorial = - "This part creates a physics object clientside which can be a box or a sphere. It will relocate the model and pac parts contained and put them in the object.\n".. - "It's not compatible with the force part, unfortunately, because it's clientside. There are other reasonably fun things it can do though.\n".. - "It only works as a direct modifier on a model." - }, - - ["jiggle"] = { - tooltip = "wobbles around", - popup_tutorial = - "This part creates a subpoint that carries base_movables, and moves around with a certain type of dynamics that can lag behind and then catch up, or wiggle back and forth for a while. Strain is how much it will wobble. The children parts will be contained within that subpoint.\n".. - "There is immense utility to control movement and have some physicality to your parts' movement. To name a few examples:\n".. - "\tThe jiggle 0 speed trick: Having your jiggle set at 0 speed will freeze what's inside. You can easily control that with two proxies: one for moving (not 0), one for stopping (0)\n".. - "\tPets and drones: Fun things that are semi-independent. Easy to do with jiggle.\n".. - "\tSmoother transitions with multiple static proxies: If you have position proxies that snap to different positions, making a model teleport too fast, using these proxies on a jiggle instead will let the jiggle do the work of smoothing things out with the movement.\n".. - "\tForward velocity indicator via a counter-lagger: jiggle lags behind an origin, model points to origin with aim part, other model is forward relative to the pointer. Result: a model that goes in the direction of your movement.\n\n".. - "The part, however, has issues when crossing certain angles (up and down)." - }, - - ["projected_texture"] = { - tooltip = "creates a lamp", - popup_tutorial = - "This part creates a dynamic light / projected texture that can project onto models or the world. That's pretty much it. It's useful for lamps, flashlights and the like.\n".. - "But if you're expecting a proper light, its directed lighting method gives mediocre results alone. With another light, and a sprite maybe, it'll look nicer. We won't have point_spotlights though.\n".. - "Its brightness works by multiplication, not magnitude. 0 is a proper 0 amount.\n\n".. - "Because it uses ITexture / VTF, it doesn't link up with pac materials. Animated textures must be done by frames instead of proxies. Although you can still set a custom image. But it's additive so the transparency can't be done with alpha, but on a black background\t".. - "fov on one hand, and horizontal/vertical fovs on the other hand, compete; so you should touch only one and leave the other." - }, - - ["hitscan"] = { - tooltip = "fires bullets", - popup_tutorial = - "This part tries to fire bullets. There are damaging serverside bullets and non-damaging clientside bullets. Both could be useful in their own scenarios.\n".. - "For serverside bullets, the server might restrict that. For example, it can force you to spread your damage among all your bullets, to notably prevent you from stacking tons of bullets to multiply your damage beyond the limit.\n".. - "Damage falloff works with a fractional floor on individual bullets, which means each bullet is lowered to a percentage of its max damage." - }, - - ["motion_blur"] = { - tooltip = "makes a trail of after-images", - popup_tutorial = - "This part continually renders a series of ghost copies of your model along its path to simulate a motion blur-like effect.\n".. - "It has limited options because of how models' clientside entity is set up, allegedly." - }, - - ["link"] = { - tooltip = "transfers variables between parts", - popup_tutorial = - "This part tries to copy variables between two parts and update them when their values change.\n".. - "It doesn't work for all variables especially booleans! Also, \"link\" is a strong word. Whatever you think it means, it's not doing that.".. - "Might require a rewear to work properly." - }, - - ["effect"] = { - tooltip = "runs a PCF effect", - popup_tutorial = - "This part uses an existing PCF effect on your game installation, from your mounted games or addons. No importable PCFs from the web.\n".. - "It apparently can use control points and make tracers work. It may or may not be supported by different effects; start by putting the effect in a basic model to position the effect.\n".. - "And PCF effects can be a gigantic pain, with for example looping issues, permanence issues (BEWARE OF TF2 UNUSUAL EFFECTS!), wrong positions etc.".. - "You should probably look into particles and think about how to layer them if you're looking for something more configurable." - }, - - ["text"] = { - tooltip = "draws 3D2D or 2D text", - popup_tutorial = - "This part renders text on a flat surface (3D2D with the DrawTextOutlined mode) or on the screen (2D with the SurfaceText mode). Due to technical limitations, some features in one may not be present in the other, such as the outline and the size scaling\n\n".. - "You can use a combination of data and text to build up your text. Combined text tells the part to use both, and text position tells you whether the text is before or after the data.\n".. - "What's this data? text override. There are a handful of presets, like your health, name, position. If you want more control, you can use Proxy, and it will use the dynamic text value (a simple number variable) which you can control with proxies.\n\n".. - "If you want to raise the resolution of the text, you should try making a bigger font. But creating fonts is expensive so it's throttled. You can only make one every 3 seconds.\n".. - "Although you can use any of gmod's or try to use your operating system's font names, there are still limits to fonts' features, both in their definitions and in the lua code. Not everything will work. But it will create a unique ID for the font it creates, and you can reuse that font in other text parts." - }, - - ["camera"] = { - tooltip = "changes your view", - popup_tutorial = - "This part runs a CalcView hook to allow you to go into a third person mode and change your view accordingly. Some parts on your player may get in the way.\n".. - "eye angle lerp determines how much you mix the original eye angles into the view. Otherwise at 0 it will fully use the part's local angles.\n".. - "Remember a right hand rule! Point forward, thumb up, middle finger perpendicular to the palm. This is how the camera will look with 0 lerp.\n".. - "\tX = Red = index finger = forward\n".. - "\tY = Green= middle finger = left\n".. - "\tZ = Blue = thumb finger = up\n".. - "As an example, if you apply this, you will learn that, on the head, you can simply take a 0,-90,-90 angle value and be done with it.\n\n".. - "Because of how pac3 works, you should be careful when toggling between cameras. I've made some fixes to prevent part of that, but if you hide with events, and lose your final camera, you can't come back unless you go back to third person (to restart the cameras) and then back into first person (to put priority back on an active camera)." - }, - - ["decal"] = { - tooltip = "applies decals", - popup_tutorial = - "Decals are a Source engine thing for bullet holes, sprays and other such flat details as manholes and posters. This part when shown emits one by tracing a line forward and applying it at the hit surface.\n".. - "It can use web images and pac materials, but still subject to rendering quirks depending on transparency and others.\n".. - "Decals are semi-permanent, you can only remove them with r_cleardecals" - }, - - ["projectile"] = { - tooltip = "throws missiles into the world", - popup_tutorial = - "the projectile part creates physical entities and launches them forward.\n".. - "the entity has physics but it can be clientside (visual) or serverside (physical)\n".. - "by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n".. - "the entity can do damage but servers can restrict that.\n".. - "For visual reference, a 1x1x1 cubeex is around radius 24, and a default pac sphere is around 8." - }, - - ["poseparameter"] = { - tooltip = "sets a pose parameter", - popup_tutorial = - "pose parameters are a Source engine thing that helps models animate using a \"blend sequence\". For instance, this is how the body and head are rotated, and how 8-way walks are blended.\n".. - "It goes without saying that not all models have those, and some have fewer supported pose parameters because of how they were made." - }, - - ["entity"] = { - tooltip = "edits an entity (legacy)", - popup_tutorial = "The legacy entity part still does the usual things you need to edit your entity. Color, model, size, no draw etc. But better use the new entity part." - }, - - ["entity2"] = { - tooltip = "edits an entity", - popup_tutorial = - "This part can edit some properties of an entity. This can be your playermodel, a pac3 model (it's a clientside entity) or a prop (you give a prop a pac outfit by selecting it with the owner name on a root group).\n".. - "It supports web models. See the model part's tutorial or read the MDL zips page on our wiki for further info.\n\n".. - "As with other bone-related things, it might not work properly if you use it on a ragdoll or some similar entities.\n\n".. - "Another warning in advance, if you wonder why your playermodel won't change, there are some addons, such as Enhanced Playermodel Selector, known to cause issues because they override your entity, thus conflicting with pac3. This one can be fixed if you disable \"enforce playermodel\"\n".. - "Other than that, the server setting pac_modifier_model and pac_modifier_size can forbid you from changing your playermodel and size respectively." - }, - - ["interpolated_multibone"] = { - tooltip = "morphs position between nodes", - popup_tutorial = - "A node-based path/morpher. This part allows you to move its contents by blending positions and angles between different points. Obviously enough, the nodes you select need to be base_movable parts.\n".. - "The first (Zeroth) node is the interpolated_multibone itself. From then on, the next node is reached when lerp reaches the corresponding number, and when you're at the end, i.e. an invalid or missing node, it morphs back to the origin.\n".. - "For example, 0.5 lerp will be halfway between the first node and the origin.\n".. - "While this part finally breaks through one of pac3's fundamental limitations (that of base_movables being limited to specific bones as anchoring points), there are still known issues, namely because of how angles are morphed. Roll angles might break.\n\n".. - "Suggested use cases: multi-position cutscene camera, returning hitpos pseudo-projectile, joints." - }, - - ["proxy"] = { - tooltip = "applies math to parts", - popup_tutorial = - "This part computes math and applies the numbers it gives to a parameter on a part, for number (x), vector (x,y,z) or boolean(true (1) or false (0)) types. It can send to the parent, to all its children, or to an external target part.\n".. - "Easy setup can help you make a rough idea quickly, but writing math yourself in the expression gives supremely superior control over what the math does.\n\n".. - "Here's a quick crash course in the syntax with basic examples showing the rules to observe:\n\n".. - "Basic numbers /math operators : 4^0.5 - 2*(0.2 / 5) + timeex()%4\n".. - "The only basic operators are: + - * / % ^\n".. - "Functions:\n".. - "\tFunctions are like variables that gather data from the world or that process math.\n".. - "\tMost functions are nullary, which means they have no argument: timeex(), time(), owner_health(), owner_armor_fraction()\n".. - "\tOthers have arguments, which can be required or optional: clamp(x,min,max), random(), random(min,max), random_once(seed,min,max), etc.\n".. - "\tAll Lua functions are declared by a set of parentheses containing arguments, possibly separated by commas.\n".. - "Arguments and tokens:\n".. - "\tMost arguments\' type is numbers, but some might be strings with some requirements; Most of the time it\'s a name or a part UID, for example:\n".. - "\tValid number arguments are numbers, functions or well-formed expressions. It\'s the same type because at the end of the day it gives you a number.\n".. - "\t\tNeedless to say, if you compose an expression, you need a coherent link between the tokens (i.e. math operators or functions). 2 + 2 is valid, 2 2 is not.\n".. - "\tValid string arguments are text declared by double quotes. Lua\'s string concatenation operator works. command(\"name\"..2) is the same as command(\"name2\")\n".. - "\t\tWithout the string declaration, Lua tries to look for a global variable. command(\"name\") is valid, command(name) is not.\n\n".. - "Nested functions (composition) : clamp(1 - timeex()^0.5,0,1)\n".. - "XYZ / Vectors (comma notation) : 100,0,50\n".. - "nil (skipping an axis) : 100,nil,0\n\n".. - "You can write pretty much any math using the existing functions as long as you observe the syntax\'s rules: the most common ones being to close your brackets properly, don't misspell your functions\' names and give them all their necessary arguments.\n\n".. - "There are lots of technical things to learn, but you can consult my example proxy bank by right clicking the expression field, and go consult our wiki for reference. https://wiki.pac3.info/part/proxy\n\n".. - "As a conclusion, I\'m gonna editorialize and give my recommendations:\n".. - "\t-Write with purpose. Avoid unnecessary math.\n".. - "\t\t->But still, write in a way that lets you understand the concept better.\n".. - "\t-More to the point, please have patience and deliberation. Make sure every piece works BEFORE moving on and making it more complex.\n".. - "\t-The fundamental mechanism of developing and applying new ideas is composition / compounding.\n".. - "\t\t->Multiplying different expression bits together tends to combine the concepts\n".. - "\t\t->e.g. clamp(0.2*timeex(),0,1)*sin(10*time()) is a fadein and a sine wave. What do you get? sine wave fading to full power.\n".. - "\t-Please read the debug messages in the console or in chat, they help the correction process if we make mistakes." - - }, - - ["sunbeams"] = { - tooltip = "shines like rays of light", - popup_tutorial = - "This part applies a sunbeam effect centered around the part.\n".. - "Multiplier is the strength of the effect. It can also be negative for engulfing darkness.\n".. - "Darken is how much to darken or brighten the whole effect. It helps control the contrast in conjunction with multiplier. With enough darken, only the brightest rays will go through, otherwise with unchecked multipliers or negative darken there's a whole blob of white that just overpowers everything\n".. - "Size affects the after-images projected around the center, which serve as a base for the effect." - }, - - ["shake"] = { - tooltip = "shakes nearby viewers\' camera", - popup_tutorial = - "This part applies a shake that uses the camera\'s position to take effect. For that reason, it may be nullified by certain third person addons, as well as the pac3 editor camera. You can still temporarily disable it to preview your shakes." - }, - - ["gesture"] = { - tooltip = "plays a gesture", - popup_tutorial = - "Gestures are a type of animation usually added as a layer on top of other animations, this part tries to play one but not all animations listed are gestures, so it might not work for most." - }, - - ["damage_zone"] = { - tooltip = "deals damage in a zone", - popup_tutorial = - "This part tries to deal hitbox damage via the server. It may or may not be allowed because of server settings (pac_sv_damage_zone) and client consents (pac_client_damage_zone_consent), etc. Server owners can add or remove entity classes that can be damaged with pac_damage_zone_blacklist_entity_class, pac_damage_zone_whitelist_entity_class commands.\n".. - "Among NPCs it should include VJ and DRG base NPCs, but only if they have npc_ or drg_ in their name\n".. - "Most shapes should be self-explanatory but you can use the preview function to see what it should cover. There are some settings for raycasts which could come in handy for some niche use cases even if the basic ones you'll use most of the time (box, sphere, cone from spheres, ray) will not really use these.".. - "There are certain special damage types. the dissolves can disintegrate entities but can be restricted in the server, prevent_physics_force suppresses the corpse force, removenoragdoll removes the corpse.\n".. - "" - }, - - ["health_modifier"] = { - tooltip = "modifies your health, armor", - popup_tutorial = - "This part allows you to quickly change your max health, max armor, damage multiplier taken, and has the possibility to give you extra health bars that absorb damage before the main health gets damaged.\n".. - "For the extra bars, you need to set a layer priority to pick which ones get damaged first. Outer ones are higher layer values. But they're still invisible for now... events and proxies will come later...\n".. - "The part's usage may or may not be allowed by the server." - } - - } - --print("we have defined the pace.TUTORIALS.PartInfos", pace.TUTORIALS.PartInfos) - - for i,v in pairs(pace.TUTORIALS.PartInfos) do - --print(i,v) - if pace.PartTemplates then - if pace.PartTemplates[i] then - pace.PartTemplates[i].TutorialInfo = v - end - end - end + + pace.TUTORIALS = pace.TUTORIALS or {} + pace.TUTORIALS.PartInfos = { + + ["trail"] = { + tooltip = "leaves a trail behind", + popup_tutorial = + "the trail part creates beams along its path to make a trail\n".. + "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. + "you can set how it looks, how big it becomes etc." + }, + + ["trail2"] = { + tooltip = "leaves a trail behind", + popup_tutorial = + "the trail part creates beams along its path to make a trail\n".. + "nothing unique that I need to tell you, this part is mostly self-explanatory.\n".. + "you can set how it looks, how big it becomes etc." + }, + + ["sound"] = { + tooltip = "plays sounds", + popup_tutorial = "plays sounds in wav, mp3, ogg formats.\n".. + "for random sounds, paste each path separated by semicolons e.g. sound1.wav;sound3.wav;sound8.wav\n".. + "we have a special bracket notation for sound lists: sound[1,50].wav\n\n".. + "some of the parameters to know:\n".. + "sound level affects the falloff along with volume; a good starting point is 70 level, 0.6 volume\n".. + "overlapping means it doesn't get cut off if hidden\n".. + "sequential plays sounds in a list in order once you have the semicolon or bracket notation;\n".. + "\tthe steps is how much you progress by each activation. it can go one by one (1), every other sound (2+), stay there (0) or go back (negative values)" + }, + + ["sound2"] = { + tooltip = "plays web sounds", + popup_tutorial = "plays sounds in wav, mp3, ogg formats, with the option to download sound files from the internet\n".. + "people usually use dropbox, google drive, other cloud hosts or their own server host to store and distribute their files. each has its limitations.\n\n".. + "WARNING! Downloading and using these sounds is only possible in the chromium branch of garry's mod!\n\n".. + "to randomize sounds, we still have the same notations as legacy sound:\n".. + "\tsemicolon notation e.g. path1.wav;https://url1.wav;https://url2.wav\n".. + "\tbracket notation e.g. sound[1,50].wav\n\n".. + "some of the parameters to know, you'll already know some of them from legacy sound:\n".. + "-radius affects the falloff distance\n".. + "-overlapping means it doesn't get cut off if hidden\n".. + "-sequential plays sounds in a list in order" + }, + + ["ogg"] = { + tooltip = "plays ogg sounds (broken)", + popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." + }, + + ["webaudio"] = { + tooltip = "plays web sounds (legacy)", + popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound." + }, + + + ["halo"] = { + tooltip = "makes models glow", + popup_tutorial = + "This part creates a halo around a model entity.\n".. + "That could be your playermodel or a pac3 model, but for some reason it doesn't work on your player if you have an entity part.\n".. + "passes is the thickness of the halo, amount is the brightness, blur x and y spread out the shape" + }, + + ["bodygroup"] = { + tooltip = "changes body parts on supported models", + popup_tutorial = + "Bodygroups are a Source engine model feature which allows to easily show or hide different pieces of a model\n".. + "those are often used for accessories and styles. But it won't work unless your model has bodygroups.\n".. + "this part does exactly that. but you might do that directly with the model part or entity part" + }, + + ["holdtype"] = { + tooltip = "changes your animation set", + popup_tutorial = + "this part allows you to change animations played in individual movement slots, so you can mix and match from the available animations in your playermodel\n".. + "a holdtype is a set of animations for holding one kind of weapon, such as one-handed pistols vs two-handed revolvers, rifles, melee weapons etc.\n".. + "The option is also in the normal animation part, but this part goes in more detail in choosing different animations" + }, + + ["clip"] = { + tooltip = "cuts a model in a plane (legacy)", + popup_tutorial = + "This part cuts off one side of the model in rendering.\n".. + "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." + }, + + ["clip2"] = { + tooltip = "cuts a model in a plane", + popup_tutorial = + "This part cuts off one side of the model in rendering.\n".. + "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes." + }, + + ["model"] = { + tooltip = "places a model (legacy)", + popup_tutorial = "The old model part still does the basic things you need a model to do" + }, + + ["model2"] = { + tooltip = "places a model", + popup_tutorial = + "The model part creates a clientside entity to draw a model locally.\n".. + "Being a base_movable, parts inside it will be physically arented to it.\n".. + "therefore, it can act as a regrouper, rail or anchoring point for your pac structuring needs, although you probably shouldn't abuse it.\n".. + "It can accept most modifiers and play animations, if present or referenced in the model.\n\n".. + "It can load MDL zips or OBJ files from a direct link to a server host or cloud provider, allowing you to use pretty much any model as long as it's the right format for Source. And on that subject, you would do well to install Crowbar, as well as Blender with Blender Source Tools, if you want to extract and edit models. Consult the valve developer community wiki for more information about QC; I view this as common knowledge rather than the purview of pac3 so you have to do some research." + }, + + ["material"] = { + tooltip = "defines a material (legacy)", + popup_tutorial = + "the old material still works as it says. it lets you define some VMT parameters for a material" + }, + + ["material_3d"] = { + tooltip = "defines a material for models", + popup_tutorial = + "This part creates a VMT material of the shader type VertexLitGeneric.\n".. + "If you have experience in Source engine things, you probably should know what some of these do, I won't expound fully but here's the essential summary anyway:\n\n".. + "\tbase texture is the base image. It's basically just color pixels.\n".. + "\tbump map / normal map is a relief that gives a texture on the surface. It uses a distinctly purple pixel format; it's not color but directional information\n".. + "\tdetail is a second base image added on top to modify the pixels. It's usually grayscale because we don't need to add color to give more grit to an image\n".. + "\tself illumination and emissive blend are glowing layers. emissive blend is more complex and needs three necessary components before it starts to work properly.\n".. + "\tenvironment map is a layer for pre-baked room reflection, by default env_cubemap tries to get the nearest cubemap but you can choose another texture, although cubemaps are a very specific format\n".. + "\tphong is a layer of dynamic light reflections\n\n".. + "If you want to edit a material, you can load its VMT with a right click on \"load vmt\", then select the right material override\n".. + "Reminder that transparent textures may need additive or some form of translucent setting on the model and on the material.\n\n".. + "For more information, search the Valve developer community site or elsewhere. Many material features are standard, and if you want to push this part to the limit, the extra research will be worth it." + }, + + ["material_2d"] = { + tooltip = "defines a material for sprites", + popup_tutorial = + "This part creates a VMT material of the shader type UnlitGeneric. This is used by particles and sprites.\n".. + "For transparent textures, use additive or vertex alpha/vertex color (for particles and decals). Some VTF or PNG textures have an alpha channel, but many just have a black background meant for additive rendering.\n\n".. + "For more information, search the Valve developer community site" + }, + + ["material_refract"] = { + tooltip = "defines a refracting material", + popup_tutorial = + "This part creates a VMT material of the shader type Refract. As with other material parts, you would find it useful to name the material to use that in multiple models' \"material\" fields\n".. + "In a way, it doesn't work by surface, but by silhouette. But the surface does determine how the refraction occurs. Setting a base texture creates a flat wall behind it that can distort in interesting ways but it'll replace the view behind.\n".. + "The normal section does most of the heavy lifting. This is where the image behind the material gets refracted according to the surface. You can blend between two normal maps in greater detail.\n".. + "Your model needs to be set to \"translucent\" rendering mode for this to work because the shader is in a multi-step rendering process.\n\n".. + "For more information, search the Valve developer community site" + }, + + ["material_eye refract"] = { + tooltip = "defines a refracting eye material", + popup_tutorial = + "This part creates a VMT material of the shader type EyeRefract.\n".. + "It's tricky to use because of how it involves projections and entity eye position, but you can more easily get something working on premade HL2 or other Source games' characters with QC eyes." + }, + + ["submaterial"] = { + tooltip = "applies a material on a submaterial zone", + popup_tutorial = + "Models can be comprised of multiple materials in different areas. This part can replace the material applied to one of these zones.\n".. + "Depending on how the model was made, it might correspond to what you want, or it might not.\n".. + "As usual, as with other model modifiers your expectations should always line up with the quality of the model you're using." + }, + + ["bone"] = { + tooltip = "changes a bone (legacy)", + popup_tutorial = + "The legacy bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." + }, + + ["bone2"] = { + tooltip = "changes a bone (legacy)", + popup_tutorial = + "The legacy experimental bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." + }, + + ["bone3"] = { + tooltip = "changes a bone", + popup_tutorial = + "This part modifies a model's bone. It can move relative to the parent bone, scale, and rotate.\n".. + "Follow part forces the bone to relocate to a base_movable part. Might have issues if you successively follow part multiple related bones. You could try to fix that by changing draw orders of the follow parts and bones." + }, + + ["player_config"] = { + tooltip = "sets your player entity's behaviour", + popup_tutorial = + "This part has access to some of your player's behavior, like whether you will play footsteps, the chat animation etc.\n".. + "Some of these may or may not work as intended..." + }, + + ["light"] = { + tooltip = "lights up the world (legacy)", + popup_tutorial = + "This legacy part still does the basic thing you want from a light, but the new light part is more fully-featured, for the most part.\n".. + "There is one thing it does that the new part doesn't, and that's styles." + }, + + ["light2"] = { + tooltip = "lights up models or the world", + popup_tutorial = + "This part creates a dynamic light that can illuminate models or the world independently.\n".. + "There are some options for the light's falloff shape (inner and outer angles).\n".. + "Its brightness works by magnitude and size, not multiplication. Which means you can still have light at 0 or lower brightness." + }, + + ["event"] = { + tooltip = "activates or deactivates other parts", + popup_tutorial = + "This part hides or shows connected parts when certain conditions are met. We won't describe them in this tutorial, you'll have to read them individually. The essential behaviour remains common accross events.\n\n".. + "Domain, in other words, which parts get affected:\n".. + "\t1-Default: The event will command its direct parent. Because parts can contain other parts, this includes the event itself, and parts beside the event too. While this is not usually a problem, you have to be aware of that.\n".. + "\t2-ACO: Affect Children Only. The event will command parts inside it, not beside, not above. This is the first step to isolate your setup and have clean logic in your pac.\n".. + "\t3-Targeted: The event gets wired to a part directly, including its children of course. This is accessed when you select a part in the \"targeted part\" field, which has an unfortunate name because there's still the old \"target part\" parameter\n\n".. + "Some events, like is_touching, can select an external \"target\" to use as a point to gather information.\n\n".. + "Operators:\n".. + "Operators are just how the event asks the question to determine when to activate or deactivate. Just read the event the same way as it asks the question: is my source equal to the value? can I find this text in my source?\n".. + "\tnumber-related operators: equal, above, below, equal or above, equal or below\n".. + "\tstring-related operators: equal, find, find simple\n".. + "There's still a caveat. If you use the wrong type of operator for your event, it will NOT work. Please trust the editor autopilot when it automatically changes your operator to a good one. Do not change it unless you know what you're doing." + }, + + ["sprite"] = { + tooltip = "draws a 2D texture", + popup_tutorial = + "Sprites are another Source engine thing which are useful for some point effects. Most textures being for model surfaces will look like squares if drawn flat, but sprite and particle textures are made specially for this purpose.\n".. + "They should have a transparent background or black background. The difference is because of rendering modes or blend modes.\n".. + "Additive rendering adds pixels' values. So, bright pixels will be more visible, but dark pixels end up being faded or invisible because their amounts are low." + }, + + ["fog"] = { + tooltip = "colors a model with fog", + popup_tutorial = + "This strange modifier renders a fog-like color over a model. Not in the world, not inside the model, but over its surface.\n".. + "For that reason, you might do well to change rendering-related values like blend mode on the host model's side\n".. + "It requires to be attached to a base_drawable part, keep in mind the start and end values are multiplied by 100 in post for some reason.".. + "start is the distance where the fog starts to appear outside, end is where the fog is thickest." + }, + + ["force"] = { + tooltip = "provides physical force", + popup_tutorial = + "This part tries to tell the server to do a force impulse, or continually request small impulses for a continuous force. It should work for most physics props, some item and ammo entities, players and NPCs. But it may or may not be allowed on the server due to server settings: pac_sv_force.\n\n".. + "There's a base force and an added xyz vector force. You have options to choose how they're applied. Aside from that, the part's area is mainly for detection.\n\n".. + "For the Base force, Radial is from to self to each entity, Locus is from locus to each entity, Local is forward of self\n\n".. + "For the Vector force, Global is on world coordinates, Local is on self's coordinates, Radial is relative to the line from the self or locus toward the entity (Used in orbits/vortex/circular motion with centrifugal force)\n\n".. + "NPCs might have weird movement so don't expect much from pushing them." + }, + + ["faceposer"] = { + tooltip = "Adjusts multiple facial expression slots", + popup_tutorial = + "This part gives access to multiple facial expressions defined by your model's shape keys in one part.\n".. + "The flex multiplier affects the whole model, so you should avoid stacking faceposers if they have different multipliers." + }, + + ["command"] = { + tooltip = "Runs a console command or lua code", + popup_tutorial = "This part attempts to run a command or Lua code on your client. It may or may not work depending on the command and some servers don't allow you to run clientside lua, because of sv_allowcslua 0.\n\n".. + "Some example lua bits:\n".. + "\tif LocalPlayer():Health() > 0 then print(\"I'm alive\") RunConsoleCommand(\"say\", \"I\'m alive\") end\n".. + "\tfor i=0,100,1 do print(\"number\" .. i) end\n".. + "\tfor _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end\n".. + "\tlocal random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)" + + }, + + ["weapon"] = { + tooltip = "configures your weapon entity", + popup_tutorial = "This part is like an entity part, but for weapons. It can change your weapon's position and appearance, for all or one weapon class." + }, + + ["woohoo"] = { + tooltip = "applies a censor square", + popup_tutorial = + "This part draws a pixelated square with what's behind it, with a possible blur filter and adjustable resolution.\n".. + "It requires a lot of resources to set up and needs to refresh in specific circumstances, which is why you can't change its resolution or blur filtering state with proxies." + }, + + ["flex"] = { + tooltip = "Adjusts one facial expression slot", + popup_tutorial = + "This part gives access to one facial expression defined by your model's shape keys." + }, + + ["particles"] = { + tooltip = "Emits particles", + popup_tutorial = + "Throws particles into the world. They are quite configurable, can be flat 3D or 2D sprites, can be stretched with start/end length.\n".. + "To start with, you may want to set zero angle to false and particle angle velocity to (0, 0, 0)\n".. + "You can use a web texture but you might still need to work around material limitations for transparent images\n".. + "They are not PCF effects though. But I think that with a wise choice and layered particles, you can recreate something that looks like an effect." + }, + + ["custom_animation"] = { + tooltip = "sets up an editable bone animation", + popup_tutorial = + "This part creates a custom animation with a separate editor menu. It is not a sequence, but it moves bones on top of your base animations. It morphs between keyframes which correspond to bones' positions and angles. This is what creates movement.\n\n".. + "Custom animation types:\n".. + "\tsequence: loopable. plays the A-pose animation as a base, layers bone movements on top.".. + "\tstance: loopable. layers bone movements on top.".. + "\tgesture: not loopable. layers bone movements on top. ideally you should start with duplicating your initial frame once for smoothly going back to 0.".. + "\tposture: only applies one non-moving frame. this is like a set of bones.\n\n".. + "There are interesting Easing styles available when you select the linear interpolation mode. They're useful in many ways, if you want to have more control over the dynamics and ultimately give character to your animation.\n".. + "While this is not the place to write a full tutorial for how to animate, or explaining animation principles in depth, I editorialize a bit and say those are two I try to aim for:\n".. + "\tinertia: trying to carry some movement over from a previous frame, because real physics take time to decelerate and accelerate between positions.\n".. + "\texaggeration: animations often use unnatural movement dynamics (e.g. different speeds at different times) to make movements look more pleasing by giving it more character. This goes in hand with anticipation." + }, + + ["beam"] = { + tooltip = "draws a rope or beam", + popup_tutorial = + "This part renders a rope or beam between itself and the end point. It can bend relative to the two endpoints' angles.\n".. + "frequency determines how many half-cycles it goes through. 1 is half a cycle (1 bump), 2 is one cycle(2 bumps)\n".. + "resolution is how many segments it tries to draw for that.\n\n".. + "And here's another reminder that while it can load url images, there are limitations so you may have to do something with a material part or blend mode if you want a custom transparent texture." + }, + + ["animation"] = { + tooltip = "plays a sequence animation", + popup_tutorial = + "This part plays a sequence animation defined in your model via the model's inherent animation definitions, included animations and active addons. Cannot load custom animations, not even .ani, .mdl or .smd\n".. + "If you want to import an animation from somewhere else, you need to know some decompiling/recompiling QC knowledge" + }, + + ["player_movement"] = { + tooltip = "edits your player movement", + popup_tutorial = "This part tells the server to handle your movement manually with a Move hook.\n".. + "Z-Velocity means you can move in the air relative to your eye angles, with WASD and jump almost like noclip. It is however still subject to air friction (needs friction to move, but friction also decelerates you) and uses ground friction as a driver.\n".. + "Friction generally cuts your movement as a percentage every tick. This is why it's very sensitive because its effect is exponential. Horizontal air friction tries to mitigate that a bit\n".. + "Reverse pitch is probably buggy. " + }, + + ["group"] = { + tooltip = "organizes parts", + popup_tutorial = + "This part groups parts. That's all it does. It bypasses parenting, which means it has no side effect, aside from when modifiers act on their direct parent, in which case the group can get in the way.\n".. + "But with a root group, (a group at the root/top level, \"my outfit\"), you can choose an owner name to select another entity to bear the pac outfit." + }, + + ["lock"] = { + tooltip = "grabs or teleports", + popup_tutorial = + "This part allows you to grab things or teleport yourself.\n\n".. + "Warning in advance: It has the most barriers because it probably has the most potential for abuse out of all parts.\n".. + "\tClients need to give consent explicitly (pac_client_grab_consent 1), otherwise you can't grab them.\n".. + "\tThis is doubly true for players' view position. That's another consent (pac_client_lock_camera_consent 1) layered on top of the existing grab consent.\n".. + "\tOn top of that, grabbed players will get a notification if you grab them, and they will know how to break the lock. Clients have multiple commands (pac_break_lock, pac_stop_lock) to request the server to force you to release them. It is mildly punitive.\n".. + "\tThere are multiple server-level settings to limit it. Some servers may even wholesale disable the new combat parts for all players by default until they're trusted/whitelisted.\n\n".. + "Now, here's business. How it works, and how to use this part:\n".. + "\tThe part searches for entities around a sphere, selects the closest one and locks onto it. You should plan ahead for the fact that it only picks up entities by their origin position, which for NPCs and players is between their feet. offset down amount compensates for this, but only for where the detection radius begins.\n".. + "\tIt will then start communicating with the server and the server may reposition the entity if it's allowed. If rejected, you may get a warning in the console, and the part will be stopped for a while.".. + "\tOverrideEyeAngles works for players only, and as stated previously, is subject to consent restrictions.\n" + }, + + ["physics"] = { + tooltip = "creates a clientside physics object", + popup_tutorial = + "This part creates a physics object clientside which can be a box or a sphere. It will relocate the model and pac parts contained and put them in the object.\n".. + "It's not compatible with the force part, unfortunately, because it's clientside. There are other reasonably fun things it can do though.\n".. + "It only works as a direct modifier on a model." + }, + + ["jiggle"] = { + tooltip = "wobbles around", + popup_tutorial = + "This part creates a subpoint that carries base_movables, and moves around with a certain type of dynamics that can lag behind and then catch up, or wiggle back and forth for a while. Strain is how much it will wobble. The children parts will be contained within that subpoint.\n".. + "There is immense utility to control movement and have some physicality to your parts' movement. To name a few examples:\n".. + "\tThe jiggle 0 speed trick: Having your jiggle set at 0 speed will freeze what's inside. You can easily control that with two proxies: one for moving (not 0), one for stopping (0)\n".. + "\tPets and drones: Fun things that are semi-independent. Easy to do with jiggle.\n".. + "\tSmoother transitions with multiple static proxies: If you have position proxies that snap to different positions, making a model teleport too fast, using these proxies on a jiggle instead will let the jiggle do the work of smoothing things out with the movement.\n".. + "\tForward velocity indicator via a counter-lagger: jiggle lags behind an origin, model points to origin with aim part, other model is forward relative to the pointer. Result: a model that goes in the direction of your movement.\n\n".. + "The part, however, has issues when crossing certain angles (up and down)." + }, + + ["projected_texture"] = { + tooltip = "creates a lamp", + popup_tutorial = + "This part creates a dynamic light / projected texture that can project onto models or the world. That's pretty much it. It's useful for lamps, flashlights and the like.\n".. + "But if you're expecting a proper light, its directed lighting method gives mediocre results alone. With another light, and a sprite maybe, it'll look nicer. We won't have point_spotlights though.\n".. + "Its brightness works by multiplication, not magnitude. 0 is a proper 0 amount.\n\n".. + "Because it uses ITexture / VTF, it doesn't link up with pac materials. Animated textures must be done by frames instead of proxies. Although you can still set a custom image. But it's additive so the transparency can't be done with alpha, but on a black background\t".. + "fov on one hand, and horizontal/vertical fovs on the other hand, compete; so you should touch only one and leave the other." + }, + + ["hitscan"] = { + tooltip = "fires bullets", + popup_tutorial = + "This part tries to fire bullets. There are damaging serverside bullets and non-damaging clientside bullets. Both could be useful in their own scenarios.\n".. + "For serverside bullets, the server might restrict that. For example, it can force you to spread your damage among all your bullets, to notably prevent you from stacking tons of bullets to multiply your damage beyond the limit.\n".. + "Damage falloff works with a fractional floor on individual bullets, which means each bullet is lowered to a percentage of its max damage." + }, + + ["motion_blur"] = { + tooltip = "makes a trail of after-images", + popup_tutorial = + "This part continually renders a series of ghost copies of your model along its path to simulate a motion blur-like effect.\n".. + "It has limited options because of how models' clientside entity is set up, allegedly." + }, + + ["link"] = { + tooltip = "transfers variables between parts", + popup_tutorial = + "This part tries to copy variables between two parts and update them when their values change.\n".. + "It doesn't work for all variables especially booleans! Also, \"link\" is a strong word. Whatever you think it means, it's not doing that.".. + "Might require a rewear to work properly." + }, + + ["effect"] = { + tooltip = "runs a PCF effect", + popup_tutorial = + "This part uses an existing PCF effect on your game installation, from your mounted games or addons. No importable PCFs from the web.\n".. + "It apparently can use control points and make tracers work. It may or may not be supported by different effects; start by putting the effect in a basic model to position the effect.\n".. + "And PCF effects can be a gigantic pain, with for example looping issues, permanence issues (BEWARE OF TF2 UNUSUAL EFFECTS!), wrong positions etc.".. + "You should probably look into particles and think about how to layer them if you're looking for something more configurable." + }, + + ["text"] = { + tooltip = "draws 3D2D or 2D text", + popup_tutorial = + "This part renders text on a flat surface (3D2D with the DrawTextOutlined mode) or on the screen (2D with the SurfaceText mode). Due to technical limitations, some features in one may not be present in the other, such as the outline and the size scaling\n\n".. + "You can use a combination of data and text to build up your text. Combined text tells the part to use both, and text position tells you whether the text is before or after the data.\n".. + "What's this data? text override. There are a handful of presets, like your health, name, position. If you want more control, you can use Proxy, and it will use the dynamic text value (a simple number variable) which you can control with proxies.\n\n".. + "If you want to raise the resolution of the text, you should try making a bigger font. But creating fonts is expensive so it's throttled. You can only make one every 3 seconds.\n".. + "Although you can use any of gmod's or try to use your operating system's font names, there are still limits to fonts' features, both in their definitions and in the lua code. Not everything will work. But it will create a unique ID for the font it creates, and you can reuse that font in other text parts." + }, + + ["camera"] = { + tooltip = "changes your view", + popup_tutorial = + "This part runs a CalcView hook to allow you to go into a third person mode and change your view accordingly. Some parts on your player may get in the way.\n".. + "eye angle lerp determines how much you mix the original eye angles into the view. Otherwise at 0 it will fully use the part's local angles.\n".. + "Remember a right hand rule! Point forward, thumb up, middle finger perpendicular to the palm. This is how the camera will look with 0 lerp.\n".. + "\tX = Red = index finger = forward\n".. + "\tY = Green= middle finger = left\n".. + "\tZ = Blue = thumb finger = up\n".. + "As an example, if you apply this, you will learn that, on the head, you can simply take a 0,-90,-90 angle value and be done with it.\n\n".. + "Because of how pac3 works, you should be careful when toggling between cameras. I've made some fixes to prevent part of that, but if you hide with events, and lose your final camera, you can't come back unless you go back to third person (to restart the cameras) and then back into first person (to put priority back on an active camera)." + }, + + ["decal"] = { + tooltip = "applies decals", + popup_tutorial = + "Decals are a Source engine thing for bullet holes, sprays and other such flat details as manholes and posters. This part when shown emits one by tracing a line forward and applying it at the hit surface.\n".. + "It can use web images and pac materials, but still subject to rendering quirks depending on transparency and others.\n".. + "Decals are semi-permanent, you can only remove them with r_cleardecals" + }, + + ["projectile"] = { + tooltip = "throws missiles into the world", + popup_tutorial = + "the projectile part creates physical entities and launches them forward.\n".. + "the entity has physics but it can be clientside (visual) or serverside (physical)\n".. + "by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n".. + "the entity can do damage but servers can restrict that.\n".. + "For visual reference, a 1x1x1 cubeex is around radius 24, and a default pac sphere is around 8." + }, + + ["poseparameter"] = { + tooltip = "sets a pose parameter", + popup_tutorial = + "pose parameters are a Source engine thing that helps models animate using a \"blend sequence\". For instance, this is how the body and head are rotated, and how 8-way walks are blended.\n".. + "It goes without saying that not all models have those, and some have fewer supported pose parameters because of how they were made." + }, + + ["entity"] = { + tooltip = "edits an entity (legacy)", + popup_tutorial = "The legacy entity part still does the usual things you need to edit your entity. Color, model, size, no draw etc. But better use the new entity part." + }, + + ["entity2"] = { + tooltip = "edits an entity", + popup_tutorial = + "This part can edit some properties of an entity. This can be your playermodel, a pac3 model (it's a clientside entity) or a prop (you give a prop a pac outfit by selecting it with the owner name on a root group).\n".. + "It supports web models. See the model part's tutorial or read the MDL zips page on our wiki for further info.\n\n".. + "As with other bone-related things, it might not work properly if you use it on a ragdoll or some similar entities.\n\n".. + "Another warning in advance, if you wonder why your playermodel won't change, there are some addons, such as Enhanced Playermodel Selector, known to cause issues because they override your entity, thus conflicting with pac3. This one can be fixed if you disable \"enforce playermodel\"\n".. + "Other than that, the server setting pac_modifier_model and pac_modifier_size can forbid you from changing your playermodel and size respectively." + }, + + ["interpolated_multibone"] = { + tooltip = "morphs position between nodes", + popup_tutorial = + "A node-based path/morpher. This part allows you to move its contents by blending positions and angles between different points. Obviously enough, the nodes you select need to be base_movable parts.\n".. + "The first (Zeroth) node is the interpolated_multibone itself. From then on, the next node is reached when lerp reaches the corresponding number, and when you're at the end, i.e. an invalid or missing node, it morphs back to the origin.\n".. + "For example, 0.5 lerp will be halfway between the first node and the origin.\n".. + "While this part finally breaks through one of pac3's fundamental limitations (that of base_movables being limited to specific bones as anchoring points), there are still known issues, namely because of how angles are morphed. Roll angles might break.\n\n".. + "Suggested use cases: multi-position cutscene camera, returning hitpos pseudo-projectile, joints." + }, + + ["proxy"] = { + tooltip = "applies math to parts", + popup_tutorial = + "This part computes math and applies the numbers it gives to a parameter on a part, for number (x), vector (x,y,z) or boolean(true (1) or false (0)) types. It can send to the parent, to all its children, or to an external target part.\n".. + "Easy setup can help you make a rough idea quickly, but writing math yourself in the expression gives supremely superior control over what the math does.\n\n".. + "Here's a quick crash course in the syntax with basic examples showing the rules to observe:\n\n".. + "Basic numbers /math operators : 4^0.5 - 2*(0.2 / 5) + timeex()%4\n".. + "The only basic operators are: + - * / % ^\n".. + "Functions:\n".. + "\tFunctions are like variables that gather data from the world or that process math.\n".. + "\tMost functions are nullary, which means they have no argument: timeex(), time(), owner_health(), owner_armor_fraction()\n".. + "\tOthers have arguments, which can be required or optional: clamp(x,min,max), random(), random(min,max), random_once(seed,min,max), etc.\n".. + "\tAll Lua functions are declared by a set of parentheses containing arguments, possibly separated by commas.\n".. + "Arguments and tokens:\n".. + "\tMost arguments\' type is numbers, but some might be strings with some requirements; Most of the time it\'s a name or a part UID, for example:\n".. + "\tValid number arguments are numbers, functions or well-formed expressions. It\'s the same type because at the end of the day it gives you a number.\n".. + "\t\tNeedless to say, if you compose an expression, you need a coherent link between the tokens (i.e. math operators or functions). 2 + 2 is valid, 2 2 is not.\n".. + "\tValid string arguments are text declared by double quotes. Lua\'s string concatenation operator works. command(\"name\"..2) is the same as command(\"name2\")\n".. + "\t\tWithout the string declaration, Lua tries to look for a global variable. command(\"name\") is valid, command(name) is not.\n\n".. + "Nested functions (composition) : clamp(1 - timeex()^0.5,0,1)\n".. + "XYZ / Vectors (comma notation) : 100,0,50\n".. + "nil (skipping an axis) : 100,nil,0\n\n".. + "You can write pretty much any math using the existing functions as long as you observe the syntax\'s rules: the most common ones being to close your brackets properly, don't misspell your functions\' names and give them all their necessary arguments.\n\n".. + "There are lots of technical things to learn, but you can consult my example proxy bank by right clicking the expression field, and go consult our wiki for reference. https://wiki.pac3.info/part/proxy\n\n".. + "As a conclusion, I\'m gonna editorialize and give my recommendations:\n".. + "\t-Write with purpose. Avoid unnecessary math.\n".. + "\t\t->But still, write in a way that lets you understand the concept better.\n".. + "\t-More to the point, please have patience and deliberation. Make sure every piece works BEFORE moving on and making it more complex.\n".. + "\t-The fundamental mechanism of developing and applying new ideas is composition / compounding.\n".. + "\t\t->Multiplying different expression bits together tends to combine the concepts\n".. + "\t\t->e.g. clamp(0.2*timeex(),0,1)*sin(10*time()) is a fadein and a sine wave. What do you get? sine wave fading to full power.\n".. + "\t-Please read the debug messages in the console or in chat, they help the correction process if we make mistakes." + + }, + + ["sunbeams"] = { + tooltip = "shines like rays of light", + popup_tutorial = + "This part applies a sunbeam effect centered around the part.\n".. + "Multiplier is the strength of the effect. It can also be negative for engulfing darkness.\n".. + "Darken is how much to darken or brighten the whole effect. It helps control the contrast in conjunction with multiplier. With enough darken, only the brightest rays will go through, otherwise with unchecked multipliers or negative darken there's a whole blob of white that just overpowers everything\n".. + "Size affects the after-images projected around the center, which serve as a base for the effect." + }, + + ["shake"] = { + tooltip = "shakes nearby viewers\' camera", + popup_tutorial = + "This part applies a shake that uses the camera\'s position to take effect. For that reason, it may be nullified by certain third person addons, as well as the pac3 editor camera. You can still temporarily disable it to preview your shakes." + }, + + ["gesture"] = { + tooltip = "plays a gesture", + popup_tutorial = + "Gestures are a type of animation usually added as a layer on top of other animations, this part tries to play one but not all animations listed are gestures, so it might not work for most." + }, + + ["damage_zone"] = { + tooltip = "deals damage in a zone", + popup_tutorial = + "This part tries to deal hitbox damage via the server. It may or may not be allowed because of server settings (pac_sv_damage_zone) and client consents (pac_client_damage_zone_consent), etc. Server owners can add or remove entity classes that can be damaged with pac_damage_zone_blacklist_entity_class, pac_damage_zone_whitelist_entity_class commands.\n".. + "Among NPCs it should include VJ and DRG base NPCs, but only if they have npc_ or drg_ in their name\n".. + "Most shapes should be self-explanatory but you can use the preview function to see what it should cover. There are some settings for raycasts which could come in handy for some niche use cases even if the basic ones you'll use most of the time (box, sphere, cone from spheres, ray) will not really use these.".. + "There are certain special damage types. the dissolves can disintegrate entities but can be restricted in the server, prevent_physics_force suppresses the corpse force, removenoragdoll removes the corpse.\n".. + "" + }, + + ["health_modifier"] = { + tooltip = "modifies your health, armor", + popup_tutorial = + "This part allows you to quickly change your max health, max armor, damage multiplier taken, and has the possibility to give you extra health bars that absorb damage before the main health gets damaged.\n".. + "For the extra bars, you need to set a layer priority to pick which ones get damaged first. Outer ones are higher layer values. But they're still invisible for now... events and proxies will come later...\n".. + "The part's usage may or may not be allowed by the server." + } + + } + --print("we have defined the pace.TUTORIALS.PartInfos", pace.TUTORIALS.PartInfos) + + for i,v in pairs(pace.TUTORIALS.PartInfos) do + --print(i,v) + if pace.PartTemplates then + if pace.PartTemplates[i] then + pace.PartTemplates[i].TutorialInfo = v + end + end + end end local motd_cvar = CreateConVar("pac_show_message_on_startup", "2", {FCVAR_ARCHIVE}, "Whether to show the update MOTD when you load in the game") function pac.OpenMOTD(from_initial_startup) - local pnl = vgui.Create("DFrame") - pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) - - local url = "https://github.com/pingu7867/pac3#readme" - - function pnl:OnClose() - if not from_initial_startup then return end - local function exit_message() - if LocalPlayer():IsAdmin() then - notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat!", NOTIFY_GENERIC, 10) - end - notification.AddLegacy("Before you go, once you're in the PAC editor, please go to the options tab and consider choosing which parts of the combat update you want to consent to.", NOTIFY_GENERIC, 10) - end - Derma_Query("Did you read the update news?", "update news", - "Yes, go away", function() motd_cvar:SetInt(0) exit_message() end, - "Bring it up next update", function() motd_cvar:SetInt(1) exit_message() end, - "No, I'll read later", function() motd_cvar:SetInt(2) exit_message() end - ) - - end - - pnl:SetTitle("Welcome to a new update!") - local html = vgui.Create("DHTML", pnl) - html:Dock(FILL) - html:OpenURL( url ) - - - pnl:Center() - pnl:MakePopup() - pace.motd_opened = true + local pnl = vgui.Create("DFrame") + pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) + + local url = "https://github.com/pingu7867/pac3#readme" + + function pnl:OnClose() + if not from_initial_startup then return end + local function exit_message() + if LocalPlayer():IsAdmin() then + notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat!", NOTIFY_GENERIC, 10) + end + notification.AddLegacy("Before you go, once you're in the PAC editor, please go to the options tab and consider choosing which parts of the combat update you want to consent to.", NOTIFY_GENERIC, 10) + end + Derma_Query("Did you read the update news?", "update news", + "Yes, go away", function() motd_cvar:SetInt(0) exit_message() end, + "Bring it up next update", function() motd_cvar:SetInt(1) exit_message() end, + "No, I'll read later", function() motd_cvar:SetInt(2) exit_message() end + ) + + end + + pnl:SetTitle("Welcome to a new update!") + local html = vgui.Create("DHTML", pnl) + html:Dock(FILL) + html:OpenURL( url ) + + + pnl:Center() + pnl:MakePopup() + pace.motd_opened = true end From 4ed37a82ca31c3d312371c0d7faef100d08a1c40 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 13:52:26 -0400 Subject: [PATCH 077/300] declare variable on init --- lua/pac3/core/client/parts/lock.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 4f05c91b8..631ecec50 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -501,7 +501,7 @@ function PART:CalculateRelativeOffset() end function PART:Initialize() - + self.default_ang = Angle(0,0,0) if not GetConVar('pac_sv_lock_grab'):GetBool() then if not GetConVar('pac_sv_lock_teleport'):GetBool() then self:SetWarning("lock part grabs and teleports are disabled on this server!") From 58511711e04a0e78f4eb74b0d9b267f4c2ffdf33 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 14:31:05 -0400 Subject: [PATCH 078/300] Update shortcuts.lua legacy mode help popup action (F1) rework my shortcut preset localize global variables --- lua/pac3/editor/client/shortcuts.lua | 115 ++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 11 deletions(-) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 19355f393..33be51cd1 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -45,6 +45,7 @@ pace.PACActionShortcut_Dictionary = { "toolbar_options", "zoom_panel", "T_Pose", + "bulk_select", "clear_bulkselect", "copy_bulkselect", "bulk_insert", @@ -179,42 +180,68 @@ pace.PACActionShortcut_NoCTRL = { } pace.PACActionShortcut_Cedric = { + ["help_info_popup"] = { + [1] = {"F1"} + }, + ["property_search_in_tree"] = { + [1] = {"CTRL", "f"} + }, ["wear"] = { [1] = {"CTRL", "n"} }, + ["pac_restart"] = { + [1] = {"CTRL", "ALT", "SHIFT", "r"} + }, + ["pac_panic"] = { + [1] = {"CTRL", "ALT", "SHIFT", "p"} + }, + ["save"] = { [1] = {"CTRL", "m"} }, + ["load"] = { + [1] = {"SHIFT", "m"} + }, + ["hide_editor"] = { [1] = {"CTRL", "e"}, [2] = {"INS"}, - [3] = {"TAB"} + [3] = {"TAB"}, + [4] = {"4"} }, ["hide_editor_visible"] = { - [1] = {"LALT", "e"} + [1] = {"ALT", "e"}, + [2] = {"5"} }, ["copy"] = { [1] = {"CTRL", "c"} }, + ["bulk_copy"] = { + [1] = {"SHIFT", "CTRL", "c"} + }, + ["paste"] = { [1] = {"CTRL", "v"} }, ["cut"] = { [1] = {"CTRL", "x"} }, + ["bulk_insert"] = { + [1] = {"CTRL", "SHIFT", "v"} + }, ["delete"] = { [1] = {"DEL"} }, - ["expand_all"] = { - [1] = {"x"} + ["bulk_delete"] = { + [1] = {"SHIFT", "DEL"} }, - ["collapse_all"] = { - [1] = {"c"} + ["clear_bulkselect"] = { + [1] = {"CTRL", "SHIFT", "DEL"} }, ["undo"] = { [1] = {"CTRL", "z"}, @@ -227,6 +254,27 @@ pace.PACActionShortcut_Cedric = { ["tpose"] = { [1] = {"CTRL", "t"} }, + ["zoom_panel"] = { + [1] = {"v"} + }, + ["toolbar_view"] = { + [1] = {"SHIFT", "v"} + }, + ["add_part"] = { + [1] = {"1"} + }, + ["partmenu"] = { + [1] = {"2"} + }, + ["bulk_select"] = { + [1] = {"3"} + }, + ["hide"] = { + [1] = {"CTRL", "h"} + }, + ["bulk_hide"] = { + [1] = {"SHIFT", "h"} + }, ["editor_up"] = { [1] = {"UPARROW"} }, @@ -573,6 +621,9 @@ function pace.DoShortcutFunc(action) if action == "T_Pose" then pace.SetTPose(not pace.GetTPose()) end + if action == "bulk_select" then + pace.DoBulkSelect(pace.current_part) + end if action == "clear_bulkselect" then pace.ClearBulkList() end if action == "copy_bulkselect" then pace.BulkCopy(pace.current_part) end if action == "bulk_insert" then pace.BulkCutPaste(pace.current_part) end @@ -676,10 +727,10 @@ pace.delaymovement = 0 --always refresh the inputs, but check if we stay the same before checking the shortcuts! -- -skip = false -no_input_override = false -has_run_something = false -previous_inputs_str = "" +local skip = false +local no_input_override = false +local has_run_something = false +local previous_inputs_str = "" function pace.CheckShortcuts() if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then @@ -732,6 +783,48 @@ function pace.CheckShortcuts() end end + if input.IsKeyDown(KEY_F1) then + last = RealTime() + 0.5 + local new_popup = true + if IsValid(pace.legacy_floating_popup_reserved) then + new_popup = false + if pace.current_part ~= pace.legacy_floating_popup_reserved_part then + if IsValid(pace.legacy_floating_popup_reserved) then + pace.legacy_floating_popup_reserved:Remove() + pace.legacy_floating_popup_reserved = nil + pace.legacy_floating_popup_reserved_part = nil + end + new_popup = true + end + else + pace.legacy_floating_popup_reserved = nil + pace.legacy_floating_popup_reserved_part = nil + end + + local popup_setup_tbl = { + obj_type = "", + clickfunc = function() pace.OnAddPartMenu(pace.current_part) end, + hoverfunc = "open", + pac_part = pace.current_part, + panel_exp_width = 900, panel_exp_height = 400, + from_legacy = true + } + + popup_setup_tbl.obj_type = "pac tree label" + popup_setup_tbl.obj = pace.current_part.pace_tree_node + + if new_popup then + local created_panel = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) + pace.legacy_floating_popup_reserved = created_panel + pace.legacy_floating_popup_reserved_part = pace.current_part + end + pac.AddHook("Think", "killpopupwheneditorunfocused", function() + if not pace:IsFocused() then + if IsValid(pace.legacy_floating_popup_reserved) then pace.legacy_floating_popup_reserved:Remove() end + end + if not IsValid(pace.legacy_floating_popup_reserved) then pace.legacy_floating_popup_reserved = nil end + end) + end end return end @@ -776,7 +869,7 @@ function pace.CheckShortcuts() for action,list_of_lists in pairs(pace.PACActionShortcut) do if not has_run_something then - if action == "hide_editor" and pace.LookupShortcutsForAction(action, input_active, true) then --we can focus back if editor is not focused + if (action == "hide_editor" or action == "hide_editor_visible") and pace.LookupShortcutsForAction(action, input_active, true) then --we can focus back if editor is not focused --pace.DoShortcutFunc(action) last = RealTime() has_run_something = true From 85bc3d9155ebea8f3b7eb6e8c476d8369fe79d34 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 14:34:15 -0400 Subject: [PATCH 079/300] Update parts.lua safety return localize a global more info in bulk select menu (contents and part orders) --- lua/pac3/editor/client/parts.lua | 43 ++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 714c58849..2bbbe4ca7 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -264,11 +264,13 @@ function pace.OnPartSelected(part, is_selecting) local startnodenumber = table.KeyFromValue( added_nodes, pace.current_part.pace_tree_node) local endnodenumber = table.KeyFromValue( added_nodes, part.pace_tree_node) + if not startnodenumber or not endnodenumber then return end + table.sort(added_nodes, function(a, b) return select(2, a:LocalToScreen()) < select(2, b:LocalToScreen()) end) local i = startnodenumber - direction = math.Clamp(endnodenumber - startnodenumber,-1,1) + local direction = math.Clamp(endnodenumber - startnodenumber,-1,1) if direction == 0 then last_direction = direction return end last_direction = last_direction or 0 if last_span_select_part == nil then last_span_select_part = part end @@ -1734,7 +1736,7 @@ do -- menu function pace.BulkCopy(obj) if #pace.BulkSelectList == 1 then pace.Copy(obj) end --at least if there's one selected, we can take it that we want to copy that part pace.BulkSelectClipboard = table.Copy(pace.BulkSelectList) --if multiple parts are selected, copy it to a new bulk clipboard - print("copied: ") + print("[PAC3 bulk select] copied: ") TestPrintTable(pace.BulkSelectClipboard,"pace.BulkSelectClipboard") end @@ -2161,9 +2163,40 @@ function pace.addPartMenuComponent(menu, obj, option_name) elseif mode == 3 then info = "halo-highlighted on preset keypress: control" elseif mode == 4 then info = "halo-highlighted on preset keypress: shift" end - bulk_menu:AddOption(L"Bulk select info: "..info, function() end):SetImage(pace.MiscIcons.info) - bulk_menu:AddOption(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts", function() end):SetImage(pace.MiscIcons.info) - + bulk_menu:AddOption(L"Bulk select info: "..info):SetImage(pace.MiscIcons.info) + if #pace.BulkSelectList == 0 then + bulk_menu:AddOption(L"Bulk select info: nothing selected"):SetImage(pace.MiscIcons.info) + else + local copied, pnl = bulk_menu:AddSubMenu(L"Bulk select info: " .. #pace.BulkSelectList .. " copied parts") + pnl:SetImage(pace.MiscIcons.info) + for i,v in ipairs(pace.BulkSelectList) do + local name_str + if v.Name == "" then + name_str = tostring(v) + else + name_str = v.Name + end + + copied:AddOption(i .. " : " .. name_str .. " (" .. v.ClassName .. ")"):SetIcon(v.Icon) + end + end + if #pace.BulkSelectClipboard == 0 then + bulk_menu:AddOption(L"Bulk select clipboard info: nothing copied"):SetImage(pace.MiscIcons.info) + else + local copied, pnl = bulk_menu:AddSubMenu(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts") + pnl:SetImage(pace.MiscIcons.info) + for i,v in ipairs(pace.BulkSelectClipboard) do + local name_str + if v.Name == "" then + name_str = tostring(v) + else + name_str = v.Name + end + + copied:AddOption(i .. " : " .. name_str .. " (" .. v.ClassName .. ")"):SetIcon(v.Icon) + end + end + bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() pace.BulkCutPaste(obj) end):SetImage('icon16/arrow_join.png') From a4a98c4c8904c5baba9a2a2910436351331e2263 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 15:33:14 -0400 Subject: [PATCH 080/300] partal fix for submaterial dynamic properties --- lua/pac3/editor/client/parts.lua | 43 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 2bbbe4ca7..4c197c27a 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1554,7 +1554,7 @@ do -- menu ]] local function GetEventArgType(part, str) - + if not part.Events then return "string" end for argn,arg in ipairs(part.Events[part.Event].__registeredArguments) do if arg[1] == str then return arg[2] @@ -1636,28 +1636,29 @@ do -- menu for i,part in ipairs(pace.BulkSelectList) do --PrintTable(part.Events[part.Event].__registeredArguments) - if part.ClassName == "event" and part.Event == basepart.Event then - local sent_var - if var_type == "number" then - sent_var = VAR_PANEL_EDITZONE:GetValue() - if not tonumber(sent_var) then - local ok, res = pac.CompileExpression(sent_var) - if ok then - sent_var = res() or 0 - end + local sent_var + if var_type == "number" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if not tonumber(sent_var) then + local ok, res = pac.CompileExpression(sent_var) + if ok then + sent_var = res() or 0 end - elseif var_type == "boolean" then - sent_var = VAR_PANEL_EDITZONE:GetChecked() - if sent_var == true then sent_var = "1" - else sent_var = "0" end - elseif var_type == "string" then - sent_var = VAR_PANEL_EDITZONE:GetValue() - if v == "Name" and sent_var ~= "" then - sent_var = sent_var..i - end - else sent_var = VAR_PANEL_EDITZONE:GetValue() end - + end + elseif var_type == "boolean" then + sent_var = VAR_PANEL_EDITZONE:GetChecked() + if sent_var == true then sent_var = "1" + else sent_var = "0" end + elseif var_type == "string" then + sent_var = VAR_PANEL_EDITZONE:GetValue() + if v == "Name" and sent_var ~= "" then + sent_var = sent_var..i + end + else sent_var = VAR_PANEL_EDITZONE:GetValue() end + if part.ClassName == "event" and part.Event == basepart.Event then part:SetArguments(ApplyArgToIndex(part:GetArguments(), sent_var, GetEventArgIndex(part,v))) + else + part:SetProperty(udata_val_name, sent_var) end if thoroughness_tickbox:GetChecked() then From 90e6607165f0937af9191b5d227deabcc92c2608 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 15:49:15 -0400 Subject: [PATCH 081/300] Update shortcuts.lua --- lua/pac3/editor/client/shortcuts.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 33be51cd1..14c14ac35 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -189,14 +189,13 @@ pace.PACActionShortcut_Cedric = { ["wear"] = { [1] = {"CTRL", "n"} }, - ["pac_restart"] = { + ["restart"] = { [1] = {"CTRL", "ALT", "SHIFT", "r"} }, - ["pac_panic"] = { + ["panic"] = { [1] = {"CTRL", "ALT", "SHIFT", "p"} }, - ["save"] = { [1] = {"CTRL", "m"} }, @@ -255,7 +254,7 @@ pace.PACActionShortcut_Cedric = { [1] = {"CTRL", "t"} }, ["zoom_panel"] = { - [1] = {"v"} + [1] = {"ALT", "v"} }, ["toolbar_view"] = { [1] = {"SHIFT", "v"} From c2f0a9be939275f76d935f943745d3e0a2739889 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 17:39:13 -0400 Subject: [PATCH 082/300] simple validity check --- lua/pac3/core/client/base_part.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index aee1e1ef6..8341a4880 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -1213,7 +1213,9 @@ function PART:SetupEditorPopup(str, force_open, tbl) pnl = pac.InfoPopup(info_string,popup_config_table) self.pace_tree_node.popupinfopnl = pnl end - pace.legacy_floating_popup_reserved = pnl + if pace then + pace.legacy_floating_popup_reserved = pnl + end return pnl end From 7b911a04dd4bcbdca699188d1f642e7c659c20a8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 18:22:44 -0400 Subject: [PATCH 083/300] include file extension in last outfit tracking --- lua/pac3/editor/client/saved_parts.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 38589542f..391beb0d7 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -201,7 +201,11 @@ function pace.LoadParts(name, clear, override_part) end else - if name ~= "autoload.txt" and not string.find(name, "pac3/__backup") then cookie.Set( "pac_last_loaded_outfit", name ) end + if name ~= "autoload.txt" and not string.find(name, "pac3/__backup") then + if file.Exists("pac3/" .. name..".txt", "DATA") then + cookie.Set( "pac_last_loaded_outfit", name .. ".txt" ) + end + end if hook.Run("PrePACLoadOutfit", name) == false then return end From 68ba86bcc9906468fac3c04f11d9ce00a6b44f12 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 18:24:39 -0400 Subject: [PATCH 084/300] removed hanging function it's overwritten later --- lua/pac3/core/client/parts/projectile.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 49398729a..5c6da368b 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -425,10 +425,6 @@ function PART:Shoot(pos, ang, multi_projectile_count) end end -function PART:SetDamage(val) - -end - function PART:SetRadius(val) self.Radius = val local sv_dist = GetConVar("pac_sv_projectile_max_phys_radius"):GetInt() @@ -584,4 +580,4 @@ do -- physical end) end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From e86ece8184136e9e54a5494468b9073ef9ccea40 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 4 Nov 2023 18:42:33 -0400 Subject: [PATCH 085/300] use the readme from upstream before merging --- README.md | 165 +++--------------------------------------------------- 1 file changed, 9 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 34221b9be..b36d0c200 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,18 @@ -# PAC4.5 +# PAC3 --- -Welcome to my experimental combat update for PAC3. Here's the overview of the important bits to expect. +PAC3 gives you the ability to personalize your player model's look by placing objects and effects on yourself. You can go from putting just a hat on your head to creating an entire new player model. PAC works on any entity and can also be used as a way to make custom weapons and npcs for your gamemode easily. +You can wear your outfit on any server with PAC3 and everyone should be able to see it on you as long as they have the content you used. -# New combat-related parts: - - damage_zone: deals damage (a more direct and controllable alternative to projectiles) - - hitscan: shoots bullets - - lock: teleport/grab - - force: does physics forces - - health_modifier: changes your health, armor etc - - interpolated_multibone: morphs position / angles between different base_movables nodes, like a path - - -The combat features work with the principle of consent. The lock part especially is severely restricted for grabbing players, for what should be obvious reasons. You can only damage or grab players who have opted in for the corresponding consent. - - pac_client_damage_zone_consent 0 - pac_client_hitscan_consent 0 - pac_client_force_consent 0 - pac_client_grab_consent 0 - pac_client_lock_camera_consent 0 - -There are also commands for clients to free themselves if they're being grabbed. - - pac_break_lock - pac_stop_lock - -Multiple options exist for servers to prevent mass abuse. Although I might've had things to say about server owners being resistant to new disruptive features, I've come to some compromises in the form of cvars, size limits, damage limits, which combat parts are allowed, as well as several net-protecting options to ease the load on the server's processing and on the network (reliable channel)... - -In sandbox, the default for the combat features will be 1 when creating the convars the first time. In other gamemodes, it will be 0. - - pac_sv_combat_whitelisting 0 - pac_sv_damage_zone 1 - pac_sv_lock 1 - pac_sv_lock_grab 1 - pac_sv_lock_teleport 1 - pac_sv_lock_max_grab_radius 200 - pac_sv_combat_enforce_netrate 0 - pac_sv_entity_limit_per_combat_operation 500 - pac_sv_entity_limit_per_player_per_combat_operation 40 - pac_sv_player_limit_as_fraction_to_drop_damage_zone 1 - pac_sv_block_combat_features_on_next_restart 0 - ... - - -# Editor features: - -## Bulk Select - -Select multiple parts and do some basic operations repeatedly. By default it's CTRL + click to select/unselect a part. - -Along with it, bulk apply properties is a new menu to change multiple parts' properties at once. - - -## Extensive customizability (user configs will be saved in data/pac3_config) - -Customizable shortcuts for almost every action (in the pac settings menu). - -Reordering the part menu actions layout (in the pac settings menu). - -Changing your part categories, with possible custom icons. - -Colors for the event wheel (with a menu) + a new grid style for command events that doesn't move too much. - - -## Expanded settings menu - -Clients can configure their editor experience, and owners with server access can configure serverwide combat-related limits and policies. - -## Favorite assets for quick access (user configs will be saved in data/pac3_config) - -right click on assets in the pac asset browser to save it to your favorites. it can also try to do series if they end in a number, but it might fail. right clicking on the related field will bring it up in your list - -## Popup system - -select a part and press F1 to open information about it. limited support but it will be useful later on. It can be configured to be on a part in your viewport, on your cursor, next to the part's tree label ... - -## Editor copilot : Foolproofing and editor assist - -Selecting an event will pick an appropriate operator, and clicking away from a proxy without a variable name will notify you about how it won't work, telling you to go back and change it - -Writing a name into an event's type will create a command event with that name if the name isn't a recognized event type, so you can quickly setup command events. - -auto-disable editor camera to preview the camera part when creating a camera part - -auto-focus on the relevant property when creating certain parts - -# Reference and help features - -proxy bank: some presets with tooltip explanations. right click on the expression field to look at them - -command bank: presets to use the command part. again, right click on the expression field to look at them - -built-in wiki written by me, for every part and most event types: short tooltips to tell you what a part does when you hover over the label when choosing which part to create, longer tutorials opened with F1 when you select an existing part. - - -# Miscellaneous features - -## Part notes - -a text field for the base_part, so you can write notes on any part. - -## Prompt for autoload - -option to get a prompt to choose between your autoload file, your latest backup or latest loaded outfit when starting. - -## Queue prop/NPC outfits (singleplayer only) - -option so that, when loading an outfit for props/NPCs, instead of hanging in the editor and needing to reassign the owner name manually, pac will not wear yet, but wait for you to spawn an appropriate prop or entity that had the outfit. - -## pac_event_sequenced - -pac_event but with more options to control series of numbered events. - -pac will try to register the max number when you create a command event with the relevant number e.g. to reach command10 you need to have a command event with the name command10. rewear for best results. - -examples: - -this increments by 1 (and loops back if necessary) - - pac_event_sequenced hat_style + - -this sets the series to 3 - - pac_event_sequenced hat_style set 3 - -keywords for going forward: +, add, forward, advance, sequence+ - -keywords for going backward: -, sub, backward, sequence- - -keyword to set: set - - -## Improvements to physics and projectile parts - -Set the surface properties, preview the sizes and some more. - -For projectiles to change the physics mesh, it might have some issues. - -## Bigger fonts for the editor + pac_editor_scale for the tree's scale - -just a quick edit for people with higher resolution screens - -## New tools - --destroy hidden parts, proxies and events. I also call it Ultra cleanup. This is a quick but destructive optimization tool to improve framerate by only keeping visible parts and obliterating non-static elements. You can mark parts to keep by writing "important" in their notes field. +--- --Engrave targets: assign proxies and events' target part to quickly allow you to reorganize them in a separate group in the editor. +Screenshot 2023-09-02 at 06 54 28 --dump model submaterials: same as dump player submaterials (prints the submaterials in the console) but for a pac3 model you select in the tree +Some links to check out: +* [wiki](https://wiki.pac3.info/start "PAC3 Wiki") +* [steam workshop](http://steamcommunity.com/sharedfiles/filedetails/?id=104691717 "Workshop Version") +* [discord server](https://discord.gg/utpR3gJ "Join PAC3 Discord Server") --- - -### Thank you for reading. Now go make something cool! - -### Yours truly, -### Cédric. From 1c8780fd6a7461717aaa377835d520a26b9f8cf6 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 5 Nov 2023 13:02:49 -0500 Subject: [PATCH 086/300] remove unneeded include --- lua/pac3/core/client/parts/lock.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 631ecec50..ff8941ce1 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -1,7 +1,3 @@ -include("pac3/extra/shared/net_combat.lua") ---pac3/extra/shared/net_combat.lua - - local pac = pac local Vector = Vector local Angle = Angle From 533e7b0e176177ed2fcce5014c16aec9318e0e9a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 5 Nov 2023 14:11:24 -0500 Subject: [PATCH 087/300] less obtrusive update message --- .../editor/client/popups_part_tutorials.lua | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index 3b550e345..2b488ed63 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -1140,30 +1140,14 @@ do end end -local motd_cvar = CreateConVar("pac_show_message_on_startup", "2", {FCVAR_ARCHIVE}, "Whether to show the update MOTD when you load in the game") +local update_cvar = CreateConVar("pac_show_message_on_startup", "1", {FCVAR_ARCHIVE}, "Whether to show the update notification when loading in") - -function pac.OpenMOTD(from_initial_startup) +--accessed in the editor under pac-help-version +function pac.OpenMOTD() local pnl = vgui.Create("DFrame") pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) local url = "https://github.com/pingu7867/pac3#readme" - - function pnl:OnClose() - if not from_initial_startup then return end - local function exit_message() - if LocalPlayer():IsAdmin() then - notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat!", NOTIFY_GENERIC, 10) - end - notification.AddLegacy("Before you go, once you're in the PAC editor, please go to the options tab and consider choosing which parts of the combat update you want to consent to.", NOTIFY_GENERIC, 10) - end - Derma_Query("Did you read the update news?", "update news", - "Yes, go away", function() motd_cvar:SetInt(0) exit_message() end, - "Bring it up next update", function() motd_cvar:SetInt(1) exit_message() end, - "No, I'll read later", function() motd_cvar:SetInt(2) exit_message() end - ) - - end pnl:SetTitle("Welcome to a new update!") local html = vgui.Create("DHTML", pnl) @@ -1175,6 +1159,21 @@ function pac.OpenMOTD(from_initial_startup) pnl:MakePopup() pace.motd_opened = true end - -timer.Simple(10, function() if motd_cvar:GetInt() ~= 0 and not pace.motd_opened then pac.OpenMOTD(true) end end) + +hook.Add("InitPostEntity", "PAC_Update_News", function() + if update_cvar:GetBool() then + timer.Simple(5, function() + if LocalPlayer():IsAdmin() then + notification.AddLegacy("Welcome. Player Appearance Customizer (PAC3) has recently received a major update, The Combat Update, or \"PAC4.5\"\nYou can review the latest additions in the editor, in pac-help-Version-update news", NOTIFY_GENERIC, 15) + end + end) + + timer.Simple(10, function() + if LocalPlayer():IsAdmin() then + notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat! They're off by default though.", NOTIFY_GENERIC, 15) + end + end) + update_cvar:SetBool(false) + end +end) From f7bbce07a1a28fcdc83f3ada1df07a7da2bd6b6a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 6 Nov 2023 20:01:32 -0500 Subject: [PATCH 088/300] projectile setting fix adjust radius limits agreement between front end settings and back end WriteUInt limit remove unneeded include --- lua/pac3/core/client/parts/projectile.lua | 4 ++-- lua/pac3/editor/client/settings.lua | 6 +++--- lua/pac3/extra/shared/projectiles.lua | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 5c6da368b..563110191 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -276,8 +276,8 @@ function PART:Shoot(pos, ang, multi_projectile_count) net.WriteUInt(attract_ids[self.AttractMode] or 2,3) --numbers - net.WriteUInt(self.Radius,8) - net.WriteUInt(self.DamageRadius,10) + net.WriteUInt(self.Radius,12) + net.WriteUInt(self.DamageRadius,12) net.WriteUInt(self.Damage,24) net.WriteUInt(1000*self.Speed,16) net.WriteUInt(self.Maximum,7) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 2e753054f..609c4bb84 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -47,7 +47,7 @@ local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") -include("pac3/editor/server/combat_bans.lua") +--include("pac3/editor/server/combat_bans.lua") pace = pace @@ -953,14 +953,14 @@ function pace.FillCombatSettings(pnl) local projectile_max_phys_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) projectile_max_phys_radius_numbox:SetText("Max projectile physical radius") projectile_max_phys_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_phys_radius"):GetInt()) - projectile_max_phys_radius_numbox:SetMin(0) projectile_max_phys_radius_numbox:SetDecimals(0) projectile_max_phys_radius_numbox:SetMax(1000) + projectile_max_phys_radius_numbox:SetMin(0) projectile_max_phys_radius_numbox:SetDecimals(0) projectile_max_phys_radius_numbox:SetMax(4095) projectile_max_phys_radius_numbox:SetSize(400,30) projectile_max_phys_radius_numbox:SetConVar("pac_sv_projectile_max_phys_radius") local projectile_max_dmg_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) projectile_max_dmg_radius_numbox:SetText("Max projectile damage radius") projectile_max_dmg_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage_radius"):GetInt()) - projectile_max_dmg_radius_numbox:SetMin(0) projectile_max_dmg_radius_numbox:SetDecimals(0) projectile_max_dmg_radius_numbox:SetMax(5000) + projectile_max_dmg_radius_numbox:SetMin(0) projectile_max_dmg_radius_numbox:SetDecimals(0) projectile_max_dmg_radius_numbox:SetMax(4095) projectile_max_dmg_radius_numbox:SetSize(400,30) projectile_max_dmg_radius_numbox:SetConVar("pac_sv_projectile_max_damage_radius") diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index 4dac3dedf..f8ac26f06 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -65,7 +65,6 @@ do -- projectile entity self.projectile_owner = ply local radius = math.Clamp(part.Radius, 1, pac_sv_projectile_max_phys_radius:GetFloat()) - if part.Sphere then self:PhysicsInitSphere(radius, part.SurfaceProperties) else @@ -543,8 +542,8 @@ if SERVER then part.AttractMode = table.KeyFromValue(attract_ids, net.ReadUInt(3)) --numbers - part.Radius = net.ReadUInt(8) - part.DamageRadius = net.ReadUInt(10) + part.Radius = net.ReadUInt(12) + part.DamageRadius = net.ReadUInt(12) part.Damage = net.ReadUInt(24) part.Speed = net.ReadUInt(16) / 1000 part.Maximum = net.ReadUInt(7) From 2c7cc90c45ffe19f42686b7309eb0652e3ccf866 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 8 Nov 2023 18:48:09 -0500 Subject: [PATCH 089/300] Update interpolated_multibone.lua editor friendly names use gmod's interpolation functions simplified if statement render preview with drawn lines --- .../client/parts/interpolated_multibone.lua | 96 ++++++++++++------- 1 file changed, 59 insertions(+), 37 deletions(-) diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index c3da33ff3..71b848757 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -1,6 +1,7 @@ local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "interpolated_multibone" +PART.FriendlyName = "interpolator" PART.Group = 'advanced' PART.Icon = 'icon16/table_multiple.png' PART.is_model_part = false @@ -17,26 +18,26 @@ BUILDER:StartStorableVars() :GetSet("InterpolatePosition", true) :GetSet("InterpolateAngles", true) :SetPropertyGroup("Nodes") - :GetSetPart("Node1") - :GetSetPart("Node2") - :GetSetPart("Node3") - :GetSetPart("Node4") - :GetSetPart("Node5") - :GetSetPart("Node6") - :GetSetPart("Node7") - :GetSetPart("Node8") - :GetSetPart("Node9") - :GetSetPart("Node10") - :GetSetPart("Node11") - :GetSetPart("Node12") - :GetSetPart("Node13") - :GetSetPart("Node14") - :GetSetPart("Node15") - :GetSetPart("Node16") - :GetSetPart("Node17") - :GetSetPart("Node18") - :GetSetPart("Node19") - :GetSetPart("Node20") + :GetSetPart("Node1", {editor_friendly = "part 1"}) + :GetSetPart("Node2", {editor_friendly = "part 2"}) + :GetSetPart("Node3", {editor_friendly = "part 3"}) + :GetSetPart("Node4", {editor_friendly = "part 4"}) + :GetSetPart("Node5", {editor_friendly = "part 5"}) + :GetSetPart("Node6", {editor_friendly = "part 6"}) + :GetSetPart("Node7", {editor_friendly = "part 7"}) + :GetSetPart("Node8", {editor_friendly = "part 8"}) + :GetSetPart("Node9", {editor_friendly = "part 9"}) + :GetSetPart("Node10", {editor_friendly = "part 10"}) + :GetSetPart("Node11", {editor_friendly = "part 11"}) + :GetSetPart("Node12", {editor_friendly = "part 12"}) + :GetSetPart("Node13", {editor_friendly = "part 13"}) + :GetSetPart("Node14", {editor_friendly = "part 14"}) + :GetSetPart("Node15", {editor_friendly = "part 15"}) + :GetSetPart("Node16", {editor_friendly = "part 16"}) + :GetSetPart("Node17", {editor_friendly = "part 17"}) + :GetSetPart("Node18", {editor_friendly = "part 18"}) + :GetSetPart("Node19", {editor_friendly = "part 19"}) + :GetSetPart("Node20", {editor_friendly = "part 20"}) :EndStorableVars() function PART:OnRemove() @@ -45,6 +46,7 @@ end function PART:Initialize() self.nodes = {} + self.valid_nodes = {} self.Owner = pac.CreateEntity("models/pac/default.mdl") self.Owner:SetNoDraw(true) self.valid_time = CurTime() + 1 @@ -83,6 +85,32 @@ function PART:OnDraw() render.DrawLine(self.pos,self.pos - self.ang:Right()*50, Color(0,255,0)) render.DrawLine(self.pos,self.pos + self.ang:Up()*50, Color(0,0,255)) render.DrawWireframeSphere(self.pos, 8 + 2*math.sin(5*RealTime()), 15, 15, Color(255,255,255), true) + local origin_pos = self:GetWorldPosition():ToScreen() + draw.DrawText("0 origin", "DermaDefaultBold", origin_pos.x, origin_pos.y) + + for i=1,20,1 do + + if i == 1 and self.valid_nodes[i] then + local startpos = self:GetWorldPosition() + local endpos = self.nodes["Node"..i]:GetWorldPosition() + local endang = self.nodes["Node"..i]:GetWorldAngles() + local screen_endpos = endpos:ToScreen() + render.DrawLine(endpos,endpos + endang:Forward()*4, Color(255,0,0)) + render.DrawLine(endpos,endpos - endang:Right()*4, Color(0,255,0)) + render.DrawLine(endpos,endpos + endang:Up()*4, Color(0,0,255)) + render.DrawLine(self:GetWorldPosition(),self.nodes["Node"..i]:GetWorldPosition(), Color(255,255,255)) + elseif self.valid_nodes[i - 1] and self.valid_nodes[i] then + local startpos = self.nodes["Node"..i-1]:GetWorldPosition() + local endpos = self.nodes["Node"..i]:GetWorldPosition() + local endang = self.nodes["Node"..i]:GetWorldAngles() + local screen_endpos = endpos:ToScreen() + render.DrawLine(endpos,endpos + endang:Forward()*4, Color(255,0,0)) + render.DrawLine(endpos,endpos - endang:Right()*4, Color(0,255,0)) + render.DrawLine(endpos,endpos + endang:Up()*4, Color(0,0,255)) + render.DrawLine(self.nodes["Node"..i-1]:GetWorldPosition(),self.nodes["Node"..i]:GetWorldPosition(), Color(255,255,255)) + end + + end end) end self:Interpolate(stage,proportion) @@ -90,8 +118,9 @@ function PART:OnDraw() end function PART:UpdateNodes() - for i=1,10,1 do + for i=1,20,1 do self.nodes["Node"..i] = self["Node"..i] + self.valid_nodes[i] = IsValid(self["Node"..i]) and self["Node"..i].GetWorldPosition end end @@ -117,24 +146,17 @@ function PART:Interpolate(stage, proportion) proportion = math.pow(proportion,self.Power) if secondnode ~= nil and secondnode ~= NULL then - self.pos = (1-proportion)*(firstnode:GetWorldPosition()) + (secondnode:GetWorldPosition())*proportion - self.ang = GetClosestAngleMidpoint(firstnode:GetWorldAngles(), secondnode:GetWorldAngles(), proportion) - --self.ang = (1-proportion)*(firstnode:GetWorldAngles() + Angle(360,360,360)) + (secondnode:GetWorldAngles() + Angle(360,360,360))*proportion - elseif proportion == 0 then - self.pos = firstnode:GetWorldPosition() - self.ang = firstnode:GetWorldAngles() - else - if self.InterpolatePosition then self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion end - if self.InterpolateAngles then self.ang = GetClosestAngleMidpoint(self:GetWorldAngles(), self:GetWorldAngles(), proportion) end + if self.InterpolatePosition then + self.pos = LerpVector(proportion,firstnode:GetWorldPosition(), secondnode:GetWorldPosition()) + --self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion + else self.pos = self:GetWorldPosition() end + if self.InterpolateAngles then + self.ang = LerpAngle(proportion, firstnode:GetWorldAngles(), secondnode:GetWorldAngles()) + --self.ang = GetClosestAngleMidpoint(self:GetWorldAngles(), self:GetWorldAngles(), proportion) + else self.ang = self:GetWorldAngles() end --self.ang = (1-proportion)*(self:GetWorldAngles() + Angle(360,360,360)) + (self:GetWorldAngles() + Angle(360,360,360))*proportion end - if not self.InterpolatePosition then - self.pos = self:GetWorldPosition() - end - if not self.InterpolateAngles then - self.ang = self:GetWorldAngles() - end self.Owner:SetPos(self.pos) self.Owner:SetAngles(self.ang) end @@ -227,4 +249,4 @@ end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From d88450f59f99d4dab6768edf30bd7a7e3ef631f5 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 8 Nov 2023 22:54:23 -0500 Subject: [PATCH 090/300] changelog feature and default partmenu adjustments update message code moved to pac_version.lua 3 current options for update messages -took some of 2023's commits as monthly changelogs as a starting point as a HTML -link to develop's commit history -my readme as a HTML factory preset for partmenu actions will include bulk select, bulk apply properties and help, and add a legacy preset without them --- lua/autorun/pac_version.lua | 311 ++++++++++++++++++ lua/pac3/editor/client/menu_bar.lua | 6 +- lua/pac3/editor/client/parts.lua | 3 +- .../editor/client/popups_part_tutorials.lua | 36 -- lua/pac3/editor/client/settings.lua | 3 + 5 files changed, 320 insertions(+), 39 deletions(-) diff --git a/lua/autorun/pac_version.lua b/lua/autorun/pac_version.lua index 6259e59ce..25b2ae48e 100644 --- a/lua/autorun/pac_version.lua +++ b/lua/autorun/pac_version.lua @@ -44,3 +44,314 @@ concommand.Add("pac_version", function() end) end end) + + + +--if a major enough update happens, we can perhaps change this cvar's behavior in the hook below and keep it to track major updates as a number +local update_cvar = CreateConVar("pac_show_message_on_startup", "1", {FCVAR_ARCHIVE}, "Whether to show the update notification when loading in") + +--accessed in the editor under pac-help-version +function pac.OpenMOTD(mode) + local pnl = vgui.Create("DFrame") + pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) + + local html = vgui.Create("DHTML", pnl) + + + if mode == "combat_update" then + pnl:SetTitle("Welcome to a new update!") + html:SetHTML(pace.cedrics_combat_update_changelog_html) + elseif mode == "local_changelog" then + pnl:SetTitle("Latest changes of this installation") + html:SetHTML(pace.current_version_changelog_html) + elseif mode == "commit_history" then + pnl:SetTitle("Newest changes from the develop branch (please update your PAC version!)") + html:OpenURL("https://github.com/CapsAdmin/pac3/commits/develop") + end + html:Dock(FILL) + + pnl:Center() + pnl:MakePopup() +end + + +hook.Add("InitPostEntity", "PAC_Update_News", function() + if update_cvar:GetBool() then + timer.Simple(5, function() + if LocalPlayer():IsAdmin() then + notification.AddLegacy("Welcome. Player Appearance Customizer (PAC3) has recently received a major update, The Combat Update, or \"PAC4.5\"\nYou can review the latest additions in the editor, in pac-help-Version-update news", NOTIFY_GENERIC, 15) + end + end) + + timer.Simple(10, function() + if LocalPlayer():IsAdmin() then + notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat! They're off by default though.", NOTIFY_GENERIC, 15) + end + end) + update_cvar:SetBool(false) + end +end) + +--CHANGELOGS + +--one for the current install. edit as needed! +pace.current_version_changelog_html = [[ + +

Reminder: If you installed from github into your addons folder, you have to update manually

+

Major update special! : The Combat Update "PAC4.5"

+ +

new parts

+

damage zone

+

lock

+

force

+

health modifier

+

hitscan

+

interpolator

+ +

major editor customizability

+

keyboard shortcuts and new actions

+

part menu actions

+

part categories

+

favorites system and proxy/command banks

+

popup system (F1) and tutorials for parts and events

+

eventwheel colors

+ +

Bulk Select + Bulk Apply Properties

+ +

PAC Copilot

+

highlight an important property when creating some parts

+

re-focus to pac camera when creating camera part

+

setup command event if writing a generic name in an event

+

right click a material part's load VMT to gather the active model's materials for fast editing

+ +

Command event features

+

pac_event_sequenced command for activating series (combos, variations etc.)

+

eventwheel colors

+

eventwheel styles and grid eventwheel

+

pac_eventwheel_visibility_rule command for filtering eventwheel with many choices and keywords

+ +

More miscellaneous features

+

nearest life aim part names : point movable parts toward the nearest NPC or player

+

prompt for autoload : optional menu to pick between autoload.txt, your autosave backup, or your latest loaded outfit

+

improvements to player movement, physics, projectile parts

+ + +

Changelog : November 2023

+

Fixed legacy lights

+ +

Changelog : October 2023

+

Fixed lights

+

Fixed webcontent limit cvar's help text

+

Add hook for autowear

+ +

Changelog : September 2023

+

Fix for .obj urls

+

Text part: define fonts, 2D text modes, more data sources

+

Updated README

+

prepare for automated develop deployment

+

keep original proxy data in .vmt files

+

fix player/weapon color proxies in mdlzips

+

add new material files when looking for missing ones

+ +

Changelog : August 2023

+

"spawn as props" bone scale retention

+

Fix/rework submitpart

+ +

Changelog : July 2023

+

small fix for sequential in legacy sound

+

less awkward selection when deleting keyframes

+

more proxy functions

+

Changed Hands SWEP to work better with pac

+

Reduce network usage on entity mutators

+

Various command part error handling

+ +

Changelog : June 2023

+

sequential playback for legacy sound and websound parts

+

more proxy functions

+ +

Changelog : June 2023

+

Fix new dropbox urls

+ +

Changelog : May 2023

+

make "free children" actually work

+

add a way to disable the eargrab animations

+ +

Changelog : April 2023

+

Fix voice volume error spam

+

Add input to proxy error

+

new hitpos pac bones

+

beam start/end width multipliers

+

text part features

+

a fix for singleplayer mute footsteps

+

options for particles

+

Prevent excessive pac size abuse

+ +

Changelog : February 2023

+

sort vmt directories

+

add support for 'patch' materials in mdlzips

+ +

Changelog : January 2023

+

fixing a small bug with autoload and button events

+

Added bearing to event and proxy to help with 2d sprites

+

maybe fix command events

+

add default values to command events

+

add cvars to hide pac cameras and/or 'in-editor' information

+ +]] + + +--cedric's PAC4.5 combat update readme. please don't touch! it's a major update that happened once so it doesn't make sense to edit it after that +pace.cedrics_combat_update_changelog_html = [[ + +

PAC4.5

+
+

Welcome to my combat update for PAC3. Here's the overview of the important bits to expect.

+

New combat-related parts:

+

+	damage_zone: deals damage (a more direct and controllable alternative to projectiles)
+	
+	hitscan: shoots bullets
+	
+	lock: teleport/grab
+	
+	force: does physics forces
+	
+	health_modifier: changes your health, armor etc
+	
+	interpolated_multibone: morphs position / angles between different base_movables nodes, like a path
+	
+

The combat features work with the principle of consent. The lock part especially is severely restricted for grabbing players, for what should be obvious reasons. You can only damage or grab players who have opted in for the corresponding consent.

+

+	pac_client_damage_zone_consent 0
+	pac_client_hitscan_consent 0
+	pac_client_force_consent 0
+	pac_client_grab_consent 0
+	pac_client_lock_camera_consent 0
+	
+

There are also commands for clients to free themselves if they're being grabbed.

+

+	pac_break_lock
+	pac_stop_lock
+	
+

Multiple options exist for servers to prevent mass abuse. cvars, size limits, damage limits, entity limits, which combat parts are allowed, as well as several net-protecting options to ease the load on the server's processing and on the network (reliable channel)...

+

I know how big of a change this is. When creating the settings the first time, the combat parts will only be enabled for singleplayer sandbox. In multiplayer and in other gamemodes, it will be 0.

+

+	pac_sv_combat_whitelisting 0
+	pac_sv_damage_zone 1
+	pac_sv_lock 1
+	pac_sv_lock_grab 1
+	pac_sv_lock_teleport 1
+	pac_sv_lock_max_grab_radius 200
+	pac_sv_combat_enforce_netrate 0
+	pac_sv_entity_limit_per_combat_operation 500
+	pac_sv_entity_limit_per_player_per_combat_operation 40
+	pac_sv_player_limit_as_fraction_to_drop_damage_zone 1
+	pac_sv_block_combat_features_on_next_restart 0
+	...
+	
+ + +

Editor features:

+

Bulk Select

+

Select multiple parts and do some basic operations repeatedly. By default it's CTRL + click to select/unselect a part.

+

Along with it, Bulk Apply Properties is a new menu to change multiple parts' properties at once.

+

pac_bulk_select_halo_mode is a setting to decide when to highlight the bulk selected parts with the usual hover halos

+ +

Extensive customizability (user configs will be saved in data/pac3_config)

+

Customizable shortcuts for almost every action (in the pac settings menu).

+

Reordering the part menu actions layout (in the pac settings menu).

+

Changing your part categories, with possible custom icons.

+

Command events:

+

pac_eventwheel_visibility_rule

+

Colors for the event wheel (with a menu)

+

A new grid style and some sub-styles to choose from

+

Changeable activation modes between mouse click and releasing the bind

+

Visibility rules / filtering modes to filter out keywords etc. see the command pac_eventwheel_visibility_rule for more info.

+ + +

Expanded settings menu

+

Clients can configure their editor experience, and owners with server access can configure serverwide combat-related limits and policies and more.

+

Favorite assets for quick access (user configs will be saved in data/pac3_config)

+

right click on assets in the pac asset browser to save it to your favorites. it can also try to do series if they end in a number, but it might fail. right clicking on the related field will bring it up in your list

+ + + +

A new framework to show information in the editor. Select a part and press F1 to open information about it. Currently holds part tutorials, part size and notes. It can be configured to be on various places, and different colors. Look for that in the editor's options tab.

+ + +

PAC copilot : Foolproofing and editor assist

+

Selecting an event will pick an appropriate operator, and clicking away from a proxy without a variable name will notify you about how it won't work, telling you to go back and change it

+ + +

Writing a name into an event's type will create a command event with that name if the name isn't a recognized event type, so you can quickly setup command events.

+ + +

auto-disable editor camera to preview the camera part when creating a camera part

+ + +

auto-focus on the relevant property when creating certain parts

+ + +

Reference and help features

+

proxy bank: some presets with tooltip explanations. right click on the expression field to look at them

+ + +

command bank: presets to use the command part. again, right click on the expression field to look at them

+ + +

A built-in wiki written by me, for every part and most event types: short tooltips to tell you what a part does when you hover over the label when choosing which part to create, longer tutorials opened with F1 when you select an existing part.

+ + +

Miscellaneous features

+

Part notes

+

a text field for the base_part, so you can write notes on any part.

+

Prompt for autoload

+

option to get a prompt to choose between your autoload file, your latest backup or latest loaded outfit when starting.

+

Queue prop/NPC outfits (singleplayer only)

+

option so that, when loading an outfit for props/NPCs, instead of hanging in the editor and needing to reassign the owner name manually, pac will not wear yet, but wait for you to spawn an appropriate prop or entity that had the outfit.

+ + +

pac_event_sequenced

+

pac_event but with more options to control series of numbered events.

+

pac will try to register the max number when you create a command event with the relevant number e.g. to reach command10 you need to have a command event with the name command10. rewear for best results.

+

examples:

+

this increments by 1 (and loops back if necessary)

+
pac_event_sequenced hat_style +
+	
+

this sets the series to 3

+
pac_event_sequenced hat_style set 3
+	
+

keywords for going forward: +, add, forward, advance, sequence+

+

keywords for going backward: -, sub, backward, sequence-

+

keyword to set: set

+ + +

Improvements to physics and projectile parts

+

Set the surface properties, preview the sizes and some more.

+

For projectiles to change the physics mesh (override mesh), it might have some issues.

+ +

Improvements to the player movement part

+

option to preserve in first person

+

An attempt to handle water, glass and ice better.

+ + +

Bigger fonts for the editor + pac_editor_scale for the tree's scale

+

just a quick edit for people with higher resolution screens

+ + +

New hover halos

+

You can recolor your hover halos for when you mouse over model parts and the bulk select

+

the default, pac_hover_color 255 255 255, is white, but you can change the R G B values or use the keywords rainbow, ocean, rave or none

+

pac_hover_pulserate controls the speed

+

pac_hover_halo_limit controls how many parts can be drawn. if there are too many, pac can break

+ +

New tools

+

-destroy hidden parts, proxies and events. I also call it Ultra cleanup. This is a quick but destructive optimization tool to improve framerate by only keeping visible parts and obliterating non-static elements. You can mark parts to keep by writing "important" in their notes field.

+

-Engrave targets: assign proxies and events' target part to quickly allow you to reorganize them in a separate group in the editor.

+

-dump model submaterials: same as dump player submaterials (prints the submaterials in the console) but for a pac3 model you select in the tree

+
+

Thank you for reading. Now go make something cool!

+

Yours truly,

+

Cédric.

+ + ]] diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 40347b728..76f359057 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -80,8 +80,10 @@ local function populate_pac(menu) version_pnl:SetImage(pace.MiscIcons.info) version:AddOption(version_string) - - version:AddOption("update news", function() pac.OpenMOTD(false) end) + + version:AddOption("local update changelogs", function() pac.OpenMOTD("local_changelog") end) + version:AddOption("external commit history", function() pac.OpenMOTD("commit_history") end) + version:AddOption("major update news (combat update)", function() pac.OpenMOTD("combat_update") end) end diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 4c197c27a..0c1a1be49 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -8,7 +8,8 @@ pace.BulkSelectClipboard = {} 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"} -pace.operations_default = {"help_part_info", "wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "spacer", "save", "load", "spacer", "remove"} +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"} pace.operations_experimental = {"help_part_info", "wear", "copy", "paste", "cut", "paste_properties", "clone", "bulk_select", "spacer", "registered_parts", "spacer", "bulk_apply_properties", "partsize_info", "copy_uid", "spacer", "save", "load", "spacer", "remove"} pace.operations_bulk_poweruser = {"bulk_select","clone", "registered_parts", "spacer", "copy", "paste", "cut", "spacer", "wear", "save", "load", "partsize_info"} diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index 2b488ed63..e7336c999 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -1140,40 +1140,4 @@ do end end -local update_cvar = CreateConVar("pac_show_message_on_startup", "1", {FCVAR_ARCHIVE}, "Whether to show the update notification when loading in") ---accessed in the editor under pac-help-version -function pac.OpenMOTD() - local pnl = vgui.Create("DFrame") - pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) - - local url = "https://github.com/pingu7867/pac3#readme" - - pnl:SetTitle("Welcome to a new update!") - local html = vgui.Create("DHTML", pnl) - html:Dock(FILL) - html:OpenURL( url ) - - - pnl:Center() - pnl:MakePopup() - pace.motd_opened = true -end - - -hook.Add("InitPostEntity", "PAC_Update_News", function() - if update_cvar:GetBool() then - timer.Simple(5, function() - if LocalPlayer():IsAdmin() then - notification.AddLegacy("Welcome. Player Appearance Customizer (PAC3) has recently received a major update, The Combat Update, or \"PAC4.5\"\nYou can review the latest additions in the editor, in pac-help-Version-update news", NOTIFY_GENERIC, 15) - end - end) - - timer.Simple(10, function() - if LocalPlayer():IsAdmin() then - notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat! They're off by default though.", NOTIFY_GENERIC, 15) - end - end) - update_cvar:SetBool(false) - end -end) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 609c4bb84..01356e128 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1366,6 +1366,7 @@ function pace.FillEditorSettings(pnl) local partmenu_order_presets = vgui.Create("DComboBox",LeftPanel) partmenu_order_presets:SetText("Select a part menu preset") partmenu_order_presets:AddChoice("factory preset") + partmenu_order_presets:AddChoice("legacy") partmenu_order_presets:AddChoice("expanded PAC4.5 preset") partmenu_order_presets:AddChoice("bulk select poweruser") partmenu_order_presets:AddChoice("user preset") @@ -1822,6 +1823,8 @@ function pace.FillEditorSettings(pnl) local temp_list = {"wear","save","load"} if value == "factory preset" then temp_list = table.Copy(pace.operations_default) + elseif value == "legacy" then + temp_list = table.Copy(pace.operations_legacy) elseif value == "expanded PAC4.5 preset" then temp_list = table.Copy(pace.operations_experimental) elseif value == "bulk select poweruser" then From 01a779a6b4475433addbff7395d67c5d25f13f67 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 9 Nov 2023 19:56:31 -0500 Subject: [PATCH 091/300] fully removed some events too much networking would be required to make them work, they don't make the cut after all --- lua/pac3/core/client/parts/event.lua | 337 +------------------------- lua/pac3/core/server/net_messages.lua | 140 ----------- 2 files changed, 1 insertion(+), 476 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 91bef3555..b509a3e55 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2080,261 +2080,7 @@ PART.OldEvents = { return false end }, ---@note take damage is like health_lost but 400% better - - take_damage = { - operator_type = "mixed", preferred_operator = "above", - arguments = {{time = "number"}, {damage = "number"}, {attackers = "string"}, {inflictors = "string"}, {damage_type = "number"}}, - userdata = {{default = 1}, {default = 10}, - {default = "any", - enums = function() - local players = {} - for i,v in ipairs(player.GetAll()) do - players[v:Nick()] = v:SteamID() - end - return players - end - }, {default = "any"}, - {default = -1, - enums = function() - local damage_enums = {} - for k,v in pairs(_G) do - if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then - damage_enums[k] = tostring(v) - end - end - return damage_enums - end - }}, - callback = function(self, ent, time, damage, attackers, inflictors, damage_type) - local time = time or 0 - if not IsValid(ent) then return false end - local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" - local found_attacker = attackers == "" or attackers == "anyone" or attackers == "any" or attackers == "all" - - local unspec_inflictor = found_inflictor - local unspec_attacker = found_attacker - - local unspec_dmg = damage_type == -1 - local matching_dmg = unspec_dmg - - local lastest_attacker = nil - local latest_hit_time = 0 - - if not ent.pac_damage_attributions then - ent.pac_damage_attributions = {} - return false - elseif (table.Count(ent.pac_damage_attributions) < 1) then - return false - end - - ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 - - if CurTime() < ent.pac_damage_attributions.IngoingGraceTime + time then return true end - if not ent.pac_damage_attributions then return false end --the entity is a damage virgin, it's not hurt - - - for attacker,tbl in pairs(ent.pac_damage_attributions) do - if IsValid(attacker) and IsValid(tbl.inflictor) then - local found_inflictor_class = false - for i,v in ipairs(string.Split(inflictors, ";")) do - if v == tbl.inflictor:GetClass() then - found_inflictor = true - end - end - for i,v in ipairs(string.Split(attackers, ";")) do - if v == tbl.attacker:GetClass() then - found_attacker = true - elseif tbl.attacker:IsPlayer() then - if tbl.attacker:SteamID() == v or tbl.attacker:Nick() == v then - found_attacker = true - end - --print("tested attacker ", tbl.attacker:GetClass(), "it aint it.", v) - end - end - if tbl.hit_time > latest_hit_time and (bit.band(tbl.dmg_type, damage_type) ~= 0 or unspec_dmg) then - matching_dmg = true - latest_hit_time = tbl.hit_time - lastest_attacker = attacker - end - else --lost entity! i.e. grenades get removed so we can't use direct entity reference anymore - --so we give it a grace period for next time - if not type(attacker) == "table" then --exclude the grace fields... - ent.pac_damage_attributions.IngoingGraceTime = CurTime() - ent.pac_damage_attributions[attacker] = nil - end - end - - end - if not unspec_dmg and not matching_dmg then return false end - --print(ent.pac_damage_attributions.IngoingGraceTime) - ent.pac_damage_attributions.IngoingGraceTime = ent.pac_damage_attributions.IngoingGraceTime or 0 - - --print("CurTime:"..CurTime(), "Grace:" .. ent.pac_damage_attributions.IngoingGraceTime) - - - if found_attacker and found_inflictor then - if ent.pac_damage_attributions[lastest_attacker] then - if CurTime() < ent.pac_damage_attributions[lastest_attacker].hit_time + time then - return self:NumberOperator(ent.pac_damage_attributions[lastest_attacker].dmg_amount, damage) - end - elseif CurTime() < ent.pac_damage_attributions.IngoingGraceTime + time then - return true - end - - end - - if unspec_attacker and unspec_inflictor then - - if ent.pac_damage_attributions.latest then - - if (CurTime() < ent.pac_damage_attributions.latest.hit_time + time) then - return self:NumberOperator(ent.pac_damage_attributions.latest.dmg_amount, damage) - end - end - end - - return false - end, - nice = function(self, ent, time, damage, attackers, inflictors, damage_type) - time = time or 0 - damage = damage or 0 - attackers = attackers or "" - inflictors = inflictors or "" - damage_type = damage_type or -1 - local str = "take_damage : [" .. self.Operator .. " " .. damage .. "]" - if attackers == "" or attackers == "any" or attackers == "anyone" or attackers == "all" then - str = str .. " | from any attacker " - else - str = str .. " | from attackers: " - for i,v in ipairs(string.Split(attackers, ";")) do - str = str .. v .. " " - end - end - for i,v in ipairs(string.Split(attackers, ";")) do - str = str .. v .. " " - end - if inflictors == "" or inflictors == "any" or inflictors == "all" then - str = str .. " | from any inflictor" - else - str = str .. " | from inflictors: " - for i,v in ipairs(string.Split(inflictors, ";")) do - str = str .. v .. " " - end - end - str = str .. " | with damage types : " .. damage_type - return str - end - }, - - inflicting_damage = { - operator_type = "mixed", preferred_operator = "above", - tutorial_explanation = "", - arguments = {{time = "number"}, {damage = "number"}, {targets = "string"}, {inflictors = "string"}, {damage_type = "number"}}, - userdata = {{default = 1}, {default = 10}, - {default = "any", - enums = function() - local players = {} - for i,v in ipairs(player.GetAll()) do - players[v:Nick()] = v:SteamID() - end - return players - end - }, {default = "any"}, - {default = -1, - enums = function() - local damage_enums = {} - for k,v in pairs(_G) do - if isstring(k) and isnumber(v) and k:sub(0,4) == "DMG_" then - damage_enums[k] = tostring(v) - end - end - return damage_enums - end - }}, - callback = function(self, ent, time, damage, targets, inflictors, damage_type) - local time = time or 0 - - if not IsValid(ent) then return false end - local found_inflictor = inflictors == "" or inflictors == "any" or inflictors == "all" - local found_target = targets == "" or targets == "anyone" or targets == "any" or targets == "all" - local unspec_dmg = damage_type == -1 - - ent.pac_damage_attributions = ent.pac_damage_attributions or {} - ent.pac_damage_attributions.OutgoingGraceTime = ent.pac_damage_attributions.OutgoingGraceTime or 0 - ent.pac_damage_attributions.OutgoingGraceTimeDMG = ent.pac_damage_attributions.OutgoingGraceTimeDMG or 0 - local latest_hit_time = ent.pac_damage_attributions.OutgoingGraceTime or 0 - - for _,target in pairs(ents.GetAll()) do --check ents we could hurt - if target.pac_damage_attributions then --skip the virgins - if target.pac_damage_attributions[ent] then --we're in. - - tbl = target.pac_damage_attributions[ent] - if not found_inflictor then - for i,v in ipairs(string.Split(inflictors, ";")) do - if v == tbl.inflictor:GetClass() then - found_inflictor = true - end - end - end - if not found_target then - for i,v in ipairs(string.Split(targets, ";")) do - if v == target:GetClass() then - found_target = true - elseif target:IsPlayer() then - if target:SteamID() == v or target:Nick() == v then - found_target = true - end - end - end - end - - if tbl.hit_time > latest_hit_time and (bit.band(tbl.dmg_type, damage_type) ~= 0 or unspec_dmg) then - latest_hit_time = CurTime() - ent.pac_damage_attributions.OutgoingGraceTime = CurTime() - ent.pac_damage_attributions.OutgoingGraceTimeDMG = tbl.dmg_amount - end - end - end - end - --WHAT ABOUT KILLS?? DONT WORRY ABOUT IT (TM) - --print("CurTime:" .. CurTime(), "out grace"..ent.pac_damage_attributions.OutgoingGraceTime) - if found_target and found_inflictor then - if CurTime() < ent.pac_damage_attributions.OutgoingGraceTime + time then - return self:NumberOperator(ent.pac_damage_attributions.OutgoingGraceTimeDMG, damage) - end - end - - return false - end, - nice = function(self, ent, time, damage, targets, inflictors, damage_type) - time = time or 0 - damage = damage or 0 - attackers = attackers or "" - targets = targets or "" - damage_type = damage_type or -1 - local str = "inflicting_damage : [" .. self.Operator .. " " .. damage .. "]" - if targets == "" or targets == "anyone" or targets == "any" or targets == "all" then - str = str .. " | to any target" - else - str = str .. " | to targets: " - for i,v in ipairs(string.Split(inflictors, ";")) do - str = str .. v .. " " - end - end - if inflictors == "" or inflictors == "any" or inflictors == "all" then - str = str .. " | from any inflictor " - else - str = str .. " | from inflictors: " - for i,v in ipairs(string.Split(inflictors, ";")) do - str = str .. v .. " " - end - end - str = str .. " | with damage types : " .. damage_type - return str - end - }, - + damage_zone_hit = { operator_type = "mixed", preferred_operator = "above", arguments = {{time = "number"}, {damage = "number"}, {uid = "string"}}, @@ -2676,15 +2422,6 @@ do end) - pac.player_inputs = {} - pac.player_inputs_update_times = {} - - net.Receive("pac.BroadcastPlayerInputs", function() - pac.player_inputs = net.ReadTable() - pac.player_inputs_update_times = net.ReadTable() - end) - - PART.OldEvents.button = { operator_type = "none", arguments = {{button = "string"}, {holdtime = "number"}, {toggle = "boolean"}}, @@ -2769,78 +2506,6 @@ do end, } - --PART.OldEvents.input = { - -- operator_type = "none", - -- arguments = {{UserInputs = "string"}, {RequireAllInputs = "boolean"}}, - -- userdata = {{enums = function() - -- return base_input_enums_names - -- end}}, - -- callback = function(self, ent, UserInputs, RequireAllInputs) - -- local ply = self:GetPlayerOwner() - -- UserInputs = UserInputs or "" - -- pac.player_inputs[ply] = pac.player_inputs[ply] or {} - -- local detect = false - -- local fulldetect = true - -- local input_list = string.Split(UserInputs, ";") - -- - -- for i,v in pairs(pac.player_inputs[ply]) do - -- for _,v2 in pairs(input_list) do - -- if pac.player_inputs[ply][input_aliases[v2]] then detect = true - -- else fulldetect = false end - -- end - -- end - -- if RequireAllInputs then return fulldetect - -- else return detect end - -- end - --} - - --[[PART.OldEvents.is_moving = { - operator_type = "none", - callback = function(self) - local ply = self:GetPlayerOwner() - pac.player_inputs = pac.player_inputs or {} - pac.player_inputs[ply] = pac.player_inputs[ply] or {} - return pac.player_inputs[ply][IN_FORWARD] or - pac.player_inputs[ply][IN_BACK] or - pac.player_inputs[ply][IN_MOVELEFT] or - pac.player_inputs[ply][IN_MOVERIGHT] or - pac.player_inputs[ply][IN_JUMP] - end - }]] - - --[[PART.OldEvents.afk = { - operator_type = "none", - arguments = {{time = "number"}, {IncludeEyeAngles = "boolean"}}, - callback = function(self, ent, time, IncludeEyeAngles) - local time = time or 0 - local IncludeEyeAngles = IncludeEyeAngles - local ply = self:GetPlayerOwner() - local time_bool = false - local eyes_bool = false - - if pac.player_inputs_update_times then - pac.player_inputs_update_times[ply] = pac.player_inputs_update_times[ply] or 0 - time_bool = pac.player_inputs_update_times[ply] + time > CurTime() - end - - ply.last_eyeang = ply.last_eyeang or ply:EyeAngles() - ply.eyeang_update_time = ply.eyeang_update_time or CurTime() - - if ply.last_eyeang ~= ply:EyeAngles() then - ply.eyeang_update_time = CurTime() - end - - eyes_bool = (ply.last_eyeang ~= ply:EyeAngles()) or (ply.eyeang_update_time + time > CurTime()) - ply.last_eyeang = ply:EyeAngles() - if IncludeEyeAngles then - return not (time_bool or eyes_bool) - else - return not time_bool - end - return true - end - }]] - end do diff --git a/lua/pac3/core/server/net_messages.lua b/lua/pac3/core/server/net_messages.lua index 235b07ea0..2076fd32c 100644 --- a/lua/pac3/core/server/net_messages.lua +++ b/lua/pac3/core/server/net_messages.lua @@ -2,13 +2,6 @@ util.AddNetworkString("pac.AllowPlayerButtons") util.AddNetworkString("pac.BroadcastPlayerButton") -util.AddNetworkString("pac.BroadcastPlayerInputs") - -util.AddNetworkString("pac.RequestPlayerObjUsed") -util.AddNetworkString("pac.SendPlayerObjUsed") - -util.AddNetworkString("pac.BroadcastDamageAttributions") - do -- button event net.Receive("pac.AllowPlayerButtons", function(length, client) local key = net.ReadUInt(8) @@ -36,136 +29,3 @@ do -- button event broadcast_key(ply, key, false) end) end - ---[[do -- input event - local input_enums = { - IN_ATTACK, --1 - IN_JUMP, --2 - IN_DUCK, --4 - IN_FORWARD, --8 - IN_BACK, --16 - IN_USE, --32 - IN_CANCEL, --64 - IN_LEFT, --128 - IN_RIGHT, --256 - IN_MOVELEFT, --512 - IN_MOVERIGHT, --1024 - IN_ATTACK2, --2048 - IN_RUN, --4096 - IN_RELOAD, --8192 - IN_ALT1, --16384 - IN_ALT2, --32768 - IN_SCORE, --65536 - IN_SPEED, --131072 - IN_WALK, --262144 - IN_ZOOM, --524288 - IN_WEAPON1, --1048576 - IN_WEAPON2, --2097152 - IN_BULLRUSH, --4194304 - IN_GRENADE1, --8388608 - IN_GRENADE2 --16777216 - } - - local pac_broadcast_inputs = {} - local last_input_broadcast = 0 - local player_last_input_broadcast_times = {} - - local function broadcast_inputs(update) - - if not update or not (last_input_broadcast + 0.05 < CurTime()) then return - elseif update - net.Start("pac.BroadcastPlayerInputs") - net.WriteTable(pac_broadcast_inputs) - net.WriteTable(player_last_input_broadcast_times) - net.Broadcast() - last_input_broadcast = CurTime() - end - end - - pac.AddHook("Tick", "PACBroadcastPlayerInputs", function() - - local update = false - local updated_players = {} - pac_broadcast_inputs = pac_broadcast_inputs or {} - local last_broadcast_inputs = table.Copy(pac_broadcast_inputs) - local time = CurTime() - for _,ply in pairs(player.GetAll()) do - if not pac_broadcast_inputs[ply] then pac_broadcast_inputs[ply] = {} end - for _,v in pairs(input_enums) do - - if ply:KeyDown( v ) then - pac_broadcast_inputs[ply][v] = true - elseif ply:KeyDownLast( v ) then - pac_broadcast_inputs[ply][v] = false - end - - if last_broadcast_inputs[ply] and pac_broadcast_inputs[ply] then - if last_broadcast_inputs[ply][v] ~= pac_broadcast_inputs[ply][v] then - update = true - player_last_input_broadcast_times[ply] = CurTime() - end - end - - - end - end - broadcast_inputs(update) - - end) -end]] - -do --is_using_entity - local function send_player_used_object(client, ent, class, b, from_client) - net.Start("pac.SendPlayerObjUsed") - net.WriteEntity(client) - net.WriteEntity(ent) - net.WriteString(class) - net.WriteBool(b) - - --print("BROADCAST", client, ent, class, b) - net.Send(player.GetAll()) - end - - net.Receive("pac.RequestPlayerObjUsed", function(length, client) - local from_client = true - local override = true - local ent_used = client:GetEntityInUse() - local class = "nil" - if ent_used and IsValid(ent_used) then - if ent_used.GetClass ~= nil then - class = ent_used:GetClass() - end - end - if class == "player_pickup" then - override = false - end - - send_player_used_object(client, ent_used, class, override, from_client) - end) - - hook.Add("PlayerUse", "pac.PlayerUse", function( client, ent ) - local class = ent:GetClass() - --print("USE", client,ent, class) - if ent:GetClass() ~= "player_pickup" then - send_player_used_object(client, ent, class, true, false) - end - end) -end - -do --damage attribution - timer.Simple(1, function()--call it regularly in case a new hook overrides the damage, we want the final one - pac.AddHook("EntityTakeDamage", "pac.AttributeDamage", function(ent, dmg) - local time = CurTime() - if IsValid(dmg:GetAttacker()) then - local tbl = {hit_time = time, attacker = dmg:GetAttacker(), dmg_amount = dmg:GetDamage(), dmg_type = dmg:GetDamageType(), inflictor = dmg:GetInflictor()} - ent[dmg:GetInflictor()] = tbl - net.Start("pac.BroadcastDamageAttributions") - net.WriteEntity(ent) - net.WriteTable(tbl) - net.WriteBool(ent:Health() < dmg:GetDamage()) - net.Broadcast() - end - - end) - end) -end \ No newline at end of file From 40734fd259f9f462b9e934626a6c3fc7ab9089aa Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 9 Nov 2023 21:34:54 -0500 Subject: [PATCH 092/300] fix lights again fix number underflow by truncating the id string a bit --- lua/pac3/core/client/parts/light.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/light.lua b/lua/pac3/core/client/parts/light.lua index 36249ad3f..2f3d041e0 100644 --- a/lua/pac3/core/client/parts/light.lua +++ b/lua/pac3/core/client/parts/light.lua @@ -22,7 +22,7 @@ BUILDER:EndStorableVars() function PART:GetLight() if not self.light then - self.light = DynamicLight(tonumber(self:GetPrintUniqueID(), 16)) + self.light = DynamicLight(tonumber(string.sub(self:GetPrintUniqueID(),1,7), 16)) end self.light.decay = 0 self.light.dietime = math.huge @@ -110,4 +110,4 @@ function PART:OnHide() self:RemoveLight() end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From ab7d14d5050e249c32c95bf74d95b39a841f8f54 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 9 Nov 2023 21:37:55 -0500 Subject: [PATCH 093/300] fix lights again (legacy) strange underflow bug --- lua/pac3/core/client/parts/legacy/light.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/legacy/light.lua b/lua/pac3/core/client/parts/legacy/light.lua index f6986ab64..69a274866 100644 --- a/lua/pac3/core/client/parts/legacy/light.lua +++ b/lua/pac3/core/client/parts/legacy/light.lua @@ -22,7 +22,7 @@ local DynamicLight = DynamicLight function PART:OnDraw() local pos = self:GetDrawPosition() - local light = self.light or DynamicLight(tonumber(self:GetPrintUniqueID(), 16)) + local light = self.light or DynamicLight(tonumber(string.sub(self:GetPrintUniqueID(),1,7), 16)) light.Pos = pos @@ -50,4 +50,4 @@ function PART:OnHide() end end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From 4995f9dc6db48fd96a76387e1e7e3778cc881277 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 9 Nov 2023 22:36:20 -0500 Subject: [PATCH 094/300] more fixes fix lock's affect player owner no-colliding the grabbed entity is optional another entity field nil check damage zone: don't exclude 0 health props in the search if trying to dissolve limit the pac_sv_combat_reinitialize_missing_receivers cvar multiplayer disables combat parts by default server option to enable custom collision meshes on physical projectiles or not --- lua/pac3/core/client/parts/lock.lua | 8 +++-- lua/pac3/core/client/parts/projectile.lua | 1 + lua/pac3/editor/client/settings.lua | 5 +++ lua/pac3/extra/shared/net_combat.lua | 44 +++++++++++++++-------- lua/pac3/extra/shared/projectiles.lua | 20 ++++++----- 5 files changed, 53 insertions(+), 25 deletions(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index ff8941ce1..2f150d827 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -29,6 +29,7 @@ BUILDER:StartStorableVars() :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"}) :GetSet("RelativeGrab", false) :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before self.grabbing are re-applied"}) + :GetSet("NoCollide", true, {description = "Whether to disable collisions on the entity while grabbed."}) :SetPropertyGroup("DetectionOrigin") :GetSet("Radius", 20) @@ -141,6 +142,7 @@ function PART:OnThink() if pac.LocalPlayer == self:GetPlayerOwner() then net.WriteBool(self.OverrideAngles) net.WriteBool(try_override_eyeang) + net.WriteBool(self.NoCollide) net.WriteEntity(self.target_ent) net.WriteEntity(self:GetRootPart():GetOwner()) local can_calcview = false @@ -430,8 +432,10 @@ function PART:DecideTarget() if self.Players and ent_candidate:IsPlayer() then --we don't want to grab ourselves if (ent_candidate ~= self:GetRootPart():GetOwner()) or (self.AffectPlayerOwner and ent_candidate == self:GetPlayerOwner()) then - chosen_ent = ent_candidate - table.insert(ents_candidates, ent_candidate) + if not (not self.AffectPlayerOwner and ent_candidate == self:GetPlayerOwner()) then + chosen_ent = ent_candidate + table.insert(ents_candidates, ent_candidate) + end elseif (self:GetPlayerOwner() ~= ent_candidate) then --if it's another player, good chosen_ent = ent_candidate table.insert(ents_candidates, ent_candidate) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 563110191..306e01553 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -254,6 +254,7 @@ function PART:Shoot(pos, ang, multi_projectile_count) net.WriteBool(self.RemoveOnCollide) net.WriteBool(self.CollideWithOwner) net.WriteBool(self.RemoveOnHide) + net.WriteBool(self.RescalePhysMesh) net.WriteBool(self.OverridePhysMesh) net.WriteBool(self.Gravity) net.WriteBool(self.AddOwnerSpeed) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 01356e128..796160910 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -950,6 +950,11 @@ function pace.FillCombatSettings(pnl) sv_projectiles_box:SetSize(400,30) sv_projectiles_box:SetConVar("pac_sv_projectiles") + local sv_projectiles_mesh_box = vgui.Create("DCheckBoxLabel", projectiles_list_list) + sv_projectiles_mesh_box:SetText("allow custom collision meshes for physical projectiles") + sv_projectiles_mesh_box:SetSize(400,30) + sv_projectiles_mesh_box:SetConVar("pac_sv_projectile_allow_custom_collision_mesh") + local projectile_max_phys_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) projectile_max_phys_radius_numbox:SetText("Max projectile physical radius") projectile_max_phys_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_phys_radius"):GetInt()) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 0283d2049..6623c7414 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -6,7 +6,7 @@ end local master_default = "0" -if string.find(engine.ActiveGamemode(), "sandbox") then +if string.find(engine.ActiveGamemode(), "sandbox") and game.SinglePlayer() then master_default = "1" end @@ -41,7 +41,7 @@ local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_ local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") -local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") +local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") cvars.AddChangeCallback("pac_sv_block_combat_features_on_next_restart", function() print("Remember that pac_sv_block_combat_features_on_next_restart is applied on server startup! Only do it if you know what you're doing. You'll need to restart the server.") end) local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter") @@ -296,14 +296,19 @@ if SERVER then return grab_consents[ply] and calcview_consents[ply] --oops it's redundant but I prefer it this way end - local function ApplyLockState(ent, bool) --Change the movement states and reset some other angle-related things + local function ApplyLockState(ent, bool, nocollide) --Change the movement states and reset some other angle-related things --the grab imposes MOVETYPE_NONE and no collisions --reverting the state requires to reset the eyeang roll in case it was modified if ent:IsPlayer() then if bool then active_grabbed_ents[ent] = true - ent:SetMoveType(MOVETYPE_NONE) - ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) + if nocollide then + ent:SetMoveType(MOVETYPE_NONE) + ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) + else + ent:SetMoveType(MOVETYPE_WALK) + ent:SetCollisionGroup(COLLISION_GROUP_NONE) + end else active_grabbed_ents[ent] = nil ent:SetMoveType(MOVETYPE_WALK) @@ -323,8 +328,13 @@ if SERVER then elseif ent:IsNPC() then if bool then active_grabbed_ents[ent] = true - ent:SetMoveType(MOVETYPE_NONE) - ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) + if nocollide then + ent:SetMoveType(MOVETYPE_NONE) + ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) + else + ent:SetMoveType(MOVETYPE_STEP) + ent:SetCollisionGroup(COLLISION_GROUP_NONE) + end else active_grabbed_ents[ent] = nil ent:SetMoveType(MOVETYPE_STEP) @@ -687,6 +697,7 @@ if SERVER then --the function to determine if we can dissolve, based on policy and setting factors local function IsDissolvable(ent) + if not pac_sv_damage_zone_allow_dissolve then return false end local dissolvable = true local prop_protected, reason = IsPropProtected(ent, attacker) local prop_protected_final = prop_protected and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false @@ -727,7 +738,7 @@ if SERVER then --the giga function to determine if we can damage local function DMGAllowed(ent) - if ent:Health() == 0 then return false end --immediately exclude entities with 0 health + if ent:Health() == 0 and not (string.find(tbl.DamageType, "dissolve")) then return false end --immediately exclude entities with 0 health, except if we want to dissolve local canhit = false --whether the policies allow the hit local prop_protected_consent = ent:GetCreator() ~= inflictor and ent ~= inflictor and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false-- and ent:GetCreator() ~= inflictor local contraption = IsPossibleContraptionEntity(ent) @@ -872,7 +883,7 @@ if SERVER then --finally we reached the normal damage event! else - if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) and pac_sv_damage_zone_allow_dissolve then + if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) then dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType]) end dmg_info:SetDamagePosition(ent:NearestPoint(pos)) @@ -994,7 +1005,7 @@ if SERVER then else ent_count = ent_count + 1 end end - if TooManyEnts(ent_count) then return end + if TooManyEnts(ent_count) and not (tbl.AffectSelf and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end for _,ent in pairs(ents_hits) do local phys_ent if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) @@ -1706,6 +1717,7 @@ if SERVER then local ang = net.ReadAngle() local override_ang = net.ReadBool() local override_eyeang = net.ReadBool() + local no_collide = net.ReadBool() local targ_ent = net.ReadEntity() local auth_ent = net.ReadEntity() local override_viewposition = net.ReadBool() @@ -1828,7 +1840,7 @@ if SERVER then targ_ent:SetPos(pos) - ApplyLockState(targ_ent, true) + ApplyLockState(targ_ent, true, no_collide) if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end --@@note lock assignation! IMPORTANT @@ -1920,6 +1932,7 @@ if SERVER then if not IsValid(ent) then active_grabbed_ents[ent] = nil elseif (ent.grabbed_by or bool) then + ent.grabbed_by_time = ent.grabbed_by_time or 0 if ent.grabbed_by_time + 0.5 < CurTime() then --restore the movetype local grabber = ent.grabbed_by ent.grabbed_by_uid = nil @@ -2152,7 +2165,12 @@ if SERVER then if not FINAL_BLOCKED_COMBAT_FEATURES["hitscan"] then DeclareHitscanReceivers() end if not FINAL_BLOCKED_COMBAT_FEATURES["health_modifier"] then DeclareHealthModifierReceivers() end - concommand.Add("pac_sv_combat_reinitialize_missing_receivers", function() + concommand.Add("pac_sv_combat_reinitialize_missing_receivers", function(ply) + if IsValid(ply) then + if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_sv_combat_reinitialize_missing_receivers", 3, 5, {"Player ", ply, " is spamming pac_sv_combat_reinitialize_missing_receivers!"} ) then + return + end + end for name,blocked in pairs(FINAL_BLOCKED_COMBAT_FEATURES) do local update = blocked and (blocked == GetConVar("pac_sv_"..name):GetBool()) local new_bool = not (blocked or not GetConVar("pac_sv_"..name):GetBool()) @@ -2166,9 +2184,7 @@ if SERVER then elseif name == "health_modifier" then DeclareHealthModifierReceivers() print("reinitialized " .. name) end end - end - net.Start("pac_inform_blocked_parts") net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES) net.Broadcast() diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index f8ac26f06..ef426a713 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -1,10 +1,11 @@ -local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'allow physical projectiles serverside') -local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum attract radius for physical projectiles') -local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum damage radius for physical projectiles') -local pac_sv_projectile_max_phys_radius = CreateConVar("pac_sv_projectile_max_phys_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum physical radius for physical projectiles') -local pac_sv_projectile_max_speed = CreateConVar("pac_sv_projectile_max_speed", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum speed for physical projectiles') -local pac_sv_projectile_max_damage = CreateConVar("pac_sv_projectile_max_damage", 100000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum damage for physical projectiles') -local pac_sv_projectile_max_mass = CreateConVar("pac_sv_projectile_max_mass", 50000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'maximum speed for physical projectiles') +local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow physical projectiles serverside") +local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum attract radius for physical projectiles") +local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum damage radius for physical projectiles") +local pac_sv_projectile_max_phys_radius = CreateConVar("pac_sv_projectile_max_phys_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum physical radius for physical projectiles") +local pac_sv_projectile_max_speed = CreateConVar("pac_sv_projectile_max_speed", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum speed for physical projectiles") +local pac_sv_projectile_max_damage = CreateConVar("pac_sv_projectile_max_damage", 100000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum damage for physical projectiles") +local pac_sv_projectile_max_mass = CreateConVar("pac_sv_projectile_max_mass", 50000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum speed for physical projectiles") +local pac_sv_projectile_allow_custom_collision_mesh = CreateConVar("pac_sv_projectile_allow_custom_collision_mesh", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to allow other models' collision mesh as a physical projectile, rather than just box and sphere") do -- projectile entity local ENT = {} @@ -68,7 +69,7 @@ do -- projectile entity if part.Sphere then self:PhysicsInitSphere(radius, part.SurfaceProperties) else - local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel) + local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel) and pac_sv_projectile_allow_custom_collision_mesh:GetBool() --print("valid fallback? " .. part.FallbackSurfpropModel , valid_fallback) self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties) @@ -78,7 +79,7 @@ do -- projectile entity self:PhysicsInitMultiConvex(self:GetPhysicsObject():GetMeshConvexes(), part.SurfaceProperties) end - if part.RescalePhysMesh then + if valid_fallback and part.RescalePhysMesh then local physmesh = self:GetPhysicsObject():GetMeshConvexes() --hack from prop resizer for convexkey, convex in pairs( physmesh ) do @@ -519,6 +520,7 @@ if SERVER then part.RemoveOnCollide = net.ReadBool() part.CollideWithOwner = net.ReadBool() part.RemoveOnHide = net.ReadBool() + part.RescalePhysMesh = net.ReadBool() part.OverridePhysMesh = net.ReadBool() part.Gravity = net.ReadBool() part.AddOwnerSpeed = net.ReadBool() From b96758a218447420d2fe7c65a17f10987d166214 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 10 Nov 2023 23:09:26 -0500 Subject: [PATCH 095/300] remove update notification giving information to users is bad, apparently --- lua/autorun/pac_version.lua | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/lua/autorun/pac_version.lua b/lua/autorun/pac_version.lua index 25b2ae48e..2a281d396 100644 --- a/lua/autorun/pac_version.lua +++ b/lua/autorun/pac_version.lua @@ -46,10 +46,6 @@ concommand.Add("pac_version", function() end) - ---if a major enough update happens, we can perhaps change this cvar's behavior in the hook below and keep it to track major updates as a number -local update_cvar = CreateConVar("pac_show_message_on_startup", "1", {FCVAR_ARCHIVE}, "Whether to show the update notification when loading in") - --accessed in the editor under pac-help-version function pac.OpenMOTD(mode) local pnl = vgui.Create("DFrame") @@ -75,23 +71,6 @@ function pac.OpenMOTD(mode) end -hook.Add("InitPostEntity", "PAC_Update_News", function() - if update_cvar:GetBool() then - timer.Simple(5, function() - if LocalPlayer():IsAdmin() then - notification.AddLegacy("Welcome. Player Appearance Customizer (PAC3) has recently received a major update, The Combat Update, or \"PAC4.5\"\nYou can review the latest additions in the editor, in pac-help-Version-update news", NOTIFY_GENERIC, 15) - end - end) - - timer.Simple(10, function() - if LocalPlayer():IsAdmin() then - notification.AddLegacy("Looks like you're an admin. You should probably go in the settings menu to configure your server's cvars for pac combat! They're off by default though.", NOTIFY_GENERIC, 15) - end - end) - update_cvar:SetBool(false) - end -end) - --CHANGELOGS --one for the current install. edit as needed! From 60e3311af10567280823d25713cef47da73be376 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 10 Nov 2023 23:46:33 -0500 Subject: [PATCH 096/300] Update event.lua --- lua/pac3/core/client/parts/event.lua | 58 ---------------------------- 1 file changed, 58 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index b509a3e55..9e6961cfb 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -581,34 +581,6 @@ PART.OldEvents = { end, }, - is_using_entity = { - operator_type = "none", - tutorial_explanation = "For when you're picking up props, clicking buttons etc.\nAlthough not all entities will do things if you +use them, the event tries to take the class of the entity you used,\nand compares it with the one written in class", - arguments = {{class = "string"}}, - callback = function(self, ent, class) - ent = self:GetPlayerOwner() - local b = false - - if not ent:IsPlayer() then return false - elseif ent == LocalPlayer() and self.singleactivatestate then - net.Start("pac.RequestPlayerObjUsed") - net.SendToServer() - self.singleactivatestate = false - self.nextactivationrefresh = CurTime() + 0.05 - end - if ent.entity_inuse or ent.entity_inuse_classname then - if ent.entity_inuse_classname == "player_pickup" then - b = true - end - if IsValid(ent.entity_inuse) and string.find(ent.entity_inuse_classname, class, 1, true) and string.find(ent.entity_inuse:GetClass(), class, 1, true) then - b = true - end - end - - return b - end - }, - eyetrace_entity_class = { operator_type = "string", preferred_operator = "find simple", tutorial_explanation = "this compares the class of the entity you point to with the one(s) written in class", @@ -4113,36 +4085,6 @@ do end -net.Receive("pac.SendPlayerObjUsed", function() - - local ply = net.ReadEntity() - local ent = net.ReadEntity() - local class = net.ReadString() - local override = net.ReadBool() - - if ply then - if ply:IsPlayer() and override then - ply.entity_inuse = ent - ply.entity_inuse_classname = class - end - end -end) - -net.Receive("pac.BroadcastDamageAttributions", function() - local ent = net.ReadEntity() - local tbl = net.ReadTable() - local kill = net.ReadBool() - if IsValid(ent) and tbl and IsValid(tbl.inflictor) then - ent.pac_damage_attributions = ent.pac_damage_attributions or {} - ent.pac_damage_attributions[tbl.attacker] = tbl - ent.pac_damage_attributions.latest = tbl - ent.pac_damage_attributions.is_kill = kill - if tbl.inflictor:GetClass() == "npc_grenade_frag" then - ent.pac_damage_attributions.IngoingGraceTime = CurTime() - end - end - -end) net.Receive("pac_update_healthbars", function() local ent = net.ReadEntity() From e7e7d1e6ae68363dc2ecdce6d18a9fa234da4486 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 11 Nov 2023 14:43:39 -0500 Subject: [PATCH 097/300] rewrite draw shadow mutator and use it properly --- lua/pac3/core/client/parts/model/entity.lua | 2 + .../shared/entity_mutators/draw_shadow.lua | 37 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/lua/pac3/core/client/parts/model/entity.lua b/lua/pac3/core/client/parts/model/entity.lua index 793838448..ef9294719 100644 --- a/lua/pac3/core/client/parts/model/entity.lua +++ b/lua/pac3/core/client/parts/model/entity.lua @@ -41,6 +41,7 @@ function PART:SetDrawShadow(b) local ent = self:GetOwner() if not ent:IsValid() then return end + pac.emut.MutateEntity(self:GetPlayerOwner(), "draw_shadow", ent, b) ent:DrawShadow(b) ent:MarkShadowAsDirty() end @@ -211,6 +212,7 @@ function PART:OnRemove() local player_owner = self:GetPlayerOwner() pac.emut.RestoreMutations(player_owner, "model", ent) + pac.emut.RestoreMutations(player_owner, "draw_shadow", ent) if ent:IsPlayer() or ent:IsNPC() then pac.emut.RestoreMutations(player_owner, "size", ent) diff --git a/lua/pac3/core/shared/entity_mutators/draw_shadow.lua b/lua/pac3/core/shared/entity_mutators/draw_shadow.lua index 331bcde73..7a069359a 100644 --- a/lua/pac3/core/shared/entity_mutators/draw_shadow.lua +++ b/lua/pac3/core/shared/entity_mutators/draw_shadow.lua @@ -1,23 +1,24 @@ local MUTATOR = {} - -MUTATOR.ClassName = "draw_shadow" -function MUTATOR:WriteArguments(enum) - net.WriteBool(enum, 8) -end - -function MUTATOR:ReadArguments() - return net.ReadBool() -end - -if SERVER then - function MUTATOR:StoreState() - return self.Entity:GetBloodColor() - end - - function MUTATOR:Mutate(enum) - self.Entity:SetBloodColor(enum) +MUTATOR.ClassName = "draw_shadow" + +function MUTATOR:WriteArguments(b) + net.WriteBool(b) +end + +function MUTATOR:ReadArguments() + return net.ReadBool() +end + +if SERVER then + function MUTATOR:StoreState() + return self.Entity.pac_emut_draw_shadow + end + + function MUTATOR:Mutate(b) + self.Entity:DrawShadow(b) + self.Entity.pac_emut_draw_shadow = b end end - + pac.emut.Register(MUTATOR) \ No newline at end of file From 27d6c71178a4dfdb19494be5318a7f6d7f321d6b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 11 Nov 2023 15:20:18 -0500 Subject: [PATCH 098/300] nearest_life aim part name adjustments limit range to 5000 compact sorting loop into a reused local function --- lua/pac3/core/client/base_movable.lua | 51 +++++++++------------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/lua/pac3/core/client/base_movable.lua b/lua/pac3/core/client/base_movable.lua index 4175fe7aa..29a28b772 100644 --- a/lua/pac3/core/client/base_movable.lua +++ b/lua/pac3/core/client/base_movable.lua @@ -190,12 +190,13 @@ function PART:CalcAngles(ang, wpos) return self.Angles + (pac.EyePos - wpos):Angle() end - if pac.StringFind(self.AimPartName, "NEAREST_LIFE_YAW", true, true) then - local nearest_ent = self:GetRootPart():GetOwner() + local function get_nearest_ent(part) + local nearest_ent = part:GetRootPart():GetOwner() local nearest_dist = math.huge - local owner_ent = self:GetRootPart():GetOwner() - local part_ent = self:GetOwner() - for _,ent in pairs(ents.GetAll()) do + local owner_ent = part:GetRootPart():GetOwner() + local part_pos = part:GetWorldPosition() + + for _,ent in pairs(ents.FindInSphere(part_pos, 5000)) do if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() if dist < nearest_dist then @@ -204,42 +205,24 @@ function PART:CalcAngles(ang, wpos) end end end - local ang = (nearest_ent:GetPos() - part_ent:GetPos()):Angle() + + return nearest_ent + end + + if pac.StringFind(self.AimPartName, "NEAREST_LIFE_YAW", true, true) then + local nearest_ent = get_nearest_ent(self) + local ang = (nearest_ent:GetPos() - self:GetWorldPosition()):Angle() return Angle(0,ang.y,0) + self.Angles end if pac.StringFind(self.AimPartName, "NEAREST_LIFE_POS", true, true) then - local nearest_ent = self:GetRootPart():GetOwner() - local nearest_dist = math.huge - local owner_ent = self:GetRootPart():GetOwner() - local part_ent = self:GetOwner() - for _,ent in pairs(ents.GetAll()) do - if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then - local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() - if dist < nearest_dist then - nearest_ent = ent - nearest_dist = dist - end - end - end - return self.Angles + (nearest_ent:GetPos() - part_ent:GetPos()):Angle() + local nearest_ent = get_nearest_ent(self) + return self.Angles + (nearest_ent:GetPos() - self:GetWorldPosition()):Angle() end if pac.StringFind(self.AimPartName, "NEAREST_LIFE", true, true) then - local nearest_ent = self:GetRootPart():GetOwner() - local nearest_dist = math.huge - local owner_ent = self:GetRootPart():GetOwner() - local part_ent = self:GetOwner() - for _,ent in pairs(ents.GetAll()) do - if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then - local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() - if dist < nearest_dist then - nearest_ent = ent - nearest_dist = dist - end - end - end - return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - part_ent:GetPos()):Angle() + local nearest_ent = get_nearest_ent(self) + return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - self:GetWorldPosition()):Angle() end if self.AimPart:IsValid() and self.AimPart.GetWorldPosition then From b7342ec6133973e9aaa56df2b5e22554a3371b9e Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 11 Nov 2023 18:11:27 -0500 Subject: [PATCH 099/300] less fonts, event hack fix attempts make less fonts (some were too close to make any difference) try to fix the event hack (related to moving / switching hiding events) to not be as heavy on big outfits (instead of refreshtree, tree node drop) eventlist fontsize cvar removed, replaced so that both eventwheel and eventlist use font name strings (patterns will try to match the number, or get 12 if doesn't end with number) --- lua/pac3/core/client/parts/event.lua | 31 ++++++++++++++++++---- lua/pac3/editor/client/fonts.lua | 33 ++++++++++++++++++++--- lua/pac3/editor/client/panels/tree.lua | 22 +++++++--------- lua/pac3/editor/client/settings.lua | 36 +++++++++++++++++++++----- 4 files changed, 94 insertions(+), 28 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 9e6961cfb..b0a226f3d 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -3028,6 +3028,21 @@ function PART:SetAffectChildrenOnly(b) end +function PART:OnRemove() + if not self.AffectChildrenOnly then + local parent = self:GetParent() + if parent:IsValid() then + parent.active_events[self] = nil + parent.active_events_ref_count = parent.active_events_ref_count - 1 + parent:CalcShowHide() + end + end + if IsValid(self.DestinationPart) then + self.DestinationPart.active_events[self] = nil + self.DestinationPart.active_events_ref_count = self.DestinationPart.active_events_ref_count - 1 + self.DestinationPart:CalcShowHide() + end +end function PART:TriggerEvent(b) self.event_triggered = b -- event_triggered is just used for the editor @@ -3051,6 +3066,10 @@ function PART:TriggerEvent(b) (self.DestinationPart):SetEventTrigger(self, b) self.previousdestinationpart = (self.DestinationPart) + elseif IsValid(self.previousdestinationpart) then + if self.DestinationPart ~= self.previousdestinationpart then --once we change the destination part we need to reset the old one + self.previousdestinationpart:SetEventTrigger(self, false) + end end end @@ -3475,7 +3494,7 @@ local eventwheel_font = CreateConVar("pac_eventwheel_font", "DermaDefault", FCVA local eventwheel_clickable = CreateConVar("pac_eventwheel_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel.\n-1 : not clickable, but activate on menu close\n0 : clickable, and activate on menu close\n1 : clickable, but doesn't activate on menu close") local eventlist_clickable = CreateConVar("pac_eventlist_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel list alternative.\n-1 : not clickable, but activate a hovered event on menu close\n0 : clickable, and activate a hovered event on menu close\n1 : clickable, but doesn't do anything on menu close") -local event_list_font_size = CreateConVar("pac_eventlist_fontsize", "12", FCVAR_ARCHIVE, "How big the font should be for the eventwheel's rectangle list counterpart.\nMight not work if the corresponding pac_font is missing") +local event_list_font = CreateConVar("pac_eventlist_font", "DermaDefault", FCVAR_ARCHIVE, "The font for the eventwheel's rectangle list counterpart. It will also scale the rectangles' height.\nMight not work if the font is missing") -- Custom event selector wheel do @@ -3896,7 +3915,8 @@ do clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1 close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0 - local height = 2*event_list_font_size:GetInt() + 8 + local base_fontsize = tonumber(string.match(event_list_font:GetString(),"%d*$")) or 12 + local height = 2*base_fontsize + 8 panels = panels or {} if not table.IsEmpty(panels) then for i, v in pairs(panels) do @@ -3954,7 +3974,8 @@ do gui.EnableScreenClicker(true) pac.AddHook("HUDPaint","custom_event_selector_list",function() - local height = 2*event_list_font_size:GetInt() + 8 + local base_fontsize = tonumber(string.match(event_list_font:GetString(),"%d*$")) or 12 + local height = 2*base_fontsize + 8 -- Right clicking cancels if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionList(true) return end @@ -4048,7 +4069,7 @@ do if lightness_value > 0.5 and eventlist_style:GetInt() ~= 0 then text_color = Color(0,0,0) end - draw.SimpleText(v.name,"pac_font_" .. event_list_font_size:GetString(),x + 4,y + 4, text_color, TEXT_ALIGN_LEFT) + draw.SimpleText(v.name,event_list_font:GetString(),x + 4,y + 4, text_color, TEXT_ALIGN_LEFT) y = y + height end @@ -4109,4 +4130,4 @@ net.Receive("pac_update_healthbars", function() end end -end) +end) \ No newline at end of file diff --git a/lua/pac3/editor/client/fonts.lua b/lua/pac3/editor/client/fonts.lua index 59b75c50f..e94c0302a 100644 --- a/lua/pac3/editor/client/fonts.lua +++ b/lua/pac3/editor/client/fonts.lua @@ -2,7 +2,8 @@ local L = pace.LanguageString pace.Fonts = {} -for i = 1, 34 do + +for i = 1, 7 do surface.CreateFont("pac_font_"..i, { font = "Arial", @@ -14,15 +15,39 @@ for i = 1, 34 do table.insert(pace.Fonts, "pac_font_"..i) end -for i = 1, 34 do - surface.CreateFont("pac_font_bold"..i, +for i = 8, 32, 4 do + surface.CreateFont("pac_font_"..i, + { + font = "Arial", + size = 11 + i, + weight = 50, + antialias = true, + }) + + table.insert(pace.Fonts, "pac_font_"..i) +end + +for i = 1, 7 do + surface.CreateFont("pac_font_bold_"..i, { font = "Arial", size = 11 + i, weight = 800, antialias = true, }) - table.insert(pace.Fonts, "pac_font_bold"..i) + table.insert(pace.Fonts, "pac_font_bold_"..i) +end + +for i = 8, 32, 4 do + surface.CreateFont("pac_font_bold_"..i, + { + font = "Arial", + size = 11 + i, + weight = 50, + antialias = true, + }) + + table.insert(pace.Fonts, "pac_font_bold_"..i) end table.insert(pace.Fonts, "DermaDefault") diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index 9be74632f..60d91b9ed 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -308,6 +308,7 @@ local function install_drag(node) if self.part and self.part:IsValid() and self.part:GetParent() ~= child.part then pace.RecordUndoHistory() self.part:SetParent(child.part) + pace.RefreshEvents() pace.RecordUndoHistory() end elseif self.part and self.part:IsValid() then @@ -316,6 +317,7 @@ local function install_drag(node) local group = pac.CreatePart("group", self.part:GetPlayerOwner()) group:SetEditorExpand(true) self.part:SetParent(group) + pace.RefreshEvents() pace.RecordUndoHistory() pace.TrySelectPart() @@ -343,6 +345,7 @@ local function install_drag(node) if self.part and self.part:IsValid() and child.part:GetParent() ~= self.part then pace.RecordUndoHistory() child.part:SetParent(self.part) + pace.RefreshEvents() pace.RecordUndoHistory() end end @@ -645,20 +648,13 @@ pac.AddHook("pace_OnVariableChanged", "pace_create_tree_nodes", function(part, k end end) -local function refresh_events_gated() - pace.final_scheduled_event_refresh = pace.final_scheduled_event_refresh or CurTime() + 0.08 - pace.event_refresh_spam_time = CurTime() - hook.Add("Tick", "pace_refresh_events", function() - if CurTime() < pace.event_refresh_spam_time + 0.08 then return end - if CurTime() > pace.final_scheduled_event_refresh then - pace.RefreshEvents() - pace.final_scheduled_event_refresh = nil - hook.Remove("Tick", "pace_refresh_events") - end - end) -end +pace.allowed_event_refresh = 0 + function pace.RefreshEvents() + --spam preventer, (load parts' initializes gets called) + if pace.allowed_event_refresh > CurTime() then return else pace.allowed_event_refresh = CurTime() + 0.1 end + local events = {} for _, part in pairs(pac.GetLocalParts()) do if part.ClassName == "event" then @@ -677,6 +673,7 @@ function pace.RefreshEvents() end child:CallRecursive("CalcShowHide", false) end + end function pace.RefreshTree(reset) @@ -689,7 +686,6 @@ function pace.RefreshTree(reset) pace.TrySelectPart() end - refresh_events_gated() end) end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 796160910..6071b9ba8 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -2373,7 +2373,7 @@ function pace.ConfigureEventWheelMenu() local circle_style_listmenu = vgui.Create("DComboBox",first_panel) circle_style_listmenu:SetText("Choose eventwheel style") - circle_style_listmenu:SetSize(200,20) + circle_style_listmenu:SetSize(150,20) circle_style_listmenu:AddChoice("legacy") circle_style_listmenu:AddChoice("concentric") circle_style_listmenu:AddChoice("alternative") @@ -2389,8 +2389,8 @@ function pace.ConfigureEventWheelMenu() local circle_clickmode = vgui.Create("DComboBox",first_panel) circle_clickmode:SetText("Choose eventwheel clickmode") - circle_clickmode:SetSize(200,20) - circle_clickmode:SetPos(200,0) + circle_clickmode:SetSize(160,20) + circle_clickmode:SetPos(150,0) circle_clickmode:AddChoice("clickable and activates on close") circle_clickmode:AddChoice("not clickable, but activate on close") circle_clickmode:AddChoice("clickable, but do not activate on close") @@ -2407,7 +2407,7 @@ function pace.ConfigureEventWheelMenu() local rectangle_style_listmenu = vgui.Create("DComboBox",first_panel) rectangle_style_listmenu:SetText("Choose eventlist style") - rectangle_style_listmenu:SetSize(200,20) + rectangle_style_listmenu:SetSize(150,20) rectangle_style_listmenu:SetPos(0,20) rectangle_style_listmenu:AddChoice("legacy-like") rectangle_style_listmenu:AddChoice("concentric") @@ -2425,8 +2425,8 @@ function pace.ConfigureEventWheelMenu() local rectangle_clickmode = vgui.Create("DComboBox",first_panel) rectangle_clickmode:SetText("Choose eventlist clickmode") - rectangle_clickmode:SetSize(200,20) - rectangle_clickmode:SetPos(200,20) + rectangle_clickmode:SetSize(160,20) + rectangle_clickmode:SetPos(150,20) rectangle_clickmode:AddChoice("clickable and activates on close") rectangle_clickmode:AddChoice("not clickable, but activate on close") rectangle_clickmode:AddChoice("clickable, but do not activate on close") @@ -2440,6 +2440,30 @@ function pace.ConfigureEventWheelMenu() end end + local rectangle_fontsize = vgui.Create("DComboBox",first_panel) + rectangle_fontsize:SetText("Eventlist font / height") + rectangle_fontsize:SetSize(160,20) + rectangle_fontsize:SetPos(310,20) + for _,font in ipairs(pace.Fonts) do + rectangle_fontsize:AddChoice(font) + end + + function rectangle_fontsize:OnSelect( index, value ) + GetConVar("pac_eventlist_font"):SetString(value) + end + + local circle_fontsize = vgui.Create("DComboBox",first_panel) + circle_fontsize:SetText("Eventwheel font size") + circle_fontsize:SetSize(160,20) + circle_fontsize:SetPos(310,0) + for _,font in ipairs(pace.Fonts) do + circle_fontsize:AddChoice(font) + end + + function circle_fontsize:OnSelect( index, value ) + GetConVar("pac_eventwheel_font"):SetString(value) + end + local events = {} for i,v in pairs(pac.GetLocalParts()) do if v.ClassName == "event" then From 49e586c5602b67792ab147ada83e67091db11657 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 12 Nov 2023 14:25:03 -0500 Subject: [PATCH 100/300] combat categories and damagezone fix legacy part categories will include combat parts another fix attempt for the damagezone load trigger issue --- lua/pac3/core/client/parts/damage_zone.lua | 6 +++--- lua/pac3/core/client/parts/force.lua | 2 +- lua/pac3/core/client/parts/health_modifier.lua | 2 +- lua/pac3/core/client/parts/hitscan.lua | 2 +- lua/pac3/core/client/parts/lock.lua | 2 +- lua/pac3/core/client/parts/projectile.lua | 2 +- lua/pac3/editor/client/icons.lua | 1 + lua/pac3/editor/client/settings.lua | 16 ++++++++++------ 8 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index d7888c32f..a44bf3efc 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -3,7 +3,7 @@ local BUILDER, PART = pac.PartTemplate("base_movable") --ultrakill parryables: club, slash, buckshot PART.ClassName = "damage_zone" -PART.Group = "advanced" +PART.Group = "combat" PART.Icon = "icon16/package.png" local renderhooks = { @@ -534,6 +534,7 @@ function PART:SendNetMessage() end function PART:OnShow() + if self.validTime > SysTime() then return end if self.Preview then self:PreviewHitbox() @@ -551,8 +552,6 @@ function PART:OnShow() self:SendNetMessage() end) - elseif (self.validTime > SysTime()) then - return else self:SendNetMessage() end @@ -999,6 +998,7 @@ function PART:Initialize() 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() + 2 + end function PART:SetRadius(val) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 52d31f0d7..d7fe3c1ef 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -1,7 +1,7 @@ local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "force" -PART.Group = "advanced" +PART.Group = "combat" PART.Icon = "icon16/database_go.png" PART.ManualDraw = true diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index dae8d0b8c..dc308f067 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -2,7 +2,7 @@ local BUILDER, PART = pac.PartTemplate("base") PART.ClassName = "health_modifier" -PART.Group = "advanced" +PART.Group = "combat" PART.Icon = "icon16/heart.png" BUILDER:StartStorableVars() diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index 7b211ec94..368032d04 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -6,7 +6,7 @@ language.Add("pac_hitscan", "Hitscan") local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "hitscan" -PART.Group = "advanced" +PART.Group = "combat" PART.Icon = "icon16/user_gray.png" BUILDER:StartStorableVars() diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 2f150d827..bed0cd236 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -19,7 +19,7 @@ local physics_point_ent_classes = { local BUILDER, PART = pac.PartTemplate("base_movable") PART.ClassName = "lock" -PART.Group = "advanced" +PART.Group = "combat" PART.Icon = "icon16/lock.png" diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 306e01553..695d11db7 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -15,7 +15,7 @@ language.Add("pac_projectile", "Projectile") local BUILDER, PART = pac.PartTemplate("base_movable") PART.ClassName = "projectile" -PART.Group = "advanced" +PART.Group = {"advanced", "combat"} PART.Icon = "icon16/bomb.png" BUILDER:StartStorableVars() diff --git a/lua/pac3/editor/client/icons.lua b/lua/pac3/editor/client/icons.lua index 85f5fd21b..a564b121c 100644 --- a/lua/pac3/editor/client/icons.lua +++ b/lua/pac3/editor/client/icons.lua @@ -31,6 +31,7 @@ pace.MiscIcons = { pace.GroupsIcons = { effects = 'icon16/wand.png', model = 'icon16/shape_square.png', + combat = 'icon16/joystick.png', entity = 'icon16/brick.png', modifiers = 'icon16/disconnect.png', advanced = 'icon16/page_white_gear.png', diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 6071b9ba8..03ae7a7fb 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -196,21 +196,25 @@ pace.partmenu_categories_default = ["webaudio"]= "webaudio", ["ogg"] = "ogg", }, - ["advanced"]= + ["combat"]= { - ["icon"] = pace.GroupsIcons.advanced, + ["icon"] = pace.GroupsIcons.combat, ["lock"]= "lock", ["force"]= "force", + ["projectile"]= "projectile", + ["damage_zone"] = "damage_zone", + ["hitscan"] = "hitscan", + ["health_modifier"] = "health_modifier", + }, + ["advanced"]= + { + ["icon"] = pace.GroupsIcons.advanced, ["custom_animation"]= "custom_animation", ["material_refract"]= "material_refract", ["projectile"]= "projectile", ["link"] = "link", - ["damage_zone"] = "damage_zone", - ["interpolated_multibone"] = "interpolated_multibone", ["material_2d"] = "material_2d", ["material_eye refract"] = "material_eye refract", - ["hitscan"] = "hitscan", - ["health_modifier"] = "health_modifier", ["command"] ="command", }, ["entity"]= From 4fb95a86cee5e4ff33a0c2e91ee61103dcad4276 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 16 Nov 2023 18:11:57 -0500 Subject: [PATCH 101/300] nearest_life aim part fix use supplied world position of CalcAngles --- lua/pac3/core/client/base_movable.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lua/pac3/core/client/base_movable.lua b/lua/pac3/core/client/base_movable.lua index 29a28b772..328b9fe1b 100644 --- a/lua/pac3/core/client/base_movable.lua +++ b/lua/pac3/core/client/base_movable.lua @@ -194,11 +194,10 @@ function PART:CalcAngles(ang, wpos) local nearest_ent = part:GetRootPart():GetOwner() local nearest_dist = math.huge local owner_ent = part:GetRootPart():GetOwner() - local part_pos = part:GetWorldPosition() - for _,ent in pairs(ents.FindInSphere(part_pos, 5000)) do + for _,ent in pairs(ents.FindInSphere(wpos, 5000)) do if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then - local dist = (owner_ent:GetPos() - ent:GetPos()):LengthSqr() + local dist = (wpos - ent:GetPos()):LengthSqr() if dist < nearest_dist then nearest_ent = ent nearest_dist = dist @@ -211,18 +210,18 @@ function PART:CalcAngles(ang, wpos) if pac.StringFind(self.AimPartName, "NEAREST_LIFE_YAW", true, true) then local nearest_ent = get_nearest_ent(self) - local ang = (nearest_ent:GetPos() - self:GetWorldPosition()):Angle() + local ang = (nearest_ent:GetPos() - wpos):Angle() return Angle(0,ang.y,0) + self.Angles end if pac.StringFind(self.AimPartName, "NEAREST_LIFE_POS", true, true) then local nearest_ent = get_nearest_ent(self) - return self.Angles + (nearest_ent:GetPos() - self:GetWorldPosition()):Angle() + return self.Angles + (nearest_ent:GetPos() - wpos):Angle() end if pac.StringFind(self.AimPartName, "NEAREST_LIFE", true, true) then local nearest_ent = get_nearest_ent(self) - return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - self:GetWorldPosition()):Angle() + return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - wpos):Angle() end if self.AimPart:IsValid() and self.AimPart.GetWorldPosition then From 415e36f9c61eaa8824b93248885752c53c9912ad Mon Sep 17 00:00:00 2001 From: Yagira Date: Sat, 18 Nov 2023 11:40:33 +0100 Subject: [PATCH 102/300] use part selection for anim frame event --- lua/pac3/core/client/parts/event.lua | 16 +---- .../editor/client/panels/extra_properties.lua | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 4673a301a..7e6f8312b 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1452,20 +1452,7 @@ do return part:GetName() end, args = { - {"animation", "string", { - enums = function(part) - local output = {} - local parts = pac.GetLocalParts() - - for i, part in pairs(parts) do - if part.ClassName == "custom_animation" then - output[i] = part - end - end - - return output - end - }}, + {"animation", "string", {editor_panel = "custom_animation_frame"}}, {"frame_start", "number", { editor_onchange = function(self, num) local anim = pace.current_part:GetProperty("animation") @@ -1504,6 +1491,7 @@ do if v == ent then local part = pac.GetPartFromUniqueID(pac.Hash(ent), animation) if not IsValid(part) then return end + if part.ClassName ~= "custom_animation" then return end local frame, delta = animations.GetEntityAnimationFrame(ent, part:GetAnimID()) if not frame or not delta then return end -- different animation part is playing return frame >= frame_start and frame <= frame_end diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index fb2283d5a..423c7592f 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -219,6 +219,72 @@ do -- part pace.RegisterPanel(PANEL) end +do -- custom animation frame event + local PANEL = {} + + PANEL.ClassName = "properties_custom_animation_frame" + PANEL.Base = "pace_properties_part" + + function PANEL:MoreOptionsLeftClick() + pace.CreateSearchList( + self, + self.CurrentKey, + L"custom animations", + + function(list) + list:AddColumn(L"name") + list:AddColumn(L"id") + end, + + function() + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + + if part.ClassName == "custom_animation" then + local name = part.Name ~= "" and part.Name or "no name" + output[i] = name + end + end + + return output + end, + + function() return pace.current_part:GetProperty("animation") end, + + function(list, key, val) + return list:AddLine(val, key) + end, + + function(key, val) return val end, + + function(key, val) return key end + ) + end + + function PANEL:MoreOptionsRightClick(key) + local menu = DermaMenu() + + menu:MakePopup() + + for _, part in pairs(pac.GetLocalParts()) do + if not part:HasParent() and part:GetShowInEditor() then + populate_part_menu(menu, part, function(part) + if not self:IsValid() then return end + if part.ClassName ~= "custom_animation" then return end + self:SetValue(part:GetUniqueID()) + self.OnValueChanged(part:GetUniqueID()) + end) + end + end + + pace.FixMenu(menu) + end + + pace.RegisterPanel(PANEL) +end + do -- owner local PANEL = {} From 93cb3f14174e24133c0c417df7ab15f261a2acdd Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 19 Nov 2023 14:48:30 -0500 Subject: [PATCH 103/300] force damping adjustment for players --- lua/pac3/extra/shared/net_combat.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 6623c7414..17123bcf5 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1144,8 +1144,8 @@ if SERVER then if (ent:IsPlayer() and tbl.Players) or (ent == ply and tbl.AffectSelf) then if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then - phys_ent:SetVelocity(oldvel * (-final_damping) + addvel) - ent:SetVelocity(oldvel * (-final_damping) + addvel) + phys_ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) + ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) end elseif (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) and tbl.PhysicsProps then From 7a1d6f6d9b20e72d99762d7314a3f9a697ee40a1 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 22 Nov 2023 19:39:09 -0500 Subject: [PATCH 104/300] report attacker in hitscans for killfeed --- lua/pac3/extra/shared/net_combat.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 17123bcf5..7d002465c 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -2000,6 +2000,7 @@ if SERVER then ply.pac_bullet_emitters = ply.pac_bullet_emitters or {} ply.pac_bullet_emitters[part_uid] = ply.pac_bullet_emitters[part_uid] or ents.Create("pac_bullet_emitter") + bulletinfo.Attacker = ply bulletinfo.Callback = function(atk, trc, dmg) dmg:SetDamageType(bulletinfo.dmgtype) if trc.Hit and IsValid(trc.Entity) then From a412034158e73ebc4275dffbab503bfb183c464f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 29 Nov 2023 23:16:27 -0500 Subject: [PATCH 105/300] eventwheel adjustments, more info on some part enums can hide the customize button from eventwheels right click only closes the eventwheels if eventwheel customization menu isn't open auto-close eventwheels when closing said menu some combat-related events that refer to parts will have their name and their parent's name in the [...] list to give context when selecting parts --- lua/pac3/core/client/parts/event.lua | 61 ++++++++++++++++++++++------ lua/pac3/editor/client/settings.lua | 9 ++++ 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index b0a226f3d..1688cb94d 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1454,7 +1454,28 @@ PART.OldEvents = { tutorial_explanation = "the command event reads your pac_event states.\nthe pac_event command can turn on (1), off (0) or toggle (2) a state that has a name.\nfor example, \"pac_event myhat 2\" can be used with a myhat command event to put the hat on or off\n\nwith this event, you read the states that contain this find name\n(equal being an exact match; find and find simple allowing to detect from different states having a part of the name)\n\nthe final result is to activate if:\n\tA) there's one active, or \n\tB) there's one recently turned off not too long ago", arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}}, userdata = { - {default = "change_me", editor_friendly = "CommandName"}, + {default = "change_me", editor_friendly = "CommandName", enums = function() + local output = {} + local parts = pac.GetLocalParts() + + for i, part in pairs(parts) do + if part.ClassName == "command" then + local str = part.String + if string.find(str,"pac_event") then + for s in string.gmatch(str, "pac_event%s[%w_]+") do + local name_substring = string.gsub(s,"pac_event%s","") + output[name_substring] = name_substring + end + end + + elseif part.ClassName == "event" and part.Event == "command" then + local cmd, time, hide = part:GetParsedArgumentsForObject(part.Events.command) + output[cmd] = cmd + end + end + + return output + end}, {default = 0, editor_friendly = "EventDuration"}, {default = false, group = "event wheel", editor_friendly = "HideInEventWheel"} }, @@ -2062,7 +2083,7 @@ PART.OldEvents = { for i, part in pairs(parts) do if part.ClassName == "damage_zone" then - output[i] = part + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part end end @@ -2108,7 +2129,7 @@ PART.OldEvents = { for i, part in pairs(parts) do if part.ClassName == "damage_zone" then - output[i] = part + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part end end @@ -2161,7 +2182,7 @@ PART.OldEvents = { for i, part in pairs(parts) do if part.ClassName == "lock" then - output[i] = part + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part end end @@ -2256,7 +2277,7 @@ PART.OldEvents = { for i, part in pairs(parts) do if part.ClassName == "health_modifier" then - output[i] = part + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part end end @@ -3489,6 +3510,7 @@ local eventwheel_visibility_rule = CreateConVar("pac_eventwheel_visibility_rule" local eventwheel_style = CreateConVar("pac_eventwheel_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel.\n0 is the default legacy style with one circle\n1 is the new style with colors, using one circle for the color and one circle for the activation indicator\n2 is an alternative style using a smaller indicator circle on the corner of the circle") local eventlist_style = CreateConVar("pac_eventlist_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel list alternative.\n0 is like the default eventwheel legacy style with one indicator for the activation\n1 is the new style with colors, using one rectangle for the color and one rectangle for the activation indicator\n2 is an alternative style using a smaller indicator on the corner") +local show_customize_button = CreateConVar("pac_eventwheel_show_customize_button", "1", FCVAR_ARCHIVE, "Whether to show the Customize button with the event wheel.") local eventwheel_font = CreateConVar("pac_eventwheel_font", "DermaDefault", FCVAR_ARCHIVE, "pac3 eventwheel font. try pac_font_ such as pac_font_20 or pac_font_bold30. the pac fonts go up to 34") local eventwheel_clickable = CreateConVar("pac_eventwheel_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel.\n-1 : not clickable, but activate on menu close\n0 : clickable, and activate on menu close\n1 : clickable, but doesn't activate on menu close") @@ -3496,6 +3518,7 @@ local eventlist_clickable = CreateConVar("pac_eventlist_clickmode", "0", FCVAR_A local event_list_font = CreateConVar("pac_eventlist_font", "DermaDefault", FCVAR_ARCHIVE, "The font for the eventwheel's rectangle list counterpart. It will also scale the rectangles' height.\nMight not work if the font is missing") + -- Custom event selector wheel do @@ -3671,7 +3694,6 @@ do open_btn:SetSize(80,30) open_btn:SetText("Customize") open_btn:SetPos(ScrW() - 80,0) - pace.command_event_menu_opened = nil function open_btn:DoClick() @@ -3683,7 +3705,11 @@ do end - open_btn:Show() + if show_customize_button:GetBool() then + open_btn:Show() + else + open_btn:Hide() + end pace.command_colors = pace.command_colors or {} clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1 close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0 @@ -3824,7 +3850,7 @@ do pac.AddHook("HUDPaint","custom_event_selector",function() -- Right clicking cancels - if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionWheel(true) return end + if input.IsButtonDown(MOUSE_RIGHT) and not IsValid(pace.command_event_menu_opened) then pac.closeEventSelectionWheel(true) return end if input.IsButtonDown(MOUSE_LEFT) and not pace.command_event_menu_opened and not open_btn:IsHovered() and clickable then if not clicking and selected then @@ -3867,9 +3893,12 @@ do render.PopFilterMin() DisableClipping(false) end) + pace.event_wheel_opened = true end function pac.closeEventSelectionWheel(cancel) + + if IsValid(pace.command_event_menu_opened) then return end open_btn:Hide() gui.EnableScreenClicker(false) pac.RemoveHook("HUDPaint","custom_event_selector") @@ -3890,6 +3919,7 @@ do end end selected = nil + pace.event_wheel_opened = false end local panels = {} @@ -3898,7 +3928,6 @@ do open_btn:SetSize(80,30) open_btn:SetText("Customize") open_btn:SetPos(ScrW() - 80,0) - pace.command_event_menu_opened = nil function open_btn:DoClick() @@ -3910,7 +3939,11 @@ do end - open_btn:Show() + if show_customize_button:GetBool() then + open_btn:Show() + else + open_btn:Hide() + end pace.command_colors = pace.command_colors or {} clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1 close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0 @@ -3977,7 +4010,7 @@ do local base_fontsize = tonumber(string.match(event_list_font:GetString(),"%d*$")) or 12 local height = 2*base_fontsize + 8 -- Right clicking cancels - if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionList(true) return end + if input.IsButtonDown(MOUSE_RIGHT) and not IsValid(pace.command_event_menu_opened) then pac.closeEventSelectionList(true) return end DisableClipping(true) render.PushFilterMag(TEXFILTER.ANISOTROPIC) @@ -4081,20 +4114,24 @@ do DisableClipping(false) end) + + pace.event_wheel_list_opened = true end function pac.closeEventSelectionList(cancel) + if IsValid(pace.command_event_menu_opened) then return end open_btn:Hide() gui.EnableScreenClicker(false) pac.RemoveHook("HUDPaint","custom_event_selector_list") - if IsValid(selected) and close_click2 then + if IsValid(selected) and close_click2 and cancel ~= true then if selected:IsHovered() then selected:DoCommand() end end for i,v in pairs(panels) do v:Remove() end selected = nil + pace.event_wheel_list_opened = false end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 03ae7a7fb..f5f077793 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -2468,6 +2468,13 @@ function pace.ConfigureEventWheelMenu() GetConVar("pac_eventwheel_font"):SetString(value) end + local customizer_button_box = vgui.Create("DCheckBox",first_panel) + customizer_button_box:SetTooltip("Show the Customize button when eventwheels are active") + customizer_button_box:SetSize(20,20) + customizer_button_box:SetPos(470,0) + customizer_button_box:SetConVar("pac_eventwheel_show_customize_button") + + local events = {} for i,v in pairs(pac.GetLocalParts()) do if v.ClassName == "event" then @@ -2594,6 +2601,8 @@ function pace.ConfigureEventWheelMenu() gui.EnableScreenClicker(false) pace.command_event_menu_opened = nil encode_table_to_file("eventwheel_colors", pace.command_colors) + if pace.event_wheel_list_opened then pac.closeEventSelectionList(true) end + if pace.event_wheel_opened then pac.closeEventSelectionWheel(true) end end master_panel:RequestFocus() From 65954995a9f962563eaa233c1054dcd53f7661d0 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 3 Dec 2023 17:42:50 -0500 Subject: [PATCH 106/300] proxy: new "sequence" functions, use wires, warn with errors string-based sequences: hexadecimal maps from 0 = 0, to f = 1 letters maps from a = 0, to z = 1 vararg-based sequence: numberlist the sequences go through the string or list relative to pac.RealTime and the frequency wires are used for part_distance, event_alternative and pac_healthbar_uidvalue since they use part references for every part_distance, event_alternative, or pac_healthbar_uidvalue in the expression, it will collect invalid uids/names and invalid classes, and list the wrong arguments into a SetError node circle --- lua/pac3/core/client/parts/proxy.lua | 101 ++++++++++++++++++++++++++- lua/pac3/editor/client/wires.lua | 66 +++++++++++++++++ 2 files changed, 165 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 885f83a08..946b9c1d5 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -356,7 +356,8 @@ PART.Inputs.timeex = function(s) end PART.Inputs.part_distance = function(self, uid1, uid2) - if not uid1 or not uid2 then return 0 end + + if not uid1 then return 0 end local owner = self:GetPlayerOwner() local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) @@ -364,9 +365,22 @@ PART.Inputs.part_distance = function(self, uid1, uid2) local PartB = pac.GetPartFromUniqueID(pac.Hash(owner), uid2) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid2) if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(owner), uid2, self) end + if not PartB:IsValid() then + if not uid2 then --no second argument, take parent + PartB = self:GetParent() + else --second argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in part_distance" + end + end + + if not PartA:IsValid() and uid1 then --first argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_distance" + end if not PartA:IsValid() or not PartB:IsValid() then return 0 end if not PartA.Position or not PartB.Position then return 0 end + self.valid_parts_in_expression[PartA] = PartA + self.valid_parts_in_expression[PartB] = PartB return (PartB:GetWorldPosition() - PartA:GetWorldPosition()):Length() end @@ -377,10 +391,21 @@ PART.Inputs.event_alternative = function(self, uid1, num1, num2) local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end + if not IsValid(PartA) then + if uid1 then --first argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid1] = "invalid argument: " .. uid1 .. " in event_alternative" + end + return 0 + end + if PartA.ClassName == "event" then + self.valid_parts_in_expression[PartA] = PartA if PartA.event_triggered then return num1 or 0 else return num2 or 0 end - else return -1 end + else + self.invalid_parts_in_expression[uid1] = "found part, but invalid class : " .. uid1 .. " : " .. tostring(PartA) .. " in event_alternative" + return -1 + end return 0 end @@ -404,6 +429,56 @@ PART.Inputs.number_operator_alternative = function(self, comp1, op, comp2, num1, if b then return num1 or 0 else return num2 or 0 end end +PART.Inputs.hexadecimal_level_sequence = function(self, freq, str) + if not str then return 0 end + local index = 1 + math.ceil(#str * freq * pac.RealTime) % #str + return (tonumber(string.sub(str,index,index),16) or 0) / 15 +end + +local letter_numbers = { + a = 0, + b = 1, + c = 2, + d = 3, + e = 4, + f = 5, + g = 6, + h = 7, + i = 8, + j = 9, + k = 10, + l = 11, + m = 12, + n = 13, + o = 14, + p = 15, + q = 16, + r = 17, + s = 18, + t = 19, + u = 20, + v = 21, + w = 22, + x = 23, + y = 24, + z = 25 +} + +PART.Inputs.letters_level_sequence = function(self, freq, str) + if not str then return 0 end + local index = 1 + math.ceil(#str * freq * pac.RealTime) % #str + local lookup_result = letter_numbers[string.sub(str,index,index)] or 0 + return lookup_result / 25 +end + +PART.Inputs.numberlist_level_sequence = function(self, freq, ...) + local args = { ... } + if not args[1] then return 0 end + local index = 1 + math.ceil(#args * freq * pac.RealTime) % #args + + return args[index] or 0 +end + do local function get_pos(self) local part = self:GetPhysicalTarget() @@ -965,7 +1040,20 @@ end PART.Inputs.pac_healthbar_uidvalue = function(self, uid) local ent = self:GetPlayerOwner() + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + + if not IsValid(pac.GetPartFromUniqueID(pac.Hash(ent), uid)) then + self.invalid_parts_in_expression[uid] = "invalid uid : " .. uid .. " in pac_healthbar_uidvalue" + elseif part.ClassName ~= "health_modifier" then + self.invalid_parts_in_expression[uid] = "invalid class : " .. uid .. " in pac_healthbar_uidvalue" + end if ent.pac_healthbars and ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals[uid] then + + if part:IsValid() then + self.valid_parts_in_expression[part] = part + end + end return ent.pac_healthbars_uidtotals[uid] or 0 end return 0 @@ -1009,6 +1097,8 @@ local allowed = { function PART:SetExpression(str) self.Expression = str self.ExpressionFunc = nil + self.valid_parts_in_expression = {} + self.invalid_parts_in_expression = {} if str and str ~= "" then local lib = {} @@ -1279,6 +1369,13 @@ function PART:OnThink() end end + if table.Count(self.invalid_parts_in_expression) > 0 then + local error_msg = "" + for str, message in pairs(self.invalid_parts_in_expression) do + error_msg = error_msg .. " " .. message .. "\n" + end + self:SetError(error_msg) + end end BUILDER:Register() diff --git a/lua/pac3/editor/client/wires.lua b/lua/pac3/editor/client/wires.lua index 6cc711d3b..3f7dc9ded 100644 --- a/lua/pac3/editor/client/wires.lua +++ b/lua/pac3/editor/client/wires.lua @@ -209,4 +209,70 @@ hook.Add("PostRenderVGUI", "beams", function() end end + if part.ClassName == "proxy" and part.valid_parts_in_expression then + for _, part2 in pairs(part.valid_parts_in_expression) do + + local info = part.pace_tree_node + --if info.udata.part_key then + --if info.udata.part_key == "Parent" then continue end + + local from = part + local to = part2 + --print("part: ", part, "\nfrom: ", from, "\nto: ", to) + if not to:IsValid() then continue end + + + local from_pnl = from.pace_properties["Expression"] + local to_pnl = to.pace_tree_node or NULL + + if not from_pnl:IsValid() then continue end + if not to_pnl:IsValid() then continue end + + local params = {} + + params["$basetexture"] = to.Icon or "gui/colors.png" + params["$vertexcolor"] = 1 + params["$vertexalpha"] = 1 + params["$nocull"] = 1 + + local path = to_pnl:GetModel() + if path then + path = "spawnicons/" .. path:sub(1, -5) .. "_32" + params["$basetexture"] = path + end + + + local mat = CreateMaterial("pac_wire_icon_" .. params["$basetexture"], "UnlitGeneric", params) + + render.SetMaterial(mat) + + local fx,fy = from_pnl:LocalToScreen(from_pnl:GetWide(), from_pnl:GetTall() / 2) + + local tx,ty = to_pnl.Icon:LocalToScreen(0,to_pnl.Icon:GetTall() / 2) + + do + local x,y = pace.tree:LocalToScreen(0,0) + local w,h = pace.tree:LocalToScreen(pace.tree:GetSize()) + + tx = math.Clamp(tx, x, w) + ty = math.Clamp(ty, y, h) + end + + from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover or 0 + + if from_pnl:IsHovered() or (from.pace_tree_node and from.pace_tree_node:IsValid() and from.pace_tree_node.Label:IsHovered()) then + from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (5 - from_pnl.wire_smooth_hover) * FrameTime() * 20 + else + from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (0 - from_pnl.wire_smooth_hover) * FrameTime() * 20 + end + + from_pnl.wire_smooth_hover = math.Clamp(from_pnl.wire_smooth_hover, 0, 5) + + if from_pnl.wire_smooth_hover > 0.01 then + draw_hermite(0,0,ScrW(),ScrH(), from_pnl.wire_smooth_hover, fx,fy, tx,ty, Color(255,255,255), Color(255,255,255, 255), 1) + end + --end + end + end + end) From 75f090a903210bdbd32209198e14ac50f99210cd Mon Sep 17 00:00:00 2001 From: Redox Date: Sat, 9 Dec 2023 17:57:12 +0100 Subject: [PATCH 107/300] Remove double isvalid check --- lua/pac3/core/client/base_movable.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/pac3/core/client/base_movable.lua b/lua/pac3/core/client/base_movable.lua index 328b9fe1b..bf59a9624 100644 --- a/lua/pac3/core/client/base_movable.lua +++ b/lua/pac3/core/client/base_movable.lua @@ -64,7 +64,7 @@ do -- bones function PART:GetBoneMatrix() local parent = self:GetParent() - if parent:IsValid() or IsValid(parent) then + if IsValid(parent) then if parent.ClassName == "jiggle" or parent.ClassName == "interpolated_multibone" then local bone_matrix = Matrix() if parent.pos then @@ -189,7 +189,7 @@ function PART:CalcAngles(ang, wpos) return self.Angles + (pac.EyePos - wpos):Angle() end - + local function get_nearest_ent(part) local nearest_ent = part:GetRootPart():GetOwner() local nearest_dist = math.huge @@ -213,12 +213,12 @@ function PART:CalcAngles(ang, wpos) local ang = (nearest_ent:GetPos() - wpos):Angle() return Angle(0,ang.y,0) + self.Angles end - + if pac.StringFind(self.AimPartName, "NEAREST_LIFE_POS", true, true) then local nearest_ent = get_nearest_ent(self) return self.Angles + (nearest_ent:GetPos() - wpos):Angle() end - + if pac.StringFind(self.AimPartName, "NEAREST_LIFE", true, true) then local nearest_ent = get_nearest_ent(self) return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - wpos):Angle() From 24ace62efb5d58af3d4e3449ca5677709f5215c7 Mon Sep 17 00:00:00 2001 From: Redox Date: Sat, 9 Dec 2023 17:59:24 +0100 Subject: [PATCH 108/300] Remove trailing whitespaces --- lua/autorun/pac_version.lua | 19 +- lua/pac3/core/client/base_part.lua | 10 +- lua/pac3/core/client/part_pool.lua | 2 +- lua/pac3/core/client/parts/beam.lua | 2 +- lua/pac3/core/client/parts/camera.lua | 2 +- lua/pac3/core/client/parts/damage_zone.lua | 34 +-- lua/pac3/core/client/parts/event.lua | 220 +++++++-------- lua/pac3/core/client/parts/force.lua | 4 +- .../core/client/parts/health_modifier.lua | 2 +- lua/pac3/core/client/parts/hitscan.lua | 2 +- .../client/parts/interpolated_multibone.lua | 14 +- lua/pac3/core/client/parts/lock.lua | 60 ++--- lua/pac3/core/client/parts/movement.lua | 6 +- lua/pac3/core/client/parts/physics.lua | 10 +- lua/pac3/core/client/parts/projectile.lua | 14 +- lua/pac3/core/client/parts/proxy.lua | 8 +- lua/pac3/core/client/parts/sound.lua | 4 +- .../shared/entity_mutators/draw_shadow.lua | 8 +- lua/pac3/core/shared/movement.lua | 12 +- lua/pac3/editor/client/asset_browser.lua | 16 +- lua/pac3/editor/client/init.lua | 4 +- lua/pac3/editor/client/menu_bar.lua | 14 +- lua/pac3/editor/client/panels/editor.lua | 6 +- .../editor/client/panels/extra_properties.lua | 14 +- lua/pac3/editor/client/panels/properties.lua | 116 ++++---- lua/pac3/editor/client/panels/tree.lua | 22 +- lua/pac3/editor/client/parts.lua | 184 ++++++------- .../editor/client/popups_part_tutorials.lua | 54 ++-- lua/pac3/editor/client/saved_parts.lua | 10 +- lua/pac3/editor/client/settings.lua | 211 ++++++++------- lua/pac3/editor/client/shortcuts.lua | 84 +++--- lua/pac3/editor/client/tools.lua | 6 +- lua/pac3/editor/client/view.lua | 4 +- lua/pac3/editor/client/wear.lua | 8 +- lua/pac3/editor/client/wires.lua | 40 +-- lua/pac3/editor/server/bans.lua | 2 +- lua/pac3/editor/server/init.lua | 10 +- .../editor/server/pac_settings_manager.lua | 2 +- lua/pac3/extra/shared/net_combat.lua | 252 +++++++++--------- lua/pac3/extra/shared/projectiles.lua | 12 +- 40 files changed, 751 insertions(+), 753 deletions(-) diff --git a/lua/autorun/pac_version.lua b/lua/autorun/pac_version.lua index 2a281d396..bb10abe9f 100644 --- a/lua/autorun/pac_version.lua +++ b/lua/autorun/pac_version.lua @@ -50,9 +50,8 @@ end) function pac.OpenMOTD(mode) local pnl = vgui.Create("DFrame") pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH())) - + local html = vgui.Create("DHTML", pnl) - if mode == "combat_update" then pnl:SetTitle("Welcome to a new update!") @@ -108,7 +107,7 @@ pace.current_version_changelog_html = [[

eventwheel colors

eventwheel styles and grid eventwheel

pac_eventwheel_visibility_rule command for filtering eventwheel with many choices and keywords

- +

More miscellaneous features

nearest life aim part names : point movable parts toward the nearest NPC or player

prompt for autoload : optional menu to pick between autoload.txt, your autosave backup, or your latest loaded outfit

@@ -135,7 +134,7 @@ pace.current_version_changelog_html = [[

Changelog : August 2023

"spawn as props" bone scale retention

Fix/rework submitpart

- +

Changelog : July 2023

small fix for sequential in legacy sound

less awkward selection when deleting keyframes

@@ -147,7 +146,7 @@ pace.current_version_changelog_html = [[

Changelog : June 2023

sequential playback for legacy sound and websound parts

more proxy functions

- +

Changelog : June 2023

Fix new dropbox urls

@@ -188,15 +187,15 @@ pace.cedrics_combat_update_changelog_html = [[

New combat-related parts:


 	damage_zone: deals damage (a more direct and controllable alternative to projectiles)
-	
+
 	hitscan: shoots bullets
-	
+
 	lock: teleport/grab
-	
+
 	force: does physics forces
-	
+
 	health_modifier: changes your health, armor etc
-	
+
 	interpolated_multibone: morphs position / angles between different base_movables nodes, like a path
 	

The combat features work with the principle of consent. The lock part especially is severely restricted for grabbing players, for what should be obvious reasons. You can only damage or grab players who have opted in for the corresponding consent.

diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 8341a4880..7c840fef1 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -1170,7 +1170,7 @@ function PART:SetupEditorPopup(str, force_open, tbl) local default_state = str == nil or str == "" local info_string = str or self.ClassName .. "\nno special information available" - + if default_state and pace then local partsize_tbl = pace.GetPartSizeInformation(self) info_string = info_string .. "\n" .. partsize_tbl.info .. ", " .. partsize_tbl.all_share_percent .. "% of all parts" @@ -1184,7 +1184,7 @@ function PART:SetupEditorPopup(str, force_open, tbl) local part = self self.killpopup = false local pnl - + --local pace = pace or {} if tree_node then tree_node.Label:SetTooltip(self.ClassName) @@ -1200,10 +1200,10 @@ function PART:SetupEditorPopup(str, force_open, tbl) ) self.popuppnl_is_up = true end - + --if IsValid(self.popupinfopnl) then self.popupinfopnl:MakePopup() end pnl = self.popupinfopnl - + end if not IsValid(self.popupinfopnl) then self.popupinfopnl = nil self.popuppnl_is_up = false end end @@ -1216,7 +1216,7 @@ function PART:SetupEditorPopup(str, force_open, tbl) if pace then pace.legacy_floating_popup_reserved = pnl end - + return pnl end diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 37105325f..a601ff395 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -518,7 +518,7 @@ function pac.FindPartByPartialUniqueID(owner_id, crumb) closest_match = part length_of_closest_match = end_i - start_i + 1 end - + end end diff --git a/lua/pac3/core/client/parts/beam.lua b/lua/pac3/core/client/parts/beam.lua index 01ec42205..0840d341f 100644 --- a/lua/pac3/core/client/parts/beam.lua +++ b/lua/pac3/core/client/parts/beam.lua @@ -25,7 +25,7 @@ do local color = Color(255, 255, 255, 255) function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size, width_start_mul, width_end_mul, width_pow) - + if not veca or not vecb or not dira or not dirb then return end ax = veca.x; ay = veca.y; az = veca.z diff --git a/lua/pac3/core/client/parts/camera.lua b/lua/pac3/core/client/parts/camera.lua index fbd40609f..dcd2c2bfa 100644 --- a/lua/pac3/core/client/parts/camera.lua +++ b/lua/pac3/core/client/parts/camera.lua @@ -145,7 +145,7 @@ pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) temp.znear = fnearz temp.zfar = ffarz temp.drawviewer = false - + if not part:IsHidden() and not part.inactive and part.priority then temp.drawviewer = not part.DrawViewModel return temp diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index a44bf3efc..ae4c0999f 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -145,7 +145,7 @@ function HasBudget(owner, part) if not owner.pac_dmgzone_hitmarker_budget then owner.pac_dmgzone_hitmarker_budget = 50000 --50kB's worth of pac parts end - + if part then --calculate based on an additional part added --print("budget:" .. string.NiceSize(owner.pac_dmgzone_hitmarker_budget) .. ", cost: " .. string.NiceSize(CalculateHitMarkerPrice(part))) return owner.pac_dmgzone_hitmarker_budget - CalculateHitMarkerPrice(part) > 0 @@ -192,7 +192,7 @@ function PART:LaunchAuditAndEnforceSoftBan(amount, reason) print(str_admonishment) end end) - + end function PART:ClearBudgetAdmonishmentWarning() @@ -240,7 +240,7 @@ local part_setup_runtimes = 0 active template_uid --to identify from which part it's derived hitmarker_id --to identify what entity it's attached to - + } ]] --[[ @@ -324,7 +324,7 @@ local function UIDMatchInStackForExistingPart(owner, ent, part_uid, ent_id) end end end - + return nil end @@ -346,7 +346,7 @@ function PART:AddHitMarkerToStack(owner, ent, part_uid, ent_id) else owner.hitparts[free] = {active = true, specimen_part = returned_part, hitmarker_id = ent_id, template_uid = part_uid} end - + return returned_part end @@ -429,7 +429,7 @@ local function RecursedHitmarker(part) end end end - + end @@ -557,7 +557,7 @@ function PART:OnShow() end net.Receive("pac_hit_results", function() - + local hit = net.ReadBool() local kill = net.ReadBool() local highest_dmg = net.ReadFloat() @@ -587,7 +587,7 @@ function PART:OnShow() local function spawn(part, pos, ang, parent_ent, duration, owner) if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker --what if people employ a more roundabout method? CRACKDOWN! - + if not owner.hitparts then owner.hitparts = {} end if owner.stop_hit_markers_until then @@ -630,9 +630,9 @@ function PART:OnShow() if part:IsValid() then --self:AttachToEntity(part, ent, parent_ent, global_hitmarker_CSEnt_seed) local newpart local bool = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) - + newpart = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) or self:AddHitMarkerToStack(owner, ent, part.UniqueID, csent_id) - + self:AssignFloatingPartToEntity(newpart, owner, ent, parent_ent, part.UniqueID, csent_id) MsgC(bool and Color(0,255,0) or Color(0,200,255), bool and "existing" or "created", " : ", newpart, "\n") @@ -656,7 +656,7 @@ function PART:OnShow() end local creation_delta = SysTime() - start - + return creation_delta end @@ -669,7 +669,7 @@ function PART:OnShow() --try not to play both sounds at once if ValidSound(self.HitSoundPart) then --if can overlap, always play - if self.AllowOverlappingHitSounds then + if self.AllowOverlappingHitSounds then self.HitSoundPart:PlaySound() --if cannot overlap, only play if there's only one entity or if we didn't kill elseif (table.Count(ents_kill) == 1) or not (kill and ValidSound(self.KillSoundPart)) then @@ -745,7 +745,7 @@ end local previousRenderingHook function PART:PreviewHitbox() - + if previousRenderingHook ~= self.RenderingHook then for _,v in pairs(renderhooks) do hook.Remove(v, "pace_draw_hitbox"..self.UniqueID) @@ -783,7 +783,7 @@ function PART:PreviewHitbox() if self.Radius ~= 0 then local sides = self.Detail if self.Detail < 1 then sides = 1 end - + local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then @@ -791,7 +791,7 @@ function PART:PreviewHitbox() steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) end steps = math.max(steps + math.abs(self.ExtraSteps),1) - + --print("steps",steps, "total casts will be "..steps*self.Detail) for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier phase = math.random() @@ -866,7 +866,7 @@ function PART:PreviewHitbox() steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1))))) end steps = math.max(steps + math.abs(self.ExtraSteps),1) - + --print("steps",steps, "total casts will be "..steps*self.Detail) for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier phase = math.random() @@ -995,7 +995,7 @@ function PART:BuildCone(obj) end function PART:Initialize() - + 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() + 2 diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 9f02e5d8c..d897b23bd 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -46,7 +46,7 @@ function PART:register_command_event(str,b) local event = str local flush = b - + local num = tonumber(string.sub(event, string.find(event,"[%d]+$") or 0)) or 0 if string.find(event,"[%d]+$") then @@ -79,7 +79,7 @@ function PART:fix_event_operator() self.Operator = PART.Events[self.Event].preferred_operator --which depends, it's usually above but we'll have cases where it's best to have below, or equal self:SetInfo("The operator was automatically changed to work with this event type, which handles numbers") end - + elseif event_type == "string" then if self.Operator ~= "find" and self.Operator ~= "find simple" and self.Operator ~= "equal" then self.Operator = PART.Events[self.Event].preferred_operator --find simple @@ -105,9 +105,9 @@ function PART:AttachEditorPopup(str) info_string = "invalid event" end --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then - + --end - + str = info_string or str end self:SetupEditorPopup(info_string, true) @@ -116,7 +116,7 @@ end function PART:SetEvent(event) local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) - + if not self.Events[event] then --invalid event? try a command event local command_was_found_in_cmd_part = false for i,v in ipairs(pac.GetLocalParts()) do @@ -133,11 +133,11 @@ function PART:SetEvent(event) --now we'll use event as a command name self:SetEvent("command") self.pace_properties["Event"]:SetValue("command") - + self:SetArguments(event .. "@@0") self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") pace.PopulateProperties(self) - + end) return end @@ -151,7 +151,7 @@ function PART:SetEvent(event) --foolproofing: fix the operator to match the event's type, and fix arguments as needed self:fix_event_operator() self:fix_args() - + pace.changed_event = self --a reference to make it refresh the popup label panel pace.changed_event_time = CurTime() @@ -174,7 +174,7 @@ function PART:Initialize() end end) end - + end local function get_default(typ) @@ -366,7 +366,7 @@ local animation_event_enums = { PART.Events = {} PART.OldEvents = { - + random = { operator_type = "number", preferred_operator = "above", arguments = {{compare = "number"}}, @@ -444,7 +444,7 @@ PART.OldEvents = { return false end self.number = time - self.time - + return self:NumberOperator(self.number, seconds) end, }, @@ -729,7 +729,7 @@ PART.OldEvents = { client_spawned = { operator_type = "number", preferred_operator = "below", tutorial_explanation = "client_spawned supposedly activates for some time after you spawn", - + arguments = {{time = "number"}}, callback = function(self, ent, time) time = time or 0.1 @@ -801,7 +801,7 @@ PART.OldEvents = { if npcs_and_players_only and (not res.Entity:IsPlayer() and not res.Entity:IsNPC()) then return false end - + return self:NumberOperator(res.Fraction * distance, compare) else local classname = parent:GetNiceName() @@ -821,7 +821,7 @@ PART.OldEvents = { arguments = {{exclude_noclip = "boolean"}, {surfaces = "string"}}, userdata = {{}, {enums = function() --grounds_enums = - return + return { ["MAT_ANTLION"] = "65", ["MAT_BLOODYFLESH"] = "66", @@ -933,7 +933,7 @@ PART.OldEvents = { return false end, }, - + --this one uses util.TraceHull is_touching = { operator_type = "none", @@ -958,7 +958,7 @@ PART.OldEvents = { mins = mins * radius maxs = maxs * radius - + local tr = util.TraceHull( { start = startpos, endpos = startpos, @@ -966,7 +966,7 @@ PART.OldEvents = { mins = mins, filter = {ent, self:GetRootPart():GetOwner()} }) - + return tr.Hit end, nice = function(self, ent, extra_radius, nearest_model) @@ -978,7 +978,7 @@ PART.OldEvents = { end radius = math.Round(math.max(radius + extra_radius + 1, 1)) - + local str = self.Event .. " [radius: " .. radius .. "]" return str end, @@ -1018,7 +1018,7 @@ PART.OldEvents = { not ( (no_npc and ent2:IsNPC()) or (no_players and ent2:IsPlayer()) ) then b = true end end - + return b end, nice = function(self, ent, extra_radius, no_npc, no_players, nearest_model) @@ -1042,11 +1042,11 @@ PART.OldEvents = { is_touching_life = { operator_type = "none", tutorial_explanation = "is_touching_life checks in a stretchable box (ents.FindInBox) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", - + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}}, userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}, {default = false}, {default = false}}, callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) - + if nearest_model then ent = self:GetOwner() end extra_radius = extra_radius or 0 no_npc = no_npc or false @@ -1075,7 +1075,7 @@ PART.OldEvents = { for _,ent2 in pairs(ents_hits) do if IsValid(ent2) and (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and (ent2:IsNPC() or ent2:IsPlayer()) - + then b = true if ent2:IsNPC() and no_npc then @@ -1111,7 +1111,7 @@ PART.OldEvents = { is_touching_scalable = { operator_type = "none", tutorial_explanation = "is_touching_life checks in a stretchable box (util.TraceHull) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", - + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {nearest_model = "boolean"}}, userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}}, callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) @@ -1121,7 +1121,7 @@ PART.OldEvents = { y_stretch = y_stretch or 1 z_stretch = z_stretch or 1 nearest_model = nearest_model or false - + local mins = Vector(-x_stretch,-y_stretch,-z_stretch) local maxs = Vector(x_stretch,y_stretch,z_stretch) local startpos = ent:WorldSpaceCenter() @@ -1158,7 +1158,7 @@ PART.OldEvents = { is_explicit = { operator_type = "none", tutorial_explanation = "is_explicit activates for viewers who want to hide explicit content with pac_hide_disturbing.\nyou can make special censoring effects for them, for example", - + callback = function(self, ent) return GetConVar("pac_hide_disturbing"):GetBool() end @@ -1380,11 +1380,11 @@ PART.OldEvents = { local data = ent.pac_anim_event local b = false - + if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then data.reset = false b = true - if try_stop_gesture then + if try_stop_gesture then if string.find(find, "attack grenade") then ent:AnimResetGestureSlot( GESTURE_SLOT_GRENADE ) elseif string.find(find, "attack") or string.find(find, "reload") then @@ -1467,7 +1467,7 @@ PART.OldEvents = { output[name_substring] = name_substring end end - + elseif part.ClassName == "event" and part.Event == "command" then local cmd, time, hide = part:GetParsedArgumentsForObject(part.Events.command) output[cmd] = cmd @@ -1485,7 +1485,7 @@ PART.OldEvents = { return "command: [" .. self.Operator .. " " .. find .."] | " .. "duration: " .. time end, callback = function(self, ent, find, time) - + time = time or 0 local ply = self:GetPlayerOwner() @@ -1802,7 +1802,7 @@ PART.OldEvents = { dot_right = { operator_type = "number", preferred_operator = "above", tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side", - + arguments = {{normal = "number"}}, callback = function(self, ent, normal) @@ -1821,7 +1821,7 @@ PART.OldEvents = { flat_dot_forward = { operator_type = "number", preferred_operator = "above", tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference", - + arguments = {{normal = "number"}}, callback = function(self, ent, normal) local owner = self:GetRootPart():GetOwner() @@ -1843,7 +1843,7 @@ PART.OldEvents = { flat_dot_right = { operator_type = "number", preferred_operator = "above", tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference", - + arguments = {{normal = "number"}}, callback = function(self, ent, normal) local owner = self:GetRootPart():GetOwner() @@ -1876,11 +1876,11 @@ PART.OldEvents = { callback = function(self, ent) if not ent:IsPlayer() then return false end local vehicle = ent:GetVehicle() - + if IsValid(vehicle) then --vehicle entity exists if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent --print(vehicle:GetParent().PassengerSeats) - + if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys return ent:IsDrivingSimfphys() and ent:GetVehicle() == ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too @@ -1906,7 +1906,7 @@ PART.OldEvents = { callback = function(self, ent) if not ent:IsPlayer() then return false end local vehicle = ent:GetVehicle() - + if IsValid(vehicle) then --vehicle entity exists if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys @@ -1990,8 +1990,8 @@ PART.OldEvents = { end end end - - + + return false end, nice = function(self, ent, name_or_id) @@ -2002,7 +2002,7 @@ PART.OldEvents = { local str = "weapon_firemode ["..self.Operator.. " " .. name_or_id .. "] | " if wep.IsFAS2Weapon then - + if wep.FireMode then str = str .. wep.FireMode .. " | options : " for i,v in ipairs(wep.FireModes) do @@ -2011,7 +2011,7 @@ PART.OldEvents = { else str = str .. "" end return str end - + if wep.ArcCW then if not IsValid(wep) then return "no active weapon" end if wep.GetFiremodeName then @@ -2050,8 +2050,8 @@ PART.OldEvents = { str = str .. " | options : 1/auto/automatic, 0/single/semi-auto/semi-automatic" return str end - - + + return str end }, @@ -2073,7 +2073,7 @@ PART.OldEvents = { return false end }, - + damage_zone_hit = { operator_type = "mixed", preferred_operator = "above", arguments = {{time = "number"}, {damage = "number"}, {uid = "string"}}, @@ -2248,7 +2248,7 @@ PART.OldEvents = { if ent.pac_healthbars_layertotals[layer] then return self:NumberOperator(ent.pac_healthbars_layertotals[layer], HpValue) end - + end return false end, @@ -2260,7 +2260,7 @@ PART.OldEvents = { else str = str .. " | not found" end - + else str = str .. " | not found" end @@ -2310,7 +2310,7 @@ PART.OldEvents = { do - + --[[local base_input_enums_names = { ["IN_ATTACK"] = 1, ["IN_JUMP"] = 2, @@ -2366,7 +2366,7 @@ do end pac.key_enums = enums - + --@note button broadcast --TODO: Rate limit!!! @@ -2394,10 +2394,10 @@ do end end end - + pac.key_enums = enums end - + key = pac.key_enums[key] or key ply.pac_buttons = ply.pac_buttons or {} @@ -2411,8 +2411,8 @@ do --outsource the part pool operations pac.UpdateButtonEvents(ply, key, down) - - + + end) PART.OldEvents.button = { @@ -2441,10 +2441,10 @@ do return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")" end, callback = function(self, ent, button, holdtime, toggle) - + local holdtime = holdtime or 0 local toggle = toggle or false - + self.togglestate = self.togglestate or false self.holdtime = holdtime self.toggle = toggle @@ -2459,12 +2459,12 @@ do --print(button, "hold" ,self.holdtime) local ply = self:GetPlayerOwner() self.pac_broadcasted_buttons_holduntil = self.pac_broadcasted_buttons_holduntil or {} - + if ply == pac.LocalPlayer then - + ply.pac_broadcast_buttons = ply.pac_broadcast_buttons or {} - + if not ply.pac_broadcast_buttons[button] then local val = enums2[button:lower()] if val then @@ -2474,7 +2474,7 @@ do end ply.pac_broadcast_buttons[button] = true end - + --print(button, ply.pac_broadcasted_buttons_holduntil[button], ply.pac_broadcast_buttons[button]) --PrintTable(ply.pac_broadcast_buttons) --PrintTable(self.pac_broadcasted_buttons_holduntil) @@ -2494,7 +2494,7 @@ do else return buttons[button] end - + end end, } @@ -2653,7 +2653,7 @@ do local arguments = data.arguments local think = data.callback local eventObject = pac.CreateEvent(classname) - + if arguments then for i, data2 in ipairs(arguments) do local key, Type = next(data2) @@ -2892,7 +2892,7 @@ local function is_hidden_by_something_else(part, ignored_part) if part.active_events_ref_count > 0 and not part.active_events[ignored_part] then return true end - + return part.Hide end @@ -2979,7 +2979,7 @@ function PART:OnThink() if not ent:IsValid() then return end local data = PART.Events[self.Event] - + if not data then return end self:fix_args() @@ -3000,14 +3000,14 @@ function PART:SetAffectChildrenOnly(b) --print("changing") local ent = get_owner(self) local data = PART.Events[self.Event] - + if ent:IsValid() and data then local b = should_trigger(self, ent, data) if self.AffectChildrenOnly then local parent = self:GetParent() if parent:IsValid() then parent:SetEventTrigger(self, b) - + for _, child in ipairs(self:GetChildren()) do if child.active_events[self] then child.active_events[self] = nil @@ -3016,7 +3016,7 @@ function PART:SetAffectChildrenOnly(b) end end end - + else for _, child in ipairs(self:GetChildren()) do child:SetEventTrigger(self, b) @@ -3029,12 +3029,12 @@ function PART:SetAffectChildrenOnly(b) parent:CallRecursive("CalcShowHide", false) end end - + end end end self.AffectChildrenOnly = b - + end function PART:OnRemove() @@ -3072,7 +3072,7 @@ function PART:TriggerEvent(b) self.previousdestinationpart:SetEventTrigger(self, false) end end - + (self.DestinationPart):SetEventTrigger(self, b) self.previousdestinationpart = (self.DestinationPart) elseif IsValid(self.previousdestinationpart) then @@ -3386,7 +3386,7 @@ concommand.Add("pac_event_sequenced", function(ply, cmd, args) ply.pac_command_events[event..i] = {name = event..i, time = 0, on = 0} end end - + if found then if action == "+" or action == "forward" or action == "add" or action == "sequence+" or action == "advance" then @@ -3408,7 +3408,7 @@ concommand.Add("pac_event_sequenced", function(ply, cmd, args) if sequence_number == min then target_number = max else target_number = sequence_number - 1 end - + print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} @@ -3523,27 +3523,27 @@ do if e == "command" then local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) local this_event_hidden = v:IsHiddenBySomethingElse(false) - + if not names[cmd] then --wheel_hidden is the hide_in_eventwheel box --possible_hidden is part hidden names[cmd] = { name = cmd, event = v, - + wheel_hidden = hide, all_wheel_hidden = hide, - + possible_hidden = this_event_hidden, all_possible_hidden = this_event_hidden, } else --if already exists, we need to check counter examples for whether all members are hidden or hide_in_eventwheel - + if not hide then names[cmd].all_wheel_hidden = false end - + if not this_event_hidden then names[cmd].all_possible_hidden = false end @@ -3558,7 +3558,7 @@ do end - + available[cmd] = {type = e, time = time, trigger = cmd} end end @@ -3611,7 +3611,7 @@ do available[cmd] = nil end end - + local list = {} if true then @@ -3621,22 +3621,22 @@ do colors[colstr] = colors[colstr] or {} colors[colstr][name] = available[name] end - + for col,tbl in pairs(colors) do - + local sublist = {} for k,v in pairs(tbl) do table.insert(sublist,available[k]) end - + table.sort(sublist, function(a, b) return a.trigger < b.trigger end) for i,v in pairs(sublist) do table.insert(list,v) end end - + local uncolored_sublist = {} for k,v in pairs(available) do @@ -3644,24 +3644,24 @@ do table.insert(uncolored_sublist,available[k]) end end - + table.sort(uncolored_sublist, function(a, b) return a.trigger < b.trigger end) for k,v in ipairs(uncolored_sublist) do table.insert(list, v) end else - + for k,v in pairs(available) do if k == names[k].name then v.trigger = k table.insert(list, v) end end - + table.sort(list, function(a, b) return a.trigger > b.trigger end) end - + return list end @@ -3682,7 +3682,7 @@ do open_btn:SetSize(80,30) open_btn:SetText("Customize") open_btn:SetPos(ScrW() - 80,0) - + function open_btn:DoClick() if (pace.command_event_menu_opened == nil) then @@ -3690,7 +3690,7 @@ do elseif IsValid(pace.command_event_menu_opened) then pace.command_event_menu_opened:Remove() end - + end if show_customize_button:GetBool() then @@ -3766,11 +3766,11 @@ do local ply = pac.LocalPlayer local data = ply.pac_command_events and ply.pac_command_events[self.event.trigger] and ply.pac_command_events[self.event.trigger] - + local d1 = 64 --indicator - - + + local d2 = 50 --color local indicator_color if data then @@ -3781,7 +3781,7 @@ do local s = Lerp(math.Clamp(f,0,1), 1, 0) local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15) indicator_color = HSVToColor(210,s,v) - + else if data.on == 1 then indicator_color = HSVToColor(210,1,0.55) @@ -3809,10 +3809,10 @@ do d2 = 50 --indicator surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1) - + surface.SetDrawColor(indicator_color) surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2) - + draw.RoundedBox(0,x-40,y-8,80,16,Color(0,0,0)) elseif eventwheel_style:GetInt() == 2 then @@ -3832,7 +3832,7 @@ do end draw.SimpleText(self.name, eventwheel_font:GetString(), x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) - + cam.PopModelMatrix() end @@ -3840,7 +3840,7 @@ do -- Right clicking cancels if input.IsButtonDown(MOUSE_RIGHT) and not IsValid(pace.command_event_menu_opened) then pac.closeEventSelectionWheel(true) return end if input.IsButtonDown(MOUSE_LEFT) and not pace.command_event_menu_opened and not open_btn:IsHovered() and clickable then - + if not clicking and selected then if not selected.event.time then RunConsoleCommand("pac_event", selected.event.trigger, "toggle") @@ -3848,7 +3848,7 @@ do RunConsoleCommand("pac_event", selected.event.trigger) else local ply = pac.LocalPlayer - + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then RunConsoleCommand("pac_event", selected.event.trigger, "0") else @@ -3916,7 +3916,7 @@ do open_btn:SetSize(80,30) open_btn:SetText("Customize") open_btn:SetPos(ScrW() - 80,0) - + function open_btn:DoClick() if (pace.command_event_menu_opened == nil) then @@ -3924,7 +3924,7 @@ do elseif IsValid(pace.command_event_menu_opened) then pace.command_event_menu_opened:Remove() end - + end if show_customize_button:GetBool() then @@ -3947,14 +3947,14 @@ do local selections = {} local events = get_events() for i, v in ipairs(events) do - - + + local list_element = vgui.Create("DPanel") - + panels[i] = list_element list_element:SetSize(250,height) list_element.event = v - + selections[i] = { grow = 0, name = v.trigger, @@ -3969,7 +3969,7 @@ do RunConsoleCommand("pac_event", selected.event.trigger) else local ply = pac.LocalPlayer - + if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then RunConsoleCommand("pac_event", selected.event.trigger, "0") else @@ -3989,7 +3989,7 @@ do elseif not input.IsMouseDown(MOUSE_LEFT) then self.was_clicked = false end end end - + end gui.EnableScreenClicker(true) @@ -4008,7 +4008,7 @@ do local y = 0 for i, v in ipairs(selections) do if IsValid(v.pnl) then - + if y + height > ScrH() then y = 0 x = x + 200 @@ -4020,7 +4020,7 @@ do local ply = pac.LocalPlayer local data = ply.pac_command_events and ply.pac_command_events[list_element.event.trigger] local indicator_color - + if data then local is_oneshot = list_element.event.time and list_element.event.time > 0 @@ -4028,7 +4028,7 @@ do local f = (pac.RealTime - data.time) / list_element.event.time local s = Lerp(math.Clamp(f,0,1), 1, 0) local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15) - + indicator_color = HSVToColor(210,s,v) else if data.on == 1 then @@ -4046,7 +4046,7 @@ do local col_str_tbl = string.Split(pace.command_colors[v.name]," ") main_color = Color(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3])) end - + local hue, sat, lightness_value = ColorToHSL(main_color) @@ -4063,7 +4063,7 @@ do surface.SetDrawColor(HSVToColor(210,0,0.15)) end surface.DrawRect(x,y,200,height) - + surface.SetDrawColor(indicator_color) surface.DrawRect(x + 200/6,y + height/6,200 * 0.666,height * 0.666,2) surface.SetDrawColor(0,0,0) @@ -4078,7 +4078,7 @@ do surface.SetDrawColor(HSVToColor(210,0,0.15)) end surface.DrawRect(x,y,200,height) - + surface.SetDrawColor(indicator_color) surface.DrawRect(x + 150,y,50,height/2,2) surface.SetDrawColor(0,0,0) @@ -4094,13 +4094,13 @@ do y = y + height end - + end render.PopFilterMag() render.PopFilterMin() DisableClipping(false) - + end) pace.event_wheel_list_opened = true diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index d7fe3c1ef..f8653ca4a 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -130,7 +130,7 @@ function PART:OnDraw() for i = 1,0,-1/steps do render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) end - + steps = math.Clamp(math.ceil(self.Length / (self.Radius or 1)),1,4) for i = 0,1/8,1/128 do render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) ) @@ -168,7 +168,7 @@ function PART:Impulse(on) local locus_pos = Vector(0,0,0) if self.Locus ~= nil then - if self.Locus:IsValid() then + if self.Locus:IsValid() then locus_pos = self.Locus:GetWorldPosition() end else locus_pos = self:GetWorldPosition() end diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index dc308f067..5455f91ac 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -29,7 +29,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("DamageMultiplier", 1) BUILDER:GetSet("ModifierId", "") BUILDER:GetSet("MultiplierResetOnHide", false) - + BUILDER:EndStorableVars() diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index 368032d04..c7318b505 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -85,7 +85,7 @@ BUILDER:StartStorableVars() ["Toolgun tracer"] = "ToolTracer", ["Laser tracer"] = "LaserTracer" }}) - + BUILDER:EndStorableVars() function PART:Initialize() diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index 71b848757..f7f471dc9 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -70,10 +70,10 @@ end function PART:OnDraw() self:UpdateNodes() if self.valid_time > CurTime() then return end - + self.pos = self.pos or self:GetWorldPosition() self.ang = self.ang or self:GetWorldAngles() - + if not self.Preview then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end local stage = math.max(0,math.floor(self.LerpValue)) @@ -109,12 +109,12 @@ function PART:OnDraw() render.DrawLine(endpos,endpos + endang:Up()*4, Color(0,0,255)) render.DrawLine(self.nodes["Node"..i-1]:GetWorldPosition(),self.nodes["Node"..i]:GetWorldPosition(), Color(255,255,255)) end - + end end) end self:Interpolate(stage,proportion) - + end function PART:UpdateNodes() @@ -138,8 +138,8 @@ function PART:Interpolate(stage, proportion) else firstnode = self.nodes["Node"..stage] or self end - - + + local secondnode = self.nodes["Node"..stage+1] if firstnode == nil or firstnode == NULL or not firstnode.GetWorldPosition then firstnode = self end if secondnode == nil or secondnode == NULL or not secondnode.GetWorldPosition then secondnode = self end @@ -181,7 +181,7 @@ function GetClosestAngleMidpoint(a1, a2, proportion) if math.abs(ang_delta_candidate2) < math.abs(ang_delta_final) then ang_delta_final = ang_delta_candidate2 end - if math.abs(ang_delta_candidate3) < math.abs(ang_delta_final) then + if math.abs(ang_delta_candidate3) < math.abs(ang_delta_final) then ang_delta_final = ang_delta_candidate3 end --print("at "..ax.." 1:"..ang_delta_candidate1.." 2:"..ang_delta_candidate2.." 3:"..ang_delta_candidate3.." pick "..ang_delta_final) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index bed0cd236..76dac00cc 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -58,13 +58,13 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:OnThink() - + if not GetConVar('pac_sv_lock'):GetBool() then return end pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end end - + if self.forcebreak then if self.next_allowed_grab < CurTime() then --we're able to resume if self.ContinuousSearch then @@ -95,14 +95,14 @@ function PART:OnThink() self.default_ang = self.target_ent:GetAngles() --record the initial ent angles end if self.OverrideEyeAngles then self.default_ang.y = self:GetWorldAngles().y end --if we want to override players eye angles we will keep recording the yaw - + elseif not self.grabbing then self.default_ang = self.target_ent:GetAngles() --record the initial ent angles anyway end - + local relative_transform_matrix = Matrix() relative_transform_matrix:Identity() - + if self.RelativeGrab then if self.is_first_time then self:CalculateRelativeOffset() end relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity() @@ -110,15 +110,15 @@ function PART:OnThink() relative_transform_matrix = Matrix() relative_transform_matrix:Identity() end - + local offset_matrix = Matrix() offset_matrix:Translate(self:GetWorldPosition()) offset_matrix:Rotate(self:GetWorldAngles()) offset_matrix:Mul(relative_transform_matrix) - + local relative_offset_pos = offset_matrix:GetTranslation() local relative_offset_ang = offset_matrix:GetAngles() - + if pac.LocalPlayer == self:GetPlayerOwner() then if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end @@ -134,7 +134,7 @@ function PART:OnThink() net.WriteAngle(self:GetWorldAngles()) end end - + local try_override_eyeang = false if self.target_ent:IsPlayer() then if self.OverrideEyeAngles then try_override_eyeang = true end @@ -150,7 +150,7 @@ function PART:OnThink() if self.OverrideEyePositionPart.GetWorldAngles then can_calcview = true end - end + end net.WriteBool(can_calcview) --print(IsValid(self.OverrideEyePositionPart), self.OverrideEyeAngles) if can_calcview then @@ -167,7 +167,7 @@ function PART:OnThink() if self.Players and self.target_ent:IsPlayer() and self.OverrideAngles then local mat = Matrix() mat:Identity() - + if self.OverrideAngles then final_ang = self:GetWorldAngles() end @@ -191,12 +191,12 @@ function PART:OnThink() --print("transform ang", final_ang) --print("part's angles", self:GetWorldAngles()) --mat:Rotate(self:GetWorldAngles()) - - + + self.target_ent:EnableMatrix("RenderMultiply", mat) - + end - + self.grabbing = true self.teleported = false end @@ -220,7 +220,7 @@ do end ent:SetGravity(1) - + ent.pac_recently_broken_free_from_lock = CurTime() ent:DisableMatrix("RenderMultiply") end @@ -244,10 +244,10 @@ do if part then if part.ClassName == "lock" then part:BreakLock(target_to_release) - + end end - + else MsgC(Color(200, 200, 200), "NOW! WE SEARCH YOUR LOCAL PARTS!\n") for i,part in pairs(pac.GetLocalParts()) do @@ -260,11 +260,11 @@ do end end end - + end) net.Receive("pac_mark_grabbed_ent", function(len) - + local target_to_mark = net.ReadEntity() local successful_grab = net.ReadBool() local uid = net.ReadString() @@ -293,7 +293,7 @@ function PART:SetRadius(val) end function PART:OnShow() - + local origin_part self.is_first_time = true if self.resetting_condition or self.forcebreak then @@ -312,7 +312,7 @@ function PART:OnShow() local sv_dist = GetConVar("pac_sv_lock_max_grab_radius"):GetInt() render.DrawLine(origin_part:GetWorldPosition(),origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount),Color(255,255,255)) - + if self.Radius < sv_dist then self:SetInfo(nil) render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), sv_dist, 30, 30, Color(50,50,150),true) @@ -327,18 +327,18 @@ function PART:OnShow() if self.Mode == "Teleport" then if not GetConVar('pac_sv_lock_teleport'):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then return end self.target_ent = nil - + local ang_yaw_only = self:GetWorldAngles() ang_yaw_only.p = 0 ang_yaw_only.r = 0 if pac.LocalPlayer == self:GetPlayerOwner() then - + local teleport_pos_final = self:GetWorldPosition() if self.ClampDistance then local ply_pos = self:GetPlayerOwner():GetPos() local pos = self:GetWorldPosition() - + if pos:Distance(ply_pos) > self.Radius then local clamped_pos = ply_pos + (pos - ply_pos):GetNormalized()*self.Radius teleport_pos_final = clamped_pos @@ -357,7 +357,7 @@ function PART:OnShow() net.WriteBool(self.OverrideAngles) net.SendToServer() end) - + end self.grabbing = false elseif self.Mode == "Grab" then @@ -378,7 +378,7 @@ end function PART:reset_ent_ang() if self.target_ent == nil then return end local reset_ent = self.target_ent - + if reset_ent:IsValid() then timer.Simple(math.min(self.RestoreDelay,5), function() if pac.LocalPlayer == self:GetPlayerOwner() then @@ -403,7 +403,7 @@ function PART:OnRemove() end function PART:DecideTarget() - + local RADIUS = math.Clamp(self.Radius,0,GetConVar("pac_sv_lock_max_grab_radius"):GetInt()) local ents_candidates = {} local chosen_ent = nil @@ -461,7 +461,7 @@ function PART:DecideTarget() chosen_ent = ent_candidate end end - + if chosen_ent ~= nil then self.target_ent = chosen_ent if pac.LocalPlayer == self:GetPlayerOwner() then @@ -477,7 +477,7 @@ end function PART:CheckEntValidity() - + if self.target_ent == nil then self.valid_ent = false elseif self.target_ent:EntIndex() == 0 then diff --git a/lua/pac3/core/client/parts/movement.lua b/lua/pac3/core/client/parts/movement.lua index 94273c9d5..bc5fd4b8b 100644 --- a/lua/pac3/core/client/parts/movement.lua +++ b/lua/pac3/core/client/parts/movement.lua @@ -66,7 +66,7 @@ BUILDER:StartStorableVars() ADD(PART, "HorizontalAirFrictionMultiplier", 1, {editor_clamp = {0, 1}, editor_sensitivity = 0.1}) ADD(PART, "MaxAirSpeed", 750) ADD(PART, "StrafingStrengthMultiplier", 1) - + BUILDER:SetPropertyGroup("view angles") ADD(PART, "ReversePitch", false) @@ -101,8 +101,8 @@ end function PART:OnShow() local ent = self:GetPlayerOwner() - - if ent:IsValid() then + + if ent:IsValid() then if ent:GetClass() == "viewmodel" then ent = self:GetPlayerOwner() end ent.last_movement_part = self:GetUniqueID() for i,v in ipairs(update_these) do diff --git a/lua/pac3/core/client/parts/physics.lua b/lua/pac3/core/client/parts/physics.lua index 3cc8fb9ab..d6562de06 100644 --- a/lua/pac3/core/client/parts/physics.lua +++ b/lua/pac3/core/client/parts/physics.lua @@ -56,7 +56,7 @@ BUILDER:StartStorableVars() :GetSetPart("InitialVelocityPart") :GetSet("OverrideInitialPosition", false, {description = "Whether the initial velocity part should be used as an initial position, otherwise it'll just be for the initial velocity's angle"}) - + BUILDER:EndStorableVars() local function IsInvalidParent(self) @@ -214,7 +214,7 @@ function PART:OnThink() phys:ComputeShadowControl(params) end - + -- this is nicer i think if self.ConstrainSphere ~= 0 and phys:GetPos():Distance(self.Parent:GetWorldPosition()) > self.ConstrainSphere then if not self.PushFollow then @@ -259,7 +259,7 @@ function PART:OnHide() self.Parent:OnShow() for _,part in pairs(self:GetParent():GetChildrenList()) do part:OnShow() - + end end) return @@ -329,8 +329,8 @@ function PART:Enable() end if valid_phys_pushers > 0 then self.phys:AddVelocity(pushvec / valid_phys_pushers) end end - - + + end) end) end diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 695d11db7..7d3cb704d 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -398,7 +398,7 @@ function PART:Shoot(pos, ang, multi_projectile_count) ent:SetCollisionGroup(COLLISION_GROUP_PROJECTILE) if self:AttachToEntity(ent, false) then - + timer.Simple(math.Clamp(self.LifeTime, 0, 10), function() if ent:IsValid() then if ent.pac_projectile_part and ent.pac_projectile_part:IsValid() then @@ -410,10 +410,10 @@ function PART:Shoot(pos, ang, multi_projectile_count) end) end end) - + end - - + + end if self.Delay == 0 then @@ -491,7 +491,7 @@ pac.AddHook("Think", "pac_cleanup_CS_projectiles", function() if ent.pac_projectile_part == rootpart then local tbl = ent.pac_projectile_part:GetChildren() local partchild = tbl[next(tbl)] --ent.pac_projectile_part is the root group, but outfit part is the first child - + if IsValid(partchild) then if partchild:IsHidden() then SafeRemoveEntity(ent) @@ -571,11 +571,11 @@ do -- physical net.Start("pac_projectile_remove") net.WriteInt(ent_id, 16) net.SendToServer() - + end end end - + end end end) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 946b9c1d5..ba77e4f69 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -321,7 +321,7 @@ PART.Inputs.random_once = function(self, seed, min, max) self.rand_id = self.rand_id or {} if seed then self.rand_id[seed] = self.rand_id[seed] or min + math.random()*(max-min) - else + else self.rand = self.rand or min + math.random()*(max-min) end @@ -356,7 +356,7 @@ PART.Inputs.timeex = function(s) end PART.Inputs.part_distance = function(self, uid1, uid2) - + if not uid1 then return 0 end local owner = self:GetPlayerOwner() @@ -397,7 +397,7 @@ PART.Inputs.event_alternative = function(self, uid1, num1, num2) end return 0 end - + if PartA.ClassName == "event" then self.valid_parts_in_expression[PartA] = PartA if PartA.event_triggered then return num1 or 0 @@ -1049,7 +1049,7 @@ PART.Inputs.pac_healthbar_uidvalue = function(self, uid) end if ent.pac_healthbars and ent.pac_healthbars_uidtotals then if ent.pac_healthbars_uidtotals[uid] then - + if part:IsValid() then self.valid_parts_in_expression[part] = part end diff --git a/lua/pac3/core/client/parts/sound.lua b/lua/pac3/core/client/parts/sound.lua index 51afcb415..57072d8d9 100644 --- a/lua/pac3/core/client/parts/sound.lua +++ b/lua/pac3/core/client/parts/sound.lua @@ -175,7 +175,7 @@ function PART:SetPath(path) pace.RefreshTree() end end - + self.seq_index = 1 self.Path = path @@ -196,7 +196,7 @@ function PART:SetPath(path) table.insert(paths, path) self.AllPaths = self.AllPaths .. ";" .. path end - + end for _, stream in pairs(self.streams) do diff --git a/lua/pac3/core/shared/entity_mutators/draw_shadow.lua b/lua/pac3/core/shared/entity_mutators/draw_shadow.lua index 7a069359a..919bed31a 100644 --- a/lua/pac3/core/shared/entity_mutators/draw_shadow.lua +++ b/lua/pac3/core/shared/entity_mutators/draw_shadow.lua @@ -1,4 +1,4 @@ -local MUTATOR = {} +local MUTATOR = {} MUTATOR.ClassName = "draw_shadow" @@ -18,7 +18,7 @@ if SERVER then function MUTATOR:Mutate(b) self.Entity:DrawShadow(b) self.Entity.pac_emut_draw_shadow = b - end -end + end +end -pac.emut.Register(MUTATOR) \ No newline at end of file +pac.emut.Register(MUTATOR) \ No newline at end of file diff --git a/lua/pac3/core/shared/movement.lua b/lua/pac3/core/shared/movement.lua index 43111e866..6967b8f56 100644 --- a/lua/pac3/core/shared/movement.lua +++ b/lua/pac3/core/shared/movement.lua @@ -167,7 +167,7 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) else ply.scale_mass = 1 end - + pac.AddHook("EntityTakeDamage", "PAC3MassDamageScale", function(target, dmginfo) if (target:IsPlayer() and dmginfo:IsDamageType(DMG_CRUSH or DMG_VEHICLE)) then dmginfo:ScaleDamage(target.scale_mass or 1) @@ -227,8 +227,8 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) elseif mv:KeyDown(IN_MOVELEFT) then vel = vel - ang:Right() end - - + + vel = vel:GetNormalized() * speed if self.AllowZVelocity then @@ -265,7 +265,7 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) -- todo: don't allow adding more velocity to existing velocity if it exceeds -- but allow decreasing if not on_ground then - + if ply:WaterLevel() >= 2 then local ground_speed = self.RunSpeed @@ -301,7 +301,7 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) end vel = vel + vel2 * math.min(FrameTime(),0.3) * 2 - + else local friction = self.AirFriction local friction_mult = -(friction) + 1 @@ -330,7 +330,7 @@ pac.AddHook("Move", "custom_movement", function(ply, mv) vel = vel * friction speed = speed:GetNormalized() * math.min(speed:Length(), self.MaxGroundSpeed) - + local trace = { start = mv:GetOrigin(), endpos = mv:GetOrigin() + Vector(0, 0, -20), diff --git a/lua/pac3/editor/client/asset_browser.lua b/lua/pac3/editor/client/asset_browser.lua index 57e3a784d..b0f0e5571 100644 --- a/lua/pac3/editor/client/asset_browser.lua +++ b/lua/pac3/editor/client/asset_browser.lua @@ -52,9 +52,9 @@ local function encode_table_to_file(str) local data = {} if not file.Exists("pac3_config", "DATA") then file.CreateDir("pac3_config") - + end - + if str == "pac_editor_shortcuts" then data = pace.PACActionShortcut @@ -72,7 +72,7 @@ local function encode_table_to_file(str) str = category file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data)) end - + end end @@ -152,7 +152,7 @@ local function install_click(icon, path, pattern, on_menu, pathid) end SetClipboardText(path) end) - + if string.match(path, "^materials/(.+)%.vmt$") or string.match(path, "^materials/(.+%.png)$") then resource_type = "materials" elseif string.match(path, "^models/") then resource_type = "models" end @@ -187,7 +187,7 @@ local function install_click(icon, path, pattern, on_menu, pathid) end):SetImage("icon16/cross.png") end end - + if on_menu then on_menu(menu) end menu:Open() end @@ -729,7 +729,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) local frame = vgui.Create("DFrame") frame.title = L"asset browser" .. " - " .. (browse_types_str:gsub(";", " ")) - + if GetConVar("pac_asset_browser_remember_layout"):GetBool() then frame:SetCookieName("pac_asset_browser") end @@ -960,7 +960,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) if not series_results.start_index then goto CONTINUE end - + local series_str = base_name .. "[" .. series_results.start_index .. "," .. series_results.end_index .. "]." .. extension if not table.HasValue(pace.bookmarked_ressources[resource_type], series_str) then @@ -980,7 +980,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) ::CONTINUE:: end - + if not table.HasValue(pace.bookmarked_ressources["sound"], sound) then menu:AddOption(L"add to favorites", function() table.insert(pace.bookmarked_ressources["sound"], sound) diff --git a/lua/pac3/editor/client/init.lua b/lua/pac3/editor/client/init.lua index eff163823..b4811d6f0 100644 --- a/lua/pac3/editor/client/init.lua +++ b/lua/pac3/editor/client/init.lua @@ -135,10 +135,10 @@ function pace.OpenEditor() editor:SetPos(0, 0) end end - + if remember_divider:GetBool() then pace.vertical_div_height = pace.vertical_div_height or ScrH()/1.4 - + timer.Simple(0, function() editor.div:SetTopHeight(pace.vertical_div_height) end) diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 76f359057..387ee2b41 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -80,13 +80,13 @@ local function populate_pac(menu) version_pnl:SetImage(pace.MiscIcons.info) version:AddOption(version_string) - + version:AddOption("local update changelogs", function() pac.OpenMOTD("local_changelog") end) version:AddOption("external commit history", function() pac.OpenMOTD("commit_history") end) version:AddOption("major update news (combat update)", function() pac.OpenMOTD("combat_update") end) end - + help:AddOption( L"about", @@ -98,7 +98,7 @@ local function populate_pac(menu) menu:AddOption(L"exit", function() pace.CloseEditor() end):SetImage(pace.MiscIcons.exit) end - + end local function populate_view(menu) @@ -143,7 +143,7 @@ local function populate_options(menu) popup_pref_mode:AddOption(L"menu bar", function() RunConsoleCommand("pac_popups_preferred_location", "menu bar") end):SetImage('icon16/layout_header.png') popup_pref_mode:AddOption(L"cursor", function() RunConsoleCommand("pac_popups_preferred_location", "cursor") end):SetImage('icon16/mouse.png') popup_pref_mode:AddOption(L"screen", function() RunConsoleCommand("pac_popups_preferred_location", "screen") end):SetImage('icon16/monitor.png') - + menu:AddOption(L"configure event wheel", pace.ConfigureEventWheelMenu):SetImage("icon16/color_wheel.png") local copilot, pnlc = menu:AddSubMenu("configure editor copilot", function() end) @@ -164,14 +164,14 @@ local function populate_options(menu) local combat_consents, pnlcc = menu:AddSubMenu("pac combat consents", function() end) combat_consents.GetDeleteSelf = function() return false end pnlcc:SetImage("icon16/joystick.png") - + combat_consents:AddCVar(L"damage_zone part (area damage)", "pac_client_damage_zone_consent", "1", "0") combat_consents:AddCVar(L"hitscan part (bullets)", "pac_client_hitscan_consent", "1", "0") combat_consents:AddCVar(L"force part (physics forces)", "pac_client_force_consent", "1", "0") combat_consents:AddCVar(L"lock part's grab (can take control of your position)", "pac_client_grab_consent", "1", "0") combat_consents:AddCVar(L"lock part's grab calcview (can take control of your view)", "pac_client_lock_camera_consent", "1", "0") - - + + menu:AddSpacer() menu:AddOption(L"position grid size", function() Derma_StringRequest(L"position grid size", L"size in units:", GetConVarNumber("pac_grid_pos_size"), function(val) diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index 36c086daa..48292c139 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -176,7 +176,7 @@ function PANEL:OnRemove() if remember_divider:GetBool() then pace.vertical_div_height = self.div:GetTopHeight() end - + if self.menu_bar:IsValid() then self.menu_bar:Remove() end @@ -274,7 +274,7 @@ function PANEL:Think(...) else self.zoomsettings:SetVisible(false) end - + end end @@ -317,7 +317,7 @@ function PANEL:PerformLayout() end end elseif sz >= 1 then - + if remember_divider:GetBool() then if remember_divider:GetBool() then self.div:SetTopHeight(pace.vertical_div_height) diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 3321911ff..13b16f021 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -789,7 +789,7 @@ do -- event is_touching local x_stretch = part:GetProperty("x_stretch") or 1 local y_stretch = part:GetProperty("y_stretch") or 1 local z_stretch = part:GetProperty("z_stretch") or 1 - + local ent if part.RootOwner then ent = part:GetRootPart():GetOwner() @@ -797,7 +797,7 @@ do -- event is_touching ent = part:GetOwner() end - if nearest_model then ent = part:GetOwner() end + if nearest_model then ent = part:GetOwner() end if not IsValid(ent) then stop() return end local radius @@ -812,9 +812,9 @@ do -- event is_touching radius = math.max(ent:BoundingRadius() + extra_radius + 1, 1) mins = mins * radius maxs = maxs * radius - + local startpos = ent:WorldSpaceCenter() - local b = false + local b = false if part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable" then local tr = util.TraceHull( { start = startpos, @@ -828,7 +828,7 @@ do -- event is_touching local found = false local ents_hits = ents.FindInBox(startpos + mins, startpos + maxs) for _,ent2 in pairs(ents_hits) do - + if IsValid(ent2) and (ent2 ~= ent and ent2 ~= part:GetRootPart():GetOwner()) and (ent2:IsNPC() or ent2:IsPlayer()) then @@ -850,7 +850,7 @@ do -- event is_touching then b = true end end end - + if self.udata then render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, b and Color(255,0,0) or Color(255,255,255), true ) end @@ -889,7 +889,7 @@ do --projectile radius render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_ph, mins_ph, Color(255,255,255), true ) render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_dm, mins_dm, Color(255,0,0), true ) end - + end end) end diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 101ff769e..f57dfae55 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -29,12 +29,12 @@ function pace.FindAssetSeriesBounds(base_directory, base_file, extension) --LEADING ZEROES FIX NOT YET IMPLEMENTED local function leading_zeros(str) str = string.StripExtension(str) - + local untilzero_pattern = "%f[1-9][0-9]+$" local afterzero_pattern = "0+%f[1-9+]" local beforenumbers_pattern = "%f[%f[1-9][0-9]+$]" --string.gsub(str, "%f[1-9][0-9]+$", "") --get the start until the zeros stop - + --string.gsub(str, "0+%f[1-9+]", "") --leave start if string.find(str, afterzero_pattern) then @@ -54,7 +54,7 @@ function pace.FindAssetSeriesBounds(base_directory, base_file, extension) local index_compressed = 1 --increasing ID number of valid files while keep_looking do - + file_n = base_directory .. "/" .. base_file .. i .. "." .. extension --print(file_n , "file" , file.Exists(file_n, "GAME") and "exists" or "doesn't exist") --print("checking" , file_n) print("\tThe file" , file.Exists(file_n, "GAME") and "exists" or "doesn't exist") @@ -64,8 +64,8 @@ function pace.FindAssetSeriesBounds(base_directory, base_file, extension) tbl.all_paths[index_compressed] = file_n index_compressed = index_compressed + 1 end - - + + i = i + 1 file_n = base_directory .. "/" .. base_file .. i .. "." .. extension next_exists = file.Exists(file_n, "GAME") @@ -137,13 +137,13 @@ function pace.AddSubmenuWithBracketExpansion(pnl, func, base_file, extension, ba --local mat = string.gsub(base_file_original, "." .. string.GetExtensionFromFilename(base_file_original), "") --pnl2:AddOption(mat, function() func(base_file_original) end):SetImage("icon16/paint_can.png") elseif base_directory == "models" then - + elseif base_directory == "sound" then local snd = base_file_original menu2 = pnl:AddOption(snd, function() func(snd) end):SetImage(icon) end end - + --print(tbl) --PrintTable(tbl.all_paths) @@ -154,7 +154,7 @@ function pace.AddSubmenuWithBracketExpansion(pnl, func, base_file, extension, ba if base_directory == "materials" then local mat = string.gsub(path_no_trunk, "." .. string.GetExtensionFromFilename(path_no_trunk), "") pnl2:AddOption(mat, function() func(mat) end):SetMaterial(pace.get_unlit_mat(path)) - + elseif base_directory == "models" then local mdl = path pnl2:AddOption(string.GetFileFromFilename(mdl), function() func(mdl) end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") @@ -166,7 +166,7 @@ function pace.AddSubmenuWithBracketExpansion(pnl, func, base_file, extension, ba end end - + end @@ -336,19 +336,19 @@ do -- container function PANEL:Flash() pace.flashes = pace.flashes or {} pace.flashes[self] = {start = CurTime(), flash_end = CurTime() + 2.5, color = Color(255,0,0)} - + do --scroll to the property local _,y = self:LocalToScreen(0,0) local _,py = pace.properties:LocalToScreen(0,0) local scry = pace.properties.scr:GetScroll() - + if y > ScrH() then pace.properties.scr:SetScroll(scry - py + y) elseif y < py - 200 then pace.properties.scr:SetScroll(scry + (y - py) - 100) end end - + do --scroll to the tree node pace.tree:ScrollToChild(self:GetChildren()[1].part.pace_tree_node) end @@ -922,7 +922,7 @@ end do -- base editable local PANEL = {} - + PANEL.ClassName = "properties_base_type" PANEL.Base = "DLabel" @@ -1037,12 +1037,12 @@ do -- base editable --command's String variable if self.CurrentKey == "String" then - + pace.bookmarked_ressources = pace.bookmarked_ressources or {} - pace.bookmarked_ressources["command"] = + pace.bookmarked_ressources["command"] = { --[[["user"] = { - + },]] ["basic lua"] = { { @@ -1125,7 +1125,7 @@ do -- base editable }, }]] } - + local menu1, pnl1 = menu:AddSubMenu(L"example commands", function() end) pnl1:SetIcon("icon16/cart_go.png") @@ -1157,7 +1157,7 @@ do -- base editable for i,v in pairs(LocalPlayer().pac_command_events) do tbl3[i] = v.on end for i,v in pairs(LocalPlayer().pac_command_events) do RunConsoleCommand("pac_event", i, "0") end new_expression = "" - + for i,v in pairs(tbl3) do new_expression = new_expression .. "pac_event " .. i .. " " .. v .. ";" end pace.current_part:SetUseLua(false) end @@ -1170,7 +1170,7 @@ do -- base editable expression = expression .. ";" .. tbl2.expression pace.current_part:SetUseLua(tbl2.lua) end - + if not hardcode then pace.current_part:SetString(expression) self:SetValue(expression) @@ -1179,20 +1179,20 @@ do -- base editable self:SetValue(new_expression) end end - + end) pnl3:SetIcon(icon) pnl3:SetTooltip(tbl2.explanation) end - + end end end --proxy expression if self.CurrentKey == "Expression" then - - + + pace.bookmarked_ressources = pace.bookmarked_ressources or {} pace.bookmarked_ressources["proxy"] = pace.bookmarked_ressources["proxy"] local menu1, pnl1 = menu:AddSubMenu(L"Proxy template bits", function() @@ -1222,16 +1222,16 @@ do -- base editable elseif true then --something present: multiply the existing bit? expression = expression .. " * " .. tbl2.expression end - + pace.current_part:SetExpression(expression) self:SetValue(expression) end - + end) pnl3:SetIcon(icon) pnl3:SetTooltip(tbl2.explanation) end - + end end end @@ -1259,7 +1259,7 @@ do -- base editable local txt_zone = vgui.Create("DTextEntry", pnl2) local str = "" for i,v in pairs(tbl) do - str = str .. i .. " = " .. v .."\n" + str = str .. i .. " = " .. v .."\n" end txt_zone:SetMultiline(true) txt_zone:SetText(str) @@ -1270,7 +1270,7 @@ do -- base editable pnl2:MakePopup() end):SetImage("icon16/table.png") - + end if self.CurrentKey == "Model" then @@ -1294,26 +1294,26 @@ do -- base editable pnl:AddOption("Current 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") - + for id,mdl in ipairs(pace.bookmarked_ressources["models"]) do pnl: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 - + if self.CurrentKey == "Material" or self.CurrentKey == "SpritePath" then pace.bookmarked_ressources = pace.bookmarked_ressources or {} if not pace.bookmarked_ressources["materials"] then @@ -1333,15 +1333,15 @@ do -- base editable "metal" } end - + local pnl, menu2 = menu:AddSubMenu(L"Load favourite materials", function() end) menu2: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.find(mat, "%[%d+,%d+%]") then --find the bracket notation mat_no_ext = string.gsub(mat_no_ext, "%[%d+,%d+%]", "") pace.AddSubmenuWithBracketExpansion(pnl, function(str) @@ -1354,7 +1354,7 @@ do -- base editable pace.current_part:SetSpritePath(str) end end, mat_no_ext, "vmt", "materials") - + else pnl:AddOption(string.StripExtension(mat), function() self:SetValue(mat_no_ext) @@ -1365,7 +1365,7 @@ do -- base editable end end):SetMaterial(mat) end - + end end @@ -1383,11 +1383,11 @@ do -- base editable "phx/explode02.wav" } end - + local pnl, menu2 = menu:AddSubMenu(L"Load favourite sounds", function() end) menu2: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) @@ -1402,7 +1402,7 @@ do -- base editable 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(pnl, function(str) @@ -1415,7 +1415,7 @@ do -- base editable end, snd_no_ext, extension, "sound") else - + local icon = "icon16/sound.png" if string.find(snd, "music") or string.find(snd, "theme") then @@ -1423,7 +1423,7 @@ do -- base editable elseif string.find(snd, "loop") then icon = "icon16/arrow_rotate_clockwise.png" end - + pnl:AddOption(snd, function() self:SetValue(snd) if self.CurrentKey == "Sound" then @@ -1431,24 +1431,24 @@ do -- base editable elseif self.CurrentKey == "Path" then pace.current_part:SetPath(snd) end - + end):SetIcon(icon) end - - + + end end end --long string menu to bypass the DLabel's limits, only applicable for sound2 for urls and base part's notes if (pace.current_part.ClassName == "sound2" and self.CurrentKey == "Path") or self.CurrentKey == "Notes" then - + menu:AddOption(L"Insert long text", function() local pnl = vgui.Create("DFrame") local DText = vgui.Create("DTextEntry", pnl) local DButtonOK = vgui.Create("DButton", pnl) DText:SetMaximumCharCount(50000) - + pnl:SetSize(1200,800) pnl:SetTitle("Long text for " .. self.CurrentKey .. ". Do not touch the label after this!") pnl:SetPos(200, 100) @@ -2253,12 +2253,12 @@ function pace.OpenTreeSearch() self:SetColor(Color(150,150,150)) end end - + local function select_match() if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end if not pace.tree_search_matches[pace.tree_search_match_index] then return end - + resulting_part = pace.tree_search_matches[pace.tree_search_match_index].part_matched matched_property = pace.tree_search_matches[pace.tree_search_match_index].key_matched if resulting_part ~= pace.current_part then pace.OnPartSelected(resulting_part, true) end @@ -2292,7 +2292,7 @@ function pace.OpenTreeSearch() base:SetDraggable(false) base:SetX(pace.Editor:GetX()) base:ShowCloseButton(false) - + close_button:SetSize(40,20) close_button:SetPos(450,2) close_button:SetText("close") @@ -2310,12 +2310,12 @@ function pace.OpenTreeSearch() search_term = edit:GetText() if not case_sensitive then search_term = string.lower(search_term) end for _,part in pairs(pac.GetLocalParts()) do - + for k,v in pairs(part:GetProperties()) do local value = v.get(part) - + if (type(value) ~= "number" and type(value) ~= "string") or tree_search_excluded_vars[v.key] then continue end - + value = tostring(value) if not case_sensitive then value = string.lower(value) end @@ -2353,7 +2353,7 @@ function pace.OpenTreeSearch() range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) select_match() end - + search_button:SetSize(50,20) search_button:SetPos(400,2) search_button:SetText("search") @@ -2371,7 +2371,7 @@ function pace.OpenTreeSearch() range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) select_match() end - + function edit.OnEnter() if self.previous_search ~= edit:GetText() then perform_search() @@ -2382,8 +2382,8 @@ function pace.OpenTreeSearch() perform_search() end select_match() - + timer.Simple(0.1,function() edit:RequestFocus() end) end - + end \ No newline at end of file diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index 60d91b9ed..54c028d1e 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -202,13 +202,13 @@ do ) and not gui.IsConsoleVisible() then - + if action == "editor_node_collapse" then pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) elseif action == "editor_node_expand" then pace.Call("VariableChanged", pace.current_part, "EditorExpand", true) end - + if action == "editor_up" or action == "editor_pageup" then local added_nodes = get_added_nodes(self) local offset = action == "editor_pageup" and 10 or 1 @@ -224,13 +224,13 @@ do end end end - + self.scrolled_up = self.scrolled_up or os.clock() + 0.4 end else self.scrolled_up = nil end - + if action == "editor_down" or action == "editor_pagedown" then local added_nodes = get_added_nodes(self) local offset = action == "editor_pagedown" and 10 or 1 @@ -246,7 +246,7 @@ do end end end - + self.scrolled_down = self.scrolled_down or os.clock() + 0.4 end else @@ -254,7 +254,7 @@ do end end end - + function pace.DoScrollControls(action) DoScrollControl(pace.tree, action) end @@ -613,7 +613,7 @@ local function remove_node(part) part.pace_tree_node:GetRoot().m_pSelectedItem = nil part.pace_tree_node:Remove() pace.RefreshTree() - + end end @@ -654,7 +654,7 @@ pace.allowed_event_refresh = 0 function pace.RefreshEvents() --spam preventer, (load parts' initializes gets called) if pace.allowed_event_refresh > CurTime() then return else pace.allowed_event_refresh = CurTime() + 0.1 end - + local events = {} for _, part in pairs(pac.GetLocalParts()) do if part.ClassName == "event" then @@ -662,7 +662,7 @@ function pace.RefreshEvents() end end local no_events = table.Count(events) == 0 - + for _, child in pairs(pac.GetLocalParts()) do child.active_events = {} child.active_events_ref_count = 0 @@ -673,7 +673,7 @@ function pace.RefreshEvents() end child:CallRecursive("CalcShowHide", false) end - + end function pace.RefreshTree(reset) @@ -688,7 +688,7 @@ function pace.RefreshTree(reset) end end) end - + end if Entity(1):IsPlayer() and not PAC_RESTART and not VLL2_FILEDEF then diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 0c1a1be49..8ac861fc0 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -67,7 +67,7 @@ function pace.WearParts(temp_wear_filter) pac.Message(reason or "the server doesn't want you to wear parts for some reason") return end - + pace.still_loading_wearing = false return pace.WearOnServer(temp_wear_filter) @@ -161,11 +161,11 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) pace.AssetBrowser(function(path) if not part:IsValid() then return end -- because we refresh the properties - + if IsValid(self) and self.OnValueChanged then self.OnValueChanged(path) end - + if pace.current_part.SetMaterials then local model = pace.current_part:GetModel() local part = pace.current_part @@ -174,16 +174,16 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) end part.pace_last_model = model end - + pace.PopulateProperties(pace.current_part) - + for k,v in ipairs(pace.properties.List) do if v.panel and v.panel.part == part and v.key == key then self = v.panel break end end - + end, "models") elseif class_name == "sound" or class_name == "sound2" then if class_name == "sound"then @@ -191,7 +191,7 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) elseif class_name == "sound2" then self = pace.current_part.pace_properties["Path"] end - + pace.AssetBrowser(function(path) if not self:IsValid() then return end @@ -211,7 +211,7 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) end end) - + end if class_name == "camera" and GetConVar("pac_copilot_force_preview_cameras"):GetBool() then RunConsoleCommand("pac_enable_editor_view", "0") @@ -228,7 +228,7 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) temp.zfar = farz temp.drawviewer = not part.DrawViewModel return temp - + end) end if GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool() then @@ -349,7 +349,7 @@ function pace.FlashProperty(obj, key, edit) end) timer.Simple(0.3, function() obj.flashing_property = false end) end - + end function pace.OnVariableChanged(obj, key, val, not_from_editor) @@ -517,7 +517,7 @@ do -- menu for _, part in pairs(pace.GetRegisteredParts()) do local class = part.ClassName local groupname = "other" - + local group = part.Group or part.Groups or "other" --print(group) if isstring(group) then @@ -532,9 +532,9 @@ do -- menu Groups[v][class] = Groups[v][class] or class end end - + Groups[groupname] = Groups[groupname] or group - + Groups[groupname][class] = Groups[groupname][class] or class --[[if isstring(group) then @@ -558,9 +558,9 @@ do -- menu end end end - + --file.Write("pac_partgroups.txt", util.TableToKeyValues(Groups)) - + local other = sortedTree.other sortedTree.other = nil @@ -615,7 +615,7 @@ do -- menu --tbl is a shallow table with part class names --PrintTable(Groups) for group, tbl in pairs(Groups) do - + local sub, pnl = menu:AddSubMenu(group, function() if Parts[group] then if group == "entity" then @@ -633,10 +633,10 @@ do -- menu end end end) - + --@note partmenu definer sub.GetDeleteSelf = function() return false end - + if tbl["icon"] then --print(tbl["icon"]) if pace.MiscIcons[string.gsub(tbl["icon"], "pace.MiscIcons.", "")] then @@ -660,7 +660,7 @@ do -- menu for i, class in pairs(tbl) do if isstring(i) and Parts[class] then local tooltip = pace.TUTORIALS.PartInfos[class].tooltip - + if not tooltip or tooltip == "" then tooltip = "no information available" end if #i > 2 then local part_submenu = add_part(sub, Parts[class]) @@ -697,11 +697,11 @@ do -- menu end local mode = GetConVar("pac_copilot_partsearch_depth"):GetInt() pace.suppress_flashing_property = false - + local base = vgui.Create("EditablePanel") base:SetPos(input.GetCursorPos()) base:SetSize(200, 300) - + base:MakePopup() function base:OnRemove() @@ -871,12 +871,12 @@ do -- menu line.DoClick = function() pace.RecordUndoHistory() pace.Call("CreatePart", part.ClassName) - + if part.ClassName == "event" then remove_now = false result:Clear() result.found = {} - + if mode == 0 then --auto-focus mode remove_now = true timer.Simple(0.1, function() @@ -968,11 +968,11 @@ do -- menu result:Clear() populate_with_models(base,result,"") end - + else base:Remove() end - + elseif base.search_mode == "events" then if pace.current_part.Event then pace.current_part:SetEvent() @@ -1000,9 +1000,9 @@ do -- menu else base:Remove() end - + end - + if result.found[1].ClassName == "event" then remove_now = false result:Clear() @@ -1040,19 +1040,19 @@ do -- menu if base.search_mode == "classes" then populate_with_classes(base, result, str) - + elseif base.search_mode == "events" then populate_with_events(base,result,str,event_template) - + elseif base.search_mode == "models" then populate_with_models(base,result,str) elseif base.search_mode == "sounds" then populate_with_sounds(base,result,str) end - + --base:SetHeight(20 * #result.found + edit:GetTall()) base:SetHeight(600 + edit:GetTall()) - + end edit:OnValueChange("") @@ -1064,12 +1064,12 @@ do -- menu end end end) - + timer.Simple(0.1, function() base:MoveToFront() base:RequestFocus() end) - + end function pace.Copy(obj) @@ -1229,17 +1229,17 @@ do -- menu pace.Editor:InvalidateLayout() pace.RefreshTree() end - + obj.ClassName = str - + timer.Simple(0, function() _G.pac_Restart() if str == "model2" then obj = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid) obj:SetModel("models/pac/default.mdl") end - + end) end) end @@ -1291,7 +1291,7 @@ do -- menu if not silent then if selected_part_added then surface.PlaySound('buttons/button1.wav') - + else surface.PlaySound('buttons/button16.wav') end end @@ -1343,12 +1343,12 @@ do -- menu --[[if not table.HasValue(pace.BulkSelectList,obj) then basepart = pace.BulkSelectList[1] end]] - + local Panel = vgui.Create( "DFrame" ) Panel:SetSize( 500, 600 ) Panel:Center() Panel:SetTitle("BULK SELECT PROPERTY EDIT - WARNING! EXPERIMENTAL FEATURE!") - + Panel:MakePopup() surface.CreateFont("Font", { font = "Arial", @@ -1377,7 +1377,7 @@ do -- menu partinfo_icon:SetSize(30,30) partinfo_icon:SetPos( 300, 0 ) partinfo_icon:SetImage(basepart.Icon) - + basepart_label:SetText("base part: "..partinfo) basepart_label:SetFont("Font") @@ -1388,12 +1388,12 @@ do -- menu ["UniqueID"] = true, ["TargetEntityUID"] = true } - + local shared_properties = {} local shared_udata_properties = {} for _,prop in pairs(basepart:GetProperties()) do - + local shared = true for _,part2 in pairs(pace.BulkSelectList) do if basepart ~= part2 and basepart.ClassName ~= part2.ClassName then @@ -1454,7 +1454,7 @@ do -- menu end end if basepart["Get" .. v] ~= nil then var_type = type(basepart["Get" .. v](basepart)) end - + if var_type == "number" then VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL) VAR_PANEL_EDITZONE:SetSize(200,30) @@ -1475,11 +1475,11 @@ do -- menu VAR_PANEL_EDITZONE:SetSize(200,30) end VAR_PANEL_EDITZONE:SetPos(200,0) - + VAR_PANEL_BUTTON:SetText("APPLY") - + VAR_PANEL:SetTitle("[" .. i .. "] "..v.." "..var_type) - + VAR_PANEL:Dock( TOP ) VAR_PANEL:DockMargin( 5, 0, 0, 5 ) VAR_PANEL_BUTTON.DoClick = function() @@ -1497,7 +1497,7 @@ do -- menu sent_var = VAR_PANEL_EDITZONE:GetChecked() elseif var_type == "string" then sent_var = VAR_PANEL_EDITZONE:GetValue() - if v == "Name" and sent_var ~= "" then + if v == "Name" and sent_var ~= "" then sent_var = sent_var..i end elseif var_type == "Vector" then @@ -1532,7 +1532,7 @@ do -- menu end scroll_panel:AddItem( VAR_PANEL ) end - + --populate panels for event "userdata" packaged into arguments if #shared_udata_properties > 0 then local fallback_event_types = {} @@ -1575,7 +1575,7 @@ do -- menu local function GetEventArgIndex(part,str) str = string.gsub(str, "event_udata_", "") - + for argn,arg in ipairs(part.Events[part.Event].__registeredArguments) do if arg[1] == str then return argn @@ -1598,7 +1598,7 @@ do -- menu if var_type == nil then var_type = "string" end local VAR_PANEL = vgui.Create("DFrame") - + VAR_PANEL:SetSize(500,30) VAR_PANEL:SetPos(0,0) VAR_PANEL:ShowCloseButton( false ) @@ -1629,12 +1629,12 @@ do -- menu VAR_PANEL_EDITZONE:SetPos(200,0) VAR_PANEL:SetTitle("[" .. i .. "] "..udata_val_name.." "..var_type) VAR_PANEL_BUTTON:SetText("APPLY") - - + + VAR_PANEL:Dock( TOP ) VAR_PANEL:DockMargin( 5, 0, 0, 5 ) VAR_PANEL_BUTTON.DoClick = function() - + for i,part in ipairs(pace.BulkSelectList) do --PrintTable(part.Events[part.Event].__registeredArguments) local sent_var @@ -1652,7 +1652,7 @@ do -- menu else sent_var = "0" end elseif var_type == "string" then sent_var = VAR_PANEL_EDITZONE:GetValue() - if v == "Name" and sent_var ~= "" then + if v == "Name" and sent_var ~= "" then sent_var = sent_var..i end else sent_var = VAR_PANEL_EDITZONE:GetValue() end @@ -1680,7 +1680,7 @@ do -- menu else sent_var = "0" end elseif var_type == "string" then sent_var = VAR_PANEL_EDITZONE:GetValue() - if v == "Name" and sent_var ~= "" then + if v == "Name" and sent_var ~= "" then sent_var = sent_var..i end else sent_var = VAR_PANEL_EDITZONE:GetValue() end @@ -1690,7 +1690,7 @@ do -- menu end end end - + pace.RefreshTree(true) timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end) end @@ -1818,7 +1818,7 @@ do -- menu for _,option_name in ipairs(pace.operations_order) do pace.addPartMenuComponent(menu, obj, option_name) end - + --[[if obj then if not obj:HasParent() then menu:AddOption(L"wear", function() @@ -1832,7 +1832,7 @@ do -- menu menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png') menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone) - + local part_size_info, psi_icon = menu:AddSubMenu(L"get part size information", function() local function GetTableSizeInfo(obj_arg) if not IsValid(obj_arg) then return { @@ -1840,7 +1840,7 @@ do -- menu info = "" } end local charsize = #util.TableToJSON(obj_arg:ToTable()) - + local kilo_range = -1 local remainder = charsize*2 while remainder / 1000 > 1 do @@ -1862,7 +1862,7 @@ do -- menu info = "raw JSON table size: " .. charsize*2 .. " bytes (" .. remainder .. " " .. unit .. ")" } end - + local part_size_info = GetTableSizeInfo(obj) local part_size_info_root = GetTableSizeInfo(obj:GetRootPart()) @@ -1889,7 +1889,7 @@ do -- menu end) psi_icon:SetImage('icon16/drive.png') - + part_size_info:AddOption(L"from bulk select", function() local cumulative_bytes = 0 for _,v in pairs(pace.BulkSelectList) do @@ -1914,7 +1914,7 @@ do -- menu pac.Message("Bulk selected parts total " .. remainder .. unit) end ) - + local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end) bap_icon:SetImage('icon16/table_multiple.png') bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end) @@ -1993,7 +1993,7 @@ do -- menu menu:AddSpacer() end]] - + --pace.AddRegisteredPartsToMenu(menu, not obj) --menu:AddSpacer() @@ -2020,7 +2020,7 @@ do -- menu function pace.OnNewPartMenu() pace.current_part = NULL local menu = DermaMenu() - + menu:MakePopup() menu:SetPos(input.GetCursorPos()) @@ -2038,7 +2038,7 @@ do -- menu end - + end function pace.GetPartSizeInformation(obj) @@ -2054,7 +2054,7 @@ function pace.GetPartSizeInformation(obj) for i,v in pairs(roots) do all_charsize = all_charsize + #util.TableToJSON(v:ToTable()) end - + return { raw_bytes = charsize*2, info = "raw JSON table size: " .. charsize*2 .. " bytes (" .. string.NiceSize(charsize*2) .. ")", @@ -2106,9 +2106,9 @@ function pace.addPartMenuComponent(menu, obj, option_name) local part_size_info, psi_icon = menu:AddSubMenu(L"get part size information", function() local part_size_info = GetTableSizeInfo(obj) local part_size_info_root = GetTableSizeInfo(obj:GetRootPart()) - + local part_size_info_root_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_root.raw_bytes,1) .. "% share of root " - + local part_size_info_parent local part_size_info_parent_processed if IsValid(obj.Parent) then @@ -2127,7 +2127,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) part_size_info_root_processed,obj:GetRootPart() ) end - + end) psi_icon:SetImage('icon16/drive.png') part_size_info:AddOption(L"from bulk select", function() @@ -2136,7 +2136,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) v.partsize_info = pace.GetPartSizeInformation(v) cumulative_bytes = cumulative_bytes + 2*#util.TableToJSON(v:ToTable()) end - + pac.Message("Bulk selected parts total " .. string.NiceSize(cumulative_bytes) .. "\nhere's the breakdown:") for _,v in pairs(pace.BulkSelectList) do local partsize_info = pace.GetPartSizeInformation(v) @@ -2178,7 +2178,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) else name_str = v.Name end - + copied:AddOption(i .. " : " .. name_str .. " (" .. v.ClassName .. ")"):SetIcon(v.Icon) end end @@ -2194,11 +2194,11 @@ function pace.addPartMenuComponent(menu, obj, option_name) else name_str = v.Name end - + copied:AddOption(i .. " : " .. name_str .. " (" .. v.ClassName .. ")"):SetIcon(v.Icon) end end - + bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() pace.BulkCutPaste(obj) end):SetImage('icon16/arrow_join.png') @@ -2212,7 +2212,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) pace.BulkCutPasteOrdered() end):SetImage('icon16/arrow_switch.png') end - + bulk_menu:AddOption(L"Copy to Bulk Clipboard", function() pace.BulkCopy(obj) @@ -2365,7 +2365,7 @@ function pace.UltraCleanup(obj) ["OutfitPart"] = nil, --projectile bullets ["EndPoint"] = nil --beams } - + if part.ClassName == "projectile" then if part.OutfitPart then if part.OutfitPart:IsValid() then @@ -2412,7 +2412,7 @@ function pace.UltraCleanup(obj) if part.ClassName == "event" or part.ClassName == "proxy" or part.ClassName == "command" then return false end - + if not part:IsHidden() and not part.Hide then else safe = false @@ -2420,7 +2420,7 @@ function pace.UltraCleanup(obj) safe = true end end - + return safe end @@ -2448,7 +2448,7 @@ function pace.UltraCleanup(obj) if part.PlayCount == 0 then return true end end end - + --fireonce particles should be wiped if c == "particle" then if part.NumberParticles == 0 or part.FireOnce then return true end @@ -2462,7 +2462,7 @@ function pace.UltraCleanup(obj) if c == "sunbeams" then if math.abs(part.Multiplier) == 0 then return true end end - + --other parts to leave forever if c == "shake" or c == "gesture" then return true @@ -2470,7 +2470,7 @@ function pace.UltraCleanup(obj) end local function FindNearestSafeParent(part) - if not part then return end + if not part then return end local root = part:GetRootPart() local parent = part:GetParent() local child = part @@ -2510,7 +2510,7 @@ function pace.UltraCleanup(obj) --extract children into nearest safe parent BUT ONLY DO IT VIA ITS DOMINANT and IF IT'S SAFE --delete remaining unsafes: events, hiddens, commands ... --but we still need to check for children to extract! - + local function Move_contents_up(part) --this will be the powerhouse recursor local parent = FindNearestSafeParent(part) --print(part, "nearest parent is", parent) @@ -2529,13 +2529,13 @@ function pace.UltraCleanup(obj) else --parent = process the children? done by the recursion --the parent still needs to be moved up child:SetParent(parent) - + safe_parts[child] = child Move_contents_up(child) --recurse end - + end - + end end @@ -2546,7 +2546,7 @@ function pace.UltraCleanup(obj) if not FoundImportantMarkedParent(v) then v:Remove() end - + end end @@ -2564,7 +2564,7 @@ function pace.UltraCleanup(obj) Move_contents_up(v) marked_for_deletion[v] = v end - + end --after that, the remaining events etc are marked for i,v in pairs(root:GetChildrenList()) do @@ -2572,15 +2572,15 @@ function pace.UltraCleanup(obj) marked_for_deletion[v] = v end end - + pace.RefreshTree() --go through delete tables except when marked as important or those protected by these for i,v in pairs(marked_for_deletion) do - + local delete = false - + if not safe_parts[v] then - + if v:IsValid() then delete = true end @@ -2699,7 +2699,7 @@ do --hover highlight halo if IsValid(v.pace_tree_node) then v.pace_tree_node:SetAlpha( 255 ) end - + end end @@ -2819,7 +2819,7 @@ end tbl = { obj = part.Label, --the associated object, could be a tree label, mouse, part etc. pac_part = part --a pac part reference, if applicable - obj_type = "pac tree label", + obj_type = "pac tree label", hoverfunc = function() end, doclickfunc = function() end, panel_exp_width = 300, panel_exp_height = 200 diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index e7336c999..1a520ceda 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -1,7 +1,7 @@ --[[ This is the framework for popups. This should be expandable for various use cases. It uses DFrame as a base, overrides the Paint function for a basic fade effect. - + Tutorials will be written here ]] @@ -44,13 +44,13 @@ function pace.OpenPopupConfig() local basecolor_pulse = vgui.Create("DNumSlider") basecolor_pulse:SetMax(255) basecolor_pulse:SetMin(0) - + if isnumber(GetConVar("pac_popups_base_color_pulse"):GetInt()) then basecolor_pulse:SetValue(GetConVar("pac_popups_base_color_pulse"):GetInt()) else basecolor_pulse:SetValue(0) end - + basecolor_pulse:SetText("base pulse") function basecolor_pulse:OnValueChanged(val) val = math.Round(tonumber(val),0) @@ -215,7 +215,7 @@ function pac.InfoPopup(str, tbl, x, y) local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ") if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2) - + local pnl = vgui.Create("DFrame") local txt_zone = vgui.Create("RichText", pnl) @@ -231,19 +231,19 @@ function pac.InfoPopup(str, tbl, x, y) end end end - - + + pnl.hoverfunc = function() end pnl.doclickfunc = function() end pnl.titletext = "Click for more information! (or F1)" pnl.alternativetitle = "Right click / Alt+P to kill popups. \"pac_popups_preserve_on_autofade\" is set to " .. GetConVar("pac_popups_preserve_on_autofade"):GetInt() .. ", " .. (GetConVar("pac_popups_preserve_on_autofade"):GetBool() and "If it fades away, the popup is allowed to reappear on hover or F1" or "If it fades away, the popup will not reappear") - + --pnl:SetPos(ScrW()/2 + math.Rand(-100,100), ScrH()/2 + math.Rand(-100,100)) function pnl:FixPartReference(tbl) if not tbl or table.IsEmpty(tbl) then self:Remove() end if tbl.pac_part then tbl.obj = tbl.pac_part.pace_tree_node end - + end function pnl:MoveToObj(tbl) @@ -284,7 +284,7 @@ function pac.InfoPopup(str, tbl, x, y) elseif tbl.obj_type == "screen" then self:SetPos(x,y) - + elseif tbl.obj_type == "cursor" then self:SetPos(input.GetCursorPos()) @@ -310,7 +310,7 @@ function pac.InfoPopup(str, tbl, x, y) if not pnl.hovering and not pnl.expand then pnl.resizing = true pnl.expand = true - + pnl.ResizeStartTime = CurTime() pnl.ResizeEndTime = CurTime() + 0.3 end @@ -322,7 +322,7 @@ function pac.InfoPopup(str, tbl, x, y) pnl.exp_height = tbl.panel_exp_height or 400 pnl.exp_width = tbl.panel_exp_width or 800 end - + pnl.exp_height = pnl.exp_height or 400 pnl.exp_width = pnl.exp_width or 800 pnl:SetSize(200,20) @@ -346,14 +346,14 @@ function pac.InfoPopup(str, tbl, x, y) --the header needs a label to click on to open the popup function pnl:DoClick() - + if input.IsKeyDown(KEY_F1) or (self:IsHovered() and not txt_zone:IsHovered()) then pnl.expand = not pnl.expand pnl.ResizeStartTime = CurTime() pnl.ResizeEndTime = CurTime() + 0.3 pnl.resizing = true end - + pnl:keep_alive(3) pnl.doclickfunc() end @@ -371,7 +371,7 @@ function pac.InfoPopup(str, tbl, x, y) self:Remove() end end - + self.F1_doclick_possible_at = self.F1_doclick_possible_at or 0 self.mouse_doclick_possible_at = self.mouse_doclick_possible_at or 0 @@ -412,7 +412,7 @@ function pac.InfoPopup(str, tbl, x, y) else width = 200 + (self.exp_width - 200)*(1 - expand_frac_h) height = 20 + (self.exp_height - 20)*(1 - expand_frac_w) - + end self:SetSize(width,height) txt_zone:SetSize(width-10,height-30) @@ -462,7 +462,7 @@ function pac.InfoPopup(str, tbl, x, y) pnl.doclickfunc = tbl.doclickfunc or function() end - + pnl.exp_height = tbl.panel_exp_height pnl.exp_width = tbl.panel_exp_width @@ -493,14 +493,14 @@ function pac.InfoPopup(str, tbl, x, y) pnl:keep_alive(3) end end - + txt_zone:SetText("") txt_zone:AppendText(str) - + txt_zone:SetVerticalScrollbarEnabled(true) function pnl:Paint( w, h ) - + self.fade_factor = self.fade_factor or 1 --base layer @@ -517,7 +517,7 @@ function pac.InfoPopup(str, tbl, x, y) --draw.RoundedBox( 0, band, 0, 1, 1, Color( 88, 179, 255, 255)) --draw.RoundedBox( 0, band, h-1, 1, 1, Color( 0, 0, 0, 255)) end - + if self.expand then draw.DrawText(self.alternativetitle, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) else @@ -543,7 +543,7 @@ function pace.FlushInfoPopups() node.popupinfopnl = nil end end - + end --[[ @@ -563,7 +563,7 @@ but then again we should probably look for better ways for the full-length expla ]] do - + pace.TUTORIALS = pace.TUTORIALS or {} pace.TUTORIALS.PartInfos = { @@ -739,7 +739,7 @@ do popup_tutorial = "The legacy experimental bone part still does the basic things you need a bone part to do, but you should probably use the new bone part." }, - + ["bone3"] = { tooltip = "changes a bone", popup_tutorial = @@ -749,7 +749,7 @@ do ["player_config"] = { tooltip = "sets your player entity's behaviour", - popup_tutorial = + popup_tutorial = "This part has access to some of your player's behavior, like whether you will play footsteps, the chat animation etc.\n".. "Some of these may or may not work as intended..." }, @@ -760,7 +760,7 @@ do "This legacy part still does the basic thing you want from a light, but the new light part is more fully-featured, for the most part.\n".. "There is one thing it does that the new part doesn't, and that's styles." }, - + ["light2"] = { tooltip = "lights up models or the world", popup_tutorial = @@ -814,7 +814,7 @@ do ["faceposer"] = { tooltip = "Adjusts multiple facial expression slots", - popup_tutorial = + popup_tutorial = "This part gives access to multiple facial expressions defined by your model's shape keys in one part.\n".. "The flex multiplier affects the whole model, so you should avoid stacking faceposers if they have different multipliers." }, @@ -1131,7 +1131,7 @@ do --print("we have defined the pace.TUTORIALS.PartInfos", pace.TUTORIALS.PartInfos) for i,v in pairs(pace.TUTORIALS.PartInfos) do - --print(i,v) + --print(i,v) if pace.PartTemplates then if pace.PartTemplates[i] then pace.PartTemplates[i].TutorialInfo = v diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 391beb0d7..f9ea3b948 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -2,7 +2,7 @@ local L = pace.LanguageString -- load only when hovered above local function add_expensive_submenu_load(pnl, callback, subdir) - + local old = pnl.OnCursorEntered pnl.OnCursorEntered = function(...) callback(subdir) @@ -271,7 +271,7 @@ function pace.LoadParts(name, clear, override_part) pace.LoadPartsFromTable(part, false, false) end end - + else if name == "autoload" and (not data or not next(data)) then @@ -288,11 +288,11 @@ function pace.LoadParts(name, clear, override_part) return end - + pace.LoadPartsFromTable(data, clear, override_part) end - + end end end @@ -589,7 +589,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) local subdir = "pac3/__backup/*" add_expensive_submenu_load(pnl, function(subdir) - + local files = file.Find("pac3/__backup/*", "DATA") local files2 = {} diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index f5f077793..634726f6b 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -52,12 +52,11 @@ local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, pace = pace -pace.partmenu_categories_cedrics = -{ +pace.partmenu_categories_cedrics = { ["new!"] = { ["icon"] = "icon16/new.png", - ["interpolated_multibone"]= "interpolated_multibone", + ["interpolated_multibone"] = "interpolated_multibone", ["damage_zone"] = "damage_zone", ["hitscan"] = "hitscan", ["lock"] = "lock", @@ -73,16 +72,16 @@ pace.partmenu_categories_cedrics = ["text"] = "text", ["link"] = "link", }, - ["scaffolds"] = + ["scaffolds"] = { ["tooltip"] = "useful to build up structures with specific positioning rules", ["icon"] = "map", ["jiggle"] = "jiggle", ["model2"] = "model2", ["projectile"] = "projectile", - ["interpolated_multibone"]= "interpolated_multibone", + ["interpolated_multibone"] = "interpolated_multibone", }, - ["combat"] = + ["combat"] = { ["icon"] = "icon16/joystick.png", ["damage_zone"] = "damage_zone", @@ -103,16 +102,16 @@ pace.partmenu_categories_cedrics = ["sprite"] = "sprite", ["particle"] = "particle", }, - ["materials"]= + ["materials"] = { ["icon"] = "pace.MiscIcons.appearance", ["material_3d"] = "material_3d", ["material_2d"] = "material_2d", ["material_refract"] = "material_refract", - ["material_eye refract"]= "material_eye refract", + ["material_eye refract"] = "material_eye refract", ["submaterial"] = "submaterial", }, - ["entity"] = + ["entity"] = { ["icon"] = "icon16/cd_go.png", ["bone3"] = "bone3", @@ -132,7 +131,7 @@ pace.partmenu_categories_cedrics = ["material_3d"] = "material_3d", ["weapon"] = "weapon", }, - ["model"] = + ["model"] = { ["icon"] = "icon16/bricks.png", ["jiggle"] = "jiggle", @@ -146,7 +145,7 @@ pace.partmenu_categories_cedrics = ["material_3d"] = "material_3d", ["model2"] = "model2", }, - ["modifiers"] = + ["modifiers"] = { ["icon"] = "icon16/connect.png", ["fog"] = "fog", @@ -158,7 +157,7 @@ pace.partmenu_categories_cedrics = ["material_3d"] = "material_3d", ["proxy"]= "proxy", }, - ["effects"] = + ["effects"] = { ["icon"] = "icon16/wand.png", ["sprite"] = "sprite", @@ -179,7 +178,7 @@ pace.partmenu_categories_cedrics = } } -pace.partmenu_categories_default = +pace.partmenu_categories_default = { ["legacy"]= { @@ -328,7 +327,7 @@ local function rebuild_bookmarks() if not pace.bookmarked_ressources["proxy"] or table.IsEmpty(pace.bookmarked_ressources["proxy"]) then pace.bookmarked_ressources["proxy"] = { --[[["user"] = { - + },]] ["fades and transitions"] ={ { @@ -342,7 +341,7 @@ local function rebuild_bookmarks() explanation = "the simplest fade's reverse.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 1, moves gradually to 0 and stops progressing at 0 due to the clamp" }, { - nicename = "standard clamp fade (delayed in)", + nicename = "standard clamp fade (delayed in)", expression = "clamp(-1 + timeex(),0,1)", explanation = "the basic fade is delayed by the fact that the clamp makes sure the negative values are pulled back to 0 until the first argument crosses 0 into the clamp's range." }, @@ -519,7 +518,7 @@ local function rebuild_bookmarks() } } } - + end end @@ -533,9 +532,9 @@ local function encode_table_to_file(str) local data = {} if not file.Exists("pac3_config", "DATA") then file.CreateDir("pac3_config") - + end - + if str == "pac_editor_shortcuts" then data = pace.PACActionShortcut @@ -579,10 +578,10 @@ local function decode_table_from_file(str) elseif str == "pac_editor_partmenu_layouts" then pace.operations_order = util.JSONToTable(data) - + elseif str == "pac_part_categories" then pace.partgroups = util.KeyValuesToTable(data) - + elseif str == "eventwheel_colors" then pace.command_colors = util.KeyValuesToTable(data) end @@ -624,12 +623,12 @@ function PANEL:Init() local combat_ban_settings = pace.FillCombatBanPanel(master_pnl) master_pnl:AddSheet("Combat Bans (SV)", combat_ban_settings) - + end - - + + self.sheet = master_pnl - + --local properties_shortcuts = pace.FillShortcutSettings(pnl) --pnl:AddSheet("Editor Shortcuts", properties_shortcuts) end @@ -666,7 +665,7 @@ function pace.FillBanPanel(pnl) ban_list:SetText("ban list") ban_list:SetSize(400,400) ban_list:SetPos(10,10) - + ban_list:AddColumn("Player name") ban_list:AddColumn("SteamID") ban_list:AddColumn("State") @@ -686,10 +685,10 @@ function pace.FillBanPanel(pnl) ply_state_list[player.GetBySteamID(line:GetColumnText( 2 ))] = state PrintTable(ply_state_list) end - + local ban_confirm_list_button = vgui.Create("DButton", BAN) ban_confirm_list_button:SetText("Send ban list update to server") - + ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!") ban_confirm_list_button:SetColor(Color(255,0,0)) ban_confirm_list_button:SetSize(200, 40) @@ -704,7 +703,7 @@ function pace.FillBanPanel(pnl) --ban_request_list_button:SetColor(Color(255,0,0)) ban_request_list_button:SetSize(200, 40) ban_request_list_button:SetPos(450, 60) - + function ban_request_list_button:DoClick() net.Start("pac.RequestBanStates") net.SendToServer() @@ -715,7 +714,7 @@ function pace.FillBanPanel(pnl) player_ban_list = players PrintTable(players) end) - + return BAN end @@ -724,13 +723,13 @@ function pace.FillCombatBanPanel(pnl) local pnl = pnl local BAN = vgui.Create("DPanel", pnl) pac.global_combat_whitelist = pac.global_combat_whitelist or {} - + local ban_list = vgui.Create("DListView", BAN) ban_list:SetText("Combat ban list") ban_list:SetSize(400,400) ban_list:SetPos(10,10) - + ban_list:AddColumn("Player name") ban_list:AddColumn("SteamID") ban_list:AddColumn("State") @@ -750,7 +749,7 @@ function pace.FillCombatBanPanel(pnl) for id,data in pairs(pac.global_combat_whitelist) do combat_bans_temp_merger[id] = data end - + for id,data in pairs(combat_bans_temp_merger) do ban_list:AddLine(data.nick,data.steamid,data.permission) end @@ -766,10 +765,10 @@ function pace.FillCombatBanPanel(pnl) pac.global_combat_whitelist[string.lower(line:GetColumnText( 2 ))].permission = state PrintTable(pac.global_combat_whitelist) end - + local ban_confirm_list_button = vgui.Create("DButton", BAN) ban_confirm_list_button:SetText("Send combat ban list update to server") - + ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!") ban_confirm_list_button:SetColor(Color(255,0,0)) ban_confirm_list_button:SetSize(200, 40) @@ -803,7 +802,7 @@ function pace.FillCombatBanPanel(pnl) for id,data in pairs(pac.global_combat_whitelist) do combat_bans_temp_merger[id] = data end - + for id,data in pairs(combat_bans_temp_merger) do ban_list:AddLine(data.nick,data.steamid,data.permission) end @@ -874,7 +873,7 @@ function pace.FillCombatSettings(pnl) sv_hard_ent_limit_numbox:SetSize(400,30) sv_hard_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_combat_operation") sv_hard_ent_limit_numbox:SetTooltip("If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") - + local sv_per_player_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) sv_per_player_ent_limit_numbox:SetText("Entity limit per player to cutoff damage zones and force parts") sv_per_player_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_player_per_combat_operation"):GetInt()) @@ -890,7 +889,7 @@ function pace.FillCombatSettings(pnl) sv_player_fraction_slider:SetSize(400,30) sv_player_fraction_slider:SetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone") sv_player_fraction_slider:SetTooltip("This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.") - + local sv_distance_slider = vgui.Create("DNumSlider", general_list_list) sv_distance_slider:SetText("distance to block combat actions that are too far") sv_distance_slider:SetValue(GetConVar("pac_sv_combat_distance_enforced"):GetFloat()) @@ -898,7 +897,7 @@ function pace.FillCombatSettings(pnl) sv_distance_slider:SetSize(400,30) sv_distance_slider:SetConVar("pac_sv_combat_distance_enforced") sv_distance_slider:SetTooltip("The distance is compared between the action's origin and the player's position.\n0 to ignore.") - + end do --hitscan @@ -1040,7 +1039,7 @@ function pace.FillCombatSettings(pnl) sv_dmgzone_allow_dissolve_box:SetText("Allow damage entity dissolvers") sv_dmgzone_allow_dissolve_box:SetSize(400,30) sv_dmgzone_allow_dissolve_box:SetConVar("pac_sv_damage_zone_allow_dissolve") - + end do --lock part @@ -1055,7 +1054,7 @@ function pace.FillCombatSettings(pnl) sv_lock_allow_box:SetText("Allow lock part") sv_lock_allow_box:SetSize(400,30) sv_lock_allow_box:SetConVar("pac_sv_lock") - + local sv_lock_grab_box = vgui.Create("DCheckBoxLabel", lock_list_list) sv_lock_grab_box:SetText("Allow lock part grabbing") sv_lock_grab_box:SetSize(400,30) @@ -1163,7 +1162,7 @@ function pace.FillServerSettings(pnl) local master_list = vgui.Create("DCategoryList", pnl) master_list:Dock(FILL) - + --models/entity --[[ pac_allow_blood_color @@ -1172,7 +1171,7 @@ function pace.FillServerSettings(pnl) pac_modifier_model pac_modifier_size ]] - + local model_category = master_list:Add("Allowed Playermodel Mutations") model_category.Header:SetSize(40,40) model_category.Header:SetFont("DermaLarge") @@ -1205,12 +1204,12 @@ function pace.FillServerSettings(pnl) pac_modifier_size_box:SetSize(400,30) pac_modifier_size_box:SetConVar("pac_modifier_size") model_category_list:Add(pac_modifier_size_box) - + --movement and mass --[[ pac_free_movement ]] - + local movement_category = master_list:Add("Player Movement") movement_category.Header:SetSize(40,40) movement_category.Header:SetFont("DermaLarge") @@ -1248,9 +1247,9 @@ function pace.FillServerSettings(pnl) --pac_allow_movement_form.form = generic_form("PAC player movement is enabled.") end end - + --mode:ChooseOption(mode_str) - + local pac_player_movement_allow_mass_box = vgui.Create("DCheckBoxLabel", movement_category_list) pac_player_movement_allow_mass_box:SetText("Allow Modify Mass") pac_player_movement_allow_mass_box:SetSize(400,30) @@ -1264,7 +1263,7 @@ function pace.FillServerSettings(pnl) playermovement_min_mass_numbox:SetSize(400,30) movement_category_list:Add(playermovement_min_mass_numbox) playermovement_min_mass_numbox:SetConVar("pac_player_movement_min_mass") - + local playermovement_max_mass_numbox = vgui.Create("DNumSlider", movement_category_list) playermovement_max_mass_numbox:SetText("Maximum mass players can set for themselves") @@ -1273,7 +1272,7 @@ function pace.FillServerSettings(pnl) playermovement_max_mass_numbox:SetSize(400,30) movement_category_list:Add(playermovement_max_mass_numbox) playermovement_max_mass_numbox:SetConVar("pac_player_movement_max_mass") - + local pac_player_movement_allow_mass_dmgscaling_box = vgui.Create("DCheckBoxLabel", movement_category_list) pac_player_movement_allow_mass_dmgscaling_box:SetText("Allow damage scaling of physics damage based on player's mass") @@ -1282,7 +1281,7 @@ function pace.FillServerSettings(pnl) pac_player_movement_allow_mass_dmgscaling_box:SetConVar("pac_player_movement_physics_damage_scaling") movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) - + --wear limits and bans --[[ pac_sv_draw_distance @@ -1292,7 +1291,7 @@ function pace.FillServerSettings(pnl) pac_ban pac_unban ]] - + local wear_list = master_list:Add("Server wearing/drawing") wear_list.Header:SetSize(40,40) wear_list.Header:SetFont("DermaLarge") @@ -1320,7 +1319,7 @@ function pace.FillServerSettings(pnl) pac_submit_spam_box:SetConVar("pac_submit_spam") - + --misc --[[ sv_pac_webcontent_allow_no_content_length @@ -1344,7 +1343,7 @@ function pace.FillServerSettings(pnl) contraption_box:SetText("allow contraptions") contraption_box:SetSize(400,30) contraption_box:SetConVar("pac_to_contraption_allow") - + local contraption_entities_numbox = vgui.Create("DNumSlider", misc_list_list) contraption_entities_numbox:SetText("PAC3 contraption entities limit") contraption_entities_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) @@ -1356,7 +1355,7 @@ function pace.FillServerSettings(pnl) cam_restrict_box:SetText("restrict PAC editor camera movement") cam_restrict_box:SetSize(400,30) cam_restrict_box:SetConVar("pac_restrictions") - + return master_list end @@ -1389,7 +1388,7 @@ function pace.FillEditorSettings(pnl) partmenu_apply_button:SetY(10) partmenu_apply_button:SetWidth(65) partmenu_apply_button:SetImage('icon16/accept.png') - + local partmenu_clearlist_button = vgui.Create("DButton", LeftPanel) partmenu_clearlist_button:SetText("Clear") partmenu_clearlist_button:SetX(285) @@ -1403,7 +1402,7 @@ function pace.FillEditorSettings(pnl) partmenu_savelist_button:SetY(10) partmenu_savelist_button:SetWidth(70) partmenu_savelist_button:SetImage('icon16/disk.png') - + local partmenu_choices = vgui.Create("DScrollPanel", LeftPanel) @@ -1433,7 +1432,7 @@ function pace.FillEditorSettings(pnl) partmenu_previews:SetWidth(200) - + local shortcutaction_choices = vgui.Create("DComboBox", LeftPanel) shortcutaction_choices:SetText("Select a PAC action") for _,name in ipairs(pace.PACActionShortcut_Dictionary) do @@ -1443,12 +1442,12 @@ function pace.FillEditorSettings(pnl) shortcutaction_choices:SetWidth(170) shortcutaction_choices:SetHeight(20) shortcutaction_choices:ChooseOptionID(1) - + function shortcutaction_choices:Think() self.next = self.next or 0 self.found = self.found or false if self.next < RealTime() then self.found = false end - if self:IsHovered() then + if self:IsHovered() then if input.IsKeyDown(KEY_UP) then if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() + 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end elseif input.IsKeyDown(KEY_DOWN) then @@ -1457,7 +1456,7 @@ function pace.FillEditorSettings(pnl) else self.found = false end end - + local shortcuts_description_text = vgui.Create("DLabel", LeftPanel) shortcuts_description_text:SetFont("DermaDefaultBold") shortcuts_description_text:SetText("Edit keyboard shortcuts") @@ -1491,7 +1490,7 @@ function pace.FillEditorSettings(pnl) end end end - + local shortcutaction_choices_textCurrentShortcut = vgui.Create("DLabel", LeftPanel) shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit:") @@ -1499,8 +1498,8 @@ function pace.FillEditorSettings(pnl) shortcutaction_choices_textCurrentShortcut:SetWidth(200) shortcutaction_choices_textCurrentShortcut:SetX(200) shortcutaction_choices_textCurrentShortcut:SetY(420) - - + + local shortcutaction_index = vgui.Create("DNumberWang", LeftPanel) shortcutaction_index:SetToolTip("index") shortcutaction_index:SetValue(1) @@ -1516,7 +1515,7 @@ function pace.FillEditorSettings(pnl) num = tonumber(num) local action, val = shortcutaction_choices:GetSelected() local strs = {} - + if action and action ~= "" then if pace.PACActionShortcut[action] and pace.PACActionShortcut[action][num] then for i,v in ipairs(pace.PACActionShortcut[action][num]) do @@ -1577,7 +1576,7 @@ function pace.FillEditorSettings(pnl) local function send_active_shortcut_to_assign(tbl) local action = shortcutaction_choices:GetValue() local index = shortcutaction_index:GetValue() - + if not tbl then pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {} pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {} @@ -1655,7 +1654,7 @@ function pace.FillEditorSettings(pnl) bindcapture_text:SetX(300) bindcapture_text:SetY(480) bindcapture_text:SetSize(300, 30) - + function bindcapture_text:Think() self:SetText(pace.bindcapturelabel_text) end @@ -1689,7 +1688,7 @@ function pace.FillEditorSettings(pnl) end end pace.bindcapturelabel_text = "Recording input:\n" .. inputs_str - + if previous_inputs_tbl and table.Count(previous_inputs_tbl) > 0 then if table.Count(inputs_tbl) < table.Count(previous_inputs_tbl) then pace.FlashNotification("ending input!" .. previous_inputs_str) @@ -1711,7 +1710,7 @@ function pace.FillEditorSettings(pnl) previous_inputs_str = inputs_str previous_inputs_tbl = inputs_tbl end) - + end local bulkbinder = vgui.Create("DBinder", LeftPanel) @@ -1734,7 +1733,7 @@ function pace.FillEditorSettings(pnl) end end - local function FindImage(option_name) + local function FindImage(option_name) if option_name == "save" then return pace.MiscIcons.save elseif option_name == "load" then @@ -1771,7 +1770,7 @@ function pace.FillEditorSettings(pnl) return pace.MiscIcons.uniqueid elseif option_name == "help_part_info" then return 'icon16/information.png' - elseif option_name == "reorder_movables" then + elseif option_name == "reorder_movables" then return 'icon16/application_double.png' end return 'icon16/world.png' @@ -1793,11 +1792,11 @@ function pace.FillEditorSettings(pnl) pnl:SetWidth(200) pnl:SetY(20*(i-1)) end - + partmenu_choices:SetWidth(200) partmenu_choices:SetHeight(320) partmenu_choices:SetVerticalScrollbarEnabled(true) - + local RightPanel = vgui.Create( "DTree", f ) Test_Node = RightPanel:AddNode( "Test", "icon16/world.png" ) @@ -1815,7 +1814,7 @@ function pace.FillEditorSettings(pnl) pace.OnPartMenu(test_part) temp_list = pace.operations_order pace.operations_order = temp_list - end + end test_part:Remove() //dumb workaround but it works @@ -1823,7 +1822,7 @@ function pace.FillEditorSettings(pnl) div:Dock( FILL ) div:SetLeft( LeftPanel ) div:SetRight( RightPanel ) - + div:SetDividerWidth( 8 ) div:SetLeftMin( 50 ) div:SetRightMin( 50 ) @@ -1848,11 +1847,11 @@ function pace.FillEditorSettings(pnl) buildlist_partmenu = temp_list end - function partmenu_apply_button:DoClick() + function partmenu_apply_button:DoClick() pace.operations_order = buildlist_partmenu end - function partmenu_clearlist_button:DoClick() + function partmenu_clearlist_button:DoClick() ClearPartMenuPreviewList() buildlist_partmenu = {} end @@ -1863,7 +1862,7 @@ function pace.FillEditorSettings(pnl) function partmenu_previews:DoDoubleClick(id, line) table.remove(buildlist_partmenu,id) - + ClearPartMenuPreviewList() for i,v in ipairs(buildlist_partmenu) do partmenu_previews:AddLine(i,v) @@ -1872,7 +1871,7 @@ function pace.FillEditorSettings(pnl) PrintTable(buildlist_partmenu) end - + if pace.operations_order then for i,v in pairs(pace.operations_order) do table.insert(buildlist_partmenu,v) @@ -1888,17 +1887,17 @@ function pace.FillEditorSettings2(pnl) local panel = vgui.Create( "DPanel", pnl ) --[[ movement binds CreateConVar("pac_editor_camera_forward_bind", "w") - + CreateConVar("pac_editor_camera_back_bind", "s") - + CreateConVar("pac_editor_camera_moveleft_bind", "a") - + CreateConVar("pac_editor_camera_moveright_bind", "d") - + CreateConVar("pac_editor_camera_up_bind", "space") - + CreateConVar("pac_editor_camera_down_bind", "") - + ]] --[[pace.camera_movement_binds = { @@ -2006,8 +2005,8 @@ function pace.FillEditorSettings2(pnl) function speed_binder:OnChange(num) pace.camera_movement_binds["speed"]:SetString(input.GetKeyName( num )) end - - --[[pace.partmenu_categories_cedrics = + + --[[pace.partmenu_categories_cedrics = { ["new!"] = { @@ -2044,9 +2043,9 @@ function pace.FillEditorSettings2(pnl) else return "icon16/page_white.png" end - + end - + local categorytree = vgui.Create("DTree", RightPanel) categorytree:SetY(30) categorytree:SetSize(360,400) @@ -2073,7 +2072,7 @@ function pace.FillEditorSettings2(pnl) local base = vgui.Create("EditablePanel") base:SetPos(input.GetCursorPos()) base:SetSize(200, 300) - + base:MakePopup() function base:OnRemove() @@ -2138,7 +2137,7 @@ function pace.FillEditorSettings2(pnl) --base:SetHeight(20 * #result.found + edit:GetTall()) base:SetHeight(600 + edit:GetTall()) - + end edit:OnValueChange("") @@ -2194,10 +2193,10 @@ function pace.FillEditorSettings2(pnl) if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end end frame:MakePopup() - + frame:SetSize(300,30) frame:SetPos(input.GetCursorPos()) - + edit:Dock(TOP) edit:RequestFocus() edit:SetUpdateOnType(true) @@ -2214,20 +2213,20 @@ function pace.FillEditorSettings2(pnl) if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end end frame:MakePopup() - + frame:SetSize(300,30) frame:SetPos(category_node.Label:LocalToScreen(category_node.Label:GetPos())) - + edit:Dock(TOP) edit:RequestFocus() edit:SetUpdateOnType(true) end - + local function load_partgroup_template_into_tree(categorytree, tbl) tbl = tbl or pace.partgroups or pace.partmenu_categories_default categorytree:Clear() for category,category_contents in pairs(tbl) do - + local category_node = categorytree:AddNode(category) category_node:SetIcon(get_icon(category_contents.icon, category)) @@ -2287,7 +2286,7 @@ function pace.FillEditorSettings2(pnl) part_categories_presets:AddChoice(string.GetFileFromFilename(filename)) end end - + part_categories_presets:SetX(10) part_categories_presets:SetY(10) part_categories_presets:SetWidth(170) part_categories_presets:SetHeight(20) @@ -2365,7 +2364,7 @@ function pace.ConfigureEventWheelMenu() master_panel:Center() local mid_panel = vgui.Create("DPanel", master_panel) mid_panel:Dock(FILL) - + local scr_pnl = vgui.Create("DScrollPanel", mid_panel) scr_pnl:SetSize(490,800) scr_pnl:SetPos(0,45) @@ -2374,7 +2373,7 @@ function pace.ConfigureEventWheelMenu() local first_panel = vgui.Create("DPanel", mid_panel) first_panel:SetSize(500,40) first_panel:Dock(TOP) - + local circle_style_listmenu = vgui.Create("DComboBox",first_panel) circle_style_listmenu:SetText("Choose eventwheel style") circle_style_listmenu:SetSize(150,20) @@ -2407,7 +2406,7 @@ function pace.ConfigureEventWheelMenu() GetConVar("pac_eventwheel_clickmode"):SetString("1") end end - + local rectangle_style_listmenu = vgui.Create("DComboBox",first_panel) rectangle_style_listmenu:SetText("Choose eventlist style") @@ -2485,12 +2484,12 @@ function pace.ConfigureEventWheelMenu() events[cmd] = cmd end end - + end local names = table.GetKeys( events ) table.sort(names, function(a, b) return a < b end) - + local copied_color = nil local lanes = {} local colorpanel @@ -2506,7 +2505,7 @@ function pace.ConfigureEventWheelMenu() for _, name in ipairs(names) do local pnl = vgui.Create("DPanel") list:Add(pnl) pnl:SetSize(400,20) local btn = vgui.Create("DButton", pnl) - + btn:SetSize(200,25) btn:SetText(name) btn:SetTooltip(name) @@ -2533,13 +2532,13 @@ function pace.ConfigureEventWheelMenu() local str_tbl = string.Split(pace.command_colors[name], " ") clr_pnl:SetBaseColor(Color(tonumber(str_tbl[1]),tonumber(str_tbl[2]),tonumber(str_tbl[3]))) end - + clr_frame:SetSize(300,200) clr_pnl:Dock(FILL) clr_frame:SetPos(self:LocalToScreen(0,0)) clr_frame:RequestFocus() function clr_pnl:Think() if input.IsMouseDown(MOUSE_LEFT) then - + if not IsValid(vgui.GetHoveredPanel()) then self:Remove() clr_frame:Remove() else @@ -2557,7 +2556,7 @@ function pace.ConfigureEventWheelMenu() pace.command_colors[name] = col.r .. " " .. col.g .. " " .. col.b btn:SetColor(col) end - + end local copypastebutton = vgui.Create("DButton", pnl) @@ -2604,7 +2603,7 @@ function pace.ConfigureEventWheelMenu() if pace.event_wheel_list_opened then pac.closeEventSelectionList(true) end if pace.event_wheel_opened then pac.closeEventSelectionWheel(true) end end - + master_panel:RequestFocus() gui.EnableScreenClicker(true) pace.command_event_menu_opened = master_panel diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 14c14ac35..f4822e1b5 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -74,7 +74,7 @@ pace.PACActionShortcut_Default = { ["hide_editor"] = { [1] = {"CTRL", "e"} }, - + ["help_info_popup"] = { [1] = {"F1"} }, @@ -97,10 +97,10 @@ pace.PACActionShortcut_Default = { [1] = {"DEL"} }, ["expand_all"] = { - + }, ["collapse_all"] = { - + }, ["undo"] = { [1] = {"CTRL", "z"} @@ -351,9 +351,9 @@ function pace.LookupShortcutsForAction(action, provided_inputs, do_it) end return true end - + local function shortcut_contains_counterexample(combo, action, inputs) - + local counterexample = false for key,bool in ipairs(inputs) do --check the input for counter-examples if input.IsKeyDown(key) then @@ -361,7 +361,7 @@ function pace.LookupShortcutsForAction(action, provided_inputs, do_it) --some keys don't count as counterexamples?? --random windows or capslocktoggle keys being pressed screw up the input --bulk select should allow rolling select with the scrolling options - + if key == pace.BulkSelectKey and not action == "editor_up" and not action == "editor_down" and not action == "editor_pageup" and not action == "editor_pagedown" then counterexample = true elseif not pace.passthrough_keys[key] and key ~= pace.BulkSelectKey then @@ -372,12 +372,12 @@ function pace.LookupShortcutsForAction(action, provided_inputs, do_it) counterexample = false end end - + end end return counterexample end - + if not pace.PACActionShortcut[action] then return false end local final_success = false @@ -390,7 +390,7 @@ function pace.LookupShortcutsForAction(action, provided_inputs, do_it) if pace.PACActionShortcut[action][i] then --is there a combo in that slot combo = pace.PACActionShortcut[action][i] local keynames_str = "" - + local single_match = false if input_contains_one_match(combo, action, provided_inputs) then single_match = true @@ -406,13 +406,13 @@ function pace.LookupShortcutsForAction(action, provided_inputs, do_it) end end end - + return final_success end function pace.AssignEditorShortcut(action, tbl, index) print("received a new shortcut assignation") - + pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {} pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {} @@ -523,10 +523,10 @@ function pace.DoShortcutFunc(action) icon:SetImage(pace.MiscIcons.load) add_expensive_submenu_load(icon, function() pace.AddSavedPartsToMenu(menu, true) end) end - + menu:SetMaxHeight(ScrH() - y) menu:MakePopup() - + end if action == "wear" then pace.Call("ShortcutWear") end @@ -535,25 +535,25 @@ function pace.DoShortcutFunc(action) if action == "panic" then pac.Panic() end if action == "restart" then RunConsoleCommand("pac_restart") end if action == "collapse_all" then - + local part = pace.current_part if not part or not part:IsValid() then pace.FlashNotification('No part to collapse') else - + end part:CallRecursive('SetEditorExpand', GetConVar("pac_reverse_collapse"):GetBool()) pace.RefreshTree(true) end if action == "expand_all" then - + local part = pace.current_part if not part or not part:IsValid() then pace.FlashNotification('No part to collapse') else - + end part:CallRecursive('SetEditorExpand', not GetConVar("pac_reverse_collapse"):GetBool()) pace.RefreshTree(true) @@ -571,7 +571,7 @@ function pace.DoShortcutFunc(action) pace.properties.search:SetEnabled(true) pace.property_searching = true end - + end if action == "property_search_in_tree" then if pace.tree_search_open then @@ -605,7 +605,7 @@ function pace.DoShortcutFunc(action) local x,y = input.GetCursorPos() menu:SetPos(x,y) pace.PopulateMenuBarTab(menu, "player") - + end if action == "toolbar_view" then menu = DermaMenu() @@ -617,7 +617,7 @@ function pace.DoShortcutFunc(action) if action == "zoom_panel" then pace.PopupMiniFOVSlider() end - + if action == "T_Pose" then pace.SetTPose(not pace.GetTPose()) end if action == "bulk_select" then @@ -655,7 +655,7 @@ function pace.DoShortcutFunc(action) if pace.floating_popup_reserved then pace.floating_popup_reserved:Remove() end - + --[[pac.InfoPopup("Looks like you don't have an active part. You should right click and go make one to get started", { obj_type = "screen", clickfunc = function() pace.OnAddPartMenu(pace.current_part) end, @@ -675,28 +675,28 @@ function pace.DoShortcutFunc(action) --obj_type types local popup_prefered_type = GetConVar("pac_popups_preferred_location"):GetString() popup_setup_tbl.obj_type = popup_prefered_type - + if popup_prefered_type == "pac tree label" then popup_setup_tbl.obj = pace.current_part.pace_tree_node pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) - + elseif popup_prefered_type == "part world" then popup_setup_tbl.obj = pace.current_part pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) - + elseif popup_prefered_type == "screen" then pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, ScrW()/2, ScrH()/2) - + elseif popup_prefered_type == "cursor" then pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, input.GetCursorPos()) - + elseif popup_prefered_type == "editor bar" then popup_setup_tbl.obj = pace.Editor pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) - + end - - + + --[[if IsValid(pace.current_part) then @@ -775,7 +775,7 @@ function pace.CheckShortcuts() pace.properties.search:RequestFocus() pace.properties.search:SetEnabled(true) pace.property_searching = true - + last = RealTime() + 0.2 else pace.OpenTreeSearch() @@ -799,7 +799,7 @@ function pace.CheckShortcuts() pace.legacy_floating_popup_reserved = nil pace.legacy_floating_popup_reserved_part = nil end - + local popup_setup_tbl = { obj_type = "", clickfunc = function() pace.OnAddPartMenu(pace.current_part) end, @@ -808,10 +808,10 @@ function pace.CheckShortcuts() panel_exp_width = 900, panel_exp_height = 400, from_legacy = true } - + popup_setup_tbl.obj_type = "pac tree label" popup_setup_tbl.obj = pace.current_part.pace_tree_node - + if new_popup then local created_panel = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) pace.legacy_floating_popup_reserved = created_panel @@ -842,30 +842,30 @@ function pace.CheckShortcuts() inputs_str = inputs_str .. input.GetKeyName(i) .. " " else input_active[i] = false end end - + if previous_inputs_str ~= inputs_str then if last + 0.2 > RealTime() and has_run_something then skip = true else has_run_something = false end - + end if no_input then skip = false end previous_inputs_str = inputs_str - + if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end if gui.IsConsoleVisible() then return end if not pace.Editor or not pace.Editor:IsValid() then return end - - + + if skip and not no_input_override then return end - + local starttime = SysTime() - + for action,list_of_lists in pairs(pace.PACActionShortcut) do if not has_run_something then if (action == "hide_editor" or action == "hide_editor_visible") and pace.LookupShortcutsForAction(action, input_active, true) then --we can focus back if editor is not focused @@ -882,7 +882,7 @@ function pace.CheckShortcuts() end end end - + end pac.AddHook("Think", "pace_shortcuts", pace.CheckShortcuts) @@ -1072,7 +1072,7 @@ do end pac.AddHook("Think", "pace_keyboard_shortcuts", function() - + if not pace.IsActive() then return end if not pace.Focused then return end if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end diff --git a/lua/pac3/editor/client/tools.lua b/lua/pac3/editor/client/tools.lua index 330eea2e3..0445d31b1 100644 --- a/lua/pac3/editor/client/tools.lua +++ b/lua/pac3/editor/client/tools.lua @@ -823,16 +823,16 @@ end) --aka pace.UltraCleanup pace.AddTool(L"Destroy hidden parts, proxies and events", function(part) - + if not part then part = pace.current_part end root = part:GetRootPart() - + pnl = Derma_Query("Only do this if you know what you're doing!\nMark parts as important in their notes to protect them.", "Warning", "Destroy!", function() pace.UltraCleanup( root ) end, "cancel", nil ) pnl:SetWidth(300) - + end) pace.AddTool(L"stop all custom animations", function() diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 3bc04a55f..59fb2ea34 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -627,7 +627,7 @@ function pace.PopupMiniFOVSlider() zoomframe.zoomslider:SetDefaultValue( 75 ) zoomframe.zoomslider:SetValue( pace.ViewFOV ) - + function zoomframe:Think(...) pace.ViewFOV = zoomframe.zoomslider:GetValue() if zoom_smooth:GetInt() == 1 then @@ -654,5 +654,5 @@ function pace.PopupMiniFOVSlider() end end end) - + end diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index c6120482e..b63837dc9 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -280,12 +280,12 @@ do local pnl = Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)", "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() pac.Message("Wearing autoload...") - pace.LoadParts("autoload") + pace.LoadParts("autoload") pace.WearParts() end, "load latest outfit : pac3/" .. latest_outfit .. " " .. string.NiceSize(file.Size("pac3/" .. latest_outfit, "DATA")), function() - + if latest_outfit and file.Exists("pac3/" .. latest_outfit, "DATA") then pac.Message("Wearing latest outfit...") pace.LoadParts(latest_outfit, true) @@ -303,7 +303,7 @@ do local pnl = Derma_Query("Do you want to load an outfit?", "PAC3 autoload (pac_prompt_for_autoload)", "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() pac.Message("Wearing autoload...") - pace.LoadParts("autoload") + pace.LoadParts("autoload") pace.WearParts() end, @@ -328,7 +328,7 @@ do local pnl = Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)", "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() pac.Message("Wearing autoload...") - pace.LoadParts("autoload") + pace.LoadParts("autoload") pace.WearParts() end, diff --git a/lua/pac3/editor/client/wires.lua b/lua/pac3/editor/client/wires.lua index 3f7dc9ded..517e49f59 100644 --- a/lua/pac3/editor/client/wires.lua +++ b/lua/pac3/editor/client/wires.lua @@ -211,68 +211,68 @@ hook.Add("PostRenderVGUI", "beams", function() if part.ClassName == "proxy" and part.valid_parts_in_expression then for _, part2 in pairs(part.valid_parts_in_expression) do - + local info = part.pace_tree_node --if info.udata.part_key then --if info.udata.part_key == "Parent" then continue end - + local from = part local to = part2 --print("part: ", part, "\nfrom: ", from, "\nto: ", to) if not to:IsValid() then continue end - - + + local from_pnl = from.pace_properties["Expression"] local to_pnl = to.pace_tree_node or NULL - + if not from_pnl:IsValid() then continue end if not to_pnl:IsValid() then continue end - + local params = {} - + params["$basetexture"] = to.Icon or "gui/colors.png" params["$vertexcolor"] = 1 params["$vertexalpha"] = 1 params["$nocull"] = 1 - + local path = to_pnl:GetModel() if path then path = "spawnicons/" .. path:sub(1, -5) .. "_32" params["$basetexture"] = path end - - + + local mat = CreateMaterial("pac_wire_icon_" .. params["$basetexture"], "UnlitGeneric", params) - + render.SetMaterial(mat) - + local fx,fy = from_pnl:LocalToScreen(from_pnl:GetWide(), from_pnl:GetTall() / 2) - + local tx,ty = to_pnl.Icon:LocalToScreen(0,to_pnl.Icon:GetTall() / 2) - + do local x,y = pace.tree:LocalToScreen(0,0) local w,h = pace.tree:LocalToScreen(pace.tree:GetSize()) - + tx = math.Clamp(tx, x, w) ty = math.Clamp(ty, y, h) end - + from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover or 0 - + if from_pnl:IsHovered() or (from.pace_tree_node and from.pace_tree_node:IsValid() and from.pace_tree_node.Label:IsHovered()) then from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (5 - from_pnl.wire_smooth_hover) * FrameTime() * 20 else from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (0 - from_pnl.wire_smooth_hover) * FrameTime() * 20 end - + from_pnl.wire_smooth_hover = math.Clamp(from_pnl.wire_smooth_hover, 0, 5) - + if from_pnl.wire_smooth_hover > 0.01 then draw_hermite(0,0,ScrW(),ScrH(), from_pnl.wire_smooth_hover, fx,fy, tx,ty, Color(255,255,255), Color(255,255,255, 255), 1) end --end end end - + end) diff --git a/lua/pac3/editor/server/bans.lua b/lua/pac3/editor/server/bans.lua index 101be43fe..f686c523b 100644 --- a/lua/pac3/editor/server/bans.lua +++ b/lua/pac3/editor/server/bans.lua @@ -135,7 +135,7 @@ net.Receive("pac.RequestBanStates", function(len,ply) pac.Message("Received ban list request from : ", ply) pac.Message("Time : ", os.date( "%a %X %x", os.time() )) local players = {} - for _,v in pairs(player.GetAll()) do + for _,v in pairs(player.GetAll()) do players[v] = false end if not pace.Bans then diff --git a/lua/pac3/editor/server/init.lua b/lua/pac3/editor/server/init.lua index 282dc2920..32512077d 100644 --- a/lua/pac3/editor/server/init.lua +++ b/lua/pac3/editor/server/init.lua @@ -44,24 +44,24 @@ function pace.CanPlayerModify(ply, ent) if ent.CPPIGetOwner and ent:CPPIGetOwner() == ply then return true end - + if GetConVar("pac_sv_prop_outfits"):GetInt() ~= 0 then if GetConVar("pac_sv_prop_outfits"):GetInt() == 1 then return not (ply ~= ent and ent:IsPlayer()) elseif GetConVar("pac_sv_prop_outfits"):GetInt() == 2 then return true end - + end - + do local tr = util.TraceLine({ start = ply:EyePos(), endpos = ent:WorldSpaceCenter(), filter = ply }) if tr.Entity == ent and hook.Run("CanTool", ply, tr, "paint") == true then return true end end - - + + return false end diff --git a/lua/pac3/editor/server/pac_settings_manager.lua b/lua/pac3/editor/server/pac_settings_manager.lua index 0a31bc019..af640e228 100644 --- a/lua/pac3/editor/server/pac_settings_manager.lua +++ b/lua/pac3/editor/server/pac_settings_manager.lua @@ -6,7 +6,7 @@ net.Receive("pac_send_sv_cvar", function(len,ply) local cmd = net.ReadString() local val = net.ReadString() --if cmd == "" then - + --end GetConVar(cmd):SetString(val) print("[PAC3]: Admin "..ply:GetName().." set "..cmd.." to "..val) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 7d002465c..dea49eed4 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -85,7 +85,7 @@ local contraption_classes = { local pre_excluded_ent_classes = { ["info_player_start"] = true, - ["aoc_spawnpoint"] = true, + ["aoc_spawnpoint"] = true, ["info_player_teamspawn"] = true, ["env_tonemap_controller"] = true, ["env_fog_controller"] = true, @@ -170,7 +170,7 @@ do --define a basic class for the bullet emitters local ENT = {} ENT.Type = "anim" ENT.ClassName = "pac_bullet_emitter" - ENT.Spawnable = false + ENT.Spawnable = false scripted_ents.Register(ENT, "pac_bullet_emitter") end @@ -216,7 +216,7 @@ if SERVER then hook.Add("PlayerSpawnedSWEP", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) hook.Add("PlayerSpawnedVehicle", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) hook.Add("PlayerSpawnedEffect", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) - + local function IsPossibleContraptionEntity(ent) if not IsValid(ent) then return false end local b = (string.find(ent:GetClass(), "phys") ~= nil @@ -228,7 +228,7 @@ if SERVER then end local function IsPropProtected(ent, ply) - + local reason = "" local pac_sv_prop_protection = global_combat_prop_protection:GetBool() @@ -254,7 +254,7 @@ if SERVER then if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Allowed" then return true end if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end end - + if global_combat_whitelisting:GetBool() then --if server uses the high-trust whitelisting mode if pac.global_combat_whitelist[string.lower(ply:SteamID())] then if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission ~= "Allowed" then return false end --if player is not in whitelist, stop! @@ -264,7 +264,7 @@ if SERVER then if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end --if player is in blacklist, stop! end end - + return true end @@ -324,7 +324,7 @@ if SERVER then net.Send(ent) ent.has_calcview = false end - + elseif ent:IsNPC() then if bool then active_grabbed_ents[ent] = true @@ -353,7 +353,7 @@ if SERVER then end end end - + ent:PhysWake() ent:SetGravity(1) end @@ -383,7 +383,7 @@ if SERVER then local function FixMaxHealths(ply) local biggest_health = 0 local biggest_armor = 0 - local found_armor = false + local found_armor = false local found_health = false if ply.pac_healthmods then @@ -392,7 +392,7 @@ if SERVER then if tbl.maxarmor then biggest_armor = math.max(biggest_armor,tbl.maxarmor) found_armor = true end end end - + if found_health then ply:SetMaxHealth(biggest_health) else @@ -476,18 +476,18 @@ if SERVER then end local function CalculateHealthBarUIDCombinedHP(ply, uid) - + end local function CalculateHealthBarLayerCombinedHP(ply, layer) - + end - + local function GatherExtraHPBars(ply) if not ply.pac_healthbars then return 0,nil end local built_tbl = {} local total_hp_value = 0 - + for layer,tbl in pairs(ply.pac_healthbars) do built_tbl[layer] = {} local layer_total = 0 @@ -498,7 +498,7 @@ if SERVER then end end return total_hp_value,built_tbl - + end --simulate on a healthbar layers copy @@ -529,7 +529,7 @@ if SERVER then local remainder = math.max(0,remaining_dmg - BARS_COPY[layer][uid]) local breakthrough_dmg = math.min(remaining_dmg, value) - + if remaining_dmg > value then --break through one of the uid clusters surviving_layer = layer - 1 BARS_COPY[layer][uid] = 0 @@ -539,10 +539,10 @@ if SERVER then local absorbfactor = ply.pac_healtbar_uid_absorbfactor[uid] side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor - + remaining_dmg = math.max(0,remaining_dmg - value) end - + end end end @@ -570,7 +570,7 @@ if SERVER then local remainder = math.max(0,remaining_dmg - ply.pac_healthbars[layer][uid]) local breakthrough_dmg = math.min(remaining_dmg, value) - + if remaining_dmg > value then --break through one of the uid clusters surviving_layer = layer - 1 ply.pac_healthbars[layer][uid] = 0 @@ -580,14 +580,14 @@ if SERVER then local absorbfactor = ply.pac_healtbar_uid_absorbfactor[uid] side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor - + remaining_dmg = math.max(0,remaining_dmg - value) end - + end end end - + return remaining_dmg,surviving_layer,side_effect_dmg end @@ -598,7 +598,7 @@ if SERVER then net.WriteTable(target.pac_healthbars) net.Broadcast() end - + --healthbars work with a 2 levels-deep table --for each player, an index table (priority) to decide which layer is damaged first --for each layer, one table for each part uid @@ -609,12 +609,12 @@ if SERVER then --ply.pac_healthbars[layer] --ply.pac_healthbars[layer][part_uid] = healthvalue - + --apply hitscan consents, eat into extra healthbars first and calculate final damage multipliers from pac3 hook.Add( "EntityTakeDamage", "ApplyPACDamageModifiers", function( target, dmginfo ) if target:IsPlayer() then local cumulative_mult = GatherDamageScales(target) - + dmginfo:ScaleDamage(cumulative_mult) local remaining_dmg,surviving_layer,side_effect_dmg = GetHPBarDamage(target, dmginfo:GetDamage()) @@ -642,9 +642,9 @@ if SERVER then dmginfo:SetDamage(side_effect_dmg + remaining_dmg) SendUpdateHealthBars(target) end - + end - + end end end) @@ -678,7 +678,7 @@ if SERVER then local pac_sv_damage_zone_allow_dissolve = GetConVar("pac_sv_damage_zone_allow_dissolve"):GetBool() local pac_sv_prop_protection = global_combat_prop_protection:GetBool() - + local inflictor = dmg_info:GetInflictor() local attacker = dmg_info:GetAttacker() @@ -701,7 +701,7 @@ if SERVER then local dissolvable = true local prop_protected, reason = IsPropProtected(ent, attacker) local prop_protected_final = prop_protected and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false - + if ent:IsPlayer() then if not kill then dissolvable = false elseif damage_zone_consents[ent] == false then dissolvable = false end @@ -737,7 +737,7 @@ if SERVER then --the giga function to determine if we can damage local function DMGAllowed(ent) - + if ent:Health() == 0 and not (string.find(tbl.DamageType, "dissolve")) then return false end --immediately exclude entities with 0 health, except if we want to dissolve local canhit = false --whether the policies allow the hit local prop_protected_consent = ent:GetCreator() ~= inflictor and ent ~= inflictor and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false-- and ent:GetCreator() ~= inflictor @@ -774,14 +774,14 @@ if SERVER then elseif (tbl.NPC and damageable_point_ent_classes[ent:GetClass()] ~= false) or (tbl.PointEntities and (damageable_point_ent_classes[ent:GetClass()] == true)) then canhit = true end - + --apply prop protection if IsPropProtected(ent, inflictor) or prop_protected_consent then canhit = false end - + end - + end return canhit @@ -814,30 +814,30 @@ if SERVER then for _,v in ipairs(ents_hits) do if v ~= ent then table.insert(ents2,v) end end - + traceresult = util.TraceLine({filter = ents2, start = pos, endpos = pos + 50000*(ent:WorldSpaceCenter() - dmg_info:GetAttacker():WorldSpaceCenter())}) bullet.Dir = traceresult.Normal bullet.Src = traceresult.HitPos + traceresult.HitNormal*5 dmg_info:GetInflictor():FireBullets(bullet) - + end - + if tbl.DamageType == "heal" then - + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) elseif tbl.DamageType == "armor" then ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) else --only "living" entities can be killed, and we checked generic entities with a ghost 0 health previously - + --now, after checking the de facto damage after extra healthbars, there's a 80% absorbtion ratio of armor. --so, the kill condition is either: --if damage is 500% of health (no amount will save you, because the remainder of 80% means death) --if damage is more than 125% of armor, and damage is more than health+armor if IsLiving(ent) and ent:Health() - de_facto_dmg <= 0 then if ent.Armor then - + if not (de_facto_dmg > 5*ent:Health()) and not (de_facto_dmg > 1.25*ent:Armor() and de_facto_dmg > ent:Health() + ent:Armor()) then kill = false else @@ -845,14 +845,14 @@ if SERVER then end else kill = true - end - + end + end if tbl.DoNotKill then kill = false --durr end if kill then successful_kill_ents[ent] = true end - + --remove weapons on kill if asked if kill and not ent:IsPlayer() and tbl.RemoveNPCWeaponsOnKill and pac_sv_damage_zone_allow_dissolve then if ent:IsNPC() then @@ -863,24 +863,24 @@ if SERVER then end end end - + --leave at a critical health if tbl.DoNotKill then local dmg_info2 = DamageInfo() - + dmg_info2:SetDamagePosition(ent:NearestPoint(pos)) dmg_info2:SetReportedPosition(pos) dmg_info2:SetDamage( math.min(ent:Health() - tbl.CriticalHealth, tbl.Damage)) dmg_info2:IsBulletDamage(tbl.Bullet) dmg_info2:SetDamageForce(Vector(0,0,0)) - + dmg_info2:SetAttacker(attacker) - + dmg_info2:SetInflictor(inflictor) ent:TakeDamageInfo(dmg_info2) max_dmg = math.max(max_dmg, dmg_info2:GetDamage()) - + --finally we reached the normal damage event! else if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) then @@ -892,7 +892,7 @@ if SERVER then max_dmg = math.max(max_dmg, dmg_info:GetDamage()) end end - + if tbl.DamageType == "fire" then ent:Ignite(5) end end @@ -906,7 +906,7 @@ if SERVER then --look through each entity for _,ent in pairs(ents_hits) do - + local canhit = DMGAllowed(ent) local oldhp = ent:Health() if canhit then @@ -920,7 +920,7 @@ if SERVER then end if not hit and (oldhp > 0 and canhit) then hit = true end end - + return hit,kill,dmg,successful_hit_ents,successful_kill_ents end @@ -970,15 +970,15 @@ if SERVER then radiation = 28, --radiation removenoragdoll = 29, --don't create a ragdoll on death slowburn = 30, -- - + fire = 31, -- ent:Ignite(5) - + -- env_entity_dissolver dissolve_energy = 32, dissolve_heavy_electrical = 33, dissolve_light_electrical = 34, dissolve_core_effect = 35, - + heal = 36, armor = 37, } @@ -1018,7 +1018,7 @@ if SERVER then or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner())) ) then - + local is_phys = true if ent:GetPhysicsObject() ~= nil then phys_ent = ent:GetPhysicsObject() @@ -1037,7 +1037,7 @@ if SERVER then else oldvel = Vector(0,0,0) end - + local addvel = Vector(0,0,0) local add_angvel = Vector(0,0,0) @@ -1093,7 +1093,7 @@ if SERVER then +ang2:Up()*tbl.AddedVectorForce.z end - + if tbl.TorqueMode == "Global" then add_angvel = tbl.Torque @@ -1105,7 +1105,7 @@ if SERVER then ang2 = dir:Angle() addvel = ang2:Forward()*tbl.Torque.x + ang2:Right()*tbl.Torque.y + ang2:Up()*tbl.Torque.z end - + local islocaltorque = tbl.TorqueMode == "TargetLocal" if is_phys and tbl.AccountMass then @@ -1132,7 +1132,7 @@ if SERVER then end damping_dist_mult = damping_dist_mult local final_damping = 1 - (tbl.Damping * damping_dist_mult) - + if tbl.Levitation then addvel.z = addvel.z * up_mult end @@ -1156,7 +1156,7 @@ if SERVER then if islocaltorque then phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) phys_ent:AddAngleVelocity(add_angvel) - + else phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) add_angvel = phys_ent:WorldToLocalVector( add_angvel ) @@ -1172,7 +1172,7 @@ if SERVER then local vec = oldvel + addvel local clamp_vec = vec:GetNormalized()*500 ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer - + else ent:SetVelocity((oldvel * final_damping) + addvel) end end elseif tbl.PointEntities then @@ -1183,7 +1183,7 @@ if SERVER then hook.Run("PhysicsUpdate", ent) hook.Run("PhysicsUpdate", phys_ent) end - + end end --first stage of force: look for targets and determine force amount if continuous @@ -1327,7 +1327,7 @@ if SERVER then --[[function net.Incoming( len, client ) - + local i = net.ReadHeader() local strName = util.NetworkIDToString( i ) if strName ~= "pac_in_editor_posang" and strName ~= "DrGBasePlayerLuminosity" then @@ -1335,17 +1335,17 @@ if SERVER then end if ( !strName ) then return end - + local func = net.Receivers[ strName:lower() ] if ( !func ) then return end - + -- -- len includes the 16 bit int which told us the message name -- len = len - 16 - + func( len, client ) - + end]] local force_hitbox_ids = {["Box"] = 0,["Cube"] = 1,["Sphere"] = 2,["Cylinder"] = 3,["Cone"] = 4,["Ray"] = 5} @@ -1361,7 +1361,7 @@ if SERVER then --server allow if not force_allow:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end - + local tbl = {} local pos = net.ReadVector() @@ -1373,10 +1373,10 @@ if SERVER then tbl.UniqueID = net.ReadString() if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then + if netrate_enforcement_sv_monitoring:GetBool() then MsgC(Color(255,255,0), "[PAC3] Force part: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") end - + hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) active_force_ids[tbl.UniqueID] = nil @@ -1463,7 +1463,7 @@ if SERVER then --print("removed an outdated force") end end - + end) end @@ -1477,7 +1477,7 @@ if SERVER then --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then + if netrate_enforcement_sv_monitoring:GetBool() then MsgC(Color(255,255,0), "[PAC3] Damage zone: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") end return @@ -1537,7 +1537,7 @@ if SERVER then if tbl.HitboxMode == "Sphere" then ents_hits = ents.FindInSphere(pos, tbl.Radius) - + elseif tbl.HitboxMode == "Box" or tbl.HitboxMode == "Cube" then local mins local maxs @@ -1550,7 +1550,7 @@ if SERVER then end ents_hits = ents.FindInBox(mins, maxs) - + elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then ents_hits = {} if tbl.Radius ~= 0 then @@ -1563,7 +1563,7 @@ if SERVER then steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1))))) end steps = math.max(steps + math.abs(tbl.ExtraSteps),1) - + for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier phase = math.random() local ray_thickness = math.Clamp(0.5*math.log(tbl.Radius) + 0.05*tbl.Radius,0,10)*(1 - 0.7*ringnumber) @@ -1592,7 +1592,7 @@ if SERVER then end end elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - + elseif tbl.HitboxMode == "CylinderSpheres" then ents_hits = {} if tbl.Length ~= 0 and tbl.Radius ~= 0 then @@ -1605,7 +1605,7 @@ if SERVER then end MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - + elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then ents_hits = {} if tbl.Radius ~= 0 then @@ -1624,7 +1624,7 @@ if SERVER then for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the ringnumber multiplier phase = math.random() local ray_thickness = 5 * (2 - ringnumber) - + for i=1,0,-1/sides do if ringnumber == 0 then i = 0 end x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize) @@ -1642,7 +1642,7 @@ if SERVER then end end elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - + elseif tbl.HitboxMode == "ConeSpheres" then ents_hits = {} local steps @@ -1654,7 +1654,7 @@ if SERVER then steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end - + elseif tbl.HitboxMode =="Ray" then local startpos = pos + Vector(0,0,0) local endpos = pos + ang:Forward()*tbl.Length @@ -1698,10 +1698,10 @@ if SERVER then if not lock_allow:GetBool() then return end if not lock_allow_grab:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end - + --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then + if netrate_enforcement_sv_monitoring:GetBool() then MsgC(Color(255,255,0), "[PAC3] Lock grab: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") end return @@ -1738,7 +1738,7 @@ if SERVER then end local prop_protected, reason = IsPropProtected(targ_ent, ply) - + local unconsenting_owner = targ_ent:GetCreator() ~= ply and (grab_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) local calcview_unconsenting = targ_ent:GetCreator() ~= ply and (calcview_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and calcview_consents[targ_ent] == false)) @@ -1772,7 +1772,7 @@ if SERVER then need_breakup = true breakup_condition = breakup_condition .. "non-consenting, " end - + --dead ent = break --but don't exclude about physics props if targ_ent:Health() == 0 and not (physics_point_ent_classes[targ_ent:GetClass()] or string.find(targ_ent:GetClass(),"item_") or string.find(targ_ent:GetClass(),"ammo_") or targ_ent:IsWeapon()) then @@ -1789,11 +1789,11 @@ if SERVER then breakup_condition = breakup_condition .. "mutual grab prevention, " end end - + end if did_grab then - + if targ_ent:IsPlayer() and targ_ent:InVehicle() then --yank player out of vehicle print("Kicking " .. targ_ent:Nick() .. " out of vehicle to be grabbed!") @@ -1805,7 +1805,7 @@ if SERVER then targ_ent:SetAngles(ang) else --players work with eyeangles if override_eyeang then - + if PlayerAllowsCalcView(targ_ent) and override_viewposition then targ_ent.nextcalcviewTick = targ_ent.nextcalcviewTick or CurTime() if targ_ent.nextcalcviewTick < CurTime() then @@ -1834,12 +1834,12 @@ if SERVER then targ_ent.has_calcview = false end end - + end end - + targ_ent:SetPos(pos) - + ApplyLockState(targ_ent, true, no_collide) if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end @@ -1864,7 +1864,7 @@ if SERVER then net.WriteString(lockpart_UID) net.WriteString(breakup_condition) net.Send(auth_ent_owner) - + else if is_first_time and did_grab then net.Start("pac_mark_grabbed_ent") @@ -1887,15 +1887,15 @@ if SERVER then if not lock_allow:GetBool() then return end if not lock_allow_teleport:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end - + --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then + if netrate_enforcement_sv_monitoring:GetBool() then MsgC(Color(255,255,0), "[PAC3] Lock teleport: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") end return end - + local lockpart_UID = net.ReadString() local pos = net.ReadVector() local ang = net.ReadAngle() @@ -1913,7 +1913,7 @@ if SERVER then net.Receive("pac_request_angle_reset_on_entity", function(len, ply) if not PlayerIsCombatAllowed(ply) then return end - + local ang = net.ReadAngle() local delay = net.ReadFloat() local targ_ent = net.ReadEntity() @@ -1921,10 +1921,10 @@ if SERVER then targ_ent:SetAngles(ang) ApplyLockState(targ_ent, false) - + end) - + hook.Add("Tick", "pac_checklocks", function() if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.2 end --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check @@ -1940,7 +1940,7 @@ if SERVER then if grabber then grabber.grabbed_ents[ent] = false end - + ApplyLockState(ent, false) active_grabbed_ents[ent] = nil end @@ -1955,10 +1955,10 @@ if SERVER then if not hitscan_allow:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end - + --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then + if netrate_enforcement_sv_monitoring:GetBool() then MsgC(Color(255,255,0), "[PAC3] Hitscan: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") end return @@ -1969,7 +1969,7 @@ if SERVER then bulletinfo.Src = net.ReadVector() local dir = net.ReadAngle() bulletinfo.Dir = dir:Forward() - + bulletinfo.dmgtype_str = table.KeyFromValue(damage_ids, net.ReadUInt(7)) bulletinfo.dmgtype = damage_types[bulletinfo.dmgtype_str] bulletinfo.Spread = net.ReadVector() @@ -1981,13 +1981,13 @@ if SERVER then bulletinfo.Num = net.ReadUInt(9) bulletinfo.TracerName = table.KeyFromValue(tracer_ids, net.ReadUInt(4)) bulletinfo.DistributeDamage = net.ReadBool() - + bulletinfo.DamageFalloff = net.ReadBool() bulletinfo.DamageFalloffDistance = net.ReadUInt(16) bulletinfo.DamageFalloffFraction = net.ReadUInt(10) / 1000 local part_uid = ply:Nick() .. net.ReadString() - + bulletinfo.Num = math.Clamp(bulletinfo.Num, 1, hitscan_max_bullets:GetInt()) bulletinfo.Damage = math.Clamp(bulletinfo.Damage, 0, hitscan_max_damage:GetInt()) bulletinfo.DamageFalloffFraction = math.Clamp(bulletinfo.DamageFalloffFraction,0,1) @@ -1995,11 +1995,11 @@ if SERVER then if hitscan_spreadout_dmg:GetBool() or bulletinfo.DistributeDamage then bulletinfo.Damage = bulletinfo.Damage / bulletinfo.Num end - + if not affect_self then bulletinfo.IgnoreEntity = ply end ply.pac_bullet_emitters = ply.pac_bullet_emitters or {} ply.pac_bullet_emitters[part_uid] = ply.pac_bullet_emitters[part_uid] or ents.Create("pac_bullet_emitter") - + bulletinfo.Attacker = ply bulletinfo.Callback = function(atk, trc, dmg) dmg:SetDamageType(bulletinfo.dmgtype) @@ -2026,16 +2026,16 @@ if SERVER then end end end - + if IsValid(ply.pac_bullet_emitters[part_uid]) then ply.pac_bullet_emitters[part_uid]:FireBullets(bulletinfo) else ply.pac_bullet_emitters[part_uid] = ents.Create("pac_bullet_emitter") end - + end) end - + function DeclareHealthModifierReceivers() util.AddNetworkString("pac_request_healthmod") util.AddNetworkString("pac_update_healthbars") @@ -2044,7 +2044,7 @@ if SERVER then --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then + if netrate_enforcement_sv_monitoring:GetBool() then MsgC(Color(255,255,0), "[PAC3] Health modifier: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") end return @@ -2068,7 +2068,7 @@ if SERVER then ply.pac_healthmods = ply.pac_healthmods or {} ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} ply.pac_healthmods[part_uid].maxhealth = num - + elseif action == "MaxArmor" then if not healthmod_allow:GetBool() then return end local num = net.ReadUInt(32) @@ -2083,11 +2083,11 @@ if SERVER then ply.pac_healthmods = ply.pac_healthmods or {} ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {} ply.pac_healthmods[part_uid].maxarmor = num - + elseif action == "DamageMultiplier" then local scale = net.ReadFloat() AddDamageScale(ply, mod_id, scale, part_uid) - + elseif action == "HealthBars" then if not healthmod_allowed_extra_bars:GetBool() then return end local num = net.ReadUInt(32) @@ -2095,9 +2095,9 @@ if SERVER then local layer = net.ReadUInt(4) local absorbfactor = net.ReadFloat() local follow = net.ReadBool() - + UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) - + elseif action == "OnRemove" then if ply.pac_damage_scalings then if ply.pac_damage_scalings[part_uid] then @@ -2107,7 +2107,7 @@ if SERVER then if ply.pac_healthmods then ply.pac_healthmods[part_uid] = nil end - + FixMaxHealths(ply) UpdateHealthBars(ply, 0, 0, 0, 0, part_uid, follow) end @@ -2204,17 +2204,17 @@ if CLIENT then CreateConVar("pac_client_damage_zone_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") CreateConVar("pac_client_force_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to pac3 physics forces") CreateConVar("pac_client_hitscan_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.") - + function pac.CountNetMessage() local ply = LocalPlayer() - + local stime = SysTime() local ms_basis = GetConVar("pac_sv_combat_enforce_netrate"):GetInt()/1000 local base_allowance = GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt() - + ply.pac_netmessage_allowance = ply.pac_netmessage_allowance or base_allowance ply.pac_netmessage_allowance_time = ply.pac_netmessage_allowance_time or 0 --initialize fields - + local timedelta = stime - ply.pac_netmessage_allowance_time --in seconds ply.pac_netmessage_allowance_time = stime local regen_rate = math.Clamp(ms_basis,0.01,10) / 20 --delay (converted from milliseconds) -> frequency (1/seconds) @@ -2236,7 +2236,7 @@ if CLIENT then ply.pac_netmessage_allowance_time = stime return ply.pac_netmessage_allowance ~= -1 end - + end local function SendConsents() @@ -2249,9 +2249,9 @@ if CLIENT then net.SendToServer() end - + local function RequestBlockedParts() - + net.Start("pac_request_blocked_parts") net.SendToServer() end @@ -2264,7 +2264,7 @@ if CLIENT then for name,b in pairs(pac.Blocked_Combat_Parts) do local blocked = b local disabled = not GetConVar("pac_sv_"..name):GetBool() - + local bool_str if disabled and blocked then bool_str = "disabled and blocked -> unavailable" @@ -2283,7 +2283,7 @@ if CLIENT then end CreateConVar("pac_break_lock_verbosity", "3", FCVAR_ARCHIVE, "How much info you want for the PAC3 lock notifications\n3:full information\n2:grabbing player + basic reminder of the lock break command\n1:grabbing player\n0:suppress the notifications") - + concommand.Add( "pac_stop_lock", function() net.Start("pac_signal_stop_lock") @@ -2294,7 +2294,7 @@ if CLIENT then net.Start("pac_signal_stop_lock") net.SendToServer() end, "asks the server to breakup any lockpart hold on your player") - + net.Receive("pac_lock_imposecalcview", function() local authority_to_calcview = net.ReadBool() and GetConVar("pac_client_lock_camera_consent"):GetBool() @@ -2310,7 +2310,7 @@ if CLIENT then hook.Remove("CalcView", "PAC_lockpart_calcview") LocalPlayer().has_calcview = false return nil - + end local view = { origin = alt_pos, @@ -2358,7 +2358,7 @@ if CLIENT then end pac.Message("You've been grabbed by " .. grabber:Nick() .. "!") - + end) hook.Add("InitPostEntity", "PAC_Send_Consents_On_Join", SendConsents) diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index ef426a713..ce67038cb 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -93,9 +93,9 @@ do -- projectile entity elseif not valid_fallback then self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties) end - + end - + local phys = self:GetPhysicsObject() phys:SetMaterial(part.SurfaceProperties) @@ -120,7 +120,7 @@ do -- projectile entity else phys:EnableCollisions(false) end - + phys:SetMass(math.Clamp(part.Mass, 0.001, pac_sv_projectile_max_mass:GetFloat())) phys:SetDamping(0, 0) @@ -608,7 +608,7 @@ if SERVER then ent:SetPos(pos) ent:SetAngles(ang) ent:Spawn() - + if not part.CollideWithOwner then ent:SetOwner(ply) @@ -697,7 +697,7 @@ if SERVER then if ent.part_data.RemoveOnHide then SafeRemoveEntity(ent) end - + end) - + end From a6138deb40d876b0513392e37749cd8429809932 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 12:51:13 -0500 Subject: [PATCH 109/300] correct model paths --- 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 f57dfae55..6b7b8d0ea 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -1278,8 +1278,8 @@ do -- base editable if not pace.bookmarked_ressources["models"] then pace.bookmarked_ressources["models"] = { "models/pac/default.mdl", - "models/cedrics_models/basic_shapes/plane.mdl", - "models/cedrics_models/basic_shapes/circle.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" @@ -2386,4 +2386,4 @@ function pace.OpenTreeSearch() timer.Simple(0.1,function() edit:RequestFocus() end) end -end \ No newline at end of file +end From 346db6c74959e0a13b99a33c62c9f3bba71f1f79 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 13:03:48 -0500 Subject: [PATCH 110/300] rename presets --- lua/pac3/editor/client/settings.lua | 37 +++++++---------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 634726f6b..21f7002bd 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -52,7 +52,7 @@ local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, pace = pace -pace.partmenu_categories_cedrics = { +pace.partmenu_categories_experimental = { ["new!"] = { ["icon"] = "icon16/new.png", @@ -1469,7 +1469,7 @@ function pace.FillEditorSettings(pnl) shortcutaction_presets:SetText("Select a shortcut preset") shortcutaction_presets:AddChoice("factory preset", pace.PACActionShortcut_Default) shortcutaction_presets:AddChoice("no CTRL preset", pace.PACActionShortcut_NoCTRL) - shortcutaction_presets:AddChoice("Cedric's preset", pace.PACActionShortcut_Cedric) + shortcutaction_presets:AddChoice("experimental preset", pace.PACActionShortcut_Experimental) for i,filename in ipairs(file.Find("pac3_config/pac_editor_shortcuts*.txt","DATA")) do local data = file.Read("pac3_config/" .. filename, "DATA") @@ -2006,27 +2006,6 @@ function pace.FillEditorSettings2(pnl) pace.camera_movement_binds["speed"]:SetString(input.GetKeyName( num )) end - --[[pace.partmenu_categories_cedrics = - { - ["new!"] = - { - ["icon"] = "icon16/new.png", - ["interpolated_multibone"]= "interpolated_multibone", - ["damage_zone"] = "damage_zone", - ["hitscan"] = "hitscan", - ["lock"] = "lock", - ["force"] = "force", - ["health_modifier"] = "health_modifier", - }, - ["logic"] = - { - ["icon"] = "icon16/server_chart.png", - ["proxy"] = "proxy", - ["command"] = "command", - ["event"] = "event", - ["text"] = "text", - ["link"] = "link", - },]] local Parts = pac.GetRegisteredParts() local function get_icon(str, fallback) if str then @@ -2275,10 +2254,10 @@ function pace.FillEditorSettings2(pnl) part_categories_presets:SetText("Select a part category preset") part_categories_presets:AddChoice("active preset") part_categories_presets:AddChoice("factory preset") - part_categories_presets:AddChoice("Cedric's preset") + part_categories_presets:AddChoice("experimental preset") local default_partgroup_presets = { ["pac_part_categories.txt"] = true, - ["pac_part_categories_cedrics.txt"] = true, + ["pac_part_categories_experimental.txt"] = true, ["pac_part_categories_default.txt"] = true } for i,filename in ipairs(file.Find("pac3_config/pac_part_categories*.txt","DATA")) do @@ -2294,8 +2273,8 @@ function pace.FillEditorSettings2(pnl) part_categories_presets.OnSelect = function( self, index, value ) if value == "factory preset" then pace.partgroups = pace.partmenu_categories_default - elseif value == "Cedric's preset" then - pace.partgroups = pace.partmenu_categories_cedrics + elseif value == "experimental preset" then + pace.partgroups = pace.partmenu_categories_experimental elseif string.find(value, ".txt") then pace.partgroups = util.KeyValuesToTable(file.Read("pac3_config/"..value)) elseif value == "active preset" then @@ -2614,8 +2593,8 @@ decode_table_from_file("pac_editor_shortcuts") decode_table_from_file("pac_editor_partmenu_layouts") decode_table_from_file("eventwheel_colors") -if not file.Exists("pac_part_categories_cedrics.txt", "DATA") then - file.Write("pac3_config/pac_part_categories_cedrics.txt", util.TableToKeyValues(pace.partmenu_categories_cedrics)) +if not file.Exists("pac_part_categories_experimental.txt", "DATA") then + file.Write("pac3_config/pac_part_categories_experimental.txt", util.TableToKeyValues(pace.partmenu_categories_experimental)) end if not file.Exists("pac_part_categories_default.txt", "DATA") then file.Write("pac3_config/pac_part_categories_default.txt", util.TableToKeyValues(pace.partmenu_categories_default)) From 98790856dcc6f5dd5f1fde8a5e1a516c41a40cf1 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 13:04:22 -0500 Subject: [PATCH 111/300] rename preset's table name --- lua/pac3/editor/client/shortcuts.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index f4822e1b5..9edab443b 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -179,7 +179,7 @@ pace.PACActionShortcut_NoCTRL = { } } -pace.PACActionShortcut_Cedric = { +pace.PACActionShortcut_Experimental = { ["help_info_popup"] = { [1] = {"F1"} }, @@ -294,7 +294,7 @@ pace.PACActionShortcut_Cedric = { } } -pace.PACActionShortcut = pace.PACActionShortcut or pace.PACActionShortcut_Cedric +pace.PACActionShortcut = pace.PACActionShortcut or pace.PACActionShortcut_Experimental --pace.PACActionShortcut = pace.PACActionShortcuts_NoCTRL From 866bb87954c013178431b834c9fa8f62f654cd5e Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 14:09:54 -0500 Subject: [PATCH 112/300] Update net_combat.lua localize functions remove unused functions limit some commands and the lock break net message --- lua/pac3/extra/shared/net_combat.lua | 44 +++++++++++++++++----------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index dea49eed4..eb3d70a3b 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -475,14 +475,6 @@ if SERVER then end - local function CalculateHealthBarUIDCombinedHP(ply, uid) - - end - - local function CalculateHealthBarLayerCombinedHP(ply, layer) - - end - local function GatherExtraHPBars(ply) if not ply.pac_healthbars then return 0,nil end local built_tbl = {} @@ -1273,8 +1265,12 @@ if SERVER then --lock break order from client net.Receive("pac_signal_stop_lock", function(len,ply) + if not pac.RatelimitPlayer( ply, "pac_signal_stop_lock", 3, 5, {"Player ", ply, " is spamming pac_signal_stop_lock!"} ) then + return + end ApplyLockState(ply, false) MsgC(Color(0,255,255), "Requesting lock break!\n") + if ply.grabbed_by then --directly go for the grabbed_by player net.Start("pac_request_lock_break") net.WriteEntity(ply) @@ -1297,17 +1293,31 @@ if SERVER then end) concommand.Add("pac_damage_zone_whitelist_entity_class", function(ply, cmd, args, argStr) + if IsValid(ply) then + if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_damage_zone_whitelist_entity_class", 3, 5, {"Player ", ply, " is spamming pac_damage_zone_whitelist_entity_class!"} ) then + return + end + end for _,v in pairs(string.Explode(";",argStr)) do - damageable_point_ent_classes[v] = true - print("added " .. v .. " to the entities you can damage") + if v ~= "" then + damageable_point_ent_classes[v] = true + print("added " .. v .. " to the entities you can damage") + end end PrintTable(damageable_point_ent_classes) end) concommand.Add("pac_damage_zone_blacklist_entity_class", function(ply, cmd, args, argStr) + if IsValid(ply) then + if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_damage_zone_blacklist_entity_class", 3, 5, {"Player ", ply, " is spamming pac_damage_zone_blacklist_entity_class!"} ) then + return + end + end for _,v in pairs(string.Explode(";",argStr)) do - damageable_point_ent_classes[v] = false - print("removed " .. v .. " from the entities you can damage") + if v ~= "" then + damageable_point_ent_classes[v] = false + print("removed " .. v .. " from the entities you can damage") + end end PrintTable(damageable_point_ent_classes) end) @@ -1354,7 +1364,7 @@ if SERVER then local ang_torque_mode_ids = {["Global"] = 0, ["TargetLocal"] = 1, ["Local"] = 2, ["Radial"] = 3} local nextcheckforce = SysTime() - function DeclareForceReceivers() + local function DeclareForceReceivers() util.AddNetworkString("pac_request_force") --the force part impulse request net message net.Receive("pac_request_force", function(len,ply) @@ -1467,7 +1477,7 @@ if SERVER then end) end - function DeclareDamageZoneReceivers() + local function DeclareDamageZoneReceivers() util.AddNetworkString("pac_request_zone_damage") util.AddNetworkString("pac_hit_results") net.Receive("pac_request_zone_damage", function(len,ply) @@ -1683,7 +1693,7 @@ if SERVER then end local nextchecklock = CurTime() - function DeclareLockReceivers() + local function DeclareLockReceivers() util.AddNetworkString("pac_request_position_override_on_entity_teleport") util.AddNetworkString("pac_request_position_override_on_entity_grab") util.AddNetworkString("pac_request_angle_reset_on_entity") @@ -1949,7 +1959,7 @@ if SERVER then end) end - function DeclareHitscanReceivers() + local function DeclareHitscanReceivers() util.AddNetworkString("pac_hitscan") net.Receive("pac_hitscan", function(len,ply) @@ -2036,7 +2046,7 @@ if SERVER then end) end - function DeclareHealthModifierReceivers() + local function DeclareHealthModifierReceivers() util.AddNetworkString("pac_request_healthmod") util.AddNetworkString("pac_update_healthbars") net.Receive("pac_request_healthmod", function(len,ply) From cffaf699b35a7bda714cce478a8e8d9c9809e871 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 14:29:27 -0500 Subject: [PATCH 113/300] localize functions --- lua/pac3/editor/client/parts.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 8ac861fc0..74a548751 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2692,7 +2692,7 @@ do --hover highlight halo if not skip then timer.Simple(0.3, function() BulkSelectRefreshFadedNodes(self) end) end end - function BulkSelectRefreshFadedNodes(part_trace) + local function BulkSelectRefreshFadedNodes(part_trace) if refresh_halo_hook then return end if part_trace then for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do @@ -2711,7 +2711,7 @@ do --hover highlight halo end end - function RebuildBulkHighlight() + local function RebuildBulkHighlight() local parts_tbl = {} local ents_tbl = {} local hover_tbl = {} @@ -2752,7 +2752,7 @@ do --hover highlight halo last_bulk_select_tbl = hover_tbl end - function TestPrintTable(tbl, tbl_name) + local function TestPrintTable(tbl, tbl_name) MsgC(Color(200,255,200), "TABLE CONTENTS:" .. tbl_name .. " = {\n") for _,v in pairs(tbl) do MsgC(Color(200,255,200), "\t", tostring(v), ", \n") @@ -2760,7 +2760,7 @@ do --hover highlight halo MsgC(Color(200,255,200), "}\n") end - function ThinkBulkHighlight() + local function ThinkBulkHighlight() if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then hook.Remove('PreDrawHalos', "BulkSelectHighlights") return @@ -2768,7 +2768,7 @@ do --hover highlight halo DrawHaloHighlight(last_bulk_select_tbl) end - function DrawHaloHighlight(tbl) + local function DrawHaloHighlight(tbl) if (type(tbl) ~= "table") then return end if not pace.Active then hook.Remove('PreDrawHalos', "BulkSelectHighlights") From b89c43b72db56919e6ea7f331fa05b6f5fb5b204 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 14:32:44 -0500 Subject: [PATCH 114/300] localize variables --- lua/pac3/editor/client/settings.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 21f7002bd..7c46792e0 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1799,8 +1799,8 @@ function pace.FillEditorSettings(pnl) local RightPanel = vgui.Create( "DTree", f ) - Test_Node = RightPanel:AddNode( "Test", "icon16/world.png" ) - test_part = pac.CreatePart("base") //the menu needs a part to get its full version in preview + local Test_Node = RightPanel:AddNode( "Test", "icon16/world.png" ) + local test_part = pac.CreatePart("base") //the menu needs a part to get its full version in preview function RightPanel:DoRightClick() temp_list = pace.operations_order pace.operations_order = buildlist_partmenu From 12eaac753a9d7cb4a170555e9bb437677275f1ee Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 14:34:28 -0500 Subject: [PATCH 115/300] localize function --- lua/pac3/editor/client/panels/tree.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index 54c028d1e..83a6745b5 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -188,7 +188,7 @@ do end end - function DoScrollControl(self, action) + local function DoScrollControl(self, action) pace.BulkSelectKey = input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString()) if pace.current_part.pace_tree_node and From e0b3c4bf7a63c68a5cb13144905ba7593ae31b7e Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 14:45:00 -0500 Subject: [PATCH 116/300] fix pac camera when closing editor --- lua/pac3/editor/client/view.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 59fb2ea34..f1f638046 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -389,7 +389,7 @@ function pace.EnableView(b) pace.SetTPose(false) end - if not enable_editor_view:GetBool() then + if not enable_editor_view:GetBool() or not pace.Editor:IsValid() then local ply = LocalPlayer() pac.RemoveHook("CalcView", "editor") pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) From 79c6db6a9cafd84425335c71c314269e36bb250c Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Dec 2023 17:01:59 -0500 Subject: [PATCH 117/300] Update parts.lua localize variable remove unused function raise scope of local functions --- lua/pac3/editor/client/parts.lua | 248 +++++++++++++++---------------- 1 file changed, 123 insertions(+), 125 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 74a548751..2d1e690f1 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -5,7 +5,7 @@ local L = pace.LanguageString pace.BulkSelectList = {} pace.BulkSelectUIDs = {} pace.BulkSelectClipboard = {} -refresh_halo_hook = true +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"} 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"} @@ -58,6 +58,128 @@ local function add_expensive_submenu_load(pnl, callback) end end + +local function BulkSelectRefreshFadedNodes(part_trace) + if refresh_halo_hook then return end + if part_trace then + for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do + if IsValid(v.pace_tree_node) then + v.pace_tree_node:SetAlpha( 255 ) + end + + end + end + + for _,v in ipairs(pace.BulkSelectList) do + if not v:IsValid() then table.RemoveByValue(pace.BulkSelectList, v) + elseif v.pace_tree_node then + v.pace_tree_node:SetAlpha( 150 ) + end + end +end + +local function RebuildBulkHighlight() + local parts_tbl = {} + local ents_tbl = {} + local hover_tbl = {} + local ent = {} + + --get potential entities and part-children from each parent in the bulk list + for _,v in pairs(pace.BulkSelectList) do --this will get parts + + if (v == v:GetRootPart()) then --if this is the root part, send the entity + table.insert(ents_tbl,v:GetRootPart():GetOwner()) + table.insert(parts_tbl,v) + else + table.insert(parts_tbl,v) + end + + for _,child in ipairs(v:GetChildrenList()) do --now do its children + table.insert(parts_tbl,child) + end + end + + --check what parts are candidates we can give to halo + for _,v in ipairs(parts_tbl) do + local can_add = false + if (v.ClassName == "model" or v.ClassName == "model2") then + can_add = true + end + if (v.ClassName == "group") or (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then + can_add = false + end + if can_add then + table.insert(hover_tbl, v:GetOwner()) + end + end + + table.Add(hover_tbl,ents_tbl) + --TestPrintTable(hover_tbl, "hover_tbl") + + last_bulk_select_tbl = hover_tbl +end + +local function TestPrintTable(tbl, tbl_name) + MsgC(Color(200,255,200), "TABLE CONTENTS:" .. tbl_name .. " = {\n") + for _,v in pairs(tbl) do + MsgC(Color(200,255,200), "\t", tostring(v), ", \n") + end + MsgC(Color(200,255,200), "}\n") +end + +local function ThinkBulkHighlight() + if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + return + end + DrawHaloHighlight(last_bulk_select_tbl) +end + +local function DrawHaloHighlight(tbl) + if (type(tbl) ~= "table") then return end + if not pace.Active then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + end + + --Find out the color and apply the halo + local color_string = GetConVar("pac_hover_color"):GetString() + local pulse_rate = math.min(math.abs(GetConVar("pac_hover_pulserate"):GetFloat()), 100) + local pulse = math.sin(SysTime() * pulse_rate) * 0.5 + 0.5 + if pulse_rate == 0 then pulse = 1 end + local pulseamount + + local halo_color = Color(255,255,255) + + if color_string == "rave" then + halo_color = Color(255*((0.33 + SysTime() * pulse_rate/20)%1), 255*((0.66 + SysTime() * pulse_rate/20)%1), 255*((SysTime() * pulse_rate/20)%1), 255) + pulseamount = 8 + elseif color_string == "funky" then + halo_color = Color(255*((0.33 + SysTime() * pulse_rate/10)%1), 255*((0.2 + SysTime() * pulse_rate/15)%1), 255*((SysTime() * pulse_rate/15)%1), 255) + pulseamount = 5 + elseif color_string == "ocean" then + halo_color = Color(0, 80 + 30*(pulse), 200 + 50*(pulse) * 0.5 + 0.5, 255) + pulseamount = 4 + elseif color_string == "rainbow" then + --halo_color = Color(255*(0.5 + 0.5*math.sin(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*-math.cos(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*math.sin(1 + pac.RealTime * pulse_rate/20)), 255) + halo_color = HSVToColor(SysTime() * 360 * pulse_rate/20, 1, 1) + pulseamount = 4 + elseif #string.Split(color_string, " ") == 3 then + halo_color_tbl = string.Split( color_string, " " ) + for i,v in ipairs(halo_color_tbl) do + if not isnumber(tonumber(halo_color_tbl[i])) then halo_color_tbl[i] = 0 end + end + halo_color = Color(pulse*halo_color_tbl[1],pulse*halo_color_tbl[2],pulse*halo_color_tbl[3],255) + pulseamount = 4 + else + halo_color = Color(255,255,255,255) + pulseamount = 2 + end + --print("using", halo_color, "blurs=" .. 2, "amount=" .. pulseamount) + + pac.haloex.Add(tbl, halo_color, 2, 2, pulseamount, true, true, pulseamount, 1, 1) + --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) +end + function pace.WearParts(temp_wear_filter) pace.still_loading_wearing = true @@ -2322,10 +2444,6 @@ function pace.addPartMenuComponent(menu, obj, option_name) end -function pace.addPartGroupMenuComponent(menu, obj, group_name) - -end - --destructive tool function pace.UltraCleanup(obj) if not obj then return end @@ -2692,126 +2810,6 @@ do --hover highlight halo if not skip then timer.Simple(0.3, function() BulkSelectRefreshFadedNodes(self) end) end end - local function BulkSelectRefreshFadedNodes(part_trace) - if refresh_halo_hook then return end - if part_trace then - for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do - if IsValid(v.pace_tree_node) then - v.pace_tree_node:SetAlpha( 255 ) - end - - end - end - - for _,v in ipairs(pace.BulkSelectList) do - if not v:IsValid() then table.RemoveByValue(pace.BulkSelectList, v) - elseif v.pace_tree_node then - v.pace_tree_node:SetAlpha( 150 ) - end - end - end - - local function RebuildBulkHighlight() - local parts_tbl = {} - local ents_tbl = {} - local hover_tbl = {} - local ent = {} - - --get potential entities and part-children from each parent in the bulk list - for _,v in pairs(pace.BulkSelectList) do --this will get parts - - if (v == v:GetRootPart()) then --if this is the root part, send the entity - table.insert(ents_tbl,v:GetRootPart():GetOwner()) - table.insert(parts_tbl,v) - else - table.insert(parts_tbl,v) - end - - for _,child in ipairs(v:GetChildrenList()) do --now do its children - table.insert(parts_tbl,child) - end - end - - --check what parts are candidates we can give to halo - for _,v in ipairs(parts_tbl) do - local can_add = false - if (v.ClassName == "model" or v.ClassName == "model2") then - can_add = true - end - if (v.ClassName == "group") or (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then - can_add = false - end - if can_add then - table.insert(hover_tbl, v:GetOwner()) - end - end - - table.Add(hover_tbl,ents_tbl) - --TestPrintTable(hover_tbl, "hover_tbl") - - last_bulk_select_tbl = hover_tbl - end - - local function TestPrintTable(tbl, tbl_name) - MsgC(Color(200,255,200), "TABLE CONTENTS:" .. tbl_name .. " = {\n") - for _,v in pairs(tbl) do - MsgC(Color(200,255,200), "\t", tostring(v), ", \n") - end - MsgC(Color(200,255,200), "}\n") - end - - local function ThinkBulkHighlight() - if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then - hook.Remove('PreDrawHalos', "BulkSelectHighlights") - return - end - DrawHaloHighlight(last_bulk_select_tbl) - end - - local function DrawHaloHighlight(tbl) - if (type(tbl) ~= "table") then return end - if not pace.Active then - hook.Remove('PreDrawHalos', "BulkSelectHighlights") - end - - --Find out the color and apply the halo - local color_string = GetConVar("pac_hover_color"):GetString() - local pulse_rate = math.min(math.abs(GetConVar("pac_hover_pulserate"):GetFloat()), 100) - local pulse = math.sin(SysTime() * pulse_rate) * 0.5 + 0.5 - if pulse_rate == 0 then pulse = 1 end - local pulseamount - - local halo_color = Color(255,255,255) - - if color_string == "rave" then - halo_color = Color(255*((0.33 + SysTime() * pulse_rate/20)%1), 255*((0.66 + SysTime() * pulse_rate/20)%1), 255*((SysTime() * pulse_rate/20)%1), 255) - pulseamount = 8 - elseif color_string == "funky" then - halo_color = Color(255*((0.33 + SysTime() * pulse_rate/10)%1), 255*((0.2 + SysTime() * pulse_rate/15)%1), 255*((SysTime() * pulse_rate/15)%1), 255) - pulseamount = 5 - elseif color_string == "ocean" then - halo_color = Color(0, 80 + 30*(pulse), 200 + 50*(pulse) * 0.5 + 0.5, 255) - pulseamount = 4 - elseif color_string == "rainbow" then - --halo_color = Color(255*(0.5 + 0.5*math.sin(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*-math.cos(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*math.sin(1 + pac.RealTime * pulse_rate/20)), 255) - halo_color = HSVToColor(SysTime() * 360 * pulse_rate/20, 1, 1) - pulseamount = 4 - elseif #string.Split(color_string, " ") == 3 then - halo_color_tbl = string.Split( color_string, " " ) - for i,v in ipairs(halo_color_tbl) do - if not isnumber(tonumber(halo_color_tbl[i])) then halo_color_tbl[i] = 0 end - end - halo_color = Color(pulse*halo_color_tbl[1],pulse*halo_color_tbl[2],pulse*halo_color_tbl[3],255) - pulseamount = 4 - else - halo_color = Color(255,255,255,255) - pulseamount = 2 - end - --print("using", halo_color, "blurs=" .. 2, "amount=" .. pulseamount) - - pac.haloex.Add(tbl, halo_color, 2, 2, pulseamount, true, true, pulseamount, 1, 1) - --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) - end end --custom info panel From a5b0a155a28130423fab00784e6ff4068ca66b45 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 10 Dec 2023 23:25:34 -0500 Subject: [PATCH 118/300] reorder functions --- lua/pac3/editor/client/parts.lua | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 2d1e690f1..2285a1554 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -127,14 +127,6 @@ local function TestPrintTable(tbl, tbl_name) MsgC(Color(200,255,200), "}\n") end -local function ThinkBulkHighlight() - if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then - hook.Remove('PreDrawHalos', "BulkSelectHighlights") - return - end - DrawHaloHighlight(last_bulk_select_tbl) -end - local function DrawHaloHighlight(tbl) if (type(tbl) ~= "table") then return end if not pace.Active then @@ -180,6 +172,15 @@ local function DrawHaloHighlight(tbl) --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape ) end +local function ThinkBulkHighlight() + if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then + hook.Remove('PreDrawHalos', "BulkSelectHighlights") + return + end + DrawHaloHighlight(last_bulk_select_tbl) +end + + function pace.WearParts(temp_wear_filter) pace.still_loading_wearing = true From c93c71cb80339ba5c3d15cbcb2d49073294bdce4 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 12 Dec 2023 18:41:44 -0500 Subject: [PATCH 119/300] suppress network string error (#1325) if parts are deactivated, instead use part label's SetError function to tell users about it instead and two minor fixes on damage zone --- lua/pac3/core/client/parts/damage_zone.lua | 3 +++ lua/pac3/core/client/parts/force.lua | 1 + lua/pac3/core/client/parts/health_modifier.lua | 1 + lua/pac3/core/client/parts/hitscan.lua | 1 + lua/pac3/core/client/parts/lock.lua | 1 + 5 files changed, 7 insertions(+) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index ae4c0999f..cf44e8c16 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -497,6 +497,7 @@ function PART:SendNetMessage() pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if not GetConVar('pac_sv_damage_zone'):GetBool() then return end + if util.NetworkStringToID( "pac_request_zone_damage" ) == 0 then self:SetError("This part is deactivated on the server") return end if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end end @@ -585,6 +586,7 @@ function PART:OnShow() --grabbed the function from projectile.lua --here, we spawn a static hitmarker and the max delay is 8 seconds local function spawn(part, pos, ang, parent_ent, duration, owner) + if not IsValid(owner) then return end if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker --what if people employ a more roundabout method? CRACKDOWN! @@ -757,6 +759,7 @@ function PART:PreviewHitbox() hook.Add(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID, function() if not self.Preview then hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end + if not IsValid(self) then hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end self:GetWorldPosition() if self.HitboxMode == "Box" then local mins = Vector(-self.Radius, -self.Radius, -self.Length) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index f8653ca4a..451582611 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -158,6 +158,7 @@ function PART:Impulse(on) if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if not on and not self.Continuous then return end if not GetConVar("pac_sv_force"):GetBool() then return end + if util.NetworkStringToID( "pac_request_force" ) == 0 then self:SetError("This part is deactivated on the server") return end pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 5455f91ac..39aab1fff 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -37,6 +37,7 @@ function PART:SendModifier(str) if self:IsHidden() then return end if LocalPlayer() ~= self:GetPlayerOwner() then return end if not GetConVar("pac_sv_health_modifier"):GetBool() then return end + if util.NetworkStringToID( "pac_request_healthmod" ) == 0 then self:SetError("This part is deactivated on the server") return end pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index c7318b505..bbfd4ebf9 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -195,6 +195,7 @@ local tracer_ids = { function PART:SendNetMessage() if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if not GetConVar('pac_sv_hitscan'):GetBool() then return end + if util.NetworkStringToID( "pac_hitscan" ) == 0 then self:SetError("This part is deactivated on the server") return end pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts[self.ClassName] then return diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 76dac00cc..5041a767c 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -60,6 +60,7 @@ BUILDER:EndStorableVars() function PART:OnThink() if not GetConVar('pac_sv_lock'):GetBool() then return end + if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end From 2a443ff57cc142308d081166a81a06866f10ec5d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 12 Dec 2023 18:46:55 -0500 Subject: [PATCH 120/300] validity checks --- lua/pac3/editor/client/panels/properties.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 6b7b8d0ea..b12c247c3 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -291,6 +291,7 @@ end) pac.AddHook("PostRenderVGUI", "flash_properties", function() if not pace.flashes then return end + if not IsValid(pace.tree) or not IsValid(pace.properties) then return end for pnl, tbl in pairs(pace.flashes) do if IsValid(pnl) then --print(pnl:LocalToScreen(0,0)) @@ -334,6 +335,7 @@ do -- container end function PANEL:Flash() + if not IsValid(pace.tree) or not IsValid(pace.properties) then return end pace.flashes = pace.flashes or {} pace.flashes[self] = {start = CurTime(), flash_end = CurTime() + 2.5, color = Color(255,0,0)} From baad4b394c984036c441c5025f3b7c7d0ebd4a90 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 12 Dec 2023 18:51:27 -0500 Subject: [PATCH 121/300] validity checks --- lua/pac3/core/client/base_movable.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/pac3/core/client/base_movable.lua b/lua/pac3/core/client/base_movable.lua index bf59a9624..aa81d9112 100644 --- a/lua/pac3/core/client/base_movable.lua +++ b/lua/pac3/core/client/base_movable.lua @@ -210,17 +210,20 @@ function PART:CalcAngles(ang, wpos) if pac.StringFind(self.AimPartName, "NEAREST_LIFE_YAW", true, true) then local nearest_ent = get_nearest_ent(self) + if not IsValid(nearest_ent) then return ang or Angle(0,0,0) end local ang = (nearest_ent:GetPos() - wpos):Angle() return Angle(0,ang.y,0) + self.Angles end if pac.StringFind(self.AimPartName, "NEAREST_LIFE_POS", true, true) then local nearest_ent = get_nearest_ent(self) + if not IsValid(nearest_ent) then return ang or Angle(0,0,0) end return self.Angles + (nearest_ent:GetPos() - wpos):Angle() end if pac.StringFind(self.AimPartName, "NEAREST_LIFE", true, true) then local nearest_ent = get_nearest_ent(self) + if not IsValid(nearest_ent) then return ang or Angle(0,0,0) end return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - wpos):Angle() end From 22e2addcc5bcd26180eb30e95715773f3945019d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 12 Dec 2023 22:23:38 -0500 Subject: [PATCH 122/300] try a variable check --- lua/pac3/editor/client/saved_parts.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index f9ea3b948..45a2a61ba 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -241,7 +241,9 @@ function pace.LoadParts(name, clear, override_part) if data and istable(data) then for i,part in pairs(data) do - if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end + if part.self then + if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end + end end end From 63a3f3233b47317fd4e5bf6b4341292e10db1ffd Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 13 Dec 2023 21:51:49 -0500 Subject: [PATCH 123/300] Update net_combat.lua CPPI standard integrations for prop protection some ulx integrations to enforce freeze and jail rework some statements --- lua/pac3/extra/shared/net_combat.lua | 219 ++++++++++++++++++++------- 1 file changed, 168 insertions(+), 51 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index eb3d70a3b..072ea8fa1 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -55,7 +55,7 @@ local ENFORCE_DISTANCE_SQR = math.pow(enforce_distance:GetInt(),2) cvars.AddChangeCallback("pac_sv_combat_distance_enforced", function() ENFORCE_DISTANCE_SQR = math.pow(enforce_distance:GetInt(),2) end) -local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") +local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force...).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") local damageable_point_ent_classes = { @@ -208,14 +208,43 @@ if SERVER then end + local function Try_CPPISetOwner(ent, ply) + if ent.CPPISetOwner then --a prop protection using CPPI probably exists + ent:CPPISetOwner(ply) + end + ent.pac_prop_protection_owner = ply --otherwise we'll have this field + end + + local function Try_CPPIGetOwner(ent) + if ent.CPPIGetOwner then --a prop protection using CPPI probably exists so we use it + return ent:CPPIGetOwner() + end + return ent.pac_prop_protection_owner or nil --otherwise we'll use the field we set or + end + --hack fix to stop GetOwner returning [NULL Entity] - hook.Add("PlayerSpawnedProp", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) - hook.Add("PlayerSpawnedNPC", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) - hook.Add("PlayerSpawnedRagdoll", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) - hook.Add("PlayerSpawnedSENT", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) - hook.Add("PlayerSpawnedSWEP", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) - hook.Add("PlayerSpawnedVehicle", "HackReAssignOwner", function(ply, ent) ent.m_PlayerCreator = ply end) - hook.Add("PlayerSpawnedEffect", "HackReAssignOwner", function(ply, model, ent) ent.m_PlayerCreator = ply end) + --uses CPPI interface from prop protectors if present + hook.Add("PlayerSpawnedProp", "HackReAssignOwner", function(ply, model, ent) + Try_CPPISetOwner(ent, ply) + end) + hook.Add("PlayerSpawnedNPC", "PAC_HackReAssignOwner", function(ply, ent) + Try_CPPISetOwner(ent, ply) + end) + hook.Add("PlayerSpawnedRagdoll", "PAC_HackReAssignOwner", function(ply, model, ent) + Try_CPPISetOwner(ent, ply) + end) + hook.Add("PlayerSpawnedSENT", "PAC_HackReAssignOwner", function(ply, ent) + Try_CPPISetOwner(ent, ply) + end) + hook.Add("PlayerSpawnedSWEP", "PAC_HackReAssignOwner", function(ply, ent) + Try_CPPISetOwner(ent, ply) + end) + hook.Add("PlayerSpawnedVehicle", "PAC_HackReAssignOwner", function(ply, ent) + Try_CPPISetOwner(ent, ply) + end) + hook.Add("PlayerSpawnedEffect", "PAC_HackReAssignOwner", function(ply, model, ent) + Try_CPPISetOwner(ent, ply) + end) local function IsPossibleContraptionEntity(ent) if not IsValid(ent) then return false end @@ -228,12 +257,18 @@ if SERVER then end local function IsPropProtected(ent, ply) + local owner = Try_CPPIGetOwner(ent) + + local prop_protected + if IsValid(owner) then --created entities should be fine + prop_protected = owner:IsPlayer() and owner ~= ply + else --players and world props could nil out + prop_protected = false + end local reason = "" local pac_sv_prop_protection = global_combat_prop_protection:GetBool() - local prop_protected = ent:GetCreator():IsPlayer() and ent:GetCreator() ~= ply - local contraption = IsPossibleContraptionEntity(ent) and ent:IsConstrained() if prop_protected and contraption then @@ -250,6 +285,8 @@ if SERVER then --whitelisting/blacklisting check local function PlayerIsCombatAllowed(ply) + if pace.IsBanned(ply) then return false end + if ulx and (ply.frozen or ply.jail) then return false end if pac.global_combat_whitelist[string.lower(ply:SteamID())] then if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Allowed" then return true end if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end @@ -297,10 +334,11 @@ if SERVER then end local function ApplyLockState(ent, bool, nocollide) --Change the movement states and reset some other angle-related things + if ulx and (ent.frozen or ent.jail) then return end --the grab imposes MOVETYPE_NONE and no collisions --reverting the state requires to reset the eyeang roll in case it was modified if ent:IsPlayer() then - if bool then + if bool then --apply lock active_grabbed_ents[ent] = true if nocollide then ent:SetMoveType(MOVETYPE_NONE) @@ -309,9 +347,12 @@ if SERVER then ent:SetMoveType(MOVETYPE_WALK) ent:SetCollisionGroup(COLLISION_GROUP_NONE) end - else + else --revert active_grabbed_ents[ent] = nil - ent:SetMoveType(MOVETYPE_WALK) + if ent.default_movetype_reserved then + ent:SetMoveType(ent.default_movetype) + ent.default_movetype_reserved = nil + end ent:SetCollisionGroup(COLLISION_GROUP_NONE) local eyeang = ent:EyeAngles() eyeang.r = 0 @@ -352,10 +393,13 @@ if SERVER then print(ent , "no longer grabbed by", ply) end end + elseif bool == false then + ent.lock_state_applied = false end ent:PhysWake() ent:SetGravity(1) + end local function maximized_ray_mins_maxs(startpos,endpos,padding) @@ -610,9 +654,10 @@ if SERVER then dmginfo:ScaleDamage(cumulative_mult) local remaining_dmg,surviving_layer,side_effect_dmg = GetHPBarDamage(target, dmginfo:GetDamage()) - - if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and hitscan_consents[target] == false then - dmginfo:SetDamage(0) + if IsValid(dmginfo:GetInflictor()) then + if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and hitscan_consents[target] == false then + dmginfo:SetDamage(0) + end else local total_hp_value,built_tbl = GatherExtraHPBars(target) if surviving_layer == nil or total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult @@ -654,6 +699,8 @@ if SERVER then local ply_prog_count = 0 for i,v in pairs(ents_hits) do if not (v:IsPlayer() or v:IsNPC() or string.find(v:GetClass(), "npc_")) and not tbl.PointEntities then ents_hits[i] = nil end + if v.CPPICanDamage and not v:CPPICanDamage(ply) then ents_hits[i] = nil end --CPPI check on the player + if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil else ent_count = ent_count + 1 @@ -689,10 +736,17 @@ if SERVER then --the function to determine if we can dissolve, based on policy and setting factors local function IsDissolvable(ent) + local owner = Try_CPPIGetOwner(ent) + + local prop_protected_final + if IsValid(owner) then --created entities should be fine + prop_protected_final = prop_protected and owner:IsPlayer() and damage_zone_consents[owner] == false + else --players and world props could nil out + prop_protected_final = false + end if not pac_sv_damage_zone_allow_dissolve then return false end local dissolvable = true local prop_protected, reason = IsPropProtected(ent, attacker) - local prop_protected_final = prop_protected and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false if ent:IsPlayer() then if not kill then dissolvable = false @@ -700,7 +754,7 @@ if SERVER then elseif inflictor == ent then dissolvable = false --do we allow that? end - if ent:IsWeapon() and IsValid(ent:GetCreator()) then + if ent:IsWeapon() and IsValid(owner) then dissolvable = false end if ent:CreatedByMap() then @@ -729,46 +783,71 @@ if SERVER then --the giga function to determine if we can damage local function DMGAllowed(ent) - if ent:Health() == 0 and not (string.find(tbl.DamageType, "dissolve")) then return false end --immediately exclude entities with 0 health, except if we want to dissolve + + local canhit = false --whether the policies allow the hit - local prop_protected_consent = ent:GetCreator() ~= inflictor and ent ~= inflictor and ent:GetCreator():IsPlayer() and damage_zone_consents[ent:GetCreator()] == false-- and ent:GetCreator() ~= inflictor + local prop_protected_consent local contraption = IsPossibleContraptionEntity(ent) - local bot_exception = true + local bot_exception = false if ent:IsPlayer() then if ent:IsBot() then bot_exception = true end end + + local owner = Try_CPPIGetOwner(ent) + local target_ply + if IsValid(owner) then --created entities should be fine + target_ply = owner + prop_protected_consent = owner ~= inflictor and ent ~= inflictor and owner:IsPlayer() and damage_zone_consents[owner] == false + else --players and world props could nil out + prop_protected_consent = false + if ent:IsPlayer() then + target_ply = ent + end + end + --first pass: entity class blacklist + if IsEntity(ent) and ((damageable_point_ent_classes[ent:GetClass()] ~= false) or ((damageable_point_ent_classes[ent:GetClass()] == nil) or (damageable_point_ent_classes[ent:GetClass()] == true))) then --second pass: the damagezone's settings --1.player hurt self if asked + local is_player = ent:IsPlayer() + local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) + local is_npc = ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity + if (tbl.AffectSelf) and ent == inflictor then canhit = true --2.main target types : players, NPC, point entities - elseif ((ent:IsPlayer() and tbl.Players) or (tbl.NPC and (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity)) or tbl.PointEntities) - and --one of the base classes + elseif --one of the base classes (damageable_point_ent_classes[ent:GetClass()] ~= false) --non-blacklisted class and --enforce prop protection - (bot_exception or (ent:GetCreator() == inflictor or ent == inflictor or (ent:GetCreator() ~= inflictor and pac_sv_prop_protection and damage_zone_consents[ent:GetCreator()] == true) or not pac_sv_prop_protection)) + (bot_exception or (owner == inflictor or ent == inflictor or (pac_sv_prop_protection and damage_zone_consents[target_ply] ~= false) or not pac_sv_prop_protection)) then - canhit = true - if ent:IsPlayer() and tbl.Players then - --rules for players: - --self can always hurt itself if asked to - if (ent == inflictor and tbl.AffectSelf) then canhit = true - --self shouldn't hurt itself if asked not to - elseif (ent == inflictor and not tbl.AffectSelf) then canhit = false - --other players need to consent, bots don't care about it - elseif damage_zone_consents[ent] == true or ent:IsBot() then canhit = true - --other players that didn't consent are excluded - else canhit = false end - - elseif (tbl.NPC and damageable_point_ent_classes[ent:GetClass()] ~= false) or (tbl.PointEntities and (damageable_point_ent_classes[ent:GetClass()] == true)) then + + if is_player then + if tbl.Players then + canhit = true + --rules for players: + --self can always hurt itself if asked to + if (ent == inflictor and not tbl.AffectSelf) then + canhit = false --self shouldn't hurt itself if asked not to + elseif (damage_zone_consents[ent] == true) or ent:IsBot() then + canhit = true --other players need to consent, bots don't care about it + --other players that didn't consent are excluded + else + canhit = false + end + end + elseif is_npc then + if tbl.NPC then + canhit = true + end + elseif tbl.PointEntities and (damageable_point_ent_classes[ent:GetClass()] == true) then canhit = true end --apply prop protection - if IsPropProtected(ent, inflictor) or prop_protected_consent then + if (IsPropProtected(ent, inflictor) and IsValid(owner) and damage_zone_consents[target_ply]) or prop_protected_consent or (ent.CPPICanDamage and not ent:CPPICanDamage(ply)) then canhit = false end @@ -993,6 +1072,8 @@ if SERVER then local function ProcessForcesList(ents_hits, tbl, pos, ang, ply) local ent_count = 0 for i,v in pairs(ents_hits) do + if v.CPPICanPickup and not v:CPPICanPickup(ply) then ents_hits[i] = nil end + if v.CPPICanPunt and not v:CPPICanPunt(ply) then ents_hits[i] = nil end if pre_excluded_ent_classes[v:GetClass()] or (v:IsNPC() and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectSelf)) then ents_hits[i] = nil else ent_count = ent_count + 1 end end @@ -1000,6 +1081,7 @@ if SERVER then if TooManyEnts(ent_count) and not (tbl.AffectSelf and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end for _,ent in pairs(ents_hits) do local phys_ent + local owner = Try_CPPIGetOwner(ent) if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) and ( ent:IsPlayer() @@ -1085,8 +1167,6 @@ if SERVER then +ang2:Up()*tbl.AddedVectorForce.z end - - if tbl.TorqueMode == "Global" then add_angvel = tbl.Torque elseif tbl.TorqueMode == "Local" then @@ -1100,13 +1180,19 @@ if SERVER then local islocaltorque = tbl.TorqueMode == "TargetLocal" + local mass = 1 + if IsValid(phys_ent) then + if phys_ent.GetMass then + phys_ent:GetMass() + end + end if is_phys and tbl.AccountMass then if not (string.find(ent:GetClass(), "npc") ~= nil) then - addvel = addvel * (1 / math.max(phys_ent:GetMass(),0.1)) + addvel = addvel * (1 / math.max(mass,0.1)) else addvel = addvel end - add_angvel = add_angvel * (1 / math.max(phys_ent:GetMass(),0.1)) + add_angvel = add_angvel * (1 / math.max(mass,0.1)) end if tbl.Falloff then @@ -1132,7 +1218,7 @@ if SERVER then addvel = addvel * dist_multiplier add_angvel = add_angvel * dist_multiplier - local unconsenting_owner = ent:GetCreator() ~= ply and force_consents[ent:GetCreator()] == false + local unconsenting_owner = owner ~= ply and force_consents[owner] == false if (ent:IsPlayer() and tbl.Players) or (ent == ply and tbl.AffectSelf) then if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then @@ -1141,7 +1227,7 @@ if SERVER then end elseif (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) and tbl.PhysicsProps then - if not IsPropProtected(ent, ply) and not (global_combat_prop_protection:GetBool() and unconsenting_owner) then + if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then if IsValid(phys_ent) then ent:PhysWake() ent:SetVelocity(final_damping * oldvel + addvel) @@ -1159,7 +1245,7 @@ if SERVER then end end elseif (ent:IsNPC() or string.find(ent:GetClass(), "npc") ~= nil) and tbl.NPC then - if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then + if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then if phys_ent:GetVelocity():Length() > 500 then local vec = oldvel + addvel local clamp_vec = vec:GetNormalized()*500 @@ -1168,7 +1254,7 @@ if SERVER then else ent:SetVelocity((oldvel * final_damping) + addvel) end end elseif tbl.PointEntities then - if not IsPropProtected(ent, ply) and not global_combat_prop_protection:GetBool() and not unconsenting_owner then + if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then phys_ent:SetVelocity(final_damping * oldvel + addvel) end end @@ -1269,6 +1355,10 @@ if SERVER then return end ApplyLockState(ply, false) + if ply.default_movetype and ply.lock_state_applied and not (ulx and (ply.frozen or ply.jail)) then + ply:SetMoveType(ply.default_movetype) + targ_ent.default_movetype_reserved = nil + end MsgC(Color(0,255,255), "Requesting lock break!\n") if ply.grabbed_by then --directly go for the grabbed_by player @@ -1708,6 +1798,7 @@ if SERVER then if not lock_allow:GetBool() then return end if not lock_allow_grab:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end + --netrate enforce if not CountNetMessage(ply) then @@ -1735,6 +1826,9 @@ if SERVER then local alt_ang = net.ReadAngle() local ask_drawviewer = net.ReadBool() + if targ_ent.CPPICanPhysgun and not targ_ent:CPPICanPhysgun(ply) then return end + if ulx and (targ_ent.frozen or targ_ent.jail) then return end --we can't grab frozen/jailed players either + if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then ApplyLockState(targ_ent, false) if ply.grabbed_ents then @@ -1749,12 +1843,19 @@ if SERVER then local prop_protected, reason = IsPropProtected(targ_ent, ply) - local unconsenting_owner = targ_ent:GetCreator() ~= ply and (grab_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) - local calcview_unconsenting = targ_ent:GetCreator() ~= ply and (calcview_consents[targ_ent:GetCreator()] == false or (targ_ent:IsPlayer() and calcview_consents[targ_ent] == false)) + local owner = Try_CPPIGetOwner(targ_ent) - if unconsenting_owner or (global_combat_prop_protection:GetBool() and prop_protected) then return end - local targ_ent_owner = targ_ent:GetCreator() or targ_ent + local unconsenting_owner = owner ~= ply and (grab_consents[owner] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) + local calcview_unconsenting = owner ~= ply and (calcview_consents[owner] == false or (targ_ent:IsPlayer() and calcview_consents[targ_ent] == false)) + + if unconsenting_owner then + if owner:IsPlayer() then return + elseif (global_combat_prop_protection:GetBool() and prop_protected) then return + end + end + + local targ_ent_owner = owner or targ_ent local auth_ent_owner = ply auth_ent_owner.grabbed_ents = auth_ent_owner.grabbed_ents or {} @@ -1766,7 +1867,7 @@ if SERVER then elseif targ_ent:IsPlayer() then --if not the same player, we cannot grab consent_break_condition = true breakup_condition = breakup_condition .. "cannot grab another player if they don't consent to grabs, " - elseif global_combat_prop_protection:GetBool() and targ_ent:GetCreator() ~= ply then + elseif global_combat_prop_protection:GetBool() and owner ~= ply then --if entity not owned by grabbing player, he cannot do it to other players' entities in the prop-protected mode consent_break_condition = true breakup_condition = breakup_condition .. "cannot grab another player's owned entities if they don't consent to grabs, " @@ -1850,6 +1951,11 @@ if SERVER then targ_ent:SetPos(pos) + if not targ_ent.lock_state_applied and not targ_ent.default_movetype_reserved then + targ_ent.default_movetype = targ_ent:GetMoveType() + targ_ent.default_movetype_reserved = true + targ_ent.lock_state_applied = true + end ApplyLockState(targ_ent, true, no_collide) if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end @@ -1928,7 +2034,18 @@ if SERVER then local delay = net.ReadFloat() local targ_ent = net.ReadEntity() local auth_ent = net.ReadEntity() + if targ_ent.CPPICanPhysgun and not targ_ent:CPPICanPhysgun(ply) then return end + local prop_protected, reason = IsPropProtected(targ_ent, ply) + local owner = Try_CPPIGetOwner(targ_ent) + + local unconsenting_owner = owner ~= ply and (grab_consents[owner] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) + if unconsenting_owner then + if owner:IsPlayer() then return + elseif (global_combat_prop_protection:GetBool() and prop_protected) then return + end + end + targ_ent:SetAngles(ang) ApplyLockState(targ_ent, false) From f82ef581467d1c3fd052f122fc9ba4c3d0e6ddaf Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 13 Dec 2023 22:13:16 -0500 Subject: [PATCH 124/300] fix for hitscan inside main modified damage hook separate the statements; the previous edit didn't go to where we need to check multipliers and extra health bars --- lua/pac3/extra/shared/net_combat.lua | 41 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 072ea8fa1..8f720978b 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -655,31 +655,30 @@ if SERVER then local remaining_dmg,surviving_layer,side_effect_dmg = GetHPBarDamage(target, dmginfo:GetDamage()) if IsValid(dmginfo:GetInflictor()) then - if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and hitscan_consents[target] == false then - dmginfo:SetDamage(0) + if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and hitscan_consents[target] == false then --unconsenting for pac hitscans = no damage, exit now + return true end - else - local total_hp_value,built_tbl = GatherExtraHPBars(target) - if surviving_layer == nil or total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult - - if cumulative_mult < 0 then - target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(dmginfo:GetDamage()),0,target:GetMaxHealth()))) - return true - else - dmginfo:SetDamage(remaining_dmg) - if target.pac_healthbars then SendUpdateHealthBars(target) end - end + end + + local total_hp_value,built_tbl = GatherExtraHPBars(target) + if surviving_layer == nil or total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult - else --shields = use the calculated cumulative side effect damage from each uid's related absorbfactor + if cumulative_mult < 0 then + target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(dmginfo:GetDamage()),0,target:GetMaxHealth()))) + return true + else + dmginfo:SetDamage(remaining_dmg) + if target.pac_healthbars then SendUpdateHealthBars(target) end + end - if side_effect_dmg < 0 then - target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(side_effect_dmg),0,target:GetMaxHealth()))) - return true - else - dmginfo:SetDamage(side_effect_dmg + remaining_dmg) - SendUpdateHealthBars(target) - end + else --shields = use the calculated cumulative side effect damage from each uid's related absorbfactor + if side_effect_dmg < 0 then + target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(side_effect_dmg),0,target:GetMaxHealth()))) + return true + else + dmginfo:SetDamage(side_effect_dmg + remaining_dmg) + SendUpdateHealthBars(target) end end From 0d703b68bdf585654b9770a445f906facff83540 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 13 Dec 2023 22:35:08 -0500 Subject: [PATCH 125/300] try a different fix instead it was related to parts being initialized on creation, instead of the tree's validity. the part's tree node field has a chance to not exist yet at that point in time --- lua/pac3/editor/client/panels/properties.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index b12c247c3..756380ce0 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -291,7 +291,6 @@ end) pac.AddHook("PostRenderVGUI", "flash_properties", function() if not pace.flashes then return end - if not IsValid(pace.tree) or not IsValid(pace.properties) then return end for pnl, tbl in pairs(pace.flashes) do if IsValid(pnl) then --print(pnl:LocalToScreen(0,0)) @@ -352,7 +351,9 @@ do -- container end do --scroll to the tree node - pace.tree:ScrollToChild(self:GetChildren()[1].part.pace_tree_node) + if self:GetChildren()[1].part.pace_tree_node then + pace.tree:ScrollToChild(self:GetChildren()[1].part.pace_tree_node) + end end end From 16b5269a6c2caa9af79238e7497de3a5de84e650 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 14 Dec 2023 17:28:32 -0500 Subject: [PATCH 126/300] don't set cppi owner on the set owner hack if present, a prop protection addon should be the only one doing that --- lua/pac3/extra/shared/net_combat.lua | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 8f720978b..aae9bb561 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -208,11 +208,8 @@ if SERVER then end - local function Try_CPPISetOwner(ent, ply) - if ent.CPPISetOwner then --a prop protection using CPPI probably exists - ent:CPPISetOwner(ply) - end - ent.pac_prop_protection_owner = ply --otherwise we'll have this field + local function SetNoCPPIFallbackOwner(ent, ply) + ent.pac_prop_protection_owner = ply end local function Try_CPPIGetOwner(ent) @@ -225,25 +222,25 @@ if SERVER then --hack fix to stop GetOwner returning [NULL Entity] --uses CPPI interface from prop protectors if present hook.Add("PlayerSpawnedProp", "HackReAssignOwner", function(ply, model, ent) - Try_CPPISetOwner(ent, ply) + SetNoCPPIFallbackOwner(ent, ply) end) hook.Add("PlayerSpawnedNPC", "PAC_HackReAssignOwner", function(ply, ent) - Try_CPPISetOwner(ent, ply) + SetNoCPPIFallbackOwner(ent, ply) end) hook.Add("PlayerSpawnedRagdoll", "PAC_HackReAssignOwner", function(ply, model, ent) - Try_CPPISetOwner(ent, ply) + SetNoCPPIFallbackOwner(ent, ply) end) hook.Add("PlayerSpawnedSENT", "PAC_HackReAssignOwner", function(ply, ent) - Try_CPPISetOwner(ent, ply) + SetNoCPPIFallbackOwner(ent, ply) end) hook.Add("PlayerSpawnedSWEP", "PAC_HackReAssignOwner", function(ply, ent) - Try_CPPISetOwner(ent, ply) + SetNoCPPIFallbackOwner(ent, ply) end) hook.Add("PlayerSpawnedVehicle", "PAC_HackReAssignOwner", function(ply, ent) - Try_CPPISetOwner(ent, ply) + SetNoCPPIFallbackOwner(ent, ply) end) hook.Add("PlayerSpawnedEffect", "PAC_HackReAssignOwner", function(ply, model, ent) - Try_CPPISetOwner(ent, ply) + SetNoCPPIFallbackOwner(ent, ply) end) local function IsPossibleContraptionEntity(ent) From 299a3e95ed508f01be0bf2506d144bcda60189ef Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 15 Dec 2023 18:28:19 -0500 Subject: [PATCH 127/300] more exit points if lock is deactivated --- lua/pac3/core/client/parts/lock.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 5041a767c..b2857b183 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -60,7 +60,7 @@ BUILDER:EndStorableVars() function PART:OnThink() if not GetConVar('pac_sv_lock'):GetBool() then return end - if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end + if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end @@ -294,7 +294,7 @@ function PART:SetRadius(val) end function PART:OnShow() - + if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end local origin_part self.is_first_time = true if self.resetting_condition or self.forcebreak then @@ -373,6 +373,7 @@ function PART:OnHide() self.grabbing = false if self.target_ent == nil then return else self.target_ent.IsGrabbed = false self.target_ent.IsGrabbedID = nil end + if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end self:reset_ent_ang() end From 0d285517471aa19cbcaea56b50e218bb27b4fbe4 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 16 Dec 2023 12:15:09 -0500 Subject: [PATCH 128/300] more exit points if healthmod disabled --- lua/pac3/core/client/parts/health_modifier.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 39aab1fff..24acc7e30 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -134,6 +134,7 @@ end function PART:OnRemove() if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + if util.NetworkStringToID( "pac_request_healthmod" ) == 0 then return end local found_remaining_healthmod = false for _,part in pairs(pac.GetLocalParts()) do if part.ClassName == "health_modifier" and part ~= self then @@ -172,6 +173,7 @@ function PART:OnShow() end function PART:OnHide() + if util.NetworkStringToID( "pac_request_healthmod" ) == 0 then self:SetError("This part is deactivated on the server") return end if self.HPBarsResetOnHide then net.Start("pac_request_healthmod") net.WriteString(self.UniqueID) @@ -200,4 +202,4 @@ function PART:Initialize() end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From 3c6705ebe5e876a7e87a0915fcc6bdba08c285b8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 17 Dec 2023 16:20:20 -0500 Subject: [PATCH 129/300] temporary settings menu fix attempt remove some server-only cvars --- lua/pac3/editor/client/settings.lua | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 7c46792e0..f70df4cbb 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1339,18 +1339,6 @@ function pace.FillServerSettings(pnl) webcontent_no_content_box:SetSize(400,30) webcontent_no_content_box:SetConVar("sv_pac_webcontent_allow_no_content_length") - local contraption_box = vgui.Create("DCheckBoxLabel", misc_list_list) - contraption_box:SetText("allow contraptions") - contraption_box:SetSize(400,30) - contraption_box:SetConVar("pac_to_contraption_allow") - - local contraption_entities_numbox = vgui.Create("DNumSlider", misc_list_list) - contraption_entities_numbox:SetText("PAC3 contraption entities limit") - contraption_entities_numbox:SetValue(GetConVar("pac_max_contraption_entities"):GetInt()) - contraption_entities_numbox:SetMin(0) contraption_entities_numbox:SetDecimals(0) contraption_entities_numbox:SetMax(200) - contraption_entities_numbox:SetSize(400,30) - contraption_entities_numbox:SetConVar("pac_max_contraption_entities") - local cam_restrict_box = vgui.Create("DCheckBoxLabel", misc_list_list) cam_restrict_box:SetText("restrict PAC editor camera movement") cam_restrict_box:SetSize(400,30) From a8c1adc8f0c5f3533fd303c464faaa64c24a478f Mon Sep 17 00:00:00 2001 From: Redox Date: Wed, 20 Dec 2023 00:40:41 +0100 Subject: [PATCH 130/300] Auto replace lower case with underscores for part convars --- lua/pac3/core/client/parts.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts.lua b/lua/pac3/core/client/parts.lua index ebebadf88..29de09872 100644 --- a/lua/pac3/core/client/parts.lua +++ b/lua/pac3/core/client/parts.lua @@ -86,7 +86,8 @@ function pac.RegisterPart(META) assert(istable(META.StorableVars), "Part " .. META.ClassName .. " has no StorableVars") do - local cvar = CreateClientConVar("pac_enable_" .. META.ClassName, "1", true) + local cvarName = "pac_enable_" .. string.Replace(META.ClassName, " ", "_"):lower() + local cvar = CreateClientConVar(cvarName, "1", true) cvars.AddChangeCallback("pac_enable_" .. META.ClassName, function(name, old, new) local enable = tobool(new) From 6d0c3ffb1ed22a249f93d4bcc660a3e50e7199db Mon Sep 17 00:00:00 2001 From: Redox Date: Wed, 20 Dec 2023 00:57:54 +0100 Subject: [PATCH 131/300] Apply cvar fix to other files too --- lua/pac3/core/client/base_part.lua | 3 ++- lua/pac3/core/client/parts.lua | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 7c840fef1..a9de08e13 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -62,7 +62,8 @@ function PART:PreInitialize() self.hide_disturbing = false self.active_events = {} self.active_events_ref_count = 0 - if not GetConVar("pac_enable_" .. self.ClassName):GetBool() then self:SetWarning("This part class is disabled! Enable it with " .. "pac_enable_" .. self.ClassName .. " 1") end + local cvarName = "pac_enable_" .. string.Replace(self.ClassName, " ", "_"):lower() + if not GetConVar(cvarName):GetBool() then self:SetWarning("This part class is disabled! Enable it with " .. cvarName .. " 1") end end function PART:Initialize() end diff --git a/lua/pac3/core/client/parts.lua b/lua/pac3/core/client/parts.lua index 29de09872..3fc9157b8 100644 --- a/lua/pac3/core/client/parts.lua +++ b/lua/pac3/core/client/parts.lua @@ -89,7 +89,7 @@ function pac.RegisterPart(META) local cvarName = "pac_enable_" .. string.Replace(META.ClassName, " ", "_"):lower() local cvar = CreateClientConVar(cvarName, "1", true) - cvars.AddChangeCallback("pac_enable_" .. META.ClassName, function(name, old, new) + cvars.AddChangeCallback(cvarName, function(name, old, new) local enable = tobool(new) META.GloballyEnabled = enable if enable then From 7950cca4a7f89352a5a732103b3bb6df2bf95cf4 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 20 Dec 2023 21:06:18 -0500 Subject: [PATCH 132/300] small changes validity check remove wearing tracker, it will be managed elsewhere remove unused loop --- lua/pac3/editor/client/parts.lua | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 2285a1554..6daa26037 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -72,7 +72,7 @@ local function BulkSelectRefreshFadedNodes(part_trace) for _,v in ipairs(pace.BulkSelectList) do if not v:IsValid() then table.RemoveByValue(pace.BulkSelectList, v) - elseif v.pace_tree_node then + elseif IsValid(v.pace_tree_node) then v.pace_tree_node:SetAlpha( 150 ) end end @@ -182,7 +182,6 @@ end function pace.WearParts(temp_wear_filter) - pace.still_loading_wearing = true local allowed, reason = pac.CallHook("CanWearParts", pac.LocalPlayer) @@ -191,8 +190,6 @@ function pace.WearParts(temp_wear_filter) return end - pace.still_loading_wearing = false - return pace.WearOnServer(temp_wear_filter) end @@ -1434,9 +1431,6 @@ do -- menu end) end - for _,v in ipairs(pace.BulkSelectList) do - --v.pace_tree_node:SetAlpha( 150 ) - end end function pace.RemoveFromBulkSelect(obj) From c8542c50d416abdcd8c2a2823d5073541dfe8e79 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 20 Dec 2023 21:21:56 -0500 Subject: [PATCH 133/300] small checks localize the variable first then check whether that table exists --- lua/pac3/editor/client/asset_browser.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/asset_browser.lua b/lua/pac3/editor/client/asset_browser.lua index b0f0e5571..7e9542acc 100644 --- a/lua/pac3/editor/client/asset_browser.lua +++ b/lua/pac3/editor/client/asset_browser.lua @@ -152,7 +152,7 @@ local function install_click(icon, path, pattern, on_menu, pathid) end SetClipboardText(path) end) - + local resource_type = "" if string.match(path, "^materials/(.+)%.vmt$") or string.match(path, "^materials/(.+%.png)$") then resource_type = "materials" elseif string.match(path, "^models/") then resource_type = "models" end @@ -161,7 +161,7 @@ local function install_click(icon, path, pattern, on_menu, pathid) elseif not pace.bookmarked_ressources[resource_type] then pace.SaveRessourceBookmarks() end - if GetConVar("pac_asset_browser_extra_options"):GetBool() then + if GetConVar("pac_asset_browser_extra_options"):GetBool() and pace.bookmarked_ressources[resource_type] then if GetConVar("pac_favorites_try_to_get_asset_series"):GetBool() then if not table.HasValue(pace.bookmarked_ressources[resource_type], path) then menu:AddOption(L"add series to favorites", function() From 9931c69ce6cd57ccb7e7be4f97bd24e96ce0db02 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 20 Dec 2023 21:24:44 -0500 Subject: [PATCH 134/300] wearing tracker can handle multiple groups --- lua/pac3/editor/client/wear.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index b63837dc9..ab0138f35 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -46,7 +46,8 @@ function pace.ClearParts() end) end - +--wearing tracker counter +pace.still_loading_wearing_count = 0 do -- to server local function net_write_table(tbl) @@ -95,6 +96,15 @@ do -- to server net.SendToServer() pac.Message(('Transmitting outfit %q to server (%s)'):format(part.Name or part.ClassName or '', string.NiceSize(bytes))) + pace.still_loading_wearing = true + pace.still_loading_wearing_count = pace.still_loading_wearing_count + 1 --this group is added to the tracked wear count + timer.Simple(8, function() + pace.still_loading_wearing_count = pace.still_loading_wearing_count - 1 --assume max 8 seconds to wear + if pace.still_loading_wearing_count == 0 then --if this is the last group to wear, we're done + pace.still_loading_wearing = false + end + end) + return true end From 3862881ea3517f81c4ea8966dc4a93d6e8ad3ff9 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 20 Dec 2023 21:52:25 -0500 Subject: [PATCH 135/300] Update damage_zone.lua rework the initialize delaying hack and try the wearing tracker to prevent wrong activations the print for hitmarkers only shows if doing preview for testing remove some unused code --- lua/pac3/core/client/parts/damage_zone.lua | 30 +++++++++------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index cf44e8c16..bd5f82c4f 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -535,6 +535,7 @@ function PART:SendNetMessage() end function PART:OnShow() + if pace.still_loading_wearing then return end if self.validTime > SysTime() then return end if self.Preview then @@ -603,6 +604,7 @@ function PART:OnShow() local start = SysTime() local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") if not ent:IsValid() then return end + ent.is_pac_hitmarker = true ent:SetNoDraw(true) ent:SetOwner(self:GetPlayerOwner(true)) ent:SetPos(pos) @@ -637,7 +639,7 @@ function PART:OnShow() self:AssignFloatingPartToEntity(newpart, owner, ent, parent_ent, part.UniqueID, csent_id) - MsgC(bool and Color(0,255,0) or Color(0,200,255), bool and "existing" or "created", " : ", newpart, "\n") + if self.Preview then MsgC("hitmarker:", bool and Color(0,255,0) or Color(0,200,255), bool and "existing" or "created", " : ", newpart, "\n") end timer.Simple(math.Clamp(duration, 0, 8), function() if ent:IsValid() then if parent_ent.pac_dmgzone_hitmarker_ents then @@ -648,10 +650,6 @@ function PART:OnShow() end end end - --[[timer.Simple(0.5, function() - RemoveHitMarker(owner, ent, part.UniqueID, csent_id) - SafeRemoveEntity(ent) - end)]] end end) end @@ -662,10 +660,6 @@ function PART:OnShow() return creation_delta end - if IsValid(self.HitMarkerPart) then - --(self.HitMarkerPart, "price is", string.NiceSize(CalculateHitMarkerPrice(self.HitMarkerPart))) - end - if hit then self.dmgzone_hit_done = CurTime() --try not to play both sounds at once @@ -704,13 +698,7 @@ function PART:OnShow() end end end - if IsValid(self.HitMarkerPart) then - --print("runtimes were " .. string.FormattedTime( part_setup_runtimes).ms .. "ms.\n\t" .. 1000*part_setup_runtimes .. " impact.\n\t" .. table.Count(self.HitMarkerPart:GetChildrenList()) .. " children, " .. 1000*part_setup_runtimes / table.Count(self.HitMarkerPart:GetChildrenList()) .. " impact per children") - end if owner.hitparts then - for i,v in ipairs(owner.hitparts) do - --print(i,v,v.active,v.specimen_part,v.hitmarker_id,v.template_uid) - end self:SetInfo(table.Count(owner.hitparts) .. " hitmarkers in slot") end @@ -998,9 +986,15 @@ function PART:BuildCone(obj) end function PART:Initialize() - 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() + 2 + self.validTime = SysTime() + 5 --jank fix to try to stop activation on load + timer.Simple(0.1, function() --jank fix on the jank fix to allow it earlier on projectiles and hitmarkers + if IsValid(self:GetRootPart():GetOwner()) then + if self:GetRootPart():GetOwner():GetClass() == "pac_projectile" or self:GetRootPart():GetOwner().is_pac_hitmarker then + self.validTime = 0 + end + end + end) end @@ -1035,4 +1029,4 @@ function PART:SetDamage(val) end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From fe187940541f7ba666539743708c01cf51d35d1a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 20 Dec 2023 21:54:31 -0500 Subject: [PATCH 136/300] make wear tracker into a reusable function --- lua/pac3/editor/client/wear.lua | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index ab0138f35..a81c91e7e 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -48,6 +48,18 @@ end --wearing tracker counter pace.still_loading_wearing_count = 0 +--reusable function +function pace.ExtendWearTracker(duration) + if not duration or not isnumber(duration) then duration = 1 end + pace.still_loading_wearing = true + pace.still_loading_wearing_count = pace.still_loading_wearing_count + 1 --this group is added to the tracked wear count + timer.Simple(duration, function() + pace.still_loading_wearing_count = pace.still_loading_wearing_count - 1 --assume max 8 seconds to wear + if pace.still_loading_wearing_count == 0 then --if this is the last group to wear, we're done + pace.still_loading_wearing = false + end + end) +end do -- to server local function net_write_table(tbl) @@ -96,14 +108,7 @@ do -- to server net.SendToServer() pac.Message(('Transmitting outfit %q to server (%s)'):format(part.Name or part.ClassName or '', string.NiceSize(bytes))) - pace.still_loading_wearing = true - pace.still_loading_wearing_count = pace.still_loading_wearing_count + 1 --this group is added to the tracked wear count - timer.Simple(8, function() - pace.still_loading_wearing_count = pace.still_loading_wearing_count - 1 --assume max 8 seconds to wear - if pace.still_loading_wearing_count == 0 then --if this is the last group to wear, we're done - pace.still_loading_wearing = false - end - end) + pace.ExtendWearTracker(8) return true end From 33a0fd87d3f912bbae457d5b5cfe8ca935da155d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 20 Dec 2023 22:03:55 -0500 Subject: [PATCH 137/300] use wear tracker for removepart to prepare for a fix for some parts activating when their events are removed before them --- lua/pac3/editor/client/parts.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 6daa26037..37c6e5322 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1231,6 +1231,7 @@ do -- menu end function pace.RemovePart(obj) + pace.ExtendWearTracker(1) if table.HasValue(pace.BulkSelectList,obj) then table.RemoveByValue(pace.BulkSelectList,obj) end pace.RecordUndoHistory() From 0a6c46ef465f595a1115bf424801e89e2c9b217a Mon Sep 17 00:00:00 2001 From: techbot Date: Fri, 22 Dec 2023 00:15:53 +0100 Subject: [PATCH 138/300] use pac hooks when possible should help prevent showing stuff when pac is disabled --- lua/autorun/pac_restart.lua | 7 +- lua/pac3/core/client/ear_grab_animation.lua | 2 +- lua/pac3/core/client/init.lua | 13 ++-- lua/pac3/core/client/integration_tools.lua | 18 ++--- lua/pac3/core/client/part_pool.lua | 4 +- lua/pac3/core/client/parts/damage_zone.lua | 16 ++-- lua/pac3/core/client/parts/legacy/model.lua | 10 +-- lua/pac3/core/client/parts/model.lua | 2 +- lua/pac3/core/client/test.lua | 14 ++-- lua/pac3/core/client/util.lua | 25 ++++-- lua/pac3/core/server/in_skybox.lua | 2 +- lua/pac3/core/server/init.lua | 6 +- lua/pac3/core/server/util.lua | 2 +- lua/pac3/core/shared/entity_mutator.lua | 12 +-- lua/pac3/core/shared/footsteps_fix.lua | 14 ++-- lua/pac3/editor/client/init.lua | 4 +- lua/pac3/editor/client/panels/tree.lua | 1 - lua/pac3/editor/client/parts.lua | 76 +++++++++---------- lua/pac3/editor/client/saved_parts.lua | 4 +- lua/pac3/editor/client/settings.lua | 4 +- lua/pac3/editor/client/show_outfit_on_use.lua | 8 +- lua/pac3/editor/client/wear.lua | 14 ++-- lua/pac3/editor/server/util.lua | 2 +- lua/pac3/editor/server/wear.lua | 10 +-- lua/pac3/extra/shared/init.lua | 2 +- 25 files changed, 141 insertions(+), 131 deletions(-) diff --git a/lua/autorun/pac_restart.lua b/lua/autorun/pac_restart.lua index 8c7e527b1..40eba4f1c 100644 --- a/lua/autorun/pac_restart.lua +++ b/lua/autorun/pac_restart.lua @@ -4,7 +4,7 @@ if SERVER then return end -local sv_allowcslua = GetConVar('sv_allowcslua') +local sv_allowcslua = GetConVar("sv_allowcslua") local prefer_local_version = CreateClientConVar("pac_restart_prefer_local_version", "0") function _G.pac_ReloadParts() @@ -92,7 +92,7 @@ function _G.pac_Restart() if pac and pac.Disable then pacLocal.Message("removing all traces of pac3 from lua") - pac.Disable() + pac.Disable(true) pac.Panic() if pace and pace.Editor then @@ -110,7 +110,8 @@ function _G.pac_Restart() for hook_name, hooks in pairs(hook.GetTable()) do for id, func in pairs(hooks) do - if isstring(id) and (id:StartWith("pace_") or id:StartWith("pac_") or id:StartWith("pac3_") or id:StartWith("pacx_")) then + local lower = isstring(id) and string.lower(id) + if lower and (lower:StartWith("pace_") or lower:StartWith("pac_") or lower:StartWith("pac3_") or lower:StartWith("pacx_")) then hook.Remove(hook_name, id) end end diff --git a/lua/pac3/core/client/ear_grab_animation.lua b/lua/pac3/core/client/ear_grab_animation.lua index 95c651c62..511def904 100644 --- a/lua/pac3/core/client/ear_grab_animation.lua +++ b/lua/pac3/core/client/ear_grab_animation.lua @@ -1,5 +1,5 @@ -- see https://github.com/Facepunch/garrysmod/blob/master/garrysmod/gamemodes/base/gamemode/animations.lua#L235 -hook.Add("PostGamemodeLoaded", "pac_ear_grab_animation",function() +pac.AddHook("PostGamemodeLoaded", "ear_grab_animation", function() if GAMEMODE.GrabEarAnimation then -- only add it if it exists local original_ear_grab_animation = GAMEMODE.GrabEarAnimation GAMEMODE.GrabEarAnimation = function(_, ply) diff --git a/lua/pac3/core/client/init.lua b/lua/pac3/core/client/init.lua index c96b0d63f..92faf69bc 100644 --- a/lua/pac3/core/client/init.lua +++ b/lua/pac3/core/client/init.lua @@ -27,14 +27,15 @@ do pac_enable:SetBool(true) end - function pac.Disable() + function pac.Disable(temp) pac.EnableDrawnEntities(false) pac.DisableAddedHooks() pac.CallHook("Disable") - pac_enable:SetBool(false) + if not temp then + pac_enable:SetBool(false) + end end end - include("util.lua") include("class.lua") @@ -58,7 +59,7 @@ include("ear_grab_animation.lua") pac.LoadParts() -hook.Add("OnEntityCreated", "pac_init", function(ent) +pac.AddHook("OnEntityCreated", "init", function(ent) local ply = LocalPlayer() if not ply:IsValid() then return end @@ -67,8 +68,8 @@ hook.Add("OnEntityCreated", "pac_init", function(ent) pac.LocalHands = pac.LocalPlayer:GetHands() pac.in_initialize = true - hook.Run("pac_Initialized") + pac.CallHook("Initialized") pac.in_initialize = nil - hook.Remove("OnEntityCreated", "pac_init") + pac.RemoveHook("OnEntityCreated", "init") end) diff --git a/lua/pac3/core/client/integration_tools.lua b/lua/pac3/core/client/integration_tools.lua index caa033c4e..380266e15 100644 --- a/lua/pac3/core/client/integration_tools.lua +++ b/lua/pac3/core/client/integration_tools.lua @@ -25,7 +25,7 @@ end do local force_draw_localplayer = false - hook.Add("ShouldDrawLocalPlayer", "pac_draw_2d_entity", function() + pac.AddHook("ShouldDrawLocalPlayer", "draw_2d_entity", function() if force_draw_localplayer == true then return true end @@ -244,7 +244,7 @@ function pac.AddEntityClassListener(class, session, check_func, draw_dist) draw_dist = 0 check_func = check_func or function(ent) return ent:GetClass() == class end - local id = "pac_auto_attach_" .. class + local id = "auto_attach_" .. class local weapons = {} local function weapon_think() @@ -282,7 +282,7 @@ function pac.AddEntityClassListener(class, session, check_func, draw_dist) if ent:IsValid() and check_func(ent) then if ent:IsWeapon() then weapons[ent:EntIndex()] = ent - hook.Add("Think", id, weapon_think) + pac.AddHook("Think", id, weapon_think) else pac.SetupENT(ent) ent:AttachPACSession(session) @@ -302,8 +302,8 @@ function pac.AddEntityClassListener(class, session, check_func, draw_dist) created(ent) end - hook.Add("EntityRemoved", id, removed) - hook.Add("OnEntityCreated", id, created) + pac.AddHook("EntityRemoved", id, removed) + pac.AddHook("OnEntityCreated", id, created) end function pac.RemoveEntityClassListener(class, session, check_func) @@ -319,11 +319,11 @@ function pac.RemoveEntityClassListener(class, session, check_func) end end - local id = "pac_auto_attach_" .. class + local id = "auto_attach_" .. class - hook.Remove("Think", id) - hook.Remove("EntityRemoved", id) - hook.Remove("OnEntityCreated", id) + pac.RemoveHook("Think", id) + pac.RemoveHook("EntityRemoved", id) + pac.RemoveHook("OnEntityCreated", id) end timer.Simple(0, function() diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index a601ff395..40a701e3d 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -349,8 +349,8 @@ pac.AddHook("Think", "events", function() lamp:SetAngles( pac.LocalPlayer:EyeAngles() ) lamp:Update() - hook.Add("PostRender", "pac_flashlight_stuck_fix", function() - hook.Remove("PostRender", "pac_flashlight_stuck_fix") + pac.AddHook("PostRender", "flashlight_stuck_fix", function() + pac.RemoveHook("PostRender", "flashlight_stuck_fix") lamp:Remove() end) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index bd5f82c4f..d09294530 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -719,16 +719,16 @@ end) function PART:OnHide() - hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) + pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) for _,v in pairs(renderhooks) do - hook.Remove(v, "pace_draw_hitbox"..self.UniqueID) + pac.RemoveHook(v, "pace_draw_hitbox"..self.UniqueID) end end function PART:OnRemove() - hook.Remove(self.RenderingHook, "pace_draw_hitbox") + pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox") for _,v in pairs(renderhooks) do - hook.Remove(v, "pace_draw_hitbox") + pac.RemoveHook(v, "pace_draw_hitbox") end end @@ -738,16 +738,16 @@ function PART:PreviewHitbox() if previousRenderingHook ~= self.RenderingHook then for _,v in pairs(renderhooks) do - hook.Remove(v, "pace_draw_hitbox"..self.UniqueID) + pac.RemoveHook(v, "pace_draw_hitbox"..self.UniqueID) end previousRenderingHook = self.RenderingHook end if not self.Preview then return end - hook.Add(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID, function() - if not self.Preview then hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end - if not IsValid(self) then hook.Remove(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end + pac.AddHook(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID, function() + if not self.Preview then pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end + if not IsValid(self) then pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end self:GetWorldPosition() if self.HitboxMode == "Box" then local mins = Vector(-self.Radius, -self.Radius, -self.Length) diff --git a/lua/pac3/core/client/parts/legacy/model.lua b/lua/pac3/core/client/parts/legacy/model.lua index 97a34dd91..505ac48d1 100644 --- a/lua/pac3/core/client/parts/legacy/model.lua +++ b/lua/pac3/core/client/parts/legacy/model.lua @@ -662,13 +662,13 @@ do self.Mesh = nil local real_model = modelPath - local ret = hook.Run("pac_model:SetModel", self, modelPath, self.ModelFallback) + local ret = pac.CallHook("model:SetModel", self, modelPath, self.ModelFallback) if ret == nil then - real_model = pac.FilterInvalidModel(real_model,self.ModelFallback) + real_model = pac.FilterInvalidModel(real_model, self.ModelFallback) else modelPath = ret or modelPath real_model = modelPath - real_model = pac.FilterInvalidModel(real_model,self.ModelFallback) + real_model = pac.FilterInvalidModel(real_model, self.ModelFallback) end self.Model = modelPath @@ -682,7 +682,7 @@ do end end -local NORMAL = Vector(1,1,1) +local NORMAL = Vector(1, 1, 1) function PART:CheckScale() -- RenderMultiply doesn't work with this.. @@ -704,7 +704,7 @@ function PART:SetAlternativeScaling(b) end function PART:SetScale(var) - var = var or Vector(1,1,1) + var = var or Vector(1, 1, 1) self.Scale = var diff --git a/lua/pac3/core/client/parts/model.lua b/lua/pac3/core/client/parts/model.lua index d8fe5de4a..ad647adba 100644 --- a/lua/pac3/core/client/parts/model.lua +++ b/lua/pac3/core/client/parts/model.lua @@ -669,7 +669,7 @@ function PART:ProcessModelChange() end ) else - local status, reason = hook.Run('PAC3AllowMDLDownload', self:GetPlayerOwner(), self, path) + local status, reason = hook.Run("PAC3AllowMDLDownload", self:GetPlayerOwner(), self, path) if ALLOW_TO_MDL:GetBool() and status ~= false then self.loading = "downloading mdl zip" diff --git a/lua/pac3/core/client/test.lua b/lua/pac3/core/client/test.lua index 200f82616..b88b7d587 100644 --- a/lua/pac3/core/client/test.lua +++ b/lua/pac3/core/client/test.lua @@ -79,17 +79,17 @@ local function start_test(name, done) end function test.Setup() - hook.Add("ShouldDrawLocalPlayer", "pac_test", function() return true end) + pac.AddHook("ShouldDrawLocalPlayer", "test", function() return true end) end function test.Teardown() - hook.Remove("ShouldDrawLocalPlayer", "pac_test") + pac.RemoveHook("ShouldDrawLocalPlayer", "test") end function test.Run(done) error("test.Run is not defined") end function test.Remove() - hook.Remove("ShouldDrawLocalPlayer", "pac_test") - hook.Remove("Think", "pac_test_coroutine") + pac.RemoveHook("ShouldDrawLocalPlayer", "test") + pac.RemoveHook("Think", "test_coroutine") if test.done then return end @@ -172,7 +172,7 @@ local function start_test(name, done) test.Remove() end - hook.Add("Think", "pac_test_coroutine", function() + pac.AddHook("Think", "test_coroutine", function() if not test.co then return end local ok, err = coroutine.resume(test.co) @@ -213,7 +213,7 @@ concommand.Add("pac_test", function(ply, _, args) local current_test = nil - hook.Add("Think", "pac_tests", function() + pac.AddHook("Think", "tests", function() if current_test then if current_test.time < os.clock() then msg_error("test ", current_test.name, " timed out") @@ -232,7 +232,7 @@ concommand.Add("pac_test", function(ply, _, args) local name = table.remove(tests, 1) if not name then msg("finished testing") - hook.Remove("Think", "pac_tests") + pac.RemoveHook("Think", "tests") return end diff --git a/lua/pac3/core/client/util.lua b/lua/pac3/core/client/util.lua index 996eea451..a405e3ce7 100644 --- a/lua/pac3/core/client/util.lua +++ b/lua/pac3/core/client/util.lua @@ -364,28 +364,29 @@ do -- hook helpers pac.added_hooks = pac.added_hooks or {} function pac.AddHook(event_name, id, func, priority) - id = "pac_" .. id + id = isstring(id) and "pac_" .. id or id if not DLib and not ULib then priority = nil end - if pac.IsEnabled() then + print("adding", event_name, id) hook.Add(event_name, id, func, priority) + print("adding hooks", event_name .. tostring(id)) + pac.added_hooks[event_name .. tostring(id)] = {event_name = event_name, id = id, func = func, priority = priority} end - - pac.added_hooks[event_name .. id] = {event_name = event_name, id = id, func = func, priority = priority} end function pac.RemoveHook(event_name, id) - id = "pac_" .. id + id = "pac_" .. tostring(id) local data = pac.added_hooks[event_name .. id] if data then hook.Remove(data.event_name, data.id) - + print("removing", data.event_name, data.id) pac.added_hooks[event_name .. id] = nil + print("removing hooks", event_name .. id) end end @@ -395,13 +396,21 @@ do -- hook helpers function pac.EnableAddedHooks() for _, data in pairs(pac.added_hooks) do - hook.Add(data.event_name, data.id, data.func, data.priority) + if ispanel(data.id) and not IsValid(data.id) then -- Panels can be NULL and are (probably) already removed + pac.added_hooks[data.event_name .. tostring(data.id)] = nil + else + hook.Add(data.event_name, data.id, data.func, data.priority) + end end end function pac.DisableAddedHooks() for _, data in pairs(pac.added_hooks) do - hook.Remove(data.event_name, data.id) + if ispanel(data.id) and not IsValid(data.id) then -- Panels can be NULL and are already removed + pac.added_hooks[data.event_name .. tostring(data.id)] = nil + else + hook.Remove(data.event_name, data.id) + end end end end diff --git a/lua/pac3/core/server/in_skybox.lua b/lua/pac3/core/server/in_skybox.lua index 35d4c54a3..01f214272 100644 --- a/lua/pac3/core/server/in_skybox.lua +++ b/lua/pac3/core/server/in_skybox.lua @@ -1,4 +1,4 @@ -hook.Add("InitPostEntity","pac_get_sky_camera",function() +pac.AddHook("InitPostEntity", "get_sky_camera", function() local sky_camera = ents.FindByClass("sky_camera")[1] if sky_camera then local nwVarName = "pac_in_skybox" diff --git a/lua/pac3/core/server/init.lua b/lua/pac3/core/server/init.lua index 4e60b8688..eac2c9fda 100644 --- a/lua/pac3/core/server/init.lua +++ b/lua/pac3/core/server/init.lua @@ -7,8 +7,8 @@ pac.resource = include("pac3/libraries/resource.lua") CreateConVar("has_pac3", "1", {FCVAR_NOTIFY}) CreateConVar("pac_allow_blood_color", "1", {FCVAR_NOTIFY}, "Allow to use custom blood color") -CreateConVar('pac_allow_mdl', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow to use custom MDLs') -CreateConVar('pac_allow_mdl_entity', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow to use custom MDLs as Entity') +CreateConVar("pac_allow_mdl", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow to use custom MDLs") +CreateConVar("pac_allow_mdl_entity", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow to use custom MDLs as Entity") include("util.lua") @@ -20,4 +20,4 @@ include("net_messages.lua") include("test_suite_backdoor.lua") include("in_skybox.lua") -hook.Run("pac_Initialized") +pac.CallHook("Initialized") diff --git a/lua/pac3/core/server/util.lua b/lua/pac3/core/server/util.lua index 4e8918a62..923f340e4 100644 --- a/lua/pac3/core/server/util.lua +++ b/lua/pac3/core/server/util.lua @@ -32,7 +32,7 @@ function pac.AddHook(str, id, func, priority) local status, a, b, c, d, e, f, g = pcall(func, ...) if not status then - pac.Message('Error on hook ' .. str .. ' (' .. id .. ')! ', a) + pac.Message("Error on hook " .. str .. " (" .. id .. ")! ", a) return end diff --git a/lua/pac3/core/shared/entity_mutator.lua b/lua/pac3/core/shared/entity_mutator.lua index 77f20e4b3..6b03d57f6 100644 --- a/lua/pac3/core/shared/entity_mutator.lua +++ b/lua/pac3/core/shared/entity_mutator.lua @@ -301,15 +301,15 @@ if SERVER then end end - hook.Add("PlayerInitialSpawn", "pac_entity_mutators_spawn", function(ply) - local id = "pac_entity_mutators_spawn" .. ply:UniqueID() - hook.Add( "SetupMove", id, function(movingPly, _, cmd) + pac.AddHook("PlayerInitialSpawn", "entity_mutators_spawn", function(ply) + local id = "entity_mutators_spawn" .. ply:UniqueID() + pac.AddHook( "SetupMove", id, function(movingPly, _, cmd) if not ply:IsValid() then - hook.Remove("SetupMove", id) + pac.RemoveHook("SetupMove", id) elseif movingPly == ply and not cmd:IsForced() then emut.ReplicateMutatorsForPlayer(ply) - hook.Remove("SetupMove", id) + pac.RemoveHook("SetupMove", id) end end) end) @@ -323,7 +323,7 @@ function emut.RemoveMutationsForPlayer(ply) end end -hook.Add("EntityRemoved", "pac_entity_mutators_left", function(ent) +pac.AddHook("EntityRemoved", "entity_mutators_left", function(ent) if not IsValid(ent) then return end if ent:IsPlayer() then if Player(ent:UserID()) == NULL then diff --git a/lua/pac3/core/shared/footsteps_fix.lua b/lua/pac3/core/shared/footsteps_fix.lua index 8440073a2..c38ac2382 100644 --- a/lua/pac3/core/shared/footsteps_fix.lua +++ b/lua/pac3/core/shared/footsteps_fix.lua @@ -1,11 +1,11 @@ if game.SinglePlayer() then if SERVER then - util.AddNetworkString('pac_footstep') - util.AddNetworkString('pac_footstep_request_state_update') - util.AddNetworkString('pac_signal_mute_footstep') + util.AddNetworkString("pac_footstep") + util.AddNetworkString("pac_footstep_request_state_update") + util.AddNetworkString("pac_signal_mute_footstep") - hook.Add("PlayerFootstep", "footstep_fix", function(ply, pos, _, snd, vol) + pac.AddHook("PlayerFootstep", "footstep_fix", function(ply, pos, _, snd, vol) net.Start("pac_footstep_request_state_update") net.Send(ply) @@ -17,14 +17,14 @@ if game.SinglePlayer() then net.Broadcast() end) - net.Receive("pac_signal_mute_footstep", function(len,ply) + net.Receive("pac_signal_mute_footstep", function(len, ply) local b = net.ReadBool() ply.pac_mute_footsteps = b if ply.pac_mute_footsteps then - hook.Add("PlayerFootstep", "pac_footstep_silence", function() + pac.AddHook("PlayerFootstep", "footstep_silence", function() return b end) - else hook.Remove("PlayerFootstep", "pac_footstep_silence") end + else pac.RemoveHook("PlayerFootstep", "footstep_silence") end end) diff --git a/lua/pac3/editor/client/init.lua b/lua/pac3/editor/client/init.lua index b4811d6f0..8980714a4 100644 --- a/lua/pac3/editor/client/init.lua +++ b/lua/pac3/editor/client/init.lua @@ -221,7 +221,7 @@ function pace.Panic() if ent:IsValid() then ent.pac_onuse_only = nil ent.pac_onuse_only_check = nil - hook.Remove('pace_OnUseOnlyUpdates', ent) + pac.RemoveHook("pace_OnUseOnlyUpdates", ent) end end end @@ -313,7 +313,7 @@ do local up = Vector(0,0,10000) - hook.Add("HUDPaint", "pac_in_editor", function() + pac.AddHook("HUDPaint", "in_editor", function() for _, ply in ipairs(player.GetAll()) do if ply ~= pac.LocalPlayer and ply:GetNW2Bool("pac_in_editor") then diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index 83a6745b5..8f479046a 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -619,7 +619,6 @@ end pac.AddHook("pac_OnPartRemove", "pace_remove_tree_nodes", remove_node) - local last_refresh = 0 local function refresh(part) if last_refresh > SysTime() then return end diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 37c6e5322..eb77a71c1 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -12,7 +12,7 @@ pace.operations_default = {"help_part_info", "wear", "copy", "paste", "cut", "pa pace.operations_legacy = {"wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "spacer", "save", "load", "spacer", "remove"} pace.operations_experimental = {"help_part_info", "wear", "copy", "paste", "cut", "paste_properties", "clone", "bulk_select", "spacer", "registered_parts", "spacer", "bulk_apply_properties", "partsize_info", "copy_uid", "spacer", "save", "load", "spacer", "remove"} -pace.operations_bulk_poweruser = {"bulk_select","clone", "registered_parts", "spacer", "copy", "paste", "cut", "spacer", "wear", "save", "load", "partsize_info"} +pace.operations_bulk_poweruser = {"bulk_select", "clone", "registered_parts", "spacer", "copy", "paste", "cut", "spacer", "wear", "save", "load", "partsize_info"} if not file.Exists("pac3_config/pac_editor_partmenu_layouts.txt", "DATA") then pace.operations_order = pace.operations_default @@ -130,7 +130,7 @@ end local function DrawHaloHighlight(tbl) if (type(tbl) ~= "table") then return end if not pace.Active then - hook.Remove('PreDrawHalos', "BulkSelectHighlights") + pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights") end --Find out the color and apply the halo @@ -174,7 +174,7 @@ end local function ThinkBulkHighlight() if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then - hook.Remove('PreDrawHalos', "BulkSelectHighlights") + pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights") return end DrawHaloHighlight(last_bulk_select_tbl) @@ -474,9 +474,9 @@ end function pace.OnVariableChanged(obj, key, val, not_from_editor) local valType = type(val) - if valType == 'Vector' then + if valType == "Vector" then val = Vector(val) - elseif valType == 'Angle' then + elseif valType == "Angle" then val = Angle(val) end @@ -580,14 +580,14 @@ do -- menu local trap if not pace.Active or refresh_halo_hook then - hook.Remove('PreDrawHalos', "BulkSelectHighlights") + pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights") end //@note registered parts function pace.AddRegisteredPartsToMenu(menu, parent) local partsToShow = {} local clicked = false - hook.Add('Think', menu, function() + pac.AddHook("Think", menu, function() local ctrl = input.IsControlDown() if clicked and not ctrl then @@ -600,7 +600,7 @@ do -- menu menu:SetDeleteSelf(not ctrl) end) - hook.Add('CloseDermaMenus', menu, function() + pac.AddHook("CloseDermaMenus", menu, function() clicked = true if input.IsControlDown() then menu:SetVisible(true) @@ -609,7 +609,7 @@ do -- menu end) local function add_part(menu, part) - local newMenuEntry = menu:AddOption(L(part.FriendlyName or part.ClassName:Replace('_', ' ')), function() + local newMenuEntry = menu:AddOption(L(part.FriendlyName or part.ClassName:Replace("_", " ")), function() pace.RecordUndoHistory() pace.Call("CreatePart", part.ClassName, nil, nil, parent) pace.RecordUndoHistory() @@ -706,7 +706,7 @@ do -- menu add_part(sub, part) end - hook.Add('Think', sub, function() + pac.AddHook("Think", sub, function() local ctrl = input.IsControlDown() if clicked and not ctrl then @@ -719,7 +719,7 @@ do -- menu sub:SetDeleteSelf(not ctrl) end) - hook.Add('CloseDermaMenus', sub, function() + pac.AddHook("CloseDermaMenus", sub, function() if input.IsControlDown() and trap then trap = false sub:SetVisible(true) @@ -796,7 +796,7 @@ do -- menu end for class_name, part in pairs(partsToShow) do - local newMenuEntry = menu:AddOption(L((part.FriendlyName or part.ClassName):Replace('_', ' ')), function() + local newMenuEntry = menu:AddOption(L((part.FriendlyName or part.ClassName):Replace("_", " ")), function() pace.RecordUndoHistory() pace.Call("CreatePart", class_name, nil, nil, parent) pace.RecordUndoHistory() @@ -1062,7 +1062,7 @@ do -- menu local label = line:Add("DLabel") label:SetTextColor(label:GetSkin().Colours.Category.Line.Text) - label:SetText(L((part.FriendlyName or part.ClassName):Replace('_', ' '))) + label:SetText(L((part.FriendlyName or part.ClassName):Replace("_", " "))) label:SizeToContents() label:MoveRightOf(btn, 4) label:SetMouseInputEnabled(false) @@ -1411,17 +1411,17 @@ do -- menu RebuildBulkHighlight() if not silent then if selected_part_added then - surface.PlaySound('buttons/button1.wav') + surface.PlaySound("buttons/button1.wav") - else surface.PlaySound('buttons/button16.wav') end + else surface.PlaySound("buttons/button16.wav") end end if table.IsEmpty(pace.BulkSelectList) then --remove halo hook - hook.Remove('PreDrawHalos', "BulkSelectHighlights") + pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights") else --start halo hook - hook.Add('PreDrawHalos', "BulkSelectHighlights", function() + pac.AddHook("PreDrawHalos", "BulkSelectHighlights", function() local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() if mode == 0 then return elseif mode == 1 then ThinkBulkHighlight() @@ -2212,7 +2212,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) elseif option_name == "paste" and obj then menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste) elseif option_name == "cut" and obj then - menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png') + menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage("icon16/cut.png") elseif option_name == "paste_properties" and obj then menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace) elseif option_name == "clone" and obj then @@ -2247,7 +2247,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) end end) - psi_icon:SetImage('icon16/drive.png') + psi_icon:SetImage("icon16/drive.png") part_size_info:AddOption(L"from bulk select", function() local cumulative_bytes = 0 for _,v in pairs(pace.BulkSelectList) do @@ -2267,12 +2267,12 @@ function pace.addPartMenuComponent(menu, obj, option_name) end) elseif option_name == "bulk_apply_properties" then local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end) - bap_icon:SetImage('icon16/application_form.png') + bap_icon:SetImage("icon16/application_form.png") bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end) bulk_apply_properties:AddOption("Policy: lenient filtering", function() pace.BulkApplyProperties(obj, "lenient") end) elseif option_name == "bulk_select" then bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#pace.BulkSelectList..")", function() pace.DoBulkSelect(obj) end) - bs_icon:SetImage('icon16/table_multiple.png') + bs_icon:SetImage("icon16/table_multiple.png") bulk_menu.GetDeleteSelf = function() return false end local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() @@ -2319,16 +2319,16 @@ function pace.addPartMenuComponent(menu, obj, option_name) bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() pace.BulkCutPaste(obj) - end):SetImage('icon16/arrow_join.png') + end):SetImage("icon16/arrow_join.png") if not pace.ordered_operation_readystate then bulk_menu:AddOption(L"prepare Ordered Insert (please select parts in order beforehand)", function() pace.BulkCutPasteOrdered() - end):SetImage('icon16/text_list_numbers.png') + end):SetImage("icon16/text_list_numbers.png") else bulk_menu:AddOption(L"do Ordered Insert (select destinations in order)", function() pace.BulkCutPasteOrdered() - end):SetImage('icon16/arrow_switch.png') + end):SetImage("icon16/arrow_switch.png") end @@ -2341,20 +2341,20 @@ function pace.addPartMenuComponent(menu, obj, option_name) --bulk paste modes bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function() pace.BulkPasteFromBulkSelectToSinglePart(obj) - end):SetImage('icon16/arrow_join.png') + end):SetImage("icon16/arrow_join.png") bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function() if not pace.Clipboard then pace.Copy(obj) end pace.BulkPasteFromSingleClipboard() - end):SetImage('icon16/arrow_divide.png') + end):SetImage("icon16/arrow_divide.png") bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function() pace.BulkPasteFromBulkClipboard(obj) - end):SetImage('icon16/arrow_join.png') + end):SetImage("icon16/arrow_join.png") bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function() pace.BulkPasteFromBulkClipboardToBulkSelect() - end):SetImage('icon16/arrow_divide.png') + end):SetImage("icon16/arrow_divide.png") bulk_menu:AddSpacer() @@ -2381,14 +2381,14 @@ function pace.addPartMenuComponent(menu, obj, option_name) part.Arguments = str..i.."@@0@@0" end end) - end):SetImage('icon16/clock.png') + end):SetImage("icon16/clock.png") bulk_menu:AddOption(L"Pack into a new root group", function() root = pac.CreatePart("group") for i,v in ipairs(pace.BulkSelectList) do v:SetParent(root) end - end):SetImage('icon16/world.png') + end):SetImage("icon16/world.png") bulk_menu:AddSpacer() @@ -2398,21 +2398,21 @@ function pace.addPartMenuComponent(menu, obj, option_name) bulk_menu:AddOption(L"Clear Bulk List", function() pace.ClearBulkList() - end):SetImage('icon16/table_delete.png') + end):SetImage("icon16/table_delete.png") elseif option_name == "spacer" then menu:AddSpacer() elseif option_name == "registered_parts" then pace.AddRegisteredPartsToMenu(menu, not obj) elseif option_name == "hide_editor" then - menu:AddOption(L"hide editor / toggle focus", function() pace.Call("ToggleFocus") end):SetImage('icon16/zoom.png') + menu:AddOption(L"hide editor / toggle focus", function() pace.Call("ToggleFocus") end):SetImage("icon16/zoom.png") elseif option_name == "expand_all" and obj then menu:AddOption(L"expand all", function() - obj:CallRecursive('SetEditorExpand', true) - pace.RefreshTree(true) end):SetImage('icon16/arrow_down.png') + obj:CallRecursive("SetEditorExpand", true) + pace.RefreshTree(true) end):SetImage("icon16/arrow_down.png") elseif option_name == "collapse_all" and obj then menu:AddOption(L"collapse all", function() - obj:CallRecursive('SetEditorExpand', false) - pace.RefreshTree(true) end):SetImage('icon16/arrow_in.png') + obj:CallRecursive("SetEditorExpand", false) + pace.RefreshTree(true) end):SetImage("icon16/arrow_in.png") elseif option_name == "copy_uid" and obj then local menu2, pnl = menu:AddSubMenu(L"Copy part UniqueID", function() pace.CopyUID(obj) end) pnl:SetIcon(pace.MiscIcons.uniqueid) @@ -2422,11 +2422,11 @@ function pace.addPartMenuComponent(menu, obj, option_name) hoverfunc = "open", pac_part = pace.current_part, panel_exp_width = 900, panel_exp_height = 400 - }) end):SetImage('icon16/information.png') + }) end):SetImage("icon16/information.png") elseif option_name == "reorder_movables" and obj then if (obj.Position and obj.Angles and obj.PositionOffset) then local substitute, pnl = menu:AddSubMenu("Reorder / replace base movable") - pnl:SetImage('icon16/application_double.png') + pnl:SetImage("icon16/application_double.png") substitute:AddOption("Create a parent for position substitution", function() pace.SubstituteBaseMovable(obj, "create_parent") end) if obj.Parent then if obj.Parent.Position and obj.Parent.Angles then diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 45a2a61ba..981867692 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -58,7 +58,7 @@ function pace.SaveParts(name, prompt_name, override_part, overrideAsUsual) end end - data = hook.Run("pac_pace.SaveParts", data) or data + data = pac.CallHook("pace.SaveParts", data) or data if not override_part and #file.Find("pac3/sessions/*", "DATA") > 0 and not name:find("/") then pace.luadata.WriteFile("pac3/sessions/" .. name .. ".txt", data) @@ -146,7 +146,7 @@ end local latestprop local latest_uid if game.SinglePlayer() then - hook.Add("OnEntityCreated", "PAC_queue_proppacs", function( ent ) + pac.AddHook("OnEntityCreated", "queue_proppacs", function( ent ) if ( ent:GetClass() == "prop_physics" or ent:IsNPC()) and not ent:CreatedByMap() and LocalPlayer().pac_propload_queuedparts then if not table.IsEmpty(LocalPlayer().pac_propload_queuedparts) then ent:EmitSound( "buttons/button4.wav" ) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index f70df4cbb..b1c3b3928 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1661,7 +1661,7 @@ function pace.FillEditorSettings(pnl) local previous_inputs_str = "" pace.FlashNotification("Recording input... Release one key when you're done") - hook.Add("Tick", "pace_buttoncapture_countdown", function() + pac.AddHook("Tick", "pace_buttoncapture_countdown", function() pace.delayshortcuts = RealTime() + 5 local inputs_tbl = {} inputs_str = "" @@ -1692,7 +1692,7 @@ function pace.FillEditorSettings(pnl) --pace.PACActionShortcut[shortcutaction_choices:GetValue()][shortcutaction_index:GetValue()] = tbl pace.delayshortcuts = RealTime() + 5 pace.bindcapturelabel_text = "Recorded input:\n" .. previous_inputs_str - hook.Remove("Tick", "pace_buttoncapture_countdown") + pac.RemoveHook("Tick", "pace_buttoncapture_countdown") end end previous_inputs_str = inputs_str diff --git a/lua/pac3/editor/client/show_outfit_on_use.lua b/lua/pac3/editor/client/show_outfit_on_use.lua index 9346e52eb..56eee8f57 100644 --- a/lua/pac3/editor/client/show_outfit_on_use.lua +++ b/lua/pac3/editor/client/show_outfit_on_use.lua @@ -17,7 +17,7 @@ end local pac_IsPacOnUseOnly = pac.IsPacOnUseOnly -hook.Add("PlayerBindPress", "pac_onuse_only", function(ply, bind, isPressed) +pac.AddHook("PlayerBindPress", "onuse_only", function(ply, bind, isPressed) if bind ~= "use" and bind ~= "+use" then return end if bind ~= "+use" and isPressed then return end if not pac_IsPacOnUseOnly() then return end @@ -47,7 +47,7 @@ do weight = 600, }) - hook.Add("HUDPaint", "pac_onuse_only", function() + pac.AddHook("HUDPaint", "onuse_only", function() if not pac_IsPacOnUseOnly() then return end local ply = pac.LocalPlayer local eyes, aim = ply:EyePos(), ply:GetAimVector() @@ -72,7 +72,7 @@ do end function pace.OnUseOnlyUpdates(cvar, ...) - hook.Call('pace_OnUseOnlyUpdates', nil, ...) + pace.Call("OnUseOnlyUpdates", ...) end cvars.AddChangeCallback("pac_onuse_only", pace.OnUseOnlyUpdates, "PAC3") @@ -122,7 +122,7 @@ function pace.HandleOnUseReceivedData(data) -- behaviour of this (if one of entities on this hook becomes invalid) -- is undefined if DLib is not installed, but anyway - hook.Add('pace_OnUseOnlyUpdates', data.owner, function() + pac.AddHook('pace_OnUseOnlyUpdates', data.owner, function() if pac_IsPacOnUseOnly() then pac.ToggleIgnoreEntity(data.owner, data.owner.pac_onuse_only_check, 'pac_onuse_only') else diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index a81c91e7e..97f6fbbe8 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -354,7 +354,7 @@ do end end - pac.RemoveHook("Think", "pac_request_outfits") + pac.RemoveHook("Think", "request_outfits") pac.Message("Requesting outfits in 8 seconds...") timer.Simple(8, function() @@ -364,14 +364,14 @@ do end local function Initialize() - pac.RemoveHook("KeyRelease", "pac_request_outfits") + pac.RemoveHook("KeyRelease", "request_outfits") if not pac.LocalPlayer:IsValid() then return end if not pac.IsEnabled() then - pac.RemoveHook("Think", "pac_request_outfits") + pac.RemoveHook("Think", "request_outfits") pace.NeverLoaded = true return end @@ -379,7 +379,7 @@ do LoadUpDefault() end - hook.Add("pac_Enable", "pac_LoadUpDefault", function() + pac.AddHook("pac_Enable", "LoadUpDefault", function() if not pace.NeverLoaded then return end pace.NeverLoaded = nil LoadUpDefault() @@ -387,7 +387,7 @@ do local frames = 0 - pac.AddHook("Think", "pac_request_outfits", function() + pac.AddHook("Think", "request_outfits", function() if RealFrameTime() > 0.2 then -- lag? return end @@ -396,13 +396,13 @@ do if frames > 400 then if not xpcall(Initialize, ErrorNoHalt) then - pac.RemoveHook("Think", "pac_request_outfits") + pac.RemoveHook("Think", "request_outfits") pace.NeverLoaded = true end end end) - pac.AddHook("KeyRelease", "pac_request_outfits", function() + pac.AddHook("KeyRelease", "request_outfits", function() local me = pac.LocalPlayer if me:IsValid() and me:GetVelocity():Length() > 50 then diff --git a/lua/pac3/editor/server/util.lua b/lua/pac3/editor/server/util.lua index c59e607c2..d1a608070 100644 --- a/lua/pac3/editor/server/util.lua +++ b/lua/pac3/editor/server/util.lua @@ -15,7 +15,7 @@ end function pace.CallHook(str, ...) - return hook.Call("pac_" .. str, GAMEMODE, ...) + return hook.Call("pace_" .. str, GAMEMODE, ...) end diff --git a/lua/pac3/editor/server/wear.lua b/lua/pac3/editor/server/wear.lua index f4472fb69..a003a5ab4 100644 --- a/lua/pac3/editor/server/wear.lua +++ b/lua/pac3/editor/server/wear.lua @@ -269,17 +269,17 @@ function pace.SubmitPartNow(data, filter) if not players or istable(players) and not next(players) then return true end -- Alternative transmission system - local ret = hook.Run("pac_SendData", players, data) + local ret = pac.CallHook("SendData", players, data) if ret == nil then net.Start("pac_submit") local bytes, err = net_write_table(data) if not bytes then local errStr = tostring(err) - ErrorNoHalt("[PAC3] Outfit broadcast failed for " .. tostring(owner) .. ": " .. errStr .. '\n') + ErrorNoHalt("[PAC3] Outfit broadcast failed for " .. tostring(owner) .. ": " .. errStr .. "\n") if owner and owner:IsValid() then - owner:ChatPrint('[PAC3] ERROR: Could not broadcast your outfit: ' .. errStr) + owner:ChatPrint("[PAC3] ERROR: Could not broadcast your outfit: " .. errStr) end else net.Send(players) @@ -378,8 +378,8 @@ end util.AddNetworkString("pac_submit") -local pac_submit_spam = CreateConVar('pac_submit_spam', '1', {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'Prevent users from spamming pac_submit') -local pac_submit_limit = CreateConVar('pac_submit_limit', '30', {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'pac_submit spam limit') +local pac_submit_spam = CreateConVar("pac_submit_spam", "1", {FCVAR_NOTIFY, FCVAR_ARCHIVE}, "Prevent users from spamming pac_submit") +local pac_submit_limit = CreateConVar("pac_submit_limit", "30", {FCVAR_NOTIFY, FCVAR_ARCHIVE}, "pac_submit spam limit") pace.PCallNetReceive(net.Receive, "pac_submit", function(len, ply) if len < 64 then return end diff --git a/lua/pac3/extra/shared/init.lua b/lua/pac3/extra/shared/init.lua index 4728c341f..5381081a1 100644 --- a/lua/pac3/extra/shared/init.lua +++ b/lua/pac3/extra/shared/init.lua @@ -7,7 +7,7 @@ include("net_combat.lua") local cvar = CreateConVar("pac_restrictions", "0", FCVAR_REPLICATED) if CLIENT then - pac.AddHook("pac_EditorCalcView", "pac_restrictions", function() + pac.AddHook("pac_EditorCalcView", "restrictions", function() if cvar:GetInt() > 0 and not pac.LocalPlayer:IsAdmin() then local ent = pace.GetViewEntity() local dir = pace.ViewPos - ent:EyePos() From a0c34ede2e48df00aae9a8b4859edda2ce743984 Mon Sep 17 00:00:00 2001 From: techbot Date: Fri, 22 Dec 2023 00:49:44 +0100 Subject: [PATCH 139/300] remove local debug prints --- lua/pac3/core/client/util.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lua/pac3/core/client/util.lua b/lua/pac3/core/client/util.lua index a405e3ce7..b032a2df3 100644 --- a/lua/pac3/core/client/util.lua +++ b/lua/pac3/core/client/util.lua @@ -370,9 +370,7 @@ do -- hook helpers priority = nil end if pac.IsEnabled() then - print("adding", event_name, id) hook.Add(event_name, id, func, priority) - print("adding hooks", event_name .. tostring(id)) pac.added_hooks[event_name .. tostring(id)] = {event_name = event_name, id = id, func = func, priority = priority} end end @@ -384,9 +382,7 @@ do -- hook helpers if data then hook.Remove(data.event_name, data.id) - print("removing", data.event_name, data.id) pac.added_hooks[event_name .. id] = nil - print("removing hooks", event_name .. id) end end From f2b521f25071078def253178d0986b121732d664 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 25 Dec 2023 21:36:51 -0500 Subject: [PATCH 140/300] event copilot tweaks function to get the tutorial text remove unneeded checks change damage_zone_hit to number operator fix enum builder for some uid arguments auto remove quotes from uid for some events --- lua/pac3/core/client/parts/event.lua | 72 +++++++++++++--------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index d897b23bd..709a914aa 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -94,23 +94,25 @@ function PART:fix_event_operator() end end +function PART:GetEventTutorialText() + if PART.Events[self.Event] then + return PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory" + else + return "invalid event" + end +end + function PART:AttachEditorPopup(str) local info_string = str or "no information available" local verbosity = "" if self.Event ~= "" then - if PART.Events[self.Event] then - info_string = PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory" - else - info_string = "invalid event" - end + info_string = self:GetEventTutorialText() --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then - --end - - str = info_string or str end - self:SetupEditorPopup(info_string, true) + str = info_string or str + self:SetupEditorPopup(str, true) end function PART:SetEvent(event) @@ -118,29 +120,16 @@ function PART:SetEvent(event) (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) if not self.Events[event] then --invalid event? try a command event - local command_was_found_in_cmd_part = false - for i,v in ipairs(pac.GetLocalParts()) do - if v.ClassName == "command" then - if string.find(v.String, "pac_event " .. event) then - command_was_found_in_cmd_part = true - end - end - end - if self:GetPlayerOwner().pac_command_events or command_was_found_in_cmd_part then - self:GetPlayerOwner().pac_command_events = self:GetPlayerOwner().pac_command_events or {} - if self:GetPlayerOwner().pac_command_events[event] or command_was_found_in_cmd_part or GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then - timer.Simple(0.2, function() - --now we'll use event as a command name - self:SetEvent("command") - self.pace_properties["Event"]:SetValue("command") - - self:SetArguments(event .. "@@0") - self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") - pace.PopulateProperties(self) - - end) - return - end + if GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then + timer.Simple(0.2, function() + --now we'll use event as a command name + self:SetEvent("command") + self.pace_properties["Event"]:SetValue("command") + self:SetArguments(event .. "@@0") + self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") + pace.PopulateProperties(self) + end) + return end end @@ -2075,7 +2064,7 @@ PART.OldEvents = { }, damage_zone_hit = { - operator_type = "mixed", preferred_operator = "above", + operator_type = "number", preferred_operator = "above", arguments = {{time = "number"}, {damage = "number"}, {uid = "string"}}, userdata = {{default = 1}, {default = 0}, {enums = function(part) local output = {} @@ -2083,7 +2072,7 @@ PART.OldEvents = { for i, part in pairs(parts) do if part.ClassName == "damage_zone" then - output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID end end @@ -2091,6 +2080,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, time, damage, uid) uid = uid or "" + uid = string.gsub(uid, "\"", "") local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do @@ -2129,7 +2119,7 @@ PART.OldEvents = { for i, part in pairs(parts) do if part.ClassName == "damage_zone" then - output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID end end @@ -2137,6 +2127,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, time, uid) uid = uid or "" + uid = string.gsub(uid, "\"", "") local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do @@ -2182,7 +2173,7 @@ PART.OldEvents = { for i, part in pairs(parts) do if part.ClassName == "lock" then - output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID end end @@ -2190,6 +2181,7 @@ PART.OldEvents = { end}}, callback = function(self, ent, uid) uid = uid or "" + uid = string.gsub(uid, "\"", "") local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then for _,part in pairs(pac.GetLocalParts()) do @@ -2277,13 +2269,15 @@ PART.OldEvents = { for i, part in pairs(parts) do if part.ClassName == "health_modifier" then - output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID end end return output end}}, callback = function(self, ent, HpValue, part_uid) + part_uid = part_uid or "" + part_uid = string.gsub(part_uid, "\"", "") if ent.pac_healthbars and ent.pac_healthbars_uidtotals then if ent.pac_healthbars_uidtotals[part_uid] then return self:NumberOperator(ent.pac_healthbars_uidtotals[part_uid], HpValue) @@ -4149,10 +4143,12 @@ net.Receive("pac_update_healthbars", function() ent.pac_healthbars_uidtotals[uid] = value ent.pac_healthbars_layertotals[layer] = ent.pac_healthbars_layertotals[layer] + value ent.pac_healthbars_total = ent.pac_healthbars_total + value + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + if IsValid(part) and part.UpdateHPBars then part:UpdateHPBars() end end else ent.pac_healthbars_layertotals[layer] = nil end end -end) \ No newline at end of file +end) From 3644b1a56355b92a025c88b0a2cd830c53744338 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 25 Dec 2023 21:38:37 -0500 Subject: [PATCH 141/300] fix popups' info_string for events --- lua/pac3/core/client/base_part.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index a9de08e13..59b1225fa 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -1170,7 +1170,11 @@ function PART:SetupEditorPopup(str, force_open, tbl) } local default_state = str == nil or str == "" - local info_string = str or self.ClassName .. "\nno special information available" + local info_string + if self.ClassName == "event" and default_state then + info_string = self:GetEventTutorialText() + end + info_string = info_string or str or self.ClassName .. "\nno special information available" if default_state and pace then local partsize_tbl = pace.GetPartSizeInformation(self) From 9dfc7e7cbc82db132fce0452f8f7c0aeb70ba7e7 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 26 Dec 2023 00:55:59 -0500 Subject: [PATCH 142/300] Update health_modifier.lua when health modifier gets an update, SetInfo to indicate the hp left and write data for the hp equivalent as a number of intact healthbars --- lua/pac3/core/client/parts/health_modifier.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 24acc7e30..8ed4100ad 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -100,12 +100,14 @@ function PART:SetBarsAmount(val) self.BarsAmount = val if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("HealthBars") + self:UpdateHPBars() end function PART:SetBarsLayer(val) self.BarsLayer = val if pac.LocalPlayer ~= self:GetPlayerOwner() then return end self:SendModifier("HealthBars") + self:UpdateHPBars() end function PART:SetMaxHealth(val) @@ -198,8 +200,19 @@ function PART:OnHide() end function PART:Initialize() + self.healthbar_index = 0 if not GetConVar("pac_sv_health_modifier"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("health modifiers are disabled on this server!") end end +function PART:UpdateHPBars() + local ent = self:GetPlayerOwner() + if ent.pac_healthbars_uidtotals then + self.healthbar_index = math.ceil(ent.pac_healthbars_uidtotals[self.UniqueID] / self.BarsAmount) + if ent.pac_healthbars_uidtotals[self.UniqueID] then + self:SetInfo("Extra healthbars:\nHP is " .. ent.pac_healthbars_uidtotals[self.UniqueID] .. "/" .. self.HealthBars * self.BarsAmount .. "\n" .. self.healthbar_index .. " of " .. self.HealthBars .. " bars") + end + end +end + BUILDER:Register() From 7c560fba9a881a5e5971b2d2267f2d3310dc1111 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 26 Dec 2023 01:47:24 -0500 Subject: [PATCH 143/300] remaining healthbars proxy, aliases new function to read a health modifier's remaining number of bars function aliases named like the corresponding events for uniformity --- lua/pac3/core/client/parts/proxy.lua | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index ba77e4f69..aa604f5c3 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -1030,6 +1030,8 @@ PART.Inputs.pac_healthbars_total = function(self) return 0 end +PART.Inputs.healthmod_bar_total = PART.Inputs.pac_healthbars_total + PART.Inputs.pac_healthbars_layertotal = function(self, layer) local ent = self:GetPlayerOwner() if ent.pac_healthbars and ent.pac_healthbars_layertotals then @@ -1038,6 +1040,8 @@ PART.Inputs.pac_healthbars_layertotal = function(self, layer) return 0 end +PART.Inputs.healthmod_bar_layertotal = PART.Inputs.pac_healthbars_layertotal + PART.Inputs.pac_healthbar_uidvalue = function(self, uid) local ent = self:GetPlayerOwner() local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) @@ -1059,6 +1063,32 @@ PART.Inputs.pac_healthbar_uidvalue = function(self, uid) return 0 end +PART.Inputs.healthmod_bar_uidvalue = PART.Inputs.pac_healthbar_uidvalue + +PART.Inputs.pac_healthbar_remaining_bars = function(self, uid) + local ent = self:GetPlayerOwner() + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) or pac.FindPartByPartialUniqueID(pac.Hash(ent), uid) + if not part:IsValid() then part = pac.FindPartByName(pac.Hash(ent), uid, self) end + + if not IsValid(part) then + self.invalid_parts_in_expression[uid] = "invalid uid or name : " .. uid .. " in pac_healthbar_remaining_bars" + elseif part.ClassName ~= "health_modifier" then + self.invalid_parts_in_expression[uid] = "invalid class : " .. uid .. " in pac_healthbar_remaining_bars" + end + if ent.pac_healthbars and ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals[uid] then + + if part:IsValid() then + self.valid_parts_in_expression[part] = part + end + end + return part.healthbar_index or 0 + end + return 0 +end + +PART.Inputs.healthmod_bar_remaining_bars = PART.Inputs.pac_healthbar_remaining_bars + net.Receive("pac_proxy", function() local ply = net.ReadEntity() From 897d65f58737fca5c2587e2b4ff7f5fceac44db2 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 26 Dec 2023 17:18:41 -0500 Subject: [PATCH 144/300] Update health_modifier.lua quick fix --- lua/pac3/core/client/parts/health_modifier.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 8ed4100ad..0f26a8c77 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -206,7 +206,7 @@ end function PART:UpdateHPBars() local ent = self:GetPlayerOwner() - if ent.pac_healthbars_uidtotals then + if ent.pac_healthbars_uidtotals and ent.pac_healthbars_uidtotals[self.UniqueID] then self.healthbar_index = math.ceil(ent.pac_healthbars_uidtotals[self.UniqueID] / self.BarsAmount) if ent.pac_healthbars_uidtotals[self.UniqueID] then self:SetInfo("Extra healthbars:\nHP is " .. ent.pac_healthbars_uidtotals[self.UniqueID] .. "/" .. self.HealthBars * self.BarsAmount .. "\n" .. self.healthbar_index .. " of " .. self.HealthBars .. " bars") From 602bde4bb3484ce16dd31b3c25b4e57e7c0a4fd8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 26 Dec 2023 20:54:27 -0500 Subject: [PATCH 145/300] validity check --- lua/pac3/extra/shared/net_combat.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index aae9bb561..13691bcd2 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1242,11 +1242,10 @@ if SERVER then end elseif (ent:IsNPC() or string.find(ent:GetClass(), "npc") ~= nil) and tbl.NPC then if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then - if phys_ent:GetVelocity():Length() > 500 then + if IsValid(phys_ent) and phys_ent:GetVelocity():Length() > 500 then local vec = oldvel + addvel local clamp_vec = vec:GetNormalized()*500 ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer - else ent:SetVelocity((oldvel * final_damping) + addvel) end end elseif tbl.PointEntities then From 593fbdc2bf6a1906e0e6f13518123f3c2e8d300a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 27 Dec 2023 16:11:48 -0500 Subject: [PATCH 146/300] Update pac_settings_manager.lua include some of the main server cvars' general description and information new net message will send data to the client realm for the reworked settings.lua menu --- .../editor/server/pac_settings_manager.lua | 105 +++++++++++++++++- 1 file changed, 99 insertions(+), 6 deletions(-) diff --git a/lua/pac3/editor/server/pac_settings_manager.lua b/lua/pac3/editor/server/pac_settings_manager.lua index af640e228..294abc54c 100644 --- a/lua/pac3/editor/server/pac_settings_manager.lua +++ b/lua/pac3/editor/server/pac_settings_manager.lua @@ -1,13 +1,106 @@ - util.AddNetworkString("pac_send_sv_cvar") +util.AddNetworkString("pac_request_sv_cvars") +util.AddNetworkString("pac_send_cvars_to_client") + +--cvars used by settings.lua +local pac_server_cvars = { + {"pac_sv_prop_protection", "Enforce generic prop protection for player-owned props and physics entities based on client consents.", "", -1, 0, 200}, + {"pac_sv_combat_whitelisting", "Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.", "off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed", -1, 0, 200}, + {"pac_sv_block_combat_features_on_next_restart", "Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts", "You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers", -1, 0, 200}, + {"pac_sv_combat_enforce_netrate_monitor_serverside", "Enable serverside monitoring prints for allowance and rate limiters", "Enable serverside monitoring prints.\n0=let clients enforce their netrate allowance before sending messages\n1=the server will receive net messages and print the outcome.", -1, 0, 200}, + {"pac_sv_combat_enforce_netrate", "Rate limiter (milliseconds)", "The milliseconds delay between net messages.\nIf this is 0, the allowance won't matter, otherwise early net messages use up the player's allowance.\nThe allowance regenerates gradually when unused, and one unit gets spent if the message is earlier than the rate limiter's delay.", 0, 0, 1000}, + {"pac_sv_combat_enforce_netrate_buffersize", "Allowance, in number of messages", "Allowance:\nIf this is 0, only the time limiter will stop pac combat messages if they're too fast.\nOtherwise, players trying to use a pac combat message earlier will deduct 1 from the player's allowance, and only stop the messages if the allowance reaches 0.", 0, 0, 400}, + {"pac_sv_entity_limit_per_combat_operation", "Hard entity limit to cutoff damage zones and force parts", "If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.", 0, 0, 1000}, + {"pac_sv_entity_limit_per_player_per_combat_operation", "Entity limit per player to cutoff damage zones and force parts", "When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.", 0, 0, 500}, + {"pac_sv_player_limit_as_fraction_to_drop_damage_zone", "block damage zones targeting this fraction of players", "This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.", 2, 0, 1}, + {"pac_sv_combat_distance_enforced", "distance to block combat actions that are too far", "The distance is compared between the action's origin and the player's position.\n0 to ignore.", 0, 0, 64000}, + + {"pac_sv_lock", "Allow lock part", "", -1, 0, 200}, + {"pac_sv_lock_teleport", "Allow lock part teleportation", "", -1, 0, 200}, + {"pac_sv_lock_grab", "Allow lock part grabbing", "", -1, 0, 200}, + {"pac_sv_lock_allow_grab_ply", "Allow grabbing players", "", -1, 0, 200}, + {"pac_sv_lock_allow_grab_npc", "Allow grabbing NPCs", "", -1, 0, 200}, + {"pac_sv_lock_allow_grab_ent", "Allow grabbing other entities", "", -1, 0, 200}, + {"pac_sv_lock_max_grab_radius", "Max lock part grab range", "", 0, 0, 5000}, + + {"pac_sv_damage_zone", "Allow damage zone", "", -1, 0, 200}, + {"pac_sv_damage_zone_max_radius", "Max damage zone radius", "", 0, 0, 32767}, + {"pac_sv_damage_zone_max_length", "Max damage zone length", "", 0, 0, 32767}, + {"pac_sv_damage_zone_max_damage", "Max damage zone damage", "", 0, 0, 268435455}, + {"pac_sv_damage_zone_allow_dissolve", "Allow damage entity dissolvers", "", -1, 0, 200}, + + {"pac_sv_force", "Allow force part", "", -1, 0, 200}, + {"pac_sv_force_max_radius", "Max force radius", "", 0, 0, 32767}, + {"pac_sv_force_max_length", "Max force length", "", 0, 0, 32767}, + {"pac_sv_force_max_length", "Max force amount", "", 0, 0, 10000000}, + + {"pac_sv_hitscan", "allow serverside bullets", "", -1, 0, 200}, + {"pac_sv_hitscan_max_damage", "Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)", "", 0, 0, 268435455}, + {"pac_sv_hitscan_divide_max_damage_by_max_bullets", "force hitscans to distribute their total damage accross bullets. if off, every bullet does full damage; if on, adding more bullets doesn't do more damage", "", -1, 0, 200}, + {"pac_sv_hitscan_max_bullets", "Maximum number of bullets for hitscan multishots", "", 0, 0, 500}, + + {"pac_sv_projectiles", "allow serverside physical projectiles", "", -1, 0, 200}, + {"pac_sv_projectile_allow_custom_collision_mesh", "allow custom collision meshes for physical projectiles", "", -1, 0, 200}, + {"pac_sv_projectile_max_phys_radius", "Max projectile physical radius", "", 0, 0, 4095}, + {"pac_sv_projectile_max_damage_radius", "Max projectile damage radius", "", 0, 0, 4095}, + {"pac_sv_projectile_max_attract_radius", "Max projectile attract radius", "", 0, 0, 100000000}, + {"pac_sv_projectile_max_damage", "Max projectile damage", "", 0, 0, 100000000}, + {"pac_sv_projectile_max_speed", "Max projectile speed", "", 0, 0, 50000}, + {"pac_sv_projectile_max_mass", "Max projectile mass", "", 0, 0, 500000}, + + {"pac_sv_health_modifier", "Allow health modifier part", "", -1, 0, 200}, + {"pac_sv_health_modifier_allow_maxhp", "Allow changing max health and max armor", "", -1, 0, 200}, + {"pac_sv_health_modifier_min_damagescaling", "Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.", "", 2, -10, 1}, + {"pac_sv_health_modifier_extra_bars", "Allow extra healthbars", "What are those? It's like an armor layer that takes damage before it gets applied to the entity.", -1, 0, 200}, + + + {"pac_modifier_blood_color", "Blood", "", -1, 0, 200}, + {"pac_allow_mdl", "MDL", "", -1, 0, 200}, + {"pac_allow_mdl_entity", "Entity MDL", "", -1, 0, 200}, + {"pac_modifier_model", "Entity model", "", -1, 0, 200}, + {"pac_modifier_size", "Entity size", "", -1, 0, 200}, + + --the playermovement enabler policy cvar is a form, not a slider nor a bool + {"pac_player_movement_allow_mass", "Allow Modify Mass", "", -1, 0, 200}, + {"pac_player_movement_min_mass", "Mimnimum mass players can set for themselves", "", 0, 0, 1000000}, + {"pac_player_movement_max_mass", "Maximum mass players can set for themselves", "", 0, 0, 1000000}, + {"pac_player_movement_physics_damage_scaling", "Allow damage scaling of physics damage based on player's mass", "", -1, 0, 200}, + + {"pac_sv_draw_distance", "PAC server draw distance", "", 0, 0, 500000}, + {"pac_submit_spam", "Limit pac_submit to prevent spam", "", -1, 0, 200}, + {"pac_submit_limit", "limit of pac_submits", "", 0, 0, 100}, + {"pac_onuse_only_force", "Players need to +USE on others to reveal outfits", "", -1, 0, 200}, + + {"sv_pac_webcontent_allow_no_content_length", "Players need to +USE on others to reveal outfits", "", -1, 0, 200}, + {"pac_to_contraption_allow", "Allow PAC to contraption tool", "", -1, 0, 200}, + {"pac_max_contraption_entities", "Entity limit for PAC to contraption", "", 0, 0, 200}, + {"pac_restrictions", "restrict PAC editor camera movement", "", -1, 0, 200}, +} net.Receive("pac_send_sv_cvar", function(len,ply) - if ply == Entity(1) or ply:IsAdmin() then print("authenticated") else return end + if not (game.SinglePlayer() or ply:IsAdmin()) then ply:ChatPrint( "Only admins can change pac3 server settings!" ) return end local cmd = net.ReadString() local val = net.ReadString() - --if cmd == "" then + if not cmd then return end + + if GetConVar(cmd) then + GetConVar(cmd):SetString(val) + end + +end) - --end - GetConVar(cmd):SetString(val) - print("[PAC3]: Admin "..ply:GetName().." set "..cmd.." to "..val) +net.Receive("pac_request_sv_cvars", function (len, ply) + local cvars_tbl = {} + for _, tbl in ipairs(pac_server_cvars) do + local cmd = tbl[1] + if GetConVar(cmd) then + cvars_tbl[cmd] = GetConVar(cmd):GetString() + end + end + timer.Simple(0, function() + net.Start("pac_send_cvars_to_client") + net.WriteTable(cvars_tbl) + net.Send(ply) + end) + end) From e384c6a863b730c93d0ff21e83e98a9491816c6c Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 27 Dec 2023 16:34:21 -0500 Subject: [PATCH 147/300] reworked settings menu modular design to avoid code repetition, the basic information is kept in some category tables instead and run through a loop to populate the vgui elements networking with the pac settings manager instead of PANEL:SetConVar (removing the need to create cvar copies) to avoid issues with the client-server realm fracture menu gets a copy of each cvar's values from the server when opening, the changes are kept in memory, the menu has a prompt on close to ask whether to send the changes to the server added the reveal outfits on +use cvar --- lua/pac3/editor/client/settings.lua | 770 ++++++++-------------------- 1 file changed, 210 insertions(+), 560 deletions(-) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index b1c3b3928..3323bc483 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1,54 +1,93 @@ include("parts.lua") include("shortcuts.lua") -local pac_submit_spam = CreateConVar('pac_submit_spam', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'Prevent users from spamming pac_submit') -local pac_submit_limit = CreateConVar('pac_submit_limit', '30', CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'pac_submit spam limit') -local hitscan_allow = CreateConVar("pac_sv_hitscan", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside") -local hitscan_max_bullets = CreateConVar("pac_sv_hitscan_max_bullets", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum number of bullets") -local hitscan_max_damage = CreateConVar("pac_sv_hitscan_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum damage") -local hitscan_spreadout_dmg = CreateConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not force hitscans to divide their damage among the number of bullets fired") +--{"cvar", "description", "tooltip", decimals, min, max} if decimals is -1 it's a bool +local convar_params_combat_generic = { + + --general sv protection + {"pac_sv_prop_protection", "Enforce generic prop protection for player-owned props and physics entities based on client consents.", "", -1, 0, 200}, + {"pac_sv_combat_whitelisting", "Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.", "off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed", -1, 0, 200}, + {"pac_sv_block_combat_features_on_next_restart", "Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts", "You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers", -1, 0, 200}, + {"pac_sv_combat_enforce_netrate_monitor_serverside", "Enable serverside monitoring prints for allowance and rate limiters", "Enable serverside monitoring prints.\n0=let clients enforce their netrate allowance before sending messages\n1=the server will receive net messages and print the outcome.", -1, 0, 200}, + {"pac_sv_combat_enforce_netrate", "Rate limiter (milliseconds)", "The milliseconds delay between net messages.\nIf this is 0, the allowance won't matter, otherwise early net messages use up the player's allowance.\nThe allowance regenerates gradually when unused, and one unit gets spent if the message is earlier than the rate limiter's delay.", 0, 0, 1000}, + {"pac_sv_combat_enforce_netrate_buffersize", "Allowance, in number of messages", "Allowance:\nIf this is 0, only the time limiter will stop pac combat messages if they're too fast.\nOtherwise, players trying to use a pac combat message earlier will deduct 1 from the player's allowance, and only stop the messages if the allowance reaches 0.", 0, 0, 400}, + {"pac_sv_entity_limit_per_combat_operation", "Hard entity limit to cutoff damage zones and force parts", "If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.", 0, 0, 1000}, + {"pac_sv_entity_limit_per_player_per_combat_operation", "Entity limit per player to cutoff damage zones and force parts", "When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.", 0, 0, 500}, + {"pac_sv_player_limit_as_fraction_to_drop_damage_zone", "block damage zones targeting this fraction of players", "This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.", 2, 0, 1}, + {"pac_sv_combat_distance_enforced", "distance to block combat actions that are too far", "The distance is compared between the action's origin and the player's position.\n0 to ignore.", 0, 0, 64000}, -local damagezone_allow = CreateConVar("pac_sv_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside") -local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum damage") -local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length") -local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius") -local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone") - -local lock_allow = CreateConVar("pac_sv_lock", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") -local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") -local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside") -local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius") -local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing players with lock part") -local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part") -local lock_allow_grab_ent = CreateConVar("pac_sv_lock_allow_grab_ent", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing other entities with lock part") - -local force_allow = CreateConVar("pac_sv_force", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside") -local force_max_length = CreateConVar("pac_sv_force_max_length", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum length") -local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum radius") -local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force") - -local healthmod_allow = CreateConVar("pac_sv_health_modifier", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") -local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") -local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") -local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") - -local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") - -local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter") -local netrate_allowance = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 60, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "the budgeted allowance to limit how many pac combat net messages can be sent in bursts. 0 to disable, otherwise a number of net messages of allowance.") -local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_netrate_monitor_serverside", 0, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not to let clients enforce their net message rates.\nSet this to 1 to get serverside prints telling you whenever someone is going over their allowance, but it'll still take the network bandwidth.\nSet this to 0 to let clients enforce their net rate and save some bandwidth but the server won't know who's spamming net messages.") -local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected") -local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit") -local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered") -local enforce_distance = CreateConVar("pac_sv_combat_distance_enforced", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enforce a limit on how far a pac combat action can originate.\nIf set to a distance, it will prevent actions that are too far from the acting player.\n0 to disable.") - - -local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") -local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") - - ---include("pac3/editor/server/combat_bans.lua") +} +local convar_params_lock = { + {"pac_sv_lock", "Allow lock part", "", -1, 0, 200}, + {"pac_sv_lock_teleport", "Allow lock part teleportation", "", -1, 0, 200}, + {"pac_sv_lock_grab", "Allow lock part grabbing", "", -1, 0, 200}, + {"pac_sv_lock_allow_grab_ply", "Allow grabbing players", "", -1, 0, 200}, + {"pac_sv_lock_allow_grab_npc", "Allow grabbing NPCs", "", -1, 0, 200}, + {"pac_sv_lock_allow_grab_ent", "Allow grabbing other entities", "", -1, 0, 200}, + {"pac_sv_lock_max_grab_radius", "Max lock part grab range", "", 0, 0, 5000}, +} +local convar_params_damage_zone = { + {"pac_sv_damage_zone", "Allow damage zone", "", -1, 0, 200}, + {"pac_sv_damage_zone_max_radius", "Max damage zone radius", "", 0, 0, 32767}, + {"pac_sv_damage_zone_max_length", "Max damage zone length", "", 0, 0, 32767}, + {"pac_sv_damage_zone_max_damage", "Max damage zone damage", "", 0, 0, 268435455}, + {"pac_sv_damage_zone_allow_dissolve", "Allow damage entity dissolvers", "", -1, 0, 200}, +} +local convar_params_force = { + {"pac_sv_force", "Allow force part", "", -1, 0, 200}, + {"pac_sv_force_max_radius", "Max force radius", "", 0, 0, 32767}, + {"pac_sv_force_max_length", "Max force length", "", 0, 0, 32767}, + {"pac_sv_force_max_length", "Max force amount", "", 0, 0, 10000000}, +} +local convar_params_hitscan = { + {"pac_sv_hitscan", "allow serverside bullets", "", -1, 0, 200}, + {"pac_sv_hitscan_max_damage", "Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)", "", 0, 0, 268435455}, + {"pac_sv_hitscan_divide_max_damage_by_max_bullets", "force hitscans to distribute their total damage accross bullets. if off, every bullet does full damage; if on, adding more bullets doesn't do more damage", "", -1, 0, 200}, + {"pac_sv_hitscan_max_bullets", "Maximum number of bullets for hitscan multishots", "", 0, 0, 500}, +} +local convar_params_projectile = { + {"pac_sv_projectiles", "allow serverside physical projectiles", "", -1, 0, 200}, + {"pac_sv_projectile_allow_custom_collision_mesh", "allow custom collision meshes for physical projectiles", "", -1, 0, 200}, + {"pac_sv_projectile_max_phys_radius", "Max projectile physical radius", "", 0, 0, 4095}, + {"pac_sv_projectile_max_damage_radius", "Max projectile damage radius", "", 0, 0, 4095}, + {"pac_sv_projectile_max_attract_radius", "Max projectile attract radius", "", 0, 0, 100000000}, + {"pac_sv_projectile_max_damage", "Max projectile damage", "", 0, 0, 100000000}, + {"pac_sv_projectile_max_speed", "Max projectile speed", "", 0, 0, 50000}, + {"pac_sv_projectile_max_mass", "Max projectile mass", "", 0, 0, 500000}, +} +local convar_params_health_modifier = { + {"pac_sv_health_modifier", "Allow health modifier part", "", -1, 0, 200}, + {"pac_sv_health_modifier_allow_maxhp", "Allow changing max health and max armor", "", -1, 0, 200}, + {"pac_sv_health_modifier_min_damagescaling", "Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.", "", 2, -10, 1}, + {"pac_sv_health_modifier_extra_bars", "Allow extra healthbars", "What are those? It's like an armor layer that takes damage before it gets applied to the entity.", -1, 0, 200}, +} +local convar_params_modifiers = { + {"pac_modifier_blood_color", "Blood", "", -1, 0, 200}, + {"pac_allow_mdl", "MDL", "", -1, 0, 200}, + {"pac_allow_mdl_entity", "Entity MDL", "", -1, 0, 200}, + {"pac_modifier_model", "Entity model", "", -1, 0, 200}, + {"pac_modifier_size", "Entity size", "", -1, 0, 200}, +} +local convar_params_movement = { + --the playermovement enabler policy cvar is a form, not a slider nor a bool + {"pac_player_movement_allow_mass", "Allow Modify Mass", "", -1, 0, 200}, + {"pac_player_movement_min_mass", "Mimnimum mass players can set for themselves", "", 0, 0, 1000000}, + {"pac_player_movement_max_mass", "Maximum mass players can set for themselves", "", 0, 0, 1000000}, + {"pac_player_movement_physics_damage_scaling", "Allow damage scaling of physics damage based on player's mass", "", -1, 0, 200}, +} +local convar_params_wearing_drawing = { + {"pac_sv_draw_distance", "PAC server draw distance", "", 0, 0, 500000}, + {"pac_submit_spam", "Limit pac_submit to prevent spam", "", -1, 0, 200}, + {"pac_submit_limit", "limit of pac_submits", "", 0, 0, 100}, + {"pac_onuse_only_force", "Players need to +USE on others to reveal outfits", "", -1, 0, 200}, +} +local convar_params_misc = { + {"sv_pac_webcontent_allow_no_content_length", "Players need to +USE on others to reveal outfits", "", -1, 0, 200}, + {"pac_to_contraption_allow", "Allow PAC to contraption tool", "", -1, 0, 200}, + {"pac_max_contraption_entities", "Entity limit for PAC to contraption", "", 0, 0, 200}, + {"pac_restrictions", "restrict PAC editor camera movement", "", -1, 0, 200}, +} pace = pace @@ -623,7 +662,7 @@ function PANEL:Init() local combat_ban_settings = pace.FillCombatBanPanel(master_pnl) master_pnl:AddSheet("Combat Bans (SV)", combat_ban_settings) - + net.Start("pac_request_sv_cvars") net.SendToServer() end @@ -646,7 +685,34 @@ function pace.OpenSettings() pnl:MakePopup() pnl:Center() pnl:SetSizable(true) - + pnl.OnClose = function() + if LocalPlayer():IsAdmin() and pace.cvar_changes then + local changes_str = "" + for cmd, val in pairs(pace.cvar_changes) do + if isbool(val) then + changes_str = changes_str .. cmd .. " set to " .. (val and "1" or "0") .. "\n" + else + changes_str = changes_str .. cmd .. " set to " .. val .. "\n" + end + end + Derma_Query("Send changes to the server?\n"..changes_str,table.Count(pace.cvar_changes) .. " server convars changed", + "Send changes to server", function() + for cmd, val in pairs(pace.cvar_changes) do + net.Start("pac_send_sv_cvar") + net.WriteString(cmd) + if isbool(val) then + net.WriteString(val and "1" or "0") + else + net.WriteString(val) + end + net.SendToServer() + end + pace.cvar_changes = nil + end, + "Cancel", function() pace.cvar_changes = nil end) + end + end + timer.Simple(0.5, function() pace.cvar_changes = nil end) local pnl = vgui.Create("pace_settings", pnl) pnl:Dock(FILL) end @@ -811,349 +877,76 @@ function pace.FillCombatBanPanel(pnl) return BAN end +local cvar_panels = {} + + +local function PopulateCategory(str, pnl, cvars_tbl) + --create a collapsible header category + local list = pnl:Add(str) + list.Header:SetSize(40,40) + list.Header:SetFont("DermaLarge") + local list_list = vgui.Create("DListLayout") + list_list:DockPadding(20,0,20,20) + list:SetContents(list_list) + + --insert the cvars for the category + for i, tbl in ipairs(cvars_tbl) do + local cvar_pnl + if tbl[4] == -1 then + cvar_pnl = vgui.Create("DCheckBoxLabel", list_list) + cvar_pnl.OnChange = function(self, val) + pace.cvar_changes = pace.cvar_changes or {} + pace.cvar_changes[tbl[1]] = val + end + else + cvar_pnl = vgui.Create("DNumSlider", list_list) + cvar_pnl:SetDecimals(tbl[4]) + cvar_pnl:SetMin(tbl[5]) + cvar_pnl:SetMax(tbl[6]) + cvar_pnl.OnValueChanged = function(self, val) + pace.cvar_changes = pace.cvar_changes or {} + pace.cvar_changes[tbl[1]] = val + end + end + cvar_panels[tbl[1]] = cvar_pnl + cvar_pnl:SetText(tbl[2]) + if tbl[3] ~= "" then cvar_pnl:SetTooltip(tbl[3]) end + cvar_pnl:SetSize(400,30) + + end + return list_list +end + +net.Receive("pac_send_cvars_to_client", function() + local cvars_tbl = net.ReadTable() + for cmd, val in pairs(cvars_tbl) do + if cvar_panels[cmd] then + --print("cvar exists " .. cmd .. " = " .. val) + cvar_panels[cmd]:SetValue(val) + else + --print("wrong cvar? " .. cmd) + end + end + pace.cvar_changes = nil +end) function pace.FillCombatSettings(pnl) local pnl = pnl local master_list = vgui.Create("DCategoryList", pnl) master_list:Dock(FILL) - --general - do - local general_list = master_list:Add("General (Global policy and Network protection)") - general_list.Header:SetSize(40,40) - general_list.Header:SetFont("DermaLarge") - local general_list_list = vgui.Create("DListLayout") - general_list_list:DockPadding(20,0,20,20) - general_list:SetContents(general_list_list) - - local sv_prop_protection_props_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_prop_protection_props_box:SetText("Enforce generic prop protection for player-owned props and physics entities.\nRelated to client consents, but the policies for each part are not uniform.") - sv_prop_protection_props_box:SetSize(400,30) - sv_prop_protection_props_box:SetConVar("pac_sv_prop_protection") - - - local sv_combat_whitelisting_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_combat_whitelisting_box:SetText("Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.") - sv_combat_whitelisting_box:SetSize(400,30) - sv_combat_whitelisting_box:SetConVar("pac_sv_combat_whitelisting") - sv_combat_whitelisting_box:SetTooltip("off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed") - - local sv_master_break_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_master_break_box:SetText("Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts") - sv_master_break_box:SetSize(400,30) - sv_master_break_box:SetConVar("pac_sv_block_combat_features_on_next_restart") - sv_master_break_box:SetTooltip("You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers") - - local sv_netrate_monitoring_box = vgui.Create("DCheckBoxLabel", general_list_list) - sv_netrate_monitoring_box:SetText("Enable serverside monitoring prints for allowance and rate limiters") - sv_netrate_monitoring_box:SetSize(400,30) - sv_netrate_monitoring_box:SetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") - sv_netrate_monitoring_box:SetTooltip("Enable serverside monitoring prints.\n0=let clients enforce their netrate allowance before sending messages\n1=the server will receive net messages and print the outcome.") - - local sv_netrate_time_numbox = vgui.Create("DNumSlider", general_list_list) - sv_netrate_time_numbox:SetText("Rate limiter (milliseconds)") - sv_netrate_time_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate"):GetInt()) - sv_netrate_time_numbox:SetMin(0) sv_netrate_time_numbox:SetDecimals(0) sv_netrate_time_numbox:SetMax(1000) - sv_netrate_time_numbox:SetSize(400,30) - sv_netrate_time_numbox:SetConVar("pac_sv_combat_enforce_netrate") - sv_netrate_time_numbox:SetTooltip("The milliseconds delay between net messages.\nIf this is 0, the allowance won't matter, otherwise early net messages use up the player's allowance.\nThe allowance regenerates gradually when unused, and one unit gets spent if the message is earlier than the rate limiter's delay.") - - local sv_netrate_buffer_numbox = vgui.Create("DNumSlider", general_list_list) - sv_netrate_buffer_numbox:SetText("Allowance, in number of messages") - sv_netrate_buffer_numbox:SetValue(GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt()) - sv_netrate_buffer_numbox:SetMin(0) sv_netrate_buffer_numbox:SetDecimals(0) sv_netrate_buffer_numbox:SetMax(400) - sv_netrate_buffer_numbox:SetSize(400,30) - sv_netrate_buffer_numbox:SetConVar("pac_sv_combat_enforce_netrate_buffersize") - sv_netrate_buffer_numbox:SetTooltip("Allowance:\nIf this is 0, only the time limiter will stop pac combat messages if they're too fast.\nOtherwise, players trying to use a pac combat message earlier will deduct 1 from the player's allowance, and only stop the messages if the allowance reaches 0.") - - local sv_hard_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) - sv_hard_ent_limit_numbox:SetText("Hard entity limit to cutoff damage zones and force parts") - sv_hard_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_combat_operation"):GetInt()) - sv_hard_ent_limit_numbox:SetMin(0) sv_hard_ent_limit_numbox:SetDecimals(0) sv_hard_ent_limit_numbox:SetMax(1000) - sv_hard_ent_limit_numbox:SetSize(400,30) - sv_hard_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_combat_operation") - sv_hard_ent_limit_numbox:SetTooltip("If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") - - local sv_per_player_ent_limit_numbox = vgui.Create("DNumSlider", general_list_list) - sv_per_player_ent_limit_numbox:SetText("Entity limit per player to cutoff damage zones and force parts") - sv_per_player_ent_limit_numbox:SetValue(GetConVar("pac_sv_entity_limit_per_player_per_combat_operation"):GetInt()) - sv_per_player_ent_limit_numbox:SetMin(0) sv_per_player_ent_limit_numbox:SetDecimals(0) sv_per_player_ent_limit_numbox:SetMax(500) - sv_per_player_ent_limit_numbox:SetSize(400,30) - sv_per_player_ent_limit_numbox:SetConVar("pac_sv_entity_limit_per_player_per_combat_operation") - sv_per_player_ent_limit_numbox:SetTooltip("When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.") - - local sv_player_fraction_slider = vgui.Create("DNumSlider", general_list_list) - sv_player_fraction_slider:SetText("block damage zones targeting this fraction of players") - sv_player_fraction_slider:SetValue(GetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone"):GetFloat()) - sv_player_fraction_slider:SetMin(0) sv_player_fraction_slider:SetDecimals(2) sv_player_fraction_slider:SetMax(1) - sv_player_fraction_slider:SetSize(400,30) - sv_player_fraction_slider:SetConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone") - sv_player_fraction_slider:SetTooltip("This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.") - - local sv_distance_slider = vgui.Create("DNumSlider", general_list_list) - sv_distance_slider:SetText("distance to block combat actions that are too far") - sv_distance_slider:SetValue(GetConVar("pac_sv_combat_distance_enforced"):GetFloat()) - sv_distance_slider:SetMin(0) sv_distance_slider:SetDecimals(0) sv_distance_slider:SetMax(64000) - sv_distance_slider:SetSize(400,30) - sv_distance_slider:SetConVar("pac_sv_combat_distance_enforced") - sv_distance_slider:SetTooltip("The distance is compared between the action's origin and the player's position.\n0 to ignore.") - - end - - do --hitscan - --[[ - pac_sv_hitscan - pac_sv_hitscan_max_bullets - pac_sv_hitscan_max_damage - pac_sv_hitscan_divide_max_damage_by_max_bullets - ]] - - local hitscans_list = master_list:Add("Hitscans") - hitscans_list.Header:SetSize(40,40) - hitscans_list.Header:SetFont("DermaLarge") - local hitscans_list_list = vgui.Create("DListLayout") - hitscans_list_list:DockPadding(20,0,20,20) - hitscans_list:SetContents(hitscans_list_list) - - local sv_hitscans_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) - sv_hitscans_box:SetText("allow serverside bullets") - sv_hitscans_box:SetSize(400,30) - sv_hitscans_box:SetConVar("pac_sv_hitscan") - - local hitscans_max_dmg_numbox = vgui.Create("DNumSlider", hitscans_list_list) - hitscans_max_dmg_numbox:SetText("Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)") - hitscans_max_dmg_numbox:SetValue(GetConVar("pac_sv_hitscan_max_damage"):GetInt()) - hitscans_max_dmg_numbox:SetMin(0) hitscans_max_dmg_numbox:SetDecimals(0) hitscans_max_dmg_numbox:SetMax(268435455) - hitscans_max_dmg_numbox:SetSize(400,30) - hitscans_max_dmg_numbox:SetConVar("pac_sv_hitscan_max_damage") - - local sv_hitscans_distribute_box = vgui.Create("DCheckBoxLabel", hitscans_list_list) - sv_hitscans_distribute_box:SetText("force hitscans to distribute their total damage accross bullets. if off, every bullet does full damage; if on, adding more bullets doesn't do more damage") - sv_hitscans_distribute_box:SetSize(400,30) - sv_hitscans_distribute_box:SetConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets") - - local hitscans_max_numbullets_numbox = vgui.Create("DNumSlider", hitscans_list_list) - hitscans_max_numbullets_numbox:SetText("Maximum number of bullets for hitscan multishots") - hitscans_max_numbullets_numbox:SetValue(GetConVar("pac_sv_hitscan_max_bullets"):GetInt()) - hitscans_max_numbullets_numbox:SetMin(1) hitscans_max_numbullets_numbox:SetDecimals(0) hitscans_max_numbullets_numbox:SetMax(500) - hitscans_max_numbullets_numbox:SetSize(400,30) - hitscans_max_numbullets_numbox:SetConVar("pac_sv_hitscan_max_bullets") - end - - do --projectiles - local projectiles_list = master_list:Add("Projectiles") - projectiles_list.Header:SetSize(40,40) - projectiles_list.Header:SetFont("DermaLarge") - local projectiles_list_list = vgui.Create("DListLayout") - projectiles_list_list:DockPadding(20,0,20,20) - projectiles_list:SetContents(projectiles_list_list) - - local sv_projectiles_box = vgui.Create("DCheckBoxLabel", projectiles_list_list) - sv_projectiles_box:SetText("allow serverside physical projectiles") - sv_projectiles_box:SetSize(400,30) - sv_projectiles_box:SetConVar("pac_sv_projectiles") - - local sv_projectiles_mesh_box = vgui.Create("DCheckBoxLabel", projectiles_list_list) - sv_projectiles_mesh_box:SetText("allow custom collision meshes for physical projectiles") - sv_projectiles_mesh_box:SetSize(400,30) - sv_projectiles_mesh_box:SetConVar("pac_sv_projectile_allow_custom_collision_mesh") - - local projectile_max_phys_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) - projectile_max_phys_radius_numbox:SetText("Max projectile physical radius") - projectile_max_phys_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_phys_radius"):GetInt()) - projectile_max_phys_radius_numbox:SetMin(0) projectile_max_phys_radius_numbox:SetDecimals(0) projectile_max_phys_radius_numbox:SetMax(4095) - projectile_max_phys_radius_numbox:SetSize(400,30) - projectile_max_phys_radius_numbox:SetConVar("pac_sv_projectile_max_phys_radius") - - local projectile_max_dmg_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) - projectile_max_dmg_radius_numbox:SetText("Max projectile damage radius") - projectile_max_dmg_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage_radius"):GetInt()) - projectile_max_dmg_radius_numbox:SetMin(0) projectile_max_dmg_radius_numbox:SetDecimals(0) projectile_max_dmg_radius_numbox:SetMax(4095) - projectile_max_dmg_radius_numbox:SetSize(400,30) - projectile_max_dmg_radius_numbox:SetConVar("pac_sv_projectile_max_damage_radius") - - local projectile_max_attract_radius_numbox = vgui.Create("DNumSlider", projectiles_list_list) - projectile_max_attract_radius_numbox:SetText("Max projectile attract radius") - projectile_max_attract_radius_numbox:SetValue(GetConVar("pac_sv_projectile_max_attract_radius"):GetInt()) - projectile_max_attract_radius_numbox:SetMin(0) projectile_max_attract_radius_numbox:SetDecimals(0) projectile_max_attract_radius_numbox:SetMax(100000000) - projectile_max_attract_radius_numbox:SetSize(400,30) - projectile_max_attract_radius_numbox:SetConVar("pac_sv_projectile_max_attract_radius") - - local projectile_max_dmg_numbox = vgui.Create("DNumSlider", projectiles_list_list) - projectile_max_dmg_numbox:SetText("Max projectile damage") - projectile_max_dmg_numbox:SetValue(GetConVar("pac_sv_projectile_max_damage"):GetInt()) - projectile_max_dmg_numbox:SetMin(0) projectile_max_dmg_numbox:SetDecimals(0) projectile_max_dmg_numbox:SetMax(100000000) - projectile_max_dmg_numbox:SetSize(400,30) - projectile_max_dmg_numbox:SetConVar("pac_sv_projectile_max_damage") - - local projectile_max_speed_numbox = vgui.Create("DNumSlider", projectiles_list_list) - projectile_max_speed_numbox:SetText("Max projectile speed") - projectile_max_speed_numbox:SetValue(GetConVar("pac_sv_projectile_max_speed"):GetInt()) - projectile_max_speed_numbox:SetMin(0) projectile_max_speed_numbox:SetDecimals(0) projectile_max_speed_numbox:SetMax(50000) - projectile_max_speed_numbox:SetSize(400,30) - projectile_max_speed_numbox:SetConVar("pac_sv_projectile_max_speed") - - local projectile_max_mass_numbox = vgui.Create("DNumSlider", projectiles_list_list) - projectile_max_mass_numbox:SetText("Max projectile mass") - projectile_max_mass_numbox:SetValue(GetConVar("pac_sv_projectile_max_mass"):GetInt()) - projectile_max_mass_numbox:SetMin(0) projectile_max_mass_numbox:SetDecimals(0) projectile_max_mass_numbox:SetMax(500000) - projectile_max_mass_numbox:SetSize(400,30) - projectile_max_mass_numbox:SetConVar("pac_sv_projectile_max_mass") - end - - do --damage zone - local damagezone_list = master_list:Add("Damage Zone") - damagezone_list.Header:SetSize(40,40) - damagezone_list.Header:SetFont("DermaLarge") - local damagezone_list_list = vgui.Create("DListLayout") - damagezone_list_list:DockPadding(20,0,20,20) - damagezone_list:SetContents(damagezone_list_list) - - local sv_dmgzone_box = vgui.Create("DCheckBoxLabel", damagezone_list_list) - sv_dmgzone_box:SetText("Allow damage zone") - sv_dmgzone_box:SetSize(400,30) - sv_dmgzone_box:SetConVar("pac_sv_damage_zone") - - local max_dmgzone_radius_numbox = vgui.Create("DNumSlider", damagezone_list_list) - max_dmgzone_radius_numbox:SetText("Max damage zone radius") - max_dmgzone_radius_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_radius"):GetInt()) - max_dmgzone_radius_numbox:SetMin(0) max_dmgzone_radius_numbox:SetDecimals(0) max_dmgzone_radius_numbox:SetMax(32767) - max_dmgzone_radius_numbox:SetSize(400,30) - max_dmgzone_radius_numbox:SetConVar("pac_sv_damage_zone_max_radius") - - local max_dmgzone_length_numbox = vgui.Create("DNumSlider", damagezone_list_list) - max_dmgzone_length_numbox:SetText("Max damage zone length") - max_dmgzone_length_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_length"):GetInt()) - max_dmgzone_length_numbox:SetMin(0) max_dmgzone_length_numbox:SetDecimals(0) max_dmgzone_length_numbox:SetMax(32767) - max_dmgzone_length_numbox:SetSize(400,30) - max_dmgzone_length_numbox:SetConVar("pac_sv_damage_zone_max_length") - - local max_dmgzone_damage_numbox = vgui.Create("DNumSlider", damagezone_list_list) - max_dmgzone_damage_numbox:SetText("Max damage zone damage") - max_dmgzone_damage_numbox:SetValue(GetConVar("pac_sv_damage_zone_max_damage"):GetInt()) - max_dmgzone_damage_numbox:SetMin(0) max_dmgzone_damage_numbox:SetDecimals(0) max_dmgzone_damage_numbox:SetMax(268435455) - max_dmgzone_damage_numbox:SetSize(400,30) - max_dmgzone_damage_numbox:SetConVar("pac_sv_damage_zone_max_damage") - - local sv_dmgzone_allow_dissolve_box = vgui.Create("DCheckBoxLabel", damagezone_list_list) - sv_dmgzone_allow_dissolve_box:SetText("Allow damage entity dissolvers") - sv_dmgzone_allow_dissolve_box:SetSize(400,30) - sv_dmgzone_allow_dissolve_box:SetConVar("pac_sv_damage_zone_allow_dissolve") - - end - do --lock part - local lock_list = master_list:Add("Lock part") - lock_list.Header:SetSize(40,40) - lock_list.Header:SetFont("DermaLarge") - local lock_list_list = vgui.Create("DListLayout") - lock_list_list:DockPadding(20,0,20,20) - lock_list:SetContents(lock_list_list) - - local sv_lock_allow_box = vgui.Create("DCheckBoxLabel", lock_list_list) - sv_lock_allow_box:SetText("Allow lock part") - sv_lock_allow_box:SetSize(400,30) - sv_lock_allow_box:SetConVar("pac_sv_lock") - - local sv_lock_grab_box = vgui.Create("DCheckBoxLabel", lock_list_list) - sv_lock_grab_box:SetText("Allow lock part grabbing") - sv_lock_grab_box:SetSize(400,30) - sv_lock_grab_box:SetConVar("pac_sv_lock_grab") - - local sv_lock_grab_ply_box = vgui.Create("DCheckBoxLabel", lock_list_list) - sv_lock_grab_ply_box:SetText("Allow grabbing players") - sv_lock_grab_ply_box:SetSize(400,30) - sv_lock_grab_ply_box:SetConVar("pac_sv_lock_allow_grab_ply") - - local sv_lock_grab_npc_box = vgui.Create("DCheckBoxLabel", lock_list_list) - sv_lock_grab_npc_box:SetText("Allow grabbing NPCs") - sv_lock_grab_npc_box:SetSize(400,30) - sv_lock_grab_npc_box:SetConVar("pac_sv_lock_allow_grab_npc") - - local sv_lock_grab_ents_box = vgui.Create("DCheckBoxLabel", lock_list_list) - sv_lock_grab_ents_box:SetText("Allow grabbing other entities") - sv_lock_grab_ents_box:SetSize(400,30) - sv_lock_grab_ents_box:SetConVar("pac_sv_lock_allow_grab_ent") - - local sv_lock_teleport_box = vgui.Create("DCheckBoxLabel", lock_list_list) - sv_lock_teleport_box:SetText("Allow lock part teleportation") - sv_lock_teleport_box:SetSize(400,30) - sv_lock_teleport_box:SetConVar("pac_sv_lock_teleport") - - local max_lock_radius_numbox = vgui.Create("DNumSlider", lock_list_list) - max_lock_radius_numbox:SetText("Max lock part grab range") - max_lock_radius_numbox:SetValue(GetConVar("pac_sv_lock_max_grab_radius"):GetInt()) - max_lock_radius_numbox:SetMin(0) max_lock_radius_numbox:SetDecimals(0) max_lock_radius_numbox:SetMax(5000) - max_lock_radius_numbox:SetSize(400,30) - max_lock_radius_numbox:SetConVar("pac_sv_lock_max_grab_radius") - end - - do --force - local force_list = master_list:Add("Force part") - force_list.Header:SetSize(40,40) - force_list.Header:SetFont("DermaLarge") - local force_list_list = vgui.Create("DListLayout") - force_list_list:DockPadding(20,0,20,20) - force_list:SetContents(force_list_list) - - local sv_force_box = vgui.Create("DCheckBoxLabel", force_list_list) - sv_force_box:SetText("Allow force part") - sv_force_box:SetSize(400,30) - sv_force_box:SetConVar("pac_sv_force") - - local max_force_radius_numbox = vgui.Create("DNumSlider", force_list_list) - max_force_radius_numbox:SetText("Max force part radius") - max_force_radius_numbox:SetValue(GetConVar("pac_sv_force_max_radius"):GetInt()) - max_force_radius_numbox:SetMin(0) max_force_radius_numbox:SetDecimals(0) max_force_radius_numbox:SetMax(32767) - max_force_radius_numbox:SetSize(400,30) - max_force_radius_numbox:SetConVar("pac_sv_force_max_radius") - - local max_force_length_numbox = vgui.Create("DNumSlider", force_list_list) - max_force_length_numbox:SetText("Max force part length") - max_force_length_numbox:SetValue(GetConVar("pac_sv_force_max_length"):GetInt()) - max_force_length_numbox:SetMin(0) max_force_length_numbox:SetDecimals(0) max_force_length_numbox:SetMax(32767) - max_force_length_numbox:SetSize(400,30) - max_force_length_numbox:SetConVar("pac_sv_force_max_length") - - local max_force_amount_numbox = vgui.Create("DNumSlider", force_list_list) - max_force_amount_numbox:SetText("Max force part amount") - max_force_amount_numbox:SetValue(GetConVar("pac_sv_force_max_amount"):GetInt()) - max_force_amount_numbox:SetMin(0) max_force_amount_numbox:SetDecimals(0) max_force_amount_numbox:SetMax(10000000) - max_force_amount_numbox:SetSize(400,30) - max_force_amount_numbox:SetConVar("pac_sv_force_max_amount") - end + --general + PopulateCategory("General (Global policy and Network protections)", master_list, convar_params_combat_generic) + + --combat parts + PopulateCategory("Force part", master_list, convar_params_force) + PopulateCategory("Damage Zone", master_list, convar_params_damage_zone) + PopulateCategory("Lock part", master_list, convar_params_lock) + PopulateCategory("Hitscan part", master_list, convar_params_hitscan) + PopulateCategory("Projectiles", master_list, convar_params_projectile) + PopulateCategory("Health modifier part", master_list, convar_params_health_modifier) - do --health_modifier - local healthmod_list = master_list:Add("Health modifier part") - healthmod_list.Header:SetSize(40,40) - healthmod_list.Header:SetFont("DermaLarge") - local healthmod_list_list = vgui.Create("DListLayout") - healthmod_list_list:DockPadding(20,0,20,20) - healthmod_list:SetContents(healthmod_list_list) - - local sv_healthmod_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) - sv_healthmod_box:SetText("Allow health modifier part") - sv_healthmod_box:SetSize(400,30) - sv_healthmod_box:SetConVar("pac_sv_health_modifier") - - local healthmod_extrabars_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) - healthmod_extrabars_box:SetText("Allow changing max health and max armor") - healthmod_extrabars_box:SetSize(400,30) - healthmod_extrabars_box:SetConVar("pac_sv_health_modifier_allow_maxhp") - - local min_healthmod_dmgmult_box = vgui.Create("DNumSlider", healthmod_list_list) - min_healthmod_dmgmult_box:SetText("Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.") - min_healthmod_dmgmult_box:SetValue(GetConVar("pac_sv_health_modifier_min_damagescaling"):GetInt()) - min_healthmod_dmgmult_box:SetMin(-10) min_healthmod_dmgmult_box:SetDecimals(2) min_healthmod_dmgmult_box:SetMax(1) - min_healthmod_dmgmult_box:SetSize(400,30) - min_healthmod_dmgmult_box:SetConVar("pac_sv_health_modifier_min_damagescaling") - - local healthmod_extrabars_box = vgui.Create("DCheckBoxLabel", healthmod_list_list) - healthmod_extrabars_box:SetText("Allow extra healthbars") - healthmod_extrabars_box:SetSize(400,30) - healthmod_extrabars_box:SetConVar("pac_sv_health_modifier_extra_bars") - healthmod_extrabars_box:SetToolTip("What are those? It's like an armor layer that takes damage before it gets applied to the entity.") - end return master_list end @@ -1163,187 +956,44 @@ function pace.FillServerSettings(pnl) local master_list = vgui.Create("DCategoryList", pnl) master_list:Dock(FILL) - --models/entity - --[[ - pac_allow_blood_color - pac_allow_mdl - pac_allow_mdl_entity - pac_modifier_model - pac_modifier_size - ]] - - local model_category = master_list:Add("Allowed Playermodel Mutations") - model_category.Header:SetSize(40,40) - model_category.Header:SetFont("DermaLarge") - local model_category_list = vgui.Create("DListLayout") - model_category_list:DockPadding(20,0,20,20) - model_category:SetContents(model_category_list) - - local pac_allow_blood_color_box = vgui.Create("DCheckBoxLabel", master_list) - pac_allow_blood_color_box:SetText("Blood") - pac_allow_blood_color_box:SetSize(400,30) - pac_allow_blood_color_box:SetConVar("pac_allow_blood_color") - model_category_list:Add(pac_allow_blood_color_box) - local pac_allow_mdl_box = vgui.Create("DCheckBoxLabel", master_list) - pac_allow_mdl_box:SetText("MDL") - pac_allow_mdl_box:SetSize(400,30) - pac_allow_mdl_box:SetConVar("pac_allow_mdl") - model_category_list:Add(pac_allow_mdl_box) - local pac_allow_mdl_entity_box = vgui.Create("DCheckBoxLabel", master_list) - pac_allow_mdl_entity_box:SetText("Entity MDL") - pac_allow_mdl_entity_box:SetSize(400,30) - pac_allow_mdl_entity_box:SetConVar("pac_allow_mdl_entity") - model_category_list:Add(pac_allow_mdl_entity_box) - local pac_modifier_model_box = vgui.Create("DCheckBoxLabel", master_list) - pac_modifier_model_box:SetText("Entity model") - pac_modifier_model_box:SetSize(400,30) - pac_modifier_model_box:SetConVar("pac_modifier_model") - model_category_list:Add(pac_modifier_model_box) - local pac_modifier_size_box = vgui.Create("DCheckBoxLabel", master_list) - pac_modifier_size_box:SetText("Entity size") - pac_modifier_size_box:SetSize(400,30) - pac_modifier_size_box:SetConVar("pac_modifier_size") - model_category_list:Add(pac_modifier_size_box) - - --movement and mass - --[[ - pac_free_movement - ]] - - local movement_category = master_list:Add("Player Movement") - movement_category.Header:SetSize(40,40) - movement_category.Header:SetFont("DermaLarge") - local movement_category_list = vgui.Create("DListLayout") - movement_category_list:DockPadding(20,20,20,20) - movement_category:SetContents(movement_category_list) + --general server stuff + PopulateCategory("Allowed Playermodel Mutations", master_list, convar_params_modifiers) + --player movement stuff + local movement_category_list = PopulateCategory("Player Movement", master_list, convar_params_movement) local pac_allow_movement_form = vgui.Create("DComboBox", movement_category_list) - pac_allow_movement_form:SetText("Allow PAC player movement") - --pac_allow_movement_form:SetSize(400,20) - pac_allow_movement_form:SetSortItems(false) - - pac_allow_movement_form:AddChoice("disabled") - pac_allow_movement_form:AddChoice("disabled if noclip not allowed") - pac_allow_movement_form:AddChoice("enabled") - - pac_allow_movement_form.OnSelect = function(_, _, value) - if value == "disabled" then - net.Start("pac_send_sv_cvar") - net.WriteString("pac_free_movement") - net.WriteString("0") - net.SendToServer() - --pac_allow_movement_form.form = generic_form("PAC player movement is disabled.") - elseif value == "disabled if noclip not allowed" then - net.Start("pac_send_sv_cvar") - net.WriteString("pac_free_movement") - net.WriteString("-1") - net.SendToServer() - --pac_allow_movement_form.form = generic_form("PAC player movement is disabled if noclip is not allowed.") - elseif value == "enabled" then - net.Start("pac_send_sv_cvar") - net.WriteString("pac_free_movement") - net.WriteString("1") - net.SendToServer() - --pac_allow_movement_form.form = generic_form("PAC player movement is enabled.") - end + pac_allow_movement_form:SetText("Allow PAC player movement") + pac_allow_movement_form:SetSize(400, 30) + pac_allow_movement_form:SetSortItems(false) + + pac_allow_movement_form:AddChoice("disabled") + pac_allow_movement_form:AddChoice("disabled if noclip not allowed") + pac_allow_movement_form:AddChoice("enabled") + + pac_allow_movement_form.OnSelect = function(_, _, value) + if value == "disabled" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("0") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is disabled.") + elseif value == "disabled if noclip not allowed" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("-1") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is disabled if noclip is not allowed.") + elseif value == "enabled" then + net.Start("pac_send_sv_cvar") + net.WriteString("pac_free_movement") + net.WriteString("1") + net.SendToServer() + --pac_allow_movement_form.form = generic_form("PAC player movement is enabled.") end + end - --mode:ChooseOption(mode_str) - - local pac_player_movement_allow_mass_box = vgui.Create("DCheckBoxLabel", movement_category_list) - pac_player_movement_allow_mass_box:SetText("Allow Modify Mass") - pac_player_movement_allow_mass_box:SetSize(400,30) - movement_category_list:Add(pac_player_movement_allow_mass_box) - pac_player_movement_allow_mass_box:SetConVar("pac_player_movement_allow_mass") - - local playermovement_min_mass_numbox = vgui.Create("DNumSlider", movement_category_list) - playermovement_min_mass_numbox:SetText("Mimnimum mass players can set for themselves") - playermovement_min_mass_numbox:SetValue(GetConVar("pac_player_movement_min_mass"):GetFloat()) - playermovement_min_mass_numbox:SetMin(0.01) playermovement_min_mass_numbox:SetDecimals(0) playermovement_min_mass_numbox:SetMax(1000000) - playermovement_min_mass_numbox:SetSize(400,30) - movement_category_list:Add(playermovement_min_mass_numbox) - playermovement_min_mass_numbox:SetConVar("pac_player_movement_min_mass") - - - local playermovement_max_mass_numbox = vgui.Create("DNumSlider", movement_category_list) - playermovement_max_mass_numbox:SetText("Maximum mass players can set for themselves") - playermovement_max_mass_numbox:SetValue(GetConVar("pac_player_movement_max_mass"):GetFloat()) - playermovement_max_mass_numbox:SetMin(0.01) playermovement_max_mass_numbox:SetDecimals(0) playermovement_max_mass_numbox:SetMax(1000000) - playermovement_max_mass_numbox:SetSize(400,30) - movement_category_list:Add(playermovement_max_mass_numbox) - playermovement_max_mass_numbox:SetConVar("pac_player_movement_max_mass") - - - local pac_player_movement_allow_mass_dmgscaling_box = vgui.Create("DCheckBoxLabel", movement_category_list) - pac_player_movement_allow_mass_dmgscaling_box:SetText("Allow damage scaling of physics damage based on player's mass") - pac_player_movement_allow_mass_dmgscaling_box:SetSize(400,30) - movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) - pac_player_movement_allow_mass_dmgscaling_box:SetConVar("pac_player_movement_physics_damage_scaling") - movement_category_list:Add(pac_player_movement_allow_mass_dmgscaling_box) - - - --wear limits and bans - --[[ - pac_sv_draw_distance - pac_sv_hide_outfit_on_death WORKSHOP DEPRECATED - pac_submit_limit - pac_submit_spam - pac_ban - pac_unban - ]] - - local wear_list = master_list:Add("Server wearing/drawing") - wear_list.Header:SetSize(40,40) - wear_list.Header:SetFont("DermaLarge") - local draw_distance_list = vgui.Create("DListLayout") - draw_distance_list:DockPadding(20,0,20,20) - wear_list:SetContents(draw_distance_list) - - local draw_dist_numbox = vgui.Create("DNumSlider", draw_distance_list) - draw_dist_numbox:SetText("Server draw distance") - draw_dist_numbox:SetValue(GetConVar("pac_sv_draw_distance"):GetInt()) - draw_dist_numbox:SetMin(0) draw_dist_numbox:SetDecimals(0) draw_dist_numbox:SetMax(50000) - draw_dist_numbox:SetSize(400,30) - draw_dist_numbox:SetConVar("pac_sv_draw_distance") - - local pac_submit_limit_numbox = vgui.Create("DNumSlider", draw_distance_list) - pac_submit_limit_numbox:SetText("pac_submit limit") - pac_submit_limit_numbox:SetValue(GetConVar("pac_submit_limit"):GetInt()) - pac_submit_limit_numbox:SetMin(0) pac_submit_limit_numbox:SetDecimals(0) pac_submit_limit_numbox:SetMax(100) - pac_submit_limit_numbox:SetSize(400,30) - pac_submit_limit_numbox:SetConVar("pac_submit_limit") - - local pac_submit_spam_box = vgui.Create("DCheckBoxLabel", draw_distance_list) - pac_submit_spam_box:SetText("prevent pac_submit spam") - pac_submit_spam_box:SetSize(400,30) - pac_submit_spam_box:SetConVar("pac_submit_spam") - - - - --misc - --[[ - sv_pac_webcontent_allow_no_content_length - sv_pac_webcontent_limit - pac_to_contraption_allow - pac_max_contraption_entities - pac_restrictions - ]] - local misc_list = master_list:Add("Misc") - misc_list.Header:SetSize(40,40) - misc_list.Header:SetFont("DermaLarge") - local misc_list_list = vgui.Create("DListLayout") - misc_list_list:DockPadding(20,0,20,20) - misc_list:SetContents(misc_list_list) - local webcontent_no_content_box = vgui.Create("DCheckBoxLabel", misc_list_list) - webcontent_no_content_box:SetText("allow downloads with no content length") - webcontent_no_content_box:SetSize(400,30) - webcontent_no_content_box:SetConVar("sv_pac_webcontent_allow_no_content_length") - - local cam_restrict_box = vgui.Create("DCheckBoxLabel", misc_list_list) - cam_restrict_box:SetText("restrict PAC editor camera movement") - cam_restrict_box:SetSize(400,30) - cam_restrict_box:SetConVar("pac_restrictions") - + PopulateCategory("Server wearing/drawing", master_list, convar_params_wearing_drawing) + PopulateCategory("Misc", master_list, convar_params_misc) return master_list end From d76f7c25a6ef02771e70574477a14febf89352e3 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 27 Dec 2023 16:41:25 -0500 Subject: [PATCH 148/300] Admin utilities in spawnmenu --- lua/pac3/editor/client/spawnmenu.lua | 84 ++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index 74467033e..67a7c607b 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -109,6 +109,79 @@ function pace.ClientSettingsMenu(self) self:NumSlider(L"Max render time: ", "pac_max_render_time", 0, 100, 0) end +function pace.AdminSettingsMenu(self) + if not LocalPlayer():IsAdmin() then return end + if not IsValid(self) then return end + self:Button("Open PAC3 settings menu (Admin)", "pace_settings") + + self:Help(L"PAC3 outfits: general server policy"):SetFont("DermaDefaultBold") + self:NumSlider(L"Server Draw distance:", "pac_sv_draw_distance", 0, 20000, 0) + self:CheckBox(L"Prevent spam with pac_submit", "pac_submit_spam") + self:CheckBox(L"Players need to +USE on others to reveal outfits", "pac_onuse_only_force") + self:CheckBox(L"Restrict editor camera", "pac_restrictions") + self:CheckBox(L"Allow MDL zips", "pac_allow_mdl") + self:CheckBox(L"Allow MDL zips for entity", "pac_allow_mdl_entity") + self:CheckBox(L"Allow entity model modifier", "pac_modifier_model") + self:CheckBox(L"Allow entity size modifier", "pac_modifier_size") + self:CheckBox(L"Allow blood color modifier", "pac_allow_blood_color") + self:Help(""):SetFont("DermaDefaultBold")--spacers + self:Help(""):SetFont("DermaDefaultBold") + + self:Help(L"PAC3 combat: general server policy"):SetFont("DermaDefaultBold") + self:NumSlider(L"Rate limiter", "pac_sv_combat_enforce_netrate", 0, 1000, 0) + self:NumSlider(L"Distance limiter", "pac_sv_combat_distance_enforced", 0, 64000, 0) + self:NumSlider(L"Allowance, in number of messages", "pac_sv_combat_enforce_netrate_buffersize", 0, 400, 0) + self:CheckBox(L"Use general prop protection based on player consents", "pac_sv_prop_protection") + self:NumSlider(L"Entity limit per combat action", "pac_sv_entity_limit_per_combat_operation", 0, 1000, 0) + self:NumSlider(L"Entity limit per player", "pac_sv_entity_limit_per_player_per_combat_operation", 0, 500, 0) + self:CheckBox(L"Only specifically allowed users can do pac3 combat actions", "pac_sv_combat_whitelisting") + self:Help(""):SetFont("DermaDefaultBold")--spacers + self:Help(""):SetFont("DermaDefaultBold") + + self:Help(L"Combat parts (more detailed settings in the full editor settings menu)"):SetFont("DermaDefaultBold") + self:Help(L"Damage Zones"):SetFont("DermaDefaultBold") + self:CheckBox(L"Enable damage zones", "pac_sv_damage_zone") + self:NumSlider(L"Max damage", "pac_sv_damage_zone_max_damage", 0, 268435455, 0) + self:NumSlider(L"Max radius", "pac_sv_damage_zone_max_radius", 0, 32767, 0) + self:NumSlider(L"Max length", "pac_sv_damage_zone_max_length", 0, 32767, 0) + self:CheckBox(L"Enable damage zone dissolve", "pac_sv_damage_zone_allow_dissolve") + + self:Help(L"Hitscan"):SetFont("DermaDefaultBold") + self:CheckBox(L"Enable hitscan part", "pac_sv_hitscan") + self:NumSlider(L"Max damage", "pac_sv_hitscan_max_damage", 0, 268435455, 0) + self:CheckBox(L"Force damage division among multi-shot bullets", "pac_sv_hitscan_divide_max_damage_by_max_bullets") + + self:Help(L"Lock part"):SetFont("DermaDefaultBold") + self:CheckBox(L"Enable lock part", "pac_sv_lock") + self:CheckBox(L"Allow grab", "pac_sv_lock_grab") + self:CheckBox(L"Allow teleport", "pac_sv_lock_teleport") + + self:Help(L"Force part"):SetFont("DermaDefaultBold") + self:CheckBox(L"Enable force part", "pac_sv_force") + self:NumSlider(L"Max amount", "pac_sv_force_max_amount", 0, 10000000, 0) + self:NumSlider(L"Max radius", "pac_sv_force_max_radius", 0, 32767, 0) + self:NumSlider(L"Max length", "pac_sv_force_max_length", 0, 32767, 0) + + self:Help(L"Force part"):SetFont("DermaDefaultBold") + self:CheckBox(L"Enable health modifier", "pac_sv_health_modifier") + self:CheckBox(L"Allow changing max health or armor", "pac_sv_health_modifier_allow_maxhp") + self:NumSlider(L"Minimum combined damage scaling", "pac_sv_health_modifier_min_damagescaling", -10, 1, 2) + self:CheckBox(L"Allow extra health bars", "pac_sv_health_modifier_extra_bars") + + self:Help(L"Projectile part"):SetFont("DermaDefaultBold") + self:CheckBox(L"Enable physical projectiles", "pac_sv_projectiles") + self:CheckBox(L"Enable custom collide meshes for physical projectiles", "pac_sv_projectile_allow_custom_collision_mesh") + self:NumSlider(L"Max speed", "pac_sv_force_max_amount", 0, 5000, 0) + self:NumSlider(L"Max physical radius", "pac_sv_projectile_max_phys_radius", 0, 4095, 0) + self:NumSlider(L"Max damage radius", "pac_sv_projectile_max_damage_radius", 0, 4095, 0) + + self:Help(L"Player movement part"):SetFont("DermaDefaultBold") + self:CheckBox(L"Allow playermovement", "pac_free_movement") + self:CheckBox(L"Allow playermovement mass", "pac_player_movement_allow_mass") + self:CheckBox(L"Allow physics damage scaling by mass", "pac_player_movement_physics_damage_scaling") + +end + local icon_cvar = CreateConVar("pac_icon", "0", {FCVAR_ARCHIVE}, "Use the new PAC4.5 icon or the old PAC icon.\n0 = use the old one\n1 = use the new one") @@ -181,6 +254,17 @@ hook.Add("PopulateToolMenu", "pac_spawnmenu", function() { } ) + spawnmenu.AddToolMenuOption( + "Utilities", + "PAC", + "PAC3Admin", + L"Admin", + "", + "", + pace.AdminSettingsMenu, + { + } + ) end) if IsValid(g_ContextMenu) and CreateContextMenu then From 44e15dec8297bd979bc930d9a9215adc5b651d90 Mon Sep 17 00:00:00 2001 From: techbot Date: Fri, 29 Dec 2023 07:16:47 +0100 Subject: [PATCH 149/300] make the animation editor less of a pain to use -you can now duplicate keyframes on the spot and to the start/end -fix broken timeline --- lua/pac3/editor/client/animation_timeline.lua | 193 +++++++++--------- 1 file changed, 102 insertions(+), 91 deletions(-) diff --git a/lua/pac3/editor/client/animation_timeline.lua b/lua/pac3/editor/client/animation_timeline.lua index 3ef70db67..91613c6db 100644 --- a/lua/pac3/editor/client/animation_timeline.lua +++ b/lua/pac3/editor/client/animation_timeline.lua @@ -115,6 +115,14 @@ function timeline.UpdateFrameData() timeline.dummy_bone:SetAngles(Angle(data.RR, data.RU, data.RF)) end +function timeline.Reindex() + timeline.frame:Clear() + for i, v in ipairs(timeline.data.FrameData) do + local keyframe = timeline.frame:AddKeyFrame(true) + keyframe:SetFrameData(i, v) + end +end + function timeline.EditBone() pace.Call("PartSelected", timeline.dummy_bone) local boneData = pac.GetModelBones(timeline.entity) @@ -174,7 +182,6 @@ function timeline.Load(data) timeline.frame:Clear() timeline.SelectKeyframe(timeline.frame:AddKeyFrame()) - timeline.Save() end timeline.UpdateFrameData() @@ -249,8 +256,8 @@ function timeline.Open(part) timeline.entity = part:GetOwner() timeline.frame = vgui.Create("pac3_timeline") - timeline.frame:SetSize(ScrW()-pace.Editor:GetWide(),93) - timeline.frame:SetPos(pace.Editor:GetWide(),ScrH()-timeline.frame:GetTall()) + timeline.frame:SetSize(ScrW()-pace.Editor:GetWide(), 93) + timeline.frame:SetPos(pace.Editor:GetWide(), ScrH()-timeline.frame:GetTall()) timeline.frame:SetTitle("") timeline.frame:ShowCloseButton(false) @@ -343,36 +350,36 @@ do local TIMELINE = {} function TIMELINE:Init() - self:DockMargin(0,0,0,0) - self:DockPadding(0,35,0,0) + self:DockMargin(0, 0, 0, 0) + self:DockPadding(0, 30, 0, 0) do -- time display info local time = self:Add("DPanel") local test = L"frame" .. ": 10.888" surface.SetFont(pace.CurrentFont) - local w,h = surface.GetTextSize(test) + local w, h = surface.GetTextSize(test) time:SetWide(w) time:SetTall(h*2 + 2) - time:SetPos(0,1) - time.Paint = function(s, w,h) + time:SetPos(0, 1) + time.Paint = function(s, w, h) self:GetSkin().tex.Tab_Control( 0, 0, w, h ) self:GetSkin().tex.CategoryList.Header( 0, 0, w, h ) if not timeline.animation_part then return end - local w,h = draw.TextShadow({ + local w, h = draw.TextShadow({ text = L"frame" .. ": " .. (animations.GetEntityAnimationFrame(timeline.entity, timeline.animation_part:GetAnimID()) or 0), font = pace.CurrentFont, - pos = {2, 0}, + pos = {5, 0}, color = self:GetSkin().Colours.Category.Header }, 1, 100) draw.TextShadow({ text = L"time" .. ": " .. math.Round(timeline.GetCycle() * animations.GetAnimationDuration(timeline.entity, timeline.animation_part:GetAnimID()), 3), font = pace.CurrentFont, - pos = {2, h}, + pos = {5, h}, color = self:GetSkin().Colours.Category.Header }, 1, 100) end @@ -393,18 +400,18 @@ do local spacing = (size - 24)/2 local play = controls:Add("DButton") - play:SetSize(size,size) + play:SetSize(size, size) play:SetText("") play.DoClick = function() self:Toggle() end play:Dock(LEFT) local stop = controls:Add("DButton") - stop:SetSize(size,size) + stop:SetSize(size, size) stop:SetText("") stop.DoClick = function() self:Stop() end stop:Dock(LEFT) - function play.PaintOver(_,w,h) + function play.PaintOver(_, w, h) surface.SetDrawColor(self:GetSkin().Colours.Button.Normal) draw.NoTexture() if self:IsPlaying() then @@ -419,9 +426,9 @@ do end end - function stop:PaintOver(w,h) + function stop:PaintOver(w, h) surface.SetDrawColor(self:GetSkin().Colours.Button.Normal) - surface.DrawRect(spacing,spacing,24,24) + surface.DrawRect(spacing, spacing, 24, 24) end end do -- save/load @@ -491,7 +498,7 @@ do menu:PerformLayout() - local x, y = bottom:LocalToScreen(0,0) + local x, y = bottom:LocalToScreen(0, 0) x = x + bottom:GetWide() menu:SetPos(x - menu:GetWide(), y - menu:GetTall()) end @@ -500,10 +507,10 @@ do end do -- keyframes - local pnl = vgui.Create("pac_scrollpanel_horizontal",self) + local pnl = vgui.Create("pac_scrollpanel_horizontal", self) pnl:Dock(FILL) - pnl:GetCanvas().Paint = function(_,w,h) + pnl:GetCanvas().Paint = function(_, w, h) derma.SkinHook( "Paint", "ListBox", self, w, h ) end @@ -528,7 +535,7 @@ do if self.moving then return end local x = 0 - for k,v in ipairs(pnl:GetCanvas():GetChildren()) do + for k, v in ipairs(pnl:GetCanvas():GetChildren()) do v:SetWide(math.max(1/v:GetData().FrameRate * secondDistance, 4)) v:SetTall(h) v:SetPos(x, 0) @@ -540,7 +547,7 @@ do end do -- timeline - local pnl = vgui.Create("DPanel",self) + local pnl = vgui.Create("DPanel", self) surface.SetFont(pace.CurrentFont) local _, h = surface.GetTextSize("|") @@ -571,20 +578,19 @@ do local start = Material("icon16/control_play_blue.png") local restart = Material("icon16/control_repeat_blue.png") local estyle = Material("icon16/arrow_branch.png") - pnl.Paint = function(s,w,h) + pnl.Paint = function(s, w, h) local offset = -self.keyframe_scroll:GetCanvas():GetPos() - local esoffset = self.keyframe_scroll:GetCanvas():GetPos() self:GetSkin().tex.Tab_Control( 0, 0, w, h ) self:GetSkin().tex.CategoryList.Header( 0, 0, w, h ) local previousSecond = offset-(offset%secondDistance) - for i=previousSecond,previousSecond+s:GetWide(),secondDistance/2 do + for i = previousSecond, previousSecond+s:GetWide(), secondDistance/2 do if i-offset > 0 and i-offset < ScrW() then local sec = i/secondDistance local x = i-offset - surface.SetDrawColor(0,0,0,100) + surface.SetDrawColor(0, 0, 0, 100) surface.DrawLine(x+1, 1+1, x+1, pnl:GetTall() - 3+1) surface.SetDrawColor(self:GetSkin().Colours.Category.Header) @@ -592,7 +598,7 @@ do surface.SetTextPos(x+2+1, 1+1) surface.SetFont(pace.CurrentFont) - surface.SetTextColor(0,0,0,100) + surface.SetTextColor(0, 0, 0, 100) surface.DrawText(sec) surface.SetTextPos(x+2, 1) @@ -602,10 +608,10 @@ do end end - for i=previousSecond,previousSecond+s:GetWide(),secondDistance/8 do + for i = previousSecond, previousSecond+s:GetWide(), secondDistance/8 do if i-offset > 0 and i-offset < ScrW() then local x = i-offset - surface.SetDrawColor(0,0,0,100) + surface.SetDrawColor(0, 0, 0, 100) surface.DrawLine(x+1, 1+1, x+1, pnl:GetTall()/2+1) surface.SetDrawColor(self:GetSkin().Colours.Category.Header) @@ -623,24 +629,28 @@ do local esmat = v.estyle and estyle or false if mat then - local x = v:GetPos() - surface.SetDrawColor(255,255,255,200) + local x = v:GetPos() - offset + if x > s:GetWide() - 10 then continue end + surface.SetDrawColor(255, 255, 255, 200) surface.DrawLine(x, -mat:Height()/2 - 5, x, h) - surface.SetDrawColor(255,255,255,255) + surface.SetDrawColor(255, 255, 255, 255) surface.SetMaterial(mat) - surface.DrawTexturedRect(1+x,mat:Height() - 5,mat:Width(), mat:Height()) + surface.DrawTexturedRect(1+x, mat:Height() - 5, mat:Width(), mat:Height()) end if esmat then local ps = v:GetSize() - local x = v:GetPos() + (ps * 0.5) - surface.SetDrawColor(255,255,255,255) + local x = v:GetPos() - offset + (ps * 0.5) + if x > s:GetWide() - 10 then continue end + surface.SetDrawColor(255, 255, 255, 255) surface.SetMaterial(esmat) - surface.DrawTexturedRect(1+x - (esmat:Width() * 0.5), esmat:Height(),esmat:Width(), esmat:Height()) + surface.DrawTexturedRect(1+x - (esmat:Width() * 0.5), esmat:Height(), esmat:Width(), esmat:Height()) if ps >= 65 then - draw.SimpleText( v.estyle, "Default", x, esmat:Height() * 2, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP ) + draw.SimpleText( v.estyle, pace.CurrentFont, x, esmat:Height() * 2, self:GetSkin().Colours.Label.Dark, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP ) + else + v:SetTooltip(v.estyle) end end end @@ -650,17 +660,17 @@ do local x = timeline.GetCycle() * self.keyframe_scroll:GetCanvas():GetWide() x = x - offset - surface.SetDrawColor(255,0,0,200) + surface.SetDrawColor(255, 0, 0, 200) surface.DrawLine(x, 0, x, h) - surface.SetDrawColor(255,0,0,255) + surface.SetDrawColor(255, 0, 0, 255) surface.SetMaterial(scrub) - surface.DrawTexturedRect(1 + x - scrub:Width()/2,-11,scrub:Width(), scrub:Height()) + surface.DrawTexturedRect(1 + x - scrub:Width()/2, -11, scrub:Width(), scrub:Height()) end end end - function TIMELINE:Paint(w,h) + function TIMELINE:Paint(w, h) self:GetSkin().tex.Tab_Control(0, 35, w, h-35) end @@ -668,11 +678,11 @@ do DFrame.Think(self) if pace.Editor:GetPos() + pace.Editor:GetWide() / 2 < ScrW() / 2 then - self:SetSize(ScrW()-(pace.Editor.x+pace.Editor:GetWide()),93) - self:SetPos(pace.Editor.x+pace.Editor:GetWide(),ScrH()-self:GetTall()) + self:SetSize(ScrW()-(pace.Editor.x+pace.Editor:GetWide()), 93) + self:SetPos(pace.Editor.x+pace.Editor:GetWide(), ScrH()-self:GetTall()) else - self:SetSize(ScrW()-(ScrW()-pace.Editor.x),93) - self:SetPos(0,ScrH()-self:GetTall()) + self:SetSize(ScrW()-(ScrW()-pace.Editor.x), 93) + self:SetPos(0, ScrH()-self:GetTall()) end if input.IsKeyDown(KEY_SPACE) then @@ -729,7 +739,7 @@ do end function TIMELINE:Clear() - for i,v in pairs(self.keyframe_scroll:GetCanvas():GetChildren()) do + for i, v in pairs(self.keyframe_scroll:GetCanvas():GetChildren()) do v:Remove() end self.add_keyframe_button:SetDisabled(false) @@ -754,7 +764,7 @@ do local restartFrame = timeline.data.RestartFrame if not restartFrame then return 0 end --no restart pos? start at the start - for i,v in ipairs(timeline.data.FrameData) do + for i, v in ipairs(timeline.data.FrameData) do if i == restartFrame then return timeInSeconds end timeInSeconds = timeInSeconds+(1/(v.FrameRate or 1)) end @@ -768,7 +778,7 @@ do local startFrame = timeline.data.StartFrame if not startFrame then return 0 end --no restart pos? start at the start - for i,v in ipairs(timeline.data.FrameData) do + for i, v in ipairs(timeline.data.FrameData) do if i == startFrame then return timeInSeconds end timeInSeconds = timeInSeconds+(1/(v.FrameRate or 1)) end @@ -793,7 +803,7 @@ do return keyframe end - vgui.Register("pac3_timeline",TIMELINE,"DFrame") + vgui.Register("pac3_timeline", TIMELINE, "DFrame") end do @@ -830,7 +840,8 @@ do function KEYFRAME:GetData() return self.DataTable end - function KEYFRAME:SetFrameData(index,tbl) + + function KEYFRAME:SetFrameData(index, tbl) self.DataTable = tbl self.AnimationKeyIndex = index self:GetParent():GetParent():InvalidateLayout() --rebuild the timeline @@ -849,19 +860,19 @@ do return self.AnimationKeyIndex end - function KEYFRAME:Paint(w,h) + function KEYFRAME:Paint(w, h) self.AltLine = self.Alternate derma.SkinHook( "Paint", "CategoryButton", self, w, h ) if timeline.selected_keyframe == self then local c = self:GetSkin().Colours.Category.Line.Button_Selected - surface.SetDrawColor(c.r,c.g,c.b,250) + surface.SetDrawColor(c.r, c.g, c.b, 250) end - surface.DrawRect(0,0,w,h) + surface.DrawRect(0, 0, w, h) - surface.SetDrawColor(0,0,0,75) - surface.DrawOutlinedRect(0,0,w,h) + surface.SetDrawColor(0, 0, 0, 75) + surface.DrawOutlinedRect(0, 0, w, h) end function KEYFRAME:Think() @@ -897,14 +908,14 @@ do return (a:GetPos() + a:GetWide() / 2) < (b:GetPos() + b:GetWide() / 2) end) - for i,v in ipairs(panels) do + for i, v in ipairs(panels) do v:SetParent(timeline.frame.keyframe_scroll) v.Alternate = #timeline.frame.keyframe_scroll:GetCanvas():GetChildren()%2 == 1 frames[i] = timeline.data.FrameData[v:GetAnimationIndex()] end - for i,v in ipairs(frames) do + for i, v in ipairs(frames) do timeline.data.FrameData[i] = v panels[i].AnimationKeyIndex = i end @@ -940,8 +951,9 @@ do timeline.frame:Toggle(false) timeline.SelectKeyframe(self) elseif mc == MOUSE_RIGHT then + timeline.SelectKeyframe(self) local menu = DermaMenu() - menu:AddOption(L"set length",function() + menu:AddOption(L"set length", function() Derma_StringRequest(L"question", L"how long should this frame be in seconds?", tostring(self:GetWide()/secondDistance), @@ -951,7 +963,7 @@ do L"cancel" ) end):SetImage("icon16/time.png") - menu:AddOption(L"multiply length",function() + menu:AddOption(L"multiply length", function() Derma_StringRequest(L"question", L"multiply "..self:GetAnimationIndex().."'s length", "1.0", @@ -962,36 +974,36 @@ do end):SetImage("icon16/time_add.png") if not self:GetRestart() then - menu:AddOption(L"set restart",function() - for _,v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do + menu:AddOption(L"set restart", function() + for _, v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do v:SetRestart(false) end self:SetRestart(true) timeline.data.RestartFrame = self:GetAnimationIndex() end):SetImage("icon16/control_repeat_blue.png") else - menu:AddOption(L"unset restart",function() + menu:AddOption(L"unset restart", function() self:SetRestart(false) timeline.data.StartFrame = nil end):SetImage("icon16/control_repeat.png") end if not self:GetStart() then - menu:AddOption(L"set start",function() - for _,v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do + menu:AddOption(L"set start", function() + for _, v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do v:SetStart(false) end self:SetStart(true) timeline.data.StartFrame = self:GetAnimationIndex() end):SetImage("icon16/control_play_blue.png") else - menu:AddOption(L"unset start",function() + menu:AddOption(L"unset start", function() self:SetStart(false) timeline.data.StartFrame = nil end):SetImage("icon16/control_play.png") end - menu:AddOption(L"reverse",function() + menu:AddOption(L"reverse", function() local frame = timeline.data.FrameData[self:GetAnimationIndex() - 1] if not frame then frame = timeline.data.FrameData[#timeline.data.FrameData] @@ -1009,32 +1021,29 @@ do timeline.UpdateFrameData() end):SetImage("icon16/control_rewind_blue.png") - menu:AddOption(L"duplicate to end", function() - local keyframe = timeline.frame:AddKeyFrame() + local function duplicateTo(index) + local data = self:GetData(); + table.insert(timeline.data.FrameData, index, table.Copy(data)) + timeline.Reindex() - local tbl = self:GetData().BoneInfo - for i, v in pairs(tbl) do - local data = keyframe:GetData() - data.BoneInfo[i] = table.Copy(self:GetData().BoneInfo[i] or {}) - data.BoneInfo[i].MU = v.MU - data.BoneInfo[i].MR = v.MR - data.BoneInfo[i].MF = v.MF - data.BoneInfo[i].RU = v.RU - data.BoneInfo[i].RR = v.RR - data.BoneInfo[i].RF = v.RF - end - keyframe:SetLength(1/(self:GetData().FrameRate)) - timeline.SelectKeyframe(keyframe) - end):SetImage("icon16/application_double.png") + timer.Simple(0, function() + timeline.SelectKeyframe(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()[index]) + end) + end + + local sub, opt = menu:AddSubMenu(L"duplicate", function() duplicateTo(self:GetAnimationIndex()) end) + sub:AddOption(L"start", function() duplicateTo(1) end):SetImage("icon16/resultset_first.png") + sub:AddOption(L"end", function() duplicateTo(#timeline.data.FrameData + 1) end):SetImage("icon16/resultset_last.png") + opt:SetIcon("icon16/page_copy.png") - menu:AddOption(L"remove",function() + menu:AddOption(L"remove", function() local frameNum = self:GetAnimationIndex() if frameNum == 1 and not timeline.data.FrameData[2] then return end table.remove(timeline.data.FrameData, frameNum) local remove_i - for i,v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do + for i, v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do if v == self then remove_i = i elseif v:GetAnimationIndex() > frameNum then @@ -1048,14 +1057,16 @@ do timeline.frame.keyframe_scroll:InvalidateLayout() self:Remove() - -- * even if it was removed from the table it still exists for some reason + local count = #timeline.frame.keyframe_scroll:GetCanvas():GetChildren() - local offset = frameNum == count and count - 1 or count - timeline.SelectKeyframe(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()[offset]) - end):SetImage("icon16/application_delete.png") + local offset = remove_i >= count and count - 1 or remove_i + timer.Simple(0, function() + timeline.SelectKeyframe(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()[offset]) + end) + end):SetImage("icon16/page_delete.png") menu:AddOption(L"set easing style", function() - if timeline.data.Interpolation != "linear" then + if timeline.data.Interpolation ~= "linear" then local frame = vgui.Create("DFrame") frame:SetSize(300, 100) frame:Center() @@ -1072,8 +1083,6 @@ do return end - local frameNum = self:GetAnimationIndex() - local frame = vgui.Create( "DFrame" ) frame:SetSize( 200, 100 ) frame:Center() @@ -1121,12 +1130,14 @@ do if not style then return end self:GetData().EaseStyle = style self.estyle = style + timeline.Save() end function KEYFRAME:RemoveEaseStyle() self:GetData().EaseStyle = nil self.estyle = nil + timeline.Save() end - vgui.Register("pac3_timeline_keyframe",KEYFRAME,"DPanel") + vgui.Register("pac3_timeline_keyframe", KEYFRAME, "DPanel") end \ No newline at end of file From 91e1b63f5a3f2cb4f24dc09ff8d0300974713018 Mon Sep 17 00:00:00 2001 From: techbot Date: Fri, 29 Dec 2023 13:22:46 +0100 Subject: [PATCH 150/300] set the default interpolation type to linear so you don't have to switch it any time and who even uses the other ones? --- lua/pac3/editor/client/animation_timeline.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/animation_timeline.lua b/lua/pac3/editor/client/animation_timeline.lua index 91613c6db..6c7a791a3 100644 --- a/lua/pac3/editor/client/animation_timeline.lua +++ b/lua/pac3/editor/client/animation_timeline.lua @@ -63,7 +63,7 @@ local function check_tpose() end end -timeline.interpolation = "cosine" +timeline.interpolation = "linear" function timeline.SetInterpolation(str) timeline.interpolation = str From 3d8cb757ca65cbe42429fd6804240f7149ff2bf1 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 29 Dec 2023 15:18:18 -0500 Subject: [PATCH 151/300] add viewed_by_owner event and correct what is_client does --- lua/pac3/core/client/parts/event.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 709a914aa..b9173559f 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -732,13 +732,21 @@ PART.OldEvents = { is_client = { operator_type = "none", - tutorial_explanation = "is_client makes something visible only for you, or others (uninverted)", + tutorial_explanation = "is_client activates when the group owner entity is your player or viewmodel, rather than another entity like a prop", callback = function(self, ent) ent = try_viewmodel(ent) return self:GetPlayerOwner() == ent end, }, + viewed_by_owner = { + operator_type = "none", + tutorial = "viewed_by_owner shows for only you. uninvert to show only to other players", + callback = function(self, ent) + return self:GetPlayerOwner() == pac.LocalPlayer + end, + }, + is_flashlight_on = { operator_type = "none", callback = function(self, ent) From a3009ca2a341d543c1ce73a8c831ef5efc9c82a5 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 29 Dec 2023 16:31:10 -0500 Subject: [PATCH 152/300] send force part PointEntities bool to server --- lua/pac3/core/client/parts/force.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 451582611..743326da4 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -215,6 +215,7 @@ function PART:Impulse(on) net.WriteBool(self.AffectSelf) net.WriteBool(self.Players) net.WriteBool(self.PhysicsProps) + net.WriteBool(self.PointEntities) net.WriteBool(self.NPC) net.SendToServer() end @@ -306,4 +307,4 @@ function PART:SetBaseForce(val) end end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() From 039a0ae516a7cd64095784d5205ab05d757e6d9b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 29 Dec 2023 16:31:16 -0500 Subject: [PATCH 153/300] Update net_combat.lua notify for more cvars rework the force statements' order receive the force part's PointEntities bool --- lua/pac3/extra/shared/net_combat.lua | 89 ++++++++++++++++------------ 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 13691bcd2..f92436ebb 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -37,8 +37,8 @@ local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force") local healthmod_allow = CreateConVar("pac_sv_health_modifier", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside") -local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") -local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") +local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") +local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") @@ -1077,21 +1077,24 @@ if SERVER then if TooManyEnts(ent_count) and not (tbl.AffectSelf and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end for _,ent in pairs(ents_hits) do local phys_ent + local ent_getphysobj = ent:GetPhysicsObject() local owner = Try_CPPIGetOwner(ent) + local is_player = ent:IsPlayer() + local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner()))) + local is_npc = ent.IsVJBaseSNPC or ent.IsDRGEntity or string.find(ent:GetClass(), "npc") or ent:IsNPC() + + if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) and ( - ent:IsPlayer() - or (string.find(ent:GetClass(), "npc") ~= nil) - or ent:IsNPC() - or physics_point_ent_classes[ent:GetClass()] - or string.find(ent:GetClass(),"item_") - or string.find(ent:GetClass(),"ammo_") - or (ent:IsWeapon() and not IsValid(ent:GetOwner())) + is_player + or is_npc + or is_physics + or IsValid( ent_getphysobj ) ) then local is_phys = true - if ent:GetPhysicsObject() ~= nil then - phys_ent = ent:GetPhysicsObject() + if ent_getphysobj ~= nil then + phys_ent = ent_getphysobj if (string.find(ent:GetClass(), "npc") ~= nil) then phys_ent = ent end @@ -1215,39 +1218,46 @@ if SERVER then add_angvel = add_angvel * dist_multiplier local unconsenting_owner = owner ~= ply and force_consents[owner] == false - - if (ent:IsPlayer() and tbl.Players) or (ent == ply and tbl.AffectSelf) then - if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then - phys_ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) - ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) + if ent:GetClass() == "pac_projectile" then print(ent, ent:GetPhysicsObject(), "is_player", is_player, "is_physics", is_physics, "is_npc", is_npc, "tbl.PointEntities", tbl.PointEntities) end + if is_player then + if tbl.Players or (ent == ply and tbl.AffectSelf) then + if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then + phys_ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) + ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) + end end - - elseif (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) and tbl.PhysicsProps then - if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then - if IsValid(phys_ent) then - ent:PhysWake() - ent:SetVelocity(final_damping * oldvel + addvel) - if islocaltorque then - phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) - phys_ent:AddAngleVelocity(add_angvel) - - else - phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) - add_angvel = phys_ent:WorldToLocalVector( add_angvel ) - phys_ent:ApplyTorqueCenter(add_angvel) + elseif is_physics then + if tbl.PhysicsProps then + if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then + if IsValid(phys_ent) then + ent:PhysWake() + ent:SetVelocity(final_damping * oldvel + addvel) + if islocaltorque then + phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) + phys_ent:AddAngleVelocity(add_angvel) + + else + phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) + add_angvel = phys_ent:WorldToLocalVector( add_angvel ) + phys_ent:ApplyTorqueCenter(add_angvel) + end + ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces + phys_ent:SetVelocity((oldvel * final_damping) + addvel) end - ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces - phys_ent:SetVelocity((oldvel * final_damping) + addvel) end end - elseif (ent:IsNPC() or string.find(ent:GetClass(), "npc") ~= nil) and tbl.NPC then - if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then - if IsValid(phys_ent) and phys_ent:GetVelocity():Length() > 500 then - local vec = oldvel + addvel - local clamp_vec = vec:GetNormalized()*500 - ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer - else ent:SetVelocity((oldvel * final_damping) + addvel) end + + elseif is_npc then + if tbl.NPC then + if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then + if IsValid(phys_ent) and phys_ent:GetVelocity():Length() > 500 then + local vec = oldvel + addvel + local clamp_vec = vec:GetNormalized()*500 + ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer + else ent:SetVelocity((oldvel * final_damping) + addvel) end + end end + elseif tbl.PointEntities then if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then phys_ent:SetVelocity(final_damping * oldvel + addvel) @@ -1505,6 +1515,7 @@ if SERVER then tbl.AffectSelf = net.ReadBool() tbl.Players = net.ReadBool() tbl.PhysicsProps = net.ReadBool() + tbl.PointEntities = net.ReadBool() tbl.NPC = net.ReadBool() --server limits From d1bb6ee936588d4522df8f99faa57bc6e4fda622 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 29 Dec 2023 16:38:07 -0500 Subject: [PATCH 154/300] remove debug print --- lua/pac3/extra/shared/net_combat.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index f92436ebb..da887d17c 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1218,7 +1218,7 @@ if SERVER then add_angvel = add_angvel * dist_multiplier local unconsenting_owner = owner ~= ply and force_consents[owner] == false - if ent:GetClass() == "pac_projectile" then print(ent, ent:GetPhysicsObject(), "is_player", is_player, "is_physics", is_physics, "is_npc", is_npc, "tbl.PointEntities", tbl.PointEntities) end + if is_player then if tbl.Players or (ent == ply and tbl.AffectSelf) then if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then From 6635cfa78371cc9a67a0c5988024ade3c0e0a439 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 29 Dec 2023 17:07:07 -0500 Subject: [PATCH 155/300] use more pac hooks --- lua/pac3/core/client/parts/force.lua | 10 +++++----- lua/pac3/core/client/parts/interpolated_multibone.lua | 8 ++++---- lua/pac3/core/client/parts/lock.lua | 4 ++-- lua/pac3/core/client/parts/text.lua | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 743326da4..a78e4eda5 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -70,22 +70,22 @@ function PART:OnShow() end function PART:OnHide() - hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) + pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) self:Impulse(false) end function PART:OnRemove() - hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) + pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) self:Impulse(false) end function PART:OnDraw() self.pos,self.ang = self:GetDrawPosition() - if not self.Preview then hook.Remove("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end + if not self.Preview then pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end if self.Preview then - hook.Add("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID, function() + pac.AddHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID, function() if self.HitboxMode == "Box" then local mins = Vector(-self.Radius, -self.Radius, -self.Length) local maxs = Vector(self.Radius, self.Radius, self.Length) @@ -307,4 +307,4 @@ function PART:SetBaseForce(val) end end -BUILDER:Register() +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index f7f471dc9..38f24a742 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -57,12 +57,12 @@ function PART:OnShow() end function PART:OnHide() - hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) + pac.RemoveHook("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end function PART:OnRemove() - hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) + pac.RemoveHook("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end --NODES self 1 2 3 --STAGE 0 1 2 3 @@ -74,13 +74,13 @@ function PART:OnDraw() self.pos = self.pos or self:GetWorldPosition() self.ang = self.ang or self:GetWorldAngles() - if not self.Preview then hook.Remove("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end + if not self.Preview then pac.RemoveHook("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end local stage = math.max(0,math.floor(self.LerpValue)) local proportion = math.max(0,self.LerpValue) % 1 if self.Preview then - hook.Add("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID, function() + pac.AddHook("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID, function() render.DrawLine(self.pos,self.pos + self.ang:Forward()*50, Color(255,0,0)) render.DrawLine(self.pos,self.pos - self.ang:Right()*50, Color(0,255,0)) render.DrawLine(self.pos,self.pos + self.ang:Up()*50, Color(0,0,255)) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index b2857b183..7fbee65bc 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -303,7 +303,7 @@ function PART:OnShow() self.resetting_condition = false end end - hook.Add("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID, function() + pac.AddHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID, function() if self.TargetPart:IsValid() then origin_part = self.TargetPart else @@ -368,7 +368,7 @@ function PART:OnShow() end function PART:OnHide() - hook.Remove("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID) + pac.RemoveHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID) self.teleported = false self.grabbing = false if self.target_ent == nil then return diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index bf83627d0..a6b296dc0 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -407,7 +407,7 @@ function PART:OnDraw() cam_End3D2D() cam_End3D() elseif self.DrawMode == "SurfaceText" or self.DrawMode == "DrawTextOutlined2D" then - hook.Add("HUDPaint", "pac.DrawText"..self.UniqueID, function() + pac.AddHook("HUDPaint", "pac.DrawText"..self.UniqueID, function() if not pcall(surface_SetFont, self.UsedFont) then return end self:SetFont(self.UsedFont) @@ -469,9 +469,9 @@ function PART:OnDraw() end) end if self.DrawMode == "DrawTextOutlined" then - hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) + pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) end - else hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) end + else pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) end end function PART:Initialize() @@ -525,10 +525,10 @@ function PART:OnShow() end function PART:OnHide() - hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) + pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) end function PART:OnRemove() - hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) + pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) end function PART:SetText(str) self.Text = str From 57ae4a481277c02450cf86fea6e86166d74c19e7 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 4 Jan 2024 19:57:34 -0500 Subject: [PATCH 156/300] Update parts.lua use correct fields for angle components --- lua/pac3/editor/client/parts.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index eb77a71c1..0e27f7b29 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1628,9 +1628,9 @@ do -- menu elseif var_type == "Angle" then local str = string.Split(VAR_PANEL_EDITZONE:GetValue(), ",") sent_var = Angle() - sent_var.r = tonumber(str[1]) or 1 - sent_var.g = tonumber(str[2]) or 1 - sent_var.b = tonumber(str[3]) or 1 + sent_var.p = tonumber(str[1]) or 1 + sent_var.y = tonumber(str[2]) or 1 + sent_var.r = tonumber(str[3]) or 1 else sent_var = VAR_PANEL_EDITZONE:GetValue() end From e6c413074a62780c42865c1822e0071ab397e1f1 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 4 Jan 2024 22:03:07 -0500 Subject: [PATCH 157/300] Update damage_zone.lua use pac_projectile_part field in the initialize hack instead of classname --- lua/pac3/core/client/parts/damage_zone.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index d09294530..9c9683e6c 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -989,8 +989,9 @@ function PART:Initialize() 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 timer.Simple(0.1, function() --jank fix on the jank fix to allow it earlier on projectiles and hitmarkers - if IsValid(self:GetRootPart():GetOwner()) then - if self:GetRootPart():GetOwner():GetClass() == "pac_projectile" or self:GetRootPart():GetOwner().is_pac_hitmarker then + local ent = self:GetRootPart():GetOwner() + if IsValid(ent) then + if ent.is_pac_hitmarker or ent.pac_projectile_part then self.validTime = 0 end end From 71985d7439f70ab2db8d93caf453a7f54718e7b3 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 6 Jan 2024 15:54:29 -0500 Subject: [PATCH 158/300] is_touching events initialization fix and some exit conditions for apply auto setup command events --- lua/pac3/core/client/parts/event.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index b9173559f..5ea8a5dee 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -122,6 +122,7 @@ function PART:SetEvent(event) if not self.Events[event] then --invalid event? try a command event if GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then timer.Simple(0.2, function() + if not self.pace_properties or self ~= pace.current_part then return end --now we'll use event as a command name self:SetEvent("command") self.pace_properties["Event"]:SetValue("command") @@ -939,6 +940,7 @@ PART.OldEvents = { userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = 0}}, callback = function(self, ent, extra_radius, nearest_model) if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return false end extra_radius = extra_radius or 0 local radius = ent:BoundingRadius() @@ -968,6 +970,7 @@ PART.OldEvents = { end, nice = function(self, ent, extra_radius, nearest_model) if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return "" end local radius = ent:BoundingRadius() if radius == 0 and IsValid(ent.pac_projectile) then @@ -988,6 +991,7 @@ PART.OldEvents = { userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = false}, {default = false}, {default = false}}, callback = function(self, ent, extra_radius, no_npc, no_players, nearest_model) if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return false end extra_radius = extra_radius or 0 no_npc = no_npc or false no_players = no_players or false @@ -1020,6 +1024,7 @@ PART.OldEvents = { end, nice = function(self, ent, extra_radius, no_npc, no_players, nearest_model) if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return "" end local radius = ent:BoundingRadius() if radius == 0 and IsValid(ent.pac_projectile) then @@ -1045,6 +1050,7 @@ PART.OldEvents = { callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return false end extra_radius = extra_radius or 0 no_npc = no_npc or false no_players = no_players or false @@ -1089,6 +1095,7 @@ PART.OldEvents = { nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model) if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return "" end local radius = ent:BoundingRadius() if radius == 0 and IsValid(ent.pac_projectile) then @@ -1113,6 +1120,7 @@ PART.OldEvents = { userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}}, callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) if nearest_model then ent = self:GetOwner() end + if not IsValid(ent) then return false end extra_radius = extra_radius or 15 x_stretch = x_stretch or 1 y_stretch = y_stretch or 1 @@ -1138,7 +1146,7 @@ PART.OldEvents = { end, nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) if nearest_model then ent = self:GetOwner() end - if not IsValid(ent) then return false end + if not IsValid(ent) then return "" end local radius = ent:BoundingRadius() if radius == 0 and IsValid(ent.pac_projectile) then From e7891b2d9c31a0bdf7939051207566a76029f0b9 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 6 Jan 2024 15:57:15 -0500 Subject: [PATCH 159/300] small correction for shortcut preset --- lua/pac3/editor/client/shortcuts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 9edab443b..2f7496c05 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -250,7 +250,7 @@ pace.PACActionShortcut_Experimental = { [1] = {"CTRL", "y"}, [2] = {"i"} }, - ["tpose"] = { + ["T_Pose"] = { [1] = {"CTRL", "t"} }, ["zoom_panel"] = { From 22b0fa1499393c16b304d2962e3cc5204da41769 Mon Sep 17 00:00:00 2001 From: Redox Date: Wed, 10 Jan 2024 16:34:27 +0100 Subject: [PATCH 160/300] Remove trailing whitespaces --- lua/pac3/editor/client/settings.lua | 10 +++---- lua/pac3/editor/client/spawnmenu.lua | 12 ++++---- .../editor/server/pac_settings_manager.lua | 2 +- lua/pac3/extra/shared/net_combat.lua | 28 +++++++++---------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 3323bc483..d6272efe6 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -691,7 +691,7 @@ function pace.OpenSettings() for cmd, val in pairs(pace.cvar_changes) do if isbool(val) then changes_str = changes_str .. cmd .. " set to " .. (val and "1" or "0") .. "\n" - else + else changes_str = changes_str .. cmd .. " set to " .. val .. "\n" end end @@ -702,12 +702,12 @@ function pace.OpenSettings() net.WriteString(cmd) if isbool(val) then net.WriteString(val and "1" or "0") - else + else net.WriteString(val) end net.SendToServer() end - pace.cvar_changes = nil + pace.cvar_changes = nil end, "Cancel", function() pace.cvar_changes = nil end) end @@ -912,7 +912,7 @@ local function PopulateCategory(str, pnl, cvars_tbl) cvar_pnl:SetText(tbl[2]) if tbl[3] ~= "" then cvar_pnl:SetTooltip(tbl[3]) end cvar_pnl:SetSize(400,30) - + end return list_list end @@ -938,7 +938,7 @@ function pace.FillCombatSettings(pnl) --general PopulateCategory("General (Global policy and Network protections)", master_list, convar_params_combat_generic) - + --combat parts PopulateCategory("Force part", master_list, convar_params_force) PopulateCategory("Damage Zone", master_list, convar_params_damage_zone) diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index 67a7c607b..a130b0e67 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -137,7 +137,7 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Only specifically allowed users can do pac3 combat actions", "pac_sv_combat_whitelisting") self:Help(""):SetFont("DermaDefaultBold")--spacers self:Help(""):SetFont("DermaDefaultBold") - + self:Help(L"Combat parts (more detailed settings in the full editor settings menu)"):SetFont("DermaDefaultBold") self:Help(L"Damage Zones"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable damage zones", "pac_sv_damage_zone") @@ -145,7 +145,7 @@ function pace.AdminSettingsMenu(self) self:NumSlider(L"Max radius", "pac_sv_damage_zone_max_radius", 0, 32767, 0) self:NumSlider(L"Max length", "pac_sv_damage_zone_max_length", 0, 32767, 0) self:CheckBox(L"Enable damage zone dissolve", "pac_sv_damage_zone_allow_dissolve") - + self:Help(L"Hitscan"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable hitscan part", "pac_sv_hitscan") self:NumSlider(L"Max damage", "pac_sv_hitscan_max_damage", 0, 268435455, 0) @@ -155,19 +155,19 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Enable lock part", "pac_sv_lock") self:CheckBox(L"Allow grab", "pac_sv_lock_grab") self:CheckBox(L"Allow teleport", "pac_sv_lock_teleport") - + self:Help(L"Force part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable force part", "pac_sv_force") self:NumSlider(L"Max amount", "pac_sv_force_max_amount", 0, 10000000, 0) self:NumSlider(L"Max radius", "pac_sv_force_max_radius", 0, 32767, 0) self:NumSlider(L"Max length", "pac_sv_force_max_length", 0, 32767, 0) - + self:Help(L"Force part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable health modifier", "pac_sv_health_modifier") self:CheckBox(L"Allow changing max health or armor", "pac_sv_health_modifier_allow_maxhp") self:NumSlider(L"Minimum combined damage scaling", "pac_sv_health_modifier_min_damagescaling", -10, 1, 2) self:CheckBox(L"Allow extra health bars", "pac_sv_health_modifier_extra_bars") - + self:Help(L"Projectile part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable physical projectiles", "pac_sv_projectiles") self:CheckBox(L"Enable custom collide meshes for physical projectiles", "pac_sv_projectile_allow_custom_collision_mesh") @@ -179,7 +179,7 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Allow playermovement", "pac_free_movement") self:CheckBox(L"Allow playermovement mass", "pac_player_movement_allow_mass") self:CheckBox(L"Allow physics damage scaling by mass", "pac_player_movement_physics_damage_scaling") - + end diff --git a/lua/pac3/editor/server/pac_settings_manager.lua b/lua/pac3/editor/server/pac_settings_manager.lua index 294abc54c..d9da6e2be 100644 --- a/lua/pac3/editor/server/pac_settings_manager.lua +++ b/lua/pac3/editor/server/pac_settings_manager.lua @@ -102,5 +102,5 @@ net.Receive("pac_request_sv_cvars", function (len, ply) net.WriteTable(cvars_tbl) net.Send(ply) end) - + end) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index da887d17c..3c0c0e4d7 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -216,7 +216,7 @@ if SERVER then if ent.CPPIGetOwner then --a prop protection using CPPI probably exists so we use it return ent:CPPIGetOwner() end - return ent.pac_prop_protection_owner or nil --otherwise we'll use the field we set or + return ent.pac_prop_protection_owner or nil --otherwise we'll use the field we set or end --hack fix to stop GetOwner returning [NULL Entity] @@ -656,7 +656,7 @@ if SERVER then return true end end - + local total_hp_value,built_tbl = GatherExtraHPBars(target) if surviving_layer == nil or total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult @@ -696,7 +696,7 @@ if SERVER then for i,v in pairs(ents_hits) do if not (v:IsPlayer() or v:IsNPC() or string.find(v:GetClass(), "npc_")) and not tbl.PointEntities then ents_hits[i] = nil end if v.CPPICanDamage and not v:CPPICanDamage(ply) then ents_hits[i] = nil end --CPPI check on the player - + if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil else ent_count = ent_count + 1 @@ -780,7 +780,7 @@ if SERVER then --the giga function to determine if we can damage local function DMGAllowed(ent) if ent:Health() == 0 and not (string.find(tbl.DamageType, "dissolve")) then return false end --immediately exclude entities with 0 health, except if we want to dissolve - + local canhit = false --whether the policies allow the hit local prop_protected_consent @@ -803,14 +803,14 @@ if SERVER then end --first pass: entity class blacklist - + if IsEntity(ent) and ((damageable_point_ent_classes[ent:GetClass()] ~= false) or ((damageable_point_ent_classes[ent:GetClass()] == nil) or (damageable_point_ent_classes[ent:GetClass()] == true))) then --second pass: the damagezone's settings --1.player hurt self if asked local is_player = ent:IsPlayer() local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) local is_npc = ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity - + if (tbl.AffectSelf) and ent == inflictor then canhit = true --2.main target types : players, NPC, point entities @@ -819,7 +819,7 @@ if SERVER then and --enforce prop protection (bot_exception or (owner == inflictor or ent == inflictor or (pac_sv_prop_protection and damage_zone_consents[target_ply] ~= false) or not pac_sv_prop_protection)) then - + if is_player then if tbl.Players then canhit = true @@ -1081,8 +1081,8 @@ if SERVER then local owner = Try_CPPIGetOwner(ent) local is_player = ent:IsPlayer() local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner()))) - local is_npc = ent.IsVJBaseSNPC or ent.IsDRGEntity or string.find(ent:GetClass(), "npc") or ent:IsNPC() - + local is_npc = ent.IsVJBaseSNPC or ent.IsDRGEntity or string.find(ent:GetClass(), "npc") or ent:IsNPC() + if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) and ( @@ -1235,7 +1235,7 @@ if SERVER then if islocaltorque then phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) phys_ent:AddAngleVelocity(add_angvel) - + else phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity()) add_angvel = phys_ent:WorldToLocalVector( add_angvel ) @@ -1246,7 +1246,7 @@ if SERVER then end end end - + elseif is_npc then if tbl.NPC then if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then @@ -1257,7 +1257,7 @@ if SERVER then else ent:SetVelocity((oldvel * final_damping) + addvel) end end end - + elseif tbl.PointEntities then if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then phys_ent:SetVelocity(final_damping * oldvel + addvel) @@ -1804,7 +1804,7 @@ if SERVER then if not lock_allow:GetBool() then return end if not lock_allow_grab:GetBool() then return end if not PlayerIsCombatAllowed(ply) then return end - + --netrate enforce if not CountNetMessage(ply) then @@ -2051,7 +2051,7 @@ if SERVER then elseif (global_combat_prop_protection:GetBool() and prop_protected) then return end end - + targ_ent:SetAngles(ang) ApplyLockState(targ_ent, false) From e342f7b6e9b3446717e0d31229db78c9a4a36b05 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 10 Jan 2024 10:44:31 -0500 Subject: [PATCH 161/300] move combat parts info prints the requesting of pac combat parts is now silent on join, verbose on the concommand --- lua/pac3/extra/shared/net_combat.lua | 36 +++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 3c0c0e4d7..0985c3ea8 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -2384,30 +2384,34 @@ if CLIENT then local function RequestBlockedParts() - net.Start("pac_request_blocked_parts") net.SendToServer() end - concommand.Add("pac_inform_about_blocked_parts", RequestBlockedParts) - net.Receive("pac_inform_blocked_parts", function() - pac.Blocked_Combat_Parts = net.ReadTable() - print("Are these pac combat parts blocked?") + concommand.Add("pac_inform_about_blocked_parts", function() + RequestBlockedParts() + pac.Message("Manually fetching info about pac3 combat parts...") - for name,b in pairs(pac.Blocked_Combat_Parts) do - local blocked = b - local disabled = not GetConVar("pac_sv_"..name):GetBool() + timer.Simple(2, function() + for name,b in pairs(pac.Blocked_Combat_Parts) do + local blocked = b + local disabled = not GetConVar("pac_sv_"..name):GetBool() - local bool_str + local bool_str - if disabled and blocked then bool_str = "disabled and blocked -> unavailable" - elseif disabled and not blocked then bool_str = "disabled -> unavailable" - elseif not disabled and blocked then bool_str = "blocked - > unavailable" - elseif not disabled and not blocked then bool_str = "available" - else bool_str = "??" end + if disabled and blocked then bool_str = "disabled and blocked -> unavailable" + elseif disabled and not blocked then bool_str = "disabled -> unavailable" + elseif not disabled and blocked then bool_str = "blocked - > unavailable" + elseif not disabled and not blocked then bool_str = "available" + else bool_str = "??" end - print(name .. " is " .. bool_str) - end + print(name .. " is " .. bool_str) + end + end) + end) + + net.Receive("pac_inform_blocked_parts", function() --silent + pac.Blocked_Combat_Parts = net.ReadTable() end) local consent_cvars = {"pac_client_grab_consent", "pac_client_lock_camera_consent", "pac_client_damage_zone_consent", "pac_client_force_consent", "pac_client_hitscan_consent"} From 1088e45e7c07cd1203a22dd788faca3db840a79b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 20 Jan 2024 18:48:20 -0500 Subject: [PATCH 162/300] implement volume setting, drawable part limiters, NPC friendliness pac_stopsound works for websound too, unlike console stopsound clients can choose their own distance limits for sounds, shakes, sunbeams and 2D texts they see shake also has duration and amplitude limits new consent cvar for clients to exclude friendly or neutral NPCs from being damaged by hitscan and damage zone added killicon for pac_bullet_emitter combat parts reinitialize: from a spawnmenu options button, remotely request the refresh via a net message on multiplayer to make it work on dedicated servers --- lua/pac3/core/client/part_pool.lua | 22 +++++ lua/pac3/core/client/parts/legacy/ogg.lua | 3 +- lua/pac3/core/client/parts/legacy/sound.lua | 52 +++++++++++- .../core/client/parts/legacy/webaudio.lua | 5 +- lua/pac3/core/client/parts/shake.lua | 12 ++- lua/pac3/core/client/parts/sound.lua | 25 +++++- lua/pac3/core/client/parts/sunbeams.lua | 8 +- lua/pac3/core/client/parts/text.lua | 22 ++--- lua/pac3/editor/client/menu_bar.lua | 14 +++- lua/pac3/editor/client/spawnmenu.lua | 48 +++++++++-- lua/pac3/extra/shared/net_combat.lua | 82 +++++++++++++++++-- lua/pac3/libraries/webaudio.lua | 2 + 12 files changed, 255 insertions(+), 40 deletions(-) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 40a701e3d..05778d92b 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -659,6 +659,28 @@ function pac.UpdateButtonEvents(ply, key, down) end end +function pac.StopSound() + for _,part in pairs(all_parts) do + if part.ClassName == "sound" or part.ClassName == "sound2" or part.ClassName == "ogg" or part.ClassName == "webaudio" then + part:StopSound(true) + end + end +end + +function pac.ForceUpdateSoundVolumes() + for _,part in pairs(all_parts) do + if part.ClassName == "sound" then + if part.csptch then part.csptch:ChangeVolume(math.Clamp(part.Volume * pac.volume, 0.001, 1), 0) end + elseif part.ClassName == "sound2" or part.ClassName == "ogg" then + if part.last_stream and part.last_stream.SetVolume then part.last_stream:SetVolume(part.Volume * pac.volume) end + elseif part.ClassName == "webaudio" then + for key, stream in pairs(part.streams) do + if stream and stream.SetVolume then stream:SetVolume(part.Volume * pac.volume) end + end + end + end +end + cvars.AddChangeCallback("pac_hide_disturbing", function() for _, part in pairs(all_parts) do if part:IsValid() then diff --git a/lua/pac3/core/client/parts/legacy/ogg.lua b/lua/pac3/core/client/parts/legacy/ogg.lua index db2d6c200..0ed146cce 100644 --- a/lua/pac3/core/client/parts/legacy/ogg.lua +++ b/lua/pac3/core/client/parts/legacy/ogg.lua @@ -208,7 +208,7 @@ function PART:PlaySound(_, additiveVolumeFraction) self.last_stream = stream end -function PART:StopSound() +function PART:StopSound(force_stop) for key, stream in pairs(self.streams) do if not stream:IsValid() then self.streams[key] = nil goto CONTINUE end @@ -219,6 +219,7 @@ function PART:StopSound() stream:Stop() end end + if force_stop then stream:Stop() end ::CONTINUE:: end end diff --git a/lua/pac3/core/client/parts/legacy/sound.lua b/lua/pac3/core/client/parts/legacy/sound.lua index 8cd06b100..dce2c2024 100644 --- a/lua/pac3/core/client/parts/legacy/sound.lua +++ b/lua/pac3/core/client/parts/legacy/sound.lua @@ -42,6 +42,9 @@ function PART:Initialize() end function PART:OnShow(from_rendering) + local pos = self:GetWorldPosition() + if pos:DistToSqr(pac.EyePos) > pac.sounds_draw_dist_sqr then return end + if not from_rendering then self.played_overlapping = false self:PlaySound() @@ -80,10 +83,31 @@ function PART:OnHide() end end +--this is not really stopping the sound, rather putting the volume very low so the sounds don't replay when going back in range +function PART:Silence(b) + if not self.csptch then return end + if not self.csptch:IsPlaying() then return end + + if b then + self.csptch:ChangeVolume(0.01, 0) + else + self.csptch:ChangeVolume(math.Clamp(self.Volume * pac.volume, 0.001, 1), 0) + end +end + function PART:OnThink() + local pos = self:GetWorldPosition() + if pos:DistToSqr(pac.EyePos) > pac.sounds_draw_dist_sqr then + self.out_of_range = true + self:Silence(true) + else + if self.out_of_range then self:Silence(false) end + self.out_of_range = false + end if not self.csptch then self:PlaySound() - else + end + if self.csptch then if self.Loop then pac.playing_sound = true if not self.csptch:IsPlaying() then self.csptch:Play() end @@ -148,7 +172,7 @@ function PART:SetVolume(num) end if self.csptch then - self.csptch:ChangeVolume(math.Clamp(self.Volume, 0.001, 1), 0) + self.csptch:ChangeVolume(math.Clamp(self.Volume * pac.volume, 0.001, 1), 0) end end @@ -244,6 +268,8 @@ function PART:PlaySound(osnd, ovol) vol = self.Volume end + vol = vol * pac.volume + local pitch if self.MinPitch == self.MaxPitch then @@ -277,10 +303,30 @@ function PART:PlaySound(osnd, ovol) end end -function PART:StopSound() +function PART:StopSound(force_stop) if self.csptch then self.csptch:Stop() end + if force_stop and self.Overlapping then + local ent = self.RootOwner and self:GetRootPart():GetOwner() or self:GetOwner() + if IsValid(ent) then + local sounds = self.Sound:Split(";") + if string.match(sounds[1],"%[(%d-),(%d-)%]") then + local min, max = string.match(sounds[1],"%[(%d-),(%d-)%]") + if max < min then + max = min + end + for i=min, max, 1 do + snd = self.Sound:gsub("(%[%d-,%d-%])", i) + ent:StopSound(snd) + end + else + for i,snd in ipairs(sounds) do + ent:StopSound(snd) + end + end + end + end end local channels = diff --git a/lua/pac3/core/client/parts/legacy/webaudio.lua b/lua/pac3/core/client/parts/legacy/webaudio.lua index d278813d8..578fd4192 100644 --- a/lua/pac3/core/client/parts/legacy/webaudio.lua +++ b/lua/pac3/core/client/parts/legacy/webaudio.lua @@ -64,7 +64,7 @@ function PART:OnDraw() local shouldMute = snd_mute_losefocus:GetBool() local focus = system.HasFocus() - local volume = shouldMute and not focus and 0 or self:GetVolume() + local volume = shouldMute and not focus and 0 or self:GetVolume() * pac.volume --we need to hack it into here because OnDraw is called often for url, streamdata in pairs(self.streams) do local stream = streamdata.stream @@ -264,7 +264,7 @@ function PART:PlaySound() self.last_stream = stream end -function PART:StopSound() +function PART:StopSound(force_stop) local toremove for key, streamdata in pairs(self.streams) do @@ -280,6 +280,7 @@ function PART:StopSound() pcall(function() stream:SetTime(0) end) stream:Pause() end + if force_stop then stream:Stop() end elseif stream then toremove = toremove or {} table.insert(toremove, key) diff --git a/lua/pac3/core/client/parts/shake.lua b/lua/pac3/core/client/parts/shake.lua index 5fe8a3028..ae2124bd9 100644 --- a/lua/pac3/core/client/parts/shake.lua +++ b/lua/pac3/core/client/parts/shake.lua @@ -4,6 +4,10 @@ PART.ClassName = "shake" PART.Group = 'effects' PART.Icon = 'icon16/transmit.png' +local draw_distance = CreateClientConVar("pac_limit_shake_draw_distance", "500", true, false) +local max_duration = CreateClientConVar("pac_limit_shake_duration", "2", true, false) +local max_amplitude = CreateClientConVar("pac_limit_shake_amplitude", "40", true, false) + BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") BUILDER:SetPropertyGroup("shake") @@ -18,14 +22,18 @@ function PART:OnShow(from_rendering) if not from_rendering then local position = self:GetDrawPosition() local eyedistance = position:Distance(pac.EyePos) - local radius = math.Clamp(self.Radius, 0.0001, 500) + + --clamp down the part's requested values with the viewer client's cvar + local radius = math.Clamp(self.Radius, 0.0001, math.max(draw_distance:GetInt(),0.0001)) + local duration = math.Clamp(self.Duration, 0.0001, max_duration:GetInt()) + local amplitude = math.Clamp(self.Amplitude, 0.0001, max_amplitude:GetInt()) if eyedistance < radius then local amplitude = self.Amplitude if self.Falloff then amplitude = amplitude * (1 - (eyedistance / radius)) end - util.ScreenShake(position, amplitude, self.Frequency, math.Clamp(self.Duration, 0.0001, 2), 0) + util.ScreenShake(position, amplitude, self.Frequency, duration, 0) end end end diff --git a/lua/pac3/core/client/parts/sound.lua b/lua/pac3/core/client/parts/sound.lua index 57072d8d9..23ca2dccc 100644 --- a/lua/pac3/core/client/parts/sound.lua +++ b/lua/pac3/core/client/parts/sound.lua @@ -8,6 +8,7 @@ PART.ClassName = "sound2" PART.Icon = 'icon16/music.png' PART.Group = 'effects' + BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") BUILDER:GetSet("Path", "", {editor_panel = "sound"}) @@ -129,13 +130,29 @@ BIND("VolumeLFOTime") BIND("Doppler") +function PART:Silence(b) + if b then + if self.last_stream and self.last_stream.SetVolume then self.last_stream:SetVolume(0) end + else + if self.last_stream and self.last_stream.SetVolume then self.last_stream:SetVolume(self.Volume * pac.volume) end + end +end + function PART:OnThink() local owner = self:GetRootPart():GetOwner() + local pos = self:GetWorldPosition() + if pos:DistToSqr(pac.EyePos) > pac.sounds_draw_dist_sqr then + self.out_of_range = true + self:Silence(true) + else + if self.out_of_range then self:Silence(false) end + self.out_of_range = false + end for url, stream in pairs(self.streams) do if not stream:IsValid() then self.streams[url] = nil goto CONTINUE end - if self.PlayCount == 0 then + if self.PlayCount == 0 and not self.stopsound then stream:Resume() end @@ -270,6 +287,8 @@ end function PART:PlaySound(_, additiveVolumeFraction) --PrintTable(self.streams) additiveVolumeFraction = additiveVolumeFraction or 0 + local pos = self:GetWorldPosition() + if pos:DistToSqr(pac.EyePos) > pac.sounds_draw_dist_sqr then return end local stream = table.Random(self.streams) or NULL if not stream:IsValid() then return end @@ -320,7 +339,7 @@ function PART:PlaySound(_, additiveVolumeFraction) self.last_stream = stream end -function PART:StopSound() +function PART:StopSound(force_stop) for key, stream in pairs(self.streams) do if stream:IsValid() then if self.PauseOnHide then @@ -328,6 +347,7 @@ function PART:StopSound() elseif self.StopOnHide then stream:Stop() end + if force_stop then stream:Stop() self.stopsound = true end end end end @@ -336,6 +356,7 @@ function PART:OnShow(from_rendering) if not from_rendering then self:PlaySound() end + self.stopsound = false end function PART:OnHide() diff --git a/lua/pac3/core/client/parts/sunbeams.lua b/lua/pac3/core/client/parts/sunbeams.lua index 2793c1063..2be731db7 100644 --- a/lua/pac3/core/client/parts/sunbeams.lua +++ b/lua/pac3/core/client/parts/sunbeams.lua @@ -7,11 +7,14 @@ local BUILDER, PART = pac.PartTemplate("base_drawable") PART.ClassName = "sunbeams" PART.Group = 'effects' PART.Icon = 'icon16/weather_sun.png' +local draw_distance = CreateClientConVar("pac_limit_sunbeams_draw_distance", "1000", true, false) + BUILDER:StartStorableVars() BUILDER:GetSet("Darken", 0) BUILDER:GetSet("Multiplier", 0.25, {editor_sensitivity = 0.25}) BUILDER:GetSet("Size", 0.1, {editor_sensitivity = 0.25}) + BUILDER:GetSet("DrawDistance", 1000, {editor_onchange = function(self, val) return math.max(val,0) end}) BUILDER:GetSet("Translucent", true) BUILDER:EndStorableVars() @@ -27,7 +30,10 @@ function PART:OnDraw() local pos = self:GetDrawPosition() local spos = pos:ToScreen() - local dist_mult = - math.Clamp(pac.EyePos:Distance(pos) / 1000, 0, 1) + 1 + --clamp down the part's requested values with the viewer client's cvar + local distance = math.min(self.DrawDistance, math.max(draw_distance:GetInt(),0.1)) + + local dist_mult = - math.Clamp(pac.EyePos:Distance(pos) / distance, 0, 1) + 1 DrawSunbeams( self.Darken, diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index a6b296dc0..2dc16fde7 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -13,6 +13,8 @@ local Color = Color local BUILDER, PART = pac.PartTemplate("base_drawable") +local draw_distance = CreateClientConVar("pac_limit_text_2d_draw_distance", "1000", true, false, "How far to see other players' text parts using 2D modes. They will start fading out 200 units before this distance.") + local default_fonts = { "BudgetLabel", "CenterPrintText", @@ -434,17 +436,11 @@ function PART:OnDraw() end surface.SetTextPos(pos2d.x, pos2d.y) - local dist = (EyePos() - self:GetWorldPosition()):Length() - local fadestartdist = 200 - local fadeenddist = 1000 - if fadestartdist == 0 then fadestartdist = 0.1 end - if fadeenddist == 0 then fadeenddist = 0.1 end - - if fadestartdist > fadeenddist then - local temp = fadestartdist - fadestartdist = fadeenddist - fadeenddist = temp - end + 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 @@ -453,10 +449,8 @@ function PART:OnDraw() elseif self.DrawMode == "SurfaceText" then surface.DrawText(DisplayText, self.ForceAdditive) end - else - local fade = math.pow(math.Clamp(1 - (dist-fadestartdist)/fadeenddist,0,1),3) - + 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 diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 387ee2b41..48a3a64a0 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -165,11 +165,21 @@ local function populate_options(menu) combat_consents.GetDeleteSelf = function() return false end pnlcc:SetImage("icon16/joystick.png") + local npc_pref = combat_consents:AddOption(L"Level of protection for friendly NPCs", function() + Derma_Query("Prevent friendly fire against NPCs? (damage zone and hitscan)", "NPC relationship preferences (pac_client_npc_exclusion_consent = " .. GetConVar("pac_client_npc_exclusion_consent"):GetInt() .. ")", + "Don't protect (0)", function() GetConVar("pac_client_npc_exclusion_consent"):SetInt(0) end, + "Protect friendly NPCs (1)", function() GetConVar("pac_client_npc_exclusion_consent"):SetInt(1) end, + "Protect friendly and neutral NPCs (2)", function() GetConVar("pac_client_npc_exclusion_consent"):SetInt(2) end, + "cancel") + end) + npc_pref:SetImage("icon16/group.png") + npc_pref:SetTooltip("\"Friendliness\" is based on an NPC's Disposition toward you: Error&Hate, Fear&Neutral, Like") + combat_consents:AddCVar(L"damage_zone part (area damage)", "pac_client_damage_zone_consent", "1", "0") combat_consents:AddCVar(L"hitscan part (bullets)", "pac_client_hitscan_consent", "1", "0") combat_consents:AddCVar(L"force part (physics forces)", "pac_client_force_consent", "1", "0") - combat_consents:AddCVar(L"lock part's grab (can take control of your position)", "pac_client_grab_consent", "1", "0") - combat_consents:AddCVar(L"lock part's grab calcview (can take control of your view)", "pac_client_lock_camera_consent", "1", "0") + combat_consents:AddCVar(L"lock part's grab (can take control of your position and eye angles)", "pac_client_grab_consent", "1", "0") + combat_consents:AddCVar(L"lock part's grab calcview (can take control of your view position)", "pac_client_lock_camera_consent", "1", "0"):SetTooltip("You're still not immune to it changing your eye angles.\nCalcviews are a different thing than eye angles.") menu:AddSpacer() diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index a130b0e67..e5e35ca2c 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -101,18 +101,54 @@ function pace.ClientOptionsMenu(self) self:Button(L"request outfits", "pac_request_outfits") end +CreateClientConVar("pac_limit_sounds_draw_distance", 20000, true, false, "Overall multiplier for PAC3 sounds") +cvars.AddChangeCallback("pac_limit_sounds_draw_distance", function(_,_,val) + if not isnumber(val) then val = 0 end + pac.sounds_draw_dist_sqr = val * val +end) +pac.sounds_draw_dist_sqr = math.pow(GetConVar("pac_limit_sounds_draw_distance"):GetInt(), 2) + +CreateClientConVar("pac_volume", 1, true, false, "Overall multiplier for PAC3 sounds",0,1) +cvars.AddChangeCallback("pac_volume", function(_,_,val) + pac.volume = math.pow(math.Clamp(val,0,1),2) --adjust for the nonlinearity of volume + pac.ForceUpdateSoundVolumes() +end) + +pac.volume = math.pow(math.Clamp(GetConVar("pac_volume"):GetFloat(),0,1), 2) + +concommand.Add("pac_stopsound", function() + pac.StopSound() +end) + function pace.ClientSettingsMenu(self) if not IsValid(self) then return end self:Help(L"Performance"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable PAC", "pac_enable") self:NumSlider(L"Draw distance:", "pac_draw_distance", 0, 20000, 0) self:NumSlider(L"Max render time: ", "pac_max_render_time", 0, 100, 0) + + self:Help(L"Sounds"):SetFont("DermaDefaultBold") + self:NumSlider(L"Sounds volume", "pac_volume", 0, 1, 2) + self:Button(L"Stop sounds", "pac_stopsound") + + self:Help(L"Part limiters"):SetFont("DermaDefaultBold") + self:NumSlider(L"Sounds draw distance:", "pac_limit_sounds_draw_distance", 0, 20000, 0) + self:NumSlider(L"2D text draw distance:", "pac_limit_text_2d_draw_distance", 0, 20000, 0) + self:NumSlider(L"Sunbeams draw distance: ", "pac_limit_sunbeams_draw_distance", 0, 20000, 0) + self:NumSlider(L"Shake draw distance: ", "pac_limit_shake_draw_distance", 0, 20000, 0) + self:NumSlider(L"Shake max duration: ", "pac_limit_shake_duration", 0, 120, 0) + self:NumSlider(L"Shake max amplitude: ", "pac_limit_shake_amplitude", 0, 1000, 0) + end function pace.AdminSettingsMenu(self) if not LocalPlayer():IsAdmin() then return end if not IsValid(self) then return end self:Button("Open PAC3 settings menu (Admin)", "pace_settings") + if GetConVar("pac_sv_block_combat_features_on_next_restart"):GetInt() ~= 0 then + self:Help(L"Remember that you have to reinitialize combat parts if you want to enable those that were blocked."):SetFont("DermaDefaultBold") + self:Button("Reinitialize combat parts", "pac_sv_reinitialize_missing_combat_parts_remotely") + end self:Help(L"PAC3 outfits: general server policy"):SetFont("DermaDefaultBold") self:NumSlider(L"Server Draw distance:", "pac_sv_draw_distance", 0, 20000, 0) @@ -137,7 +173,7 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Only specifically allowed users can do pac3 combat actions", "pac_sv_combat_whitelisting") self:Help(""):SetFont("DermaDefaultBold")--spacers self:Help(""):SetFont("DermaDefaultBold") - + self:Help(L"Combat parts (more detailed settings in the full editor settings menu)"):SetFont("DermaDefaultBold") self:Help(L"Damage Zones"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable damage zones", "pac_sv_damage_zone") @@ -145,7 +181,7 @@ function pace.AdminSettingsMenu(self) self:NumSlider(L"Max radius", "pac_sv_damage_zone_max_radius", 0, 32767, 0) self:NumSlider(L"Max length", "pac_sv_damage_zone_max_length", 0, 32767, 0) self:CheckBox(L"Enable damage zone dissolve", "pac_sv_damage_zone_allow_dissolve") - + self:Help(L"Hitscan"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable hitscan part", "pac_sv_hitscan") self:NumSlider(L"Max damage", "pac_sv_hitscan_max_damage", 0, 268435455, 0) @@ -155,19 +191,19 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Enable lock part", "pac_sv_lock") self:CheckBox(L"Allow grab", "pac_sv_lock_grab") self:CheckBox(L"Allow teleport", "pac_sv_lock_teleport") - + self:Help(L"Force part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable force part", "pac_sv_force") self:NumSlider(L"Max amount", "pac_sv_force_max_amount", 0, 10000000, 0) self:NumSlider(L"Max radius", "pac_sv_force_max_radius", 0, 32767, 0) self:NumSlider(L"Max length", "pac_sv_force_max_length", 0, 32767, 0) - + self:Help(L"Force part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable health modifier", "pac_sv_health_modifier") self:CheckBox(L"Allow changing max health or armor", "pac_sv_health_modifier_allow_maxhp") self:NumSlider(L"Minimum combined damage scaling", "pac_sv_health_modifier_min_damagescaling", -10, 1, 2) self:CheckBox(L"Allow extra health bars", "pac_sv_health_modifier_extra_bars") - + self:Help(L"Projectile part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable physical projectiles", "pac_sv_projectiles") self:CheckBox(L"Enable custom collide meshes for physical projectiles", "pac_sv_projectile_allow_custom_collision_mesh") @@ -179,7 +215,7 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Allow playermovement", "pac_free_movement") self:CheckBox(L"Allow playermovement mass", "pac_player_movement_allow_mass") self:CheckBox(L"Allow physics damage scaling by mass", "pac_player_movement_physics_damage_scaling") - + end diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 0985c3ea8..ffd629126 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -121,6 +121,38 @@ local calcview_consents = {} local active_force_ids = {} local active_grabbed_ents = {} + +local friendly_NPC_preferences = {} +--we compare player's preference with the disposition's overall "friendliness". if relationship is more friendly than the preference, do not affect +local disposition_friendliness_level = { + [D_ER] = 0, --Error + [D_HT] = 0, --Hate + [D_FR] = 1, --Frightened / Fear + [D_LI] = 2, --Like + [D_NU] = 1, --Neutral +} + + +local function NPCDispositionAllowsIt(ply, ent) + + if not (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) or not ent.Disposition then return true end + + if not friendly_NPC_preferences[ply] then return true end + + local player_friendliness = friendly_NPC_preferences[ply] + local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)] + + if player_friendliness == 0 then --me agressive + return true --hurt anyone + elseif player_friendliness == 1 then --me not fully agressive + return relationship_friendliness <= 1 --hurt who is neutral or hostile + elseif player_friendliness == 2 then --me mostly friendly + return relationship_friendliness == 0 --hurt who is hostile + end + + return true +end + local damage_types = { generic = 0, --generic damage crush = 1, --caused by physics interaction @@ -697,6 +729,8 @@ if SERVER then if not (v:IsPlayer() or v:IsNPC() or string.find(v:GetClass(), "npc_")) and not tbl.PointEntities then ents_hits[i] = nil end if v.CPPICanDamage and not v:CPPICanDamage(ply) then ents_hits[i] = nil end --CPPI check on the player + if not NPCDispositionAllowsIt(ply, v) then ents_hits[i] = nil end + if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil else ent_count = ent_count + 1 @@ -1342,11 +1376,13 @@ if SERVER then --consent message from clients net.Receive("pac_signal_player_combat_consent", function(len,ply) + local friendly_NPC_preference = net.ReadUInt(2) -- GetConVar("pac_client_npc_exclusion_consent"):GetInt() local grab = net.ReadBool() -- GetConVar("pac_client_grab_consent"):GetBool() local damagezone = net.ReadBool() -- GetConVar("pac_client_damage_zone_consent"):GetBool() local calcview = net.ReadBool() -- GetConVar("pac_client_lock_camera_consent"):GetBool() local force = net.ReadBool() -- GetConVar("pac_client_force_consent"):GetBool() local hitscan = net.ReadBool() -- GetConVar("pac_client_hitscan_consent"):GetBool() + friendly_NPC_preferences[ply] = friendly_NPC_preference grab_consents[ply] = grab damage_zone_consents[ply] = damagezone calcview_consents[ply] = calcview @@ -2137,6 +2173,7 @@ if SERVER then bulletinfo.Callback = function(atk, trc, dmg) dmg:SetDamageType(bulletinfo.dmgtype) if trc.Hit and IsValid(trc.Entity) then + if not NPCDispositionAllowsIt(ply, trc.Entity) then return {effects = false, damage = false} end local distance = (trc.HitPos):Distance(trc.StartPos) local fraction = math.Clamp(1 - (1-bulletinfo.DamageFalloffFraction)*(distance / bulletinfo.DamageFalloffDistance),bulletinfo.DamageFalloffFraction,1) local ent = trc.Entity @@ -2299,12 +2336,7 @@ if SERVER then if not FINAL_BLOCKED_COMBAT_FEATURES["hitscan"] then DeclareHitscanReceivers() end if not FINAL_BLOCKED_COMBAT_FEATURES["health_modifier"] then DeclareHealthModifierReceivers() end - concommand.Add("pac_sv_combat_reinitialize_missing_receivers", function(ply) - if IsValid(ply) then - if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_sv_combat_reinitialize_missing_receivers", 3, 5, {"Player ", ply, " is spamming pac_sv_combat_reinitialize_missing_receivers!"} ) then - return - end - end + local function ReinitializeCombatReceivers() for name,blocked in pairs(FINAL_BLOCKED_COMBAT_FEATURES) do local update = blocked and (blocked == GetConVar("pac_sv_"..name):GetBool()) local new_bool = not (blocked or not GetConVar("pac_sv_"..name):GetBool()) @@ -2322,6 +2354,26 @@ if SERVER then net.Start("pac_inform_blocked_parts") net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES) net.Broadcast() + end + + concommand.Add("pac_sv_combat_reinitialize_missing_receivers", function(ply) + if IsValid(ply) then + if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_sv_combat_reinitialize_missing_receivers", 3, 5, {"Player ", ply, " is spamming pac_sv_combat_reinitialize_missing_receivers!"} ) then + return + end + ReinitializeCombatReceivers() + end + + end) + + util.AddNetworkString("pac_request_blocked_parts_reinitialization") + net.Receive("pac_request_blocked_parts_reinitialization", function(len, ply) + if IsValid(ply) then + if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_sv_combat_reinitialize_missing_receivers", 3, 5, {"Player ", ply, " is spamming pac_sv_combat_reinitialize_missing_receivers!"} ) then + return + end + ReinitializeCombatReceivers() + end end) net.Receive("pac_request_blocked_parts", function(len, ply) @@ -2329,9 +2381,24 @@ if SERVER then net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES) net.Send(ply) end) + end if CLIENT then + killicon.Add( "pac_bullet_emitter", "icon16/user_gray.png", Color(255,255,255) ) + + concommand.Add("pac_sv_reinitialize_missing_combat_parts_remotely", function(ply) + if IsValid(ply) then + if not ply:IsAdmin() then + return + end + net.Start("pac_request_blocked_parts_reinitialization") + net.SendToServer() + end + end) + + + CreateConVar("pac_client_npc_exclusion_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to protect some npcs based on their disposition or faction. So far it only works with Dispositions.0 = ignore factions and relationships and target any NPC\n1 = protect friendlies\n2 = protect friendlies and neutrals") CreateConVar("pac_client_grab_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part") CreateConVar("pac_client_lock_camera_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to having lock parts override your view") CreateConVar("pac_client_damage_zone_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part") @@ -2374,6 +2441,7 @@ if CLIENT then local function SendConsents() net.Start("pac_signal_player_combat_consent") + net.WriteUInt(GetConVar("pac_client_npc_exclusion_consent"):GetInt(),2) net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool()) net.WriteBool(GetConVar("pac_client_damage_zone_consent"):GetBool()) net.WriteBool(GetConVar("pac_client_lock_camera_consent"):GetBool()) @@ -2414,7 +2482,7 @@ if CLIENT then pac.Blocked_Combat_Parts = net.ReadTable() end) - local consent_cvars = {"pac_client_grab_consent", "pac_client_lock_camera_consent", "pac_client_damage_zone_consent", "pac_client_force_consent", "pac_client_hitscan_consent"} + local consent_cvars = {"pac_client_npc_exclusion_consent", "pac_client_grab_consent", "pac_client_lock_camera_consent", "pac_client_damage_zone_consent", "pac_client_force_consent", "pac_client_hitscan_consent"} for _,cmd in ipairs(consent_cvars) do cvars.AddChangeCallback(cmd, SendConsents) end diff --git a/lua/pac3/libraries/webaudio.lua b/lua/pac3/libraries/webaudio.lua index 3dc9f1d0c..bbb50f773 100644 --- a/lua/pac3/libraries/webaudio.lua +++ b/lua/pac3/libraries/webaudio.lua @@ -783,6 +783,8 @@ do end function META:SetVolume(volumeFraction) + pac.volume = pac.volume or 1 + volumeFraction = volumeFraction * pac.volume if self.Volume == volumeFraction then return self end self.Volume = volumeFraction From 16284b25943934e0d560590ca39fa9a360754d92 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 21 Jan 2024 14:47:51 -0500 Subject: [PATCH 163/300] replace D enums with their numbers apparently they don't exist before pac loads when we need them --- lua/pac3/extra/shared/net_combat.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index ffd629126..2adb7b219 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -112,7 +112,6 @@ local pre_excluded_ent_classes = { } - local grab_consents = {} local damage_zone_consents = {} local force_consents = {} @@ -125,11 +124,11 @@ local active_grabbed_ents = {} local friendly_NPC_preferences = {} --we compare player's preference with the disposition's overall "friendliness". if relationship is more friendly than the preference, do not affect local disposition_friendliness_level = { - [D_ER] = 0, --Error - [D_HT] = 0, --Hate - [D_FR] = 1, --Frightened / Fear - [D_LI] = 2, --Like - [D_NU] = 1, --Neutral + [0] = 0, --D_ER Error + [1] = 0, --D_HT Hate + [2] = 1, --D_FR Frightened / Fear + [3] = 2, --D_LI Like + [4] = 1, --D_NU Neutral } From e4bacaa2ad5ac478e83c4ebe0884bc45c2b947ab Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 27 Jan 2024 18:32:14 -0500 Subject: [PATCH 164/300] proxy: expression slots, text preview, ent_color functions new expression slots: -expression on hide will send an expression once at OnHide, running the Think one last time. for lighter toggled setups, but not meant for "moving math" -extras will compute at the start of the proxy's Think, then the corresponding var1(), var2() / extra1() etc. which are up to 5, can take the result so it's written more compactly in the main expression. optional argument uid / name can search and link to another proxy's extra expression to read it preview output is a one-click thing to get visual feedback in the world / view panel. easier than setting up a text part. ent_color series (rgba): should work with colorable parts and owner entity --- lua/pac3/core/client/parts/proxy.lua | 223 +++++++++++++++++++++++++-- 1 file changed, 209 insertions(+), 14 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index aa604f5c3..7a37066c0 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -49,7 +49,15 @@ BUILDER:StartStorableVars() BUILDER:GetSet("ZeroEyePitch", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will force the angle to be horizon level."}) BUILDER:GetSet("ResetVelocitiesOnHide", true, {description = "Because velocity calculators use smoothing that makes the output converge toward a crude rolling average, it might matter whether you want to get a clean slate readout.\n(VelocityRoughness is how close to the snapshots it will be. Lower means smoother but delayed. Higher means less smoothing but it might overshoot and be inaccurate because of frame time works and varies)"}) BUILDER:GetSet("VelocityRoughness", 10) - + BUILDER:GetSet("PreviewOutput", false, {description = "Previews the proxy's output (for yourself) next to the nearest owner entity in the game"}) + + BUILDER:SetPropertyGroup("extra expressions") + BUILDER:GetSet("ExpressionOnHide", "") + BUILDER:GetSet("Extra1", "") + BUILDER:GetSet("Extra2", "") + BUILDER:GetSet("Extra3", "") + BUILDER:GetSet("Extra4", "") + BUILDER:GetSet("Extra5", "") BUILDER:EndStorableVars() -- redirect @@ -409,6 +417,25 @@ PART.Inputs.event_alternative = function(self, uid1, num1, num2) return 0 end +for i=1,5,1 do + PART.Inputs["var" .. i] = function(self, uid1) + if not uid1 then + return self["feedback_extra" .. i] + elseif self["extra_referred_part"..i] then --a thing to skip part searching when we found the part + return self["extra_referred_part"..i]["feedback_extra" .. i] + else + local owner = self:GetPlayerOwner() + local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) + if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end + if IsValid(PartA) and PartA.ClassName == "proxy" then + self["extra_referred_part"..i] = PartA + end + end + return 0 + end + PART.Inputs["extra" .. i] = PART.Inputs["var" .. i] --alias +end + PART.Inputs.number_operator_alternative = function(self, comp1, op, comp2, num1, num2) if not (comp1 and op and comp2 and num1 and num2) then return -1 end if not (isnumber(comp1) and isnumber(comp2) and isnumber(num1) and isnumber(num2)) then return -1 end @@ -850,7 +877,7 @@ do -- health and armor end end -do -- weapon and player color +do -- weapon, player and owner entity/part color local Color = Color local function get_color(self, get, field) local color = field and get(self)[field] or get(self) @@ -892,6 +919,41 @@ do -- weapon and player color PART.Inputs.weapon_color_g = function(self) return get_color(self, get_weapon_color, "g") end PART.Inputs.weapon_color_b = function(self) return get_color(self, get_weapon_color, "b") end end + + do + local function reformat_color(col, proper_in, proper_out) + local multiplier = 1 + if not proper_in then multiplier = multiplier / 255 end + if not proper_out then multiplier = multiplier * 255 end + col.r = math.Clamp(col.r * multiplier,0,255) + col.g = math.Clamp(col.g * multiplier,0,255) + col.b = math.Clamp(col.b * multiplier,0,255) + col.a = math.Clamp(col.a * multiplier,0,255) + return col + end + local function get_entity_color(self, field) + local owner = get_owner(self) + local part = self:GetTarget() + if not self.RootOwner then --we can get the color from a pac part + owner = self:GetParent() + if not owner.GetColor then --or from the root owner entity... + owner = get_owner(self) + end + end + local color = owner:GetColor() + if isvector(color) then --pac parts color are vector. reformat to a color first + color = Color(color[1], color[2], color[3], owner.GetAlpha and owner:GetAlpha() or 255) + end + reformat_color(color, owner.ProperColorRange, part.ProperColorRange) --cram or un-cram the 255 range into 1 or vice versa + if field then return color[field] else return color["r"], color["g"], color["b"] end + end + + PART.Inputs.ent_color = function(self) return get_entity_color(self) end + PART.Inputs.ent_color_r = function(self) return get_entity_color(self, "r") end + PART.Inputs.ent_color_g = function(self) return get_entity_color(self, "g") end + PART.Inputs.ent_color_b = function(self) return get_entity_color(self, "b") end + PART.Inputs.ent_color_a = function(self) return get_entity_color(self, "a") end + end end do -- ammo @@ -1124,11 +1186,20 @@ local allowed = { boolean = true, } -function PART:SetExpression(str) - self.Expression = str - self.ExpressionFunc = nil - self.valid_parts_in_expression = {} - self.invalid_parts_in_expression = {} +function PART:SetExpression(str, slot) + if not slot then --that's the default expression + self.Expression = str + self.ExpressionFunc = nil + self.valid_parts_in_expression = {} + self.invalid_parts_in_expression = {} + elseif slot == 0 then --that's the expression on hide + self.ExpressionOnHide = str + self.ExpressionOnHideFunc = nil + elseif slot <= 5 then --that's one of the custom variables + self["CustomVariable" .. slot] = str + self["Extra" .. slot .. "Func"] = nil + self.has_extras = true + end if str and str ~= "" then local lib = {} @@ -1139,17 +1210,69 @@ function PART:SetExpression(str) local ok, res = pac.CompileExpression(str, lib) if ok then - self.ExpressionFunc = res - self.ExpressionError = nil - self:SetError() + if not slot then --that's the default expression + self.ExpressionFunc = res + self.ExpressionError = nil + self:SetError() + elseif slot == 0 then --that's the expression on hide + self.ExpressionOnHideFunc = res + self.ExpressionOnHideError = nil + self:SetError() + elseif slot <= 5 then --that's one of the extra custom variables + self["Extra" .. slot .. "Func"] = res + self["Extra" .. slot .. "Error"] = nil + self:SetError() + end + else - self.ExpressionFunc = true - self.ExpressionError = res - self:SetError(res) + if not slot then --that's the default expression + self.ExpressionFunc = true + self.ExpressionError = res + self:SetError(res) + elseif slot == 0 then --that's the expression on hide + self.ExpressionOnHideFunc = true + self.ExpressionOnHideError = res + self:SetError(res) + elseif slot <= 5 then --that's one of the extra custom variables + self["Extra" .. slot .. "Func"] = true + self["Extra" .. slot .. "Error"] = res + self:SetError(res) + end + end end end +function PART:SetExpressionOnHide(str) + self.ExpressionOnHide = str + self:SetExpression(str, 0) +end + +function PART:SetExtra1(str) + self.Extra1 = str + self:SetExpression(str, 1) +end + +function PART:SetExtra2(str) + self.Extra2 = str + self:SetExpression(str, 2) +end + +function PART:SetExtra3(str) + self.Extra3 = str + self:SetExpression(str, 3) +end + +function PART:SetExtra4(str) + self.Extra4 = str + self:SetExpression(str, 4) +end + +function PART:SetExtra5(str) + self.Extra5 = str + self:SetExpression(str, 5) +end + function PART:OnHide() self.time = nil self.rand = nil @@ -1175,6 +1298,10 @@ function PART:OnHide() part.proxy_hide = nil end end + + if self.ExpressionOnHide ~= "" and self.ExpressionOnHideFunc then + self:OnThink(true) + end end function PART:OnShow() @@ -1256,7 +1383,55 @@ function PART:RunExpression(ExpressionFunc) return pcall(ExpressionFunc) end -function PART:OnThink() +local function get_preview_draw_string(self) + local str = self.debug_var or "" + local name = self.Name + if self.Name == "" then + name = self:GetTarget().Name + if name == "" then + name = self:GetTarget():GetNiceName() + end + end + str = name .. "." .. self.VariableName .. " = " .. str + return str +end + +local function remove_line_from_previews(self) + text_preview_ent_rows = {} + if text_preview_ent_rows[self:GetOwner()] then + text_preview_ent_rows[self:GetOwner()][self] = {} + end + pace.flush_proxy_previews = true + timer.Simple(0.1, function() pace.flush_proxy_previews = false end) +end + + +local text_preview_ent_rows = {} + +local function draw_proxy_text(self, str) + if pace.flush_proxy_previews then text_preview_ent_rows = {} return end + local ent = self:GetOwner() + local pos = ent:GetPos() + local tbl = pos:ToScreen() + text_preview_ent_rows[ent] = text_preview_ent_rows[ent] or {} + if not text_preview_ent_rows[ent][self] then + text_preview_ent_rows[ent][self] = table.Count(text_preview_ent_rows[ent]) + end + tbl.x = tbl.x + 20 + tbl.y = tbl.y + 10 * text_preview_ent_rows[ent][self] + draw.DrawText(get_preview_draw_string(self), "ChatFont", tbl.x, tbl.y) +end + +function PART:OnWorn() + remove_line_from_previews(self) --just for a quick refresh +end + +function PART:OnRemove() + remove_line_from_previews(self) + pac.RemoveHook("HUDPaint", "proxy" .. self.UniqueID) +end + +function PART:OnThink(to_hide) local part = self:GetTarget() if not part:IsValid() then return end if part.ClassName == 'woohoo' then --why a part hardcode exclusion?? @@ -1277,7 +1452,19 @@ function PART:OnThink() self:CalcVelocity() + if self.has_extras then --pre-calculate the extra expressions if needed + for i=1,5,1 do + if self["Extra" .. i] ~= "" then + local ok, x,y,z = self:RunExpression(self["Extra" .. i .. "Func"]) + if ok then + self["feedback_extra" .. i] = x + end + end + end + end + local ExpressionFunc = self.ExpressionFunc + if to_hide then ExpressionFunc = self.ExpressionOnHideFunc end if not ExpressionFunc then self:SetExpression(self.Expression) @@ -1406,6 +1593,14 @@ function PART:OnThink() end self:SetError(error_msg) end + if self:GetPlayerOwner() == pac.LocalPlayer then + if self.PreviewOutput then + pac.AddHook("HUDPaint", "proxy" .. self.UniqueID, function() draw_proxy_text(self, str) end) + else + pac.RemoveHook("HUDPaint", "proxy" .. self.UniqueID) + end + end + end BUILDER:Register() From 0e762038acaf70889f0fe8a4117d0d2260552cb0 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 27 Jan 2024 18:36:06 -0500 Subject: [PATCH 165/300] net_combat: some server stuff moved to serverside --- lua/pac3/extra/shared/net_combat.lua | 279 +++++++++++++-------------- 1 file changed, 139 insertions(+), 140 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 2adb7b219..926717907 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -58,145 +58,6 @@ cvars.AddChangeCallback("pac_sv_combat_distance_enforced", function() ENFORCE_DI local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force...).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ") local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.") -local damageable_point_ent_classes = { - ["predicted_viewmodel"] = false, - ["prop_physics"] = true, - ["weapon_striderbuster"] = true, - ["item_item_crate"] = true, - ["func_breakable_surf"] = true, - ["func_breakable"] = true, - ["physics_cannister"] = true -} - -local physics_point_ent_classes = { - ["prop_physics"] = true, - ["prop_physics_multiplayer"] = true, - ["prop_ragdoll"] = true, - ["weapon_striderbuster"] = true, - ["item_item_crate"] = true, - ["func_breakable_surf"] = true, - ["func_breakable"] = true, - ["physics_cannister"] = true -} - -local contraption_classes = { - ["prop_physics"] = true, -} - -local pre_excluded_ent_classes = { - ["info_player_start"] = true, - ["aoc_spawnpoint"] = true, - ["info_player_teamspawn"] = true, - ["env_tonemap_controller"] = true, - ["env_fog_controller"] = true, - ["env_skypaint"] = true, - ["shadow_control"] = true, - ["env_sun"] = true, - ["predicted_viewmodel"] = true, - ["physgun_beam"] = true, - ["ambient_generic"] = true, - ["trigger_once"] = true, - ["trigger_multiple"] = true, - ["trigger_hurt"] = true, - ["info_ladder_dismount"] = true, - ["info_particle_system"] = true, - ["env_sprite"] = true, - ["env_fire"] = true, - ["env_soundscape"] = true, - ["env_smokestack"] = true, - ["light"] = true, - ["move_rope"] = true, - ["keyframe_rope"] = true, - ["env_soundscape_proxy"] = true, - ["gmod_hands"] = true, -} - - -local grab_consents = {} -local damage_zone_consents = {} -local force_consents = {} -local hitscan_consents = {} -local calcview_consents = {} -local active_force_ids = {} -local active_grabbed_ents = {} - - -local friendly_NPC_preferences = {} ---we compare player's preference with the disposition's overall "friendliness". if relationship is more friendly than the preference, do not affect -local disposition_friendliness_level = { - [0] = 0, --D_ER Error - [1] = 0, --D_HT Hate - [2] = 1, --D_FR Frightened / Fear - [3] = 2, --D_LI Like - [4] = 1, --D_NU Neutral -} - - -local function NPCDispositionAllowsIt(ply, ent) - - if not (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) or not ent.Disposition then return true end - - if not friendly_NPC_preferences[ply] then return true end - - local player_friendliness = friendly_NPC_preferences[ply] - local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)] - - if player_friendliness == 0 then --me agressive - return true --hurt anyone - elseif player_friendliness == 1 then --me not fully agressive - return relationship_friendliness <= 1 --hurt who is neutral or hostile - elseif player_friendliness == 2 then --me mostly friendly - return relationship_friendliness == 0 --hurt who is hostile - end - - return true -end - -local damage_types = { - generic = 0, --generic damage - crush = 1, --caused by physics interaction - bullet = 2, --bullet damage - slash = 4, --sharp objects, such as manhacks or other npcs attacks - burn = 8, --damage from fire - vehicle = 16, --hit by a vehicle - fall = 32, --fall damage - blast = 64, --explosion damage - club = 128, --crowbar damage - shock = 256, --electrical damage, shows smoke at the damage position - sonic = 512, --sonic damage,used by the gargantua and houndeye npcs - energybeam = 1024, --laser - nevergib = 4096, --don't create gibs - alwaysgib = 8192, --always create gibs - drown = 16384, --drown damage - paralyze = 32768, --same as dmg_poison - nervegas = 65536, --neurotoxin damage - poison = 131072, --poison damage - acid = 1048576, -- - airboat = 33554432, --airboat gun damage - blast_surface = 134217728, --this won't hurt the player underwater - buckshot = 536870912, --the pellets fired from a shotgun - direct = 268435456, -- - dissolve = 67108864, --forces the entity to dissolve on death - drownrecover = 524288, --damage applied to the player to restore health after drowning - physgun = 8388608, --damage done by the gravity gun - plasma = 16777216, -- - prevent_physics_force = 2048, -- - radiation = 262144, --radiation - removenoragdoll = 4194304, --don't create a ragdoll on death - slowburn = 2097152, -- - - fire = -1, -- ent:Ignite(5) - - -- env_entity_dissolver - dissolve_energy = 0, - dissolve_heavy_electrical = 1, - dissolve_light_electrical = 2, - dissolve_core_effect = 3, - - heal = -1, - armor = -1, -} - do --define a basic class for the bullet emitters local ENT = {} ENT.Type = "anim" @@ -206,7 +67,145 @@ do --define a basic class for the bullet emitters end if SERVER then - + local damageable_point_ent_classes = { + ["predicted_viewmodel"] = false, + ["prop_physics"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true + } + + local physics_point_ent_classes = { + ["prop_physics"] = true, + ["prop_physics_multiplayer"] = true, + ["prop_ragdoll"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true + } + + local contraption_classes = { + ["prop_physics"] = true, + } + + local pre_excluded_ent_classes = { + ["info_player_start"] = true, + ["aoc_spawnpoint"] = true, + ["info_player_teamspawn"] = true, + ["env_tonemap_controller"] = true, + ["env_fog_controller"] = true, + ["env_skypaint"] = true, + ["shadow_control"] = true, + ["env_sun"] = true, + ["predicted_viewmodel"] = true, + ["physgun_beam"] = true, + ["ambient_generic"] = true, + ["trigger_once"] = true, + ["trigger_multiple"] = true, + ["trigger_hurt"] = true, + ["info_ladder_dismount"] = true, + ["info_particle_system"] = true, + ["env_sprite"] = true, + ["env_fire"] = true, + ["env_soundscape"] = true, + ["env_smokestack"] = true, + ["light"] = true, + ["move_rope"] = true, + ["keyframe_rope"] = true, + ["env_soundscape_proxy"] = true, + ["gmod_hands"] = true, + } + + + local grab_consents = {} + local damage_zone_consents = {} + local force_consents = {} + local hitscan_consents = {} + local calcview_consents = {} + local active_force_ids = {} + local active_grabbed_ents = {} + + + local friendly_NPC_preferences = {} + --we compare player's preference with the disposition's overall "friendliness". if relationship is more friendly than the preference, do not affect + local disposition_friendliness_level = { + [0] = 0, --D_ER Error + [1] = 0, --D_HT Hate + [2] = 1, --D_FR Frightened / Fear + [3] = 2, --D_LI Like + [4] = 1, --D_NU Neutral + } + + + local function NPCDispositionAllowsIt(ply, ent) + + if not (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) or not ent.Disposition then return true end + + if not friendly_NPC_preferences[ply] then return true end + + local player_friendliness = friendly_NPC_preferences[ply] + local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)] + + if player_friendliness == 0 then --me agressive + return true --hurt anyone + elseif player_friendliness == 1 then --me not fully agressive + return relationship_friendliness <= 1 --hurt who is neutral or hostile + elseif player_friendliness == 2 then --me mostly friendly + return relationship_friendliness == 0 --hurt who is hostile + end + + return true + end + + local damage_types = { + generic = 0, --generic damage + crush = 1, --caused by physics interaction + bullet = 2, --bullet damage + slash = 4, --sharp objects, such as manhacks or other npcs attacks + burn = 8, --damage from fire + vehicle = 16, --hit by a vehicle + fall = 32, --fall damage + blast = 64, --explosion damage + club = 128, --crowbar damage + shock = 256, --electrical damage, shows smoke at the damage position + sonic = 512, --sonic damage,used by the gargantua and houndeye npcs + energybeam = 1024, --laser + nevergib = 4096, --don't create gibs + alwaysgib = 8192, --always create gibs + drown = 16384, --drown damage + paralyze = 32768, --same as dmg_poison + nervegas = 65536, --neurotoxin damage + poison = 131072, --poison damage + acid = 1048576, -- + airboat = 33554432, --airboat gun damage + blast_surface = 134217728, --this won't hurt the player underwater + buckshot = 536870912, --the pellets fired from a shotgun + direct = 268435456, -- + dissolve = 67108864, --forces the entity to dissolve on death + drownrecover = 524288, --damage applied to the player to restore health after drowning + physgun = 8388608, --damage done by the gravity gun + plasma = 16777216, -- + prevent_physics_force = 2048, -- + radiation = 262144, --radiation + removenoragdoll = 4194304, --don't create a ragdoll on death + slowburn = 2097152, -- + + fire = -1, -- ent:Ignite(5) + + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = -1, + armor = -1, + } + local function CountNetMessage(ply) local stime = SysTime() local ms_basis = enforce_netrate:GetInt()/1000 From fb2d3c7d0863d2739c78cdb26f17c7338556cb05 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 5 Feb 2024 21:08:40 -0500 Subject: [PATCH 166/300] improve hitscan precision use uints instead of vector, but it'll cost some more as the tradeoff --- lua/pac3/core/client/parts/hitscan.lua | 3 ++- lua/pac3/extra/shared/net_combat.lua | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index bbfd4ebf9..8c68ec4c3 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -210,7 +210,8 @@ function PART:SendNetMessage() net.WriteAngle(self:GetWorldAngles()) net.WriteUInt(damage_ids[self.DamageType] or 0,7) - net.WriteVector(Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0)) + net.WriteUInt(math.abs(math.Clamp(10000 * self.SpreadX*self.Spread, 0, 1048575)), 20) + net.WriteUInt(math.abs(math.Clamp(10000 * self.SpreadY*self.Spread, 0, 1048575)), 20) net.WriteUInt(self.Damage, 28) net.WriteUInt(self.TracerSparseness, 8) net.WriteUInt(self.Force, 16) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 926717907..7c38d97db 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -2139,7 +2139,9 @@ if SERVER then bulletinfo.dmgtype_str = table.KeyFromValue(damage_ids, net.ReadUInt(7)) bulletinfo.dmgtype = damage_types[bulletinfo.dmgtype_str] - bulletinfo.Spread = net.ReadVector() + local spreadx = net.ReadUInt(20) / 10000 + local spready = net.ReadUInt(20) / 10000 + bulletinfo.Spread = Vector(spreadx, spready, 0) bulletinfo.Damage = net.ReadUInt(28) bulletinfo.Tracer = net.ReadUInt(8) bulletinfo.Force = net.ReadUInt(16) From b0a135cc295495ab90a7744794244ba37bb8217c Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 10 Feb 2024 14:11:43 -0500 Subject: [PATCH 167/300] more damagezone options like the NPC preferences settings, a filter for each "disposition friendliness" more developed "DoNotKill" options DoNotKill will now stop healing at the critical point, the same way that it damaged no further than the critical point (converge to critical health) ReverseDoNotKill reverses the rule into a health-based filter : don't heal if health is below, don't damage if health is above (diverge from critical health) skip some hitmarker code when none are assigned --- lua/pac3/core/client/parts/damage_zone.lua | 46 +++++---- lua/pac3/extra/shared/net_combat.lua | 111 +++++++++++++++------ 2 files changed, 111 insertions(+), 46 deletions(-) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 9c9683e6c..156961d32 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -28,6 +28,9 @@ BUILDER:StartStorableVars() :GetSet("Players",true) :GetSet("NPC",true) :GetSet("PointEntities",true, {description = "Other source engine entities such as item_item_crate and prop_physics"}) + :GetSet("Friendlies", true, {description = "friendly NPCs can be targeted"}) + :GetSet("Neutrals", true, {description = "neutral NPCs can be targeted"}) + :GetSet("Hostiles", true, {description = "hostile NPCs can be targeted"}) :SetPropertyGroup("Shape and Sampling") :GetSet("Radius", 20, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) @@ -113,7 +116,8 @@ BUILDER:StartStorableVars() heal = -1, armor = -1, }}) - :GetSet("DoNotKill",false, {description = "Will only damage to as low as critical health"}) + :GetSet("DoNotKill",false, {description = "Only damage to as low as critical health;\nOnly heal to as high as critical health\nIn other words, converge to the critical health"}) + :GetSet("ReverseDoNotKill",false, {description = "Heal only if health is above critical health;\nDamage only if health is below critical health\nIn other words, move away from the critical health"}) :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("HitOutcome") @@ -519,6 +523,9 @@ function PART:SendNetMessage() net.WriteBool(self.NPC) net.WriteBool(self.Players) net.WriteBool(self.PointEntities) + net.WriteBool(self.Friendlies) + net.WriteBool(self.Neutrals) + net.WriteBool(self.Hostiles) net.WriteUInt(hitbox_ids[self.HitboxMode] or 1,5) net.WriteUInt(damage_ids[self.DamageType] or 0,7) net.WriteInt(self.Detail,6) @@ -529,6 +536,7 @@ function PART:SendNetMessage() net.WriteInt(math.floor(math.Clamp(8*self.DamageFalloffPower,-512, 511)), 12) net.WriteBool(self.Bullet) net.WriteBool(self.DoNotKill) + net.WriteBool(self.ReverseDoNotKill) net.WriteUInt(self.CriticalHealth, 16) net.WriteBool(self.RemoveNPCWeaponsOnKill) net.SendToServer() @@ -672,15 +680,17 @@ function PART:OnShow() self.HitSoundPart:PlaySound() end end - for ent,_ in pairs(ents_hit) do - if IsValid(ent) then - local ang = (ent:GetPos() - pos):Angle() - if ents_kill[ent] then - if self.AllowOverlappingHitMarkers then + if self.HitMarkerPart then + for ent,_ in pairs(ents_hit) do + if IsValid(ent) then + local ang = (ent:GetPos() - pos):Angle() + if ents_kill[ent] then + if self.AllowOverlappingHitMarkers then + part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) + end + else part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) end - else - part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) end end end @@ -690,18 +700,20 @@ function PART:OnShow() if ValidSound(self.KillSoundPart) then self.KillSoundPart:PlaySound() end - - for ent,_ in pairs(ents_kill) do - if IsValid(ent) then - local ang = (ent:GetPos() - pos):Angle() - part_setup_runtimes = part_setup_runtimes + (spawn(self.KillMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.KillMarkerLifetime, owner) or 0) + if self.KillMarkerPart then + for ent,_ in pairs(ents_kill) do + if IsValid(ent) then + local ang = (ent:GetPos() - pos):Angle() + part_setup_runtimes = part_setup_runtimes + (spawn(self.KillMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.KillMarkerLifetime, owner) or 0) + end end end end - if owner.hitparts then - self:SetInfo(table.Count(owner.hitparts) .. " hitmarkers in slot") + if self.HitMarkerPart or self.KillMarkerPart then + if owner.hitparts then + self:SetInfo(table.Count(owner.hitparts) .. " hitmarkers in slot") + end end - end) end @@ -1030,4 +1042,4 @@ function PART:SetDamage(val) end -BUILDER:Register() +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 7c38d97db..f7120432a 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -142,14 +142,14 @@ if SERVER then local function NPCDispositionAllowsIt(ply, ent) - + if not (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) or not ent.Disposition then return true end - + if not friendly_NPC_preferences[ply] then return true end - + local player_friendliness = friendly_NPC_preferences[ply] local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)] - + if player_friendliness == 0 then --me agressive return true --hurt anyone elseif player_friendliness == 1 then --me not fully agressive @@ -157,9 +157,22 @@ if SERVER then elseif player_friendliness == 2 then --me mostly friendly return relationship_friendliness == 0 --hurt who is hostile end - + return true end + + local function NPCDispositionIsFilteredOut(ply, ent, friendly, neutral, hostile) + if not (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) or not ent.Disposition then return false end + local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)] + + if relationship_friendliness == 0 then --it hostile + return not hostile + elseif relationship_friendliness == 1 then --it neutral + return not neutral + elseif relationship_friendliness == 2 then --it friendly + return not friendly + end + end local damage_types = { generic = 0, --generic damage @@ -728,6 +741,7 @@ if SERVER then if v.CPPICanDamage and not v:CPPICanDamage(ply) then ents_hits[i] = nil end --CPPI check on the player if not NPCDispositionAllowsIt(ply, v) then ents_hits[i] = nil end + if NPCDispositionIsFilteredOut(ply,v, tbl.FilterFriendlies, tbl.FilterNeutrals, tbl.FilterHostiles) then ents_hits[i] = nil end if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil else @@ -923,10 +937,33 @@ if SERVER then end if tbl.DamageType == "heal" then - - ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) + if tbl.ReverseDoNotKill then --don't heal if health is below critical + if ent:Health() > tbl.CriticalHealth then --default behavior + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) + end --else do nothing + else + if tbl.DoNotKill then --stop healing at the critical health + if ent:Health() < tbl.CriticalHealth then + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.min(tbl.CriticalHealth, ent:GetMaxHealth()))) + end --else do nothing, we're already above critical + else + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) + end + end elseif tbl.DamageType == "armor" then - ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) + if tbl.ReverseDoNotKill then --don't heal if armor is below critical + if ent:Armor() > tbl.CriticalHealth then --default behavior + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) + end --else do nothing + else + if tbl.DoNotKill then --stop healing at the critical health + if ent:Armor() < tbl.CriticalHealth then + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.min(tbl.CriticalHealth, ent:GetMaxArmor()))) + end --else do nothing, we're already above critical + else + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) + end + end else --only "living" entities can be killed, and we checked generic entities with a ghost 0 health previously @@ -963,32 +1000,44 @@ if SERVER then end end - --leave at a critical health - if tbl.DoNotKill then - local dmg_info2 = DamageInfo() - - dmg_info2:SetDamagePosition(ent:NearestPoint(pos)) - dmg_info2:SetReportedPosition(pos) - dmg_info2:SetDamage( math.min(ent:Health() - tbl.CriticalHealth, tbl.Damage)) - dmg_info2:IsBulletDamage(tbl.Bullet) - dmg_info2:SetDamageForce(Vector(0,0,0)) + if tbl.ReverseDoNotKill then + --don't damage if health is above critical + if ent:Health() < tbl.CriticalHealth then + if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) then + dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType]) + end + dmg_info:SetDamagePosition(ent:NearestPoint(pos)) + dmg_info:SetReportedPosition(pos) + ent:TakeDamageInfo(dmg_info) + max_dmg = math.max(max_dmg, dmg_info:GetDamage()) + end + else + --leave at a critical health + if tbl.DoNotKill then + local dmg_info2 = DamageInfo() - dmg_info2:SetAttacker(attacker) + dmg_info2:SetDamagePosition(ent:NearestPoint(pos)) + dmg_info2:SetReportedPosition(pos) + dmg_info2:SetDamage( math.min(ent:Health() - tbl.CriticalHealth, tbl.Damage)) + dmg_info2:IsBulletDamage(tbl.Bullet) + dmg_info2:SetDamageForce(Vector(0,0,0)) - dmg_info2:SetInflictor(inflictor) + dmg_info2:SetAttacker(attacker) - ent:TakeDamageInfo(dmg_info2) - max_dmg = math.max(max_dmg, dmg_info2:GetDamage()) + dmg_info2:SetInflictor(inflictor) - --finally we reached the normal damage event! - else - if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) then - dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType]) + ent:TakeDamageInfo(dmg_info2) + max_dmg = math.max(max_dmg, dmg_info2:GetDamage()) + --finally we reached the normal damage event! + else + if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) then + dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType]) + end + dmg_info:SetDamagePosition(ent:NearestPoint(pos)) + dmg_info:SetReportedPosition(pos) + ent:TakeDamageInfo(dmg_info) + max_dmg = math.max(max_dmg, dmg_info:GetDamage()) end - dmg_info:SetDamagePosition(ent:NearestPoint(pos)) - dmg_info:SetReportedPosition(pos) - ent:TakeDamageInfo(dmg_info) - max_dmg = math.max(max_dmg, dmg_info:GetDamage()) end end @@ -1637,6 +1686,9 @@ if SERVER then tbl.NPC = net.ReadBool() tbl.Players = net.ReadBool() tbl.PointEntities = net.ReadBool() + tbl.FilterFriendlies = net.ReadBool() + tbl.FilterNeutrals = net.ReadBool() + tbl.FilterHostiles = net.ReadBool() tbl.HitboxMode = table.KeyFromValue(hitbox_ids, net.ReadUInt(5)) tbl.DamageType = table.KeyFromValue(damage_ids, net.ReadUInt(7)) @@ -1649,6 +1701,7 @@ if SERVER then tbl.DamageFalloffPower = net.ReadInt(12) / 8 tbl.Bullet = net.ReadBool() tbl.DoNotKill = net.ReadBool() + tbl.ReverseDoNotKill = net.ReadBool() tbl.CriticalHealth = net.ReadUInt(16) tbl.RemoveNPCWeaponsOnKill = net.ReadBool() From d1e68b3593a2e515037195c79efcd262b139af2a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 17 Feb 2024 17:40:52 -0500 Subject: [PATCH 168/300] text part chat support ChatTyping updates when typing (outgoing networking is stopped when the part is removed and no more text parts requiring ChatTyping are present) ChatSent reuses say event's existing data --- lua/pac3/core/client/parts/text.lua | 47 +++++++++++++++++++++++++-- lua/pac3/core/server/net_messages.lua | 10 ++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index 2dc16fde7..865113758 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -15,6 +15,13 @@ local BUILDER, PART = pac.PartTemplate("base_drawable") local draw_distance = CreateClientConVar("pac_limit_text_2d_draw_distance", "1000", true, false, "How far to see other players' text parts using 2D modes. They will start fading out 200 units before this distance.") + +net.Receive("pac_chat_typing_mirror_broadcast", function(len) + local text = net.ReadString() + local ply = net.ReadEntity() + ply.pac_mirrored_chat_text = text +end) + local default_fonts = { "BudgetLabel", "CenterPrintText", @@ -135,7 +142,9 @@ BUILDER:StartStorableVars() ["Ground Entity Class"] = "GroundEntityClass", ["Players"] = "Players", ["Max Players"] = "MaxPlayers", - ["Difficulty"] = "GameDifficulty" + ["Difficulty"] = "GameDifficulty", + ["Chat Being Typed"] = "ChatTyping", + ["Last Chat Sent"] = "ChatSent", }}) :GetSet("DynamicTextValue", 0) :GetSet("RoundingPosition", 2, {editor_onchange = function(self, num) @@ -368,6 +377,27 @@ function PART:OnDraw() else DisplayText = "not driving" end elseif self.TextOverride == "Proxy" then DisplayText = ""..math.Round(self.DynamicTextValue,self.RoundingPosition) + elseif self.TextOverride == "ChatTyping" then + if self:GetPlayerOwner() == pac.LocalPlayer and not pac.broadcast_chat_typing then + pac.AddHook("ChatTextChanged", "broadcast_chat_typing", function(text) + net.Start("pac_chat_typing_mirror") + net.WriteString(text) + net.SendToServer() + end) + pac.AddHook("FinishChat", "end_chat_typing", function(text) + net.Start("pac_chat_typing_mirror") + net.WriteString("") + net.SendToServer() + end) + pac.broadcast_chat_typing = true + end + DisplayText = self:GetPlayerOwner().pac_mirrored_chat_text or "" + elseif self.TextOverride == "ChatSent" then + if self:GetPlayerOwner().pac_say_event then + DisplayText = self:GetPlayerOwner().pac_say_event.str + else + DisplayText = "" + end end if self.ConcatenateTextAndOverrideValue then @@ -521,11 +551,24 @@ end function PART:OnHide() pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) end + function PART:OnRemove() pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) + local remains_chat_text_part = false + for i,v in ipairs(pac.GetLocalParts()) do + if v.ClassName == "text" then + if v.TextOverride == "ChatTyping" then + remains_chat_text_part = true + end + end + end + if not remains_chat_text_part then + pac.RemoveHook("ChatTextChanged", "broadcast_chat_typing") + pac.broadcast_chat_typing = false + end end function PART:SetText(str) self.Text = str end -BUILDER:Register() \ No newline at end of file +BUILDER:Register() diff --git a/lua/pac3/core/server/net_messages.lua b/lua/pac3/core/server/net_messages.lua index 2076fd32c..2f37bc6b4 100644 --- a/lua/pac3/core/server/net_messages.lua +++ b/lua/pac3/core/server/net_messages.lua @@ -1,6 +1,8 @@ util.AddNetworkString("pac.AllowPlayerButtons") util.AddNetworkString("pac.BroadcastPlayerButton") +util.AddNetworkString("pac_chat_typing_mirror") +util.AddNetworkString("pac_chat_typing_mirror_broadcast") do -- button event net.Receive("pac.AllowPlayerButtons", function(length, client) @@ -29,3 +31,11 @@ do -- button event broadcast_key(ply, key, false) end) end + +net.Receive("pac_chat_typing_mirror", function(len, ply) + local str = net.ReadString() + net.Start("pac_chat_typing_mirror_broadcast") + net.WriteString(str) + net.WriteEntity(ply) + net.Broadcast() +end) From 00d958f5b8a276dc7f3c478b1a8fc9f532c95436 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 17 Feb 2024 22:56:16 -0500 Subject: [PATCH 169/300] new shorthand proxy functions -nsin makes a normalized sine ranging from 0 to 1, similar to what easy setup needs to do -nsin2 is the same as nsin but half a pi radians less, so that the cycle's output starts at 0 (assuming 0, the starting value for timeex-based expressions) instead of 0.5 -ncos makes a normalized cosine -ncos2 is ncos with pi radians less to start output at 0 shortened standard timeex-based fades to replace dual clamp fade-in-fade-out setups. they are normalized (0-1 range). -ezfade(speed, starttime, endtime) resolves the fade equations by using a desired speed and the extrema. if not enough speed, it might not make the full transition and it'll crossfade before reaching 1 -ezfade_4pt(in_starttime, in_endtime, out_starttime, out_endtime) resolves the fade equations by using the four crossing points new aliases to save a little bit of text space or perhaps be more meaningful to some: -if_else = number_operator_alternative -if_event = if_else_event = event_alternative event_alternative now defaults the 2nd and 3rd args to 0 and 1. reflecting the event's state, if they are not specified and server population-related inputs --- lua/pac3/core/client/parts/proxy.lua | 86 +++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 7a37066c0..183d3728f 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -394,6 +394,8 @@ end PART.Inputs.event_alternative = function(self, uid1, num1, num2) if not uid1 then return 0 end + num1 = num1 or 0 + num2 = num2 or 1 local owner = self:GetPlayerOwner() local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) @@ -416,6 +418,76 @@ PART.Inputs.event_alternative = function(self, uid1, num1, num2) end return 0 end +PART.Inputs.if_else_event = PART.Inputs.event_alternative +PART.Inputs.if_event = PART.Inputs.event_alternative + +--normalized sine +PART.Inputs.nsin = function(self, radians) return 0.5 + 0.5*math.sin(radians) end +--normalized sin starting at 0 with timeex +PART.Inputs.nsin2 = function(self, radians) return 0.5 + 0.5*math.sin(-math.pi/2 + radians) end +--normalized cos +PART.Inputs.ncos = function(self, radians) return 0.5 + 0.5*math.cos(radians) end +--normalized cos starting at 0 with timeex +PART.Inputs.ncos2 = function(self, radians) return 0.5 + 0.5*math.cos(-math.pi + radians) end + +--easy clamp fades +--speed and two zero-crossing points (when it begins moving, when it goes back to 0) +PART.Inputs.ezfade = function(self, speed, starttime, endtime) + speed = speed or 1 + starttime = starttime or 0 + self.time = self.time or pac.RealTime + local timeex = pac.RealTime - self.time + local start_offset_constant = -starttime * speed + + if not endtime then --only a fadein + return math.Clamp(start_offset_constant + timeex * speed, 0, 1) + else --fadein fadeout + local end_offset_constant = endtime * speed + return math.Clamp(start_offset_constant + timeex * speed, 0, 1) * math.Clamp(end_offset_constant - timeex * speed, 0, 1) + end +end +--four crossing points +PART.Inputs.ezfade_4pt = function(self, in_starttime, in_endtime, out_starttime, out_endtime) + if not in_starttime or not in_endtime then self:SetError("ezfade_4pt needs at least two arguments! (in_starttime, in_endtime, out_starttime, out_endtime)") return 0 end -- needs at least two args. we could assume 0 starting point and first arg is fadein end, but it'll mess up the order and confuse people + local fadein_result = 0 + local fadeout_result = 1 + local in_speed = 1 + local out_speed = 1 + + self.time = self.time or pac.RealTime + local timeex = pac.RealTime - self.time + + if in_starttime == in_endtime then + if timeex < in_starttime then + fadein_result = 0 + else + fadein_result = 1 + end + else + in_speed = 1 / (in_endtime - in_starttime) + local start_offset_constant = -in_starttime * in_speed + fadein_result = math.Clamp(start_offset_constant + timeex * in_speed, 0, 1) + end + if not out_starttime then --missing data, assume no fadeout + fadeout_result = 1 + elseif not out_endtime then --missing data, assume no fadeout + fadeout_result = 1 + else + if out_starttime ~= out_endtime then + out_speed = 1 / (out_endtime - out_starttime) + local end_offset_constant = out_endtime * out_speed + fadeout_result = math.Clamp(end_offset_constant - timeex * out_speed, 0, 1) + else + if timeex > out_starttime then + fadeout_result = 0 + else + fadeout_result = 1 + end + end + + end + return fadein_result * fadeout_result +end for i=1,5,1 do PART.Inputs["var" .. i] = function(self, uid1) @@ -455,6 +527,7 @@ PART.Inputs.number_operator_alternative = function(self, comp1, op, comp2, num1, end if b then return num1 or 0 else return num2 or 0 end end +PART.Inputs.if_else = PART.Inputs.number_operator_alternative PART.Inputs.hexadecimal_level_sequence = function(self, freq, str) if not str then return 0 end @@ -1084,6 +1157,15 @@ PART.Inputs.flat_dot_right = function(self) end +PART.Inputs.server_maxplayers = function(self) + return game.MaxPlayers() +end +PART.Inputs.server_playercount = function(self) return #player.GetAll() end +PART.Inputs.server_population = PART.Inputs.server_playercount +PART.Inputs.server_botcount = function(self) return #player.GetBots() end +PART.Inputs.server_humancount = function(self) return #player.GetHumans() end + + PART.Inputs.pac_healthbars_total = function(self) local ent = self:GetPlayerOwner() if ent.pac_healthbars then @@ -1441,9 +1523,9 @@ function PART:OnThink(to_hide) end end - --foolproofing: scream at the user if they didn't set a variable name + --foolproofing: scream at the user if they didn't set a variable name and there's no extra expressions ready to be used if self == pace.current_part then self.touched = true end - if self ~= pace.current_part and self.VariableName == "" and self.touched then + if self ~= pace.current_part and self.VariableName == "" and self.touched and self.Extra1 == "" and self.Extra2 == "" and self.Extra3 == "" and self.Extra4 == "" and self.Extra5 == "" then self:AttachEditorPopup("You forgot to set a variable name! The proxy won't work until it knows where to send the math!", true) pace.FlashNotification("An edited proxy still has no variable name! The proxy won't work until it knows where to send the math!") self:SetWarning("You forgot to set a variable name! The proxy won't work until it knows where to send the math!") From bc00adb6a4032de1ee1e58657d5fefcdb8033daf Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 29 Feb 2024 22:49:40 -0500 Subject: [PATCH 170/300] camera rework and related stuff yet another attempt at making cameras more usable with some better code but also raw hackery -view camera when activating a pac_event related to it -manually view camera via partmenu option, some other part classes have similar activation/preview actions -better auto-switching between cameras in various cases PART:SetSmallIcon(str) overrides the tiny event indicator on part icons used when manually viewing a camera to display an eye showing that this camera is the one being viewed, might be used later PART:GetReasonsHidden() gives a whole list of reasons as a table of part-indexed strings instead of doing early returns as soon as one reason is found. there are often multiple reasons, a common one is parent hiding also added parent hiding reason to PART:GetReasonHidden() use interpolated_multibone's friendlyname in nicename to reflect that we'll refer to interpolated_multibone as interpolator, and added a better nicename showing the nodes event seen_by_player detects other players' eyeangle raycasts on an adjustable bounding box --- lua/pac3/core/client/base_part.lua | 85 +++++++++ lua/pac3/core/client/parts/camera.lua | 180 ++++++++++++++---- lua/pac3/core/client/parts/event.lua | 110 ++++++++--- .../client/parts/interpolated_multibone.lua | 21 ++ .../editor/client/panels/extra_properties.lua | 61 ++++++ lua/pac3/editor/client/panels/tree.lua | 2 +- lua/pac3/editor/client/parts.lua | 115 ++++++++--- lua/pac3/editor/client/view.lua | 106 +++++++---- lua/pac3/editor/client/wear.lua | 6 + 9 files changed, 563 insertions(+), 123 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 59b1225fa..31310ab0a 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -564,6 +564,35 @@ do -- scene graph end end + function PART:SetSmallIcon(str) + if str == "event" then str = "icon16/clock_red.png" end + if self.pace_tree_node then + if self.pace_tree_node.Icon then + if not self.pace_tree_node.Icon.event_icon then + local pnl = vgui.Create("DImage", self.pace_tree_node.Icon) + self.pace_tree_node.Icon.event_icon_alt = true + self.pace_tree_node.Icon.event_icon = pnl + pnl:SetSize(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1))) + pnl:SetPos(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1))) + end + self.pace_tree_node.Icon.event_icon_alt = true + self.pace_tree_node.Icon.event_icon:SetImage(str) + self.pace_tree_node.Icon.event_icon:SetVisible(true) + end + end + end + function PART:RemoveSmallIcon() + if self.pace_tree_node then + if self.pace_tree_node.Icon then + if self.pace_tree_node.Icon.event_icon then + self.pace_tree_node.Icon.event_icon_alt = false + self.pace_tree_node.Icon.event_icon:SetImage("icon16/clock_red.png") + self.pace_tree_node.Icon.event_icon:SetVisible(false) + end + end + end + end + end do -- hidden / events @@ -669,6 +698,17 @@ do -- hidden / events self.active_events[event_part] = event_part self.active_events_ref_count = self.active_events_ref_count + 1 self:CallRecursive("CalcShowHide", false) + if self.ClassName == "camera" and pac.LocalPlayer == self:GetPlayerOwner() then + if event_part.Event == "command" then + pac.camera_linked_command_events[string.Split(event_part.Arguments,"@@")[1]] = true + end + if pac.active_camera_manual == self then --we're force-viewing this camera on the editor, assume we want to swap + pace.ManuallySelectCamera(self, false) + else + pac.TryToAwakenDormantCameras(self) + end + self:SetSmallIcon("event") + end end else if self.active_events[event_part] then @@ -676,6 +716,12 @@ do -- hidden / events self.active_events_ref_count = self.active_events_ref_count - 1 self:CallRecursive("CalcShowHide", false) end + if self.ClassName == "camera" and pac.LocalPlayer == self:GetPlayerOwner() then + if pac.active_camera_manual then --we're force-viewing another camera on the editor, since we're showing a new camera, assume we want to swap + pace.ManuallySelectCamera(self, true) + end + self:SetSmallIcon("event") + end end end @@ -698,9 +744,48 @@ do -- hidden / events return "pac_hide_disturbing is set to 1" end + for i,part in ipairs(self:GetParentList()) do + if part:IsHidden() then + table_insert(found, tostring(part) .. " is parent hiding") + end + end + if found[1] then + return table.concat(found, "\n") + end + return "" end + function PART:GetReasonsHidden() + local found = {} + + for part in pairs(self.active_events) do + found[part] = "event hiding" + end + + if self.Hide then + found[self] = "self hiding" + end + + if self.hide_disturbing then + if self.Hide then + found[self] = "self hiding and disturbing" + else + found[self] = "disturbing" + end + end + + for i,part in ipairs(self:GetParentList()) do + if not found[part] then + if part:IsHidden() then + found[part] = "parent hidden" + end + end + end + + return found + end + function PART:CalcShowHide(from_rendering) local b = self:IsHidden() diff --git a/lua/pac3/core/client/parts/camera.lua b/lua/pac3/core/client/parts/camera.lua index dcd2c2bfa..382537168 100644 --- a/lua/pac3/core/client/parts/camera.lua +++ b/lua/pac3/core/client/parts/camera.lua @@ -19,24 +19,76 @@ for i, ply in ipairs(player.GetAll()) do ply.pac_cameras = nil end -function PART:OnShow() - local owner = self:GetPlayerOwner() - if not owner:IsValid() then return end - self.inactive = false +pac.client_camera_parts = {} - owner.pac_cameras = owner.pac_cameras or {} - owner.pac_cameras[self] = self +local function CheckCamerasAgain(ply) + local cams = ply.pac_cameras or {} + local fpos, fang, ffov, fnearz, ffarz - --the policy is that a shown camera takes priority over all others - for _, part in pairs(owner.pac_cameras) do + for _, part in pairs(cams) do + if (not part.inactive or part.priority) and not part:IsHidden() then + return true + end + end +end + +function pac.RebuildCameras(restricted_search) + local found_cams = false + pac.LocalPlayer.pac_cameras = {} + pac.client_camera_parts = {} + local parts_to_check + if restricted_search then parts_to_check = pac.client_camera_parts else parts_to_check = pac.GetLocalParts() end + if table.IsEmpty(pac.client_camera_parts) then + parts_to_check = pac.GetLocalParts() + end + for _,part in pairs(parts_to_check) do + if part:IsValid() then + part.inactive = nil + if part.ClassName == "camera" then + pac.nocams = false + found_cams = true + pac.client_camera_parts[part.UniqueID] = part + if not part.inactive or not part:IsHidden() or part.priority then + pac.LocalPlayer.pac_cameras[part] = part + end + end + + end + end + if not found_cams then + pac.nocams = true + end +end + +function PART:CameraTakePriority(then_view) + self:GetPlayerOwner().pac_cameras = self:GetPlayerOwner().pac_cameras or {} + for _, part in pairs(self:GetPlayerOwner().pac_cameras) do if part ~= self then part.priority = false + part.inactive = true + part:RemoveSmallIcon() end end self.priority = true + self.inactive = false timer.Simple(0.02, function() self.priority = true end) + if then_view then + timer.Simple(0.2, function() pace.CameraPartSwapView(true) end) + end +end + +function PART:OnShow() + local owner = self:GetPlayerOwner() + if not owner:IsValid() then return end + self.inactive = false + + owner.pac_cameras = owner.pac_cameras or {} + owner.pac_cameras[self] = self + + --the policy is that a shown camera takes priority over all others + self:CameraTakePriority() end function PART:OnHide() @@ -54,6 +106,44 @@ function PART:OnHide() self.inactive = true self.priority = false owner.pac_cameras[self] = nil + pac.TryToAwakenDormantCameras() +end + +function PART:OnRemove() + local owner = self:GetPlayerOwner() + + if LocalPlayer() == owner then + owner.pac_cameras = owner.pac_cameras or {} + pac.client_camera_parts[self.UniqueID] = nil + local other_visible_cameras = 0 + --this camera cedes priority to others that may be active + for _, part in pairs(owner.pac_cameras) do + if part.UniqueID ~= self.UniqueID and not part:IsHidden() then + part.priority = true + other_visible_cameras = other_visible_cameras + 1 + end + end + owner.pac_cameras[self] = nil + if not pace.hack_camera_part_donot_treat_wear_as_creating_part and not pace.is_still_loading_wearing then + timer.Simple(0.2, function() + pace.EnableView(true) + end) + timer.Simple(0.4, function() + pace.ResetView() + pace.CameraPartSwapView(true) + end) + end + if pac.active_camera == self then pac.active_camera = nil end + if pac.active_camera_manual == self then pac.active_camera_manual = nil end + pac.RebuildCameras() + end +end + +function PART:Initialize() + if pac.LocalPlayer == self:GetPlayerOwner() then + pac.nocams = false + pac.client_camera_parts[self.UniqueID] = self + end end --[[function PART:OnHide() @@ -88,47 +178,55 @@ local temp = {} local remaining_camera = false local remaining_camera_time_buffer = CurTime() -local function CheckCamerasAgain(ply) - local cams = ply.pac_cameras or {} - local fpos, fang, ffov, fnearz, ffarz - for _, part in pairs(cams) do - if (not part.inactive or part.priority) and not part:IsHidden() then - return true - end +function pac.TryToAwakenDormantCameras(calling_part) + if pace.Editor:IsValid() then return end + + if not isbool(calling_part) then + pac.RebuildCameras() end -end -local function RebuildCameras(ply) - ply.pac_cameras = {} - for _,part in pairs(pac.GetLocalParts()) do + for _,part in pairs(pac.client_camera_parts) do if part:IsValid() then - if part.ClassName == "camera" and (not part.inactive or not part:IsHidden() or part.priority) then - if part:GetPlayerOwner() == ply then - ply.pac_cameras[part] = part - end + if part.ClassName == "camera" and part ~= calling_part then + part:GetRootPart():CallRecursive("Think") end end end + + pace.EnableView(false) end -pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) +pac.nocams = true +pac.nocam_counter = 0 +function pac.HandleCameraPart(ply, pos, ang, fov, nearz, farz) + local chosen_part local fpos, fang, ffov, fnearz, ffarz - local warning_state = not ply.pac_cameras + local ply = pac.LocalPlayer + if pac.nocams then return end + ply.pac_cameras = ply.pac_cameras or {} + + local warning_state = ply.pac_cameras == nil if not warning_state then warning_state = table.IsEmpty(ply.pac_cameras) end if ply:GetViewEntity() ~= ply then return end remaining_camera = false remaining_camera_time_buffer = remaining_camera_time_buffer or CurTime() + pace.delaymovement = RealTime() + 1 --we need to do that so that while testing cameras, you don't fly off when walking and end up far from your character if warning_state then - RebuildCameras(ply) + pac.RebuildCameras(true) + pac.nocam_counter = pac.nocam_counter + 1 + --go back to early returns to avoid looping through localparts when no cameras are active checked 500 times + if pac.nocam_counter > 500 then pac.nocams = true return end else + if not IsValid(pac.active_camera) then pac.active_camera = nil pac.RebuildCameras(true) end + pac.nocam_counter = 0 + local chosen_camera for _, part in pairs(ply.pac_cameras) do if part.ClassName ~= "camera" then ply.pac_cameras[part] = nil end - if part.ClassName == "camera" and part:IsValid() then if not part:IsHidden() then remaining_camera = true @@ -138,7 +236,7 @@ pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) part:CalcShowHide() if not part.inactive then --calculate values ahead of the return, used as a fallback just in case - fpos, fang, ffov, fnearz, ffarz = part:CalcView(ply, pos, ang, fov, nearz, farz) + fpos, fang, ffov, fnearz, ffarz = part:CalcView(_,_,ply:EyeAngles()) temp.origin = fpos temp.angles = fang temp.fov = ffov @@ -147,16 +245,27 @@ pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) temp.drawviewer = false if not part:IsHidden() and not part.inactive and part.priority then + pac.active_camera = part temp.drawviewer = not part.DrawViewModel - return temp + chosen_camera = part + break end end else ply.pac_cameras[part] = nil end end + + + if chosen_camera then + chosen_camera:SetSmallIcon("icon16/eye.png") + return temp + end end + if not pac.active_camera then + pac.RebuildCameras() + end if remaining_camera or CurTime() < remaining_camera_time_buffer then return temp end @@ -164,11 +273,16 @@ pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) --final fallback, just give us any valid pac camera to preserve the view! priority will be handled elsewhere if CheckCamerasAgain(ply) then return temp - else - return end - - return --only time to return to first person is if all camera parts are hidden AFTER we pass the buffer time filter --until we make reversible first person a thing, letting some non-drawable parts think, this is the best solution I could come up with +end + +function pac.HasRemainingCameraPart() + pac.RebuildCameras() + return table.Count(pac.LocalPlayer.pac_cameras) ~= 0 +end + +pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) + pac.HandleCameraPart(ply, pos, ang, fov, nearz, farz) end) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 5ea8a5dee..b9ec9d213 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -748,6 +748,47 @@ PART.OldEvents = { end, }, + seen_by_player = { + operator_type = "none", + tutorial = "looked_at_by_player activates when a player is looking at you, determined by whether a box around you touches the direct eyeangle line", + arguments = {{extra_radius = "number"}, {require_line_of_sight = "boolean"}}, + userdata = {{editor_panel = "seen_by_player"}}, + callback = function(self, ent, extra_radius, require_line_of_sight) + extra_radius = extra_radius or 0 + self.nextcheck = self.nextcheck or CurTime() + 0.1 + if CurTime() > self.nextcheck then + for _,v in ipairs(player.GetAll()) do + if v == ent then continue end + local eyetrace = v:GetEyeTrace() + + if util.IntersectRayWithOBB(eyetrace.StartPos, eyetrace.HitPos - eyetrace.StartPos, LocalPlayer():GetPos() + LocalPlayer():OBBCenter(), Angle(0,0,0), Vector(-extra_radius,-extra_radius,-extra_radius), Vector(extra_radius,extra_radius,extra_radius)) then + self.trace_success = true + self.trace_success_ply = v + self.nextcheck = CurTime() + 0.1 + goto CHECKOUT + end + if eyetrace.Entity == ent then + self.trace_success = true + self.trace_success_ply = v + self.nextcheck = CurTime() + 0.1 + goto CHECKOUT + end + end + self.trace_success = false + self.nextcheck = CurTime() + 0.1 + end + ::CHECKOUT:: + if require_line_of_sight then + return self.trace_success + and self.trace_success_ply:IsLineOfSightClear(ent) --check world LOS + and ((util.QuickTrace(self.trace_success_ply:EyePos(), ent:EyePos() - self.trace_success_ply:EyePos(), self.trace_success_ply).Entity == ent) + or (util.QuickTrace(self.trace_success_ply:EyePos(), ent:GetPos() + ent:OBBCenter() - self.trace_success_ply:EyePos(), self.trace_success_ply).Entity == ent)) + else + return self.trace_success + end + end, + }, + is_flashlight_on = { operator_type = "none", callback = function(self, ent) @@ -3337,6 +3378,11 @@ pac.AddHook("EntityFireBullets", "firebullets", function(ent, data) end end) +--for regaining focus on cameras from first person, hacky thing to not loop through localparts every time +--only if the received command name matches that of a camera's linked command event +--we won't be finding from substrings +pac.camera_linked_command_events = {} +local initially_check_camera_linked_command_events = true net.Receive("pac_event", function(umr) local ply = net.ReadEntity() @@ -3351,12 +3397,21 @@ net.Receive("pac_event", function(umr) if ply:IsValid() then ply.pac_command_events = ply.pac_command_events or {} ply.pac_command_events[str] = {name = str, time = pac.RealTime, on = on} + if pac.LocalPlayer == ply then + if pac.camera_linked_command_events[str] then --if this might be related to a camera + pac.TryToAwakenDormantCameras() + elseif initially_check_camera_linked_command_events then --if it's not known, check only once for initialize this might be related to a camera + pac.TryToAwakenDormantCameras(true) + initially_check_camera_linked_command_events = false + end + end end end) concommand.Add("pac_wipe_events", function(ply) ply.pac_command_events = nil ply.pac_command_event_sequencebases = nil + pac.camera_linked_command_events = {} end) concommand.Add("pac_print_events", function(ply) ply.pac_command_events = ply.pac_command_events or {} @@ -3624,43 +3679,44 @@ do local list = {} - if true then - local colors = {} - - for name,colstr in pairs(pace.command_colors) do - colors[colstr] = colors[colstr] or {} - colors[colstr][name] = available[name] - end + local colors = {} - for col,tbl in pairs(colors) do + for name,colstr in pairs(pace.command_colors) do + colors[colstr] = colors[colstr] or {} + colors[colstr][name] = available[name] + end - local sublist = {} - for k,v in pairs(tbl) do - table.insert(sublist,available[k]) - end - table.sort(sublist, function(a, b) return a.trigger < b.trigger end) + for col,tbl in pairs(colors) do - for i,v in pairs(sublist) do - table.insert(list,v) - end + local sublist = {} + for k,v in pairs(tbl) do + table.insert(sublist,available[k]) end - local uncolored_sublist = {} + table.sort(sublist, function(a, b) return a.trigger < b.trigger end) - for k,v in pairs(available) do - if uncolored_events[k] then - table.insert(uncolored_sublist,available[k]) - end + for i,v in pairs(sublist) do + table.insert(list,v) end + end - table.sort(uncolored_sublist, function(a, b) return a.trigger < b.trigger end) + local uncolored_sublist = {} - for k,v in ipairs(uncolored_sublist) do - table.insert(list, v) + for k,v in pairs(available) do + if uncolored_events[k] then + table.insert(uncolored_sublist,available[k]) end - else + end + + table.sort(uncolored_sublist, function(a, b) return a.trigger < b.trigger end) + + for k,v in ipairs(uncolored_sublist) do + table.insert(list, v) + end + + --[[legacy behavior for k,v in pairs(available) do if k == names[k].name then @@ -3670,7 +3726,7 @@ do end table.sort(list, function(a, b) return a.trigger > b.trigger end) - end + ]] return list end @@ -4167,4 +4223,4 @@ net.Receive("pac_update_healthbars", function() end end -end) +end) \ No newline at end of file diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua index 38f24a742..168709755 100644 --- a/lua/pac3/core/client/parts/interpolated_multibone.lua +++ b/lua/pac3/core/client/parts/interpolated_multibone.lua @@ -44,6 +44,27 @@ function PART:OnRemove() SafeRemoveEntityDelayed(self.Owner,0.1) end +function PART:GetNiceName() + if self.Name ~= "" then return self.Name end + + if not self.valid_nodes then return self.FriendlyName end + local has_valid_node = false + for i,b in ipairs(self.valid_nodes) do + if b then has_valid_node = true end + end + if not has_valid_node then return self.FriendlyName end + + local str = "Interpolator: " + local firstnodecounted = false + for i=1,20,1 do + if IsValid(self["Node"..i]) then + str = str .. (firstnodecounted and "; " or "") .. "[" .. i .. "]" .. (self["Node"..i].Name ~= "" and self["Node"..i].Name or self["Node"..i].ClassName) + firstnodecounted = true + end + end + return str +end + function PART:Initialize() self.nodes = {} self.valid_nodes = {} diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 13b16f021..65f04b10d 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -860,6 +860,67 @@ do -- event is_touching pace.RegisterPanel(PANEL) end +do -- event seen_by_player + local PANEL = {} + + PANEL.ClassName = "properties_seen_by_player" + PANEL.Base = "pace_properties_number" + + function PANEL:OnValueSet() + local function stop() + hook.Remove("PostDrawOpaqueRenderables", "pace_draw_is_touching2") + end + local last_part = pace.current_part + + hook.Add("PostDrawOpaqueRenderables", "pace_draw_is_touching2", function() + local part = pace.current_part + if part ~= last_part then stop() return end + if not part:IsValid() then stop() return end + if part.ClassName ~= "event" then stop() return end + if not (part:GetEvent() == "seen_by_player") then stop() return end + if not pace.IsActive() then stop() return end + + local extra_radius = part:GetProperty("extra_radius") or 0 + + local ent + if part.RootOwner then + ent = part:GetRootPart():GetOwner() + else + ent = part:GetOwner() + end + + if not IsValid(ent) then stop() return end + + local mins = Vector(-extra_radius,-extra_radius,-extra_radius) + local maxs = Vector(extra_radius,extra_radius,extra_radius) + + local b = false + local players_see = {} + for _,v in ipairs(player.GetAll()) do + if v == ent then continue end + local eyetrace = v:GetEyeTrace() + + local this_player_sees = false + if util.IntersectRayWithOBB(eyetrace.StartPos, eyetrace.HitPos - eyetrace.StartPos, LocalPlayer():GetPos() + LocalPlayer():OBBCenter(), Angle(0,0,0), Vector(-extra_radius,-extra_radius,-extra_radius), Vector(extra_radius,extra_radius,extra_radius)) then + b = true + this_player_sees = true + end + if eyetrace.Entity == ent then + b = true + this_player_sees = true + end + render.DrawLine(eyetrace.StartPos, eyetrace.HitPos, this_player_sees and Color(255, 0,0) or Color(255,255,255), true) + end + ::CHECKOUT:: + if self.udata then + render.DrawWireframeBox( ent:GetPos() + ent:OBBCenter(), Angle( 0, 0, 0 ), mins, maxs, b and Color(255,0,0) or Color(255,255,255), true ) + end + end) + end + + pace.RegisterPanel(PANEL) +end + do --projectile radius local PANEL = {} diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index 8f479046a..2fd6dff24 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -168,7 +168,7 @@ do node.Icon.event_icon:SetVisible(true) else - if node.Icon.event_icon then + if node.Icon.event_icon and not node.Icon.event_icon_alt then node.Icon.event_icon:SetVisible(false) end end diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 0e27f7b29..adec193ec 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -334,22 +334,7 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent) end if class_name == "camera" and GetConVar("pac_copilot_force_preview_cameras"):GetBool() then - RunConsoleCommand("pac_enable_editor_view", "0") - pace.EnableView(true) - pac.RemoveHook("CalcView", "editor") - pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) - part:CalcShowHide() - pos, ang, fov, nearz, farz = part:CalcView(_,_,ply:EyeAngles()) - local temp = {} - temp.origin = pos - temp.angles = ang - temp.fov = fov - temp.znear = nearz - temp.zfar = farz - temp.drawviewer = not part.DrawViewModel - return temp - - end) + timer.Simple(0.2, function() pace.EnableView(false) end) end if GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool() then if star_properties[part.ClassName] then @@ -1243,17 +1228,7 @@ do -- menu if not obj:HasParent() and obj.ClassName == "group" then pace.RemovePartOnServer(obj:GetUniqueID(), false, true) end - if obj.ClassName == "camera" and GetConVar("pac_copilot_force_preview_cameras"):GetBool() then - local no_camera_part = true - for i,v in ipairs(pac.GetLocalParts()) do - if v.ClassName == "camera" then no_camera_part = false end - end - if no_camera_part then - RunConsoleCommand("pac_enable_editor_view", "1") - pac.RemoveHook("CalcView", "camera_part") - pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil) - end - end + end function pace.SwapBaseMovables(obj1, obj2, promote) @@ -1933,6 +1908,7 @@ do -- menu --new_operations_order --default_operations_order --if not obj then obj = pace.current_part end + if obj then pace.AddClassSpecificPartMenuComponents(menu, obj) end for _,option_name in ipairs(pace.operations_order) do pace.addPartMenuComponent(menu, obj, option_name) end @@ -2185,6 +2161,91 @@ function pace.GetPartSizeInformation(obj) } end +function pace.AddClassSpecificPartMenuComponents(menu, obj) + if obj.ClassName == "camera" then + if not obj:IsHidden() then + if obj ~= pac.active_camera then + menu:AddOption("View this camera", function() + pace.ManuallySelectCamera(obj, true) + end):SetIcon("icon16/star.png") + else + menu:AddOption("Unview this camera", function() + pace.EnableView(true) + pace.ResetView() + pac.active_camera_manual = nil + if obj.pace_tree_node then + if obj.pace_tree_node.Icon then + if obj.pace_tree_node.Icon.event_icon then + obj.pace_tree_node.Icon.event_icon_alt = false + obj.pace_tree_node.Icon.event_icon:SetImage("event") + obj.pace_tree_node.Icon.event_icon:SetVisible(false) + end + end + end + end):SetIcon("icon16/camera_delete.png") + end + else + menu:AddOption("View this camera", function() + local toggleable_command_events = {} + for part,reason in pairs(obj:GetReasonsHidden()) do + if reason == "event hiding" then + if part.Event == "command" then + local cmd, time, hide = part:GetParsedArgumentsForObject(part.Events.command) + if time == 0 then + toggleable_command_events[part] = cmd + end + end + end + end + for part,cmd in pairs(toggleable_command_events) do + RunConsoleCommand("pac_event", cmd, part.Invert and "1" or "0") + end + timer.Simple(0.1, function() + pace.ManuallySelectCamera(obj, true) + end) + end):SetIcon("icon16/star.png") + end + elseif obj.ClassName == "command" then + menu:AddOption("run command", function() obj:Execute() end):SetIcon("icon16/star.png") + elseif obj.ClassName == "sound" or obj.ClassName == "sound2" then + menu:AddOption("play sound", function() obj:PlaySound() end):SetIcon("icon16/star.png") + elseif obj.ClassName == "projectile" then + local pos, ang = obj:GetDrawPosition() + menu:AddOption("fire", function() obj:Shoot(pos, ang, obj.NumberProjectiles) end):SetIcon("icon16/star.png") + elseif obj.ClassName == "hitscan" then + menu:AddOption("fire", function() obj:Shoot() end):SetIcon("icon16/star.png") + elseif obj.ClassName == "damage_zone" then + menu:AddOption("run command", function() obj:OnShow() end):SetIcon("icon16/star.png") + elseif obj.ClassName == "particles" then + if obj.FireOnce then + menu:AddOption("(FireOnce only) spew", function() obj:OnShow() end):SetIcon("icon16/star.png") + end + elseif obj.ClassName == "proxy" then + if string.find(obj.Expression, "timeex") or string.find(obj.Expression, "ezfade") then + menu:AddOption("(timeex) reset clock", function() obj:OnHide() obj:OnShow() end):SetIcon("icon16/star.png") + end + elseif obj.ClassName == "shake" then + menu:AddOption("activate (editor camera should be off)", function() obj:OnHide() obj:OnShow() end):SetIcon("icon16/star.png") + elseif obj.ClassName == "event" then + if obj.Event == "command" then + local cmd, time, hide = obj:GetParsedArgumentsForObject(obj.Events.command) + if time == 0 then --toggling mode + pac.LocalPlayer.pac_command_events[cmd] = pac.LocalPlayer.pac_command_events[cmd] or {name = cmd, time = pac.RealTime, on = 0} + ----MORE PAC JANK?? SOMETIMES, THE 2 NOTATION DOESN'T CHANGE THE STATE YET + if pac.LocalPlayer.pac_command_events[cmd].on == 1 then + menu:AddOption("(command) toggle", function() RunConsoleCommand("pac_event", cmd, "0") end):SetIcon("icon16/star.png") + else + menu:AddOption("(command) toggle", function() RunConsoleCommand("pac_event", cmd, "1") end):SetIcon("icon16/star.png") + end + + else + menu:AddOption("(command) trigger", function() RunConsoleCommand("pac_event", cmd) end):SetIcon("icon16/star.png") + end + + end + end +end + function pace.addPartMenuComponent(menu, obj, option_name) if option_name == "save" and obj then diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index f1f638046..1d36dab55 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -139,17 +139,6 @@ function pace.GUIMouseReleased(mc) if pace.editing_viewmodel or pace.editing_hands then return end mcode = nil - if not GetConVar("pac_enable_editor_view"):GetBool() then pace.EnableView(true) - else - pac.RemoveHook("CalcView", "camera_part") - pac.AddHook("GUIMousePressed", "editor", pace.GUIMousePressed) - pac.AddHook("GUIMouseReleased", "editor", pace.GUIMouseReleased) - pac.AddHook("ShouldDrawLocalPlayer", "editor", pace.ShouldDrawLocalPlayer, DLib and -4 or ULib and -1 or nil) - pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil) - pac.AddHook("HUDPaint", "editor", pace.HUDPaint) - pac.AddHook("HUDShouldDraw", "editor", pace.HUDShouldDraw) - pac.AddHook("PostRenderVGUI", "editor", pace.PostRenderVGUI) - end end local function set_mouse_pos(x, y) @@ -166,6 +155,7 @@ local function MovementBindDown(name) end local function CalcDrag() + if not pace.properties or not pace.properties.search then return end if pace.BusyWithProperties:IsValid() or @@ -276,6 +266,14 @@ end local follow_entity = CreateClientConVar("pac_camera_follow_entity", "0", true) local enable_editor_view = CreateClientConVar("pac_enable_editor_view", "1", true) +cvars.AddChangeCallback("pac_enable_editor_view", function(name, old, new) + if new == "1" then + pace.EnableView(true) + else + pace.CameraPartSwapView() + end +end, "pace_update_editor_view") + local lastEntityPos function pace.CalcView(ply, pos, ang, fov) @@ -364,13 +362,18 @@ function pace.PostRenderVGUI() end function pace.EnableView(b) - if b then pac.AddHook("GUIMousePressed", "editor", pace.GUIMousePressed) pac.AddHook("GUIMouseReleased", "editor", pace.GUIMouseReleased) pac.AddHook("ShouldDrawLocalPlayer", "editor", pace.ShouldDrawLocalPlayer, DLib and -4 or ULib and -1 or nil) - pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil) - pac.RemoveHook("CalcView", "camera_part") + if enable_editor_view:GetBool() then + pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil) + pac.RemoveHook("CalcView", "camera_part") + pac.active_camera = nil + else + if pac.HasRemainingCameraPart() then pace.CameraPartSwapView() end + pac.RemoveHook("CalcView", "editor") + end pac.AddHook("HUDPaint", "editor", pace.HUDPaint) pac.AddHook("HUDShouldDraw", "editor", pace.HUDShouldDraw) pac.AddHook("PostRenderVGUI", "editor", pace.PostRenderVGUI) @@ -382,36 +385,69 @@ function pace.EnableView(b) pac.RemoveHook("GUIMouseReleased", "editor") pac.RemoveHook("ShouldDrawLocalPlayer", "editor") pac.RemoveHook("CalcView", "editor") - pac.RemoveHook("CalcView", "camera_part") + pac.AddHook("CalcView", "camera_part", pac.HandleCameraPart) pac.RemoveHook("HUDPaint", "editor") pac.RemoveHook("HUDShouldDraw", "editor") pac.RemoveHook("PostRenderVGUI", "editor") pace.SetTPose(false) end +end - if not enable_editor_view:GetBool() or not pace.Editor:IsValid() then - local ply = LocalPlayer() - pac.RemoveHook("CalcView", "editor") - pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz) - for _, part in pairs(pac.GetLocalParts()) do - if part:IsValid() and part.ClassName == "camera" then - part:CalcShowHide() - local temp = {} - if not part:IsHidden() then - pos, ang, fov, nearz, farz = part:CalcView(_,_,ply:EyeAngles()) - temp.origin = pos - temp.angles = ang - temp.fov = fov - temp.znear = nearz - temp.zfar = farz - temp.drawviewer = not part.DrawViewModel - return temp - end +function pace.ManuallySelectCamera(obj, doselect) + if obj and doselect then + obj:CameraTakePriority(true) + pace.CameraPartSwapView(true) + pac.active_camera_manual = obj + elseif not doselect then + for i,v in pairs(pac.GetLocalParts()) do + if v.ClassName == "camera" then + if not v:IsHidden() and v ~= obj then + v:CameraTakePriority(true) + pace.CameraPartSwapView(true) + pac.active_camera_manual = v + return end end - end) - --pac.RemoveHook("ShouldDrawLocalPlayer", "editor") + end + pac.active_camera_manual = nil + else + for i,v in pairs(pac.GetLocalParts()) do + if v.ClassName == "camera" then + if not v:IsHidden() then + v:CameraTakePriority(true) + pace.CameraPartSwapView(true) + pac.active_camera_manual = v + return + end + end + end + end +end + +function pace.CameraPartSwapView(force_pac_camera) + local pac_camera_parts_should_override = not enable_editor_view:GetBool() or not pace.Editor:IsValid() or pac.HasRemainingCameraPart() + + if pace.Editor:IsValid() and enable_editor_view:GetBool() and not force_pac_camera then pac_camera_parts_should_override = false end + + if pac.HandleCameraPart() == nil then --no cameras + if not pace.ShouldDrawLocalPlayer() then + pace.EnableView(false) + end + pac.RemoveHook("CalcView", "camera_part") + elseif pac_camera_parts_should_override then --cameras + pac.AddHook("CalcView", "camera_part", pac.HandleCameraPart) + pac.RemoveHook("CalcView", "editor") + else + pace.EnableView(enable_editor_view:GetBool()) + --[[if not GetConVar("pac_copilot_force_preview_cameras"):GetBool() then + + else + pace.EnableView(false) + end]] end + + + return pac.active_camera end local function CalcAnimationFix(ent) diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index 97f6fbbe8..c300b8f6b 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -89,6 +89,12 @@ do -- to server local data = {part = part:ToTable()} + --hack so that camera part doesn't force-gain focus if it's not manually created, because wearing removes and re-creates parts. + pace.hack_camera_part_donot_treat_wear_as_creating_part = true + timer.Simple(2, function() + pace.hack_camera_part_donot_treat_wear_as_creating_part = nil + end) + if extra then table.Merge(data, extra) end From b9e5f7b74f3bf18fef9b24a595fba9cb8df470a7 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 29 Feb 2024 23:21:07 -0500 Subject: [PATCH 171/300] hack hotfix for recent camera rework pac.TryToAwakenDormantCameras calls Think recursively, which triggered seteventtrigger again in some setups so stop the pac.TryToAwakenDormantCameras call if we're already running it, only do the recursive thinks once starting from the first calling part, and refresh after a second --- lua/pac3/core/client/base_part.lua | 2 +- lua/pac3/core/client/parts/camera.lua | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 31310ab0a..0ed6ad904 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -704,7 +704,7 @@ do -- hidden / events end if pac.active_camera_manual == self then --we're force-viewing this camera on the editor, assume we want to swap pace.ManuallySelectCamera(self, false) - else + elseif not pac.awakening_dormant_cameras then pac.TryToAwakenDormantCameras(self) end self:SetSmallIcon("event") diff --git a/lua/pac3/core/client/parts/camera.lua b/lua/pac3/core/client/parts/camera.lua index 382537168..9352305cb 100644 --- a/lua/pac3/core/client/parts/camera.lua +++ b/lua/pac3/core/client/parts/camera.lua @@ -180,12 +180,14 @@ local remaining_camera_time_buffer = CurTime() function pac.TryToAwakenDormantCameras(calling_part) + if pace.still_loading_wearing then return end if pace.Editor:IsValid() then return end + if pac.awakening_dormant_cameras then return end if not isbool(calling_part) then pac.RebuildCameras() end - + pac.awakening_dormant_cameras = true for _,part in pairs(pac.client_camera_parts) do if part:IsValid() then if part.ClassName == "camera" and part ~= calling_part then @@ -193,6 +195,9 @@ function pac.TryToAwakenDormantCameras(calling_part) end end end + timer.Simple(1, function() + pac.awakening_dormant_cameras = nil + end) pace.EnableView(false) end From 074dd0c9671773f115024030fa08ea449820402e Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 1 Mar 2024 19:26:02 -0500 Subject: [PATCH 172/300] revert hackery from base part to outsource it to camera part's code by overriding the base part functions only for this class and use calcshowhide instead of only eventtrigger to detect show/hide operations --- lua/pac3/core/client/base_part.lua | 17 ------- lua/pac3/core/client/parts/camera.lua | 68 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 0ed6ad904..598db302a 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -698,17 +698,6 @@ do -- hidden / events self.active_events[event_part] = event_part self.active_events_ref_count = self.active_events_ref_count + 1 self:CallRecursive("CalcShowHide", false) - if self.ClassName == "camera" and pac.LocalPlayer == self:GetPlayerOwner() then - if event_part.Event == "command" then - pac.camera_linked_command_events[string.Split(event_part.Arguments,"@@")[1]] = true - end - if pac.active_camera_manual == self then --we're force-viewing this camera on the editor, assume we want to swap - pace.ManuallySelectCamera(self, false) - elseif not pac.awakening_dormant_cameras then - pac.TryToAwakenDormantCameras(self) - end - self:SetSmallIcon("event") - end end else if self.active_events[event_part] then @@ -716,12 +705,6 @@ do -- hidden / events self.active_events_ref_count = self.active_events_ref_count - 1 self:CallRecursive("CalcShowHide", false) end - if self.ClassName == "camera" and pac.LocalPlayer == self:GetPlayerOwner() then - if pac.active_camera_manual then --we're force-viewing another camera on the editor, since we're showing a new camera, assume we want to swap - pace.ManuallySelectCamera(self, true) - end - self:SetSmallIcon("event") - end end end diff --git a/lua/pac3/core/client/parts/camera.lua b/lua/pac3/core/client/parts/camera.lua index 9352305cb..828c95690 100644 --- a/lua/pac3/core/client/parts/camera.lua +++ b/lua/pac3/core/client/parts/camera.lua @@ -139,6 +139,74 @@ function PART:OnRemove() end end +local doing_calcshowhide = false + +function PART:PostOnCalcShowHide(hide) + if doing_calcshowhide then return end + doing_calcshowhide = true + timer.Simple(0.3, function() + doing_calcshowhide = false + end) + if hide then + if pac.active_camera_manual == self then --we're force-viewing this camera on the editor, assume we want to swap + pace.ManuallySelectCamera(self, false) + elseif not pac.awakening_dormant_cameras then + pac.TryToAwakenDormantCameras(self) + end + self:SetSmallIcon("event") + else + if pac.active_camera_manual then --we're force-viewing another camera on the editor, since we're showing a new camera, assume we want to swap + pace.ManuallySelectCamera(self, true) + end + self:SetSmallIcon("event") + end +end + +--these hacks are outsourced instead of being on base part +function PART:SetEventTrigger(event_part, enable) + if enable then + if not self.active_events[event_part] then + self.active_events[event_part] = event_part + self.active_events_ref_count = self.active_events_ref_count + 1 + self:CallRecursive("CalcShowHide", false) + end + + else + if self.active_events[event_part] then + self.active_events[event_part] = nil + self.active_events_ref_count = self.active_events_ref_count - 1 + self:CallRecursive("CalcShowHide", false) + end + end + + if pac.LocalPlayer == self:GetPlayerOwner() then + if event_part.Event == "command" then + pac.camera_linked_command_events[string.Split(event_part.Arguments,"@@")[1]] = true + end + + self:PostOnCalcShowHide(enable) + end +end + +function PART:CalcShowHide(from_rendering) + local b = self:IsHidden() + + if b ~= self.last_hidden then + if b then + self:OnHide(from_rendering) + else + self:OnShow(from_rendering) + end + if pac.LocalPlayer == self:GetPlayerOwner() then + self:PostOnCalcShowHide(b) + end + end + + self.last_hidden = b +end + + + function PART:Initialize() if pac.LocalPlayer == self:GetPlayerOwner() then pac.nocams = false From d830ea11b9cc958dd57bc1e7b670fa9ec7534ea8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 1 Mar 2024 20:47:36 -0500 Subject: [PATCH 173/300] add hover halo configs to menu bar and don't draw bulk select highlight if main hover halo color is "none", not only if the bulk hover mode is 0 --- lua/pac3/editor/client/menu_bar.lua | 45 +++++++++++++++++++++++++++++ lua/pac3/editor/client/parts.lua | 3 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 48a3a64a0..2dc387ca1 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -129,6 +129,51 @@ local function populate_options(menu) prop_pac_load_mode:AddOption(L"Queue parts if there's one or more groups", function() RunConsoleCommand("pac_autoload_preferred_prop", "2") end) menu:AddCVar(L"show parts IDs", "pac_show_uniqueid", "1", "0") + + local halos, pnlh = menu:AddSubMenu("configure hover halo highlights", function() end) + halos.GetDeleteSelf = function() return false end + pnlh:SetImage("icon16/shading.png") + halos:AddCVar(L"disable hover halos", "pac_hover_color", "none", "255 255 255") + halos:AddOption("object limit (performance)", function() + Derma_StringRequest("pac_hover_halo_limit ", "how many objects can halo at once?", GetConVarNumber("pac_hover_halo_limit"), function(val) RunConsoleCommand("pac_hover_halo_limit", val) end) + end):SetImage("icon16/sitemap.png") + halos:AddOption("pulse rate", function() + Derma_StringRequest("pac_hover_pulserate", "how fast to pulse?", GetConVarNumber("pac_hover_pulserate"), function(val) RunConsoleCommand("pac_hover_pulserate", val) end) + end):SetImage("icon16/time.png") + + halos:AddOption("How it reacts to bulk select", function() + local bulk_key_option_str = "bulk select key (current bind:" .. GetConVar("pac_bulk_select_key"):GetString() .. ")" + Derma_Query("What keys should trigger the hover halo on bulk select?","pac_bulk_select_halo_mode", + "passive",function() RunConsoleCommand("pac_bulk_select_halo_mode", 1) end, + bulk_key_option_str, function() RunConsoleCommand("pac_bulk_select_halo_mode", 2) end, + "control", function() RunConsoleCommand("pac_bulk_select_halo_mode", 3) end, + "shift", function() RunConsoleCommand("pac_bulk_select_halo_mode", 4) end + ) + end):SetImage("icon16/table_multiple.png") + halos:AddOption("Do not highlight bulk select", function() + RunConsoleCommand("pac_bulk_select_halo_mode", "0") + end):SetImage("icon16/table_delete.png") + + local halos_color, pnlhclr = halos:AddSubMenu("hover halo color", function() end) + pnlhclr:SetImage("icon16/color_wheel.png") + halos_color.GetDeleteSelf = function() return false end + halos_color:AddOption(L"none (disable halos)", function() RunConsoleCommand("pac_hover_color", "none") end):SetImage('icon16/page_white.png') + halos_color:AddOption(L"white (default)", function() RunConsoleCommand("pac_hover_color", "255 255 255") end):SetImage('icon16/bullet_white.png') + halos_color:AddOption(L"color (opens a menu)", function() + local clr_frame = vgui.Create("DFrame") + clr_frame:SetSize(300,200) clr_frame:Center() + local clr_pnl = vgui.Create("DColorMixer", clr_frame) + clr_frame:SetSize(300,200) clr_pnl:Dock(FILL) + clr_frame:RequestFocus() + function clr_pnl:ValueChanged(col) + hover_color:SetString(col.r .. " " .. col.g .. " " .. col.b) + end + end):SetImage('icon16/color_swatch.png') + halos_color:AddOption(L"ocean", function() RunConsoleCommand("pac_hover_color", "ocean") end):SetImage('icon16/bullet_blue.png') + halos_color:AddOption(L"funky", function() RunConsoleCommand("pac_hover_color", "funky") end):SetImage('icon16/color_wheel.png') + halos_color:AddOption(L"rave", function() RunConsoleCommand("pac_hover_color", "rave") end):SetImage('icon16/color_wheel.png') + halos_color:AddOption(L"rainbow", function() RunConsoleCommand("pac_hover_color", "rainbow") end):SetImage('icon16/rainbow.png') + local popups, pnlp = menu:AddSubMenu("configure editor popups", function() end) popups.GetDeleteSelf = function() return false end pnlp:SetImage("icon16/comment.png") diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index adec193ec..e334e2dd8 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -18,7 +18,7 @@ if not file.Exists("pac3_config/pac_editor_partmenu_layouts.txt", "DATA") then pace.operations_order = pace.operations_default end -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") +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") @@ -1398,6 +1398,7 @@ do -- menu --start halo hook pac.AddHook("PreDrawHalos", "BulkSelectHighlights", function() local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() + if hover_color:GetString() == "none" then return end if mode == 0 then return elseif mode == 1 then ThinkBulkHighlight() elseif mode == 2 then if input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) then ThinkBulkHighlight() end From 75ed8828cec2039036f0c83eb1bb3357cee93daf Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 1 Mar 2024 23:45:48 -0500 Subject: [PATCH 174/300] fixes for particle stick / align use correct part variable names apply particle angle post-stick and allow double sided particle to stick add some descriptions --- lua/pac3/core/client/parts/particles.lua | 26 +++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lua/pac3/core/client/parts/particles.lua b/lua/pac3/core/client/parts/particles.lua index 0ef3a2ce2..ad01513d8 100644 --- a/lua/pac3/core/client/parts/particles.lua +++ b/lua/pac3/core/client/parts/particles.lua @@ -30,8 +30,8 @@ BUILDER:StartStorableVars() BUILDER:GetSet("ParticleAngle", Angle(0,0,0)) BUILDER:GetSet("AddFrametimeLife", false) BUILDER:SetPropertyGroup("stick") - BUILDER:GetSet("AlignToSurface", true) - BUILDER:GetSet("StickToSurface", true) + BUILDER:GetSet("AlignToSurface", true, {description = "requires 3D set to true"}) + BUILDER:GetSet("StickToSurface", true, {description = "requires 3D set to true, and sliding set to false"}) BUILDER:GetSet("StickLifetime", 2) BUILDER:GetSet("StickStartSize", 20) BUILDER:GetSet("StickEndSize", 0) @@ -46,11 +46,11 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Color1", Vector(255, 255, 255), {editor_panel = "color"}) BUILDER:GetSet("RandomColor", false) BUILDER:GetSet("Lighting", true) - BUILDER:GetSet("3D", false) + BUILDER:GetSet("3D", false, {description = "The particles are oriented relative to the part instead of the viewer.\nYou might want to set zero angle to false if you use this."}) BUILDER:GetSet("DoubleSided", true) BUILDER:GetSet("DrawManual", false) BUILDER:SetPropertyGroup("rotation") - BUILDER:GetSet("ZeroAngle",true) + BUILDER:GetSet("ZeroAngle",true, {description = "A workaround for non-3D particles' roll with certain oriented textures. Forces 0,0,0 angles when the particle is emitted\nWith round textures you don't notice, but the same cannot be said of textures which need to be upright rather than having strangely tilted copies."}) BUILDER:GetSet("RandomRollSpeed", 0) BUILDER:GetSet("RollDelta", 0) BUILDER:GetSet("ParticleAngleVelocity", Vector(50, 50, 50)) @@ -100,7 +100,7 @@ local function StickCallback(particle, hitpos, normal) if particle.Align then local ang = normal:Angle() ang:RotateAroundAxis(normal, particle:GetAngles().y) - particle:SetAngles(ang) + particle:SetAngles(ang + particle.ParticleAngle + (particle.is_doubleside == true and Angle(180,0,0) or Angle(0,0,0))) end if particle.Stick then @@ -336,18 +336,24 @@ function PART:EmitParticles(pos, ang, real_ang) if self["3D"] then if not self.Sliding then - if i == 1 then + if i == 1 and not self.StickToSurface then particle:SetCollideCallback(RemoveCallback) else - particle:SetCollideCallback(StickCallback) + if i == 1 then + particle:SetCollideCallback(StickCallback) + else + particle.is_doubleside = true + particle:SetCollideCallback(StickCallback) + end end end particle:SetAngleVelocity(Angle(self.ParticleAngleVelocity.x, self.ParticleAngleVelocity.y, self.ParticleAngleVelocity.z)) - particle.Align = self.Align - particle.Stick = self.Stick - particle.StickLifeTime = self.StickLifeTime + particle.ParticleAngle = self.ParticleAngle + particle.Align = self.AlignToSurface + particle.Stick = self.StickToSurface + particle.StickLifeTime = self.StickLifetime particle.StickStartSize = self.StickStartSize particle.StickEndSize = self.StickEndSize particle.StickStartAlpha = self.StickStartAlpha From b6faf91869330437dfae64e9bc7108cd98d863fb Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 5 Mar 2024 21:19:49 -0500 Subject: [PATCH 175/300] actually add animation rate in player config it already had an EntityField link, but we needed a part variable visible to the user --- lua/pac3/core/client/parts/player_config.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/client/parts/player_config.lua b/lua/pac3/core/client/parts/player_config.lua index 10a121e38..cdcc88508 100644 --- a/lua/pac3/core/client/parts/player_config.lua +++ b/lua/pac3/core/client/parts/player_config.lua @@ -29,6 +29,7 @@ BUILDER:SetPropertyGroup("generic") BUILDER:SetPropertyGroup("behavior") BUILDER:GetSet("MuteFootsteps", false) + BUILDER:GetSet("AnimationRate", 1) BUILDER:SetPropertyGroup("death") BUILDER:GetSet("FallApartOnDeath", false) From 95ca54e1f19e0067428de29b939988d461c49ace Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Mar 2024 15:25:56 -0500 Subject: [PATCH 176/300] small fix create the pac_command_events table when creating a command event also hide some editor-related code behind whether owner is localplayer --- lua/pac3/core/client/parts/event.lua | 41 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index b9ec9d213..986a08105 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -119,18 +119,23 @@ function PART:SetEvent(event) local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) - if not self.Events[event] then --invalid event? try a command event - if GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then - timer.Simple(0.2, function() - if not self.pace_properties or self ~= pace.current_part then return end - --now we'll use event as a command name - self:SetEvent("command") - self.pace_properties["Event"]:SetValue("command") - self:SetArguments(event .. "@@0") - self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") - pace.PopulateProperties(self) - end) - return + local owner = self:GetPlayerOwner() + + if owner == pac.LocalPlayer then + if event == "command" then owner.pac_command_events = owner.pac_command_events or {} end + if not self.Events[event] then --invalid event? try a command event + if GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then + timer.Simple(0.2, function() + if not self.pace_properties or self ~= pace.current_part then return end + --now we'll use event as a command name + self:SetEvent("command") + self.pace_properties["Event"]:SetValue("command") + self:SetArguments(event .. "@@0") + self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") + pace.PopulateProperties(self) + end) + return + end end end @@ -142,14 +147,16 @@ function PART:SetEvent(event) self:fix_event_operator() self:fix_args() - pace.changed_event = self --a reference to make it refresh the popup label panel - pace.changed_event_time = CurTime() + if owner == pac.LocalPlayer then + pace.changed_event = self --a reference to make it refresh the popup label panel + pace.changed_event_time = CurTime() - if self == pace.current_part and GetConVar("pac_copilot_make_popup_when_selecting_event"):GetBool() then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit + if self == pace.current_part and GetConVar("pac_copilot_make_popup_when_selecting_event"):GetBool() then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit - self:GetDynamicProperties(reset) - if not GetConVar("pac_editor_remember_divider_height"):GetBool() and IsValid(pace.Editor) then pace.Editor.div:SetTopHeight(ScrH() - 520) end + self:GetDynamicProperties(reset) + if not GetConVar("pac_editor_remember_divider_height"):GetBool() and IsValid(pace.Editor) then pace.Editor.div:SetTopHeight(ScrH() - 520) end + end end function PART:Initialize() From 3593b68b6ec4cd544ebbcd9afeb352ba93d3c163 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 9 Mar 2024 16:46:40 -0500 Subject: [PATCH 177/300] fix proxy extra expressions index collisions --- lua/pac3/core/client/parts/proxy.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 183d3728f..e51e7746f 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -493,14 +493,14 @@ for i=1,5,1 do PART.Inputs["var" .. i] = function(self, uid1) if not uid1 then return self["feedback_extra" .. i] - elseif self["extra_referred_part"..i] then --a thing to skip part searching when we found the part - return self["extra_referred_part"..i]["feedback_extra" .. i] + elseif self.last_extra_feedbacks[i][uid1] then --a thing to skip part searching when we found the part + return self.last_extra_feedbacks[i][uid1]["feedback_extra" .. i] or 0 else local owner = self:GetPlayerOwner() local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end if IsValid(PartA) and PartA.ClassName == "proxy" then - self["extra_referred_part"..i] = PartA + self.last_extra_feedbacks[i][uid1] = PartA end end return 0 @@ -1282,6 +1282,13 @@ function PART:SetExpression(str, slot) self["Extra" .. slot .. "Func"] = nil self.has_extras = true end + self.last_extra_feedbacks = { + [1] = {}, + [2] = {}, + [3] = {}, + [4] = {}, + [5] = {}, + } if str and str ~= "" then local lib = {} From 152d670223cdbe26f31fa73566d6d41e5e40ab24 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 24 Mar 2024 14:53:39 -0400 Subject: [PATCH 178/300] implement health modifier id for damage multiplier for controlling one named "type" of damage multiplier involving multiple parts (instead of proxies) when multiple parts can be active at a time, it's probably simpler to have the option to cleanup like this instead of making a complex event setup to force all the others to hide for the reset on hide --- lua/pac3/extra/shared/net_combat.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index f7120432a..048668345 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -461,7 +461,16 @@ if SERVER then local function AddDamageScale(ply, id,scale, part_uid) ply.pac_damage_scalings = ply.pac_damage_scalings or {} - ply.pac_damage_scalings[part_uid] = {scale = scale, id = id, uid = part_uid} + if id == "" then --no mod id = part uid mode, don't overwrite another part + ply.pac_damage_scalings[part_uid] = {scale = scale, id = id, uid = part_uid} + else --mod id = try to remove competing parts whose multipliers have the same mod id + for existing_uid,tbl in pairs(ply.pac_damage_scalings) do + if tbl.id == id then + ply.pac_damage_scalings[existing_uid] = nil + end + end + ply.pac_damage_scalings[part_uid] = {scale = scale, id = id, uid = part_uid} + end end local function FixMaxHealths(ply) From 621e1ab3001c58be69e78e8bf67f94e272681ace Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 24 Mar 2024 16:15:17 -0400 Subject: [PATCH 179/300] apply wearing bypass fix to more parts preventing lock teleportations and OnShow command activations during weartime --- lua/pac3/core/client/parts/command.lua | 1 + lua/pac3/core/client/parts/lock.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index 8e320a7b3..cf8ee4a95 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -24,6 +24,7 @@ function PART:OnWorn() end function PART:OnShow(from_rendering) + if not pace.still_loading_wearing then return end if not from_rendering and self:GetExecuteOnShow() then timer.Simple(0, function() if self.Hide or self:IsHidden() then return end diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 7fbee65bc..7fe0b53eb 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -327,6 +327,7 @@ function PART:OnShow() end) if self.Mode == "Teleport" then if not GetConVar('pac_sv_lock_teleport'):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then return end + if pace.still_loading_wearing then return end self.target_ent = nil local ang_yaw_only = self:GetWorldAngles() From de7e4bcf0c2d6fef553fcc32a19ecc0f467b2201 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 24 Mar 2024 17:03:05 -0400 Subject: [PATCH 180/300] hotfix on wearing bypass hack fix bad logic --- 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 cf8ee4a95..f4852a878 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -24,7 +24,7 @@ function PART:OnWorn() end function PART:OnShow(from_rendering) - if not pace.still_loading_wearing then return end + if pace.still_loading_wearing then return end if not from_rendering and self:GetExecuteOnShow() then timer.Simple(0, function() if self.Hide or self:IsHidden() then return end From cf6019c9b5e2d9c3a710ba138591453d5910f292 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 25 Mar 2024 23:40:15 -0400 Subject: [PATCH 181/300] fixing drgbase npc compatibility --- lua/pac3/extra/shared/net_combat.lua | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 048668345..6e04e589a 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -143,7 +143,7 @@ if SERVER then local function NPCDispositionAllowsIt(ply, ent) - if not (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) or not ent.Disposition then return true end + if not (ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity) or not ent.Disposition then return true end if not friendly_NPC_preferences[ply] then return true end @@ -162,7 +162,7 @@ if SERVER then end local function NPCDispositionIsFilteredOut(ply, ent, friendly, neutral, hostile) - if not (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) or not ent.Disposition then return false end + if not (ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity) or not ent.Disposition then return false end local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)] if relationship_friendliness == 0 then --it hostile @@ -864,7 +864,7 @@ if SERVER then --1.player hurt self if asked local is_player = ent:IsPlayer() local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) - local is_npc = ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity + local is_npc = ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity if (tbl.AffectSelf) and ent == inflictor then canhit = true @@ -910,7 +910,7 @@ if SERVER then end local function IsLiving(ent) --players and NPCs - return ent:IsPlayer() or (ent:IsNPC() or string.find(ent:GetClass(), "npc") or ent.IsVJBaseSNPC or ent.IsDRGEntity) + return ent:IsPlayer() or (ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity) end --final action to apply the DamageInfo @@ -1171,7 +1171,7 @@ if SERVER then local owner = Try_CPPIGetOwner(ent) local is_player = ent:IsPlayer() local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner()))) - local is_npc = ent.IsVJBaseSNPC or ent.IsDRGEntity or string.find(ent:GetClass(), "npc") or ent:IsNPC() + local is_npc = ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) @@ -1185,7 +1185,7 @@ if SERVER then local is_phys = true if ent_getphysobj ~= nil then phys_ent = ent_getphysobj - if (string.find(ent:GetClass(), "npc") ~= nil) then + if is_npc then phys_ent = ent end else @@ -1276,7 +1276,7 @@ if SERVER then end end if is_phys and tbl.AccountMass then - if not (string.find(ent:GetClass(), "npc") ~= nil) then + if not is_npc then addvel = addvel * (1 / math.max(mass,0.1)) else addvel = addvel @@ -1340,11 +1340,20 @@ if SERVER then elseif is_npc then if tbl.NPC then if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then + if ent.IsDrGEntity then --welcome to episode 40 of intercompatibility hackery + phys_ent = ent.loco + local jumpHeight = ent.loco:GetJumpHeight() + ent.loco:SetJumpHeight(1) + ent.loco:Jump() + ent.loco:SetJumpHeight(jumpHeight) + end if IsValid(phys_ent) and phys_ent:GetVelocity():Length() > 500 then local vec = oldvel + addvel local clamp_vec = vec:GetNormalized()*500 ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer - else ent:SetVelocity((oldvel * final_damping) + addvel) end + else + ent:SetVelocity((oldvel * final_damping) + addvel) + end end end @@ -2059,6 +2068,9 @@ if SERVER then targ_ent.lock_state_applied = true end ApplyLockState(targ_ent, true, no_collide) + if targ_ent.IsDrGEntity then + targ_ent.loco:SetVelocity(Vector(0,0,0)) --counter gravity speed buildup + end if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end --@@note lock assignation! IMPORTANT From 5c34b12e9e88f7e0f460cc3812c00d4869ee4c24 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 29 Mar 2024 19:03:06 -0400 Subject: [PATCH 182/300] another revision for entity classification --- lua/pac3/extra/shared/net_combat.lua | 30 +++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 6e04e589a..c781f7691 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -72,6 +72,7 @@ if SERVER then ["prop_physics"] = true, ["weapon_striderbuster"] = true, ["item_item_crate"] = true, + ["npc_satchel"] = true, ["func_breakable_surf"] = true, ["func_breakable"] = true, ["physics_cannister"] = true @@ -140,10 +141,13 @@ if SERVER then [4] = 1, --D_NU Neutral } + local function Is_NPC(ent) + return ent:IsNPC() or ent:IsNextBot() or ent.IsDrGEntity or ent.IsVJBaseSNPC + end local function NPCDispositionAllowsIt(ply, ent) - if not (ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity) or not ent.Disposition then return true end + if not Is_NPC(ent) or not ent.Disposition then return true end if not friendly_NPC_preferences[ply] then return true end @@ -162,7 +166,7 @@ if SERVER then end local function NPCDispositionIsFilteredOut(ply, ent, friendly, neutral, hostile) - if not (ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity) or not ent.Disposition then return false end + if not Is_NPC(ent) or not ent.Disposition then return false end local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)] if relationship_friendliness == 0 then --it hostile @@ -289,10 +293,8 @@ if SERVER then local function IsPossibleContraptionEntity(ent) if not IsValid(ent) then return false end local b = (string.find(ent:GetClass(), "phys") ~= nil - or string.find(ent:GetClass(), "anchor") ~= nil - or string.find(ent:GetClass(), "rope") ~= nil - or string.find(ent:GetClass(), "gmod") ~= nil) - --print("entity", ent, "contraption?", b) + or string.find(ent:GetClass(), "gmod") ~= nil + or ent:IsConstraint()) return b end @@ -746,8 +748,9 @@ if SERVER then local ply_count = 0 local ply_prog_count = 0 for i,v in pairs(ents_hits) do - if not (v:IsPlayer() or v:IsNPC() or string.find(v:GetClass(), "npc_")) and not tbl.PointEntities then ents_hits[i] = nil end + if not (v:IsPlayer() or Is_NPC(v)) and not tbl.PointEntities then ents_hits[i] = nil end if v.CPPICanDamage and not v:CPPICanDamage(ply) then ents_hits[i] = nil end --CPPI check on the player + if v:IsConstraint() then ents_hits[i] = nil end if not NPCDispositionAllowsIt(ply, v) then ents_hits[i] = nil end if NPCDispositionIsFilteredOut(ply,v, tbl.FilterFriendlies, tbl.FilterNeutrals, tbl.FilterHostiles) then ents_hits[i] = nil end @@ -863,8 +866,8 @@ if SERVER then --second pass: the damagezone's settings --1.player hurt self if asked local is_player = ent:IsPlayer() - local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or ent:IsWeapon()) - local is_npc = ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity + local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_")) + local is_npc = Is_NPC(ent) if (tbl.AffectSelf) and ent == inflictor then canhit = true @@ -910,7 +913,7 @@ if SERVER then end local function IsLiving(ent) --players and NPCs - return ent:IsPlayer() or (ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity) + return ent:IsPlayer() or Is_NPC(ent) end --final action to apply the DamageInfo @@ -1063,7 +1066,6 @@ if SERVER then --look through each entity for _,ent in pairs(ents_hits) do - local canhit = DMGAllowed(ent) local oldhp = ent:Health() if canhit then @@ -1160,10 +1162,10 @@ if SERVER then for i,v in pairs(ents_hits) do if v.CPPICanPickup and not v:CPPICanPickup(ply) then ents_hits[i] = nil end if v.CPPICanPunt and not v:CPPICanPunt(ply) then ents_hits[i] = nil end - if pre_excluded_ent_classes[v:GetClass()] or (v:IsNPC() and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectSelf)) then ents_hits[i] = nil + if v:IsConstraint() then ents_hits[i] = nil end + if pre_excluded_ent_classes[v:GetClass()] or (Is_NPC(v) and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectSelf)) then ents_hits[i] = nil else ent_count = ent_count + 1 end end - if TooManyEnts(ent_count) and not (tbl.AffectSelf and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end for _,ent in pairs(ents_hits) do local phys_ent @@ -1171,7 +1173,7 @@ if SERVER then local owner = Try_CPPIGetOwner(ent) local is_player = ent:IsPlayer() local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner()))) - local is_npc = ent:IsNPC() or (string.find(ent:GetClass(), "npc") ~= nil) or ent.IsVJBaseSNPC or ent.IsDrGEntity + local is_npc = Is_NPC(ent) if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) From 151602213df133e61347a0eb14eb9b8ab4ae5d59 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 31 Mar 2024 16:54:32 -0400 Subject: [PATCH 183/300] clear error when valid part class --- lua/pac3/core/client/parts/event.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 986a08105..79945b4bf 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2166,6 +2166,7 @@ PART.OldEvents = { return true end end + self:SetError() else self:SetError("You set a UID that's not a damage zone!") end @@ -2213,6 +2214,7 @@ PART.OldEvents = { return true end end + self:SetError() else self:SetError("You set a UID that's not a damage zone!") end @@ -2263,6 +2265,7 @@ PART.OldEvents = { if part.grabbing then return IsValid(part.target_ent) end + self:SetError() else self:SetError("You set a UID that's not a lock part!") end @@ -4230,4 +4233,4 @@ net.Receive("pac_update_healthbars", function() end end -end) \ No newline at end of file +end) From 1d338cae824281d80658977ec4b58116a8530993 Mon Sep 17 00:00:00 2001 From: Denneisk <20892685+Denneisk@users.noreply.github.com> Date: Sun, 14 Apr 2024 18:41:45 -0400 Subject: [PATCH 184/300] Add size limit to E2 functions (#1348) * Add byte limit for sending data * Combine to single networkstring Decrease limit to 2048 * Change limit to be per second Make messages unreliable * Fix ratelimit used int for a float value * Update convar descriptions --- .../gmod_wire_expression2/core/custom/pac.lua | 72 ++++++++++++------- .../client/wire_expression_extension.lua | 14 +++- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/lua/entities/gmod_wire_expression2/core/custom/pac.lua b/lua/entities/gmod_wire_expression2/core/custom/pac.lua index b9890f78e..6f184a9a0 100644 --- a/lua/entities/gmod_wire_expression2/core/custom/pac.lua +++ b/lua/entities/gmod_wire_expression2/core/custom/pac.lua @@ -1,69 +1,91 @@ E2Lib.RegisterExtension("pac", true) -util.AddNetworkString("pac_e2_setkeyvalue_str") -util.AddNetworkString("pac_e2_setkeyvalue_num") -util.AddNetworkString("pac_e2_setkeyvalue_vec") -util.AddNetworkString("pac_e2_setkeyvalue_ang") +util.AddNetworkString("pac_e2_setkeyvalue") -local enabledConvar = CreateConVar("pac_e2_ratelimit_enable", "1", {FCVAR_ARCHIVE}, "If the e2 ratelimit should be enabled.", 0, 1) -local rate = CreateConVar("pac_e2_ratelimit_refill", "0.025", {FCVAR_ARCHIVE}, "The speed at which the ratelimit buffer refills.", 0, 1000) -local buffer = CreateConVar("pac_e2_ratelimit_buffer", "300", {FCVAR_ARCHIVE}, "How large the ratelimit buffer should be.", 0, 1000) +local enabledConvar = CreateConVar("pac_e2_ratelimit_enable", "1", FCVAR_ARCHIVE, "If the e2 ratelimit should be enabled.", 0, 1) +local rate = CreateConVar("pac_e2_ratelimit_refill", "0.025", FCVAR_ARCHIVE, "The amount at which the ratelimit buffer refills per second.", 0, 1000) +local buffer = CreateConVar("pac_e2_ratelimit_buffer", "300", FCVAR_ARCHIVE, "How many PAC E2 operations are allowed before the rate limit is hit.", 0, 1000) +local bytes = CreateConVar("pac_e2_bytelimit", "2048", FCVAR_ARCHIVE, "Limit number of bytes sent per second for PAC E2 messages.", 0, 65532) + +local byteLimits = WireLib.RegisterPlayerTable() +local function canRunFunction(self, g, k, v) + local byteLimit = byteLimits[self.player] + local ct = CurTime() + if not byteLimit then + byteLimit = { ct + 1, 0 } + byteLimits[self.player] = byteLimit + end + + local lim = #g + #k + #v + if ct < byteLimit[1] then + lim = lim + byteLimit[2] + else + byteLimit[1] = ct + 1 + end + byteLimit[2] = lim + + if lim >= bytes:GetInt() then return self:throw("pac3 e2 byte limit exceeded", false) end -local function canRunFunction(self) if not enabledConvar:GetBool() then return true end - local allowed = pac.RatelimitPlayer(self.player, "e2_extension", buffer:GetInt(), rate:GetInt()) + local allowed = pac.RatelimitPlayer(self.player, "e2_extension", buffer:GetInt(), rate:GetFloat()) if not allowed then - E2Lib.raiseException("pac3 e2 ratelimit exceeded") - return false + return self:throw("pac3 e2 ratelimit exceeded", false) end return true end +--- Domain-specific type IDs for networking E2 keyvalues +---@alias pac.E2.NetID +---| 0 # String +---| 1 # Number +---| 2 # Vector +---| 3 # Angle + e2function void pacSetKeyValue(entity owner, string global_id, string key, string value) - if not canRunFunction(self) then return end - net.Start("pac_e2_setkeyvalue_str") + if not canRunFunction(self, global_id, key, value) then return end + net.Start("pac_e2_setkeyvalue", true) net.WriteEntity(self.player) net.WriteEntity(owner) net.WriteString(global_id) net.WriteString(key) - + net.WriteUInt(0, 2) net.WriteString(value) net.Broadcast() end e2function void pacSetKeyValue(entity owner, string global_id, string key, number value) - if not canRunFunction(self) then return end - net.Start("pac_e2_setkeyvalue_num") + if not canRunFunction(self, global_id, key, "nmbr") then return end -- Workaround because I don't want to add cases for each type, 4 bytes + net.Start("pac_e2_setkeyvalue", true) net.WriteEntity(self.player) net.WriteEntity(owner) net.WriteString(global_id) net.WriteString(key) - + net.WriteUInt(1, 2) net.WriteFloat(value) net.Broadcast() end e2function void pacSetKeyValue(entity owner, string global_id, string key, vector value) - if not canRunFunction(self) then return end - net.Start("pac_e2_setkeyvalue_vec") + if not canRunFunction(self, global_id, key, "vctrvctrvctr") then return end -- 4 bytes, 3 times + net.Start("pac_e2_setkeyvalue", true) net.WriteEntity(self.player) net.WriteEntity(owner) net.WriteString(global_id) net.WriteString(key) - - net.WriteVector(Vector(value[1], value[2], value[3])) + net.WriteUInt(2, 2) + net.WriteVector(value) net.Broadcast() end e2function void pacSetKeyValue(entity owner, string global_id, string key, angle value) - if not canRunFunction(self) then return end - net.Start("pac_e2_setkeyvalue_ang") + if not canRunFunction(self, global_id, key, "vctrvctrvctr") then return end + net.Start("pac_e2_setkeyvalue", true) net.WriteEntity(self.player) net.WriteEntity(owner) net.WriteString(global_id) net.WriteString(key) - - net.WriteAngle(Angle(value[1], value[2], value[3])) + net.WriteUInt(3, 2) + net.WriteAngle(value) net.Broadcast() end diff --git a/lua/pac3/extra/client/wire_expression_extension.lua b/lua/pac3/extra/client/wire_expression_extension.lua index cd01765da..004722a1d 100644 --- a/lua/pac3/extra/client/wire_expression_extension.lua +++ b/lua/pac3/extra/client/wire_expression_extension.lua @@ -31,14 +31,24 @@ local function SetKeyValue(ply, ent, unique_id, key, val) end end -net.Receive("pac_e2_setkeyvalue_str", function() +net.Receive("pac_e2_setkeyvalue", function() local ply = net.ReadEntity() if ply:IsValid() then local ent = net.ReadEntity() local id = net.ReadString() local key = net.ReadString() - local val = net.ReadString() + local type = net.ReadUInt(2) ---@type pac.E2.NetID + local val + if type == 0 then + val = net.ReadString() + elseif type == 1 then + val = net.ReadFloat() + elseif type == 2 then + val = net.ReadVector() + else + val = net.ReadAngle() + end SetKeyValue(ply, ent, id, key, val) end From df0aaf4df6580d5bb822497ad3e20d9ea9b10e74 Mon Sep 17 00:00:00 2001 From: techbot Date: Tue, 4 Jun 2024 21:09:55 +0200 Subject: [PATCH 185/300] fix broken animation timeline selection --- lua/pac3/editor/client/animation_timeline.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lua/pac3/editor/client/animation_timeline.lua b/lua/pac3/editor/client/animation_timeline.lua index 6c7a791a3..687feabb7 100644 --- a/lua/pac3/editor/client/animation_timeline.lua +++ b/lua/pac3/editor/client/animation_timeline.lua @@ -1059,10 +1059,8 @@ do self:Remove() local count = #timeline.frame.keyframe_scroll:GetCanvas():GetChildren() - local offset = remove_i >= count and count - 1 or remove_i - timer.Simple(0, function() - timeline.SelectKeyframe(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()[offset]) - end) + local offset = remove_i >= count and count - 1 or remove_i + 1 + timeline.SelectKeyframe(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()[offset]) end):SetImage("icon16/page_delete.png") menu:AddOption(L"set easing style", function() From 67cafaceb459b6a6af9b5ca3f66ae3d0c8a73c42 Mon Sep 17 00:00:00 2001 From: techbot Date: Tue, 4 Jun 2024 22:17:23 +0200 Subject: [PATCH 186/300] fix error if target is not a player --- lua/pac3/core/client/parts/event.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 79945b4bf..7ffd77c7b 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1918,9 +1918,10 @@ PART.OldEvents = { is_sitting = { operator_type = "none", callback = function(self, ent) - if not ent:GetVehicle() then return false end - if ent.GetSitting then return (IsValid(ent:GetVehicle()) or ent:GetSitting()) and ent:GetVehicle():GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" end --sit anywhere script - return IsValid(ent:GetVehicle()) and ent:GetVehicle():GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" --no prison pod! + if not ent:IsPlayer() then return false end + local vehicle = ent:GetVehicle() + if ent.GetSitting then return ent:GetSitting() end --sit anywhere script + return IsValid(vehicle) and vehicle:GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" --no prison pod! end }, From 4db80ae27a85a279c6bed88da7230aa8b9f1f069 Mon Sep 17 00:00:00 2001 From: textstack <46581273+textstack@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:21:54 -0700 Subject: [PATCH 187/300] prevent healing functions from removing overheal (#1356) --- lua/pac3/extra/shared/net_combat.lua | 93 +++++++++++++++------------ lua/pac3/extra/shared/projectiles.lua | 7 +- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index c781f7691..a81418816 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -77,7 +77,7 @@ if SERVER then ["func_breakable"] = true, ["physics_cannister"] = true } - + local physics_point_ent_classes = { ["prop_physics"] = true, ["prop_physics_multiplayer"] = true, @@ -88,11 +88,11 @@ if SERVER then ["func_breakable"] = true, ["physics_cannister"] = true } - + local contraption_classes = { ["prop_physics"] = true, } - + local pre_excluded_ent_classes = { ["info_player_start"] = true, ["aoc_spawnpoint"] = true, @@ -120,8 +120,8 @@ if SERVER then ["env_soundscape_proxy"] = true, ["gmod_hands"] = true, } - - + + local grab_consents = {} local damage_zone_consents = {} local force_consents = {} @@ -129,8 +129,8 @@ if SERVER then local calcview_consents = {} local active_force_ids = {} local active_grabbed_ents = {} - - + + local friendly_NPC_preferences = {} --we compare player's preference with the disposition's overall "friendliness". if relationship is more friendly than the preference, do not affect local disposition_friendliness_level = { @@ -140,11 +140,11 @@ if SERVER then [3] = 2, --D_LI Like [4] = 1, --D_NU Neutral } - + local function Is_NPC(ent) return ent:IsNPC() or ent:IsNextBot() or ent.IsDrGEntity or ent.IsVJBaseSNPC end - + local function NPCDispositionAllowsIt(ply, ent) if not Is_NPC(ent) or not ent.Disposition then return true end @@ -177,7 +177,7 @@ if SERVER then return not friendly end end - + local damage_types = { generic = 0, --generic damage crush = 1, --caused by physics interaction @@ -210,19 +210,19 @@ if SERVER then radiation = 262144, --radiation removenoragdoll = 4194304, --don't create a ragdoll on death slowburn = 2097152, -- - + fire = -1, -- ent:Ignite(5) - + -- env_entity_dissolver dissolve_energy = 0, dissolve_heavy_electrical = 1, dissolve_light_electrical = 2, dissolve_core_effect = 3, - + heal = -1, armor = -1, } - + local function CountNetMessage(ply) local stime = SysTime() local ms_basis = enforce_netrate:GetInt()/1000 @@ -948,32 +948,37 @@ if SERVER then end + --this may benefit from some flattening treatment, lotta pyramids over here if tbl.DamageType == "heal" then - if tbl.ReverseDoNotKill then --don't heal if health is below critical - if ent:Health() > tbl.CriticalHealth then --default behavior - ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) - end --else do nothing - else - if tbl.DoNotKill then --stop healing at the critical health - if ent:Health() < tbl.CriticalHealth then - ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.min(tbl.CriticalHealth, ent:GetMaxHealth()))) - end --else do nothing, we're already above critical + if ent:Health() < ent:GetMaxHealth() then + if tbl.ReverseDoNotKill then --don't heal if health is below critical + if ent:Health() > tbl.CriticalHealth then --default behavior + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) + end --else do nothing else - ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) + if tbl.DoNotKill then --stop healing at the critical health + if ent:Health() < tbl.CriticalHealth then + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.min(tbl.CriticalHealth, ent:GetMaxHealth()))) + end --else do nothing, we're already above critical + else + ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth()))) + end end end elseif tbl.DamageType == "armor" then - if tbl.ReverseDoNotKill then --don't heal if armor is below critical - if ent:Armor() > tbl.CriticalHealth then --default behavior - ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) - end --else do nothing - else - if tbl.DoNotKill then --stop healing at the critical health - if ent:Armor() < tbl.CriticalHealth then - ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.min(tbl.CriticalHealth, ent:GetMaxArmor()))) - end --else do nothing, we're already above critical + if ent:Armor() < ent:GetMaxArmor() then + if tbl.ReverseDoNotKill then --don't heal if armor is below critical + if ent:Armor() > tbl.CriticalHealth then --default behavior + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) + end --else do nothing else - ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) + if tbl.DoNotKill then --stop healing at the critical health + if ent:Armor() < tbl.CriticalHealth then + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.min(tbl.CriticalHealth, ent:GetMaxArmor()))) + end --else do nothing, we're already above critical + else + ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor()))) + end end end else @@ -2256,17 +2261,25 @@ if SERVER then if bulletinfo.dmgtype_str == "heal" then dmg:SetDamageType(0) - ent:SetHealth(math.min(ent:Health() + fraction * dmg:GetDamage(), math.max(ent:Health(), ent:GetMaxHealth()))) + + if ent:Health() < ent:GetMaxHealth() then + ent:SetHealth(math.min(ent:Health() + fraction * dmg:GetDamage(), math.max(ent:Health(), ent:GetMaxHealth()))) + end + dmg:SetDamage(0) return elseif bulletinfo.dmgtype_str == "armor" then dmg:SetDamageType(0) - ent:SetArmor(math.min(ent:Armor() + fraction * dmg:GetDamage(), math.max(ent:Armor(), ent:GetMaxArmor()))) + + if ent:Armor() < ent:GetMaxArmor() then + ent:SetArmor(math.min(ent:Armor() + fraction * dmg:GetDamage(), math.max(ent:Armor(), ent:GetMaxArmor()))) + end + dmg:SetDamage(0) return end if bulletinfo.DamageFalloff and trc.Hit and IsValid(trc.Entity) then - if not bulletinfo.dmgtype_str == "heal" and not bulletinfo.dmgtype_str == "armor" then + if bulletinfo.dmgtype_str ~= "heal" and bulletinfo.dmgtype_str ~= "armor" then dmg:SetDamage(fraction * dmg:GetDamage()) end end @@ -2439,7 +2452,7 @@ if SERVER then end ReinitializeCombatReceivers() end - + end) util.AddNetworkString("pac_request_blocked_parts_reinitialization") @@ -2457,12 +2470,12 @@ if SERVER then net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES) net.Send(ply) end) - + end if CLIENT then killicon.Add( "pac_bullet_emitter", "icon16/user_gray.png", Color(255,255,255) ) - + concommand.Add("pac_sv_reinitialize_missing_combat_parts_remotely", function(ply) if IsValid(ply) then if not ply:IsAdmin() then diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index ce67038cb..a4e46fa1b 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -358,7 +358,7 @@ do -- projectile entity if self.part_data.DamageType == "heal" then if damage_radius > 0 then for _, ent in ipairs(ents.FindInSphere(data.HitPos, damage_radius)) do - if ent ~= ply or self.part_data.CollideWithOwner then + if (ent ~= ply or self.part_data.CollideWithOwner) and ent:Health() < ent:GetMaxHealth() then ent:SetHealth(math.min(ent:Health() + self.part_data.Damage, ent:GetMaxHealth())) end end @@ -369,8 +369,9 @@ do -- projectile entity if damage_radius > 0 then for _, ent in ipairs(ents.FindInSphere(data.HitPos, damage_radius)) do if ent.SetArmor and ent.Armor then - if ent ~= ply or self.part_data.CollideWithOwner then - ent:SetArmor(math.min(ent:Armor() + self.part_data.Damage, ent.GetMaxArmor and ent:GetMaxArmor() or 100)) + local maxArmor = ent.GetMaxArmor and ent:GetMaxArmor() or 100 + if (ent ~= ply or self.part_data.CollideWithOwner) and ent:Armor() < maxArmor then + ent:SetArmor(math.min(ent:Armor() + self.part_data.Damage, maxArmor)) end end end From 42796bcb83a00b7994d8189b07818b3c8bbea59e Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 4 Jun 2024 20:03:05 -0400 Subject: [PATCH 188/300] wires hack fix FindMetaTable("Color") call seems broken, the result is pretty much empty get the color metatable via the color object we created instead --- lua/pac3/editor/client/wires.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/pac3/editor/client/wires.lua b/lua/pac3/editor/client/wires.lua index 517e49f59..c5fcf1aa1 100644 --- a/lua/pac3/editor/client/wires.lua +++ b/lua/pac3/editor/client/wires.lua @@ -16,8 +16,11 @@ local math_min = math.min local math_cos = math.cos local math_pi = math.pi +local black = Color(0,0,0,100) +local vector_add = Vector(0, 2.5, 0) + local mtVector = FindMetaTable("Vector") -local mtColor = FindMetaTable("Color") +local mtColor = getmetatable(black) local render_startBeam = render.StartBeam local render_endBeam = render.EndBeam local render_addBeam = render.AddBeam @@ -25,9 +28,6 @@ local setVecUnpacked = mtVector.SetUnpacked local setColUnpacked = mtColor.SetUnpacked local colUnpack = mtColor.Unpack -local black = Color(0,0,0,100) -local vector_add = Vector(0, 2.5, 0) - local function DrawHermite(width, x0,y0,x1,y1,c0,c1,alpha,samples) --[[if x0 < 0 and x1 < 0 then return end From 9a9971ff8b168e72309eb2a78b2888d8b2f90983 Mon Sep 17 00:00:00 2001 From: thegrb93 Date: Sun, 9 Jun 2024 17:33:15 -0400 Subject: [PATCH 189/300] Rewrote netstream (#1361) * Rewrote netstream * Only add timer when queue empty * Only update activitytimeout if queue is empty too * Move activity timeout reset into successful write * Move net read before return --- lua/autorun/netstream.lua | 762 +++++++++++++++++++------------------- 1 file changed, 386 insertions(+), 376 deletions(-) diff --git a/lua/autorun/netstream.lua b/lua/autorun/netstream.lua index bbbb2eace..632c71b94 100644 --- a/lua/autorun/netstream.lua +++ b/lua/autorun/netstream.lua @@ -1,376 +1,386 @@ ---A net extension which allows sending large streams of data without overflowing the reliable channel ---Keep it in lua/autorun so it will be shared between addons -AddCSLuaFile() - -net.Stream = {} -net.Stream.ReadStreamQueues = {} --This holds a read stream for each player, or one read stream for the server if running on the CLIENT -net.Stream.WriteStreams = {} --This holds the write streams -net.Stream.SendSize = 20000 --This is the maximum size of each stream to send -net.Stream.Timeout = 30 --How long the data should exist in the store without being used before being destroyed -net.Stream.MaxServerReadStreams = 128 --The maximum number of keep-alives to have queued. This should prevent naughty players from flooding the network with keep-alive messages. -net.Stream.MaxServerChunks = 3200 --Maximum number of pieces the stream can send to the server. 64 MB -net.Stream.MaxTries = 3 --Maximum times the client may retry downloading the whole data -net.Stream.MaxKeepalive = 15 --Maximum times the client may request data stay live - -net.Stream.ReadStream = {} ---Send the data sender a request for data -function net.Stream.ReadStream:Request() - if self.downloads == net.Stream.MaxTries * self.numchunks then self:Remove() return end - self.downloads = self.downloads + 1 - -- print("Requesting",self.identifier,false,false,#self.chunks) - - net.Start("NetStreamRequest") - net.WriteUInt(self.identifier, 32) - net.WriteBit(false) - net.WriteBit(false) - net.WriteUInt(#self.chunks, 32) - if CLIENT then net.SendToServer() else net.Send(self.player) end - - timer.Create("NetStreamReadTimeout" .. self.identifier, net.Stream.Timeout/2, 1, function() self:Request() end) -end - ---Received data so process it -function net.Stream.ReadStream:Read(size) - timer.Remove("NetStreamReadTimeout" .. self.identifier) - - local progress = net.ReadUInt(32) - if self.chunks[progress] then return end - - local crc = net.ReadString() - local data = net.ReadData(size) - - if crc == util.CRC(data) then - self.chunks[progress] = data - else - pac.Message("net.Stream.ReadStream:Read(): hash received and hash of chunk do not match match") - end - - if #self.chunks == self.numchunks then - self.returndata = table.concat(self.chunks) - - if self.compressed then - self.returndata = util.Decompress(self.returndata) - - if not self.returndata then - pac.Message("net.Stream.ReadStream:Read(): Failed to decompress data") - end - end - - self:Remove() - else - self:Request() - end -end - ---Gets the download progress -function net.Stream.ReadStream:GetProgress() - return #self.chunks/self.numchunks -end - ---Pop the queue and start the next task -function net.Stream.ReadStream:Remove() - local ok, err = xpcall(self.callback, debug.traceback, self.returndata) - if not ok then ErrorNoHalt(err) end - - net.Start("NetStreamRequest") - net.WriteUInt(self.identifier, 32) - net.WriteBit(false) - net.WriteBit(true) - if CLIENT then net.SendToServer() else net.Send(self.player) end - - timer.Remove("NetStreamReadTimeout" .. self.identifier) - timer.Remove("NetStreamKeepAlive" .. self.identifier) - - if self == self.queue[1] then - table.remove(self.queue, 1) - local nextInQueue = self.queue[1] - if nextInQueue then - timer.Remove("NetStreamKeepAlive" .. nextInQueue.identifier) - nextInQueue:Request() - else - net.Stream.ReadStreamQueues[self.player] = nil - end - else - for k, v in ipairs(self.queue) do - if v == self then - table.remove(self.queue, k) - break - end - end - end -end - -net.Stream.ReadStream.__index = net.Stream.ReadStream - -net.Stream.WriteStream = {} - --- The player wants some data -function net.Stream.WriteStream:Write(ply) - local progress = net.ReadUInt(32)+1 - local chunk = self.chunks[progress] - if chunk then - self.clients[ply].progress = progress - net.Start("NetStreamDownload") - net.WriteUInt(#chunk.data, 32) - net.WriteUInt(progress, 32) - net.WriteString(chunk.crc) - net.WriteData(chunk.data, #chunk.data) - if CLIENT then net.SendToServer() else net.Send(ply) end - end -end - --- The player notified us they finished downloading or cancelled -function net.Stream.WriteStream:Finished(ply) - self.clients[ply].finished = true - - if self.callback then - local ok, err = xpcall(self.callback, debug.traceback, ply) - if not ok then ErrorNoHalt(err) end - end -end - --- Get player's download progress -function net.Stream.WriteStream:GetProgress(ply) - return self.clients[ply].progress / #self.chunks -end - --- If the stream owner cancels it, notify everyone who is subscribed -function net.Stream.WriteStream:Remove() - local sendTo = {} - for ply, client in pairs(self.clients) do - if not client.finished then - client.finished = true - if ply:IsValid() then sendTo[#sendTo+1] = ply end - end - end - - net.Start("NetStreamDownload") - net.WriteUInt(0, 32) - net.WriteUInt(self.identifier, 32) - if SERVER then net.Send(sendTo) else net.SendToServer() end - net.Stream.WriteStreams[self.identifier] = nil -end - -net.Stream.WriteStream.__index = net.Stream.WriteStream - ---Store the data and write the file info so receivers can request it. -local identifier = 1 - -function net.WriteStream(data, callback, dontcompress) - if not isstring(data) then - error("bad argument #1 to 'WriteStream' (string expected, got " .. type(data) .. ")", 2) - end - - if callback ~= nil and not isfunction(callback) then - error("bad argument #2 to 'WriteStream' (function expected, got " .. type(callback) .. ")", 2) - end - - local compressed = not dontcompress - if compressed then - data = util.Compress(data) or "" - end - - if #data == 0 then - net.WriteUInt(0, 32) - return - end - - local numchunks = math.ceil(#data / net.Stream.SendSize) - if CLIENT and numchunks > net.Stream.MaxServerChunks then - ErrorNoHalt("net.WriteStream request is too large! ", #data/1048576, "MiB") - net.WriteUInt(0, 32) - return - end - - local chunks = {} - for i=1, numchunks do - local datachunk = string.sub(data, (i - 1) * net.Stream.SendSize + 1, i * net.Stream.SendSize) - chunks[i] = { - data = datachunk, - crc = util.CRC(datachunk), - } - end - - local startid = identifier - while net.Stream.WriteStreams[identifier] do - identifier = identifier % 1024 + 1 - if identifier == startid then - ErrorNoHalt("Netstream is full of WriteStreams!\n" .. debug.traceback() .. "\n") - net.WriteUInt(0, 32) - return - end - end - - local stream = { - identifier = identifier, - chunks = chunks, - compressed = compressed, - numchunks = numchunks, - callback = callback, - clients = setmetatable({},{__index = function(t,k) - local r = { - finished = false, - downloads = 0, - keepalives = 0, - progress = 0, - } t[k]=r return r - end}) - } - - setmetatable(stream, net.Stream.WriteStream) - - net.Stream.WriteStreams[identifier] = stream - timer.Create("NetStreamWriteTimeout" .. identifier, net.Stream.Timeout, 1, function() stream:Remove() end) - - net.WriteUInt(numchunks, 32) - net.WriteUInt(identifier, 32) - net.WriteBool(compressed) - - return stream -end - ---If the receiver is a player then add it to a queue. ---If the receiver is the server then add it to a queue for each individual player -function net.ReadStream(ply, callback) - if CLIENT then - ply = NULL - else - if type(ply) ~= "Player" then - error("bad argument #1 to 'ReadStream' (Player expected, got " .. type(ply) .. ")", 2) - elseif not ply:IsValid() then - error("bad argument #1 to 'ReadStream' (Tried to use a NULL entity!)", 2) - end - end - - if not isfunction(callback) then - error("bad argument #2 to 'ReadStream' (function expected, got " .. type(callback) .. ")", 2) - end - - local queue = net.Stream.ReadStreamQueues[ply] - if queue then - if SERVER and #queue == net.Stream.MaxServerReadStreams then - ErrorNoHalt("Receiving too many ReadStream requests from ", ply) - return - end - else - queue = {} net.Stream.ReadStreamQueues[ply] = queue - end - - local numchunks = net.ReadUInt(32) - - if numchunks == nil then - return - elseif numchunks == 0 then - local ok, err = xpcall(callback, debug.traceback, "") - if not ok then ErrorNoHalt(err) end - return - end - - if SERVER and numchunks > net.Stream.MaxServerChunks then - ErrorNoHalt("ReadStream requests from ", ply, " is too large! ", numchunks * net.Stream.SendSize / 1048576, "MiB") - return - end - - local identifier = net.ReadUInt(32) - local compressed = net.ReadBool() - --print("Got info", numchunks, identifier, compressed) - - for _, v in ipairs(queue) do - if v.identifier == identifier then - ErrorNoHalt("Tried to start a new ReadStream for an already existing stream!\n" .. debug.traceback() .. "\n") - return - end - end - - local stream = { - identifier = identifier, - chunks = {}, - compressed = compressed, - numchunks = numchunks, - callback = callback, - queue = queue, - player = ply, - downloads = 0 - } - - setmetatable(stream, net.Stream.ReadStream) - - queue[#queue + 1] = stream - if #queue > 1 then - timer.Create("NetStreamKeepAlive" .. identifier, net.Stream.Timeout / 2, 0, function() - net.Start("NetStreamRequest") - net.WriteUInt(identifier, 32) - net.WriteBit(true) - if CLIENT then net.SendToServer() else net.Send(ply) end - end) - else - stream:Request() - end - - return stream -end - -if SERVER then - - util.AddNetworkString("NetStreamRequest") - util.AddNetworkString("NetStreamDownload") - -end - ---Stream data is requested -net.Receive("NetStreamRequest", function(len, ply) - - local identifier = net.ReadUInt(32) - local stream = net.Stream.WriteStreams[identifier] - - if stream then - ply = ply or NULL - local client = stream.clients[ply] - - if not client.finished then - local keepalive = net.ReadBit() == 1 - if keepalive then - if client.keepalives < net.Stream.MaxKeepalive then - client.keepalives = client.keepalives + 1 - timer.Adjust("NetStreamWriteTimeout" .. identifier, net.Stream.Timeout, 1) - end - else - local completed = net.ReadBit() == 1 - if completed then - stream:Finished(ply) - else - if client.downloads < net.Stream.MaxTries * #stream.chunks then - client.downloads = client.downloads + 1 - stream:Write(ply) - timer.Adjust("NetStreamWriteTimeout" .. identifier, net.Stream.Timeout, 1) - else - client.finished = true - end - end - end - end - end - -end) - ---Download the stream data -net.Receive("NetStreamDownload", function(len, ply) - - ply = ply or NULL - local queue = net.Stream.ReadStreamQueues[ply] - if queue then - local size = net.ReadUInt(32) - if size > 0 then - queue[1]:Read(size) - else - local id = net.ReadUInt(32) - for k, v in ipairs(queue) do - if v.identifier == id then - v:Remove() - break - end - end - end - end - -end) +--A net extension which allows sending large streams of data without overflowing the reliable channel +--Keep it in lua/autorun so it will be shared between addons +AddCSLuaFile() + +net.Stream = {} +net.Stream.SendSize = 20000 --This is the size of each packet to send +net.Stream.Timeout = 10 --How long to wait for client response before cleaning up +net.Stream.MaxWriteStreams = 1024 --The maximum number of write data items to store +net.Stream.MaxReadStreams = 128 --The maximum number of queued read data items to store +net.Stream.MaxChunks = 3200 --Maximum number of pieces the stream can send to the server. 64 MB +net.Stream.MaxSize = net.Stream.SendSize*net.Stream.MaxChunks +net.Stream.MaxTries = 3 --Maximum times the client may retry downloading the whole data + +local WriteStreamQueue = { + __index = { + Add = function(self, stream) + local identifier = self.curidentifier + local startid = identifier + while self.queue[identifier] do + identifier = identifier % net.Stream.MaxWriteStreams + 1 + if identifier == startid then + ErrorNoHalt("Netstream is full of WriteStreams!") + net.WriteUInt(0, 32) + return + end + end + self.curidentifier = identifier % net.Stream.MaxWriteStreams + 1 + + if next(self.queue)==nil then + self.activitytimeout = CurTime()+net.Stream.Timeout + timer.Create("netstream_queueclean", 5, 0, function() self:Clean() end) + end + self.queue[identifier] = stream + stream.identifier = identifier + return stream + end, + + Write = function(self, ply) + local identifier = net.ReadUInt(32) + local chunkidx = net.ReadUInt(32) + local stream = self.queue[identifier] + --print("Got request", identifier, chunkidx, stream) + if stream then + if stream:Write(ply, chunkidx) then + self.activitytimeout = CurTime()+net.Stream.Timeout + stream.timeout = CurTime()+net.Stream.Timeout + end + else + -- Tell them the stream doesn't exist + net.Start("NetStreamRead") + net.WriteUInt(identifier, 32) + net.WriteUInt(0, 32) + if SERVER then net.Send(ply) else net.SendToServer() end + end + end, + + Clean = function(self) + local t = CurTime() + for k, stream in pairs(self.queue) do + if (next(stream.clients)~=nil and t >= stream.timeout) or t >= self.activitytimeout then + stream:Remove() + self.queue[k] = nil + end + end + if next(self.queue)==nil then + timer.Remove("netstream_queueclean") + end + end, + }, + __call = function(t) + return setmetatable({ + activitytimeout = CurTime()+net.Stream.Timeout, + curidentifier = 1, + queue = {} + }, t) + end +} +setmetatable(WriteStreamQueue, WriteStreamQueue) +net.Stream.WriteStreams = WriteStreamQueue() + +local ReadStreamQueue = { + __index = { + Add = function(self, stream) + local queue = self.queues[stream.player] + + if #queue == net.Stream.MaxReadStreams then + ErrorNoHalt("Receiving too many ReadStream requests!") + return + end + + for _, v in ipairs(queue) do + if v.identifier == stream.identifier then + ErrorNoHalt("Tried to start a new ReadStream for an already existing stream!") + return + end + end + + queue[#queue+1] = stream + if #queue == 1 then + stream:Request() + end + return stream + end, + + Remove = function(self, stream) + local queue = rawget(self.queues, stream.player) + if queue then + if stream == queue[1] then + table.remove(queue, 1) + local nextInQueue = queue[1] + if nextInQueue then + nextInQueue:Request() + else + self.queues[stream.player] = nil + end + else + for k, v in ipairs(queue) do + if v == stream then + table.remove(queue, k) + break + end + end + end + end + end, + + Read = function(self, ply) + local identifier = net.ReadUInt(32) + local queue = rawget(self.queues, ply) + if queue and queue[1] then + queue[1]:Read(identifier) + end + end + }, + __call = function(t) + return setmetatable({ + queues = setmetatable({}, {__index = function(t,k) local r={} t[k]=r return r end}) + }, t) + end +} +setmetatable(ReadStreamQueue, ReadStreamQueue) +net.Stream.ReadStreams = ReadStreamQueue() + + +local WritingDataItem = { + __index = { + Write = function(self, ply, chunkidx) + local client = self.clients[ply] + if client.finished then return false end + if chunkidx == #self.chunks+1 then self:Finished(ply) return true end + + if client.downloads+#self.chunks-client.progress >= net.Stream.MaxTries * #self.chunks then self:Finished(ply) return false end + client.downloads = client.downloads + 1 + + local chunk = self.chunks[chunkidx] + if not chunk then return false end + + client.progress = chunkidx + + --print("Sending", "NetStreamRead", self.identifier, #chunk.data, chunkidx, chunk.crc) + net.Start("NetStreamRead") + net.WriteUInt(self.identifier, 32) + net.WriteUInt(#chunk.data, 32) + net.WriteUInt(chunkidx, 32) + net.WriteString(chunk.crc) + net.WriteData(chunk.data, #chunk.data) + if CLIENT then net.SendToServer() else net.Send(ply) end + return true + end, + + Finished = function(self, ply) + self.clients[ply].finished = true + if self.callback then + local ok, err = xpcall(self.callback, debug.traceback, ply) + if not ok then ErrorNoHalt(err) end + end + end, + + GetProgress = function(self, ply) + return self.clients[ply].progress / #self.chunks + end, + + Remove = function(self) + local sendTo = {} + for ply, client in pairs(self.clients) do + if not client.finished then + client.finished = true + if CLIENT or ply:IsValid() then sendTo[#sendTo+1] = ply end + end + end + + if next(sendTo)~=nil then + --print("Sending", "NetStreamRead", self.identifier, 0) + net.Start("NetStreamRead") + net.WriteUInt(self.identifier, 32) + net.WriteUInt(0, 32) + if SERVER then net.Send(sendTo) else net.SendToServer() end + end + end + + }, + __call = function(t, data, callback) + local chunks = {} + for i=1, math.ceil(#data / net.Stream.SendSize) do + local datachunk = string.sub(data, (i - 1) * net.Stream.SendSize + 1, i * net.Stream.SendSize) + chunks[i] = { data = datachunk, crc = util.CRC(datachunk) } + end + + return setmetatable({ + timeout = CurTime()+net.Stream.Timeout, + chunks = chunks, + callback = callback, + lasttouched = 0, + clients = setmetatable({},{__index = function(t,k) + local r = { + finished = false, + downloads = 0, + progress = 0, + } t[k]=r return r + end}) + }, t) + end +} +setmetatable(WritingDataItem, WritingDataItem) + +local ReadingDataItem = { + __index = { + Request = function(self) + if self.downloads+self.numchunks-#self.chunks >= net.Stream.MaxTries*self.numchunks then self:Remove() return end + self.downloads = self.downloads + 1 + timer.Create("NetStreamReadTimeout" .. self.identifier, net.Stream.Timeout*0.5, 1, function() self:Request() end) + self:WriteRequest() + end, + + WriteRequest = function(self) + --print("Requesting", self.identifier, #self.chunks) + net.Start("NetStreamWrite") + net.WriteUInt(self.identifier, 32) + net.WriteUInt(#self.chunks+1, 32) + if CLIENT then net.SendToServer() else net.Send(self.player) end + end, + + Read = function(self, identifier) + if self.identifier ~= identifier then self:Request() return end + + local size = net.ReadUInt(32) + if size == 0 then self:Remove() return end + + local chunkidx = net.ReadUInt(32) + if chunkidx ~= #self.chunks+1 then self:Request() return end + + local crc = net.ReadString() + local data = net.ReadData(size) + + if crc ~= util.CRC(data) then self:Request() return end + + self.chunks[chunkidx] = data + if #self.chunks == self.numchunks then self:Remove(true) return end + + self:Request() + end, + + GetProgress = function(self) + return #self.chunks/self.numchunks + end, + + Remove = function(self, finished) + timer.Remove("NetStreamReadTimeout" .. self.identifier) + + local data + if finished then + data = table.concat(self.chunks) + if self.compressed then + data = util.Decompress(data, net.Stream.MaxSize) + end + self:WriteRequest() -- Notify we finished + end + + local ok, err = xpcall(self.callback, debug.traceback, data) + if not ok then ErrorNoHalt(err) end + + net.Stream.ReadStreams:Remove(self) + end + }, + __call = function(t, ply, callback, numchunks, identifier, compressed) + return setmetatable({ + identifier = identifier, + chunks = {}, + compressed = compressed, + numchunks = numchunks, + callback = callback, + player = ply, + downloads = 0 + }, t) + end +} +setmetatable(ReadingDataItem, ReadingDataItem) + + +function net.WriteStream(data, callback, dontcompress) + if not isstring(data) then + error("bad argument #1 to 'WriteStream' (string expected, got " .. type(data) .. ")", 2) + end + if callback ~= nil and not isfunction(callback) then + error("bad argument #2 to 'WriteStream' (function expected, got " .. type(callback) .. ")", 2) + end + + local compressed = not dontcompress + if compressed then + data = util.Compress(data) or "" + end + + if #data == 0 then + net.WriteUInt(0, 32) + return + end + + if #data > net.Stream.MaxSize then + ErrorNoHalt("net.WriteStream request is too large! ", #data/1048576, "MiB") + net.WriteUInt(0, 32) + return + end + + local stream = net.Stream.WriteStreams:Add(WritingDataItem(data, callback, compressed)) + if not stream then return end + + --print("WriteStream", #stream.chunks, stream.identifier, compressed) + net.WriteUInt(#stream.chunks, 32) + net.WriteUInt(stream.identifier, 32) + net.WriteBool(compressed) + + return stream +end + +--If the receiver is a player then add it to a queue. +--If the receiver is the server then add it to a queue for each individual player +function net.ReadStream(ply, callback) + if CLIENT then + ply = NULL + else + if type(ply) ~= "Player" then + error("bad argument #1 to 'ReadStream' (Player expected, got " .. type(ply) .. ")", 2) + elseif not ply:IsValid() then + error("bad argument #1 to 'ReadStream' (Tried to use a NULL entity!)", 2) + end + end + if not isfunction(callback) then + error("bad argument #2 to 'ReadStream' (function expected, got " .. type(callback) .. ")", 2) + end + + local numchunks = net.ReadUInt(32) + if numchunks == nil then + return + elseif numchunks == 0 then + local ok, err = xpcall(callback, debug.traceback, "") + if not ok then ErrorNoHalt(err) end + return + end + + local identifier = net.ReadUInt(32) + local compressed = net.ReadBool() + + if numchunks > net.Stream.MaxChunks then + ErrorNoHalt("ReadStream requests from ", ply, " is too large! ", numchunks * net.Stream.SendSize / 1048576, "MiB") + return + end + + --print("ReadStream", numchunks, identifier, compressed) + + return net.Stream.ReadStreams:Add(ReadingDataItem(ply, callback, numchunks, identifier, compressed)) +end + +if SERVER then + util.AddNetworkString("NetStreamWrite") + util.AddNetworkString("NetStreamRead") +end + +--Send requested stream data +net.Receive("NetStreamWrite", function(len, ply) + net.Stream.WriteStreams:Write(ply or NULL) +end) + +--Download the sent stream data +net.Receive("NetStreamRead", function(len, ply) + net.Stream.ReadStreams:Read(ply or NULL) +end) From fb65d1b18cbfec6c17bdd7023ae1d3f7e6ec9ebe Mon Sep 17 00:00:00 2001 From: techbot Date: Fri, 14 Jun 2024 19:06:10 +0200 Subject: [PATCH 190/300] fix the annoying `Invalid model scale 0.000000` spam --- lua/pac3/core/client/owner_name.lua | 4 +- lua/pac3/core/client/parts/legacy/model.lua | 2 +- lua/pac3/core/client/parts/model.lua | 57 ++++++++++----------- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/lua/pac3/core/client/owner_name.lua b/lua/pac3/core/client/owner_name.lua index c79f81e64..a405f5e21 100644 --- a/lua/pac3/core/client/owner_name.lua +++ b/lua/pac3/core/client/owner_name.lua @@ -43,10 +43,10 @@ function pac.GetWorldEntity() if not pac.WorldEntity:IsValid() then local ent = pac.CreateEntity("models/error.mdl") - ent:SetPos(Vector(0,0,0)) + ent:SetPos(Vector(0, 0, 0)) -- go away ugh - ent:SetModelScale(0,0) + ent:SetMaterial("materials/null") ent.IsPACWorldEntity = true diff --git a/lua/pac3/core/client/parts/legacy/model.lua b/lua/pac3/core/client/parts/legacy/model.lua index 505ac48d1..e38ddc1a8 100644 --- a/lua/pac3/core/client/parts/legacy/model.lua +++ b/lua/pac3/core/client/parts/legacy/model.lua @@ -409,7 +409,7 @@ surface.CreateFont("pac_urlobj_loading", -- ugh lol local function RealDrawModel(self, ent, pos, ang) if self.Mesh then - ent:SetModelScale(0,0) + ent:SetModelScale(0.001, 0) ent:DrawModel() local matrix = Matrix() diff --git a/lua/pac3/core/client/parts/model.lua b/lua/pac3/core/client/parts/model.lua index ad647adba..938e6df45 100644 --- a/lua/pac3/core/client/parts/model.lua +++ b/lua/pac3/core/client/parts/model.lua @@ -5,25 +5,25 @@ CreateConVar( "pac_model_max_scales", "10000", FCVAR_ARCHIVE, "Maximum scales mo local pac = pac -local render_SetColorModulation = render.SetColorModulation -local render_SetBlend = render.SetBlend -local render_CullMode = render.CullMode -local MATERIAL_CULLMODE_CW = MATERIAL_CULLMODE_CW -local MATERIAL_CULLMODE_CCW = MATERIAL_CULLMODE_CCW -local render_MaterialOverride = render.ModelMaterialOverride -local cam_PushModelMatrix = cam.PushModelMatrix +local cam = cam local cam_PopModelMatrix = cam.PopModelMatrix -local Vector = Vector -local EF_BONEMERGE = EF_BONEMERGE -local NULL = NULL +local cam_PushModelMatrix = cam.PushModelMatrix local Color = Color +local EF_BONEMERGE = EF_BONEMERGE +local MATERIAL_CULLMODE_CCW = MATERIAL_CULLMODE_CCW +local MATERIAL_CULLMODE_CW = MATERIAL_CULLMODE_CW local Matrix = Matrix -local vector_origin = vector_origin +local NULL = NULL local render = render -local cam = cam -local surface = surface +local render_CullMode = render.CullMode +local render_MaterialOverride = render.ModelMaterialOverride local render_MaterialOverrideByIndex = render.MaterialOverrideByIndex +local render_SetBlend = render.SetBlend +local render_SetColorModulation = render.SetColorModulation local render_SuppressEngineLighting = render.SuppressEngineLighting +local surface = surface +local Vector = Vector +local vector_origin = vector_origin local BUILDER, PART = pac.PartTemplate("base_drawable") @@ -32,10 +32,10 @@ PART.ClassName = "model2" PART.Category = "model" PART.ManualDraw = true PART.HandleModifiersManually = true -PART.Icon = 'icon16/shape_square.png' +PART.Icon = "icon16/shape_square.png" PART.is_model_part = true PART.ProperColorRange = true -PART.Group = 'model' +PART.Group = "model" BUILDER:StartStorableVars() :SetPropertyGroup("generic") @@ -176,7 +176,7 @@ end function PART:ModelModifiersToString(tbl) local str = "" - for k,v in pairs(tbl) do + for k, v in pairs(tbl) do str = str .. k .. "=" .. v .. ";" end return str @@ -304,7 +304,7 @@ end function PART:OnRemove() if not self.loading then - SafeRemoveEntityDelayed(self.Owner,0.1) + SafeRemoveEntityDelayed(self.Owner, 0.1) end end @@ -433,12 +433,11 @@ end local matrix = Matrix() -local IDENT_SCALE = Vector(1,1,1) local _self, _ent, _pos, _ang local function ent_draw_model(self, ent, pos, ang) if self.obj_mesh then - ent:SetModelScale(0,0) + ent:SetModelScale(0.001, 0) ent:DrawModel() matrix:Identity() @@ -487,10 +486,10 @@ function PART:DrawModel(ent, pos, ang) if self.ClassName ~= "entity2" then render.PushFlashlightMode(true) - material_bound = self:BindMaterials(ent) or material_bound - ent.pac_drawing_model = true - ProtectedCall(protected_ent_draw_model) - ent.pac_drawing_model = false + material_bound = self:BindMaterials(ent) or material_bound + ent.pac_drawing_model = true + ProtectedCall(protected_ent_draw_model) + ent.pac_drawing_model = false render.PopFlashlightMode() end @@ -536,7 +535,7 @@ function PART:DrawLoadingText(ent, pos) cam.End2D() end -local ALLOW_TO_MDL = CreateConVar('pac_allow_mdl', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow to use custom MDLs') +local ALLOW_TO_MDL = CreateConVar("pac_allow_mdl", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow to use custom MDLs") function PART:RefreshModel() if self.refreshing_model then return end @@ -578,7 +577,7 @@ end local function RealDrawModel(self, ent, pos, ang) if self.Mesh then - ent:SetModelScale(0,0) + ent:SetModelScale(0.001, 0) ent:DrawModel() local matrix = Matrix() @@ -607,7 +606,7 @@ function PART:ProcessModelChange() if path:find("://", nil, true) then if path:StartWith("objhttp") or path:StartWith("obj:http") or path:match("%.obj%p?") or self.ForceObjUrl then - path = path:gsub("^objhttp","http"):gsub("^obj:http","http") + path = path:gsub("^objhttp", "http"):gsub("^obj:http", "http") self.loading = "downloading obj" pac.urlobj.GetObjFromURL(path, false, false, @@ -722,7 +721,7 @@ function PART:SetModel(path) self:ProcessModelChange() end -local NORMAL = Vector(1,1,1) +local NORMAL = Vector(1, 1, 1) function PART:CheckScale() local owner = self:GetOwner() @@ -756,7 +755,7 @@ function PART:SetScale(vec) if largest_scale > 10000 then --warn about the default max scale self:SetError("Scale is being limited due to having an excessive component. Default maximum values are 10000") else self:SetError() end --if ok, clear the warning - vec = vec or Vector(1,1,1) + vec = vec or Vector(1, 1, 1) self.Scale = vec @@ -765,7 +764,7 @@ function PART:SetScale(vec) end end -local vec_one = Vector(1,1,1) +local vec_one = Vector(1, 1, 1) function PART:ApplyMatrix() local ent = self:GetOwner() From 7a0f5509e9ec8827510a8c2b1ed95073c652ae64 Mon Sep 17 00:00:00 2001 From: techbot Date: Fri, 14 Jun 2024 19:07:14 +0200 Subject: [PATCH 191/300] don't use a deprecated render function --- lua/pac3/core/client/parts/model.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/pac3/core/client/parts/model.lua b/lua/pac3/core/client/parts/model.lua index 938e6df45..420a73c7f 100644 --- a/lua/pac3/core/client/parts/model.lua +++ b/lua/pac3/core/client/parts/model.lua @@ -18,6 +18,7 @@ local render = render local render_CullMode = render.CullMode local render_MaterialOverride = render.ModelMaterialOverride local render_MaterialOverrideByIndex = render.MaterialOverrideByIndex +local render_RenderFlashlights = render.RenderFlashlights local render_SetBlend = render.SetBlend local render_SetColorModulation = render.SetColorModulation local render_SuppressEngineLighting = render.SuppressEngineLighting @@ -484,14 +485,12 @@ function PART:DrawModel(ent, pos, ang) _self, _ent, _pos, _ang = self, ent, pos, ang if self.ClassName ~= "entity2" then - render.PushFlashlightMode(true) - + render_RenderFlashlights(function() material_bound = self:BindMaterials(ent) or material_bound ent.pac_drawing_model = true ProtectedCall(protected_ent_draw_model) ent.pac_drawing_model = false - - render.PopFlashlightMode() + end) end if self.NoCulling then From 138c04b5e1fd4595a0105efa7031d5da8e8cdb78 Mon Sep 17 00:00:00 2001 From: techbot Date: Fri, 14 Jun 2024 19:08:19 +0200 Subject: [PATCH 192/300] fix error --- lua/pac3/core/client/parts/event.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 7ffd77c7b..50eccf7df 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -4213,6 +4213,8 @@ net.Receive("pac_update_healthbars", function() local ent = net.ReadEntity() local tbl = net.ReadTable() + if not IsValid(ent) then return end + ent.pac_healthbars = tbl ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {} From 26099cc5238ba1d39825fc3bbecc531b2a27cb7f Mon Sep 17 00:00:00 2001 From: TW1STaL1CKY Date: Fri, 14 Jun 2024 22:02:08 +0100 Subject: [PATCH 193/300] Make pac.GetBonePosAng use GetBoneMatrix instead, fixing parts lagging behind when shot at --- lua/pac3/core/client/bones.lua | 38 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/lua/pac3/core/client/bones.lua b/lua/pac3/core/client/bones.lua index 477c506dd..71f3aab04 100644 --- a/lua/pac3/core/client/bones.lua +++ b/lua/pac3/core/client/bones.lua @@ -180,34 +180,35 @@ function pac.ResetBoneCache(ent) end end -local UP = Vector(0,0,1):Angle() +local viewmodelClassName = "viewmodel" local function GetBonePosition(ent, id) - local pos, ang = ent:GetBonePosition(id) + local mat = ent:GetBoneMatrix(id) + if not mat then return end - if not pos then return end + local pos, ang = mat:GetTranslation(), mat:GetAngles() if ang.p ~= ang.p then ang.p = 0 end if ang.y ~= ang.y then ang.y = 0 end if ang.r ~= ang.r then ang.r = 0 end - if pos == ent:GetPos() then - local mat = ent:GetBoneMatrix(id) - if mat then - pos = mat:GetTranslation() - ang = mat:GetAngles() - end - end + if ent == pac.LocalHands or ent:GetClass() == viewmodelClassName then + local owner = ent:GetOwner() + + if owner:IsPlayer() then + local ownerwep = owner:GetActiveWeapon() - if (ent:GetClass() == "viewmodel" or ent == pac.LocalHands) and - ent:GetOwner():IsPlayer() and ent:GetOwner():GetActiveWeapon().ViewModelFlip then - ang.r = -ang.r + if ownerwep:IsValid() and ownerwep.ViewModelFlip then + ang.r = -ang.r + end + end end return pos, ang end local angle_origin = Angle(0,0,0) +local UP = Vector(0,0,1):Angle() function pac.GetBonePosAng(ent, id, parent) if not ent:IsValid() then return Vector(), Angle() end @@ -222,8 +223,15 @@ function pac.GetBonePosAng(ent, id, parent) if enabled then if target:IsValid() then if bone ~= 0 then - local wpos, wang = target:GetBonePosition(target:TranslatePhysBoneToBone(bone)) - endpos = LocalToWorld(hitpos, Angle(), wpos, wang) + local mat = target:GetBoneMatrix(target:TranslatePhysBoneToBone(bone)) + local wpos, wang + + if mat then + wpos, wang = mat:GetTranslation(), mat:GetAngles() + endpos = LocalToWorld(hitpos, Angle(), wpos, wang) + else + endpos = target:LocalToWorld(hitpos) + end else endpos = target:LocalToWorld(hitpos) end From 11acae243e49f7cadeb29a1055b5db0dc4fdf26f Mon Sep 17 00:00:00 2001 From: techbot Date: Sat, 15 Jun 2024 12:22:02 +0200 Subject: [PATCH 194/300] make it more clear that you can load only outfits via "load from url" --- lua/pac3/editor/client/saved_parts.lua | 98 ++++++++++++++------------ 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 981867692..2d68607d0 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -97,7 +97,7 @@ end local last_backup local maxBackups = CreateConVar("pac_backup_limit", "100", {FCVAR_ARCHIVE}, "Maximal amount of backups") local autoload_prompt = CreateConVar("pac_prompt_for_autoload", "1", {FCVAR_ARCHIVE}, "Whether to ask before loading autoload. The prompt can let you choose to not load, pick autoload or the newest backup") -local auto_spawn_prop = CreateConVar("pac_autoload_preferred_prop", "2", {FCVAR_ARCHIVE}, "When loading a pac with an owner name suggesting a prop, notify you and then wait before auto-applying the outfit next time you spawn a prop.\n".. +local auto_spawn_prop = CreateConVar("pac_autoload_preferred_prop", "2", {FCVAR_ARCHIVE}, "When loading a pac with an owner name suggesting a prop, notify you and then wait before auto-applying the outfit next time you spawn a prop.\n" .. "0 : do not check\n1 : check if only 1 such group is present\n2 : check if multiple such groups are present and queue one group at a time") @@ -137,7 +137,7 @@ function pace.Backup(data, name) local str = pace.luadata.Encode(data) if str ~= last_backup then - file.Write("pac3/__backup/" .. (name=="" and name or (name..'_')) .. date .. ".txt", str) + file.Write("pac3/__backup/" .. (name == "" and name or (name .. "_")) .. date .. ".txt", str) last_backup = str end end @@ -202,7 +202,7 @@ function pace.LoadParts(name, clear, override_part) else if name ~= "autoload.txt" and not string.find(name, "pac3/__backup") then - if file.Exists("pac3/" .. name..".txt", "DATA") then + if file.Exists("pac3/" .. name .. ".txt", "DATA") then cookie.Set( "pac_last_loaded_outfit", name .. ".txt" ) end end @@ -213,36 +213,43 @@ function pace.LoadParts(name, clear, override_part) pac.dprint("loading Parts %s", name) if name:find("https?://") then - local function callback(str) - if string.find( str, "" ) then - pace.MessagePrompt("Invalid URL, the website returned a HTML file. If you're using Github then use the RAW option.", "URL Failed", "OK") - return - end - local data, err = pace.luadata.Decode(str) - if not data then - ErrorNoHalt(("URL fail: %s : %s\n"):format(name,err)) - local message = string.format("URL fail: %s : %s\n", name, err) - pace.MessagePrompt(message, "URL Failed", "OK") - return + local ext = name:match("/.+%.(%a+)[%?&]?.-") + if ext == "txt" then + local function callback(str) + if string.find( str, "" ) then + pace.MessagePrompt("Invalid URL, the website returned a HTML file. If you're using Github then use the RAW option.", "URL Failed", "OK") + return + end + + local data, err = pace.luadata.Decode(str) + if not data then + ErrorNoHalt(("URL fail: %s : %s\n"):format(name, err)) + local message = string.format("URL fail: %s : %s\n", name, err) + pace.MessagePrompt(message, "URL Failed", "OK") + return + end + + pace.LoadPartsFromTable(data, clear, override_part) end - pace.LoadPartsFromTable(data, clear, override_part) + pac.HTTPGet(name, callback, function(err) + pace.MessagePrompt(err, "HTTP Request Failed for " .. name, "OK") + end) + else + pace.MessagePrompt("Invalid file", ".txt file expected, got" .. ext, "OK") + return end - - pac.HTTPGet(name, callback, function(err) - pace.MessagePrompt(err, "HTTP Request Failed for " .. name, "OK") - end) else name = name:gsub("%.txt", "") - local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") + local data, err = pace.luadata.ReadFile("pac3/" .. name .. ".txt") local has_possible_prop_pacs = false if data and istable(data) then - for i,part in pairs(data) do - if part.self then - if isnumber(tonumber(part.self.OwnerName)) then has_possible_prop_pacs = true end + for i, part in pairs(data) do + if part.self and isnumber(tonumber(part.self.OwnerName)) then + has_possible_prop_pacs = true end end end @@ -253,7 +260,7 @@ function pace.LoadParts(name, clear, override_part) LocalPlayer().pac_propload_queuedparts = LocalPlayer().pac_propload_queuedparts or {} --check all root parts from data. format: each data member is a {self, children} table of the part and the list of children - for i,part in pairs(data) do + for i, part in pairs(data) do local possible_prop_pac = isnumber(tonumber(part.self.OwnerName)) if part.self.ClassName == "group" and possible_prop_pac then @@ -278,7 +285,7 @@ function pace.LoadParts(name, clear, override_part) if name == "autoload" and (not data or not next(data)) then local err - data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true) + data, err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt", nil, true) if not data then if err then ErrorNoHalt(("Autoload failed: %s\n"):format(err)) @@ -286,7 +293,7 @@ function pace.LoadParts(name, clear, override_part) return end elseif not data then - ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err)) + ErrorNoHalt(("Decoding %s failed: %s\n"):format(name, err)) return end @@ -299,11 +306,11 @@ function pace.LoadParts(name, clear, override_part) end end -concommand.Add('pac_load_url', function(ply, cmd, args) - if not args[1] then return print('[PAC3] No URL specified') end +concommand.Add("pac_load_url", function(ply, cmd, args) + if not args[1] then return print("[PAC3] No URL specified") end local url = args[1]:Trim() - if not url:find("https?://") then return print('[PAC3] Invalid URL specified') end - pac.Message('Loading specified URL') + if not url:find("https?://") then return print("[PAC3] Invalid URL specified") end + pac.Message("Loading specified URL") if args[2] == nil then args[2] = '1' end pace.LoadParts(url, tobool(args[2])) end) @@ -348,8 +355,8 @@ function pace.LoadPartsFromTable(data, clear, override_part) pace.RefreshTree(true) for i, part in ipairs(partsLoaded) do - part:CallRecursive('OnOutfitLoaded') - part:CallRecursive('PostApplyFixes') + part:CallRecursive("OnOutfitLoaded") + part:CallRecursive("PostApplyFixes") end pac.LocalPlayer.pac_fix_show_from_render = SysTime() + 1 @@ -362,10 +369,9 @@ local function add_files(tbl, dir) if folders then for key, folder in pairs(folders) do - if folder == "__backup" or folder == "objcache" or folder == "__animations" or folder == "__backup_save" then goto CONTINUE end + if folder == "__backup" or folder == "objcache" or folder == "__animations" or folder == "__backup_save" then continue end tbl[folder] = {} add_files(tbl[folder], dir .. "/" .. folder) - ::CONTINUE:: end end @@ -385,13 +391,13 @@ local function add_files(tbl, dir) data.Path = path data.RelativePath = (dir .. "/" .. data.Name):sub(2) - local dat,err=pace.luadata.ReadFile(path) + local dat, err = pace.luadata.ReadFile(path) data.Content = dat if dat then table.insert(tbl, data) else - pac.dprint(("Decoding %s failed: %s\n"):format(path,err)) + pac.dprint(("Decoding %s failed: %s\n"):format(path, err)) chat.AddText(("Could not load: %s\n"):format(path)) end @@ -400,7 +406,7 @@ local function add_files(tbl, dir) end end - table.sort(tbl, function(a,b) + table.sort(tbl, function(a, b) if a.Time and b.Time then return a.Name < b.Name end @@ -457,7 +463,7 @@ end local function populate_parts(menu, tbl, override_part, clear) for key, data in pairs(tbl) do if not data.Path then - local menu, pnl = menu:AddSubMenu(key, function()end, data) + local menu, pnl = menu:AddSubMenu(key, function() end, data) pnl:SetImage(pace.MiscIcons.load) menu.GetDeleteSelf = function() return false end local old = menu.Open @@ -503,14 +509,14 @@ function pace.AddOneDirectorySavedPartsToMenu(menu, subdir, nicename) if not subdir then return end local subdir_head = subdir .. "/" - local exp_submenu, pnl = menu:AddSubMenu(L""..subdir) + local exp_submenu, pnl = menu:AddSubMenu(L"" .. subdir) pnl:SetImage(pace.MiscIcons.load) exp_submenu.GetDeleteSelf = function() return false end subdir = "pac3/" .. subdir if nicename then exp_submenu:SetText(nicename) end add_expensive_submenu_load(pnl, function(subdir) - local files = file.Find(subdir.."/*", "DATA") + local files = file.Find(subdir .. "/*", "DATA") local files2 = {} --PrintTable(files) for i, filename in ipairs(files) do @@ -554,7 +560,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) "", function(name) - local data,err = pace.luadata.Decode(name) + local data, _ = pace.luadata.Decode(name) if data then pace.LoadPartsFromTable(data, clear, override_part) end @@ -568,7 +574,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part) examples.GetDeleteSelf = function() return false end local sorted = {} - for k,v in pairs(pace.example_outfits) do sorted[#sorted + 1] = {k = k, v = v} end + for k, v in pairs(pace.example_outfits) do sorted[#sorted + 1] = {k = k, v = v} end table.sort(sorted, function(a, b) return a.k < b.k end) for _, data in pairs(sorted) do @@ -681,7 +687,7 @@ local function populate_parts(menu, tbl, dir, override_part) menu:AddSpacer() for key, data in pairs(tbl) do if not data.Path then - local menu, pnl = menu:AddSubMenu(key, function()end, data) + local menu, pnl = menu:AddSubMenu(key, function() end, data) pnl:SetImage(pace.MiscIcons.load) menu.GetDeleteSelf = function() return false end populate_parts(menu, data, dir .. "/" .. key, override_part) @@ -718,11 +724,11 @@ local function populate_parts(menu, tbl, dir, override_part) local function delete_directory(dir) local files, folders = file.Find(dir .. "*", "DATA") - for k,v in ipairs(files) do + for k, v in ipairs(files) do file.Delete(dir .. v) end - for k,v in ipairs(folders) do + for k, v in ipairs(folders) do delete_directory(dir .. v .. "/") end @@ -818,7 +824,7 @@ function pace.FixBadGrouping(data) }, } - for k,v in pairs(other) do + for k, v in pairs(other) do table.insert(out, v) end From 9593563e04f438bf213b0049ea7d071f9345adaf Mon Sep 17 00:00:00 2001 From: techbot Date: Sat, 15 Jun 2024 12:39:35 +0200 Subject: [PATCH 195/300] fix mixed up title and message --- lua/pac3/editor/client/saved_parts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 2d68607d0..337eb456d 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -237,7 +237,7 @@ function pace.LoadParts(name, clear, override_part) pace.MessagePrompt(err, "HTTP Request Failed for " .. name, "OK") end) else - pace.MessagePrompt("Invalid file", ".txt file expected, got" .. ext, "OK") + pace.MessagePrompt( ".txt file expected, got" .. ext, "Invalid file", "OK") return end else From f9c7e8fc0bca2e55fc24fc6e73d35d520a52a678 Mon Sep 17 00:00:00 2001 From: techbot Date: Sat, 15 Jun 2024 12:40:20 +0200 Subject: [PATCH 196/300] =?UTF-8?q?=F0=9F=92=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lua/pac3/editor/client/saved_parts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 337eb456d..59843276a 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -237,7 +237,7 @@ function pace.LoadParts(name, clear, override_part) pace.MessagePrompt(err, "HTTP Request Failed for " .. name, "OK") end) else - pace.MessagePrompt( ".txt file expected, got" .. ext, "Invalid file", "OK") + pace.MessagePrompt(".txt file expected, got" .. ext, "Invalid file", "OK") return end else From 264793bde66d5bd5e8419f8b582ce7b58757be3e Mon Sep 17 00:00:00 2001 From: techbot Date: Sat, 15 Jun 2024 12:59:15 +0200 Subject: [PATCH 197/300] show prompts instead of calling ErrorNoHalt --- lua/pac3/editor/client/saved_parts.lua | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 59843276a..233f6131f 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -224,7 +224,6 @@ function pace.LoadParts(name, clear, override_part) local data, err = pace.luadata.Decode(str) if not data then - ErrorNoHalt(("URL fail: %s : %s\n"):format(name, err)) local message = string.format("URL fail: %s : %s\n", name, err) pace.MessagePrompt(message, "URL Failed", "OK") return @@ -282,26 +281,19 @@ function pace.LoadParts(name, clear, override_part) end else - if name == "autoload" and (not data or not next(data)) then - local err data, err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt", nil, true) if not data then - if err then - ErrorNoHalt(("Autoload failed: %s\n"):format(err)) - end + pace.MessagePrompt(err, "Autoload failed", "OK") return end elseif not data then - ErrorNoHalt(("Decoding %s failed: %s\n"):format(name, err)) + pace.MessagePrompt(err, ("Decoding %s failed"):format(name), "OK") return end - pace.LoadPartsFromTable(data, clear, override_part) - end - end end end From 32ee247b1ceaed0b22888439e5d206fc890aa865 Mon Sep 17 00:00:00 2001 From: Astralcircle <142503363+Astralcircle@users.noreply.github.com> Date: Sun, 16 Jun 2024 01:33:31 +0300 Subject: [PATCH 198/300] Use player.Iterator() instead of player.GetAll() (Development branch) (#1364) * Update part_pool.lua * Update init.lua --- lua/pac3/core/client/part_pool.lua | 2 +- lua/pac3/editor/client/init.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 05778d92b..04146425a 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -251,7 +251,7 @@ function pac.UnhookEntityRender(ent, part) end pac.AddHook("Think", "events", function() - for _, ply in ipairs(player.GetAll()) do + for _, ply in player.Iterator() do if not ent_parts[ply] then continue end if pac.IsEntityIgnored(ply) then continue end diff --git a/lua/pac3/editor/client/init.lua b/lua/pac3/editor/client/init.lua index 8980714a4..7de793b69 100644 --- a/lua/pac3/editor/client/init.lua +++ b/lua/pac3/editor/client/init.lua @@ -314,7 +314,7 @@ do local up = Vector(0,0,10000) pac.AddHook("HUDPaint", "in_editor", function() - for _, ply in ipairs(player.GetAll()) do + for _, ply in player.Iterator() do if ply ~= pac.LocalPlayer and ply:GetNW2Bool("pac_in_editor") then if showCameras:GetInt() == 1 then From 5794b6d55945780fdb365e359699738325b92cff Mon Sep 17 00:00:00 2001 From: techbot Date: Sun, 16 Jun 2024 11:44:48 +0200 Subject: [PATCH 199/300] only show prompt if there was a decoding error --- lua/pac3/editor/client/saved_parts.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 233f6131f..6a2004298 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -284,7 +284,9 @@ function pace.LoadParts(name, clear, override_part) if name == "autoload" and (not data or not next(data)) then data, err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt", nil, true) if not data then - pace.MessagePrompt(err, "Autoload failed", "OK") + if err then + pace.MessagePrompt(err, "Autoload failed", "OK") + end return end elseif not data then From 4b8b99eadc0d259eb8ee5591f38b33c97ef0de9e Mon Sep 17 00:00:00 2001 From: techbot Date: Sat, 29 Jun 2024 17:03:45 +0200 Subject: [PATCH 200/300] only lint changed files --- .github/workflows/lua_linter.yaml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lua_linter.yaml b/.github/workflows/lua_linter.yaml index d849e9d9b..27f9911dc 100644 --- a/.github/workflows/lua_linter.yaml +++ b/.github/workflows/lua_linter.yaml @@ -2,9 +2,31 @@ name: lint on: pull_request: - types: [opened, synchronize, reopened] + paths: + - "lua/**" + types: [opened, synchronize] workflow_call: +# partially taken from wiremod/wire/master/.github/workflows/lint.yml jobs: - lua-lint: - uses: FPtje/GLuaFixer/.github/workflows/glualint.yml@master \ No newline at end of file + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@master + + - name: Get any changed files + id: changed-files + uses: tj-actions/changed-files@v41 + with: + files: | + **.lua + + - name: Download GluaFixer + run: | + curl -o glualint.zip -L https://github.com/FPtje/GLuaFixer/releases/download/1.28.0/glualint-1.28.0-x86_64-linux.zip + unzip glualint.zip + + - name: Lint Code + run: | + ./glualint ${{ steps.changed-files.outputs.all_changed_files }} \ No newline at end of file From e26947b6cde98e8befb1cde289c85e9d1fb5d2ac Mon Sep 17 00:00:00 2001 From: techbot Date: Sat, 29 Jun 2024 17:19:55 +0200 Subject: [PATCH 201/300] lint only if lua files have been changed [skip ci] --- .github/workflows/lua_linter.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/lua_linter.yaml b/.github/workflows/lua_linter.yaml index 27f9911dc..008380e6f 100644 --- a/.github/workflows/lua_linter.yaml +++ b/.github/workflows/lua_linter.yaml @@ -23,10 +23,12 @@ jobs: **.lua - name: Download GluaFixer + if: steps.changed-files.outputs.any_changed run: | curl -o glualint.zip -L https://github.com/FPtje/GLuaFixer/releases/download/1.28.0/glualint-1.28.0-x86_64-linux.zip unzip glualint.zip - name: Lint Code + if: steps.changed-files.outputs.any_changed run: | ./glualint ${{ steps.changed-files.outputs.all_changed_files }} \ No newline at end of file From 44544dab29c8152d371703b78bead2d4d8da4025 Mon Sep 17 00:00:00 2001 From: techbot Date: Sat, 29 Jun 2024 17:20:39 +0200 Subject: [PATCH 202/300] update checkout due to deprecation [skip ci] --- .github/workflows/update_workshop.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update_workshop.yaml b/.github/workflows/update_workshop.yaml index a4a781ac1..04d674a49 100644 --- a/.github/workflows/update_workshop.yaml +++ b/.github/workflows/update_workshop.yaml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Publish to Steam Workshop uses: PAC3-Server/gmod-upload@master From 2d756a76c89b4f47f5a26f38d255c620280b6d74 Mon Sep 17 00:00:00 2001 From: Techbot121 Date: Sun, 30 Jun 2024 09:26:39 +0200 Subject: [PATCH 203/300] fix outfit filters not working (#1369) * actually ignore outfits from people you have filtered * - change config path (why was it separate anyway?) - fix return value of get_config_value it would only work if you opened the menu once * ask for migration when the editor is being opened * use __index * maybe unnecessary? * fix wearing not refreshing the part in the editor --- lua/pac3/editor/client/wear.lua | 30 ++++---- lua/pac3/editor/client/wear_filter.lua | 96 +++++++++++++++++--------- 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index c300b8f6b..038b8123e 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -112,7 +112,7 @@ do -- to server end net.SendToServer() - pac.Message(('Transmitting outfit %q to server (%s)'):format(part.Name or part.ClassName or '', string.NiceSize(bytes))) + pac.Message(("Transmitting outfit %q to server (%s)"):format(part.Name or part.ClassName or "", string.NiceSize(bytes))) pace.ExtendWearTracker(8) @@ -127,9 +127,9 @@ do -- to server end net.Start("pac_submit") - local ret,err = net_write_table(data) + local ret, err = net_write_table(data) if ret == nil then - pace.Notify(false, "unable to transfer data to server: "..tostring(err or "too big"), name) + pace.Notify(false, "unable to transfer data to server: " .. tostring(err or "too big"), name) return false end net.SendToServer() @@ -182,11 +182,11 @@ do -- from server end if owner == pac.LocalPlayer then - pace.CallHook("OnWoreOutfit", part) + pac.CallHook("OnWoreOutfit", part) end - part:CallRecursive('OnWorn') - part:CallRecursive('PostApplyFixes') + part:CallRecursive("OnWorn") + part:CallRecursive("PostApplyFixes") if part.UpdateOwnerName then part:UpdateOwnerName(true) @@ -229,7 +229,7 @@ function pace.HandleReceiveData(data, doitnow) elseif T == "string" then return pace.RemovePartFromServer(data.owner, data.part, data) else - ErrorNoHalt("PAC: Unhandled "..T..'!?\n') + ErrorNoHalt("PAC: Unhandled " .. T .. "!?\n") end end @@ -246,7 +246,7 @@ net.Receive("pac_submit", function() local data = buffer:readTable() if type(data.owner) ~= "Player" or not data.owner:IsValid() then - pac.Message("received message from server but owner is not valid!? typeof " .. type(data.owner) .. ' || ', data.owner) + pac.Message("received message from server but owner is not valid!? typeof " .. type(data.owner) .. " || ", data.owner) return end @@ -262,9 +262,9 @@ function pace.Notify(allowed, reason, name) name = name or "???" if allowed == true then - pac.Message(string.format('Your part %q has been applied', name)) + pac.Message(string.format("Your part %q has been applied", name)) else - chat.AddText(Color(255, 255, 0), "[PAC3] ", Color(255, 0, 0), string.format('The server rejected applying your part (%q) - %s', name, reason)) + chat.AddText(Color(255, 255, 0), "[PAC3] ", Color(255, 0, 0), string.format("The server rejected applying your part (%q) - %s", name, reason)) end end @@ -297,6 +297,7 @@ do else --prompt local backup_files, directories = file.Find( "pac3/__backup/*.txt", "DATA", "datedesc") + local latest_outfit = cookie.GetString( "pac_last_loaded_outfit", "" ) if not backup_files then local pnl = Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)", "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() @@ -320,7 +321,6 @@ do else if backup_files[1] then local latest_autosave = "pac3/__backup/" .. backup_files[1] - local latest_outfit = cookie.GetString( "pac_last_loaded_outfit", "" ) local pnl = Derma_Query("Do you want to load an outfit?", "PAC3 autoload (pac_prompt_for_autoload)", "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function() pac.Message("Wearing autoload...") @@ -400,11 +400,9 @@ do frames = frames + 1 - if frames > 400 then - if not xpcall(Initialize, ErrorNoHalt) then - pac.RemoveHook("Think", "request_outfits") - pace.NeverLoaded = true - end + if frames > 400 and not xpcall(Initialize, ErrorNoHalt) then + pac.RemoveHook("Think", "request_outfits") + pace.NeverLoaded = true end end) diff --git a/lua/pac3/editor/client/wear_filter.lua b/lua/pac3/editor/client/wear_filter.lua index 52fb3dc65..7fa9b805b 100644 --- a/lua/pac3/editor/client/wear_filter.lua +++ b/lua/pac3/editor/client/wear_filter.lua @@ -1,18 +1,46 @@ local list_form = include("panels/list.lua") local L = pace.LanguageString -local cache = {} +local path = "pac3_config/" + +-- old path migration +pac.AddHook("PrePACEditorOpen", "wear_filter_config_migration", function() + if file.IsDir("pac/config", "DATA") and cookie.GetString("pac3_config_migration_dismissed", "0") == "0" then + Derma_Query( + L "Do you want to migrate the old pac/config folder to pac3_config?", + L "Old Config Folder Detected", + L "Yes", + function() + local files, _ = file.Find("pac/config/*.json", "DATA") + for i = 1, #files do + local f = files[i] + local content = file.Read("pac/config/" .. f, "DATA") + file.Write(path .. f, content) + file.Delete("pac/config/" .. f, "DATA") + end + file.Delete("pac/config", "DATA") + file.Delete("pac", "DATA") + end, + L "No", + function() + cookie.Set("pac3_config_migration_dismissed", "1") + end + ) + end +end) -local function store_config(id, tbl) - file.CreateDir("pac/config") - file.Write("pac/config/"..id..".json", util.TableToJSON(tbl)) - cache[id] = tbl -end +local cache = setmetatable({}, {__index = function(self, key) + local val = util.JSONToTable(file.Read(path .. key .. ".json", "DATA") or "{}") or {} + self[key] = val + return self +end}) -local function read_config(id) - local tbl = util.JSONToTable(file.Read("pac/config/"..id..".json", "DATA") or "{}") or {} +local function store_config(id, tbl) + if not file.IsDir(path, "DATA") then + file.CreateDir(path) + end + file.Write(path .. id .. ".json", util.TableToJSON(tbl)) cache[id] = tbl - return tbl end local function get_config_value(id, key) @@ -83,7 +111,7 @@ do end end - table.insert(ent.pac_ignored_callbacks, {callback = callback, index = index}) + table.insert(ent.pac_ignored_callbacks, { callback = callback, index = index }) end function pac.CleanupEntityIgnoreBound(ent) @@ -194,7 +222,7 @@ local function generic_form(help) local pnl = vgui.Create("DListLayout") local label = pnl:Add("DLabel") - label:DockMargin(0,5,0,5) + label:DockMargin(0, 5, 0, 5) label:SetWrap(true) label:SetDark(true) label:SetAutoStretchVertical(true) @@ -207,18 +235,18 @@ local function player_list_form(name, id, help) local pnl = vgui.Create("DListLayout") local label = pnl:Add("DLabel") - label:DockMargin(0,5,0,5) + label:DockMargin(0, 5, 0, 5) label:SetWrap(true) label:SetDark(true) label:SetAutoStretchVertical(true) label:SetText(help) list_form(pnl, name, { - empty_message = L"No players online.", + empty_message = L "No players online.", name_left = "players", populate_left = function() - local blacklist = read_config(id) + local blacklist = cache[id] local tbl = {} for _, ply in ipairs(player.GetHumans()) do @@ -233,7 +261,7 @@ local function player_list_form(name, id, help) return tbl end, store_left = function(kv) - local tbl = read_config(id) + local tbl = cache[id] tbl[jsonid(kv.value)] = kv.name store_config(id, tbl) @@ -245,7 +273,7 @@ local function player_list_form(name, id, help) name_right = name, populate_right = function() local tbl = {} - for id, nick in pairs(read_config(id)) do + for id, nick in pairs(cache[id]) do local ply = pac.ReverseHash(id:sub(2), "Player") if ply == pac.LocalPlayer then continue end @@ -264,7 +292,7 @@ local function player_list_form(name, id, help) return tbl end, store_right = function(kv) - local tbl = read_config(id) + local tbl = cache[id] tbl[kv.value] = nil store_config(id, tbl) @@ -294,7 +322,7 @@ do for _, ply in ipairs(player.GetHumans()) do if ply == pac.LocalPlayer then continue end - local icon = menu:AddOption(L"wear only for " .. ply:Nick(), function() + local icon = menu:AddOption(L "wear only for " .. ply:Nick(), function() pace.WearParts(ply) end) icon:SetImage(pace.MiscIcons.wear) @@ -307,11 +335,11 @@ function pace.FillWearSettings(pnl) list:Dock(FILL) do - local cat = list:Add(L"wear filter") - cat.Header:SetSize(40,40) + local cat = list:Add(L "wear filter") + cat.Header:SetSize(40, 40) cat.Header:SetFont("DermaLarge") local list = vgui.Create("DListLayout") - list:DockPadding(20,20,20,20) + list:DockPadding(20, 20, 20, 20) cat:SetContents(list) local mode = vgui.Create("DComboBox", list) @@ -327,13 +355,15 @@ function pace.FillWearSettings(pnl) end if value == "steam friends" then - mode.form = generic_form(L"Only your steam friends can see your worn outfit.") + mode.form = generic_form(L "Only your steam friends can see your worn outfit.") elseif value == "whitelist" then - mode.form = player_list_form(L"whitelist", "wear_whitelist", L"Only the players in the whitelist can see your worn outfit.") + mode.form = player_list_form(L "whitelist", "wear_whitelist", + L "Only the players in the whitelist can see your worn outfit.") elseif value == "blacklist" then - mode.form = player_list_form( L"blacklist", "wear_blacklist", L"The players in the blacklist cannot see your worn outfit.") + mode.form = player_list_form(L "blacklist", "wear_blacklist", + L "The players in the blacklist cannot see your worn outfit.") elseif value == "disabled" then - mode.form = generic_form(L"Everyone can see your worn outfit.") + mode.form = generic_form(L "Everyone can see your worn outfit.") end GetConVar("pace_wear_filter_mode"):SetString(value:gsub(" ", "_")) @@ -347,11 +377,11 @@ function pace.FillWearSettings(pnl) end do - local cat = list:Add(L"outfit filter") - cat.Header:SetSize(40,40) + local cat = list:Add(L "outfit filter") + cat.Header:SetSize(40, 40) cat.Header:SetFont("DermaLarge") local list = vgui.Create("DListLayout") - list:DockPadding(20,20,20,20) + list:DockPadding(20, 20, 20, 20) cat:SetContents(list) local mode = vgui.Create("DComboBox", list) @@ -367,13 +397,15 @@ function pace.FillWearSettings(pnl) end if value == "steam friends" then - mode.form = generic_form(L"You will only see outfits from your steam friends.") + mode.form = generic_form(L "You will only see outfits from your steam friends.") elseif value == "whitelist" then - mode.form = player_list_form(L"whitelist", "outfit_whitelist", L"You will only see outfits from the players in the whitelist.") + mode.form = player_list_form(L "whitelist", "outfit_whitelist", + L "You will only see outfits from the players in the whitelist.") elseif value == "blacklist" then - mode.form = player_list_form(L"blacklist", "outfit_blacklist", L"You will see outfits from everyone except the players in the blacklist.") + mode.form = player_list_form(L "blacklist", "outfit_blacklist", + L "You will see outfits from everyone except the players in the blacklist.") elseif value == "disabled" then - mode.form = generic_form(L"You will see everyone's outfits.") + mode.form = generic_form(L "You will see everyone's outfits.") end GetConVar("pace_outfit_filter_mode"):SetString(value:gsub(" ", "_")) From b09268141ac163a7ba4bf0f4b6cd3198e548dd20 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 13 Jul 2024 15:09:53 -0400 Subject: [PATCH 204/300] DoT, hitpart adjustments, debug verbosity convar, extra healthbars control command damage zone: -damage over time -more adjustment to hit part / kill part feature -don't send the hit entities tables if not needed (pretty neat technique, using bools and reshaping the net receive structure, could save some sent data elsewhere) -hitparts can now be applied to the target entity with bones (instead of the floating marker entity), and it can assign to corpses. but it might have issues. the convar for this networking, pac_sv_damage_zone_allow_ragdoll_hitparts, is off by default and it will try to only send the ragdolls if the damagezone user has a damage zone with AttachPartsToTargetEntity left it will not however work with clientside ragdolls, we can't apply pacs to these non-entities after all might need some work and later adjustments based on the fact it can apply parts to players. convars are still disabling this by default though debugging cvar to reduce verbosity of server prints pac_healthbar command (like pac_proxy or pac_event) for doing some operations on extra healthbars manually, like set, add, subtract or refill --- lua/pac3/core/client/parts/damage_zone.lua | 453 ++++++++++++------ .../core/client/parts/health_modifier.lua | 31 +- lua/pac3/core/client/parts/lock.lua | 47 +- lua/pac3/extra/shared/net_combat.lua | 235 +++++++-- 4 files changed, 550 insertions(+), 216 deletions(-) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 156961d32..2b2e51829 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -120,6 +120,11 @@ BUILDER:StartStorableVars() :GetSet("ReverseDoNotKill",false, {description = "Heal only if health is above critical health;\nDamage only if health is below critical health\nIn other words, move away from the critical health"}) :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, {description = "Repeats your damage a few times. Subject to serverside convar."}) + :GetSet("DOTTime", 0, {editor_clamp = {0,32}}) + :GetSet("DOTCount", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,127)) end}) + :GetSet("NoInitialDOT", false, {description = "Skips the first instance (the instant one) of damage to achieve a delayed damage for example."}) :SetPropertyGroup("HitOutcome") :GetSetPart("HitSoundPart") :GetSetPart("KillSoundPart") @@ -130,13 +135,13 @@ BUILDER:StartStorableVars() :GetSet("AllowOverlappingHitSounds", false, {description = "If false, then when there are entities killed, do not play the hit sound part at the same time, since the kill sound takes priority"}) :GetSet("AllowOverlappingHitMarkers", false, {description = "If false, then for entities killed, do not spawn the hit marker part, since the kill marker takes priority and we don't want an overlap"}) :GetSet("RemoveDuplicateHitMarkers", true, {description = "If true, hit markers on an entity will be removed before creating a new one.\nBE WARNED. You still have a limited budget to create hit markers. It will be enforced."}) + :GetSet("AttachPartsToTargetEntity",false, {description = "hitparts will be applied to the target entity rather than on the floating hitmarker entity\nThis will require pac_sv_damage_zone_allow_ragdoll_hitparts to be set to 1 serverside"}) :GetSet("RemoveNPCWeaponsOnKill",false) BUILDER:EndStorableVars() - - +--[[UNUSED --a budget system to prevent mass abuse of hit marker parts function CalculateHitMarkerPrice(part) if not part then return end @@ -157,7 +162,7 @@ function HasBudget(owner, part) --print("budget:" .. string.NiceSize(owner.pac_dmgzone_hitmarker_budget)) return owner.pac_dmgzone_hitmarker_budget > 0 end -end +end]] function PART:LaunchAuditAndEnforceSoftBan(amount, reason) if reason == "recursive loop" then @@ -252,39 +257,111 @@ local part_setup_runtimes = 0 owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} ]] -function PART:FindOrCreateFloatingPart(owner, ent, part_uid, id) +local must_remove_class = { + entity, entity2, player_movement, weapon +} +local function CleanupParts(group) + for i,part in ipairs(group:GetChildrenList()) do + if must_remove_class[part.ClassName] then + part:Remove() + end + end +end + +function PART:FindOrCreateFloatingPart(owner, ent, part_uid, id, parent_ent) owner.hitmarker_partpool = owner.hitmarker_partpool or {} for spec_uid,tbl in pairs(owner.hitmarker_partpool) do if tbl.template_uid == part_uid then if not tbl.active then + local part = pac.GetPartFromUniqueID(pac.Hash(owner), spec_uid) + local group = part:GetRootPart() + group:CallRecursive("Think") return pac.GetPartFromUniqueID(pac.Hash(owner), spec_uid) --whoowee we found an already existing part end end end --what if we don't! local tbl = pac.GetPartFromUniqueID(pac.Hash(owner), part_uid):ToTable() - local group = pac.CreatePart("group", self:GetPlayerOwner()) --print("\tcreated a group for " .. id) + local group = pac.CreatePart("group", owner) --print("\tcreated a group for " .. id) + group:SetShowInEditor(false) - local part = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl)) + local part = pac.CreatePart(tbl.self.ClassName, owner, tbl, tostring(tbl)) group:AddChild(part) + CleanupParts(group) group:CallRecursive("Think") - owner.hitmarker_partpool[group.UniqueID] = {active = true, hitmarker_id = id, template_uid = part_uid, group_part_data = group} + owner.hitmarker_partpool[group.UniqueID] = {player_owner = self:GetPlayerOwner(), active = true, hitmarker_id = id, template_uid = part_uid, group_part_data = group} return group, owner.hitmarker_partpool[group.UniqueID] end +local ragdolls = {} + +net.Receive("pac_send_ragdoll", function(len) + local entindex = net.ReadUInt(12) + local rag = net.ReadEntity() + ragdolls[entindex] = rag + timer.Simple(2, function() ragdolls[entindex] = nil end) +end) + +local function TryAttachPartToAnEntity(self,group,parent_ent,marker_ent,killing) + local can_do_ragdolls = GetConVar("pac_sv_damage_zone_allow_ragdoll_hitparts"):GetBool() + if killing and can_do_ragdolls then + if isstring(killing) then + group:SetOwner(parent_ent) + return + end + end + if self.AttachPartsToTargetEntity then + --how to determine consent?? dunno I'll add a layer for outfit application consents if I ever implement pac sharing, but pac_sv_prop_outfits works for now + if parent_ent:IsPlayer() then + if killing and can_do_ragdolls then + timer.Simple(0.05, function() + local rag = parent_ent:GetRagdollEntity() + TryAttachPartToAnEntity(self,group,rag,marker_ent,false) + end) + return + end + if GetConVar("pac_sv_prop_outfits"):GetInt() == 2 then + group:SetOwnerName(parent_ent:EntIndex()) + else + group:SetOwner(marker_ent) + end + else + if killing and can_do_ragdolls then + local ent_index = parent_ent:EntIndex() + timer.Simple(0.05, function() + rag = ragdolls[ent_index] + if IsValid(rag) then + rag = rag + TryAttachPartToAnEntity(self,group,rag,ent, "ragdoll") + end + end) + return + end + group:SetOwnerName(parent_ent:EntIndex()) + end + + else + group:SetOwner(marker_ent) + end +end + local function FreeSpotInStack(owner) owner.hitparts = owner.hitparts or {} - for i=1,20,1 do - if owner.hitparts[i] then - if not owner.hitparts[i].active then + owner.hitparts_freespots = owner.hitparts_freespots or {} + for i=1,50,1 do + if owner.hitparts_freespots[i] == nil then owner.hitparts_freespots[i] = false return i end + if owner.hitparts_freespots[i] ~= false then + if owner.hitparts[i] then + if not owner.hitparts[i].active then + return i + end + else return i end - else - return i end end return nil @@ -295,9 +372,9 @@ end owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} ]] -local function MatchInStack(owner, ent, uid, id) +local function MatchInStack(owner, ent) owner.hitparts = owner.hitparts or {} - for i=1,20,1 do + for i=1,50,1 do if owner.hitparts[i] then if owner.hitparts[i].template_uid == ent.template_uid and owner.hitparts[i].hitmarker_id == ent.marker_id then return i @@ -312,7 +389,7 @@ end local function UIDMatchInStackForExistingPart(owner, ent, part_uid, ent_id) owner.hitparts = owner.hitparts or {} - for i=1,20,1 do + for i=1,50,1 do if owner.hitparts[i] then --print(i, "match compare:", owner.hitparts[i].active, owner.hitparts[i].specimen_part, owner.hitparts[i].hitmarker_id, owner.hitparts[i].template_uid == part_uid) if owner.hitparts[i].template_uid == part_uid then @@ -336,7 +413,9 @@ end owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} ]] -function PART:AddHitMarkerToStack(owner, ent, part_uid, ent_id) +function PART:AddHitMarkerToStack(index, owner, ent, part_uid, ent_id, parent_ent, killing) + --print("trying to add to stack:") + --print("\t\t",owner, ent, part_uid, ent_id, parent_ent) owner.hitparts = owner.hitparts or {} local free = FreeSpotInStack(owner) local returned_part = nil @@ -344,12 +423,15 @@ function PART:AddHitMarkerToStack(owner, ent, part_uid, ent_id) returned_part = existingpart if free and not existingpart then - local group, tbl = self:FindOrCreateFloatingPart(owner, ent, part_uid, ent_id) - owner.hitparts[free] = {active = true, specimen_part = group, hitmarker_id = ent_id, template_uid = part_uid} - returned_part = owner.hitparts[free].specimen_part + local group, tbl = self:FindOrCreateFloatingPart(owner, ent, part_uid, ent_id, parent_ent) + owner.hitparts[index] = {active = true, specimen_part = group, hitmarker_id = ent_id, template_uid = part_uid, csent = ent, parent_ent = parent_ent} + returned_part = owner.hitparts[index].specimen_part + TryAttachPartToAnEntity(self,group,parent_ent,ent, killing) else - owner.hitparts[free] = {active = true, specimen_part = returned_part, hitmarker_id = ent_id, template_uid = part_uid} + owner.hitparts[index] = {active = true, specimen_part = returned_part, hitmarker_id = ent_id, template_uid = part_uid, csent = ent, parent_ent = parent_ent} + TryAttachPartToAnEntity(self,existingpart,parent_ent,ent, killing) end + return returned_part end @@ -357,7 +439,7 @@ end local function RemoveHitMarker(owner, ent, uid, id) owner.hitparts = owner.hitparts or {} - local match = MatchInStack(owner, ent, uid, id) + local match = MatchInStack(owner, ent) if match then if owner.hitparts[match] then owner.hitparts[match].active = false @@ -369,7 +451,7 @@ local function RemoveHitMarker(owner, ent, uid, id) tbl.active = false tbl.group_part_data:SetHide(true) tbl.group_part_data:SetShowInEditor(false) - tbl.group_part_data:SetOwner(owner) + tbl.group_part_data:SetOwnerName(owner:EntIndex()) --print(tbl.group_part_data, "dormant") end end @@ -381,7 +463,8 @@ end owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data} owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid} ]] -function PART:AssignFloatingPartToEntity(part, owner, ent, parent_ent, template_uid, marker_id) +function PART:AssignFloatingPartToEntity(index, part, owner, ent, parent_ent, template_uid, marker_id) + if not IsValid(part) then return false end ent.pac_draw_distance = 0 @@ -389,10 +472,9 @@ function PART:AssignFloatingPartToEntity(part, owner, ent, parent_ent, template_ local group = part local part2 = group:GetChildrenList()[1] - group:CallRecursive("Think") - - owner.hitmarker_partpool[group.UniqueID] = {active = true, hitmarker_id = marker_id, template_uid = template_uid, group_part_data = group} - owner.hitparts[FreeSpotInStack(owner) or 1] = {active = true, specimen_part = group, hitmarker_id = marker_id, template_uid = template_uid} + owner.hitmarker_partpool[group.UniqueID] = {active = true, hitmarker_id = marker_id, template_uid = template_uid, group_part_data = group} + owner.hitparts[index] = {active = true, hitmarker_id = marker_id, template_uid = template_uid, specimen_part = group, csent = ent, parent_ent = parent_ent} + self.hitmarkers[group.UniqueID] = owner.hitmarker_partpool[group.UniqueID] parent_ent.pac_dmgzone_hitmarker_ents = parent_ent.pac_dmgzone_hitmarker_ents or {} ent.part = group @@ -402,15 +484,34 @@ function PART:AssignFloatingPartToEntity(part, owner, ent, parent_ent, template_ ent.marker_id = marker_id group:SetShowInEditor(false) - group:SetOwner(ent) - group.Owner = ent - group:SetHide(false) - part2:SetHide(false) - group:CallRecursive("Think") + + TryAttachPartToAnEntity(self,group,parent_ent,ent) + + + timer.Simple(0, function() group:SetHide(false) part2:SetHide(false) group:CallRecursive("Think") group:CallRecursive("CalcShowHide") end) + + + --print(parent_ent, group:IsHidden(), part2:IsHidden()) + + owner.hitparts_freespots[index] = false --print(group, "assigned to " .. marker_id .. " / " .. parent_ent:EntIndex()) end +function PART:ClearHitMarkers() + for uid, part in pairs(self.hitmarkers) do + if IsValid(part) then part:GetRootOwner():Remove() end + end + local ply = self:GetPlayerOwner() + if ply.hitparts then + for i,v in pairs(ply.hitparts) do + v.specimen_part:Remove() + end + end + ply.hitmarker_partpool = nil + ply.hitparts = nil +end + local function RecursedHitmarker(part) if part.HitMarkerPart == part or part.KillMarkerPart == part then return true @@ -496,8 +597,12 @@ local hitbox_ids = { ["Ray"] = 10 } +--the hit results net receiver needs to resolve to the part but UID strings is a bit weighty so partial UID are a compromise +local part_partialUID_caches = {} + --more compressed net message function PART:SendNetMessage() + part_partialUID_caches[string.sub(self.UniqueID,0,6)] = self pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if not GetConVar('pac_sv_damage_zone'):GetBool() then return end @@ -539,6 +644,13 @@ 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.WriteUInt(self.DOTCount, 7) + net.WriteUInt(math.ceil(math.Clamp(64*self.DOTTime, 0, 2047)), 11) + net.WriteString(string.sub(self.UniqueID,0,6)) + local using_hit_feedback = self.HitMarkerPart ~= nil or self.KillMarkerPart ~= nil + net.WriteBool(using_hit_feedback) net.SendToServer() end @@ -565,158 +677,197 @@ function PART:OnShow() else self:SendNetMessage() end +end - net.Receive("pac_hit_results", function() +local dmgzone_requesting_corpses = {} +function PART:SetAttachPartsToTargetEntity(b) + self.AttachPartsToTargetEntity = b + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + if self.KillMarkerPart == nil then return end + if b then + net.Start("pac_request_ragdoll_sends") + net.WriteBool(true) + net.SendToServer() + dmgzone_requesting_corpses[self] = true + else + dmgzone_requesting_corpses[self] = nil + if table.Count(dmgzone_requesting_corpses) == 0 then + net.Start("pac_request_ragdoll_sends") + net.WriteBool(false) + net.SendToServer() + end + end +end - local hit = net.ReadBool() - local kill = net.ReadBool() - local highest_dmg = net.ReadFloat() - local ents_hit = net.ReadTable() - local ents_kill = net.ReadTable() - part_setup_runtimes = 0 +net.Receive("pac_hit_results", function(len) + local uid = net.ReadString() or "" + local self = part_partialUID_caches[uid] + if not self then return end + local hit = net.ReadBool() + if hit then + self.dmgzone_hit_done = CurTime() + end + local kill = net.ReadBool() + if kill then + self.dmgzone_kill_done = CurTime() + end + local highest_dmg = net.ReadFloat() or 0 + --most damagezone won't use hitparts, skip the writetables + local do_ents_feedback = net.ReadBool() + local ents_hit = {} + local ents_kill = {} + if do_ents_feedback then + ents_hit = net.ReadTable(true) + if kill then ents_kill = net.ReadTable(true) end + end + part_setup_runtimes = 0 - if RecursedHitmarker(self) then - self:LaunchAuditAndEnforceSoftBan(nil,"recursive loop") - end + if RecursedHitmarker(self) then + self:LaunchAuditAndEnforceSoftBan(nil,"recursive loop") + end - local pos = self:GetWorldPosition() - local owner = self:GetPlayerOwner() + local pos = self:GetWorldPosition() + local owner = self:GetPlayerOwner() - self.lag_risk = table.Count(ents_hit) > 15 + self.lag_risk = table.Count(ents_hit) > 15 - local function ValidSound(part) - if part ~= nil then - if part.ClassName == "sound" or part.ClassName == "sound2" then - return true - end + local function ValidSound(part) + if part ~= nil then + if part.ClassName == "sound" or part.ClassName == "sound2" then + return true end - return false end - --grabbed the function from projectile.lua - --here, we spawn a static hitmarker and the max delay is 8 seconds - local function spawn(part, pos, ang, parent_ent, duration, owner) - if not IsValid(owner) then return end - if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker - --what if people employ a more roundabout method? CRACKDOWN! + return false + end + --grabbed the function from projectile.lua + --here, we spawn a static hitmarker and the max delay is 8 seconds + local function spawn(part, pos, ang, parent_ent, duration, owner, killing) + if not IsValid(owner) then return end + if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker + --what if people employ a more roundabout method? CRACKDOWN! - if not owner.hitparts then owner.hitparts = {} end + if not owner.hitparts then owner.hitparts = {} end - if owner.stop_hit_markers_until then - if owner.stop_hit_markers_until > CurTime() then return end - end - if self.lag_risk and math.random() > 0.5 then return end - if not self:IsValid() then return end - if not part:IsValid() then return end - - - local start = SysTime() - local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") - if not ent:IsValid() then return end - ent.is_pac_hitmarker = true - ent:SetNoDraw(true) - ent:SetOwner(self:GetPlayerOwner(true)) - ent:SetPos(pos) - ent:SetAngles(ang) - global_hitmarker_CSEnt_seed = global_hitmarker_CSEnt_seed + 1 - local csent_id = global_hitmarker_CSEnt_seed - - --the spawn order needs to decide whether it can or can't create an ent or part - - local flush = self.RemoveDuplicateHitMarkers - if flush then - --go through the entity and remove the clientside hitmarkers entities - if parent_ent.pac_dmgzone_hitmarker_ents then - for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do - if IsValid(ent2) then - if ent2.part:IsValid() then - --owner.pac_dmgzone_hitmarker_budget = owner.pac_dmgzone_hitmarker_budget + CalculateHitMarkerPrice(part) - ent2.part:SetHide(true) - end - --RemoveHitMarker(owner, ent, part.UniqueID, id) + if owner.stop_hit_markers_until then + if owner.stop_hit_markers_until > CurTime() then return end + end + if self.lag_risk and math.random() > 0.5 then return end + if not self:IsValid() then return end + if not part:IsValid() then return end + + + local start = SysTime() + local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") + if not ent:IsValid() then return end + ent.is_pac_hitmarker = true + ent:SetNoDraw(true) + ent:SetOwner(self:GetPlayerOwner()) + ent:SetPos(pos) + ent:SetAngles(ang) + global_hitmarker_CSEnt_seed = global_hitmarker_CSEnt_seed + 1 + local csent_id = global_hitmarker_CSEnt_seed + + --the spawn order needs to decide whether it can or can't create an ent or part + + local flush = self.RemoveDuplicateHitMarkers + if flush then + --go through the entity and remove the clientside hitmarkers entities + if parent_ent.pac_dmgzone_hitmarker_ents then + for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do + if IsValid(ent2) then + if ent2.part:IsValid() then + ent2.part:SetHide(true) end end end end + end - if FreeSpotInStack(owner) then - if part:IsValid() then --self:AttachToEntity(part, ent, parent_ent, global_hitmarker_CSEnt_seed) - local newpart - local bool = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) - - newpart = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) or self:AddHitMarkerToStack(owner, ent, part.UniqueID, csent_id) - - self:AssignFloatingPartToEntity(newpart, owner, ent, parent_ent, part.UniqueID, csent_id) + local free_spot = FreeSpotInStack(owner) + + if free_spot then + if part:IsValid() then --self:AttachToEntity(part, ent, parent_ent, global_hitmarker_CSEnt_seed) + --print("free spot should be " .. free_spot) + local newpart + local bool = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id) + if bool then + newpart = bool + --print("\tpart is existing") + else + newpart = self:AddHitMarkerToStack(free_spot, owner, ent, part.UniqueID, csent_id, parent_ent, killing) + --print("\tpart should be added") + end - if self.Preview then MsgC("hitmarker:", bool and Color(0,255,0) or Color(0,200,255), bool and "existing" or "created", " : ", newpart, "\n") end - timer.Simple(math.Clamp(duration, 0, 8), function() - if ent:IsValid() then - if parent_ent.pac_dmgzone_hitmarker_ents then - for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do - if IsValid(ent2) then - RemoveHitMarker(owner, ent2, part.UniqueID, id) - --SafeRemoveEntity(ent2) - end + self:AssignFloatingPartToEntity(free_spot, newpart, owner, ent, parent_ent, part.UniqueID, csent_id) + + if self.Preview then MsgC("hitmarker:", bool and Color(0,255,0) or Color(0,200,255), bool and "existing" or "created", " : ", newpart, "\n") end + timer.Simple(math.Clamp(duration, 0, 8), function() + if ent:IsValid() then + if parent_ent.pac_dmgzone_hitmarker_ents then + for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do + if IsValid(ent2) then + RemoveHitMarker(owner, ent2, part.UniqueID, id) + owner.hitparts_freespots[free_spot] = true + --SafeRemoveEntity(ent2) end end end - end) - end + end + end) end + end - local creation_delta = SysTime() - start + local creation_delta = SysTime() - start - return creation_delta - end + return creation_delta + end - if hit then - self.dmgzone_hit_done = CurTime() - --try not to play both sounds at once - if ValidSound(self.HitSoundPart) then - --if can overlap, always play - if self.AllowOverlappingHitSounds then - self.HitSoundPart:PlaySound() - --if cannot overlap, only play if there's only one entity or if we didn't kill - elseif (table.Count(ents_kill) == 1) or not (kill and ValidSound(self.KillSoundPart)) then - self.HitSoundPart:PlaySound() - end + if hit then + --try not to play both sounds at once + if ValidSound(self.HitSoundPart) then + --if can overlap, always play + if self.AllowOverlappingHitSounds then + self.HitSoundPart:PlaySound() + --if cannot overlap, only play if there's only one entity or if we didn't kill + elseif (table.Count(ents_kill) <= 1) or not (kill and ValidSound(self.KillSoundPart)) then + self.HitSoundPart:PlaySound() end - if self.HitMarkerPart then - for ent,_ in pairs(ents_hit) do - if IsValid(ent) then - local ang = (ent:GetPos() - pos):Angle() - if ents_kill[ent] then - if self.AllowOverlappingHitMarkers then - part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) - end - else + end + if self.HitMarkerPart then + for _,ent in ipairs(ents_hit) do + if IsValid(ent) then + local ang = (ent:GetPos() - pos):Angle() + if ents_kill[ent] then + if self.AllowOverlappingHitMarkers then part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) end + else + part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0) end end end end - if kill then - self.dmgzone_kill_done = CurTime() - if ValidSound(self.KillSoundPart) then - self.KillSoundPart:PlaySound() - end - if self.KillMarkerPart then - for ent,_ in pairs(ents_kill) do - if IsValid(ent) then - local ang = (ent:GetPos() - pos):Angle() - part_setup_runtimes = part_setup_runtimes + (spawn(self.KillMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.KillMarkerLifetime, owner) or 0) - end + end + if kill then + self.dmgzone_kill_done = CurTime() + if ValidSound(self.KillSoundPart) then + self.KillSoundPart:PlaySound() + end + if self.KillMarkerPart then + for _,ent in ipairs(ents_kill) do + if IsValid(ent) then + local ang = (ent:GetPos() - pos):Angle() + part_setup_runtimes = part_setup_runtimes + (spawn(self.KillMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.KillMarkerLifetime, owner, true) or 0) end end end - if self.HitMarkerPart or self.KillMarkerPart then - if owner.hitparts then - self:SetInfo(table.Count(owner.hitparts) .. " hitmarkers in slot") - end + end + if self.HitMarkerPart or self.KillMarkerPart then + if owner.hitparts then + self:SetInfo(table.Count(owner.hitparts) .. " hitmarkers in slot") end - end) - -end + end +end) concommand.Add("pac_cleanup_damagezone_hitmarks", function() if LocalPlayer().hitparts then @@ -738,6 +889,7 @@ function PART:OnHide() end function PART:OnRemove() + part_partialUID_caches[string.sub(self.UniqueID,0,6)] = nil pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox") for _,v in pairs(renderhooks) do pac.RemoveHook(v, "pace_draw_hitbox") @@ -998,6 +1150,7 @@ function PART:BuildCone(obj) end function PART:Initialize() + 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 timer.Simple(0.1, function() --jank fix on the jank fix to allow it earlier on projectiles and hitmarkers diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 0f26a8c77..10a2fa721 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -32,6 +32,7 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() +local part_UID_caches = {} function PART:SendModifier(str) if self:IsHidden() then return end @@ -45,6 +46,8 @@ function PART:SendModifier(str) if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end end + part_UID_caches[self.UniqueID] = self + if self.Name ~= "" then part_UID_caches[self.Name] = self end if str == "MaxHealth" and self.ChangeHealth then net.Start("pac_request_healthmod") @@ -135,6 +138,7 @@ function PART:SetDamageMultiplier(val) end function PART:OnRemove() + part_UID_caches = {} --we'll need this part removed from the cache if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if util.NetworkStringToID( "pac_request_healthmod" ) == 0 then return end local found_remaining_healthmod = false @@ -214,5 +218,30 @@ function PART:UpdateHPBars() end end +--expected structure : pac_healthbars uid_or_name action number +--actions: set, add, subtract, refill, replenish, remove +concommand.Add("pac_healthbar", function(ply, cmd, args) + local uid_or_name = args[1] + local num = tonumber(args[3]) or 0 + if part_UID_caches[uid_or_name] ~= nil and args[2] ~= nil then + local part = part_UID_caches[uid_or_name] + uid = part.UniqueID + local action = args[2] or "" + + --doesnt make sense to add or subtract 0 + if ((action == "add" or action == "subtract") and num == 0) or (action == "") then return end + --replenish means set to full + if action == "refill" or action == "replenish" then + action = "set" + num = part.BarsAmount * part.HealthBars + end + if action == "remove" then action = "set" num = 0 end + net.Start("pac_request_extrahealthbars_action") + net.WriteString(uid) + net.WriteString(action) + net.WriteInt(num, 16) + net.SendToServer() + end +end, nil, "changes your health modifier's extra health value. arguments:\nuid or name: the unique ID or the name of the part\naction: add, subtract, refill, replenish, remove, set\nnumber") -BUILDER:Register() +BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 7fe0b53eb..788e90f8a 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -7,14 +7,23 @@ local Matrix = Matrix local physics_point_ent_classes = { ["prop_physics"] = true, ["prop_physics_multiplayer"] = true, + ["prop_physics_respawnable"] = true, ["prop_ragdoll"] = true, ["weapon_striderbuster"] = true, ["item_item_crate"] = true, ["func_breakable_surf"] = true, ["func_breakable"] = true, - ["physics_cannister"] = true + ["physics_cannister"] = true, + ["npc_satchel"] = true, + ["npc_grenade_frag"] = true, } +local convar_lock = GetConVar("pac_sv_lock") +local convar_lock_grab = GetConVar("pac_sv_lock_grab") +local convar_lock_max_grab_radius = GetConVar("pac_sv_lock_max_grab_radius") +local convar_lock_teleport = GetConVar("pac_sv_lock_teleport") +local convar_combat_enforce_netrate = GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") + local BUILDER, PART = pac.PartTemplate("base_movable") @@ -58,9 +67,8 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:OnThink() - - if not GetConVar('pac_sv_lock'):GetBool() then return end - if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end + if not convar_lock:GetBool() then return end + if util.NetworkStringToID("pac_request_position_override_on_entity_grab") == 0 then self:SetError("This part is deactivated on the server") return end pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return end @@ -79,7 +87,7 @@ function PART:OnThink() end if self.Mode == "Grab" then - if not GetConVar('pac_sv_lock_grab'):GetBool() then return end + if not convar_lock_grab:GetBool() then return end if pac.Blocked_Combat_Parts then if pac.Blocked_Combat_Parts[self.ClassName] then return @@ -90,7 +98,7 @@ function PART:OnThink() end self:CheckEntValidity() if self.valid_ent then - local final_ang = Angle(0,0,0) + local final_ang = Angle(0, 0, 0) if self.OverrideAngles then --if overriding angles if self.is_first_time then self.default_ang = self.target_ent:GetAngles() --record the initial ent angles @@ -120,8 +128,10 @@ function PART:OnThink() local relative_offset_pos = offset_matrix:GetTranslation() local relative_offset_ang = offset_matrix:GetAngles() - if pac.LocalPlayer == self:GetPlayerOwner() then - if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + local ply_owner = self:GetPlayerOwner() + + if pac.LocalPlayer == ply_owner then + if not convar_combat_enforce_netrate:GetBool() then if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end end net.Start("pac_request_position_override_on_entity_grab") @@ -140,7 +150,7 @@ function PART:OnThink() if self.target_ent:IsPlayer() then if self.OverrideEyeAngles then try_override_eyeang = true end end - if pac.LocalPlayer == self:GetPlayerOwner() then + if pac.LocalPlayer == ply_owner then net.WriteBool(self.OverrideAngles) net.WriteBool(try_override_eyeang) net.WriteBool(self.NoCollide) @@ -184,18 +194,16 @@ function PART:OnThink() local eyeang = self.target_ent:EyeAngles() --print("eyeang", eyeang) - eyeang.p = 0*eyeang.p + eyeang.p = 0 eyeang.y = eyeang.y - eyeang.r = 0*eyeang.r + eyeang.r = 0 mat:Rotate(final_ang - eyeang) --this works --mat:Rotate(eyeang) --print("transform ang", final_ang) --print("part's angles", self:GetWorldAngles()) --mat:Rotate(self:GetWorldAngles()) - self.target_ent:EnableMatrix("RenderMultiply", mat) - end self.grabbing = true @@ -245,10 +253,8 @@ do if part then if part.ClassName == "lock" then part:BreakLock(target_to_release) - end end - else MsgC(Color(200, 200, 200), "NOW! WE SEARCH YOUR LOCAL PARTS!\n") for i,part in pairs(pac.GetLocalParts()) do @@ -267,6 +273,8 @@ do net.Receive("pac_mark_grabbed_ent", function(len) local target_to_mark = net.ReadEntity() + if not IsValid(target_to_mark) then return end + if target_to_mark:EntIndex() == 0 then return end local successful_grab = net.ReadBool() local uid = net.ReadString() local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid) @@ -279,13 +287,12 @@ do target_to_mark.IsGrabbedID = uid target_to_mark:SetGravity(0) end - end) end function PART:SetRadius(val) self.Radius = val - local sv_dist = GetConVar("pac_sv_lock_max_grab_radius"):GetInt() + local sv_dist = convar_lock_max_grab_radius:GetInt() if self.Radius > sv_dist then self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist) else @@ -294,7 +301,7 @@ function PART:SetRadius(val) end function PART:OnShow() - if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end + if util.NetworkStringToID("pac_request_position_override_on_entity_grab") == 0 then self:SetError("This part is deactivated on the server") return end local origin_part self.is_first_time = true if self.resetting_condition or self.forcebreak then @@ -303,7 +310,7 @@ function PART:OnShow() self.resetting_condition = false end end - pac.AddHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID, function() + pac.AddHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview" .. self.UniqueID, function() if self.TargetPart:IsValid() then origin_part = self.TargetPart else @@ -446,7 +453,7 @@ function PART:DecideTarget() elseif self.PhysicsProps and (physics_point_ent_classes[ent_candidate:GetClass()] or string.find(ent_candidate:GetClass(),"item_") or string.find(ent_candidate:GetClass(),"ammo_")) then chosen_ent = ent_candidate table.insert(ents_candidates, ent_candidate) - elseif self.NPC and (ent_candidate:IsNPC() or ent_candidate.IsDrGEntity or ent_candidate.IsVJBaseSNPC) then + elseif self.NPC and (ent_candidate:IsNPC() or ent_candidate:IsNextBot() or ent_candidate.IsDrGEntity or ent_candidate.IsVJBaseSNPC) then chosen_ent = ent_candidate table.insert(ents_candidates, ent_candidate) end diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index a81418816..0c30eba3e 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -22,6 +22,9 @@ local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "200 local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length") local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius") local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone") +local damagezone_allow_damageovertime = CreateConVar("pac_sv_damage_zone_allow_damage_over_time", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage over time for damagezone") +local damagezone_max_damageovertime_total_time = CreateConVar("pac_sv_damage_zone_max_damage_over_time_total_time", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum time that a DoT instance is allowed to last in total.\nIf your tick time multiplied by the count is beyond that, it will compress the ticks, but if your total time is more than 200% of the limit, it will reject the attack") +local damagezone_allow_ragdoll_networking_for_hitpart = CreateConVar("pac_sv_damage_zone_allow_ragdoll_hitparts", "0", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to send information about corpses to all clients when a player's damage zone needs it for attaching hitparts") local lock_allow = CreateConVar("pac_sv_lock", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") @@ -44,6 +47,7 @@ local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_da local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") cvars.AddChangeCallback("pac_sv_block_combat_features_on_next_restart", function() print("Remember that pac_sv_block_combat_features_on_next_restart is applied on server startup! Only do it if you know what you're doing. You'll need to restart the server.") end) +local debugging = CreateConVar("pac_sv_combat_debugging", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to get log prints for combat activity. If a player targets too many entities or sends messages too often, it will say it in the server console.") local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter") local netrate_allowance = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 60, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "the budgeted allowance to limit how many pac combat net messages can be sent in bursts. 0 to disable, otherwise a number of net messages of allowance.") local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_netrate_monitor_serverside", 0, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not to let clients enforce their net message rates.\nSet this to 1 to get serverside prints telling you whenever someone is going over their allowance, but it'll still take the network bandwidth.\nSet this to 0 to let clients enforce their net rate and save some bandwidth but the server won't know who's spamming net messages.") @@ -119,6 +123,13 @@ if SERVER then ["keyframe_rope"] = true, ["env_soundscape_proxy"] = true, ["gmod_hands"] = true, + ["env_lightglow"] = true, + ["point_spotlight"] = true, + ["spotlight_end"] = true, + ["beam"] = true, + ["info_target"] = true, + ["func_lod"] = true, + } @@ -223,7 +234,20 @@ if SERVER then armor = -1, } + local when_to_print_messages = {} + local can_print = {} + local function CountDebugMessage(ply) + if CurTime() < when_to_print_messages[ply] then + can_print[ply] = false + else + can_print[ply] = true + end + when_to_print_messages[ply] = CurTime() + 1 + end local function CountNetMessage(ply) + if can_print[ply] == nil then can_print[ply] = true end + when_to_print_messages[ply] = when_to_print_messages[ply] or 0 + local stime = SysTime() local ms_basis = enforce_netrate:GetInt()/1000 local base_allowance = netrate_allowance:GetInt() @@ -348,25 +372,31 @@ if SERVER then end --stopping condition to stop force or damage operation if too many entities, because net impact is proportional to players - local function TooManyEnts(count) + local function TooManyEnts(count, ply) local playercount = player.GetCount() local hard_limit = raw_ent_limit:GetInt() local per_ply = per_ply_limit:GetInt() --print(count .. " compared against hard limit " .. hard_limit .. " and " .. playercount .. " players*" .. per_ply .. " limit (" .. count*playercount .. " | " .. playercount*per_ply .. ")") if count > hard_limit then - MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond hard limit.\n") + if debugging:GetBool() and can_print[ply] then + MsgC(Color(255,255,0), "[PAC3] : ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " TOO MANY ENTS (" .. count .. "). Beyond hard limit (".. hard_limit ..")\n") + end return true end - if not game.SinglePlayer() then + --if not game.SinglePlayer() then if count > per_ply_limit:GetInt() * playercount then - MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond per-player sending limit.\n") + if debugging:GetBool() and can_print[ply] then + MsgC(Color(255,255,0), "[PAC3] : ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " TOO MANY ENTS (" .. count .. "). Beyond per-player sending limit (".. per_ply_limit:GetInt() .." per player)\n") + end return true end if count * playercount > math.min(hard_limit, per_ply*playercount) then - MsgC(Color(255,0,0), "TOO MANY ENTS. Beyond hard limit or player limit\n") + if debugging:GetBool() and can_print[ply] then + MsgC(Color(255,255,0), "[PAC3] : ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " TOO MANY ENTS (" .. count .. "). Beyond hard limit or player limit (" .. math.min(hard_limit, per_ply*playercount) .. ")\n") + end return true end - end + --end return false end @@ -532,15 +562,15 @@ if SERVER then local function UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) local existing_uidlayer = true local healthvalue = 0 - if not ply.pac_healthbars then + if ply.pac_healthbars == nil then existing_uidlayer = false ply.pac_healthbars = {} end - if not ply.pac_healthbars[layer] then + if ply.pac_healthbars[layer] == nil then existing_uidlayer = false ply.pac_healthbars[layer] = {} end - if not ply.pac_healthbars[layer][part_uid] then + if ply.pac_healthbars[layer][part_uid] == nil then existing_uidlayer = false ply.pac_healthbars[layer][part_uid] = num*barsize healthvalue = num*barsize @@ -570,8 +600,32 @@ if SERVER then end + local function UpdateHealthBarsFromCMD(ply, action, num, part_uid) + if ply.pac_healthbars == nil then return end + + local target_tbl + for checklayer,tbl in pairs(ply.pac_healthbars) do + if tbl[part_uid] ~= nil then + target_tbl = tbl + end + end + + if target_tbl == nil then return end + + --actions: set, add, subtract, replenish, remove + if action == "set" then + target_tbl[part_uid] = num + elseif action == "add" then + target_tbl[part_uid] = math.max(target_tbl[part_uid] + num,0) + elseif action == "subtract" then + target_tbl[part_uid] = math.max(target_tbl[part_uid] - num,0) + elseif action == "remove" then + target_tbl[part_uid] = nil + end + end + local function GatherExtraHPBars(ply) - if not ply.pac_healthbars then return 0,nil end + if ply.pac_healthbars == nil then return 0,nil end local built_tbl = {} local total_hp_value = 0 @@ -748,14 +802,14 @@ if SERVER then local ply_count = 0 local ply_prog_count = 0 for i,v in pairs(ents_hits) do - if not (v:IsPlayer() or Is_NPC(v)) and not tbl.PointEntities then ents_hits[i] = nil end - if v.CPPICanDamage and not v:CPPICanDamage(ply) then ents_hits[i] = nil end --CPPI check on the player - if v:IsConstraint() then ents_hits[i] = nil end + if not (v:IsPlayer() or Is_NPC(v)) and not tbl.PointEntities then ents_hits[i] = nil continue end + if v.CPPICanDamage and not v:CPPICanDamage(ply) then ents_hits[i] = nil continue end --CPPI check on the player + if v:IsConstraint() then ents_hits[i] = nil continue end - if not NPCDispositionAllowsIt(ply, v) then ents_hits[i] = nil end + if not NPCDispositionAllowsIt(ply, v) then ents_hits[i] = nil continue end if NPCDispositionIsFilteredOut(ply,v, tbl.FilterFriendlies, tbl.FilterNeutrals, tbl.FilterHostiles) then ents_hits[i] = nil end - if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil + if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil continue else ent_count = ent_count + 1 --print(v, "counted") @@ -763,9 +817,9 @@ if SERVER then end end + --dangerous conditions: absurd amounts of entities, damaging a large percentage of the server's players beyond a certain point - if TooManyEnts(ent_count) or ((ply_count) > 12 and (ply_count > player_fraction:GetFloat() * player.GetCount())) then - print("early exit") + if TooManyEnts(ent_count, ply) or ((ply_count) > 12 and (ply_count > player_fraction:GetFloat() * player.GetCount())) then return false,false,nil,{},{} end @@ -932,7 +986,7 @@ if SERVER then dmg_info:SetDamage(fraction * tbl.Damage) end - successful_hit_ents[ent] = true + table.insert(successful_hit_ents,ent) --fire bullets if asked local ents2 = {inflictor} if tbl.Bullet then @@ -1004,7 +1058,11 @@ if SERVER then if tbl.DoNotKill then kill = false --durr end - if kill then successful_kill_ents[ent] = true end + if kill then + table.insert(successful_kill_ents,ent) + ent.pac_damagezone_need_send_ragdoll = true + ent.pac_damagezone_killer = ply + end --remove weapons on kill if asked if kill and not ent:IsPlayer() and tbl.RemoveNPCWeaponsOnKill and pac_sv_damage_zone_allow_dissolve then @@ -1079,11 +1137,34 @@ if SERVER then timer.Simple(ply_prog_count / 32, function() DoDamage(ent) end) ply_prog_count = ply_prog_count + 1 else - DoDamage(ent) + if tbl.DOTMode then + 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) + else + if timer.Exists(timer_entid) then + timer.Adjust(tbl.UniqueID, tbl.DOTTime, counts) + else + timer.Create(timer_entid, tbl.DOTTime, counts, function() + if not IsValid(ent) then timer.Remove(timer_entid) return end + DoDamage(ent) + end) + end + end + + end + if not tbl.NoInitialDOT then DoDamage(ent) end end end if not hit and (oldhp > 0 and canhit) then hit = true end end + if IsValid(ent) then + if kill then + timer.Remove(tbl.UniqueID .. "_" .. ent:GetClass() .. "_" .. ent:EntIndex()) + end + return + end return hit,kill,dmg,successful_hit_ents,successful_kill_ents end @@ -1157,7 +1238,7 @@ if SERVER then ["HunterTracer"] = 7, ["StriderTracer"] = 8, ["GunshipTracer"] = 9, - ["ToolgunTracer"] = 10, + ["ToolTracer"] = 10, ["LaserTracer"] = 11 } @@ -1171,7 +1252,7 @@ if SERVER then if pre_excluded_ent_classes[v:GetClass()] or (Is_NPC(v) and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectSelf)) then ents_hits[i] = nil else ent_count = ent_count + 1 end end - if TooManyEnts(ent_count) and not (tbl.AffectSelf and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end + if TooManyEnts(ent_count, ply) and not (tbl.AffectSelf and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end for _,ent in pairs(ents_hits) do local phys_ent local ent_getphysobj = ent:GetPhysicsObject() @@ -1472,7 +1553,7 @@ if SERVER then ply:SetMoveType(ply.default_movetype) targ_ent.default_movetype_reserved = nil end - MsgC(Color(0,255,255), "Requesting lock break!\n") + if debugging:GetBool() and can_print[ply] then MsgC(Color(0,255,255), "Requesting lock break!\n") end if ply.grabbed_by then --directly go for the grabbed_by player net.Start("pac_request_lock_break") @@ -1586,13 +1667,10 @@ if SERVER then tbl.UniqueID = net.ReadString() if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then - MsgC(Color(255,255,0), "[PAC3] Force part: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") - end - + if debugging:GetBool() and can_print[ply] then MsgC(Color(255,255,0), "[PAC3] Force part: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n") end hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID) active_force_ids[tbl.UniqueID] = nil - + CountDebugMessage(ply) return end @@ -1680,10 +1758,36 @@ if SERVER then end) end + + local active_DoT = {} + local requesting_corpses = {} local function DeclareDamageZoneReceivers() + --networking for damagezone hitparts on corpses + hook.Add("CreateEntityRagdoll", "pac_ragdoll_assign", function(ent, rag) + if not ent.pac_damagezone_need_send_ragdoll then return end + if not ent.pac_damagezone_killer then return end + if not damagezone_allow:GetBool() then return end + if not damagezone_allow_ragdoll_networking_for_hitpart:GetBool() then return end + if not PlayerIsCombatAllowed(ent.pac_damagezone_killer) then return end + if not requesting_corpses[ent.pac_damagezone_killer] then return end + net.Start("pac_send_ragdoll") + net.WriteUInt(ent:EntIndex(), 12) + net.WriteEntity(rag) + net.Broadcast() + end) + + net.Receive("pac_request_ragdoll_sends", function(len, ply) + local b = net.ReadBool() + if not damagezone_allow:GetBool() then return end + if not PlayerIsCombatAllowed(ply) then return end + requesting_corpses[ply] = b + end) + util.AddNetworkString("pac_request_zone_damage") util.AddNetworkString("pac_hit_results") + util.AddNetworkString("pac_request_ragdoll_sends") + util.AddNetworkString("pac_send_ragdoll") net.Receive("pac_request_zone_damage", function(len,ply) --server allow if not damagezone_allow:GetBool() then return end @@ -1691,9 +1795,11 @@ if SERVER then --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then - MsgC(Color(255,255,0), "[PAC3] Damage zone: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + if debugging:GetBool() and can_print[ply] then + MsgC(Color(255,255,0), "[PAC3] Damage zone: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n") + can_print[ply] = false end + CountDebugMessage(ply) return end @@ -1730,6 +1836,18 @@ if SERVER then tbl.CriticalHealth = net.ReadUInt(16) tbl.RemoveNPCWeaponsOnKill = net.ReadBool() + tbl.DOTMode = net.ReadBool() + tbl.NoInitialDOT = net.ReadBool() + tbl.DOTCount = net.ReadUInt(7) + tbl.DOTTime = net.ReadUInt(11) / 64 + tbl.UniqueID = net.ReadString() + local do_ents_feedback = net.ReadBool() + if not tbl.UniqueID then return end + + if tbl.DOTTime == 0 or (tbl.DOTCount == 0 and not tbl.NoInitialDOT) then + tbl.DOTMode = false + end + local dmg_info = DamageInfo() --server limits @@ -1891,13 +2009,23 @@ if SERVER then hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply) highest_dmg = highest_dmg or 0 net.Start("pac_hit_results", true) + net.WriteString(tbl.UniqueID) net.WriteBool(hit) net.WriteBool(kill) net.WriteFloat(highest_dmg) - net.WriteTable(successful_hit_ents) - net.WriteTable(successful_kill_ents) + net.WriteBool(hit and do_ents_feedback) + if successful_hit_ents and hit and do_ents_feedback and #successful_hit_ents < 20 then + local _,bits_before_hit = net.BytesWritten() + net.WriteTable(successful_hit_ents, true) + local _,bits_after_hit = net.BytesWritten() + --print("table is length " .. bits_after_hit - bits_before_hit .. " for " .. table.Count(successful_hit_ents) .. " ents hit, or about " .. ((bits_after_hit - bits_before_hit) / table.Count(successful_hit_ents) - 16) .. " per ent") + if kill then net.WriteTable(successful_kill_ents, true) end + local _,bits_after_kill = net.BytesWritten() + --print("table is length " .. bits_after_kill - bits_after_hit .. " for " .. table.Count(successful_kill_ents) .. " ents killed, or about " .. ((bits_after_kill - bits_after_hit) / table.Count(successful_kill_ents) - 16) .. " per ent") + end net.Broadcast() end) + end local nextchecklock = CurTime() @@ -1920,9 +2048,11 @@ if SERVER then --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then - MsgC(Color(255,255,0), "[PAC3] Lock grab: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + if debugging:GetBool() and can_print[ply] then + MsgC(Color(255,255,0), "[PAC3] Lock grab: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n") + can_print[ply] = false end + CountDebugMessage(ply) return end @@ -2025,7 +2155,7 @@ if SERVER then if targ_ent:IsPlayer() and targ_ent:InVehicle() then --yank player out of vehicle - print("Kicking " .. targ_ent:Nick() .. " out of vehicle to be grabbed!") + if debugging:GetBool() and can_print[ply] then print("Kicking " .. targ_ent:Nick() .. " out of vehicle to be grabbed!") end targ_ent:ExitVehicle() end @@ -2046,7 +2176,7 @@ if SERVER then net.Send(targ_ent) targ_ent.nextcalcviewTick = CurTime() + 0.1 targ_ent.has_calcview = true - else print("skipping") end + end targ_ent:SetEyeAngles(alt_ang) targ_ent:SetAngles(alt_ang) else @@ -2085,7 +2215,7 @@ if SERVER then auth_ent_owner.grabbed_ents[targ_ent] = true targ_ent.grabbed_by = auth_ent_owner targ_ent.grabbed_by_uid = lockpart_UID - print(auth_ent, "grabbed", targ_ent, "owner grabber is", auth_ent_owner) + if debugging:GetBool() and can_print[ply] then print(auth_ent, "grabbed", targ_ent, "owner grabber is", auth_ent_owner) end end targ_ent.grabbed_by_time = CurTime() else @@ -2095,7 +2225,7 @@ if SERVER then end if need_breakup then - print("stop this now! reason: " .. breakup_condition) + if debugging:GetBool() and can_print[ply] then print("stop this now! reason: " .. breakup_condition) end net.Start("pac_request_lock_break") net.WriteEntity(targ_ent) net.WriteString(lockpart_UID) @@ -2127,9 +2257,11 @@ if SERVER then --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then - MsgC(Color(255,255,0), "[PAC3] Lock teleport: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + if debugging:GetBool() and can_print[ply] then + MsgC(Color(255,255,0), "[PAC3] Lock teleport: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n") + can_print[ply] = false end + CountDebugMessage(ply) return end @@ -2206,9 +2338,11 @@ if SERVER then --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then - MsgC(Color(255,255,0), "[PAC3] Hitscan: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + if debugging:GetBool() and can_print[ply] then + MsgC(Color(255,255,0), "[PAC3] Hitscan: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n") + can_print[ply] = false end + CountDebugMessage(ply) return end @@ -2295,17 +2429,21 @@ if SERVER then end) end + local active_healthmod_bars = {} local function DeclareHealthModifierReceivers() util.AddNetworkString("pac_request_healthmod") util.AddNetworkString("pac_update_healthbars") + util.AddNetworkString("pac_request_extrahealthbars_action") net.Receive("pac_request_healthmod", function(len,ply) if not healthmod_allow:GetBool() then return end --netrate enforce if not CountNetMessage(ply) then - if netrate_enforcement_sv_monitoring:GetBool() then - MsgC(Color(255,255,0), "[PAC3] Health modifier: ") MsgC(Color(255,0,0), ply, " over allowance or delay!\n") + if debugging:GetBool() and can_print[ply] then + MsgC(Color(255,255,0), "[PAC3] Health modifier: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n") + can_print[ply] = false end + CountDebugMessage(ply) return end @@ -2356,6 +2494,7 @@ if SERVER then local follow = net.ReadBool() UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) + active_healthmod_bars[part_uid] = ply.pac_healthmods[part_uid] elseif action == "OnRemove" then if ply.pac_damage_scalings then @@ -2372,6 +2511,13 @@ if SERVER then end SendUpdateHealthBars(ply) end) + net.Receive("pac_request_extrahealthbars_action", function(len, ply) + local part_uid = net.ReadString() + local action = net.ReadString() + local num = net.ReadInt(16) + UpdateHealthBarsFromCMD(ply, action, num, part_uid) + SendUpdateHealthBars(ply) + end) end --[[util.AddNetworkString("pac_hitscan") @@ -2659,4 +2805,3 @@ if CLIENT then hook.Add("InitPostEntity", "PAC_Request_BlockedParts_On_Join", RequestBlockedParts) pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {} end - From 696f4d5123e6ba6cbe7286b5aafb5416f9279864 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 13 Jul 2024 15:25:06 -0400 Subject: [PATCH 205/300] hotfix remove recent code that didn't end up being used --- lua/pac3/extra/shared/net_combat.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 0c30eba3e..44f7ee392 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -2429,7 +2429,6 @@ if SERVER then end) end - local active_healthmod_bars = {} local function DeclareHealthModifierReceivers() util.AddNetworkString("pac_request_healthmod") util.AddNetworkString("pac_update_healthbars") @@ -2494,7 +2493,6 @@ if SERVER then local follow = net.ReadBool() UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) - active_healthmod_bars[part_uid] = ply.pac_healthmods[part_uid] elseif action == "OnRemove" then if ply.pac_damage_scalings then From c51845915dc73a4efb3cb33f1766f6995b408a52 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 14 Jul 2024 00:01:41 -0400 Subject: [PATCH 206/300] warn when insufficient args are given for some commands for pac_healthbar, pac_event, +pac_event and pac_proxy and added help texts as well --- .../core/client/parts/health_modifier.lua | 3 ++- lua/pac3/core/server/event.lua | 25 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 10a2fa721..5a498ceb7 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -242,6 +242,7 @@ concommand.Add("pac_healthbar", function(ply, cmd, args) net.WriteInt(num, 16) net.SendToServer() end -end, nil, "changes your health modifier's extra health value. arguments:\nuid or name: the unique ID or the name of the part\naction: add, subtract, refill, replenish, remove, set\nnumber") + if args[2] == nil then ply:PrintMessage(HUD_PRINTCONSOLE, "\nthis command needs at least two arguments.\nuid or name: the unique ID or the name of the part\naction: add, subtract, refill, replenish, remove, set\nnumber\n\nexample: pac_healthbar my_healthmod add 50\n") end +end, nil, "changes your health modifier's extra health value. arguments:\nuid or name: the unique ID or the name of the part\naction: add, subtract, refill, replenish, remove, set\nnumber\n\nexample: pac_healthbar my_healthmod add 50") BUILDER:Register() \ No newline at end of file diff --git a/lua/pac3/core/server/event.lua b/lua/pac3/core/server/event.lua index ba156128a..3ce200749 100644 --- a/lua/pac3/core/server/event.lua +++ b/lua/pac3/core/server/event.lua @@ -15,7 +15,10 @@ end) -- event concommand.Add("pac_event", function(ply, _, args) - if not args[1] then return end + if args[1] == nil then + ply:PrintMessage(HUD_PRINTCONSOLE, "\npac_event needs at least one argument.\nname: any name, preferably without spaces\nmode: a number.\n\t0 turns off\n\t1 turns on\n\t2 toggles on/off\n\twithout a second argument, the event is a single-shot\n\ne.g. pac_event light 2\n") + return + end local event = args[1] local extra = tonumber(args[2]) or 0 @@ -35,10 +38,13 @@ concommand.Add("pac_event", function(ply, _, args) ply.pac_command_events = ply.pac_command_events or {} ply.pac_command_events[event] = ply.pac_command_events[event] or {} ply.pac_command_events[event] = {name = event, time = pac.RealTime, on = extra} -end) +end, nil, "pac_event triggers command events. it needs at least one argument.\nname: any name, preferably without spaces\nmode: a number.\n\t0 turns off\n\t1 turns on\n\t2 toggles on/off\n\twithout a second argument, the event is a single-shot\n\ne.g. pac_event light 2") concommand.Add("+pac_event", function(ply, _, args) - if not args[1] then return end + if not args[1] then + ply:PrintMessage(HUD_PRINTCONSOLE, "+pac_event needs a name argument, and the toggling argument. e.g. +pac_event hold_light 2\nwithout the toggling arg, implicitly adds _on to the command name, like running \"pac_event name_on\", and \"pac_event name_off\" when released") + return + end if args[2] == "2" or args[2] == "toggle" then local event = args[1] @@ -56,10 +62,13 @@ concommand.Add("+pac_event", function(ply, _, args) net.WriteString(args[1] .. "_on") net.Broadcast() end -end) +end, nil, "activates a command event when bound. \ne.g. \"+pac_event name\" will run \"pac_event name_on\" when the button is held, \"pac_event name_off\" when the button is held. Take note these are instant commands, they would need a command event with duration.\nmeanwhile, \"+pac_event name 2\" will run \"pac_event name 1\" when the button is held, \"pac_event name 0\" when the button is held. Take note these are held commands.") concommand.Add("-pac_event", function(ply, _, args) - if not args[1] then return end + if not args[1] then + ply:PrintMessage(HUD_PRINTCONSOLE, "-pac_event needs a name argument, and the toggling argument. e.g. +pac_event hold_light 2\nwithout the toggling arg, implicitly adds _on to the command name, like running \"pac_event name_off\"") + return + end if args[2] == "2" or args[2] == "toggle" then local event = args[1] @@ -81,6 +90,10 @@ end) -- proxy concommand.Add("pac_proxy", function(ply, _, args) str = args[1] + if args[1] == nil then + ply:PrintMessage(HUD_PRINTCONSOLE, "\npac_proxy needs at least two arguments.\nname\nnumber, or a series of numbers for a vector. increment notation is available, such as ++1 or --1.\ne.g. pac_proxy myvector 1 2 3\ne.g. pac_proxy value ++5") + return + end if ply:IsValid() then ply.pac_proxy_events = ply.pac_proxy_events or {} @@ -125,4 +138,4 @@ concommand.Add("pac_proxy", function(ply, _, args) net.Broadcast() --PrintTable(ply.pac_proxy_events[str]) -end) +end, nil, "pac_proxy sets the number of a command function in a proxy. it is typically accessed as command(\"value\") needs at least two arguments.\nname: any name, preferably without spaces\nnumbers: a number, or a series of numbers for a vector. increment notation is available, such as ++1 and --1.\ne.g. pac_proxy myvector 1 2 3\ne.g. pac_proxy value ++5") From 3b43e6bab85ad0b0275b791f754aec3e73520927 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 16 Jul 2024 20:09:52 -0400 Subject: [PATCH 207/300] hotfix apparently some of the replicated convars for lock didn't exist unlike other parts... a delayed reacquisition seems to work auto-kill preview render hook if self part is removed in case OnRemove removal fails --- lua/pac3/core/client/parts/lock.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 788e90f8a..c9d747434 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -24,6 +24,13 @@ local convar_lock_max_grab_radius = GetConVar("pac_sv_lock_max_grab_radius") local convar_lock_teleport = GetConVar("pac_sv_lock_teleport") local convar_combat_enforce_netrate = GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") +--sorcerous hack fix +if convar_lock == nil then timer.Simple(10, function() convar_lock = GetConVar("pac_sv_lock") end) end +if convar_lock_grab == nil then timer.Simple(10, function() convar_lock_grab = GetConVar("pac_sv_lock_grab") end) end +if convar_lock_teleport == nil then timer.Simple(10, function() convar_lock_teleport = GetConVar("pac_sv_lock_teleport") end) end +if convar_lock_max_grab_radius == nil then timer.Simple(10, function() convar_lock_max_grab_radius = GetConVar("pac_sv_lock_max_grab_radius") end) end +if convar_combat_enforce_netrate == nil then timer.Simple(10, function() convar_combat_enforce_netrate = GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") end) end + local BUILDER, PART = pac.PartTemplate("base_movable") @@ -310,7 +317,9 @@ function PART:OnShow() self.resetting_condition = false end end - pac.AddHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview" .. self.UniqueID, function() + local hookID = "pace_draw_lockpart_preview" .. self.UniqueID + pac.AddHook("PostDrawOpaqueRenderables", hookID, function() + if not IsValid(self) then pac.RemoveHook("PostDrawOpaqueRenderables", hookID) return end if self.TargetPart:IsValid() then origin_part = self.TargetPart else From 959e3fd5803e17cc148cd6cdcc881b2696d22114 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 28 Jul 2024 02:07:41 -0400 Subject: [PATCH 208/300] more editor features, projectile tweaks wires: draw multiple wires from a single property (beams, proxies events etc will have multiple targets at a later update, and proxies with inbuilt part references in their expressions will point to the found parts) select: able to select part-type properties by clicking parts on the tree. uses wires projectiles: friendly name for collide mesh use model browser emit collision sounds manually freeze option clamp the mass because 50000 is actually enforced by gmod preview collision meshes multicopy: copy various combinations of appearance and positioning-related properties for use in paste. regular copy will still be at the first submenu option but it expands a submenu if appropriate. paste will use whichever mode was last used, the tooltip will tell you what's in the clipboard asset browser: sound quickselect mode (auto-copypaste selected sounds into a semicolon-based list, please use the play button for previews) folder favorites (the option only shows when you select a folder. it is quite expensive to do it for all, otherwise) properties: renamed some menu and panel variables, they were swapped add active weapon to model selection if active playermodel is different to the selected playermodel (which indeed, pac can change your pm), add the selected playermodel to model selection when using Load VMT, write it in the notes for further review add a section of your pac materials to the material, split into each shader type --- lua/pac3/core/client/parts/projectile.lua | 32 +- lua/pac3/editor/client/asset_browser.lua | 50 ++- .../editor/client/panels/extra_properties.lua | 39 +- lua/pac3/editor/client/panels/properties.lua | 350 +++++++++++++++--- lua/pac3/editor/client/select.lua | 18 +- lua/pac3/editor/client/wires.lua | 139 ++++--- lua/pac3/extra/shared/projectiles.lua | 50 ++- 7 files changed, 546 insertions(+), 132 deletions(-) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 7d3cb704d..40a2276f5 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -29,11 +29,13 @@ BUILDER:StartStorableVars() BUILDER:GetSet("RandomAngleVelocity", Vector(0,0,0)) BUILDER:GetSet("LocalAngleVelocity", Vector(0,0,0)) BUILDER:SetPropertyGroup("Physics") - BUILDER:GetSet("Mass", 100) + BUILDER:GetSet("Freeze", false, {description = "frozen like physgun"}) + BUILDER:GetSet("Mass", 100, {editor_clamp = {0,50000}}) --there's actually a 50k limit + BUILDER:GetSet("ImpactSounds", true, {description = "allow physics impact sounds, applies to physical projectiles"}) BUILDER:GetSet("SurfaceProperties", "default", {enums = physprop_enums}) BUILDER:GetSet("RescalePhysMesh", false, {description = "experimental! tries to scale the collide mesh by the radius! Stay within small numbers! 1 radius should be associated with a full-size model"}) BUILDER:GetSet("OverridePhysMesh", false, {description = "experimental! tries to redefine the projectile's model to change the physics mesh"}) - BUILDER:GetSet("FallbackSurfpropModel", "models/props_junk/PopCan01a.mdl") + BUILDER:GetSet("FallbackSurfpropModel", "models/props_junk/PopCan01a.mdl", {editor_friendly = "collide mesh", editor_panel = "model"}) BUILDER:GetSet("Damping", 0) BUILDER:GetSet("Gravity", true) BUILDER:GetSet("Collisions", true) @@ -264,6 +266,8 @@ function PART:Shoot(pos, ang, multi_projectile_count) net.WriteBool(self.DrawShadow) net.WriteBool(self.Sticky) net.WriteBool(self.BulletImpact) + net.WriteBool(self.Freeze) + net.WriteBool(self.ImpactSounds) --vectors net.WriteVector(self.RandomAngleVelocity) @@ -277,14 +281,21 @@ function PART:Shoot(pos, ang, multi_projectile_count) net.WriteUInt(attract_ids[self.AttractMode] or 2,3) --numbers - net.WriteUInt(self.Radius,12) + local using_decimal = (self.Radius % 1 ~= 0) and self.RescalePhysMesh + net.WriteBool(using_decimal) + if using_decimal then + net.WriteFloat(self.Radius) + else + net.WriteUInt(self.Radius,12) + end + net.WriteUInt(self.DamageRadius,12) net.WriteUInt(self.Damage,24) - net.WriteUInt(1000*self.Speed,16) + net.WriteInt(1000*self.Speed,18) net.WriteUInt(self.Maximum,7) net.WriteUInt(100*self.LifeTime,14) --might need decimals net.WriteUInt(100*self.Delay,9) --might need decimals - net.WriteUInt(self.Mass,18) + net.WriteUInt(self.Mass,16) net.WriteInt(100*self.Spread,10) net.WriteInt(100*self.Damping,20) --might need decimals net.WriteInt(self.Attract,14) @@ -364,6 +375,15 @@ function PART:Shoot(pos, ang, multi_projectile_count) ent:PhysicsInitSphere(math.Clamp(self.Radius, 1, 500), self.SurfaceProperties) else ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 500), Vector(1,1,1) * math.Clamp(self.Radius, 1, 500), self.SurfaceProperties) + if self.OverridePhysMesh then + local valid_fallback = util.IsValidModel( self.FallbackSurfpropModel ) and not IsUselessModel(self.FallbackSurfpropModel) + ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 500), Vector(1,1,1) * math.Clamp(self.Radius, 1, 500), self.FallbackSurfpropModel) + if self.OverridePhysMesh and valid_fallback then + ent:SetModel(self.FallbackSurfpropModel) + ent:PhysicsInit(SOLID_VPHYSICS) + ent:GetPhysicsObject():SetMaterial(self.SurfaceProperties) + end + end end ent.RenderOverride = function() @@ -471,6 +491,8 @@ function PART:SetMass(val) local sv_max = GetConVar("pac_sv_projectile_max_mass"):GetInt() if self.Mass > sv_max then self:SetInfo("Your mass is beyond the server's maximum permitted! Server max is " .. sv_max) + elseif val > 50000 then + self:SetInfo("The game has a maximum of 50k mass") else self:SetInfo(nil) end diff --git a/lua/pac3/editor/client/asset_browser.lua b/lua/pac3/editor/client/asset_browser.lua index 7e9542acc..06910721a 100644 --- a/lua/pac3/editor/client/asset_browser.lua +++ b/lua/pac3/editor/client/asset_browser.lua @@ -196,6 +196,32 @@ local function install_click(icon, path, pattern, on_menu, pathid) end end +local function install_right_click_for_favorite_folder(icon, path, pathid, resource_type) + resource_type = resource_type or pace.model_browser_browse_types_tbl[1] or "models" + icon.DoRightClick = function() + local menu = DermaMenu() + if not table.HasValue(pace.bookmarked_ressources[resource_type], "folder:" .. path) then + menu:AddOption(L"add folder to favorites : " .. path, function() + table.insert(pace.bookmarked_ressources[resource_type], "folder:" .. path) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/star.png") + else + menu:AddOption(L"remove folder from favorites : " .. path, function() + table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], "folder:" .. path )) + pace.SaveRessourceBookmarks() + end):SetImage("icon16/cross.png") + end + menu:Open() + end + timer.Simple(1, function() + if not icon.GetChildNodes then return end + for i,child in ipairs(icon:GetChildNodes()) do + install_right_click_for_favorite_folder(child, child:GetFolder(), child:GetPathID(), resource_type) + end + end) + +end + local function get_unlit_mat(path) if path:find("%.png$") then return Material(path:match("materials/(.+)")) @@ -899,9 +925,11 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) sound_list:Dock(FILL) sound_list:SetMultiSelect(false) sound_list:SetVisible(false) - + frame.sound = "" + frame.lines = {} local function AddGeneric(self, sound, ...) local line = self:AddLine(sound, ...) + table.insert(frame.lines, line) local play = vgui.Create("DImageButton", line) play:SetImage("icon16/control_play.png") play:SizeToContents() @@ -993,8 +1021,21 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) end):SetImage("icon16/cross.png") end end + if not frame.QuickListBuildMode then + local pnl = menu:AddOption("Enable Quick list build mode", function() frame.QuickListBuildMode = true frame.sound = "" end) pnl:SetTooltip("Left click will concatenate a new sound to the part's list using semicolon notation. Preview using the play button instead.") + else + menu:AddOption("Disable Quick list build mode", function() + frame.QuickListBuildMode = nil frame.sound = "" + for i,v in ipairs(frame.lines) do if v.Columns then v.Columns[1]:SetColor(Color(0,0,0)) end end + end) + end menu:MakePopup() menu:RequestFocus() else + if frame.QuickListBuildMode then + line.Columns[1]:SetColor(Color(0,250,60)) --the file name + if frame.sound ~= "" then sound = frame.sound .. ";" .. sound end + frame.sound = sound + end pace.model_browser_callback(sound, "GAME") end end @@ -1200,6 +1241,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) do -- mounted local function addBrowseContent(viewPanel, node, name, icon, path, pathid) local function on_select(self, node) + install_right_click_for_favorite_folder(node, node:GetFolder(), pathid, resource_type) if viewPanel and viewPanel.currentNode and viewPanel.currentNode == node then return end node.dir = self.dir @@ -1315,12 +1357,12 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) tree:OnNodeSelected(node) viewPanel.currentNode = node end - + local oldnode = node + oldnode.name = name node = node:AddNode(name, icon) node:SetFolder("") node:SetPathID(pathid) node.viewPanel = viewPanel - for _, dir in ipairs(browse_types) do local files, folders = file.Find(path .. dir .. "/*", pathid) if files and (files[1] or folders[1]) then @@ -1341,6 +1383,8 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) end node.OnNodeSelected = on_select + install_right_click_for_favorite_folder(node, node:GetFolder(), node:GetPathID(), resource_type) + end local viewPanel = vgui.Create("pac_AssetBrowser_ContentContainer", frame.PropPanel) diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 65f04b10d..3ed454f95 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -188,7 +188,7 @@ do -- part if not self:IsValid() then return end self:SetValue(part:GetUniqueID()) self.OnValueChanged(part) - end) + end, self) end function PANEL:MoreOptionsRightClick(key) @@ -927,14 +927,19 @@ do --projectile radius PANEL.ClassName = "properties_projectile_radii" PANEL.Base = "pace_properties_number" + local testing_mesh + local drawing = false + local phys_mesh_vis = {} function PANEL:OnValueSet() time = os.clock() + 6 local function stop() hook.Remove("PostDrawOpaqueRenderables", "pace_draw_projectile_radii") + timer.Simple(0.2, function() SafeRemoveEntity(testing_mesh) end) drawing = false end local last_part = pace.current_part hook.Add("PostDrawOpaqueRenderables", "pace_draw_projectile_radii", function() + drawing = true if time < os.clock() then stop() end @@ -947,7 +952,37 @@ do --projectile radius else local mins_ph = Vector(last_part.Radius,last_part.Radius,last_part.Radius) local mins_dm = Vector(last_part.DamageRadius,last_part.DamageRadius,last_part.DamageRadius) - render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_ph, mins_ph, Color(255,255,255), true ) + if last_part.OverridePhysMesh then + if not IsValid(testing_mesh) then testing_mesh = ents.CreateClientProp("models/props_junk/PopCan01a.mdl") end + testing_mesh:PhysicsInit(SOLID_VPHYSICS) + if testing_mesh:GetModel() ~= last_part.FallbackSurfpropModel then + testing_mesh:SetModel(last_part.FallbackSurfpropModel) + testing_mesh:PhysicsInit(SOLID_VPHYSICS) + phys_mesh_vis = {} + for i = 0, testing_mesh:GetPhysicsObjectCount() - 1 do + for i2,tri in ipairs(testing_mesh:GetPhysicsObjectNum( i ):GetMeshConvexes()) do + for i3,vert in ipairs(tri) do + table.insert(phys_mesh_vis, vert) + end + end + end + end + local obj = Mesh() + obj:BuildFromTriangles(phys_mesh_vis) + cam.Start3D(pac.EyePos, pac.EyeAng) + render.SetMaterial(Material("models/wireframe")) + local mat = Matrix() + mat:Translate(last_part:GetWorldPosition()) + mat:Rotate(last_part:GetWorldAngles()) + mat:Scale(Vector(last_part.Radius,last_part.Radius,last_part.Radius)) + cam.PushModelMatrix( mat ) + render.CullMode(MATERIAL_CULLMODE_CW)obj:Draw() + render.CullMode(MATERIAL_CULLMODE_CCW)obj:Draw() + cam.PopModelMatrix() + cam.End3D() + else + render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_ph, mins_ph, Color(255,255,255), true ) + end render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_dm, mins_dm, Color(255,0,0), true ) end diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 756380ce0..6691a6c1a 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -170,6 +170,24 @@ function pace.AddSubmenuWithBracketExpansion(pnl, func, base_file, extension, ba end +local function get_files_recursively(tbl, path, extension) + local returning_call = false + if not tbl then returning_call = true tbl = {} end + local files, folders = file.Find(path .. "/*", "GAME") + for _,file in ipairs(files) do + if istable(extension) then + for _,ext in ipairs(extension) do + if string.GetExtensionFromFilename(file) == ext then + table.insert(tbl, path.."/"..file) + end + end + elseif string.GetExtensionFromFilename(file) == extension then + table.insert(tbl, path.."/"..file) + end + end + for _,folder in ipairs(folders) do get_files_recursively(tbl, path.."/"..folder, extension) end + if returning_call then return tbl end +end local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight) local btn = vgui.Create("DButton", self) @@ -648,6 +666,7 @@ do -- list end function PANEL:Clear() + if pace.bypass_tree then return end for key, data in pairs(self.List) do data.left:Remove() data.right:Remove() @@ -701,6 +720,7 @@ do -- list end function PANEL:Populate(flat_list) + if pace.bypass_tree then return end self:Clear() for _, data in ipairs(SortGroups(FlatListToGroups(flat_list))) do @@ -922,6 +942,88 @@ do -- non editable string pace.RegisterPanel(PANEL) end +local position_multicopy_properties = { + ["Position"] = true, + ["Angles"] = true, + ["PositionOffset"] = true, + ["AngleOffset"] = true, +} +local appearance_multicopy_properties = { + ["Material"] = true, + ["Color"] = true, + ["Brightness"] = true, + ["Alpha"] = true, + ["Translucent"] = true, + ["BlendMode"] = true +} + +local function install_movable_multicopy(copymenu, key) + if position_multicopy_properties[key] then + copymenu:AddOption("Copy Angles & Position", function() + pace.MultiCopy(pace.current_part, {"Angles", "Position"}) + end) + copymenu:AddOption("Copy Angle & Position Offsets", function() + pace.MultiCopy(pace.current_part, {"AngleOffset", "PositionOffset"}) + end) + copymenu:AddOption("Copy Angle & Position and their Offsets", function() + pace.MultiCopy(pace.current_part, {"Angles", "Position", "AngleOffset", "PositionOffset"}) + end) + copymenu:AddOption("Copy Angles & Angle Offset", function() + pace.MultiCopy(pace.current_part, {"Angles", "AngleOffset"}) + end) + copymenu:AddOption("Copy Position & Position Offset", function() + pace.MultiCopy(pace.current_part, {"Position", "PositionOffset"}) + end) + end +end +local function install_appearance_multicopy(copymenu, key) + if appearance_multicopy_properties[key] then + copymenu:AddOption("Material & Color", function() + pace.MultiCopy(pace.current_part, {"Material", "Color"}) + end) + copymenu:AddOption("Material & Color & Brightness", function() + pace.MultiCopy(pace.current_part, {"Material", "Color", "Brightness"}) + end) + copymenu:AddOption("Transparency", function() + pace.MultiCopy(pace.current_part, {"Alpha", "Translucent", "BlendMode"}) + end):SetTooltip("Alpha, Translucent, Blend mode") + copymenu:AddOption("Copy Material & Color & Alpha", function() + pace.MultiCopy(pace.current_part, {"Material", "Color", "Alpha"}) + end) + copymenu:AddOption("All appearance-related properties", function() + pace.MultiCopy(pace.current_part, {"Material", "Color", "Brightness", "Alpha", "Translucent", "BlendMode"}) + end):SetTooltip("Material, Color, Brightness, Alpha, Translucent, Blend mode") + end +end +local function reformat_color(col, proper_in, proper_out) + local multiplier = 1 + if not proper_in then multiplier = multiplier / 255 end + if not proper_out then multiplier = multiplier * 255 end + col.r = math.Clamp(col.r * multiplier,0,255) + col.g = math.Clamp(col.g * multiplier,0,255) + col.b = math.Clamp(col.b * multiplier,0,255) + col.a = math.Clamp(col.a * multiplier,0,255) + return col +end +local function do_multicopy() + if not pace.multicopy_source or not pace.multicopy_selected_properties then return end + for i,v in ipairs(pace.multicopy_selected_properties) do + local key = v[1] + local val = v[2] if not val then continue end if val == "" then continue end + if pace.current_part["Set"..key] then + if key == "Color" then + local color = pace.multicopy_source:GetColor() + local color_copy = Color(color.r,color.g,color.b) + reformat_color(color_copy, pace.multicopy_source.ProperColorRange, pace.current_part.ProperColorRange) + local vec = Vector(color_copy.r,color_copy.g,color_copy.b) + pace.current_part["Set"..key](pace.current_part,vec) + else + pace.current_part["Set"..key](pace.current_part,val) + end + end + end +end + do -- base editable local PANEL = {} @@ -1029,14 +1131,41 @@ do -- base editable end end + function pace.MultiCopy(part, tbl) + pace.clipboardtooltip = "" + pace.multicopy_selected_properties = {} + local str_tbl = {[1] = "multiple properties from " .. tostring(part)} + for i,v in ipairs(tbl) do + if part["Get"..v] then + local val = part["Get" .. v](part) + table.insert(pace.multicopy_selected_properties, {v, val}) + table.insert(str_tbl,v .. " : " .. tostring(val)) + end + end + pace.clipboardtooltip = table.concat(str_tbl, "\n") + pace.multicopying = true + pace.multicopy_source = part + end function PANEL:PopulateContextMenu(menu) - menu:AddOption(L"copy", function() + + pace.clipboardtooltip = pace.clipboardtooltip or "" + local copymenu, copypnl = menu:AddSubMenu(L"copy", function() pace.clipboard = pac.CopyValue(self:GetValue()) - end):SetImage(pace.MiscIcons.copy) - menu:AddOption(L"paste", function() - self:SetValue(pac.CopyValue(pace.clipboard)) - self.OnValueChanged(self:GetValue()) - end):SetImage(pace.MiscIcons.paste) + pace.clipboardtooltip = pace.clipboard .. " (from " .. tostring(pace.current_part) .. ")" + pace.multicopying = false + end) copypnl:SetImage(pace.MiscIcons.copy) copymenu.GetDeleteSelf = function() return false end + install_movable_multicopy(copymenu, self.CurrentKey) + install_appearance_multicopy(copymenu, self.CurrentKey) + + local pnl = menu:AddOption(L"paste", function() + if pace.multicopying then + do_multicopy() + pace.PopulateProperties(pace.current_part) + else + self:SetValue(pac.CopyValue(pace.clipboard)) + self.OnValueChanged(self:GetValue()) + end + end) pnl:SetImage(pace.MiscIcons.paste) pnl:SetTooltip(pace.clipboardtooltip) --command's String variable if self.CurrentKey == "String" then @@ -1251,6 +1380,7 @@ do -- base editable for id,mat in ipairs(mats) do pnl:AddOption(string.GetFileFromFilename(mat), function() pace.current_part:SetLoadVmt(mat) + pace.current_part:SetNotes("last loaded VMT: " .. mat) end) end end @@ -1289,31 +1419,66 @@ do -- base editable } end - local pnl, menu2 = menu:AddSubMenu(L"Load favourite models", function() + local menu2, pnl = menu:AddSubMenu(L"Load favourite models", function() end) - menu2:SetImage("icon16/cart_go.png") + pnl:SetImage("icon16/cart_go.png") local pm = pace.current_part:GetPlayerOwner():GetModel() + local pm_selected = player_manager.TranslatePlayerModel(GetConVar("cl_playermodel"):GetString()) - pnl:AddOption("Current playermodel - " .. string.gsub(string.GetFileFromFilename(pm), ".mdl", ""), function() - pace.current_part:SetModel(pm) + 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 + 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") - for id,mdl in ipairs(pace.bookmarked_ressources["models"]) do - pnl:AddOption(string.GetFileFromFilename(mdl), function() - self:SetValue(mdl) + 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 - pace.current_part:SetModel(mdl) - timer.Simple(0.2, function() - pace.current_part.pace_properties["Model"]:SetValue(mdl) - pace.PopulateProperties(pace.current_part) + 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) - - end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png") + 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 end @@ -1337,9 +1502,9 @@ do -- base editable } end - local pnl, menu2 = menu:AddSubMenu(L"Load favourite materials", function() + local menu2, pnl = menu:AddSubMenu(L"Load favourite materials", function() end) - menu2:SetImage("icon16/cart_go.png") + pnl:SetImage("icon16/cart_go.png") for id,mat in ipairs(pace.bookmarked_ressources["materials"]) do mat = string.gsub(mat, "^materials/", "") @@ -1347,7 +1512,7 @@ do -- base editable if string.find(mat, "%[%d+,%d+%]") then --find the bracket notation mat_no_ext = string.gsub(mat_no_ext, "%[%d+,%d+%]", "") - pace.AddSubmenuWithBracketExpansion(pnl, function(str) + pace.AddSubmenuWithBracketExpansion(menu2, function(str) str = str or "" str = string.StripExtension(string.gsub(str, "^materials/", "")) self:SetValue(str) @@ -1359,7 +1524,7 @@ do -- base editable end, mat_no_ext, "vmt", "materials") else - pnl:AddOption(string.StripExtension(mat), function() + menu2:AddOption(string.StripExtension(mat), function() self:SetValue(mat_no_ext) if self.CurrentKey == "Material" then pace.current_part:SetMaterial(mat_no_ext) @@ -1370,6 +1535,50 @@ do -- base editable 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 end if string.find(pace.current_part.ClassName, "sound") then @@ -1387,17 +1596,43 @@ do -- base editable } end - local pnl, menu2 = menu:AddSubMenu(L"Load favourite sounds", function() + local menu2, pnl = menu:AddSubMenu(L"Load favourite sounds", function() end) - menu2:SetImage("icon16/cart_go.png") + 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.find(snd_no_ext, "%[%d+,%d+%]") then --find the bracket notation - pace.AddSubmenuWithBracketExpansion(pnl, function(str) + 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) @@ -1408,7 +1643,7 @@ do -- base editable 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(pnl, function(str) + pace.AddSubmenuWithBracketExpansion(menu2, function(str) self:SetValue(str) if self.CurrentKey == "Sound" then pace.current_part:SetSound(str) @@ -1427,7 +1662,7 @@ do -- base editable icon = "icon16/arrow_rotate_clockwise.png" end - pnl:AddOption(snd, function() + menu2:AddOption(snd, function() self:SetValue(snd) if self.CurrentKey == "Sound" then pace.current_part:SetSound(snd) @@ -1444,8 +1679,7 @@ do -- base editable end --long string menu to bypass the DLabel's limits, only applicable for sound2 for urls and base part's notes - if (pace.current_part.ClassName == "sound2" and self.CurrentKey == "Path") or self.CurrentKey == "Notes" then - + if (pace.current_part.ClassName == "sound2" and self.CurrentKey == "Path") or self.CurrentKey == "Notes" or (pace.current_part.ClassName == "text" and self.CurrentKey == "Text") then menu:AddOption(L"Insert long text", function() local pnl = vgui.Create("DFrame") local DText = vgui.Create("DTextEntry", pnl) @@ -1453,7 +1687,7 @@ do -- base editable DText:SetMaximumCharCount(50000) pnl:SetSize(1200,800) - pnl:SetTitle("Long text for " .. self.CurrentKey .. ". Do not touch the label after this!") + 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) @@ -1464,11 +1698,14 @@ do -- base editable DText:SetContentAlignment(7) pnl:MakePopup() DText:RequestFocus() + DText:SetText(pace.current_part[self.CurrentKey]) DButtonOK.DoClick = function() local str = DText:GetText() if self.CurrentKey == "Notes" then pace.current_part.Notes = str + elseif self.CurrentKey == "Text" then + pace.current_part.Text = str elseif pace.current_part.ClassName == "sound2" then pace.current_part.AllPaths = str pace.current_part:UpdateSoundsFromAll() @@ -1810,25 +2047,36 @@ do -- vector end function PANEL:PopulateContextMenu(menu) - menu:AddOption(L"copy", function() + pace.clipboardtooltip = pace.clipboardtooltip or "" + local copymenu, copypnl = menu:AddSubMenu(L"copy", function() pace.clipboard = pac.CopyValue(self.vector) - end):SetImage(pace.MiscIcons.copy) - menu:AddOption(L"paste", function() - local val = pac.CopyValue(pace.clipboard) - if isnumber(val) then - val = ctor(val, val, val) - elseif isvector(val) and type == "angle" then - val = ctor(val.x, val.y, val.z) - elseif isangle(val) and type == "vector" then - val = ctor(val.p, val.y, val.r) - end + pace.clipboardtooltip = tostring(pace.clipboard) .. " (from " .. tostring(pace.current_part) .. ")" + pace.multicopying = false + end) copypnl:SetImage(pace.MiscIcons.copy) copymenu.GetDeleteSelf = function() return false end + install_movable_multicopy(copymenu, self.CurrentKey) + install_appearance_multicopy(copymenu, self.CurrentKey) + + local pnl = menu:AddOption(L"paste", function() + if pace.multicopying then + do_multicopy() + pace.PopulateProperties(pace.current_part) + else + local val = pac.CopyValue(pace.clipboard) + if isnumber(val) then + val = ctor(val, val, val) + elseif isvector(val) and type == "angle" then + val = ctor(val.x, val.y, val.z) + elseif isangle(val) and type == "vector" then + val = ctor(val.p, val.y, val.r) + end - if _G.type(val):lower() == type or type == "color" or type == "color2" then - self:SetValue(val) + if _G.type(val):lower() == type or type == "color" or type == "color2" then + self:SetValue(val) - self.OnValueChanged(self.vector * 1) + self.OnValueChanged(self.vector * 1) + end end - end):SetImage(pace.MiscIcons.paste) + end) pnl:SetImage(pace.MiscIcons.paste) pnl:SetTooltip(pace.clipboardtooltip) menu:AddSpacer() menu:AddOption(L"reset", function() if pace.current_part and pace.current_part.DefaultVars[self.CurrentKey] then @@ -2376,9 +2624,9 @@ function pace.OpenTreeSearch() end function edit.OnEnter() - if self.previous_search ~= edit:GetText() then + if edit.previous_search ~= edit:GetText() then perform_search() - self.previous_search = edit:GetText() + edit.previous_search = edit:GetText() elseif not table.IsEmpty(pace.tree_search_matches) then fwd:DoClick() else @@ -2389,4 +2637,4 @@ function pace.OpenTreeSearch() timer.Simple(0.1,function() edit:RequestFocus() end) end -end +end \ No newline at end of file diff --git a/lua/pac3/editor/client/select.lua b/lua/pac3/editor/client/select.lua index 87de84cef..76c64384d 100644 --- a/lua/pac3/editor/client/select.lua +++ b/lua/pac3/editor/client/select.lua @@ -334,7 +334,23 @@ function pace.SelectBone(ent, callback, only_movable) ) end -function pace.SelectPart(parts, callback) +function pace.SelectPart(parts, callback, property) + --mark some editor-related info for selecting the part via the editor labels + last_current_part = pace.current_part + pace.bypass_tree = true + pace.selecting_property_key = property.CurrentKey + pace.selecting_property = property + pac.AddHook("Tick", "selecting_part", function() + if not pace.selecting_property_key then pac.RemoveHook("Tick", "selecting_part") pace.bypass_tree = false end + if last_current_part ~= pace.current_part then --we've selected another part so + local new_select = pace.current_part + last_current_part["Set" .. pace.selecting_property_key](last_current_part, new_select) + pace.bypass_tree = false + timer.Simple(0.4, function() pace.OnPartSelected(last_current_part, false) end) + pace.StopSelect() + end + if not pace.IsSelecting then pac.RemoveHook("Tick", "selecting_part") pace.bypass_tree = false end + end) select_something( parts, diff --git a/lua/pac3/editor/client/wires.lua b/lua/pac3/editor/client/wires.lua index c5fcf1aa1..922424407 100644 --- a/lua/pac3/editor/client/wires.lua +++ b/lua/pac3/editor/client/wires.lua @@ -95,6 +95,63 @@ local function DrawHermite(width, x0,y0,x1,y1,c0,c1,alpha,samples) render.PopFilterMin() end +local function draw_hermite_list(part, tbl, property) + for _, part2 in pairs(tbl) do + local from = part + local to = part2 + if not to:IsValid() then continue end + + local from_pnl = from.pace_properties[property] + local to_pnl = to.pace_tree_node or NULL + + if not from_pnl:IsValid() then continue end + if not to_pnl:IsValid() then continue end + + local params = {} + + params["$basetexture"] = to.Icon or "gui/colors.png" + params["$vertexcolor"] = 1 + params["$vertexalpha"] = 1 + params["$nocull"] = 1 + + local path = to_pnl:GetModel() + if path then + path = "spawnicons/" .. path:sub(1, -5) .. "_32" + params["$basetexture"] = path + end + + local mat = CreateMaterial("pac_wire_icon_" .. params["$basetexture"], "UnlitGeneric", params) + + render.SetMaterial(mat) + + local fx,fy = from_pnl:LocalToScreen(from_pnl:GetWide(), from_pnl:GetTall() / 2) + + local tx,ty = to_pnl.Icon:LocalToScreen(0,to_pnl.Icon:GetTall() / 2) + + do + local x,y = pace.tree:LocalToScreen(0,0) + local w,h = pace.tree:LocalToScreen(pace.tree:GetSize()) + + tx = math.Clamp(tx, x, w) + ty = math.Clamp(ty, y, h) + end + + from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover or 0 + + if from_pnl:IsHovered() or (from.pace_tree_node and from.pace_tree_node:IsValid() and from.pace_tree_node.Label:IsHovered()) then + from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (5 - from_pnl.wire_smooth_hover) * FrameTime() * 20 + else + from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (0 - from_pnl.wire_smooth_hover) * FrameTime() * 20 + end + + from_pnl.wire_smooth_hover = math.Clamp(from_pnl.wire_smooth_hover, 0, 5) + + if from_pnl.wire_smooth_hover > 0.01 then + draw_hermite(0,0,ScrW(),ScrH(), from_pnl.wire_smooth_hover, fx,fy, tx,ty, Color(255,255,255), Color(255,255,255, 255), 1) + end + end +end + local function draw_hermite(x,y, w,h, ...) local cam3d = { type = "3D", @@ -210,68 +267,32 @@ hook.Add("PostRenderVGUI", "beams", function() end if part.ClassName == "proxy" and part.valid_parts_in_expression then - for _, part2 in pairs(part.valid_parts_in_expression) do - - local info = part.pace_tree_node - --if info.udata.part_key then - --if info.udata.part_key == "Parent" then continue end - - local from = part - local to = part2 - --print("part: ", part, "\nfrom: ", from, "\nto: ", to) - if not to:IsValid() then continue end - - - local from_pnl = from.pace_properties["Expression"] - local to_pnl = to.pace_tree_node or NULL - - if not from_pnl:IsValid() then continue end - if not to_pnl:IsValid() then continue end - - local params = {} - - params["$basetexture"] = to.Icon or "gui/colors.png" - params["$vertexcolor"] = 1 - params["$vertexalpha"] = 1 - params["$nocull"] = 1 - - local path = to_pnl:GetModel() - if path then - path = "spawnicons/" .. path:sub(1, -5) .. "_32" - params["$basetexture"] = path - end - - - local mat = CreateMaterial("pac_wire_icon_" .. params["$basetexture"], "UnlitGeneric", params) - - render.SetMaterial(mat) - - local fx,fy = from_pnl:LocalToScreen(from_pnl:GetWide(), from_pnl:GetTall() / 2) - - local tx,ty = to_pnl.Icon:LocalToScreen(0,to_pnl.Icon:GetTall() / 2) - - do - local x,y = pace.tree:LocalToScreen(0,0) - local w,h = pace.tree:LocalToScreen(pace.tree:GetSize()) - - tx = math.Clamp(tx, x, w) - ty = math.Clamp(ty, y, h) - end - - from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover or 0 + draw_hermite_list(part, part.valid_parts_in_expression, "Expression") + end - if from_pnl:IsHovered() or (from.pace_tree_node and from.pace_tree_node:IsValid() and from.pace_tree_node.Label:IsHovered()) then - from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (5 - from_pnl.wire_smooth_hover) * FrameTime() * 20 - else - from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (0 - from_pnl.wire_smooth_hover) * FrameTime() * 20 - end + if part.ExtraHermites then + draw_hermite_list(part, part.ExtraHermites, part.ExtraHermites_Property) + end - from_pnl.wire_smooth_hover = math.Clamp(from_pnl.wire_smooth_hover, 0, 5) + if pace.selecting_property and pace.IsSelecting and pace.bypass_tree then + render.SetMaterial(Material("icon16/add.png")) + local tx,ty = input.GetCursorPos() + local hovered_pnl = vgui.GetHoveredPanel() + if hovered_pnl then + local should_draw = false + local to_pnl = hovered_pnl + local tall = to_pnl:GetTall() / 2 + if hovered_pnl.Icon then to_pnl = hovered_pnl:GetParent() should_draw = true end + if hovered_pnl.ClassName == "pac_dtree_node_button" then to_pnl = hovered_pnl should_draw = true end + if should_draw then tx,ty = to_pnl:LocalToScreen(0,tall) end + end + local fx,fy = pace.selecting_property:LocalToScreen(pace.selecting_property:GetWide(), pace.selecting_property:GetTall() / 2) - if from_pnl.wire_smooth_hover > 0.01 then - draw_hermite(0,0,ScrW(),ScrH(), from_pnl.wire_smooth_hover, fx,fy, tx,ty, Color(255,255,255), Color(255,255,255, 255), 1) - end - --end + if math.abs(fy-ty) > 40 then + local ex, ey = pace.Editor:GetPos() + if (tx < (ex + pace.Editor:GetWide())) and (tx > ex) then --mouse inside the editor + draw_hermite(0,0,ScrW(),ScrH(), 5, fx,fy, tx,ty, Color(255,255,255), Color(255,255,255, 255), 1) + end end end diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index a4e46fa1b..ea6890eae 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -44,6 +44,13 @@ do -- projectile entity end if SERVER then + local physprop_indices = {} + for i=0,200,1 do + local name = util.GetSurfacePropName(i) + if name ~= "" then + physprop_indices[name] = i + end + end pac.AddHook("EntityTakeDamage", "pac_projectile", function(ent, dmg) local a, i = dmg:GetAttacker(), dmg:GetInflictor() @@ -65,7 +72,7 @@ do -- projectile entity self.projectile_owner = ply - local radius = math.Clamp(part.Radius, 1, pac_sv_projectile_max_phys_radius:GetFloat()) + local radius = math.Clamp(part.Radius, 0.01, pac_sv_projectile_max_phys_radius:GetFloat()) if part.Sphere then self:PhysicsInitSphere(radius, part.SurfaceProperties) else @@ -76,14 +83,13 @@ do -- projectile entity if part.OverridePhysMesh and valid_fallback then self:SetModel(part.FallbackSurfpropModel) self:PhysicsInit(SOLID_VPHYSICS) - self:PhysicsInitMultiConvex(self:GetPhysicsObject():GetMeshConvexes(), part.SurfaceProperties) end if valid_fallback and part.RescalePhysMesh then local physmesh = self:GetPhysicsObject():GetMeshConvexes() --hack from prop resizer - for convexkey, convex in pairs( physmesh ) do - for poskey, postab in pairs( convex ) do + for convexkey, convex in ipairs( physmesh ) do + for poskey, postab in ipairs( convex ) do convex[ poskey ] = postab.pos * radius end end @@ -100,10 +106,13 @@ do -- projectile entity local phys = self:GetPhysicsObject() phys:SetMaterial(part.SurfaceProperties) - phys:EnableGravity(part.Gravity) - phys:AddVelocity((ang:Forward() + (VectorRand():Angle():Forward() * part.Spread)) * part.Speed * 1000) - phys:AddAngleVelocity(Vector(part.RandomAngleVelocity.x * math.Rand(-1,1), part.RandomAngleVelocity.y * math.Rand(-1,1), part.RandomAngleVelocity.z * math.Rand(-1,1))) + if not part.Freeze then + phys:AddVelocity((ang:Forward() + (VectorRand():Angle():Forward() * part.Spread)) * part.Speed * 1000) + phys:AddAngleVelocity(Vector(part.RandomAngleVelocity.x * math.Rand(-1,1), part.RandomAngleVelocity.y * math.Rand(-1,1), part.RandomAngleVelocity.z * math.Rand(-1,1))) + else + phys:EnableMotion(false) + end phys:AddAngleVelocity(part.LocalAngleVelocity) @@ -129,6 +138,7 @@ do -- projectile entity self:SetAimDir(part.AimDir) self:DrawShadow(part.DrawShadow) self.part_data = part + self.surface_data = util.GetSurfaceData(physprop_indices[part.SurfaceProperties]) end local damage_types = { @@ -276,6 +286,20 @@ do -- projectile entity if not self.part_data then return end if not self.projectile_owner:IsValid() then return end + local our_surfdata = self.surface_data + local their_surfdata = util.GetSurfaceData(data.TheirSurfaceProps) + + if (self.part_data.ImpactSounds) then + if data.Speed >= 300 then + if (data.Speed >= our_surfdata.hardVelocityThreshold) or (our_surfdata.hardnessFactor >= their_surfdata.hardThreshold) then + self:EmitSound(our_surfdata.impactHardSound) + else + self:EmitSound(our_surfdata.impactSoftSound) + end + elseif data.Speed >= 50 then + self:EmitSound(our_surfdata.impactSoftSound) + end + end net.Start("pac_projectile_collide_event", true) net.WriteEntity(self) net.WriteTable({}) -- nothing for now @@ -531,6 +555,8 @@ if SERVER then part.DrawShadow = net.ReadBool() part.Sticky = net.ReadBool() part.BulletImpact = net.ReadBool() + part.Freeze = net.ReadBool() + part.ImpactSounds = net.ReadBool() --vectors part.RandomAngleVelocity = net.ReadVector() @@ -545,14 +571,16 @@ if SERVER then part.AttractMode = table.KeyFromValue(attract_ids, net.ReadUInt(3)) --numbers - part.Radius = net.ReadUInt(12) + local using_decimal = net.ReadBool() + if not using_decimal then part.Radius = net.ReadUInt(12) else part.Radius = net.ReadFloat() end + part.DamageRadius = net.ReadUInt(12) - part.Damage = net.ReadUInt(24) - part.Speed = net.ReadUInt(16) / 1000 + part.Damage = math.Clamp(net.ReadUInt(24), 0, pac_sv_projectile_max_damage:GetFloat()) + part.Speed = math.Clamp(net.ReadInt(18) / 1000, -pac_sv_projectile_max_speed:GetFloat(), pac_sv_projectile_max_speed:GetFloat()) part.Maximum = net.ReadUInt(7) part.LifeTime = net.ReadUInt(14) / 100 part.Delay = net.ReadUInt(9) / 100 - part.Mass = net.ReadUInt(18) + part.Mass = net.ReadUInt(16) part.Spread = net.ReadInt(10) / 100 part.Damping = net.ReadInt(20) / 100 part.Attract = net.ReadInt(14) From 5067ff14f349ab2fb9850e13ebb2a33b70d89210 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 28 Jul 2024 16:04:07 -0400 Subject: [PATCH 209/300] popup fixes, add commands in menu bar fix pace being localized as nil in base_part. we need it in the popups setup in the popups code it should be "menu bar" and not "editor bar" add command events, sequenced commands and command proxies to the player tab of the menu bar. the menus will force-refresh to auto-update according to current values --- lua/pac3/core/client/base_part.lua | 3 + lua/pac3/editor/client/menu_bar.lua | 143 ++++++++++++++++++ .../editor/client/popups_part_tutorials.lua | 13 +- 3 files changed, 154 insertions(+), 5 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 598db302a..a20d95a6f 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -54,6 +54,7 @@ function PART:IsValid() end function PART:PreInitialize() + if pace == nil then pace = _G.pace return end --I found that it is localized before pace was created self.Children = {} self.ChildrenMap = {} self.modifiers = {} @@ -1222,6 +1223,8 @@ end --the popup system function PART:SetupEditorPopup(str, force_open, tbl) + if pace.Editor == nil then return end + if self.pace_tree_node == nil then return end local legacy_help_popup_hack = false if not tbl then legacy_help_popup_hack = false diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 2dc387ca1..909ef1001 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -255,11 +255,154 @@ local function populate_options(menu) rendering:AddCVar(L"no outfit reflections", "pac_optimization_render_once_per_frame", "1", "0") end +local function get_events() + local events = {} + for k,v in pairs(pac.GetLocalParts()) do + if v.ClassName == "event" then + local e = v:GetEvent() + if e == "command" then + local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command) + local b = false + events[cmd] = pac.LocalPlayer.pac_command_events[cmd] and pac.LocalPlayer.pac_command_events[cmd].on == 1 or false + end + end + end + return events +end + local function populate_player(menu) local pnl = menu:AddOption(L"t pose", function() pace.SetTPose(not pace.GetTPose()) end):SetImage("icon16/user_go.png") menu:AddOption(L"reset eye angles", function() pace.ResetEyeAngles() end):SetImage("icon16/user_delete.png") menu:AddOption(L"reset zoom", function() pace.ResetZoom() end):SetImage("icon16/magnifier.png") + local seq_cmdmenu, pnl2 = menu:AddSubMenu(L"sequenced command events") pnl2:SetImage("icon16/clock.png") + seq_cmdmenu.GetDeleteSelf = function() return false end + + local full_cmdmenu, pnl3 = menu:AddSubMenu(L"full list of command events") pnl3:SetImage("icon16/clock_play.png") + full_cmdmenu.GetDeleteSelf = function() return false end + + local full_proxymenu, pnl4 = menu:AddSubMenu(L"full list of command proxies") pnl4:SetImage("icon16/calculator.png") + full_proxymenu.GetDeleteSelf = function() return false end + + local rebuild_events_menu + local rebuild_seq_menu + local rebuild_proxies_menu + + + local rebuild_seq_menu = function() + seq_cmdmenu:Clear() + if pac.LocalPlayer.pac_command_event_sequencebases == nil then return end + for cmd, tbl in pairs(pac.LocalPlayer.pac_command_event_sequencebases) do + if tbl.max ~= 0 then + local submenu, pnl3 = seq_cmdmenu:AddSubMenu(cmd) pnl3:SetImage("icon16/clock_red.png") + submenu.GetDeleteSelf = function() return false end + if tbl.min == nil then return end + for i=tbl.min,tbl.max,1 do + local func_sequenced = function() + RunConsoleCommand("pac_event_sequenced", cmd, "set", tostring(i,0)) rebuild_events_menu() + end + local option = submenu:AddOption(cmd..i,func_sequenced) option:SetIsCheckable(true) option:SetRadio(true) + if i == tbl.current then option:SetChecked(true) end + if pac.LocalPlayer.pac_command_events[cmd..i] then + if pac.LocalPlayer.pac_command_events[cmd..i].on == 1 then + option:SetChecked(true) + end + end + function option:SetChecked(b) + if ( self:GetChecked() != b ) then + self:OnChecked( b ) + end + self.m_bChecked = b + if b then func_sequenced() end + timer.Simple(0.4, rebuild_events_menu) + end + end + end + end + end + + if pac.LocalPlayer.pac_command_event_sequencebases then + if table.Count(pac.LocalPlayer.pac_command_event_sequencebases) > 0 then + rebuild_seq_menu() + end + end + + rebuild_events_menu = function() + full_cmdmenu:Clear() + for cmd, b in SortedPairs(get_events()) do + local option = full_cmdmenu:AddOption(cmd,function() RunConsoleCommand("pac_event", cmd, "2") end) option:SetIsCheckable(true) + if b then option:SetChecked(true) end + function option:OnChecked(b) + if b then RunConsoleCommand("pac_event", cmd, "1") else RunConsoleCommand("pac_event", cmd, "0") end rebuild_seq_menu() + end + if pace.command_colors[cmd] ~= nil then + local clr = Color(unpack(string.Split(pace.command_colors[cmd]," "))) + clr.a = 100 + option.PaintOver = function(_,w,h) surface.SetDrawColor(clr) surface.DrawRect(0,0,w,h) end + end + end + end + + rebuild_proxies_menu = function() + full_proxymenu:Clear() + for cmd, tbl in SortedPairs(pac.LocalPlayer.pac_proxy_events) do + local num = tbl.x + if tbl.y ~= 0 or tbl.z ~= 0 then + num = tbl.x .. " " .. tbl.y .. " " .. tbl.z + end + full_proxymenu:AddOption(cmd .. " : " .. num,function() + Derma_StringRequest("Set new value for pac_proxy " .. cmd, "please input a number or spaced vector-notation.\n++ and -- notation is also supported for any component.\nit shall be used in a proxy expression as command(\""..cmd.."\")", num, + function(str) + local args = string.Split(str, " ") + RunConsoleCommand("pac_proxy", cmd, unpack(args)) + timer.Simple(0.4, rebuild_proxies_menu) + end) + end) + end + end + + if pac.LocalPlayer.pac_command_events then + if table.Count(pac.LocalPlayer.pac_command_events) > 0 then + rebuild_events_menu() + end + end + if pac.LocalPlayer.pac_proxy_events then + if table.Count(pac.LocalPlayer.pac_proxy_events) > 0 then + rebuild_proxies_menu() + end + end + + function pnl2:Think() + if self:IsHovered() then + if not self.isrebuilt then + rebuild_events_menu() + self.isrebuilt = true + end + else + self.isrebuilt = false + end + end + function pnl3:Think() + if self:IsHovered() then + if not self.isrebuilt then + rebuild_seq_menu() + self.isrebuilt = true + end + else + self.isrebuilt = false + end + end + function pnl4:Think() + if self:IsHovered() then + if not self.isrebuilt then + rebuild_proxies_menu() + self.isrebuilt = true + end + else + self.isrebuilt = false + end + end + -- this should be in pacx but it's kinda stupid to add a hook just to populate the player menu -- make it more generic if pacx and pacx.GetServerModifiers then diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index 1a520ceda..2bca37b50 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -21,7 +21,7 @@ CreateConVar("pac_popups_preferred_location", "pac tree label", FCVAR_ARCHIVE, " "part world : if part is base_movable, place it next to the part in the viewport\n".. "screen : static x,y on screen no matter what. That would be at the center\n".. "cursor : right on the cursor\n".. - "editor bar : next to the toolbar") + "menu bar : next to the toolbar") function pace.OpenPopupConfig() @@ -272,7 +272,10 @@ function pac.InfoPopup(str, tbl, x, y) elseif tbl.obj_type == "part world" then if tbl.pac_part then - local global_position = tbl.pac_part:GetRootPart():GetOwner():GetPos() + tbl.pac_part:GetRootPart():GetOwner():OBBCenter()*1.5 + local ent = tbl.pac_part:GetRootPart():GetOwner() + if not IsValid(ent) then ent = pac.LocalPlayer end + local global_position = pac.LocalPlayer:GetPos() + if ent.GetPos then global_position = (ent:GetPos() + ent:OBBCenter()*1.5) end if tbl.pac_part.GetWorldPosition then global_position = tbl.pac_part:GetWorldPosition() --if part is a base_movable, we'll get its position right away elseif tbl.pac_part:GetParent().GetWorldPosition then @@ -288,7 +291,7 @@ function pac.InfoPopup(str, tbl, x, y) elseif tbl.obj_type == "cursor" then self:SetPos(input.GetCursorPos()) - elseif tbl.obj_type == "editor bar" then + elseif tbl.obj_type == "menu bar" then if not pace.Editor:IsLeft() then self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY()) else @@ -362,7 +365,7 @@ function pac.InfoPopup(str, tbl, x, y) function pnl:Think() self:MoveToObj(tbl) if input.IsButtonDown(KEY_P) and input.IsButtonDown(KEY_LALT) then --auto-kill if alt-p - tbl.pac_part.killpopup = true + if tbl.pac_part then tbl.pac_part.killpopup = true end self:Remove() end @@ -397,7 +400,7 @@ function pac.InfoPopup(str, tbl, x, y) self.mouse_doclick_possible_at = CurTime() end - if not IsValid(tbl.pac_part) and tbl.pac_part ~= false then self:Remove() end + if not IsValid(tbl.pac_part) and tbl.pac_part ~= false and tbl.pac_part ~= nil then self:Remove() end self.exp_width = self.exp_width or 800 self.exp_height = self.exp_height or 500 --resizing code, initially the label should start small From 60c0f36b2d332548845a07e7c94ec001e5f9ec52 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 28 Jul 2024 16:06:31 -0400 Subject: [PATCH 210/300] hotfix --- lua/pac3/core/client/base_part.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index a20d95a6f..4c5a8a769 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -54,7 +54,7 @@ function PART:IsValid() end function PART:PreInitialize() - if pace == nil then pace = _G.pace return end --I found that it is localized before pace was created + if pace == nil then pace = _G.pace end --I found that it is localized before pace was created self.Children = {} self.ChildrenMap = {} self.modifiers = {} From 713381b381d45023b7bc4add1631948f5a68046c Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 28 Jul 2024 19:45:31 -0400 Subject: [PATCH 211/300] changes to Load VMT outputs moved the note-taking to the part code. it only applied to the model's existing VMT quickloads. now it should work with the asset browser and material pastes new verbosity convar for the console prints, add descriptive titles and space out the prints a bit more. when erroring, apply a warning circle for each VMT line that errored --- lua/pac3/core/client/parts/material.lua | 25 +++++++++++++++----- lua/pac3/editor/client/panels/properties.lua | 1 - 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lua/pac3/core/client/parts/material.lua b/lua/pac3/core/client/parts/material.lua index 0d49d2a02..b531a0459 100644 --- a/lua/pac3/core/client/parts/material.lua +++ b/lua/pac3/core/client/parts/material.lua @@ -1,6 +1,7 @@ local shader_params = include("pac3/libraries/shader_params.lua") local mat_hdr_level = GetConVar("mat_hdr_level") +local dump_vmt_when_load_vmt = CreateConVar("pac_material_dump_vmt", "2", FCVAR_ARCHIVE, "whether to print the VMT information when using the Load VMT field in materials\n0= don't\n1 = print only the path as a single line\n2 = full prints for the raw VMT and extracted table") local material_flags = { debug = bit.lshift(1, 0), @@ -120,6 +121,9 @@ for shader_name, groups in pairs(shader_params.shaders) do BUILDER:GetSet("LoadVmt", "", {editor_panel = "material"}) function PART:SetLoadVmt(path) if not path or path == "" then return end + if (self.Notes == "") or (string.sub(self.Notes, 1, 15) == "last loaded VMT") then + self:SetNotes("last loaded VMT: " .. path) + end local str = file.Read("materials/" .. path .. ".vmt", "GAME") @@ -139,11 +143,16 @@ for shader_name, groups in pairs(shader_params.shaders) do end end - print(str) - print("======") - PrintTable(vmt) - print("======") - + if dump_vmt_when_load_vmt:GetInt() == 1 then + print("====== VMT loaded: " .. path) + elseif dump_vmt_when_load_vmt:GetInt() == 2 then + print("\n====== " .. path .. " raw VMT text:\n") + print(str) + print("====== extracted table:\n") + PrintTable(vmt) + print("\n======") + end + local errors = {"cannot convert material parameter:"} for k,v in pairs(vmt) do if k:StartWith("$") then k = k:sub(2) end @@ -175,9 +184,13 @@ for shader_name, groups in pairs(shader_params.shaders) do func(self, v) else - pac.Message("cannot convert material parameter " .. k) + table.insert(errors,k) + if dump_vmt_when_load_vmt:GetInt() == 2 then + pac.Message("cannot convert material parameter " .. k) + end end end + if #errors > 1 then self:SetWarning(table.concat(errors, "\n")) else self:SetWarning() end end BUILDER:GetSet("MaterialOverride", "all", {enums = function(self, str) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 6691a6c1a..323b577c7 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -1380,7 +1380,6 @@ do -- base editable for id,mat in ipairs(mats) do pnl:AddOption(string.GetFileFromFilename(mat), function() pace.current_part:SetLoadVmt(mat) - pace.current_part:SetNotes("last loaded VMT: " .. mat) end) end end From 4942203336898a4a3c8774277aa1b082a315a89b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 28 Jul 2024 20:19:39 -0400 Subject: [PATCH 212/300] small hack fix for material the base texture transform angle doesn't get applied properly on a plain part load, so manually re-apply it once on initialize --- lua/pac3/core/client/parts/material.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/pac3/core/client/parts/material.lua b/lua/pac3/core/client/parts/material.lua index b531a0459..04886206f 100644 --- a/lua/pac3/core/client/parts/material.lua +++ b/lua/pac3/core/client/parts/material.lua @@ -269,6 +269,9 @@ for shader_name, groups in pairs(shader_params.shaders) do function PART:Initialize() self.translation_vector = Vector() self.rotation_angle = Angle(0, 0, 0) + timer.Simple(0, function() + self:SetbasetexturetransformAngle(self:GetbasetexturetransformAngle()) + end) end From 6177accc2618a18d233b3c0c6c0134e1d6fd7426 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 28 Jul 2024 22:31:50 -0400 Subject: [PATCH 213/300] hotfix don't assume these tables exist already --- lua/pac3/editor/client/menu_bar.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 909ef1001..f63cf534c 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -296,7 +296,7 @@ local function populate_player(menu) if tbl.max ~= 0 then local submenu, pnl3 = seq_cmdmenu:AddSubMenu(cmd) pnl3:SetImage("icon16/clock_red.png") submenu.GetDeleteSelf = function() return false end - if tbl.min == nil then return end + if tbl.min == nil then continue end for i=tbl.min,tbl.max,1 do local func_sequenced = function() RunConsoleCommand("pac_event_sequenced", cmd, "set", tostring(i,0)) rebuild_events_menu() @@ -335,6 +335,7 @@ local function populate_player(menu) function option:OnChecked(b) if b then RunConsoleCommand("pac_event", cmd, "1") else RunConsoleCommand("pac_event", cmd, "0") end rebuild_seq_menu() end + if pace.command_colors == nil then continue end if pace.command_colors[cmd] ~= nil then local clr = Color(unpack(string.Split(pace.command_colors[cmd]," "))) clr.a = 100 @@ -345,6 +346,7 @@ local function populate_player(menu) rebuild_proxies_menu = function() full_proxymenu:Clear() + if pac.LocalPlayer.pac_proxy_events == nil then return end for cmd, tbl in SortedPairs(pac.LocalPlayer.pac_proxy_events) do local num = tbl.x if tbl.y ~= 0 or tbl.z ~= 0 then From 5e93d392cfb4e6df26a7d0370a645ab65af1af18 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 31 Jul 2024 21:45:03 -0400 Subject: [PATCH 214/300] hotfix: reorder functions --- lua/pac3/editor/client/wires.lua | 56 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/lua/pac3/editor/client/wires.lua b/lua/pac3/editor/client/wires.lua index 922424407..6e95871a0 100644 --- a/lua/pac3/editor/client/wires.lua +++ b/lua/pac3/editor/client/wires.lua @@ -95,6 +95,35 @@ local function DrawHermite(width, x0,y0,x1,y1,c0,c1,alpha,samples) render.PopFilterMin() end +local function draw_hermite(x,y, w,h, ...) + local cam3d = { + type = "3D", + + x = 0, + y = 0, + w = w, + h = h, + + znear = -10000, + zfar = 10000, + + origin = Vector(x,y,-1000), + angles = Angle(-90,0,90), + + ortho = { + left = 0, + right = w, + + top = -h, + bottom = 0 + } + } + + cam.Start(cam3d) + DrawHermite(...) + cam.End(cam3d) +end + local function draw_hermite_list(part, tbl, property) for _, part2 in pairs(tbl) do local from = part @@ -152,34 +181,7 @@ local function draw_hermite_list(part, tbl, property) end end -local function draw_hermite(x,y, w,h, ...) - local cam3d = { - type = "3D", - - x = 0, - y = 0, - w = w, - h = h, - - znear = -10000, - zfar = 10000, - - origin = Vector(x,y,-1000), - angles = Angle(-90,0,90), - - ortho = { - left = 0, - right = w, - - top = -h, - bottom = 0 - } - } - cam.Start(cam3d) - DrawHermite(...) - cam.End(cam3d) -end --[[ function PANEL:DrawHermite(...) local x, y = self:ScreenToLocal(0,0) From 516ff5bdc4973104744302f2f7ae9a2acd6360cd Mon Sep 17 00:00:00 2001 From: thegrb93 Date: Sat, 10 Aug 2024 16:54:22 -0400 Subject: [PATCH 215/300] Make netstream timeout 30. Gmod apparently struggles sending 20kb in under 10s sometimes --- lua/autorun/netstream.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/autorun/netstream.lua b/lua/autorun/netstream.lua index 632c71b94..588df6424 100644 --- a/lua/autorun/netstream.lua +++ b/lua/autorun/netstream.lua @@ -4,7 +4,7 @@ AddCSLuaFile() net.Stream = {} net.Stream.SendSize = 20000 --This is the size of each packet to send -net.Stream.Timeout = 10 --How long to wait for client response before cleaning up +net.Stream.Timeout = 30 --How long to wait for client response before cleaning up net.Stream.MaxWriteStreams = 1024 --The maximum number of write data items to store net.Stream.MaxReadStreams = 128 --The maximum number of queued read data items to store net.Stream.MaxChunks = 3200 --Maximum number of pieces the stream can send to the server. 64 MB From 66a351d5d2c9e0c5de1eac962206497b69c479f1 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 25 Aug 2024 21:51:24 -0400 Subject: [PATCH 216/300] multiple targets, experimental editor actions, part quick setups beams, events, proxy: multiple target parts if bulk selected parts, option to paste UID list there will appear on the part quick setups and on string properties beta/experimental editor actions arraying menu: make multiple copies of movable parts on a basic shape (circle, rectangle, line) criteria process: according to search criteria, replace properties or delete parts (more actions and ways to process the criteria might be added) e.g. replacing multiple events by matching their event type and argument strings bulk morph property : gradually set a property across multiple parts e.g. for color fades Quick setups: right click on parts to set up some useful things. proxy: part-based functions caching sample_and_hold(seed, duration, min, max ease)/random_drift/drift/samplehold randomly picks a random value regularly and can ease into it with "linear", or our known eases like "InSine" etc sum, product, average/mean, median (uses varargs) part_pos_x, part_pos_y, part_pos_z gets world position of a part delta_x, delta_y, delta_z difference of positions between two parts or one (optional argument missing means taking the parent) bodygroup(name, uid) sequenced_event_number(name) reads pac_event_sequenced data (command event series) ezfade now has a simple fadeout mode if the speed is negative discover new features: menu bar option on the pac tab to quickly review some of the new features, they can dismiss it quickly by clicking on the menu option new file: proxy_function_tutorials.lua the tutorials will be on the main part info action, on right clicking expressions, on the inputs/functions enum list picker, properties: insert tooltip on text label translate easy setup into an expression load vmt checks for parent owners to get their materials basic long string menu used for proxy expressions and command string indicate multiline texts as red (it gets ugly on the display if it tries to display multiple lines in one line. at least tooltips have decent multiline support already) add Lua patterns button to tree search, off by default popups: default colors are now black(dark gray) on white can now change the font started revising some part class tutorials --- lua/pac3/core/client/parts/beam.lua | 141 +- lua/pac3/core/client/parts/event.lua | 98 +- lua/pac3/core/client/parts/proxy.lua | 508 ++++- lua/pac3/editor/client/asset_browser.lua | 2 +- lua/pac3/editor/client/menu_bar.lua | 99 +- lua/pac3/editor/client/panels/properties.lua | 148 +- lua/pac3/editor/client/parts.lua | 1963 ++++++++++++++++- .../editor/client/popups_part_tutorials.lua | 281 ++- .../client/proxy_function_tutorials.lua | 742 +++++++ lua/pac3/editor/client/settings.lua | 35 +- lua/pac3/editor/client/shortcuts.lua | 22 +- lua/pac3/editor/client/tools.lua | 8 +- lua/pac3/editor/client/util.lua | 30 +- 13 files changed, 3865 insertions(+), 212 deletions(-) create mode 100644 lua/pac3/editor/client/proxy_function_tutorials.lua diff --git a/lua/pac3/core/client/parts/beam.lua b/lua/pac3/core/client/parts/beam.lua index 0840d341f..673b18101 100644 --- a/lua/pac3/core/client/parts/beam.lua +++ b/lua/pac3/core/client/parts/beam.lua @@ -91,15 +91,18 @@ BUILDER:StartStorableVars() BUILDER:PropertyOrder("ParentName") BUILDER:GetSet("Material", "cable/rope") BUILDER:GetSetPart("EndPoint") - BUILDER:GetSet("Bend", 10) - BUILDER:GetSet("Frequency", 1) - BUILDER:GetSet("Resolution", 16) + BUILDER:GetSet("MultipleEndPoints","") + BUILDER:SetPropertyGroup("beam size") BUILDER:GetSet("Width", 1) BUILDER:GetSet("WidthBend", 0) BUILDER:GetSet("WidthBendSize", 1) BUILDER:GetSet("StartWidthMultiplier", 1) BUILDER:GetSet("EndWidthMultiplier", 1) BUILDER:GetSet("WidthMorphPower", 1) + BUILDER:SetPropertyGroup("beam detail") + BUILDER:GetSet("Bend", 10) + BUILDER:GetSet("Frequency", 1) + BUILDER:GetSet("Resolution", 16) BUILDER:GetSet("TextureStretch", 1) BUILDER:GetSet("TextureScroll", 0) BUILDER:SetPropertyGroup("orientation") @@ -124,6 +127,68 @@ function PART:Initialize() self.EndColorC = Color(255, 255, 255, 255) end +function PART:GetOrFindCachedPart(uid_or_name) + local part = nil + 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 + + local owner = self:GetPlayerOwner() + part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name) + if not part:IsValid() then + part = pac.FindPartByName(pac.Hash(owner), uid_or_name, self) + else + self.found_cached_parts[uid_or_name] = part + return part + end + if not part:IsValid() then + self.erroring_cached_parts[uid_or_name] = true + else + self.found_cached_parts[uid_or_name] = part + return part + end + return part +end + +function PART:SetMultipleEndPoints(str) + self.MultipleEndPoints = str + if str == "" then self.MultiEndPoint = nil self.ExtraHermites = nil return end + timer.Simple(0.2, function() + if not string.find(str, ";") then + local part = self:GetOrFindCachedPart(str) + if IsValid(part) then + self:SetEndPoint(part) + self.MultipleEndPoints = "" + else + timer.Simple(3, function() + local part = self:GetOrFindCachedPart(str) + if part then + self:SetEndPoint(part) + self.MultipleEndPoints = "" + end + end) + end + self.MultiEndPoint = nil + else + self:SetEndPoint() + self.MultiEndPoint = {} + self.ExtraHermites = {} + local uid_splits = string.Split(str, ";") + for i,uid2 in ipairs(uid_splits) do + local part = self:GetOrFindCachedPart(uid2) + if not IsValid(part) then + timer.Simple(3, function() + local part = self:GetOrFindCachedPart(uid2) + if part then table.insert(self.MultiEndPoint, part) table.insert(self.ExtraHermites, part) end + end) + else table.insert(self.MultiEndPoint, part) table.insert(self.ExtraHermites, part) end + end + self.ExtraHermites_Property = "MultipleEndPoints" + end + end) +end + function PART:SetStartColor(v) self.StartColorC = self.StartColorC or Color(255, 255, 255, 255) @@ -203,30 +268,56 @@ end function PART:OnDraw() local part = self.EndPoint - if self.Materialm and self.StartColorC and self.EndColorC and part:IsValid() and part.GetWorldPosition then + if self.Materialm and self.StartColorC and self.EndColorC and ((part:IsValid() and part.GetWorldPosition) or self.MultiEndPoint) then local pos, ang = self:GetDrawPosition() render.SetMaterial(self.Materialm) - pac.DrawBeam( - pos, - part:GetWorldPosition(), - - ang:Forward(), - part:GetWorldAngles():Forward(), - - self.Bend, - math.Clamp(self.Resolution, 1, 256), - self.Width, - self.StartColorC, - self.EndColorC, - self.Frequency, - self.TextureStretch, - self.TextureScroll, - self.WidthBend, - self.WidthBendSize, - self.StartWidthMultiplier, - self.EndWidthMultiplier, - self.WidthMorphPower - ) + if self.MultiEndPoint then + for _,part in ipairs(self.MultiEndPoint) do + pac.DrawBeam( + pos, + part:GetWorldPosition(), + + ang:Forward(), + part:GetWorldAngles():Forward(), + + self.Bend, + math.Clamp(self.Resolution, 1, 256), + self.Width, + self.StartColorC, + self.EndColorC, + self.Frequency, + self.TextureStretch, + self.TextureScroll, + self.WidthBend, + self.WidthBendSize, + self.StartWidthMultiplier, + self.EndWidthMultiplier, + self.WidthMorphPower + ) + end + else + pac.DrawBeam( + pos, + part:GetWorldPosition(), + + ang:Forward(), + part:GetWorldAngles():Forward(), + + self.Bend, + math.Clamp(self.Resolution, 1, 256), + self.Width, + self.StartColorC, + self.EndColorC, + self.Frequency, + self.TextureStretch, + self.TextureScroll, + self.WidthBend, + self.WidthBendSize, + self.StartWidthMultiplier, + self.EndWidthMultiplier, + self.WidthMorphPower + ) + end end end diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 50eccf7df..81e154bd9 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -37,9 +37,13 @@ BUILDER:StartStorableVars() BUILDER:GetSet("ZeroEyePitch", false) BUILDER:GetSetPart("TargetPart", {editor_friendly = "ExternalOriginPart"}) BUILDER:GetSetPart("DestinationPart", {editor_friendly = "TargetedPart"}) + BUILDER:GetSet("MultipleTargetParts", "") BUILDER:EndStorableVars() +PART.Tutorials = {} + local registered_command_event_series = {} +local event_series_bounds = {} function PART:register_command_event(str,b) local ply = self:GetPlayerOwner() @@ -102,6 +106,15 @@ function PART:GetEventTutorialText() end end +function PART:GetTutorial(str) + if not str then + if pace and pace.TUTORIALS then + return pace.TUTORIALS.PartInfos[self.ClassName].popup_tutorial + end + end + return self:GetEventTutorialText() +end + function PART:AttachEditorPopup(str) local info_string = str or "no information available" @@ -160,6 +173,7 @@ function PART:SetEvent(event) end function PART:Initialize() + self.ExtraHermites = {} if self:GetPlayerOwner() == LocalPlayer() then timer.Simple(0.2, function() if self.Event == "command" then @@ -174,6 +188,64 @@ function PART:Initialize() end +function PART:GetOrFindCachedPart(uid_or_name) + local part = nil + self.found_cached_parts = self.found_cached_parts or {} + if self.found_cached_parts[uid_or_name] then return self.found_cached_parts[uid_or_name] end + + local owner = self:GetPlayerOwner() + part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name) + if not part:IsValid() then + part = pac.FindPartByName(pac.Hash(owner), uid_or_name, self) + else + self.found_cached_parts[uid_or_name] = part + return part + end + if part:IsValid() then + self.found_cached_parts[uid_or_name] = part + return part + end + return part +end + +function PART:SetMultipleTargetParts(str) + self.MultipleTargetParts = str + self.MultiTargetPart = {} + if str == "" then self.MultiTargetPart = nil self.ExtraHermites = nil return end + if not string.find(str, ";") then + local part = self:GetOrFindCachedPart(str) + if IsValid(part) then + self:SetDestinationPart(part) + self.MultipleTargetParts = "" + else + timer.Simple(3, function() + local part = self:GetOrFindCachedPart(str) + if part then + self:SetDestinationPart(part) + self.MultipleTargetParts = "" + end + end) + end + self.MultiTargetPart = nil + else + self:SetDestinationPart() + self.MultiTargetPart = {} + self.ExtraHermites = {} + local uid_splits = string.Split(str, ";") + for i,uid2 in ipairs(uid_splits) do + local part = self:GetOrFindCachedPart(uid2) + if not IsValid(part) then + timer.Simple(3, function() + local part = self:GetOrFindCachedPart(uid2) + if part then table.insert(self.MultiTargetPart, part) table.insert(self.ExtraHermites, part) end + end) + else table.insert(self.MultiTargetPart, part) table.insert(self.ExtraHermites, part) end + end + self.ExtraHermites_Property = "MultipleTargetParts" + end + +end + local function get_default(typ) if typ == "string" then return "" @@ -2732,6 +2804,8 @@ do eventObject.preferred_operator = preferred_operator eventObject.tutorial_explanation = tutorial_explanation + PART.Tutorials[classname] = tutorial_explanation + function eventObject:Think(event, ent, ...) return think(event, ent, ...) end @@ -2741,6 +2815,7 @@ do timer.Simple(0, function() -- After all addons has loaded hook.Call('PAC3RegisterEvents', nil, pac.CreateEvent, pac.RegisterEvent) + pace.TUTORIALS["events"] = PART.Tutorials end) end @@ -3119,13 +3194,25 @@ function PART:TriggerEvent(b) self.event_triggered = b -- event_triggered is just used for the editor if self.AffectChildrenOnly then - for _, child in ipairs(self:GetChildren()) do - child:SetEventTrigger(self, b) + if self.MultiTargetPart then + for _,part2 in ipairs(self.MultiTargetPart) do + if part2.SetEventTrigger then part2:SetEventTrigger(self, b) end + end + else + for _, child in ipairs(self:GetChildren()) do + child:SetEventTrigger(self, b) + end end else - local parent = self:GetParent() - if parent:IsValid() then - parent:SetEventTrigger(self, b) + if self.MultiTargetPart then + for _,part2 in ipairs(self.MultiTargetPart) do + if part2.SetEventTrigger then part2:SetEventTrigger(self, b) end + end + else + local parent = self:GetParent() + if parent:IsValid() then + parent:SetEventTrigger(self, b) + end end end if IsValid(self.DestinationPart) then --target part. the proper one. @@ -3134,7 +3221,6 @@ function PART:TriggerEvent(b) self.previousdestinationpart:SetEventTrigger(self, false) end end - (self.DestinationPart):SetEventTrigger(self, b) self.previousdestinationpart = (self.DestinationPart) elseif IsValid(self.previousdestinationpart) then diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index e51e7746f..cb84f6e65 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -28,6 +28,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("RootOwner", false) BUILDER:GetSetPart("TargetPart") + BUILDER:GetSet("MultipleTargetParts", "") BUILDER:GetSetPart("OutputTargetPart", {hide_in_editor = true}) BUILDER:GetSet("AffectChildren", false) BUILDER:GetSet("Expression", "") @@ -101,6 +102,69 @@ function PART:GetTarget() return self:GetParent() end +function PART:GetOrFindCachedPart(uid_or_name) + local part = nil + 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 + + local owner = self:GetPlayerOwner() + part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name) + if not part:IsValid() then + part = pac.FindPartByName(pac.Hash(owner), uid_or_name, self) + else + self.found_cached_parts[uid_or_name] = part + return part + end + if not part:IsValid() then + self.erroring_cached_parts[uid_or_name] = true + else + self.found_cached_parts[uid_or_name] = part + return part + end + return part +end + +function PART:SetMultipleTargetParts(str) + self.MultipleTargetParts = str + self.MultiTargetPart = {} + if str == "" then self.MultiTargetPart = nil self.ExtraHermites = nil return end + if not string.find(str, ";") then + local part = self:GetOrFindCachedPart(str) + if IsValid(part) then + self:SetTargetPart(part) + self.MultipleTargetParts = "" + else + timer.Simple(3, function() + local part = self:GetOrFindCachedPart(str) + if part then + self:SetTargetPart(part) + self.MultipleTargetParts = "" + end + end) + end + self.MultiTargetPart = nil + else + self:SetTargetPart() + self.MultiTargetPart = {} + self.ExtraHermites = {} + local uid_splits = string.Split(str, ";") + for i,uid2 in ipairs(uid_splits) do + local part = self:GetOrFindCachedPart(uid2) + if not IsValid(part) then + timer.Simple(3, function() + local part = self:GetOrFindCachedPart(uid2) + if part then table.insert(self.MultiTargetPart, part) table.insert(self.ExtraHermites, part) end + end) + else table.insert(self.MultiTargetPart, part) table.insert(self.ExtraHermites, part) end + end + self.ExtraHermites_Property = "MultipleTargetParts" + end + +end + + function PART:SetVariableName(str) self.VariableName = str end @@ -132,8 +196,17 @@ end function PART:Initialize() self.vec_additive = {} self.next_vel_calc = 0 + self.invalid_parts_in_expression = {} + if self:GetPlayerOwner() == pac.LocalPlayer then + self.errors_override = true + timer.Simple(5, function() self.errors_override = false end) --initialize hack to stop erroring when referenced parts aren't created yet but will be created shortly + end + end +PART.Tutorials = include("pac3/editor/client/proxy_function_tutorials.lua") + + PART.Functions = { none = function(n) return n end, @@ -357,6 +430,42 @@ for ease,f in pairs(math.ease) do end end +PART.Inputs.sample_and_hold = function(self, seed, duration, min, max, ease) + if not seed then self:SetInfo("sample_and_hold's arguments are (seed, duration, min, max, ease)\nease is a string like \"linear\" \"InSine\" or \"InOutElastic\"") end + seed = seed or 0 + + min = min or 0 + max = max or 1 + + duration = duration or 1 + if duration == 0 then return min + math.random()*(max-min) end + + self.samplehold = self.samplehold or {} + self.samplehold_prev = self.samplehold_prev 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} + + local prev = self.samplehold_prev[seed].value + local frac = 1 - (self.samplehold[seed].refresh - CurTime()) / duration + 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} + end + if not ease then + return self.samplehold[seed].value + elseif ease == "lin" or ease == "linear" then + return prev + frac * delta + else + local eased_frac = math.ease[ease_aliases[ease]] and math.ease[ease_aliases[ease]](frac) or 1 + return prev + eased_frac*delta + end +end +PART.Inputs.samplehold = PART.Inputs.sample_and_hold +PART.Inputs.random_drift = PART.Inputs.sample_and_hold +PART.Inputs.drift = PART.Inputs.sample_and_hold + PART.Inputs.timeex = function(s) s.time = s.time or pac.RealTime @@ -364,42 +473,222 @@ PART.Inputs.timeex = function(s) end PART.Inputs.part_distance = function(self, uid1, uid2) - if not uid1 then return 0 end - local owner = self:GetPlayerOwner() - - local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end + local PartA = self:GetOrFindCachedPart(uid1) + local PartB + if not uid2 then + PartB = self:GetParent() + else + PartB = self:GetOrFindCachedPart(uid2) + end - local PartB = pac.GetPartFromUniqueID(pac.Hash(owner), uid2) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid2) - if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(owner), uid2, self) end - if not PartB:IsValid() then - if not uid2 then --no second argument, take parent - PartB = self:GetParent() - else --second argument exists and failed to find anything, ERROR + if not IsValid(PartB) then + if uid2 then + --second argument exists and failed to find anything, ERROR self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in part_distance" end end - if not PartA:IsValid() and uid1 then --first argument exists and failed to find anything, ERROR + if not IsValid(PartA) then --first argument exists and failed to find anything, ERROR self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_distance" end - if not PartA:IsValid() or not PartB:IsValid() then return 0 end + if not IsValid(PartA) or not IsValid(PartB) then return 0 end if not PartA.Position or not PartB.Position then return 0 end self.valid_parts_in_expression[PartA] = PartA self.valid_parts_in_expression[PartB] = PartB return (PartB:GetWorldPosition() - PartA:GetWorldPosition()):Length() end +PART.Inputs.sum = function(self, ...) + sum = 0 + local args = { ... } + if not args[1] then return 0 end + for i=1,#args,1 do + sum = sum + args[i] + end + return sum +end + +PART.Inputs.product = function(self, ...) + sum = 1 + local args = { ... } + if not args[1] then return 0 end + for i=1,#args,1 do + sum = sum * args[i] + end + return sum +end + +PART.Inputs.average = function(self, ...) + sum = 0 + local args = { ... } if isvector(args[1]) then sum = vector_origin end + if not args[1] then return 0 end + for i=1,#args,1 do + sum = sum + args[i] + end + return sum / #args +end +PART.Inputs.mean = PART.Inputs.average + +PART.Inputs.median = function(self, ...) + sum = 0 + local args = { ... } if isvector(args[1]) then sum = vector_origin end + if not args[1] then return 0 end + table.sort(args) + local count = #args + if (count % 2) == 1 then + return args[math.floor(count/2) + 1] + else + return (args[count/2] + args[count/2 + 1]) / 2 + end + return sum +end + + +PART.Inputs.part_pos_x = function(self, uid1) + local PartA + if not uid1 then --no argument, take parent + PartA = self:GetParent() + return PartA:GetWorldPosition().x + else + PartA = self:GetOrFindCachedPart(uid1) + end + + if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_pos_x" + end + + if not IsValid(PartA) then return 0 end + if not PartA.Position then return 0 end + self.valid_parts_in_expression[PartA] = PartA + return PartA:GetWorldPosition().x +end + +PART.Inputs.part_pos_y = function(self, uid1) + local PartA + if not uid1 then --no argument, take parent + PartA = self:GetParent() + return PartA:GetWorldPosition().y + else + PartA = self:GetOrFindCachedPart(uid1) + end + + if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_pos_y" + end + + if not IsValid(PartA) then return 0 end + if not PartA.Position then return 0 end + self.valid_parts_in_expression[PartA] = PartA + return PartA:GetWorldPosition().y +end + +PART.Inputs.part_pos_z = function(self, uid1) + local PartA + if not uid1 then --no argument, take parent + PartA = self:GetParent() + return PartA:GetWorldPosition().z + else + PartA = self:GetOrFindCachedPart(uid1) + end + + if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_pos_z" + end + + if not IsValid(PartA) then return 0 end + if not PartA.Position then return 0 end + self.valid_parts_in_expression[PartA] = PartA + return PartA:GetWorldPosition().z +end + +PART.Inputs.delta_x = function(self, uid1, uid2) + if not uid1 then return 0 end + local PartA = self:GetOrFindCachedPart(uid1) + local PartB + if not uid2 then + PartB = self:GetParent() + else + PartB = self:GetOrFindCachedPart(uid2) + end + if not IsValid(PartB) then + if uid2 then + --second argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in delta_x" + end + end + + if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in delta_x" + end + + if not IsValid(PartA) or not IsValid(PartB) then return 0 end + if not PartA.Position or not PartB.Position then return 0 end + self.valid_parts_in_expression[PartA] = PartA + self.valid_parts_in_expression[PartB] = PartB + return PartB:GetWorldPosition().x - PartA:GetWorldPosition().x +end + +PART.Inputs.delta_y = function(self, uid1, uid2) + if not uid1 then return 0 end + local PartA = self:GetOrFindCachedPart(uid1) + local PartB + if not uid2 then + PartB = self:GetParent() + else + PartB = self:GetOrFindCachedPart(uid2) + end + if not IsValid(PartB) then + if uid2 then + --second argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in delta_y" + end + end + + if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in delta_y" + end + + if not IsValid(PartA) or not IsValid(PartB) then return 0 end + if not PartA.Position or not PartB.Position then return 0 end + self.valid_parts_in_expression[PartA] = PartA + self.valid_parts_in_expression[PartB] = PartB + return PartB:GetWorldPosition().y - PartA:GetWorldPosition().y +end + +PART.Inputs.delta_z = function(self, uid1, uid2) + if not uid1 then return 0 end + local PartA = self:GetOrFindCachedPart(uid1) + local PartB + if not uid2 then + PartB = self:GetParent() + else + PartB = self:GetOrFindCachedPart(uid2) + end + if not IsValid(PartB) then + if uid2 then + --second argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in delta_z" + end + end + + if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR + self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in delta_z" + end + + if not IsValid(PartA) or not IsValid(PartB) then return 0 end + if not PartA.Position or not PartB.Position then return 0 end + self.valid_parts_in_expression[PartA] = PartA + self.valid_parts_in_expression[PartB] = PartB + return PartB:GetWorldPosition().z - PartA:GetWorldPosition().z +end + PART.Inputs.event_alternative = function(self, uid1, num1, num2) if not uid1 then return 0 end num1 = num1 or 0 num2 = num2 or 1 - local owner = self:GetPlayerOwner() - - local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end + local PartA = self:GetOrFindCachedPart(uid1) if not IsValid(PartA) then if uid1 then --first argument exists and failed to find anything, ERROR @@ -438,17 +727,30 @@ PART.Inputs.ezfade = function(self, speed, starttime, endtime) self.time = self.time or pac.RealTime local timeex = pac.RealTime - self.time local start_offset_constant = -starttime * speed + local result = 0 - if not endtime then --only a fadein - return math.Clamp(start_offset_constant + timeex * speed, 0, 1) + if speed < 0 then --if negative, we can use that as a simple fadeout notation + speed = -speed + endtime = endtime or starttime + if endtime == 0 then endtime = 1/speed end + local end_offset_constant = endtime * speed + result = math.Clamp(end_offset_constant - timeex * speed, 0, 1) + elseif endtime == nil then --only a fadein + result = math.Clamp(start_offset_constant + timeex * speed, 0, 1) else --fadein fadeout local end_offset_constant = endtime * speed - return math.Clamp(start_offset_constant + timeex * speed, 0, 1) * math.Clamp(end_offset_constant - timeex * speed, 0, 1) + result = math.Clamp(start_offset_constant + timeex * speed, 0, 1) * math.Clamp(end_offset_constant - timeex * speed, 0, 1) end + return result end + --four crossing points PART.Inputs.ezfade_4pt = function(self, in_starttime, in_endtime, out_starttime, out_endtime) - if not in_starttime or not in_endtime then self:SetError("ezfade_4pt needs at least two arguments! (in_starttime, in_endtime, out_starttime, out_endtime)") return 0 end -- needs at least two args. we could assume 0 starting point and first arg is fadein end, but it'll mess up the order and confuse people + if not in_starttime or not in_endtime then + if not self.errors_override then self:SetError("ezfade_4pt needs at least two arguments! (in_starttime, in_endtime, out_starttime, out_endtime)") end + self.error = true return 0 + end -- needs at least two args. we could assume 0 starting point and first arg is fadein end, but it'll mess up the order and confuse people + local fadein_result = 0 local fadeout_result = 1 local in_speed = 1 @@ -484,7 +786,7 @@ PART.Inputs.ezfade_4pt = function(self, in_starttime, in_endtime, out_starttime, fadeout_result = 1 end end - + end return fadein_result * fadeout_result end @@ -496,9 +798,7 @@ for i=1,5,1 do elseif self.last_extra_feedbacks[i][uid1] then --a thing to skip part searching when we found the part return self.last_extra_feedbacks[i][uid1]["feedback_extra" .. i] or 0 else - local owner = self:GetPlayerOwner() - local PartA = pac.GetPartFromUniqueID(pac.Hash(owner), uid1) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid1) - if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(owner), uid1, self) end + local PartA = self:GetOrFindCachedPart(uid1) if IsValid(PartA) and PartA.ClassName == "proxy" then self.last_extra_feedbacks[i][uid1] = PartA end @@ -509,8 +809,8 @@ for i=1,5,1 do end PART.Inputs.number_operator_alternative = function(self, comp1, op, comp2, num1, num2) - if not (comp1 and op and comp2 and num1 and num2) then return -1 end - if not (isnumber(comp1) and isnumber(comp2) and isnumber(num1) and isnumber(num2)) then return -1 end + if not (comp1 and op and comp2) then return -1 end + if not (isnumber(comp1) and isnumber(comp2) and (isnumber(num1) or isvector(num1)) and (isnumber(num2) or isvector(num2))) then return -1 end local b = true if op == "=" or op == "==" or op == "equal" then b = comp1 == comp2 @@ -522,10 +822,10 @@ PART.Inputs.number_operator_alternative = function(self, comp1, op, comp2, num1, b = comp1 < comp2 elseif op == "<=" or op == "below or equal" or op == "less or equal" or op == "less than or equal" then b = comp1 <= comp2 - elseif op == "~=" or op == "~=" or op == "not equal" then + elseif op == "~=" or op == "!=" or op == "not equal" then b = comp1 ~= comp2 end - if b then return num1 or 0 else return num2 or 0 end + if b then return num1 or 1 else return num2 or 0 end end PART.Inputs.if_else = PART.Inputs.number_operator_alternative @@ -808,7 +1108,7 @@ do -- scale end PART.Inputs.parent_scale_x = function(self) return get_scale(self, "x") end PART.Inputs.parent_scale_y = function(self) return get_scale(self, "y") end - PART.Inputs.parent_scale_z = function(self) return get_scale(self, "z") end + PART.Inputs.parent_scale_z = function(self) return get_scale(self, "z") end end PART.Inputs.pose_parameter = function(self, name) @@ -829,6 +1129,28 @@ PART.Inputs.pose_parameter_true = function(self, name) return 0 end +PART.Inputs.bodygroup = function(self, name, uid) + local owner + if not uid then + if self:GetParent().Bodygroup then + owner = self:GetParent():GetOwner() + else + owner = get_owner(self) + end + else + owner = self:GetOrFindCachedPart(uid):GetOwner() + end + local bgs = owner:GetBodyGroups() + if bgs then + for i,tbl in ipairs(bgs) do + if tbl.name == name then return owner:GetBodygroup(tbl.id) end + end + end + return 0 +end + +PART.Inputs.model_bodygroup = PART.Inputs.bodygroup + PART.Inputs.command = function(self, name) local ply = self:GetPlayerOwner() if ply.pac_proxy_events then @@ -847,17 +1169,25 @@ PART.Inputs.command = function(self, name) return 0, 0, 0 end +PART.Inputs.sequenced_event_number = function(self, name) + local ply = self:GetPlayerOwner() + if ply.pac_command_event_sequencebases then + if ply.pac_command_event_sequencebases[name] then + return ply.pac_command_event_sequencebases[name].current + end + end + return 0 +end + PART.Inputs.voice_volume = function(self) local ply = self:GetPlayerOwner() if not IsValid(ply) then return 0 end return ply:VoiceVolume() end - PART.Inputs.voice_volume_scale = function(self) local ply = self:GetPlayerOwner() return ply:GetVoiceVolumeScale() end - do -- light amount local ColorToHSV = ColorToHSV local render = render @@ -929,7 +1259,6 @@ do -- health and armor return owner:Health() / owner:GetMaxHealth() end - PART.Inputs.owner_armor = function(self) local owner = self:GetPlayerOwner() if not owner:IsValid() then return 0 end @@ -1042,7 +1371,6 @@ do -- ammo return owner.GetActiveWeapon and owner:GetActiveWeapon() or owner, owner end - PART.Inputs.owner_total_ammo = function(self, id) local owner = self:GetPlayerOwner() id = id and id:lower() @@ -1051,7 +1379,6 @@ do -- ammo return (owner.GetAmmoCount and id) and owner:GetAmmoCount(id) or 0 end - PART.Inputs.weapon_primary_ammo = function(self) local wep = get_weapon(self) @@ -1107,17 +1434,14 @@ do if not self.feedback then return 0 end return self.feedback[1] or 0 end - PART.Inputs.feedback_x = function(self) if not self.feedback then return 0 end return self.feedback[1] or 0 end - PART.Inputs.feedback_y = function(self) if not self.feedback then return 0 end return self.feedback[2] or 0 end - PART.Inputs.feedback_z = function(self) if not self.feedback then return 0 end return self.feedback[3] or 0 @@ -1156,7 +1480,6 @@ PART.Inputs.flat_dot_right = function(self) return 0 end - PART.Inputs.server_maxplayers = function(self) return game.MaxPlayers() end @@ -1165,7 +1488,6 @@ PART.Inputs.server_population = PART.Inputs.server_playercount PART.Inputs.server_botcount = function(self) return #player.GetBots() end PART.Inputs.server_humancount = function(self) return #player.GetHumans() end - PART.Inputs.pac_healthbars_total = function(self) local ent = self:GetPlayerOwner() if ent.pac_healthbars then @@ -1188,21 +1510,21 @@ PART.Inputs.healthmod_bar_layertotal = PART.Inputs.pac_healthbars_layertotal PART.Inputs.pac_healthbar_uidvalue = function(self, uid) local ent = self:GetPlayerOwner() - local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) + local part = self:GetOrFindCachedPart(uid) - if not IsValid(pac.GetPartFromUniqueID(pac.Hash(ent), uid)) then + if not IsValid(part) then self.invalid_parts_in_expression[uid] = "invalid uid : " .. uid .. " in pac_healthbar_uidvalue" elseif part.ClassName ~= "health_modifier" then self.invalid_parts_in_expression[uid] = "invalid class : " .. uid .. " in pac_healthbar_uidvalue" end if ent.pac_healthbars and ent.pac_healthbars_uidtotals then - if ent.pac_healthbars_uidtotals[uid] then + if ent.pac_healthbars_uidtotals[part.UniqueID] then if part:IsValid() then self.valid_parts_in_expression[part] = part end end - return ent.pac_healthbars_uidtotals[uid] or 0 + return ent.pac_healthbars_uidtotals[part.UniqueID] or 0 end return 0 end @@ -1211,9 +1533,7 @@ PART.Inputs.healthmod_bar_uidvalue = PART.Inputs.pac_healthbar_uidvalue PART.Inputs.pac_healthbar_remaining_bars = function(self, uid) local ent = self:GetPlayerOwner() - local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) or pac.FindPartByPartialUniqueID(pac.Hash(ent), uid) - if not part:IsValid() then part = pac.FindPartByName(pac.Hash(ent), uid, self) end - + local part = self:GetOrFindCachedPart(uid) if not IsValid(part) then self.invalid_parts_in_expression[uid] = "invalid uid or name : " .. uid .. " in pac_healthbar_remaining_bars" elseif part.ClassName ~= "health_modifier" then @@ -1537,7 +1857,7 @@ function PART:OnThink(to_hide) pace.FlashNotification("An edited proxy still has no variable name! The proxy won't work until it knows where to send the math!") self:SetWarning("You forgot to set a variable name! The proxy won't work until it knows where to send the math!") self.touched = false - elseif self.VariableName ~= "" then self:SetWarning() end + elseif self.VariableName ~= "" and not self.error and not self.errors_override then self:SetWarning() end self:CalcVelocity() @@ -1564,7 +1884,7 @@ function PART:OnThink(to_hide) local ok, x,y,z = self:RunExpression(ExpressionFunc) - if not ok then + if not ok then self.error = true if self:GetPlayerOwner() == pac.LocalPlayer and self.Expression ~= self.LastBadExpression then chat.AddText(Color(255,180,180),"============\n[ERR] PAC Proxy error on "..tostring(self)..":\n"..x.."\n============\n") self.LastBadExpression = self.Expression @@ -1600,11 +1920,23 @@ function PART:OnThink(to_hide) self.feedback[3] = z if self.AffectChildren then - for _, part in ipairs(self:GetChildren()) do - set(self, part, x, y, z, true) + if self.MultiTargetPart then + for _,part2 in ipairs(self.MultiTargetPart) do + set(self, part2, x, y, z, true) + end + else + for _, part in ipairs(self:GetChildren()) do + set(self, part, x, y, z, true) + end end else - set(self, part, x, y, z) + if self.MultiTargetPart then + for i,v in ipairs(self.MultiTargetPart) do + set(self, v, x, y, z) + end + else + set(self, part, x, y, z) + end end if pace and pace.IsActive() then @@ -1619,7 +1951,7 @@ function PART:OnThink(to_hide) local T = type(val) if T == "boolean" then - str = tonumber(x) > 0 and "true" or "false" + str = (tonumber(x) or 0) > 0 and "true" or "false" elseif T == "Vector" then str = "Vector(" .. str .. ")" elseif T == "Angle" then @@ -1658,11 +1990,24 @@ function PART:OnThink(to_hide) end if self.AffectChildren then - for _, part in ipairs(self:GetChildren()) do - set(self, part, num, nil, nil, true) + if self.MultiTargetPart then + for _,part2 in ipairs(self.MultiTargetPart) do + set(self, part2, num, nil, nil, true) + end + else + for _, part in ipairs(self:GetChildren()) do + set(self, part, num, nil, nil, true) + end end else - set(self, part, num) + if self.MultiTargetPart then + for i,part2 in ipairs(self.MultiTargetPart) do + set(self, part2, num) + end + else + set(self, part, num) + end + end if pace and pace.IsActive() then @@ -1680,7 +2025,7 @@ function PART:OnThink(to_hide) for str, message in pairs(self.invalid_parts_in_expression) do error_msg = error_msg .. " " .. message .. "\n" end - self:SetError(error_msg) + self:SetError(error_msg) self.error = true end if self:GetPlayerOwner() == pac.LocalPlayer then if self.PreviewOutput then @@ -1689,7 +2034,54 @@ function PART:OnThink(to_hide) pac.RemoveHook("HUDPaint", "proxy" .. self.UniqueID) end end - + +end + + +function PART:GetActiveFunctions() + if self.Expression == "" then return {self.Input, self.Function} end + local possible_funcs = {} + for kw,_ in pairs(PART.Inputs) do + local kw2 = kw .. "(" + if string.find(self.Expression, kw2, 0, true) ~= nil then + table.insert(possible_funcs, kw) + end + end + + return possible_funcs +end + +function PART:GetTutorial(str) + if not str then + if pace and pace.TUTORIALS then + return pace.TUTORIALS.PartInfos[self.ClassName].popup_tutorial + end + end + return PART.Tutorials[str] +end + +function PART:AttachEditorPopup(str, flash, tbl) + if str == nil then + local funcs = self:GetActiveFunctions() + if #funcs > 0 then + str = "active functions" + if self.Expression ~= "" then + str = self.Expression .. "\n\nactive functions" + end + for i, kw in ipairs(self:GetActiveFunctions()) do + str = str .. "\n\n====================================================================\n\n" .. self:GetTutorial(kw) + end + end + end + local pnl = self:SetupEditorPopup(str, flash, tbl) + if flash and pnl then + pnl:MakePopup() + end + return pnl end +timer.Simple(10, function() + pace.TUTORIALS["proxy_functions"] = PART.Tutorials +end) + BUILDER:Register() diff --git a/lua/pac3/editor/client/asset_browser.lua b/lua/pac3/editor/client/asset_browser.lua index 06910721a..fd8910a1e 100644 --- a/lua/pac3/editor/client/asset_browser.lua +++ b/lua/pac3/editor/client/asset_browser.lua @@ -984,7 +984,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key) --print(resource_type, base_name, extension) local series_results = pace.FindAssetSeriesBounds(resource_type, base_name, extension) - PrintTable(series_results) + --PrintTable(series_results) if not series_results.start_index then goto CONTINUE end diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index f63cf534c..b53313c96 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -94,6 +94,83 @@ local function populate_pac(menu) ):SetImage(pace.MiscIcons.about) end + do + if cookie.GetNumber("pac3_new_features_review") == nil then cookie.Set("pac3_new_features_review", 1) end + if cookie.GetNumber("pac3_new_features_review") ~= 0 then + local experimentals, pnl_exp = menu:AddSubMenu(L"Discover new features", function() + Derma_Query("Do you wish to remove \"Discover new features\" from the pac tab?", "New feature review", + "remove", function() cookie.Set("pac3_new_features_review", 0) pace.CloseEditor() pace.OpenEditor() pace.RefreshTree() end, + "cancel", function() end) + end) + experimentals.GetDeleteSelf = function() return false end + pnl_exp:SetImage("icon16/medal_gold_1.png") pnl_exp:SetTooltip("You can hide this menu by clicking it for the prompt.") + + local pnl = experimentals:AddOption("Bookmark favorite assets (models, sounds, materials)") pnl:SetIcon("icon16/cart_go.png") pnl:SetTooltip("Right click on the text fields to access your favorites.\nRight click on asset browser items/lines to set a single favorite\nThe option to favorite a series may pop up if there's a number.\nSelect, then right click on the folder in the directory tree to favorite a folder") + pnl = experimentals:AddOption("Customizable editor: reorder menu actions and custom shortcuts", function() pace.OpenSettings("Editor menu Settings") end) pnl:SetIcon("icon16/table_refresh.png") pnl:SetTooltip("You can define your editor actions in any order, leave some out, and there are new actions.\nShortcuts are also configurable.") + pnl = experimentals:AddOption("Customizable editor: custom part categories", function() pace.OpenSettings("Editor menu Settings 2") end) pnl:SetIcon("icon16/application_view_list.png") pnl:SetTooltip("categorize parts with custom categories") + local popups_tutorials, popups_pnl = experimentals:AddSubMenu("popups and tutorials") popups_pnl:SetImage("icon16/help.png") popups_pnl:SetTooltip("default shortut : F1") + popups_tutorials.GetDeleteSelf = function() return false end + popups_tutorials:AddOption("part tutorials (F1)", function() pace.current_part:AttachEditorPopup() end):SetIcon("icon16/help.png") + popups_tutorials:AddOption("proxy tutorials", function() + if pace.current_part.ClassName == "proxy" then + pace.current_part:AttachEditorPopup() + else + local has_proxy = false + local proxy_found + for i,v in pairs(pac.GetLocalParts()) do + if v.ClassName == "proxy" then + has_proxy = true + proxy_found = v + end + end + if has_proxy then + proxy_found:AttachEditorPopup() + else + pace.FlashNotification("There were no proxy parts found in the outfit.") + end + end + end):SetIcon("icon16/calculator.png") + local popup_cfg, popups_pnl2 = popups_tutorials:AddSubMenu("Configure popups", pace.OpenPopupConfig) + popups_pnl2:SetImage("icon16/color_wheel.png") + popup_cfg.GetDeleteSelf = function() return false end + popup_cfg:AddOption("Open popup config", pace.OpenPopupConfig):SetImage("icon16/color_wheel.png") + popup_cfg:AddOption("preset: day mode", function() + GetConVar("pac_popups_base_alpha"):SetString("255") + GetConVar("pac_popups_base_color"):SetString("255 255 255") + GetConVar("pac_popups_fade_alpha"):SetString("0") + GetConVar("pac_popups_fade_color"):SetString("255 255 255") + GetConVar("pac_popups_text_color"):SetString("40 40 40") + end):SetImage("icon16/contrast.png") + popup_cfg:AddOption("preset: night mode", function() + GetConVar("pac_popups_base_alpha"):SetString("255") + GetConVar("pac_popups_base_color"):SetString("40 40 40") + GetConVar("pac_popups_fade_alpha"):SetString("0") + GetConVar("pac_popups_fade_color"):SetString("0 0 0") + GetConVar("pac_popups_text_color"):SetString("255 255 255") + end):SetImage("icon16/contrast.png") + local popup_pref_mode, pnlppm = popup_cfg:AddSubMenu("prefered location", function() end) + pnlppm:SetImage("icon16/layout_header.png") + popup_pref_mode.GetDeleteSelf = function() return false end + popup_pref_mode:AddOption(L"parts on viewport", function() RunConsoleCommand("pac_popups_preferred_location", "part world") end):SetImage('icon16/camera.png') + popup_pref_mode:AddOption(L"part label on tree", function() RunConsoleCommand("pac_popups_preferred_location", "pac tree label") end):SetImage('icon16/layout_content.png') + popup_pref_mode:AddOption(L"menu bar", function() RunConsoleCommand("pac_popups_preferred_location", "menu bar") end):SetImage('icon16/layout_header.png') + popup_pref_mode:AddOption(L"cursor", function() RunConsoleCommand("pac_popups_preferred_location", "cursor") end):SetImage('icon16/mouse.png') + popup_pref_mode:AddOption(L"screen", function() RunConsoleCommand("pac_popups_preferred_location", "screen") end):SetImage('icon16/monitor.png') + + + pnl = experimentals:AddOption("Bulk Select : " .. GetConVar("pac_bulk_select_key"):GetString() .. " + click to select; operations are in the part menu") pnl:SetIcon("icon16/table_multiple.png") pnl:SetTooltip("Bulk Select selects multiple parts to do operations quickly.\nIt has an order. The order of selection can matter for some operations like Bulk Morph Property.") + pnl = experimentals:AddOption("Morph properties on bulk select", pace.BulkMorphProperty) pnl:SetIcon("icon16/chart_line.png") pnl:SetTooltip("Once you have selected parts with Bulk Select, set variables gradually.\nIt can achieve color fades across multiple parts.\nThe order of selection matters.") + pnl = experimentals:AddOption("Arraying menu : select a matrix part and a stackable part", function() pace.OpenArrayingMenu(pace.current_part) end) pnl:SetIcon("icon16/shape_group.png") pnl:SetTooltip("Select an origin/matrix part before opening the menu.\nThen select an arrayed part\nThus you can quickly place models in a circle for example.\nBoth the matrix and arrayed part need to be movables.") + pnl = experimentals:AddOption("Process by Criteria", function() + for i,v in pairs(pace.Tools) do + if v.name == (L"Process by Criteria") then + v.callback(pace.current_part) + end + end + end) pnl:SetIcon("icon16/text_list_numbers.png") pnl:SetTooltip("Write criteria to process parts.\nThis is only useful if you have lots of parts with a certain number or text you want to replace in bulk.\nFor example you can mass replace events of one type with another, and set arguments to match.") + end + end + do menu:AddOption(L"exit", function() pace.CloseEditor() end):SetImage(pace.MiscIcons.exit) end @@ -114,6 +191,7 @@ end local function populate_options(menu) menu:AddOption(L"settings", function() pace.OpenSettings() end) + menu:AddCVar(L"Keyboard shortcuts: Legacy mode", "pac_editor_shortcuts_legacy_mode", "1", "0") menu:AddCVar(L"inverse collapse/expand controls", "pac_reverse_collapse", "1", "0") menu:AddCVar(L"enable shift+move/rotate clone", "pac_grab_clone", "1", "0") @@ -166,7 +244,7 @@ local function populate_options(menu) clr_frame:SetSize(300,200) clr_pnl:Dock(FILL) clr_frame:RequestFocus() function clr_pnl:ValueChanged(col) - hover_color:SetString(col.r .. " " .. col.g .. " " .. col.b) + GetConVar("pac_hover_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) end end):SetImage('icon16/color_swatch.png') halos_color:AddOption(L"ocean", function() RunConsoleCommand("pac_hover_color", "ocean") end):SetImage('icon16/bullet_blue.png') @@ -180,6 +258,20 @@ local function populate_options(menu) popups:AddCVar(L"enable editor popups", "pac_popups_enable", "1", "0") popups:AddCVar(L"don't kill popups on autofade", "pac_popups_preserve_on_autofade", "1", "0") popups:AddOption("Configure popups appearance", function() pace.OpenPopupConfig() end):SetImage('icon16/color_wheel.png') + popups:AddOption("preset: day mode", function() + GetConVar("pac_popups_base_alpha"):SetString("255") + GetConVar("pac_popups_base_color"):SetString("255 255 255") + GetConVar("pac_popups_fade_alpha"):SetString("0") + GetConVar("pac_popups_fade_color"):SetString("255 255 255") + GetConVar("pac_popups_text_color"):SetString("40 40 40") + end):SetImage("icon16/contrast.png") + popups:AddOption("preset: night mode", function() + GetConVar("pac_popups_base_alpha"):SetString("255") + GetConVar("pac_popups_base_color"):SetString("40 40 40") + GetConVar("pac_popups_fade_alpha"):SetString("0") + GetConVar("pac_popups_fade_color"):SetString("0 0 0") + GetConVar("pac_popups_text_color"):SetString("255 255 255") + end):SetImage("icon16/contrast.png") local popup_pref_mode, pnlppm = popups:AddSubMenu("prefered location", function() end) pnlppm:SetImage("icon16/layout_header.png") popup_pref_mode.GetDeleteSelf = function() return false end @@ -246,6 +338,9 @@ local function populate_options(menu) pace.AddLanguagesToMenu(menu) pace.AddFontsToMenu(menu) menu:AddCVar(L"Use the new PAC4.5 icon", "pac_icon", "1", "0") + if cookie.GetNumber("pac3_new_features_review") == 0 then + menu:AddOption("re-show new features review", function() cookie.Set("pac3_new_features_review", 1) pace.CloseEditor() pace.OpenEditor() pace.RefreshTree() end):SetIcon("icon16/medal_gold_1.png") + end menu:AddSpacer() @@ -362,7 +457,7 @@ local function populate_player(menu) end) end end - + if pac.LocalPlayer.pac_command_events then if table.Count(pac.LocalPlayer.pac_command_events) > 0 then rebuild_events_menu() diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 323b577c7..2e4a60c27 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -262,6 +262,12 @@ function pace.CreateSearchList(property, key, name, add_columns, get_list, get_c pnl.list_key = key pnl.list_val = val + if name == "Input" or name == "Function" then --insert proxy function tutorials as tooltips + pnl:SetTooltip(pace.TUTORIALS["proxy_functions"][key] or "") + elseif name == "Event" then --insert event tutorials as tooltips + pnl:SetTooltip(pace.TUTORIALS["events"][key]) + end + if not first:IsValid() then first = pnl end @@ -610,6 +616,10 @@ do -- list local btn = pace.CreatePanel("properties_label") btn:SetTall(self:GetItemHeight()) + --description tooltips should be on the text label. they are broken on every type except boolean. + if udata and udata.description then + btn:SetTooltip(udata.description) + end do local key = key if key:EndsWith("UID") then @@ -1075,20 +1085,26 @@ do -- base editable -- visually round numbers so 0.6 doesn't show up as 0.600000000001231231 on wear value = math.Round(value, 7) end - local str = tostring(value) + local str = tostring(value) --this is the text that will end up on the display + local original_str = string.Trim(str,"\n") --this is the minimally-altered text that will remain as the internal value + local lines = string.Explode("\n", original_str) + if #lines > 1 then + str = "" + end self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text) + if str == "" then self:SetTextColor(Color(160,0,80)) end self:SetFont(pace.CurrentFont) - self:SetText(" " .. str) -- ugh + self:SetText(" " .. string.Trim(str,"\n")) -- ugh self:SizeToContents() if #str > 10 then - self:SetTooltip(str) + self:SetTooltip(original_str) else self:SetTooltip() end - self.original_str = str + self.original_str = original_str self.original_var = var if self.OnValueSet then @@ -1167,6 +1183,19 @@ do -- base editable end end) pnl:SetImage(pace.MiscIcons.paste) pnl:SetTooltip(pace.clipboardtooltip) + if #pace.BulkSelectList > 0 then + local uid_tbl = {} + local names_tbl = {} + for i,part in ipairs(pace.BulkSelectList) do + table.insert(uid_tbl, part.UniqueID) + table.insert(names_tbl, part:GetName()) + end + local pnl = menu:AddOption(L"paste UID list", function() + self:SetValue(table.concat(uid_tbl,";")) + self.OnValueChanged(self:GetValue()) + end) pnl:SetImage(pace.MiscIcons.paste) pnl:SetTooltip(table.concat(names_tbl,"\n")) + end + --command's String variable if self.CurrentKey == "String" then @@ -1366,9 +1395,50 @@ do -- base editable end end + + local tutorials, pnl2 = menu:AddSubMenu(L"Tutorials for the active functions") + for i, kw in ipairs(pace.current_part:GetActiveFunctions()) do + pace.current_part.errors_override = true --hack to stop competing SetInfo, SetWarning and SetError buttons + local tutorial = pace.current_part:GetTutorial(kw) if tutorial == nil then continue end + local pnl3 = tutorials:AddOption(kw, function() + pace.alternate_message_prompts = true + pace.current_part:SetInfo(tutorial) + pace.current_part:AttachEditorPopup(tutorial, true) + end) pnl3:SetIcon("icon16/calculator.png") + pnl3:SetTooltip(tutorial) + end + pnl2:SetImage("icon16/information.png") + end + + if self.CurrentKey == "Function" or self.CurrentKey == "Input" then + local proxy = pace.current_part + menu:AddOption("Translate easy setup into an expression", function() + proxy:SetExpression( + proxy.Min .. " + (" .. proxy.Max .. "-" .. proxy.Min .. ") * (" .. + "(" .. proxy.Function .. "(((" .. proxy.Input .. "()/" .. proxy.InputDivider .. ") + " .. proxy.Offset .. ") * " .. + proxy.InputMultiplier .. ") + 1) / 2) ^" .. proxy.Pow + ) + pace.PopulateProperties(proxy) + end):SetIcon("icon16/calculator.png") + + local tutorials, pnl2 = menu:AddSubMenu(L"Tutorials for the active functions") + for i, kw in ipairs(pace.current_part:GetActiveFunctions()) do + pace.current_part.errors_override = true --hack to stop competing SetInfo, SetWarning and SetError buttons + local tutorial = pace.current_part:GetTutorial(kw) if tutorial == nil then continue end + local pnl3 = tutorials:AddOption(kw, function() + pace.alternate_message_prompts = true + pace.current_part:SetInfo(tutorial) + pace.current_part:AttachEditorPopup(tutorial, true) + end) pnl3:SetIcon("icon16/calculator.png") + pnl3:SetTooltip(tutorial) + end + pnl2:SetTooltip(pace.current_part:GetTutorial(pace.current_part[self.CurrentKey])) + pnl2:SetImage("icon16/information.png") end if self.CurrentKey == "LoadVmt" then + local inserted_mat_owners = {} + local owner = pace.current_part:GetOwner() local name = string.GetFileFromFilename( owner:GetModel() ) local mats = owner:GetMaterials() @@ -1376,12 +1446,33 @@ do -- base editable local pnl, menu2 = menu:AddSubMenu(L"Load " .. name .. "'s material", function() end) menu2:SetImage("icon16/paintcan.png") + inserted_mat_owners[owner:GetModel()] = true for id,mat in ipairs(mats) do pnl:AddOption(string.GetFileFromFilename(mat), function() pace.current_part:SetLoadVmt(mat) end) end + + --add parent owners (including the owner entity at root) + for i,part in ipairs(pace.current_part:GetParentList()) do + local owner = part:GetOwner() + local name = string.GetFileFromFilename( owner:GetModel() ) + local mats = owner:GetMaterials() + if not inserted_mat_owners[owner:GetModel()] then + local pnl, menu2 = menu:AddSubMenu(L"Load " .. name .. "'s material", function() + end) + menu2:SetImage("icon16/paintcan.png") + inserted_mat_owners[owner:GetModel()] = true + + for id,mat in ipairs(mats) do + pnl:AddOption(string.GetFileFromFilename(mat), function() + pace.current_part:SetLoadVmt(mat) + end) + end + end + + end end if self.CurrentKey == "SurfaceProperties" and pace.current_part.GetSurfacePropsTable then @@ -1677,8 +1768,10 @@ do -- base editable end end - --long string menu to bypass the DLabel's limits, only applicable for sound2 for urls and base part's notes - if (pace.current_part.ClassName == "sound2" and self.CurrentKey == "Path") or self.CurrentKey == "Notes" or (pace.current_part.ClassName == "text" and self.CurrentKey == "Text") then + --long string menu to bypass the DLabel's limits for some fields + if (pace.current_part.ClassName == "sound2" and self.CurrentKey == "Path") or self.CurrentKey == "Notes" or (pace.current_part.ClassName == "text" and self.CurrentKey == "Text") + or (pace.current_part.ClassName == "command" and self.CurrentKey == "String") + or self.CurrentKey == "Expression" or self.CurrentKey == "ExpressionOnHide" or self.CurrentKey == "Extra1" or self.CurrentKey == "Extra2" or self.CurrentKey == "Extra3" or self.CurrentKey == "Extra4" or self.CurrentKey == "Extra5" then menu:AddOption(L"Insert long text", function() local pnl = vgui.Create("DFrame") local DText = vgui.Create("DTextEntry", pnl) @@ -1701,14 +1794,12 @@ do -- base editable DButtonOK.DoClick = function() local str = DText:GetText() - if self.CurrentKey == "Notes" then - pace.current_part.Notes = str - elseif self.CurrentKey == "Text" then - pace.current_part.Text = str - elseif pace.current_part.ClassName == "sound2" then + pace.current_part[self.CurrentKey] = str + if pace.current_part.ClassName == "sound2" then pace.current_part.AllPaths = str pace.current_part:UpdateSoundsFromAll() end + pace.PopulateProperties(pace.current_part) pnl:Remove() end end):SetImage('icon16/text_letter_omega.png') @@ -2484,6 +2575,7 @@ function pace.OpenTreeSearch() local base = vgui.Create("DFrame") pace.tree_searcher = base local edit = vgui.Create("DTextEntry", base) + local patterns = vgui.Create("DButton", base) local search_button = vgui.Create("DButton", base) local range_label = vgui.Create("DLabel", base) local close_button = vgui.Create("DButton", base) @@ -2495,6 +2587,14 @@ function pace.OpenTreeSearch() case_box:SetTooltip("case sensitive") case_box:SetColor(Color(150,150,150)) case_box:SetFont("DermaDefaultBold") + + patterns:SetText("^[abc]") + patterns:SetPos(490,2) + patterns:SetSize(40,20) + patterns:SetTooltip("use Lua patterns") + patterns:SetColor(Color(150,150,150)) + patterns:SetFont("DermaDefaultBold") + function case_box:DoClick() self.on = not self.on if self.on then @@ -2504,6 +2604,15 @@ function pace.OpenTreeSearch() end end + function patterns:DoClick() + self.on = not self.on + if self.on then + self:SetColor(Color(0,0,0)) + else + self:SetColor(Color(150,150,150)) + end + end + local function select_match() if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end @@ -2534,6 +2643,7 @@ function pace.OpenTreeSearch() if not IsValid(pace.Editor) then base:Remove() return end if not pace.Focused then base:Remove() end base:SetX(pace.Editor:GetX()) + base:SetWide(pace.Editor:GetWide()) end function base.Paint(_,w,h) surface.SetDrawColor(Color(255,255,255)) @@ -2558,8 +2668,13 @@ function pace.OpenTreeSearch() matches = {} pace.tree_search_matches = {} search_term = edit:GetText() + local nopatterns = patterns.on if not case_sensitive then search_term = string.lower(search_term) end for _,part in pairs(pac.GetLocalParts()) do + if (string.find(part.UniqueID, string.sub(search_term,2,#search_term-1)) or string.find(part.UniqueID, search_term)) and (#search_term > 8) then + table.insert(matches, #matches + 1, {part_matched = part, key_matched = "UniqueID"}) + table.insert(pace.tree_search_matches, #matches, {part_matched = part, key_matched = "UniqueID"}) + end for k,v in pairs(part:GetProperties()) do local value = v.get(part) @@ -2569,19 +2684,24 @@ function pace.OpenTreeSearch() value = tostring(value) if not case_sensitive then value = string.lower(value) end - if string.find(case_sensitive and v.key or string.lower(v.key), search_term) or (string.find(value, search_term)) then + + if string.find(case_sensitive and v.key or string.lower(v.key), search_term) or (string.find(value, search_term,1, not nopatterns)) then if v.key == "Name" and part.Name == "" then continue end table.insert(matches, #matches + 1, {part_matched = part, key_matched = v.key}) table.insert(pace.tree_search_matches, #matches, {part_matched = part, key_matched = v.key}) end end end - table.sort(pace.tree_search_matches, function(a, b) return select(2, a.part_matched.pace_tree_node:LocalToScreen()) < select(2, b.part_matched.pace_tree_node:LocalToScreen()) end) + table.sort(pace.tree_search_matches, function(a, b) + if not IsValid(a.part_matched.pace_tree_node) then return false end + if not IsValid(b.part_matched.pace_tree_node) then return false end + return select(2, a.part_matched.pace_tree_node:LocalToScreen()) < select(2, b.part_matched.pace_tree_node:LocalToScreen()) + end) if table.IsEmpty(matches) then range_label:SetText("0 / 0") else pace.tree_search_match_index = 1 end range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches) end - base:SetSize(492,24) + base:SetSize(pace.Editor:GetWide(),24) edit:SetSize(290,20) edit:SetPos(0,2) base:MakePopup() diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index e334e2dd8..c8ac4cd04 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"} +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"} 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"} @@ -414,6 +414,8 @@ function pace.OnPartSelected(part, is_selecting) end pace.current_part = part + if pace.bypass_tree then return end + pace.PopulateProperties(part) pace.mctrl.SetTarget(part) @@ -1228,7 +1230,7 @@ do -- menu if not obj:HasParent() and obj.ClassName == "group" then pace.RemovePartOnServer(obj:GetUniqueID(), false, true) end - + end function pace.SwapBaseMovables(obj1, obj2, promote) @@ -1270,11 +1272,12 @@ do -- menu pace.RefreshTree() end - function pace.SubstituteBaseMovable(obj,action) + function pace.SubstituteBaseMovable(obj,action,cast_class) + local prompt = (cast_class == nil) + cast_class = cast_class or "model2" if action == "create_parent" then - Derma_StringRequest("Create substitute parent", "Select a class name to create a parent", "model2", - function(str) - if str == "model" then str = "model2" end --I don't care, stop using legacy + local function func(str) + if str == "model" then str = "model2" end --I don't care, stop using legacy local newObj = pac.CreatePart(str) if not IsValid(newObj) then return end @@ -1305,7 +1308,12 @@ do -- menu obj:SetBone("head") pace.RefreshTree() - end) + end + if prompt then + Derma_StringRequest("Create substitute parent", "Select a class name to create a parent", "model2", function(str) func(str) end) + else + func(cast_class) + end elseif action == "reorder_child" then if obj.Parent then if obj.Parent.Position and obj.Parent.Angles then @@ -1314,8 +1322,7 @@ do -- menu end pace.RefreshTree() elseif action == "cast" then - Derma_StringRequest("Cast", "Select a class name to convert to. Make sure you know what you\'re doing! It will do a pac_restart after!", "model2", - function(str) + local function func(str) if str == obj.ClassName then return end if str == "model" then str = "model2" end --I don't care, stop using legacy local uid = obj.UniqueID @@ -1337,8 +1344,14 @@ do -- menu end end) - end) + end + if prompt then + Derma_StringRequest("Cast", "Select a class name to convert to. Make sure you know what you\'re doing! It will do a pac_restart after!", "model2", function(str) func(str) end) + else + func(cast_class) + end end + pace.recently_substituted_movable_part = obj end function pace.ClearBulkList() @@ -1897,6 +1910,283 @@ do -- menu --timer.Simple(0.1, function BulkSelectRefreshFadedNodes() end) end + function pace.BulkMorphProperty() + if #pace.BulkSelectList == 0 then timer.Simple(0.3, function() + pace.FlashNotification("Bulk Morph Property needs parts in bulk select!") end) + end + + local parts_backup_properties_values = {} + local excluded_properties = {["ParentUID"] = true,["UniqueID"] = true} + for i,v in ipairs(pace.BulkSelectList) do + parts_backup_properties_values[v] = {} + for _,prop in pairs(v:GetProperties()) do + if not excluded_properties[prop.key] then + parts_backup_properties_values[v][prop.key] = v["Get"..prop.key](v) + end + end + end + + local main_panel = vgui.Create("DFrame") + main_panel:SetTitle("Morph properties") + main_panel:SetSize(400,280) + + local properties_pnl = pace.CreatePanel("properties", main_panel) properties_pnl:SetSize(380,150) properties_pnl:SetPos(10,125) + local start_value = pace.CreatePanel("properties_number") properties_pnl:AddKeyValue("StartValue",start_value) + local end_value = pace.CreatePanel("properties_number") properties_pnl:AddKeyValue("EndValue",end_value) + start_value:SetNumberValue(1) end_value:SetNumberValue(1) + local function swap_properties(property, property_type, success) + properties_pnl:Clear() + properties_pnl:InvalidateLayout() + --timer.Simple(0.2, function() + if success then + start_value = pace.CreatePanel("properties_" .. property_type, properties_pnl) properties_pnl:AddKeyValue("StartValue",start_value) + end_value = pace.CreatePanel("properties_" .. property_type, properties_pnl) properties_pnl:AddKeyValue("EndValue",end_value) + + else + start_value = pace.CreatePanel("properties_label", properties_pnl) properties_pnl:AddKeyValue("ERROR",start_value) + end_value = pace.CreatePanel("properties_label", properties_pnl) properties_pnl:AddKeyValue("ERROR",end_value) + end + --end) + if start_value.Restart then start_value:Restart() end if end_value.Restart then end_value:Restart() end + if start_value.OnValueChanged then start_value.OnValueChanged(start_value:GetValue()) end + if start_value.OnValueChanged then end_value.OnValueChanged(end_value:GetValue()) end + end + + local function setsingle(part, property_name, property_type, frac) + + if property_type == "vector" then + local start_val = Vector(start_value.left:GetValue(), start_value.middle:GetValue(), start_value.right:GetValue()) + local end_val = Vector(end_value.left:GetValue(), end_value.middle:GetValue(), end_value.right:GetValue()) + local delta = end_val - start_val + part["Set"..property_name](part, start_val + frac*delta) + elseif property_type == "angle" then + local start_val = Angle(start_value.left:GetValue(), start_value.middle:GetValue(), start_value.right:GetValue()) + local end_val = Angle(end_value.left:GetValue(), end_value.middle:GetValue(), end_value.right:GetValue()) + local delta = end_val - start_val + part["Set"..property_name](part, start_val + frac*delta) + elseif property_type == "color" then + local r1 = start_value.left:GetValue() + local g1 = start_value.middle:GetValue() + local b1 = start_value.right:GetValue() + local r2 = start_value.left:GetValue() + local g2 = start_value.middle:GetValue() + local b2 = start_value.right:GetValue() + + part["Set"..property_name](part, Color(r1 + frac*(r2-r1), g1 + frac*(g2-g1), b1 + frac*(b2-b1))) + elseif property_type == "number" then + local start_val = start_value:GetValue() + local end_val = end_value:GetValue() + local delta = end_val - start_val + part["Set"..property_name](part, start_val + frac*delta) + end + end + local function setmultiple(property_name, property_type) + if #pace.BulkSelectList <= 1 then return end + for i,v in ipairs(pace.BulkSelectList) do + local frac = (i-1) / (#pace.BulkSelectList-1) + setsingle(v, property_name, property_type, frac) + end + end + local function reset_initial_properties() + --self.left = left + --self.middle = middle + --self.right = right + if start_value.left then + print(start_value.left:GetValue(), start_value.middle:GetValue(), start_value.right:GetValue()) + print(end_value.left:GetValue(), end_value.middle:GetValue(), end_value.right:GetValue()) + else + print(start_value:GetValue()) + print(end_value:GetValue()) + end + + for part, tbl in pairs(parts_backup_properties_values) do + for prop, value in pairs(tbl) do + part["Set"..prop](part, value) + end + end + end + + local properties_2 = pace.CreatePanel("properties", main_panel) properties_2:SetSize(380,85) properties_2:SetPos(10,30) + local variable_name = "" + local full_success = false + local found_type = "number" + local variable_name_pnl = pace.CreatePanel("properties_string", main_panel) properties_2:AddKeyValue("VariableName", variable_name_pnl) + function variable_name_pnl:SetValue(var) + local str = tostring(var) + variable_name = str + self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text) + self:SetFont(pace.CurrentFont) + self:SetText(" " .. str) -- ugh + self:SizeToContents() + + if #str > 10 then + self:SetTooltip(str) + else + self:SetTooltip() + end + self.original_str = str + self.original_var = var + if self.OnValueSet then + self:OnValueSet(str) + end + + full_success = true + found_type = "number" + for _,v in ipairs(pace.BulkSelectList) do + if not v["Get"..str] or not v["Set"..str] then full_success = false + else + if full_success then found_type = string.lower(type(v["Get"..str](v))) end + end + end + swap_properties(str, found_type, full_success) + if full_success then setmultiple(str, found_type) end + end + function variable_name_pnl:EditText() + local oldText = self:GetText() + self:SetText("") + + local pnl = vgui.Create("DTextEntry") + self.editing = pnl + pnl:SetFont(pace.CurrentFont) + pnl:SetDrawBackground(false) + pnl:SetDrawBorder(false) + pnl:SetText(self:EncodeEdit(self.original_str or "")) + pnl:SetKeyboardInputEnabled(true) + pnl:SetDrawLanguageID(false) + pnl:RequestFocus() + pnl:SelectAllOnFocus(true) + + pnl.OnTextChanged = function() oldText = pnl:GetText() end + + local hookID = tostring({}) + local textEntry = pnl + local delay = os.clock() + 0.1 + + pac.AddHook('Think', hookID, function(code) + if not IsValid(self) or not IsValid(textEntry) 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 + pac.RemoveHook('Think', hookID) + self.editing = false + pace.BusyWithProperties = NULL + textEntry:Remove() + self:SetText(oldText) + pnl:OnEnter() + end) + + --local x,y = pnl:GetPos() + --pnl:SetPos(x+3,y-4) + --pnl:Dock(FILL) + local x, y = self:LocalToScreen() + local inset_x = self:GetTextInset() + pnl:SetPos(x+5 + inset_x, y) + pnl:SetSize(self:GetSize()) + pnl:SetWide(ScrW()) + pnl:MakePopup() + + pnl.OnEnter = function() + pace.BusyWithProperties = NULL + self.editing = false + + pnl:Remove() + self:SetText(pnl:GetText()) + self:SetValue(pnl:GetText()) + end + + local old = pnl.Paint + pnl.Paint = function(...) + if not self:IsValid() then pnl:Remove() return end + + surface.SetFont(pnl:GetFont()) + local w = surface.GetTextSize(pnl:GetText()) + 6 + + surface.DrawRect(0, 0, w, pnl:GetTall()) + surface.SetDrawColor(self:GetSkin().Colours.Properties.Border) + surface.DrawOutlinedRect(0, 0, w, pnl:GetTall()) + + pnl:SetWide(w) + + old(...) + end + + pace.BusyWithProperties = pnl + end + variable_name_pnl:SetValue(variable_name) + local btn = vgui.Create("DButton", variable_name_pnl) + btn:SetSize(16, 16) + btn:Dock(RIGHT) + btn:SetText("...") + btn.DoClick = function() + do + local get_list = function() + local enums = {} + local excluded_properties = {["ParentUID"] = true,["UniqueID"] = true} + for i,v in ipairs(pace.BulkSelectList) do + for _,prop in pairs(v:GetProperties()) do + if not excluded_properties[prop.key] and type(v["Get"..prop.key](v)) ~= "string" and type(v["Get"..prop.key](v)) ~= "boolean" then + enums[prop.key] = prop.key + end + end + end + return enums + end + pace.SafeRemoveSpecialPanel() + + local frame = vgui.Create("DFrame") + frame:SetTitle("Variable name") + frame:SetSize(300, 300) + frame:Center() + frame:SetSizable(true) + + local list = vgui.Create("DListView", frame) + list:Dock(FILL) + list:SetMultiSelect(false) + list:AddColumn("Variable name", 1) + + list.OnRowSelected = function(_, id, line) + local val = line.list_key + variable_name_pnl:SetValue(val) + variable_name = val + end + + local first = NULL + + local function build(find) + list:Clear() + + for key, val in pairs(get_list()) do + local pnl = list:AddLine(key) pnl.list_key = key + end + end + + local search = vgui.Create("DTextEntry", frame) + search:Dock(BOTTOM) + search.OnTextChanged = function() build(search:GetValue()) end + search.OnEnter = function() if first:IsValid() then list:SelectItem(first) end frame:Remove() end + search:RequestFocus() + frame:MakePopup() + + build() + + pace.ActiveSpecialPanel = frame + end + end + + -- + + local reset_button = vgui.Create("DButton", main_panel) + reset_button:SetText("reset") reset_button:SetSize(190,30) + properties_2:AddKeyValue("revert", reset_button) + function reset_button:DoClick() reset_initial_properties() end + + local apply_button = vgui.Create("DButton", main_panel) + apply_button:SetText("confirm") apply_button:SetSize(190,30) + properties_2:AddKeyValue("apply", apply_button) + function apply_button:DoClick() if full_success then setmultiple(variable_name, found_type) end end + main_panel:Center() + end + function pace.CopyUID(obj) pace.Clipboard = obj.UniqueID SetClipboardText("\"" .. obj.UniqueID .. "\"") @@ -2162,7 +2452,905 @@ function pace.GetPartSizeInformation(obj) } end +local part_classes_with_quicksetups = { + text = true, + particles = true, + proxy = true, + sprite = true, + projectile = true, + entity2 = true, + model2 = true, + group = true, + camera = true, + faceposer = true, + command = true, + bone3 = true, + health_modifier = true, + hitscan = true, + jiggle = true, + +} + +--those are more to configure a part into common setups, might involve creating other parts +function pace.AddQuickSetupsToPartMenu(menu, obj) + if not part_classes_with_quicksetups[obj.ClassName] and not obj.GetDrawPosition then + return + end + + local main, pnlmain = menu:AddSubMenu("quick setups") pnlmain:SetIcon("icon16/basket_go.png") + --base_movables can restructure, but nah bones aint it + if obj.GetDrawPosition and obj.ClassName ~= "bone" and obj.ClassName ~= "bone2" and obj.ClassName ~= "bone3" then + local substitutes, pnl = main:AddSubMenu("Restructure / Create parent substitute", function() + pace.SubstituteBaseMovable(obj, "create_parent") + timer.Simple(20, function() if pace.recently_substituted_movable_part == obj then pace.recently_substituted_movable_part = nil end end) + end) pnl:SetImage("icon16/application_double.png") + substitutes:AddOption("empty model", function() + --pulled from pace.SubstituteBaseMovable(obj, "create_parent") + local newObj = pac.CreatePart("model2") + if not IsValid(newObj) then return end + + newObj:SetParent(obj.Parent) + obj:SetParent(newObj) + + newObj:SetPosition(obj.Position) + newObj:SetPositionOffset(obj.PositionOffset) + newObj:SetAngles(obj.Angles) + newObj:SetAngleOffset(obj.AngleOffset) + newObj:SetEyeAngles(obj.EyeAngles) + newObj:SetAimPart(obj.AimPart) + newObj:SetAimPartName(obj.AimPartName) + newObj:SetBone(obj.Bone) + newObj:SetEditorExpand(true) + newObj:SetSize(0) + newObj:SetModel("models/empty.mdl") + + obj:SetPosition(Vector(0,0,0)) + obj:SetPositionOffset(Vector(0,0,0)) + obj:SetAngles(Angle(0,0,0)) + obj:SetAngleOffset(Angle(0,0,0)) + obj:SetEyeAngles(false) + obj:SetAimPart(nil) + obj:SetAimPartName("") + obj:SetBone("head") + + pace.RefreshTree() + end):SetIcon("icon16/anchor.png") + substitutes:AddOption("jiggle", function() + pace.SubstituteBaseMovable(obj, "create_parent", "jiggle") + end):SetIcon("icon16/chart_line.png") + substitutes:AddOption("interpolator", function() + pace.SubstituteBaseMovable(obj, "create_parent", "interpolated_multibone") + end):SetIcon("icon16/table_multiple.png") + end + + if obj.ClassName == "particles" then + main:AddOption("bare 3D setup", function() + obj:Set3D(true) obj:SetZeroAngle(false) obj:SetVelocity(0) obj:SetParticleAngleVelocity(Vector(0,0,0)) obj:SetGravity(Vector(0,0,0)) + end):SetIcon("icon16/star.png") + main:AddOption("simple 3D setup : Blast", function() + obj:Set3D(true) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetAngleOffset(Angle(90,0,0)) obj:SetStartSize(0) obj:SetEndSize(500) obj:SetFireOnce(true) obj:SetMaterial("particle/Particle_Ring_Wave_Additive") obj:SetVelocity(0) obj:SetParticleAngleVelocity(Vector(0,0,0)) obj:SetGravity(Vector(0,0,0)) obj:SetDieTime(1.5) + end):SetIcon("icon16/transmit.png") + main:AddOption("simple 3D setup : Slash", function() + obj:Set3D(true) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetAngleOffset(Angle(90,0,0)) obj:SetStartSize(100) obj:SetEndSize(90) obj:SetFireOnce(true) obj:SetMaterial("particle/Particle_Crescent") obj:SetVelocity(0) obj:SetParticleAngleVelocity(Vector(0,0,1500)) obj:SetGravity(Vector(0,0,0)) obj:SetDieTime(0.4) + end):SetIcon("icon16/arrow_refresh.png") + main:AddOption("simple setup : Piercer", function() + obj:Set3D(false) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetStartSize(30) obj:SetEndSize(10) obj:SetEndLength(100) obj:SetEndLength(1000) obj:SetFireOnce(true) obj:SetVelocity(50) obj:SetDieTime(0.2) + end):SetIcon("icon16/asterisk_orange.png") + main:AddOption("simple setup : Twinkle cloud", function() + obj:Set3D(false) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetStartSize(10) obj:SetEndSize(0) obj:SetEndLength(0) obj:SetEndLength(0) obj:SetFireOnce(false) obj:SetVelocity(0) obj:SetDieTime(0.5) obj:SetNumberParticles(2) obj:SetFireDelay(0.03) obj:SetPositionSpread(50) obj:SetGravity(Vector(0,0,0)) obj:SetMaterial("sprites/light_ignorez") + end):SetIcon("icon16/weather_snow.png") + main:AddOption("simple setup : Dust cloud", function() + obj:Set3D(false) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetStartSize(60) obj:SetEndSize(100) obj:SetEndLength(0) obj:SetEndLength(0) obj:SetStartAlpha(100) obj:SetFireOnce(false) obj:SetVelocity(0) obj:SetDieTime(2) obj:SetNumberParticles(2) obj:SetFireDelay(0.03) obj:SetPositionSpread(100) obj:SetGravity(Vector(0,0,-20)) + end):SetIcon("icon16/weather_clouds.png") + main:AddOption("simple setup : Dust kickup", function() + obj:Set3D(false) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetStartSize(10) obj:SetEndSize(15) obj:SetEndLength(0) obj:SetEndLength(0) obj:SetStartAlpha(100) obj:SetFireOnce(true) obj:SetSpread(0.8) obj:SetVelocity(100) obj:SetDieTime(2) obj:SetNumberParticles(10) obj:SetPositionSpread(1) obj:SetAirResistance(80) obj:SetGravity(Vector(0,0,-100)) + end):SetIcon("icon16/weather_clouds.png") + elseif obj.ClassName == "sprite" then + main:AddOption("simple shockwave (will use " .. (obj.Size == 1 and "size 200" or "existing size " .. obj.Size) ..")", function() + local proxyAlpha = pac.CreatePart("proxy") + proxyAlpha:SetParent(obj) + proxyAlpha:SetVariableName("Alpha") + proxyAlpha:SetExpression("clamp(1 - timeex()^0.5,0,1)") + local proxySize = pac.CreatePart("proxy") + proxySize:SetParent(obj) + proxySize:SetVariableName("Size") + proxySize:SetExpression((obj.Size == 1 and 200 or obj.Size) .. " * clamp(timeex()^0.5,0,1)") + obj:SetNotes("showhidetest") + + pace.FlashNotification("Hide and unhide the sprite to review its effects. An additional menu option will be provided for this.") + end):SetIcon("icon16/transmit.png") + main:AddOption("cross flare", function() + obj:SetSizeY(0.1) + local proxy1 = pac.CreatePart("proxy") + proxy1:SetParent(obj) + proxy1:SetVariableName("SizeY") + proxy1:SetExpression("0.15*clamp(1 - timeex()^0.5,0,1)") + local proxy1_size = pac.CreatePart("proxy") + proxy1_size:SetParent(obj) + proxy1_size:SetVariableName("Size") + proxy1_size:SetExpression("100 + 100*clamp(timeex()^0.5,0,1)") + + local sprite2 = pac.CreatePart("sprite") + sprite2:SetSpritePath(obj:GetSpritePath()) + sprite2:SetParent(obj) + sprite2:SetSizeX(0.1) + local proxy2 = pac.CreatePart("proxy") + proxy2:SetParent(sprite2) + proxy2:SetVariableName("SizeX") + proxy2:SetExpression("0.15*clamp(1 - timeex()^0.5,0,1)") + local proxy2_size = pac.CreatePart("proxy") + proxy2_size:SetParent(sprite2) + proxy2_size:SetVariableName("Size") + proxy2_size:SetExpression("100 + 100*clamp(timeex()^0.5,0,1)") + obj:SetNotes("showhidetest") + end):SetIcon("icon16/asterisk_yellow.png") + elseif obj.ClassName == "proxy" then + pnlmain:SetTooltip("remember you also have a preset library by right clicking on the expression field") + main:AddOption("command feedback attractor setup (-100, -50, 0, 50, 100)", function() + Derma_StringRequest("What should we call this attractor?", "Type a name for the commands.\nThese number ranges would be appropriate for positions\nIf you make more, name them something different", "target_number", function(str) + if str == "" then return end if str == " " then return end + local demonstration_values = {-100, -50, 0, 50, 100} + for i,value in ipairs(demonstration_values) do + local test_cmd_part = pac.CreatePart("command") test_cmd_part:SetParent(obj) + test_cmd_part:SetString("pac_proxy " .. str .. " " .. value) + end + obj:SetExpression("feedback() + 3*ftime()*(command(\"".. str .. "\") - feedback())") + end) + end):SetIcon("icon16/calculator.png") + main:AddOption("variable attractor multiplier base +1/0/-1", function() + Derma_StringRequest("What should we call this attractor?", "Type a name for the commands.\nIf you make more, name them something different", "target_number", function(str) + if str == "" then return end if str == " " then return end + local demonstration_values = {-1, 0, 1} + for i,value in ipairs(demonstration_values) do + local test_cmd_part = pac.CreatePart("command") test_cmd_part:SetParent(obj) + test_cmd_part:SetString("pac_proxy " .. str .. " " .. value) + end + local outsourced_proxy = pac.CreatePart("proxy") + outsourced_proxy:SetParent(obj) outsourced_proxy:SetName(str) + outsourced_proxy:SetExpression("feedback() + 3*ftime()*(command(\"".. str .. "\") - feedback())") + outsourced_proxy:SetExtra1("feedback() + 3*ftime()*(command(\"".. str .. "\") - feedback())") + if obj.Expression == "" then + obj:SetExpression("var1(\"".. str .. "\")") + else + obj:SetExpression(obj.Expression .. " * var1(\"".. str .. "\")") + end + end) + end):SetIcon("icon16/calculator.png") + main:AddOption("smoothen (wrap into dynamic feedback attractor)", function() + obj:SetExpression("feedback() + 4*ftime()*((" .. obj.Expression .. ") - feedback())") + end):SetIcon("icon16/calculator.png") + main:AddOption("smoothen (make extra variable attractor)", function() + Derma_StringRequest("What should we call this attractor variable?", "Type a name for the attractor. It will be used somewhere else like var1(\"eased_function\") for example\nsuggestions from the active functions:\n"..table.concat(obj:GetActiveFunctions(),"\n"), "eased_function", function(str) + local new_proxy = pac.CreatePart("proxy") new_proxy:SetParent(obj.Parent) + new_proxy:SetExpression("feedback() + 4*ftime()*((" .. obj.Expression .. ") - feedback())") + new_proxy:SetName(str) + new_proxy:SetExtra1(new_proxy.Expression) + end) + end):SetIcon("icon16/calculator.png") + elseif obj.ClassName == "text" then + main:AddOption("fast proxy link", function() + obj:SetTextOverride("Proxy") + obj:SetConcatenateTextAndOverrideValue(true) + --add proxy + local proxy = pac.CreatePart("proxy") + proxy:SetParent(obj) + proxy:SetVariableName("DynamicTextValue") + pace.Call("PartSelected", proxy) + end):SetIcon("icon16/calculator_link.png") + main:AddOption("quick large 2D text", function() + obj:SetDrawMode("DrawDrawText") + obj:SetFont("DermaLarge") + end):SetIcon("icon16/text_letter_omega.png") + main:AddOption("make HUD", function() + obj:SetBone("player_eyes") + obj:SetPosition(Vector(10,0,0)) + obj:SetDrawMode("SurfaceText") + local newevent = pac.CreatePart("event") + newevent:SetParent(obj) + newevent:SetEvent("viewed_by_owner") + end):SetIcon("icon16/monitor.png") + elseif obj.ClassName == "projectile" then + if obj.OutfitPartUID ~= "" then + local modelpart = obj.OutfitPart + if not modelpart.ClassName == "model2" then + modelpart = modelpart:GetChildren()[1] + if not modelpart.ClassName == "model2" then + return + end + end + if not modelpart.Model then return end + if obj.FallbackSurfpropModel ~= modelpart.Model then + main:AddOption("Reshape outfit part into a throwable prop: " .. obj.FallbackSurfpropModel, function() + obj:SetOverridePhysMesh(true) + obj:SetPhysical(true) + obj:SetRescalePhysMesh(true) + obj:SetRadius(modelpart.Size) + obj:SetFallbackSurfpropModel(modelpart.Model) + modelpart:SetHide(true) + end):SetIcon("materials/spawnicons/"..string.gsub(obj.FallbackSurfpropModel, ".mdl", "")..".png") + main:AddOption("Shape projectile into a throwable prop: " .. modelpart.Model, function() + obj:SetOverridePhysMesh(true) + obj:SetPhysical(true) + obj:SetRescalePhysMesh(true) + obj:SetFallbackSurfpropModel(modelpart.Model) + modelpart:SetHide(true) + modelpart:SetSize(obj.Radius) + modelpart:SetModel(obj.FallbackSurfpropModel) + end):SetIcon("materials/spawnicons/"..string.gsub(modelpart.Model, ".mdl", "")..".png") + end + + else + if obj.FallbackSurfpropModel then + main:AddOption("make throwable prop (" .. obj.FallbackSurfpropModel .. ")", function() + local modelpart = pac.CreatePart("model2") + modelpart:SetParent(obj) + obj:SetOverridePhysMesh(true) + obj:SetPhysical(true) + obj:SetRescalePhysMesh(true) + obj:SetOutfitPart(modelpart) + modelpart:SetHide(true) + modelpart:SetSize(obj.Radius) + modelpart:SetModel(obj.FallbackSurfpropModel) + end):SetIcon("materials/spawnicons/"..string.gsub(obj.FallbackSurfpropModel, ".mdl", "")..".png") + end + main:AddOption("make throwable prop (opens asset browser)", function() + local modelpart = pac.CreatePart("model2") + modelpart:SetParent(obj) + obj:SetRadius(1) + obj:SetOverridePhysMesh(true) + obj:SetPhysical(true) + obj:SetRescalePhysMesh(true) + obj:SetOutfitPart(modelpart) + modelpart:SetHide(true) + pace.AssetBrowser(function(path) + modelpart:SetModel(path) + obj:SetFallbackSurfpropModel(path) + end, "models") + end):SetIcon("icon16/link.png") + end + main:AddOption("make shield", function() + local model = pac.CreatePart("model2") model:SetModel("models/props_lab/blastdoor001c.mdl") + model:SetParent(obj) + obj:SetPosition(Vector(60,0,0)) + obj:SetRadius(1) + obj:SetSpeed(0) + obj:SetMass(10000) + obj:SetBone("invalidbone") + obj:SetOverridePhysMesh(true) + obj:SetPhysical(true) + obj:SetOutfitPart(model) + obj:SetCollideWithOwner(true) + obj:SetCollideWithSelf(true) + obj:SetFallbackSurfpropModel("models/props_lab/blastdoor001c.mdl") + model:SetHide(true) + pace.PopulateProperties(obj) + end):SetIcon("icon16/shield.png") + + elseif obj.ClassName == "entity2" then + if obj:GetOwner().GetBodyGroups then + local bodygroups = obj:GetOwner():GetBodyGroups() + if #bodygroups > 0 then + local submenu, pnl = main:AddSubMenu("toggleable bodygroup with a dual proxy") pnl:SetImage("icon16/table_refresh.png") + pnl:SetTooltip("It will apply 1 and 0. But if there are more variations in that bodygroup, change the expression and the expression on hide if you wish") + for i,bodygroup in ipairs(bodygroups) do + if bodygroup.num == 1 then continue end + local pnl = submenu:AddOption(bodygroup.name, function() + local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) + proxy:SetExpression("1") proxy:SetExpressionOnHide("0") + proxy:SetVariableName(bodygroup.name) + local event = pac.CreatePart("event") event:SetParent(proxy) event:SetEvent("command") event:SetArguments(string.Replace(bodygroup.name, " ")) + end) + pnl:SetTooltip(table.ToString(bodygroup.submodels, nil, true)) + end + end + end + main:AddOption("create submaterial zone togglers (hide/show materials)", function() + local mats = obj:GetOwner():GetMaterials() + local mats_str = table.concat(mats,"\n") + local dyn_props = obj:GetDynamicProperties() + Derma_StringRequest("submaterial togglers", "please input a submaterial name or a list of submaterial names with spaces\navailable materials:\n"..mats_str, "", function(str) + local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) event:SetEvent("command") event:SetArguments("materials_"..string.sub(obj.UniqueID,1,6)) + local proxy = pac.CreatePart("proxy") proxy:SetAffectChildren(true) proxy:SetVariableName("no_draw") proxy:SetExpression("0") proxy:SetExpressionOnHide("1") + event:SetParent(obj) proxy:SetParent(event) + for i, kw in ipairs(string.Split(str, " ")) do + for id,mat2 in ipairs(mats) do + if string.GetFileFromFilename(mat2) == kw then + local mat = pac.CreatePart("material_3d") mat:SetParent(proxy) + mat:SetName("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) + mat:SetLoadVmt(mat2) + dyn_props[kw].set("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) + end + end + end + end) + end):SetIcon("icon16/picture_delete.png") + elseif obj.ClassName == "model2" then + local pm = pace.current_part:GetPlayerOwner():GetModel() + local pm_selected = player_manager.TranslatePlayerModel(GetConVar("cl_playermodel"):GetString()) + + if pm_selected ~= pm then + main:AddOption("Selected playermodel - " .. string.gsub(string.GetFileFromFilename(pm_selected), ".mdl", ""), function() + obj:SetModel(pm_selected) + obj.pace_properties["Model"]:SetValue(pm_selected) + pace.PopulateProperties(obj) + + end):SetImage("materials/spawnicons/"..string.gsub(pm_selected, ".mdl", "")..".png") + end + main: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(obj) + + 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() + if wep:GetClass() ~= "none" then --the uh hands have no model + main:AddOption("Active weapon - " .. wep:GetClass() .. " - model - " .. string.gsub(string.GetFileFromFilename(wep_mdl), ".mdl", ""), function() + obj:SetModel(wep_mdl) + obj.pace_properties["Model"]:SetValue(wep_mdl) + pace.PopulateProperties(obj) + end):SetImage("materials/spawnicons/"..string.gsub(wep_mdl, ".mdl", "")..".png") + end + end + if obj.Owner.GetBodyGroups then + local bodygroups = obj.Owner:GetBodyGroups() + if (#bodygroups > 1) or (#bodygroups[1].submodels > 1) then + local submenu, pnl = main:AddSubMenu("toggleable bodygroup with a dual proxy") pnl:SetImage("icon16/table_refresh.png") + pnl:SetTooltip("It will apply 1 and 0. But if there are more variations in that bodygroup, change the expression and the expression on hide if you wish") + for i,bodygroup in ipairs(bodygroups) do + if bodygroup.num == 1 then continue end + local pnl = submenu:AddOption(bodygroup.name, function() + local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) + proxy:SetExpression("1") proxy:SetExpressionOnHide("0") + proxy:SetVariableName(bodygroup.name) + local event = pac.CreatePart("event") event:SetParent(proxy) event:SetEvent("command") event:SetArguments(string.Replace(bodygroup.name, " ")) + end) + pnl:SetTooltip(table.ToString(bodygroup.submodels, nil, true)) + end + end + end + + main:AddOption("create submaterial zone togglers (hide/show materials)", function() + local mats = obj:GetOwner():GetMaterials() + local mats_str = table.concat(mats,"\n") + local dyn_props = obj:GetDynamicProperties() + Derma_StringRequest("submaterial togglers", "please input a submaterial name or a list of submaterial names with spaces\navailable materials:\n"..mats_str, "", function(str) + local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) event:SetEvent("command") event:SetArguments("materials_"..string.sub(obj.UniqueID,1,6)) + local proxy = pac.CreatePart("proxy") proxy:SetAffectChildren(true) proxy:SetVariableName("no_draw") proxy:SetExpression("0") proxy:SetExpressionOnHide("1") + event:SetParent(obj) proxy:SetParent(event) + for i, kw in ipairs(string.Split(str, " ")) do + for id,mat2 in ipairs(mats) do + if string.GetFileFromFilename(mat2) == kw then + local mat = pac.CreatePart("material_3d") mat:SetParent(proxy) + mat:SetName("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) + mat:SetLoadVmt(mat2) + dyn_props[kw].set("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) + end + end + end + end) + end):SetIcon("icon16/picture_delete.png") + + local collapses, pnl = main:AddSubMenu("bone collapsers") pnl:SetImage("icon16/compress.png") + collapses:AddOption("collapse arms", function() + local group = pac.CreatePart("group") group:SetParent(obj) + local right = pac.CreatePart("bone3") right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right clavicle") + local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left clavicle") + end):SetIcon("icon16/user.png") + collapses:AddOption("collapse legs", function() + local group = pac.CreatePart("group") group:SetParent(obj) + local right = obj + right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right thigh") + local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left thigh") + end):SetIcon("icon16/user.png") + collapses:AddOption("collapse by keyword", function() + Derma_StringRequest("collapse bones", "please input a keyword to match", "head", function(str) + local group = pac.CreatePart("group") group:SetParent(obj) + local ent = obj:GetOwner() + for bone,tbl in pairs(pac.GetAllBones(ent)) do + if string.find(bone, str) ~= nil then + local newbone = pac.CreatePart("bone3") newbone:SetParent(group) newbone:SetSize(0) newbone:SetScaleChildren(true) newbone:SetBone(bone) + end + end + end) + end):SetIcon("icon16/text_align_center.png") + + elseif obj.ClassName == "group" then + main:AddOption("Assign to viewmodel", function() + obj:SetParent() + obj:SetOwnerName("viewmodel") + pace.RefreshTree(true) + end):SetIcon("icon16/user.png") + main:AddOption("Assign to hands", function() + obj:SetParent() + obj:SetOwnerName("hands") + pace.RefreshTree(true) + end):SetIcon("icon16/user.png") + main:AddOption("Assign to active vehicle", function() + obj:SetParent() + obj:SetOwnerName("active vehicle") + pace.RefreshTree(true) + end):SetIcon("icon16/user.png") + main:AddOption("Assign to active weapon", function() + obj:SetParent() + obj:SetOwnerName("active weapon") + pace.RefreshTree(true) + end):SetIcon("icon16/user.png") + main:AddOption("gather arm parts into hands", function() + if #obj:GetChildrenList() == 0 then return end + local gatherable_classes = { + model2 = true, + model = true, + } + local groupable_classes = { + group = true, + event = true, + } + local newgroup = pac.CreatePart("group") + local function ProcessDrawablePartsRecursively(part, root) + if gatherable_classes[part.ClassName] then + if not (string.find(part.Bone, "hand") ~= nil or string.find(part.Bone, "upperarm") ~= nil or string.find(part.Bone, "forearm") ~= nil + or string.find(part.Bone, "wrist") ~= nil or string.find(part.Bone, "ulna") ~= nil or string.find(part.Bone, "bicep") ~= nil + or string.find(part.Bone, "finger") ~= nil) + then + part:Remove() + end + elseif groupable_classes[part.ClassName] then + for i, child in ipairs(part:GetChildrenList()) do + ProcessDrawablePartsRecursively(child, root) + end + else + part:Remove() + end + end + pace.Copy(obj) + pace.Paste(newgroup) + ProcessDrawablePartsRecursively(newgroup, newgroup) + + newgroup:SetOwnerName("hands") + newgroup:SetName("[HANDS]") + pace.RefreshTree(true) + end):SetIcon("icon16/user.png") + elseif obj.ClassName == "camera" then + local function extract_camera_from_jiggle() + camera = obj + if not IsValid(camera.recent_jiggle) then + return + end + local jig = camera.recent_jiggle + local camang = jig:GetAngles() + local campos = jig:GetPosition() + local cambone = jig:GetBone() + local camparent = jig:GetParent() + camera:SetParent(camparent) + camera:SetBone(cambone) camera:SetAngles(camang) camera:SetPosition(campos) + jig:SetBone("head") jig:SetAngles(Angle(0,0,0)) jig:SetPosition(Vector(0,0,0)) + end + local function insert_camera_into_jiggle() + camera = obj + local jig = camera.recent_jiggle + if not IsValid(camera.recent_jiggle) then + jig = pac.CreatePart("jiggle") + camera.recent_jiggle = jig + jig:SetEditorExpand(true) + end + jig:SetParent(camera:GetParent()) + jig:SetBone(camera.Bone) jig:SetAngles(camera:GetAngles()) jig:SetPosition(camera:GetPosition()) + camera:SetBone("head") camera:SetAngles(Angle(0,0,0)) camera:SetPosition(Vector(0,0,0)) + camera:SetParent(jig) + return jig + end + + --helper variable to adjust relative to player height + local ent = obj:GetRootPart():GetOwner() + local height = math.Round((ent:GetBonePosition(ent:LookupBone("ValveBiped.Bip01_Head1")) - ent:GetPos()).z,1) + main:AddOption("calculated head height : " .. height):SetIcon("icon16/help.png") + + local fp, pnl = main:AddSubMenu("first person camera setups") pnl:SetImage("icon16/eye.png") + fp:AddOption("easy first person (head)", function() + extract_camera_from_jiggle() + obj:SetBone("head") + obj:SetPosition(Vector(5,-4,0)) obj:SetEyeAnglesLerp(1) obj:SetAngles(Angle(0,-90,-90)) + pace.PopulateProperties(obj) + end):SetIcon("icon16/eye.png") + + fp:AddOption("on neck + collapsed head", function() + extract_camera_from_jiggle() + obj:SetBone("neck") + obj:SetPosition(Vector(5,0,0)) obj:SetEyeAnglesLerp(1) obj:SetAngles(Angle(0,-90,-90)) + local bone = pac.CreatePart("bone3") + bone:SetScaleChildren(true) bone:SetSize(0) + bone:SetParent(obj) + local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(bone) + pace.PopulateProperties(obj) + end):SetIcon("icon16/eye.png") + + fp:AddOption("on neck + collapsed head + eyeang limiter", function() + extract_camera_from_jiggle() + obj:SetBone("neck") + obj:SetPosition(Vector(5,0,0)) obj:SetEyeAnglesLerp(0.7) obj:SetAngles(Angle(0,-90,-90)) + local bone = pac.CreatePart("bone3") + bone:SetScaleChildren(true) bone:SetSize(0) + bone:SetParent(obj) + local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(bone) + pace.PopulateProperties(obj) + end):SetIcon("icon16/eye.png") + + main:AddOption("smoothen", function() + insert_camera_into_jiggle() + pace.PopulateProperties(obj) + end):SetIcon("icon16/chart_line.png") + + main:AddOption("close up (zoomed on the face)", function() + extract_camera_from_jiggle() + obj:SetBone("head") obj:SetAngles(Angle(0,90,90)) obj:SetPosition(Vector(3,-20,0)) obj:SetEyeAnglesLerp(0) obj:SetFOV(45) + pace.PopulateProperties(obj) + end):SetIcon("icon16/monkey.png") + + main:AddOption("Cowboy / medium shot (waist up) (relative to neck)", function() + extract_camera_from_jiggle() + obj:SetBone("neck") obj:SetAngles(Angle(0,120,90)) obj:SetPosition(Vector(14,-24,0)) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) + pace.PopulateProperties(obj) + end):SetIcon("icon16/user.png") + + main:AddOption("Cowboy / medium shot (waist up) (no bone) (20 + 0.6*height = " .. (20 + 0.6*height) .. ")", function() + extract_camera_from_jiggle() + obj:SetBone("invalidbone") obj:SetAngles(Angle(0,180,0)) obj:SetPosition(Vector(40,0,20 + 0.6*height)) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) + pace.PopulateProperties(obj) + end):SetIcon("icon16/user.png") + + main:AddOption("over the shoulder (no bone) (12 + 0.8*height = " .. 12 + 0.8*height .. ")", function() + extract_camera_from_jiggle() + obj:SetBone("invalidbone") obj:SetAngles(Angle(0,0,0)) obj:SetPosition(Vector(-30,15,12 + 0.8*height)) obj:SetEyeAnglesLerp(0.3) obj:SetFOV(-1) + pace.PopulateProperties(obj) + end):SetIcon("icon16/user_gray.png") + + main:AddOption("over the shoulder (with jiggle)", function() + local jiggle = insert_camera_into_jiggle() + jiggle:SetConstrainSphere(75) jiggle:SetSpeed(3) + obj:SetEyeAnglesLerp(0.7) obj:SetFOV(-1) + jiggle:SetBone("neck") jiggle:SetAngles(Angle(180,90,90)) jiggle:SetPosition(Vector(-2,18,-10)) + pace.PopulateProperties(obj) + end):SetIcon("icon16/user_gray.png") + + main:AddOption("full shot", function() + extract_camera_from_jiggle() + obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) + obj:SetBone("invalidbone") obj:SetAngles(Angle(6,180,0)) obj:SetPosition(Vector(height,-15,height * 0.7)) + pace.PopulateProperties(obj) + end):SetIcon("icon16/user_suit.png") + + main:AddOption("wide shot (with jiggle)", function() + local jiggle = insert_camera_into_jiggle() + jiggle:SetConstrainSphere(150) jiggle:SetSpeed(1) + obj:SetEyeAnglesLerp(0.2) obj:SetFOV(-1) + jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(0,15,120)) + obj:SetPosition(Vector(-250,0,0)) + pace.PopulateProperties(obj) + end):SetIcon("icon16/arrow_out.png") + + main:AddOption("extreme wide shot (with jiggle)", function() + local jiggle = insert_camera_into_jiggle() + jiggle:SetConstrainSphere(0) jiggle:SetSpeed(0.3) + obj:SetEyeAnglesLerp(0.1) obj:SetFOV(-1) + jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(-500,0,200)) + obj:SetPosition(Vector(0,0,0)) obj:SetAngles(Angle(15,0,0)) + pace.PopulateProperties(obj) + end):SetIcon("icon16/map.png") + + main:AddOption("bird eye view (with jiggle)", function() + local jiggle = insert_camera_into_jiggle() + jiggle:SetConstrainSphere(300) jiggle:SetSpeed(1) + obj:SetEyeAnglesLerp(0.2) obj:SetFOV(-1) + jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(-150,0,300)) + obj:SetPosition(Vector(0,0,0)) obj:SetAngles(Angle(70,0,0)) + pace.PopulateProperties(obj) + end):SetIcon("icon16/map_magnify.png") + + main:AddOption("Dutch shot (tilt)", function() + local jiggle = insert_camera_into_jiggle() + jiggle:SetConstrainSphere(150) jiggle:SetSpeed(1) + obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) + jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(0,15,50)) + obj:SetPosition(Vector(-75,0,0)) obj:SetAngles(Angle(0,0,25)) + pace.PopulateProperties(obj) + end):SetIcon("icon16/arrow_refresh.png") + elseif obj.ClassName == "faceposer" then + if obj:GetDynamicProperties() == nil then main:AddOption("No flexes found!"):SetIcon("icon16/cancel.png") return end + main:AddOption("reset expressions", function() + for i,prop in pairs(obj:GetDynamicProperties()) do + if string.lower(prop.key) == prop.key or prop.key == "Blink" then + prop.set(obj,0) + end + end + pace.PopulateProperties(obj) + end):SetIcon("icon16/cancel.png") + local flexes = {} + for i,prop in pairs(obj:GetDynamicProperties()) do + flexes[prop.key] = prop.key + end + + local function full_match(tbl) + for i,v in pairs(tbl) do + if not flexes[v] then + return false + end + end + return true + end + local common_combinations = { + {"eyes_look_down", "eyes_look_up", "eyes_look_right", "eyes_look_left"}, + {"eyes-look-down", "eyes-look-up", "eyes-look-right", "eyes-look-left"}, + {"eye_left_down", "eye_left_up", "eye_left_right", "eye_left_left", "eye_right_down", "eye_right_up", "eye_right_right", "eye_right_left"}, + {"Eyes Down", "Eyes Up", "Eyes Right", "Eyes Left"}, + {"eyes down", "eyes up", "eyes right", "eyes left"}, + {"eye_down", "eye_up", "eye_right", "eye_left"}, + {"LookDown", "LookUp", "LookRight", "LookLeft"} + } + local final_combination + for i,tbl in ipairs(common_combinations) do + if full_match(tbl) then + final_combination = tbl + end + end + + if final_combination then + main:AddOption("4-way look", function() + for _, flex in ipairs(final_combination) do + local new_proxy = pac.CreatePart("proxy") new_proxy:SetParent(obj) + if string.match(string.lower(flex), "down$") then + new_proxy:SetExpression("pose_parameter_true(\"head_pitch\")/60") + elseif string.match(string.lower(flex), "up$") then + new_proxy:SetExpression("-pose_parameter_true(\"head_pitch\")/60") + elseif string.match(string.lower(flex), "left$") then + new_proxy:SetExpression("pose_parameter_true(\"head_yaw\")/30") + elseif string.match(string.lower(flex), "right$") then + new_proxy:SetExpression("-pose_parameter_true(\"head_yaw\")/30") + end + new_proxy:SetVariableName(flex) + end + end):SetIcon("icon16/calculator.png") + else --what if those are bones? + + end + + main:AddOption("add face camera and view it", function() + local cam = pac.CreatePart("camera") cam:SetParent(obj) + cam:SetBone("head") cam:SetAngles(Angle(0,90,90)) cam:SetPosition(Vector(3,-20,0)) cam:SetEyeAnglesLerp(0) cam:SetFOV(45) + pace.PopulateProperties(cam) + pace.ManuallySelectCamera(cam, true) + end):SetIcon("icon16/camera.png") + elseif obj.ClassName == "command" then + if pac.LocalPlayer.pac_command_events then + local cmd_menu, pnl = main:AddSubMenu("command event activators") pnl:SetImage("icon16/clock_red.png") + for cmd,_ in SortedPairs(pac.LocalPlayer.pac_command_events) do + cmd_menu2, pnl2 = cmd_menu:AddSubMenu(cmd) pnl2:SetImage("icon16/clock_red.png") + cmd_menu2:AddOption("instant", function() + obj:SetString("pac_event " .. cmd) + end):SetImage("icon16/clock_red.png") + cmd_menu2:AddOption("on", function() + obj:SetString("pac_event " .. cmd .. " 1") + end):SetImage("icon16/clock_red.png") + cmd_menu2:AddOption("off", function() + obj:SetString("pac_event " .. cmd .. " 0") + end):SetImage("icon16/clock_red.png") + cmd_menu2:AddOption("toggle", function() + obj:SetString("pac_event " .. cmd .. " 2") + end):SetImage("icon16/clock_red.png") + end + + main:AddOption("save current events to a single command", function() + local tbl3 = {} + for i,v in pairs(pac.LocalPlayer.pac_command_events) do tbl3[i] = v.on end + new_expression = "" + for i,v in pairs(tbl3) do new_expression = new_expression .. "pac_event " .. i .. " " .. v .. ";" end + obj:SetUseLua(false) + end):SetIcon("icon16/application_xp_terminal.png") + + end + local inputs = {"forward", "back", "moveleft", "moveright", "attack", "attack2", "use", "left", "right", "jump", "duck", "speed", "walk", "reload", "alt1", "alt2", "showscores", "grenade1", "grenade2"} + local input_menu, pnl = main:AddSubMenu("movement controllers (dash etc.)") + --standard blip + local input_menu1, pnl2 = input_menu:AddSubMenu("quick trigger") pnl2:SetImage("icon16/asterisk_yellow.png") + for i,mv in ipairs(inputs) do + input_menu1:AddOption(mv, function() + obj:SetString("+"..mv) + local timerx = pac.CreatePart("event") timerx:SetParent(obj) timerx:SetAffectChildrenOnly(true) timerx:SetEvent("timerx") timerx:SetArguments("0.2@@1@@0") + local off_cmd = pac.CreatePart("command") off_cmd:SetParent(timerx) off_cmd:SetString("-"..mv) + end):SetIcon("icon16/asterisk_yellow.png") + end + --button substitutor + local input_menu2, pnl2 = input_menu:AddSubMenu("button pair (fake bind)") pnl2:SetImage("icon16/contrast_high.png") + for i,mv in ipairs(inputs) do + input_menu2:AddOption(mv, function() + Derma_StringRequest("movement command setup", "write a button to use!", "mouse_left", function(str) + obj:SetString("+"..mv) + local newevent1 = pac.CreatePart("event") newevent1:SetEvent("button") newevent1:SetInvert(true) newevent1:SetArguments(str) + local newevent2 = pac.CreatePart("event") newevent2:SetEvent("button") newevent2:SetInvert(false) newevent2:SetArguments(str) + local off_cmd = pac.CreatePart("command") off_cmd:SetString("-"..mv) + + off_cmd:SetParent(obj.Parent) + if obj.Parent.ClassName == "event" and obj.Parent.AffectChildrenOnly then + local parent = obj.Parent + newevent1:SetParent(parent) + newevent1:SetAffectChildrenOnly(true) + obj:SetParent(newevent1) + newevent2:SetParent(parent) + newevent2:SetAffectChildrenOnly(true) + off_cmd:SetParent(newevent2) + else + newevent1:SetParent(obj) + newevent2:SetParent(off_cmd) + off_cmd:SetParent(obj.Parent) + end + end) + end):SetIcon("icon16/contrast_high.png") + end + pnl:SetImage("icon16/keyboard.png") + + + local lua_menu, pnl = main:AddSubMenu("Lua hackery") pnl:SetImage("icon16/page_code.png") + lua_menu:AddOption("Chat decoder -> command proxy", function() + Derma_StringRequest("create chat decoder", "please input a name to use for the decoder.\ne.g. you will say \"value=5", "", function(str) + obj:SetUseLua(true) obj:SetString([[local strs = string.Split(LocalPlayer().pac_say_event.str, "=") RunConsoleCommand("pac_proxy", "]] .. str .. [[", tonumber(strs[2]))]]) + local say = pac.CreatePart("event") say:SetEvent("say") say:SetInvert(true) say:SetArguments(str .. "=0.5") say:SetAffectChildrenOnly(true) + local timerx = pac.CreatePart("event") timerx:SetEvent("timerx") timerx:SetInvert(false) timerx:SetArguments("0.2@@1@@0") timerx:SetAffectChildrenOnly(true) + say:SetParent(obj.Parent) timerx:SetParent(say) obj:SetParent(say) + local proxy = pac.CreatePart("proxy") proxy:SetExpression("command(\"" .. str .. "\")") + proxy:SetParent(obj.Parent) + end) + end):SetIcon("icon16/comment.png") + lua_menu:AddOption("random command (e.g. trigger random animations)", function() + Derma_StringRequest("create random command", "please input a name for the event series\nyou should probably already have a series of command events like animation1, animation2, animation3 etc", "", function(str) + obj:SetUseLua(true) obj:SetString([[local num = math.floor(math.random()*5) RunConsoleCommand("pac_event", "]] .. str .. [[" num]]) + end) + end):SetIcon("icon16/award_star_gold_1.png") + lua_menu:AddOption("random command (pac_proxy)", function() + Derma_StringRequest("create random command", "please input a name for the proxy command", "", function(str) + obj:SetUseLua(true) obj:SetString([[local num = math.random()*100 RunConsoleCommand("pac_proxy", "]] .. str .. [[" num]]) + end) + end):SetIcon("icon16/calculator.png") + lua_menu:AddOption("X-Ray hook (halos)", function() + obj:SetName("halos on") obj:SetString([["hook.Add("PreDrawHalos","xray_halos", function() halo.Add(ents.FindByClass("npc_combine_s"), Color(255,0,0), 5, 5, 5, true, true) end)"]]) + local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) obj:SetName("halos off") obj:SetString([["hook.Remove("PreDrawHalos","xray_halos")"]]) + obj:SetName("halos on") obj:SetString([["hook.Add("PreDrawHalos","xray_halos", function() halo.Add(ents.FindByClass("npc_combine_s"), Color(255,0,0), 5, 5, 5, true, true) end)"]]) + local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) obj:SetName("halos off") obj:SetString([["hook.Remove("PreDrawHalos","xray_halos")"]]) + obj:SetUseLua(true) newobj:SetUseLua(true) + end):SetIcon("icon16/shading.png") + lua_menu:AddOption("X-Ray hook (ignorez)", function() + obj:SetName("ignoreZ on") obj:SetString([["hook.Add("PreDrawHalos","xray_halos", function() halo.Add(ents.FindByClass("npc_combine_s"), Color(255,0,0), 5, 5, 5, true, true) end)"]]) + local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) obj:SetName("halos off") obj:SetString([["hook.Remove("PreDrawHalos","xray_halos")"]]) + obj:SetName("ignoreZ off") obj:SetString([["hook.Add("PostDrawTranslucentRenderables","xray_ignorez", function() cam.IgnoreZ( true ) for i,ent in pairs(ents.FindByClass("npc_combine_s")) do ent:DrawModel() end cam.IgnoreZ( false ) end)"]]) + local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) obj:SetName("halos off") obj:SetString([["hook.Remove("PostDrawTranslucentRenderables","xray_ignorez")"]]) + obj:SetUseLua(true) newobj:SetUseLua(true) + end):SetIcon("icon16/shape_move_front.png") + elseif obj.ClassName == "bone3" then + local collapses, pnl = main:AddSubMenu("bone collapsers") pnl:SetImage("icon16/compress.png") + collapses:AddOption("collapse arms", function() + local group = pac.CreatePart("group") group:SetParent(obj.Parent) + local right = pac.CreatePart("bone3") right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right clavicle") + local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left clavicle") + end):SetIcon("icon16/compress.png") + collapses:AddOption("collapse legs", function() + local group = pac.CreatePart("group") group:SetParent(obj.Parent) + local right = obj + right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right thigh") + local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left thigh") + end):SetIcon("icon16/compress.png") + collapses:AddOption("collapse by keyword", function() + Derma_StringRequest("collapse bones", "please input a keyword to match", "head", function(str) + local group = pac.CreatePart("group") group:SetParent(obj.Parent) + local ent = obj:GetOwner() + for bone,tbl in pairs(pac.GetAllBones(ent)) do + if string.find(bone, str) ~= nil then + local newbone = pac.CreatePart("bone3") newbone:SetParent(group) newbone:SetSize(0) newbone:SetScaleChildren(true) newbone:SetBone(bone) + end + end + end) + end):SetIcon("icon16/text_align_center.png") + elseif obj.ClassName == "health_modifier" then + main:AddOption("setup HUD display for extra health (total)", function() + local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") + local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") + cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75}) +surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true}) +local x = 50 +local y = ScrH() - 190 +local clr = Color(255,230,0) +hook.Add("HUDPaint", "extrahealth_total", function() + draw.DrawText("PAC EX HP", "Trebuchet24", x, y + 20, clr) + draw.DrawText("subtitle", "Trebuchet18", x, y + 40, clr) + draw.DrawText(LocalPlayer().pac_healthbars_total, "HudNumbersGlow_Bigger", x + 100, y, clr) + draw.DrawText(LocalPlayer().pac_healthbars_total, "HudNumbers_Bigger", x + 100, y, clr) +end)]]) + cmd_off:SetString([[hook.Remove("HUDPaint", "extrahealth_total")]]) + end):SetIcon("icon16/application_xp_terminal.png") + + main:AddOption("setup HUD display for extra health (this part only)", function() + local function setup() + local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") + local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") + cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75}) +surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true}) +local x = 50 +local y = ScrH() - 190 +local clr = Color(255,230,0) +hook.Add("HUDPaint", "extrahealth_]]..obj.UniqueID..[[", function() + draw.DrawText("PAC EX HP\n]]..obj:GetName()..[[", "Trebuchet24", x, y + 20, clr) + draw.DrawText(LocalPlayer().pac_healthbars_uidtotals["]]..obj.UniqueID..[["], "HudNumbersGlow_Bigger", x + 100, y, clr) + draw.DrawText(LocalPlayer().pac_healthbars_uidtotals["]]..obj.UniqueID..[["], "HudNumbers_Bigger", x + 100, y, clr) +end)]]) + cmd_off:SetString([[hook.Remove("HUDPaint", "extrahealth_]]..obj.UniqueID..[[")]]) + end + if obj.Name == "" then + Derma_StringRequest("prompt", "Looks like your health modifier doesn't have a name.\ngive it one?", "", function(str) obj:SetName(str) setup() end) + else + setup(obj.Name) + end + end):SetIcon("icon16/application_xp_terminal.png") + + main:AddOption("setup HUD display for extra health (this layer)", function() + local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") + local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") + cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75}) +surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true}) +local x = 50 +local y = ScrH() - 190 +local clr = Color(255,230,0) +hook.Add("HUDPaint", "extrahealth_layer_]]..obj.BarsLayer..[[", function() + draw.DrawText("PAC EX HP\nLYR]]..obj.BarsLayer..[[", "Trebuchet24", x, y + 20, clr) + draw.DrawText(LocalPlayer().pac_healthbars_layertotals[]]..obj.BarsLayer..[[], "HudNumbersGlow_Bigger", x + 100, y, clr) + draw.DrawText(LocalPlayer().pac_healthbars_layertotals[]]..obj.BarsLayer..[[], "HudNumbers_Bigger", x + 100, y, clr) +end)]]) + cmd_off:SetString([[hook.Remove("HUDPaint", "extrahealth_layer_]]..obj.BarsLayer..[[")]]) + end):SetIcon("icon16/application_xp_terminal.png") + + main:AddOption("Use extra health (total value) in a proxy", function() local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) proxy:SetExpression("pac_healthbars_total()") proxy:SetExtra1(obj.Expression) end):SetIcon("icon16/calculator.png") + main:AddOption("Use extra health (this part's current HP) in a proxy", function() local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) proxy:SetExpression("pac_healthbar_uidvalue(\""..obj.UniqueID.."\")") end):SetIcon("icon16/calculator.png") + main:AddOption("Use extra health (this part's remaining number of bars) in a proxy", function() local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) proxy:SetExpression("pac_healthbar_remaining_bars(\""..obj.UniqueID.."\")") end):SetIcon("icon16/calculator.png") + main:AddOption("Use extra health (this layer's current total value) in a proxy", function() local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) proxy:SetExpression("pac_healthbars_layertotal("..obj.BarsLayer..")") end):SetIcon("icon16/calculator.png") + elseif obj.ClassName == "hitscan" then + main:AddOption("approximate tracers from particles", function() + if not obj.previous_tracerparticle then + obj.previous_tracerparticle = pac.CreatePart("particles") + end + local particle = obj.previous_tracerparticle + particle:SetParent(obj) + particle:SetNumberParticles(obj.NumberBullets) particle:SetDieTime(0.3) + particle:SetSpread(obj.Spread) obj:SetTracerSparseness(0) + particle:SetMaterial("sprites/orangecore1") particle:SetLighting(false) particle:SetCollide(false) + particle:SetFireOnce(true) particle:SetStartSize(10) particle:SetEndSize(0) particle:SetStartLength(250) particle:SetEndLength(2000) + particle:SetGravity(Vector(0,0,0)) + end):SetIcon("icon16/water.png") + elseif obj.ClassName == "jiggle" then + local named_part = obj.Parent + if pace.recently_substituted_movable_part then + if pace.recently_substituted_movable_part.Parent == obj then + named_part = pace.recently_substituted_movable_part + end + end + local str = named_part:GetName() str = string.Replace(str," ","") + main:AddOption("jiggle speed trick: deployable anchor (hidden by event)", function() + obj:SetSpeed(0) obj:SetResetOnHide(true) + local event = pac.CreatePart("event") event:SetParent(obj) + event:SetEvent("command") event:SetArguments("jiggle_anchor_"..str) + end):SetIcon("icon16/anchor.png") + main:AddOption("jiggle speed trick: movable anchor (proxy control)", function() + obj:SetSpeed(0) obj:SetResetOnHide(true) + local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) + proxy:SetVariableName("Speed") + proxy:SetExpression("3") proxy:SetExpressionOnHide("0") + local event = pac.CreatePart("event") event:SetParent(proxy) + event:SetEvent("command") event:SetArguments("jiggle_anchor_"..str) + end):SetIcon("icon16/anchor.png") + end +end + +--these are more to perform an action that doesn't really affect many different parameters. maybe one or two at most function pace.AddClassSpecificPartMenuComponents(menu, obj) + if obj.Notes == "showhidetest" then menu:AddOption("(hide/show test) reset", function() obj:CallRecursive("OnShow") end):SetIcon("icon16/star.png") end + if obj.ClassName == "camera" then if not obj:IsHidden() then if obj ~= pac.active_camera then @@ -2216,7 +3404,10 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) elseif obj.ClassName == "hitscan" then menu:AddOption("fire", function() obj:Shoot() end):SetIcon("icon16/star.png") elseif obj.ClassName == "damage_zone" then - menu:AddOption("run command", function() obj:OnShow() end):SetIcon("icon16/star.png") + menu:AddOption("do damage", function() obj:OnShow() end):SetIcon("icon16/star.png") + menu:AddOption("debug: clear hit markers", function() obj:ClearHitMarkers() end):SetIcon("icon16/star.png") + elseif obj.ClassName == "force" and not obj.Continuous then + menu:AddOption("(non-continuous only) force impulse", function() obj:OnShow() end):SetIcon("icon16/star.png") elseif obj.ClassName == "particles" then if obj.FireOnce then menu:AddOption("(FireOnce only) spew", function() obj:OnShow() end):SetIcon("icon16/star.png") @@ -2225,10 +3416,45 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) if string.find(obj.Expression, "timeex") or string.find(obj.Expression, "ezfade") then menu:AddOption("(timeex) reset clock", function() obj:OnHide() obj:OnShow() end):SetIcon("icon16/star.png") end + if not IsValid(obj.TargetPart) and obj.MultipleTargetParts == "" then + menu:AddOption("engrave / quick-link to parent", function() + if not obj.AffectChildrenOnly then + obj:SetTargetPart(obj:GetParent()) + elseif #obj:GetChildrenList() == 1 then + obj:SetTargetPart(obj:GetChildrenList()[1]) + end + + end):SetIcon("icon16/star.png") + end + if #pace.BulkSelectList > 0 then + menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set multiple target parts", function() + local uid_tbl = {} + for i,part in ipairs(pace.BulkSelectList) do + table.insert(uid_tbl, part.UniqueID) + end + obj:SetMultipleTargetParts(table.concat(uid_tbl,";")) + end):SetIcon("icon16/star.png") + end + elseif obj.ClassName == "beam" then + if not IsValid(obj.TargetPart) and obj.MultipleEndPoints == "" then + menu:AddOption("Link parent as end point", function() + obj:SetEndPoint(obj:GetParent()) + end):SetIcon("icon16/star.png") + end + if #pace.BulkSelectList > 0 then + menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set multiple end points", function() + local uid_tbl = {} + for i,part in ipairs(pace.BulkSelectList) do + if not part.GetWorldPosition then erroring = true else table.insert(uid_tbl, part.UniqueID) end + end + if erroring then pac.InfoPopup("Some selected parts were invalid endpoints as they are not base_movables", {pac_part = false, obj_type = "cursor", panel_exp_height = 100}) end + obj:SetMultipleEndPoints(table.concat(uid_tbl,";")) + end):SetIcon("icon16/star.png") + end elseif obj.ClassName == "shake" then menu:AddOption("activate (editor camera should be off)", function() obj:OnHide() obj:OnShow() end):SetIcon("icon16/star.png") elseif obj.ClassName == "event" then - if obj.Event == "command" then + if obj.Event == "command" and pac.LocalPlayer.pac_command_events then local cmd, time, hide = obj:GetParsedArgumentsForObject(obj.Events.command) if time == 0 then --toggling mode pac.LocalPlayer.pac_command_events[cmd] = pac.LocalPlayer.pac_command_events[cmd] or {name = cmd, time = pac.RealTime, on = 0} @@ -2238,13 +3464,27 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) else menu:AddOption("(command) toggle", function() RunConsoleCommand("pac_event", cmd, "1") end):SetIcon("icon16/star.png") end - + else menu:AddOption("(command) trigger", function() RunConsoleCommand("pac_event", cmd) end):SetIcon("icon16/star.png") end - + + end + if #pace.BulkSelectList > 0 then + menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set multiple target parts", function() + local uid_tbl = {} + for i,part in ipairs(pace.BulkSelectList) do + table.insert(uid_tbl, part.UniqueID) + end + obj:SetMultipleTargetParts(table.concat(uid_tbl,";")) + end):SetIcon("icon16/star.png") + end + if not IsValid(obj.DestinationPart) then + menu:AddOption("engrave / quick-link to parent", function() obj:SetTargetedPart(obj:GetParent()) end):SetIcon("icon16/star.png") end end + + pace.AddQuickSetupsToPartMenu(menu, obj) end function pace.addPartMenuComponent(menu, obj, option_name) @@ -2327,6 +3567,22 @@ function pace.addPartMenuComponent(menu, obj, option_name) MsgC(Color(200,200,200), " of total local parts)\n") end end) + elseif option_name == "arraying_menu" then + local arraying_menu, pnl = menu:AddSubMenu(L"arraying menu", function() pace.OpenArrayingMenu(obj) end) pnl:SetImage('icon16/table_multiple.png') + if obj.GetWorldPosition then + local icon = obj.pace_tree_node.ModelPath or obj.Icon + if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end + arraying_menu:AddOption(L"base:" .. obj:GetName(), function() pace.OpenArrayingMenu(obj) end):SetImage(icon) + end + if obj.Parent.GetWorldPosition then + local icon = obj.pace_tree_node.ModelPath or obj.Icon + if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end + arraying_menu:AddOption(L"base:" .. obj.Parent:GetName(), function() pace.OpenArrayingMenu(obj.Parent) end):SetImage(icon) + end + elseif option_name == "criteria_process" then + menu:AddOption("Process parts by criteria", function() pace.PromptProcessPartsByCriteria(obj) end):SetIcon("icon16/text_list_numbers.png") + elseif option_name == "bulk_morph" then + menu:AddOption("Morph Properties over bulk select", function() pace.BulkMorphProperty() end):SetIcon("icon16/chart_line.png") elseif option_name == "bulk_apply_properties" then local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end) bap_icon:SetImage("icon16/application_form.png") @@ -2379,23 +3635,40 @@ function pace.addPartMenuComponent(menu, obj, option_name) end end + local resetting_mode, resetpnl = bulk_menu:AddSubMenu("Clear selection after operation?") resetpnl:SetImage("icon16/table_delete.png") + local resetting_mode1 = resetting_mode:AddOption("Yes") resetting_mode1:SetIsCheckable(true) resetting_mode1:SetRadio(true) + local resetting_mode2 = resetting_mode:AddOption("No") resetting_mode2:SetIsCheckable(true) resetting_mode2:SetRadio(true) + if pace.BulkSelect_clear_after_operation == nil then pace.BulkSelect_clear_after_operation = true end + + function resetting_mode1.OnChecked(b) + pace.BulkSelect_clear_after_operation = true + end + function resetting_mode2.OnChecked(b) + pace.BulkSelect_clear_after_operation = false + end + if pace.BulkSelect_clear_after_operation then resetting_mode1:SetChecked(true) else resetting_mode2:SetChecked(true) end + bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function() pace.BulkCutPaste(obj) + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage("icon16/arrow_join.png") if not pace.ordered_operation_readystate then bulk_menu:AddOption(L"prepare Ordered Insert (please select parts in order beforehand)", function() pace.BulkCutPasteOrdered() + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage("icon16/text_list_numbers.png") else bulk_menu:AddOption(L"do Ordered Insert (select destinations in order)", function() pace.BulkCutPasteOrdered() + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage("icon16/arrow_switch.png") end bulk_menu:AddOption(L"Copy to Bulk Clipboard", function() pace.BulkCopy(obj) + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage(pace.MiscIcons.copy) bulk_menu:AddSpacer() @@ -2403,19 +3676,23 @@ function pace.addPartMenuComponent(menu, obj, option_name) --bulk paste modes bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function() pace.BulkPasteFromBulkSelectToSinglePart(obj) + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage("icon16/arrow_join.png") bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function() if not pace.Clipboard then pace.Copy(obj) end pace.BulkPasteFromSingleClipboard() + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage("icon16/arrow_divide.png") bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function() pace.BulkPasteFromBulkClipboard(obj) + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage("icon16/arrow_join.png") bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function() pace.BulkPasteFromBulkClipboardToBulkSelect() + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage("icon16/arrow_divide.png") bulk_menu:AddSpacer() @@ -2425,12 +3702,14 @@ function pace.addPartMenuComponent(menu, obj, option_name) for _,v in ipairs(pace.BulkSelectList) do pace.PasteProperties(v) end + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage(pace.MiscIcons.replace) bulk_menu:AddOption(L"Bulk paste properties from clipboard", function() for _,v in ipairs(pace.BulkSelectList) do pace.PasteProperties(v) end + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage(pace.MiscIcons.replace) bulk_menu:AddOption(L"Deploy a numbered command event series ("..#pace.BulkSelectList..")", function() @@ -2442,18 +3721,48 @@ function pace.addPartMenuComponent(menu, obj, option_name) part.Event = "command" part.Arguments = str..i.."@@0@@0" end + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end) end):SetImage("icon16/clock.png") + bulk_menu:AddOption(L"Pack into a new group", function() + local root = pac.CreatePart("group") + root:SetParent(obj:GetParent()) + for i,v in ipairs(pace.BulkSelectList) do + v:SetParent(root) + end + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end + end):SetImage("icon16/world.png") bulk_menu:AddOption(L"Pack into a new root group", function() - root = pac.CreatePart("group") + local root = pac.CreatePart("group") for i,v in ipairs(pace.BulkSelectList) do v:SetParent(root) end + if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end end):SetImage("icon16/world.png") bulk_menu:AddSpacer() + bulk_menu:AddOption(L"Morph properties over bulk select", function() + pace.BulkMorphProperty() + end):SetImage("icon16/chart_line_edit.png") + + bulk_menu:AddOption(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end):SetImage('icon16/table_multiple.png') + + local arraying_menu, pnl = bulk_menu:AddSubMenu(L"arraying menu", function() pace.OpenArrayingMenu(obj) end) pnl:SetImage('icon16/table_multiple.png') + if obj and obj.GetWorldPosition then + local icon = obj.pace_tree_node.ModelPath or obj.Icon + if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end + arraying_menu:AddOption(L"base:" .. tostring(obj), function() pace.OpenArrayingMenu(obj) end):SetImage(icon) + end + if obj and obj.Parent.GetWorldPosition then + local icon = obj.pace_tree_node.ModelPath or obj.Icon + if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end + arraying_menu:AddOption(L"base:" .. tostring(obj.Parent), function() pace.OpenArrayingMenu(obj.Parent) end):SetImage(icon) + end + + bulk_menu:AddSpacer() + bulk_menu:AddOption(L"Bulk Delete", function() pace.BulkRemovePart() end):SetImage(pace.MiscIcons.clear) @@ -2479,12 +3788,14 @@ function pace.addPartMenuComponent(menu, obj, option_name) local menu2, pnl = menu:AddSubMenu(L"Copy part UniqueID", function() pace.CopyUID(obj) end) pnl:SetIcon(pace.MiscIcons.uniqueid) elseif option_name == "help_part_info" and obj then - menu:AddOption(L"View help or info about this part", function() pac.AttachInfoPopupToPart(obj, nil, { - obj_type = GetConVar("pac_popups_preferred_location"):GetString(), - hoverfunc = "open", - pac_part = pace.current_part, - panel_exp_width = 900, panel_exp_height = 400 - }) end):SetImage("icon16/information.png") + local pnl = menu:AddOption(L"View specific help or info about this part", function() + pac.AttachInfoPopupToPart(obj, nil, { + obj_type = GetConVar("pac_popups_preferred_location"):GetString(), + hoverfunc = "open", + pac_part = pace.current_part, + panel_exp_width = 900, panel_exp_height = 400 + }) + end) pnl:SetImage("icon16/information.png") pnl:SetTooltip("for some classes it'll be the same as hitting F1, giving you the basic class tutorial, but for proxies and events they will be more specific") elseif option_name == "reorder_movables" and obj then if (obj.Position and obj.Angles and obj.PositionOffset) then local substitute, pnl = menu:AddSubMenu("Reorder / replace base movable") @@ -2506,7 +3817,7 @@ end function pace.UltraCleanup(obj) if not obj then return end - local root = obj:GetRootPart() + local root = obj local safe_parts = {} local parts_have_saved_parts = {} local marked_for_deletion = {} @@ -2520,7 +3831,6 @@ function pace.UltraCleanup(obj) local function FoundImportantMarkedParent(part) if not IsValid(part) then return false end if IsImportantMarked(part) then return true end - local root = part:GetRootPart() local parent = part while parent ~= root do if parent.Notes and parent.Notes == "important" then return true end @@ -2794,6 +4104,150 @@ function pace.UltraCleanup(obj) end +--match parts then replace properties or do other stuff like deleting +function pace.ProcessPartsByCriteria(raw_args) + + local match_criteria_tbl = {} + local process_actions = {} + local function match_criteria(part) + for i,v in ipairs(match_criteria_tbl) do + if v[2] == "=" then + if part[v[1]] ~= v[3] then + return false + end + elseif v[2] == ">" then + if part[v[1]] <= v[3] then + return false + end + elseif v[2] == ">=" then + if part[v[1]] < v[3] then + return false + end + elseif v[2] == "<" then + if part[v[1]] >= v[3] then + return false + end + elseif v[2] == "<=" then + if part[v[1]] > v[3] then + return false + end + else --bad operator + return false + end + end + return true + end + local function process(part) + print(part, "ready for processing") + for i,v in ipairs(process_actions) do + local action = v[1] + local key = v[2] + local value = v[3] + if action == "DELETE" then part:Remove() return end + if action == "REPLACE" then + if part["Set"..key] then + local type = type(part["Get"..key](part)) + if type == "string" then + part["Set"..key](part,value) + elseif type == "Vector" then + local tbl = string.Split(value, " ") + if tbl[3] then + local vec = Vector(tonumber(tbl[1]),tonumber(tbl[2]),tonumber(tbl[3])) + part["Set"..key](part,vec) + end + elseif type == "Angle" then + local tbl = string.Split(value, " ") + if tbl[3] then + local ang = Angle(tonumber(tbl[1]),tonumber(tbl[2]),tonumber(tbl[3])) + part["Set"..key](part,ang) + end + elseif type == "number" then + part["Set"..key](part,tonumber(value)) + elseif type == "boolean" then + part["Set"..key](part,tobool(value)) + end + end + end + end + end + + if isstring(raw_args) then + local reading_criteria = false + local reading_processing = false + local process + for i,line in ipairs(string.Split(raw_args, "\n")) do + local line_tbl = string.Split(line, "=") + if string.sub(line,1,8) == "CRITERIA" then + reading_criteria = true + elseif string.sub(line, 1,7) == "REPLACE" then + process = "REPLACE" + reading_criteria = false + reading_processing = true + elseif string.sub(line, 1,6) == "DELETE" then + process = "DELETE" + reading_criteria = false + reading_processing = true + elseif line ~= "" then + if reading_criteria then + table.insert(match_criteria_tbl, {line_tbl[1], "=", line_tbl[2]}) + elseif reading_processing then + if process ~= nil then + table.insert(process_actions, {process, line_tbl[1], line_tbl[2] or ""}) + end + end + end + end + elseif istable(raw_args) then + match_criteria_tbl = raw_args[1] + process_actions = raw_args[2] + else + return + end + pac.Message("PROCESS BY CRITERIA") + pac.Message("====================CRITERIA====================") + PrintTable(match_criteria_tbl) + print("\n") + pac.Message("====================PROCESSING====================") + PrintTable(process_actions) + pace.processing = true + for _,part in pairs(pac.GetLocalParts()) do + if match_criteria(part) then + process(part) + end + end + pace.processing = false +end + +function pace.PromptProcessPartsByCriteria(part) + local default_args = "" + local default_class = "" + if part then + default_class = part.ClassName + if part.ClassName == "event" then + default_args = default_args .. "CRITERIA" + default_args = default_args .. "\nClassName=event" + default_args = default_args .. "\nArguments="..part:GetArguments() + default_args = default_args .. "\nEvent="..part:GetEvent() + default_args = default_args .. "\n\nREPLACE" + default_args = default_args .. "\nEvent=" + default_args = default_args .. "\nArguments=" + else + default_args = default_args .. "CRITERIA" + default_args = default_args .. "\nClassName=" .. default_class + default_args = default_args .. "\nKey=Value" + default_args = default_args .. "\n\nREPLACE" + default_args = default_args .. "\nKey=NewValue" + end + else + default_args = default_args .. "CRITERIA" + default_args = default_args .. "\nClassName=class" + default_args = default_args .. "\nKey=Value" + default_args = default_args .. "\n\nREPLACE" + default_args = default_args .. "\nKey=NewValue" + end + pace.MultilineStringRequest("Process by criteria", "enter arguments", default_args, function(str) pace.ProcessPartsByCriteria(str) end) +end + do --hover highlight halo pac.haloex = include("pac3/libraries/haloex.lua") @@ -2870,6 +4324,469 @@ do --hover highlight halo end +pace.arraying = false +local last_clone = nil +local axis_choice = "x" +local axis_choice_id = 1 +local mode_choice = "Circle" +local subdivisions = 1 +local length = 50 +local height = 50 +local offset = 0 +local save_settings = false +local angle_follow = false + +function pace.OpenArrayingMenu(obj) + local locked_matrix_part = obj or pace.current_part + if locked_matrix_part.GetWorldPosition == nil then pace.FlashNotification("Please select a movable part before using the arraying menu") return end + + local pos, ang = pace.mctrl.GetWorldPosition() + local mctrl = pos:ToScreen() + mctrl.x = mctrl.x + 100 + + local main_panel = vgui.Create("DFrame") main_panel:SetSize(600,400) main_panel:SetPos(mctrl.x + 100, mctrl.y - 200) main_panel:SetSizable(true) + + main_panel:SetTitle("Arraying Menu - Please select an arrayed part contained inside " .. tostring(locked_matrix_part)) + local properties_pnl = pace.CreatePanel("properties", main_panel) properties_pnl:SetSize(580,360) properties_pnl:SetPos(10,30) + + properties_pnl:AddCollapser("Parts") + local matrix_part_selector = pace.CreatePanel("properties_part") + matrix_part_selector.part = locked_matrix_part + properties_pnl:AddKeyValue("Matrix",matrix_part_selector) + matrix_part_selector:SetValue(locked_matrix_part.UniqueID) + matrix_part_selector:PostInit() + local arraying_part_selector = pace.CreatePanel("properties_part") + properties_pnl:AddKeyValue("ArrayedPart",arraying_part_selector) + arraying_part_selector:PostInit() + + properties_pnl:AddCollapser("Dimensions") + local height_slider = pace.CreatePanel("properties_number") + properties_pnl:AddKeyValue("Height",height_slider) + local length_slider = pace.CreatePanel("properties_number") + properties_pnl:AddKeyValue("Length",length_slider) + local array_modes = vgui.Create("DComboBox") + array_modes:AddChoice("Circle", "Circle", true, "icon16/cd.png") + array_modes:AddChoice("Rectangle", "Rectangle", false, "icon16/collision_on.png") + array_modes:AddChoice("Line", "Line", false, "icon16/chart_line.png") + properties_pnl:AddKeyValue("Mode",array_modes) + function array_modes:OnSelect(index, val, data) mode_choice = data end + local axes = vgui.Create("DComboBox") + axes:AddChoice("x", "x", true) + axes:AddChoice("y", "y", false) + axes:AddChoice("z", "z", false) + properties_pnl:AddKeyValue("Axis",axes) + function axes:OnSelect(index, val, data) axis_choice = data axis_choice_id = index end + + properties_pnl:AddCollapser("Utilities") + local subdivs_slider = pace.CreatePanel("properties_number") + properties_pnl:AddKeyValue("Count",subdivs_slider) + local offset_slider = pace.CreatePanel("properties_number") + properties_pnl:AddKeyValue("Offset",offset_slider) + local anglefollow = pace.CreatePanel("properties_boolean") + properties_pnl:AddKeyValue("AlignToShape",anglefollow) + anglefollow:SetTooltip("Sets the Angles field in accordance to the shape. If you want to offset from that, use AngleOffset") + anglefollow:SetValue(false) + local savesettings = pace.CreatePanel("properties_boolean") + properties_pnl:AddKeyValue("SaveSettings",savesettings) + savesettings:SetTooltip("Preserves your settings if you close the window") + function savesettings.chck:OnChange(b) save_settings = b end + savesettings:SetValue(save_settings) + local force_update = vgui.Create("DButton") + force_update:SetText("Refresh") + force_update:SetTooltip("Updates clones (paste properties from the first part)") + properties_pnl:AddKeyValue("ForceUpdate",force_update) + + if save_settings then + axes:ChooseOption(axis_choice, axis_choice_id) + anglefollow:SetValue(angle_follow) + subdivs_slider:SetValue(subdivisions) + offset_slider:SetValue(offset) + length_slider:SetValue(length) + height_slider:SetValue(height) + if last_clone then + arraying_part_selector:SetValue(last_clone.UniqueID) + arraying_part_selector:PostInit() + end + else + axes:ChooseOption("x",1) + anglefollow:SetValue(false) + subdivs_slider:SetValue(1) + offset_slider:SetValue(0) + length_slider:SetValue(50) + height_slider:SetValue(50) + end + + local clone_positions = {} + local clones = {} + do + local toremove = {} + for i,v in ipairs(locked_matrix_part:GetChildren()) do + if v.Notes == "original array instance" then + last_clone = v + elseif v.Notes == "arrayed copy" then + table.insert(toremove, v) + end + end + if last_clone and save_settings then + arraying_part_selector:SetValue(last_clone.UniqueID) + end + for i,v in ipairs(toremove) do v:Remove() end + end + + local clone_original = last_clone or arraying_part_selector.part + + function main_panel:OnClose() pac.RemoveHook("PostDrawTranslucentRenderables", "ArrayingVisualize") pace.arraying = false end + + local function get_basis(axis) + + end + + local function get_shape_angle(tbl, i) + if mode_choice == "Circle" then + return tbl.basis_angle * (tbl.index - 1) + tbl.offset_angle + elseif mode_choice == "Rectangle" then + return tbl.basis_angle + elseif mode_choice == "Line" then + if axis_choice == "x" then + if length >= 0 then return Angle(0,0,0) else return Angle(180,0,0) end + elseif axis_choice == "y" then + if length >= 0 then return tbl.basis_angle else return -tbl.basis_angle end + elseif axis_choice == "z" then + if length >= 0 then return tbl.basis_angle else return -tbl.basis_angle end + end + end + end + + local function update_clones(recreate_parts) + for i,v in pairs(clones) do + if i > #clone_positions then + v:Remove() + clones[i] = nil + end + end + + if arraying_part_selector:GetValue() == "" then print("empty boys") return end + clone_original = arraying_part_selector.part --pac.GetPartFromUniqueID(pac.Hash(LocalPlayer()), :DecodeEdit(arraying_part_selector:GetValue())) + last_clone = clone_original + local warning = false + if not clone_original then return end + + if clone_original:HasChild(locked_matrix_part) or clone_original == locked_matrix_part then --avoid bad case of recursion + warning = true + end + for i,v in ipairs(clone_positions) do + if i ~= 1 then + local clone = clones[i] + if not clone then + --if recreate_parts and not warning then + clone = clone_original:Clone() + clone.Notes = "arrayed copy" + local name = "" .. i + if math.floor(math.log10(i)) == 0 then + name = "00" .. name + elseif math.floor(math.log10(i)) == 1 then + name = "0" .. name + end + clone.Name = "[" .. name .. "]" + clones[i] = clone + --end + end + clone:SetPosition(v.vec) + if anglefollow.chck:GetChecked() then + clone:SetAngleOffset(clone_original:GetAngleOffset()) + clone:SetAngles(get_shape_angle(v, i-1)) + --clone:SetAngles((i-1) * v.basis_angle + v.offset_angle) + end + else + if string.sub(clone_original:GetName(),1,5) ~= "[001]" then clone_original:SetName("[001]" .. clone_original:GetName()) end + clone_original:SetPosition(v.vec) + if anglefollow.chck:GetChecked() then + + clone_original:SetAngles(get_shape_angle(v, i)) + end + end + end + end + + --that's a nice preview but what about local positions + local last_offset = 0 + local function draw_circle(pos, basis_normal, basis_x, basis_y, length, height, subdivs) + --[[render.DrawLine(pos, pos + 50*basis_normal, Color(255,255,255), false) + render.DrawLine(pos, pos + 50*basis_x, Color(255,0,0), false) + render.DrawLine(pos, pos + 50*basis_y, Color(0,255,0), false)]] + clone_positions = {} + local radiansubdiv = 2*math.pi / subdivs + for i=0,subdivs,1 do + local pos1 = pos + math.sin(i*radiansubdiv)*basis_y*height + math.cos(i*radiansubdiv)*basis_x*length + local pos2 = pos + math.sin((i+1)*radiansubdiv)*basis_y*height + math.cos((i+1)*radiansubdiv)*basis_x*length + render.DrawLine(pos1,pos2,Color(255,255,200 + 50*math.sin(CurTime()*10)),true) + end + radiansubdiv = 2*math.pi / (subdivisions) + local matrix_pos, matrix_ang = locked_matrix_part:GetDrawPosition() + matrix_ang = matrix_ang -- locked_matrix_part.Angles + render.DrawLine(matrix_pos, matrix_pos + 50*matrix_ang:Forward(), Color(255,0,0), false) + render.DrawLine(matrix_pos, matrix_pos + 50*matrix_ang:Right(), Color(0,255,0), false) + for i=0,subdivisions,1 do + local degrees = offset + 360*i*radiansubdiv/(math.pi * 2) + local degrees2 = offset + 360*(i+1)*radiansubdiv/(math.pi * 2) + local radians = (degrees/360)*math.pi*2 + local radians2 = (degrees2/360)*math.pi*2 + if i == subdivisions then break end --don't make overlapping one + local ellipse_x = math.cos(radians)*length + local ellipse_y = math.sin(radians)*height + local pos1 = pos + math.sin(radians)*basis_y*height + math.cos(radians)*basis_x*length + local pos2 = pos + math.sin(radians2)*basis_y*height + math.cos(radians2)*basis_x*length + + local the_original = false + if i == 0 then the_original = true end + + local localpos, localang = WorldToLocal( pos1, ang, pos, matrix_ang ) + + local basis_angle = Angle() + local offset_angle = Angle() + if axis_choice == "y" then + basis_angle = Angle(-1,0,0) * (360 / subdivisions) + offset_angle = Angle(-offset,0,0) + elseif axis_choice == "z" then + basis_angle = Angle(0,-1,0) * (360 / subdivisions) + offset_angle = Angle(0,-offset,0) + elseif axis_choice == "x" then + basis_angle = Angle(0,0,1) * (360 / subdivisions) + offset_angle = Angle(0,0,offset) + end + table.insert(clone_positions, i+1, {wpos = pos1, wang = ang, vec = localpos, ang = localang, basis_angle = basis_angle, offset_angle = offset_angle, is_the_original = the_original, index = i+1, x = ellipse_x, y = ellipse_y, degrees = degrees}) + end + if last_offset ~= offset_slider:GetValue() then last_offset = offset_slider:GetValue() update_clones() end + end + local function draw_rectangle(pos, basis_normal, basis_x, basis_y, length, height) + render.DrawLine(pos, pos + 50*basis_normal, Color(255,255,255), false) + render.DrawLine(pos, pos + 50*basis_x, Color(255,0,0), false) + render.DrawLine(pos, pos + 50*basis_y, Color(0,255,0), false) + clone_positions = {} + + local x = basis_x*length + local y = basis_y*height + render.DrawLine(pos + x - y,pos + x + y,Color(255,255,200 + 50*math.sin(CurTime()*10)),true) + render.DrawLine(pos + x + y,pos - x + y,Color(255,255,200 + 50*math.sin(CurTime()*10)),true) + render.DrawLine(pos - x + y,pos - x - y,Color(255,255,200 + 50*math.sin(CurTime()*10)),true) + render.DrawLine(pos - x - y,pos + x - y,Color(255,255,200 + 50*math.sin(CurTime()*10)),true) + + local matrix_pos, matrix_ang = locked_matrix_part:GetBonePosition() + for i=0,subdivisions,1 do + local frac = (offset/360 + (i-1)/subdivisions) % 1 + local x + local y + local basis_ang_value = 0 + + if (frac >= 0.875) or (frac < 0.125) then --right side + x = basis_x*length + basis_ang_value = 0 + if frac >= 0.875 then + y = 8*(frac-1)*basis_y*height + elseif frac < 0.125 then + y = 8*frac*basis_y*height + end + elseif (frac >= 0.125) and (frac < 0.375) then --up side + y = basis_y*height + basis_ang_value = 90 + if frac < 0.25 then + x = 8*(-frac+0.25)*basis_x*length + elseif frac >= 0.25 then + x = 8*(-frac+0.25)*basis_x*length + end + elseif (frac >= 0.375) and (frac < 0.625) then --left side + x = -basis_x*length + basis_ang_value = 180 + if frac < 0.5 then + y = 8*(-frac+0.5)*basis_y*height + elseif frac >= 0.5 then + y = 8*(-frac+0.5)*basis_y*height + end + elseif frac >= 0.625 then --down side + y = -basis_y*height + basis_ang_value = -90 + if frac < 0.75 then + x = 8*(frac-0.75)*basis_x*length + elseif frac >= 0.75 then + x = 8*(frac-0.75)*basis_x*length + end + end + if i == subdivisions then break end --don't make overlapping one + local pos1 = pos + x + y + + local the_original = false + if i == 0 then the_original = true end + + local localpos, localang = WorldToLocal( pos1, matrix_ang, pos, ang ) + + local basis_angle = Angle() + local offset_angle = Angle() + if axis_choice == "x" then + basis_angle = Angle(0,0,1)*basis_ang_value + offset_angle = Angle(0,0,0) + elseif axis_choice == "y" then + basis_angle = Angle(-1,0,0)*basis_ang_value + offset_angle = Angle(0,0,0) + elseif axis_choice == "z" then + basis_angle = Angle(0,1,0)*basis_ang_value + offset_angle = Angle(0,0,0) + end + table.insert(clone_positions, i+1, {frac = frac, wpos = pos1, wang = ang, vec = localpos, ang = localang, basis_angle = basis_angle, offset_angle = offset_angle, is_the_original = the_original, index = i+1}) + + end + if last_offset ~= offset_slider:GetValue() then last_offset = offset_slider:GetValue() update_clones() end + end + local function draw_line(pos, basis_normal, length) + clone_positions = {} + + render.DrawLine(pos, pos + basis_normal*length,Color(255,255,200 + 50*math.sin(CurTime()*10)),true) + + for i=0,subdivisions,1 do + local forward = offset + (length*i)/subdivisions + local pos1 = pos + forward*basis_normal + + local the_original = false + if i == 0 then the_original = true end + + local localpos + local localang = Angle(0,0,0) + local basis_angle = Angle(0,0,0) + local offset_angle = Angle(0,0,0) + if axis_choice == "x" then + localpos = Vector(1,0,0)*forward + basis_angle = Angle(0,0,0) + elseif axis_choice == "y" then + localpos = Vector(0,1,0)*forward + basis_angle = Angle(0,90,0) + elseif axis_choice == "z" then + localpos = Vector(0,0,1)*forward + basis_angle = Angle(-90,0,0) + end + + + table.insert(clone_positions, i+1, {wpos = pos1, wang = ang, vec = localpos, ang = localang, basis_angle = basis_angle, offset_angle = offset_angle, is_the_original = the_original, index = i+1}) + end + if last_offset ~= offset_slider:GetValue() then last_offset = offset_slider:GetValue() update_clones() end + end + + --oof this one's gonna be rough how do we even do this + local function draw_clones() + update_clones(false) + for i,v in pairs(clone_positions) do + render.DrawLine(v.wpos, v.wpos + 10*v.wang:Forward(),Color(255,0,0),true) + render.DrawLine(v.wpos, v.wpos - 10*v.wang:Right(),Color(0,255,0),true) + render.DrawLine(v.wpos, v.wpos + 10*v.wang:Up(),Color(0,0,255),true) + if length < 10 or height < 10 then return end + if i == 1 then + render.SetMaterial(Material("sprites/grip_hover.vmt")) render.DrawSprite( v.wpos, 5, 5, Color( 255, 255, 255) ) + else + render.SetMaterial(Material("sprites/grip.vmt")) render.DrawSprite( v.wpos, 3, 3, Color( 255, 255, 255) ) + end + end + end + + function subdivs_slider.OnValueChanged(val) + subdivisions = math.floor(val) + update_clones(true) + subdivs_slider:SetValue(math.floor(val)) + end + function anglefollow.OnValueChanged(b) + angle_follow = b + anglefollow:SetValue(b) + end + function length_slider.OnValueChanged(val) + length = val + length_slider:SetValue(val) + end + function height_slider.OnValueChanged(val) + height = val + height_slider:SetValue(val) + end + function offset_slider.OnValueChanged(val) + offset = val + offset_slider:SetValue(val) + end + function force_update.DoClick() + local skip_properties = { + ["Position"] = true, + ["Angles"] = true, + ["Name"] = true, + ["Notes"] = true, + } + local originalpart = arraying_part_selector.part + local properties = originalpart:GetProperties() + for i,tbl in ipairs(properties) do + if skip_properties[tbl.key] then continue end + local val = originalpart["Get"..tbl.key](originalpart) + for _,part in pairs(clones) do + part["Set"..tbl.key](part, val) + end + end + + end + + if pace.arraying then pac.RemoveHook("PostDrawTranslucentRenderables", "ArrayingVisualize") pace.arraying = false return end + + timer.Simple(0.3, function() + pac.AddHook("PostDrawTranslucentRenderables", "ArrayingVisualize", function() + matrix_part_selector.part = pac.GetPartFromUniqueID(pac.Hash(LocalPlayer()), matrix_part_selector:DecodeEdit(matrix_part_selector:GetValue())) + locked_matrix_part = matrix_part_selector.part + pace.mctrl.SetTarget(locked_matrix_part) + if arraying_part_selector:GetValue() then + if arraying_part_selector:GetValue() ~= locked_matrix_part.UniqueID then + arraying_part_selector.part = pac.GetPartFromUniqueID(pac.Hash(LocalPlayer()), arraying_part_selector:GetValue()) + arraying_part_selector:SetValue(arraying_part_selector.part.UniqueID) + end + elseif pace.current_part ~= locked_matrix_part or ((arraying_part_selector.part ~= nil) and (arraying_part_selector.part ~= locked_matrix_part)) then + arraying_part_selector.part = pace.current_part + arraying_part_selector:SetValue(arraying_part_selector.part.UniqueID) + end + + subdivisions = subdivs_slider:GetValue() + length = length_slider:GetValue() + height = height_slider:GetValue() + offset = offset_slider:GetValue() + + --it's possible the part gets deleted + if not locked_matrix_part.GetDrawPosition then main_panel:Remove() pac.RemoveHook("PostDrawTranslucentRenderables", "ArrayingVisualize") return end + local pos, ang = locked_matrix_part:GetDrawPosition() + if not pos or not ang then return end + local forward, right, up = pace.mctrl.GetAxes(ang) + + local basis_x, basis_y, basis_normal + if axis_choice == "x" then + basis_x = right + basis_y = up + basis_normal = forward + elseif axis_choice == "y" then + basis_x = forward + basis_y = up + basis_normal = right + elseif axis_choice == "z" then + basis_x = right + basis_y = forward + basis_normal = up + else + basis_x, basis_y, basis_normal = pace.mctrl.GetAxes(ang) + end + + if not locked_matrix_part.GetWorldPosition then print("early exit 3") return end + if mode_choice == "Circle" then + draw_circle(pos, basis_normal, basis_x, basis_y, length_slider:GetValue(), height_slider:GetValue(), 40) + elseif mode_choice == "Rectangle" then + draw_rectangle(pos, basis_normal, basis_x, basis_y, length_slider:GetValue(), height_slider:GetValue()) + elseif mode_choice == "Line" then + draw_line(pos, basis_normal, length_slider:GetValue()) + end + draw_clones() + end) + end) + + pace.arraying = true + + +end + --custom info panel --[[args tbl = { diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index 2bca37b50..4b58b66a6 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -5,16 +5,53 @@ Tutorials will be written here ]] - -CreateConVar("pac_popups_enable", 1, FCVAR_ARCHIVE, "Enables PAC editor popups. They provide some information but can be annoying") +local default_fonts = { + "BudgetLabel", + "CenterPrintText", + "ChatFont", + "CloseCaption_Bold", + "CloseCaption_BoldItalic", + "CloseCaption_Italic", + "CloseCaption_Normal", + "CreditsOutroText", + "CreditsText", + "DebugFixed", + "DebugFixedSmall", + "DebugOverlay", + "Default", + "DefaultFixed", + "DefaultFixedDropShadow", + "DefaultSmall", + "DefaultUnderline", + "DefaultVerySmall", + "HDRDemoText", + "HudDefault", + "HudHintTextLarge", + "HudHintTextSmall", + "HudSelectionText", + "TargetID", + "TargetIDSmall", + "Trebuchet18", + "Trebuchet24", + "DermaDefault", + "DermaDefaultBold", + "DermaLarge", + "GModNotify", + "ScoreboardDefault", + "ScoreboardDefaultTitle", + +} + +CreateConVar("pac_popups_enable", 1, FCVAR_ARCHIVE, "Enables PAC editor popups. They provide some information but can be annoying if you use autopilot options that make them automatically on certain contexts.") +CreateConVar("pac_popups_font", "DermaDefaultBold", FCVAR_ARCHIVE, "PAC editor popups font") CreateConVar("pac_popups_preserve_on_autofade", 1, FCVAR_ARCHIVE, "If set to 0, PAC editor popups appear only once and don't reappear when hovering over the part label or pressing F1") -CreateConVar("pac_popups_base_color", "215 230 255", FCVAR_ARCHIVE, "The color of the base filler rectangle for editor popups") +CreateConVar("pac_popups_base_color", "255 255 255", FCVAR_ARCHIVE, "The color of the base filler rectangle for editor popups") CreateConVar("pac_popups_base_color_pulse", "0", FCVAR_ARCHIVE, "Amount of pulse of the base filler rectangle for editor popups") -CreateConVar("pac_popups_base_alpha", "0.5", FCVAR_ARCHIVE, "The alpha opacity of the base filler rectangle for editor popups") +CreateConVar("pac_popups_base_alpha", "255", FCVAR_ARCHIVE, "The alpha opacity of the base filler rectangle for editor popups") CreateConVar("pac_popups_fade_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") -CreateConVar("pac_popups_fade_alpha", "1", FCVAR_ARCHIVE, "The alpha opacity of the fading effect for editor popups") -CreateConVar("pac_popups_text_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") +CreateConVar("pac_popups_fade_alpha", "0", FCVAR_ARCHIVE, "The alpha opacity of the fading effect for editor popups") +CreateConVar("pac_popups_text_color", "40 40 40", FCVAR_ARCHIVE, "The color of the fading effect for editor popups") CreateConVar("pac_popups_verbosity", "beginner tutorial", FCVAR_ARCHIVE, "Sets the amount of information added to PAC editor popups. While in development, there will be limited contextual support. If no special information is defined, it will indicate the part size information. Here are the planned modes: \nbeginner tutorial : Basic tutorials about pac parts, for beginners or casual users looking for a quick reference for what a part does\nReference tutorial : doesn't give part tutorials, but still keeps events' tutorial explanations.\n") CreateConVar("pac_popups_preferred_location", "pac tree label", FCVAR_ARCHIVE, "Sets the preferred method of PAC editor popups.\n".. "pac tree label : the part label on the pac tree\n".. @@ -79,6 +116,20 @@ function pace.OpenPopupConfig() GetConVar("pac_popups_text_color"):SetString(col.r .. " " .. col.g .. " " .. col.b) end + pace.popups_font = GetConVar("pac_popups_font"):GetString() + local font = vgui.Create("DComboBox", master_pnl) + font:SetSize(200, 20) + font:SetPos(200,26) + font:SetText("font") + for _,f in ipairs(default_fonts) do + font:AddChoice(f,f) + end + function font:ChooseOption(val,id) + self:SetText(val) + GetConVar("pac_popups_font"):SetString(val) + pace.popups_font = val + end + local invertcolor_btn = vgui.Create("DButton") invertcolor_btn:SetSize(400,30) invertcolor_btn:SetText("Use text invert color (experimental)") @@ -132,7 +183,7 @@ function pace.OpenPopupConfig() fade = math.pow(fade,2) draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2)) end - draw.DrawText(label_text, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,255)) + draw.DrawText(label_text, pace.popups_font, 5, 5, Color(r3,g3,b3,255)) end list_pnl:Add(Label("Base color")) @@ -227,7 +278,7 @@ function pac.InfoPopup(str, tbl, x, y) if tbl.pac_part then if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then if pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName] then - str = str .. "\n\n" .. pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName].popup_tutorial .. "\n" + str = str .. "\n\n====================================================================\n\nPart Class Tutorial for " .. tbl.pac_part.ClassName .. "\n" .. pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName].popup_tutorial .. "\n" end end end @@ -485,8 +536,8 @@ function pac.InfoPopup(str, tbl, x, y) local col = Color(r3,g3,b3,255) - --txt_zone:SetFont("DermaDefaultBold") function txt_zone:PerformLayout() + self:SetFontInternal(pace.popups_font or "DermaDefaultBold") txt_zone:SetBGColor(0,0,0,0) txt_zone:SetFGColor(col) end @@ -522,9 +573,9 @@ function pac.InfoPopup(str, tbl, x, y) end if self.expand then - draw.DrawText(self.alternativetitle, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) + draw.DrawText(self.alternativetitle, pace.popups_font, 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) else - draw.DrawText(self.titletext, "DermaDefaultBold", 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) + draw.DrawText(self.titletext, pace.popups_font, 5, 5, Color(r3,g3,b3,self.fade_factor * 255)) end end @@ -1049,46 +1100,117 @@ do ["interpolated_multibone"] = { tooltip = "morphs position between nodes", popup_tutorial = - "A node-based path/morpher. This part allows you to move its contents by blending positions and angles between different points. Obviously enough, the nodes you select need to be base_movable parts.\n".. - "The first (Zeroth) node is the interpolated_multibone itself. From then on, the next node is reached when lerp reaches the corresponding number, and when you're at the end, i.e. an invalid or missing node, it morphs back to the origin.\n".. - "For example, 0.5 lerp will be halfway between the first node and the origin.\n".. - "While this part finally breaks through one of pac3's fundamental limitations (that of base_movables being limited to specific bones as anchoring points), there are still known issues, namely because of how angles are morphed. Roll angles might break.\n\n".. - "Suggested use cases: multi-position cutscene camera, returning hitpos pseudo-projectile, joints." +[[This part repositions its contents according to a path with multiple nodes. It blends the position and angle by mixing those of the current node with those of the next node. +Obviously enough, the nodes you select need to be base_movable parts. + + +As you may know, like with jiggles, it's like making a container inside a part, the action happens when the container moves in a special way. +Jiggles and interpolators' "containers" are dynamic. Unlike models', which are just located on the parent model itself. +Jiggles would be as if the container is attached with springs all around, that's how it jiggles around. +The interpolator would be as if the container is a cart on a rail, going to different places. + + +The "first" / prime / home / origin (Zeroth) node is the interpolator itself. When lerp value is 0, this is where the container will be. +It's useful to start near zero if you want to create interpolators dynamically, as with projectiles. Or to simply have a reference point. + +The first outside node, part1, will be used if lerp value is more than 0. At 1 the container will be located at part1. at 0.5, it would be halfway between home and part1. +The second outside node, part2, will be used if lerp value is more than 1. At 2 the container will be located at part2. at 1.5, it would be halfway between part1 and part2. +and so on. + + +In terms of how it works as a base_movable, it's decent enough but you should still be aware of some things, perhaps as a reminder. +If you want to adjust the thing inside, think about where it'll end up. Adjustments can be made on the prime node, the subsequent nodes, the contained parts themselves or even on the path itself. + +If the angle or position to adjust depends on only one node, it's probably a good idea to make your adjustment on the node in question. As long as your offsets on the contained parts aren't too strong, its angle should be representative enough. +Why this matters is that you may think it looks good now, but if you offset too much on the contained parts, the offsets will still be there on the other nodes. + +Also, with an interpolation very close to one node, try not to overcorrect. If it takes 90 degrees to tweak the appearance just a little bit, maybe it's a good idea to think about the path itself instead of the node. +If you do this haphazardly, it will mess up other interpolators or parts that rely on that node. +Maybe your knee should've been more in 0.5 ranges instead of pushing excessive angles on the other end that get reduced by the fact the lerp value is like 0.2. +Any adjustment you make is only gonna show up as much as the morphing progress allows it. + +Now for most users, you can just keep playing around with it and find out that maybe you'd prefer adjusting the nodes instead, or instead tweaking the way your lerp value is set up. Or that a slight offset is fine once it shows on all nodes. + + +There may be some issues with how the angles are calculated, and there's the occasional issue with base_movable lagging but you can hack some temporary fixes by checking translucent on some things. + + +Suggested uses for this part: +camera with multiple positions for a cutscene +joints : like a kneepad or some other articulated bit between two moving parts/bones +returning hitpos pseudo-projectile : like a boomerang +reposition pets +crazy position randomizer: position a part on random positions]] + }, ["proxy"] = { tooltip = "applies math to parts", popup_tutorial = - "This part computes math and applies the numbers it gives to a parameter on a part, for number (x), vector (x,y,z) or boolean(true (1) or false (0)) types. It can send to the parent, to all its children, or to an external target part.\n".. - "Easy setup can help you make a rough idea quickly, but writing math yourself in the expression gives supremely superior control over what the math does.\n\n".. - "Here's a quick crash course in the syntax with basic examples showing the rules to observe:\n\n".. - "Basic numbers /math operators : 4^0.5 - 2*(0.2 / 5) + timeex()%4\n".. - "The only basic operators are: + - * / % ^\n".. - "Functions:\n".. - "\tFunctions are like variables that gather data from the world or that process math.\n".. - "\tMost functions are nullary, which means they have no argument: timeex(), time(), owner_health(), owner_armor_fraction()\n".. - "\tOthers have arguments, which can be required or optional: clamp(x,min,max), random(), random(min,max), random_once(seed,min,max), etc.\n".. - "\tAll Lua functions are declared by a set of parentheses containing arguments, possibly separated by commas.\n".. - "Arguments and tokens:\n".. - "\tMost arguments\' type is numbers, but some might be strings with some requirements; Most of the time it\'s a name or a part UID, for example:\n".. - "\tValid number arguments are numbers, functions or well-formed expressions. It\'s the same type because at the end of the day it gives you a number.\n".. - "\t\tNeedless to say, if you compose an expression, you need a coherent link between the tokens (i.e. math operators or functions). 2 + 2 is valid, 2 2 is not.\n".. - "\tValid string arguments are text declared by double quotes. Lua\'s string concatenation operator works. command(\"name\"..2) is the same as command(\"name2\")\n".. - "\t\tWithout the string declaration, Lua tries to look for a global variable. command(\"name\") is valid, command(name) is not.\n\n".. - "Nested functions (composition) : clamp(1 - timeex()^0.5,0,1)\n".. - "XYZ / Vectors (comma notation) : 100,0,50\n".. - "nil (skipping an axis) : 100,nil,0\n\n".. - "You can write pretty much any math using the existing functions as long as you observe the syntax\'s rules: the most common ones being to close your brackets properly, don't misspell your functions\' names and give them all their necessary arguments.\n\n".. - "There are lots of technical things to learn, but you can consult my example proxy bank by right clicking the expression field, and go consult our wiki for reference. https://wiki.pac3.info/part/proxy\n\n".. - "As a conclusion, I\'m gonna editorialize and give my recommendations:\n".. - "\t-Write with purpose. Avoid unnecessary math.\n".. - "\t\t->But still, write in a way that lets you understand the concept better.\n".. - "\t-More to the point, please have patience and deliberation. Make sure every piece works BEFORE moving on and making it more complex.\n".. - "\t-The fundamental mechanism of developing and applying new ideas is composition / compounding.\n".. - "\t\t->Multiplying different expression bits together tends to combine the concepts\n".. - "\t\t->e.g. clamp(0.2*timeex(),0,1)*sin(10*time()) is a fadein and a sine wave. What do you get? sine wave fading to full power.\n".. - "\t-Please read the debug messages in the console or in chat, they help the correction process if we make mistakes." +[[This part computes math and applies the numbers it gives to a parameter on a part, for number (x), vector (x,y,z) or boolean(true (1) or false (0)) types. It can send to the parent, to all its children, or to an external target part. +Easy setup can help you make a rough idea quickly, but writing math yourself in the expression gives supremely superior control over what the math does. + + +Here's a quick crash course in the syntax with basic examples showing the rules to observe: + +Basic numbers /math operators : 4^0.5 - 2*(0.2 / 5) + timeex()%4 +The only basic operators are: + - * / % ^ + +Functions: + Functions are like variables that gather data from the world or that process math. + Most functions are nullary, which means they have no argument: timeex(), time(), owner_health(), owner_armor_fraction() + Others have arguments, which can be required or optional: clamp(x,min,max), random(), random(min,max), random_once(seed,min,max), etc. + All Lua functions are declared by a set of parentheses containing arguments, possibly separated by commas. + +Arguments and tokens: + Most arguments' type is numbers, but some might be strings with some requirements; Most of the time it's a name or a part UID, for example: + Valid number arguments are numbers, functions or well-formed expressions. It's the same type because at the end of the day it gives you a number. + Needless to say, if you compose an expression, you need a coherent link between the tokens (i.e. math operators or functions). 2 + 2 is valid, 2 2 is not. + Valid string arguments are text declared by double quotes. Lua's string concatenation operator works. command("name"..2) is the same as command("name2") + Without the string declaration, Lua tries to look for a global variable. command("name") is valid, command(name) is not. + +Nested functions (composition) : clamp(1 - timeex()^0.5,0,1) + As you can see, you can have functions inside of functions. + +XYZ / Vectors (comma notation) : 100,0,50 + vectors for position, angles, colors etc. are written that way. + +nil (skipping an axis) : 100,nil,0 + if you have an "invalid" variable name like our commonly-used dummy nil, the proxy will leave that axis unchanged, so you can adjust the value manually or with another proxy. + you could also set your axis in easy setup. + +You can write pretty much any math using the existing functions as long as you observe the syntax's rules: the most common ones being to close your brackets properly, don't misspell your functions' names and give them all their necessary arguments. + +There are lots of technical things to learn. Do not be overwhelmed. Feed your curiosity instead. Documentation is NOT lacking. +I wrote builtin tutorials for pretty much every last function. It's accessible pretty much everywhere right from the editor. +By right clicking the expression field, you can consult my example proxy bank. You will also see options for tutorials for any active function on that proxy. + +Do that, or use the "view specific help or info about this part" option right clicking on the part once you've selected an input or written some functions. + +Better yet, opening the inputs list will even have these tutorial entries as tooltips! + +Go consult our wiki for reference. https://wiki.pac3.info/part/proxy +It's not fully up to date on new functions because it reflects the main version, but you're currently on develop. The function tutorials are available in the input list. + + +As a conclusion, I'm gonna editorialize and give my recommendations: + Write with purpose. Avoid unnecessary math. + ->But still, write in a way that lets you understand the concept. It's not bad to have an imperfect expression. + ->This is why I added support for names for uid-based functions. var1("fade_factor") is more + + -More to the point, please have patience and deliberation. Make sure every piece works BEFORE moving on and making it more complex. + ->A very common problem that people do is they add stuff randomly by cobbling stuff together not knowing why. Please don't do things haphazardly. + ->Blind faith will cause problems for exactly that reason. + + -The fundamental mechanism of developing and applying new ideas is composition / compounding. + ->Multiplying different expression bits together tends to combine the concepts. + ->e.g. ezfade(0.2)*sin(10*time()) is a fadein and a sine wave. What do you get? sine wave fading to full power. + + -Please read the debug messages in the console or in chat. They will help the correction process. + + -You know where to look for help. With a good enough topic, we can discuss math at length. Why else would they have whole university courses for real math? Think about it. There's lots of ways to approach it. + ->But I can't help you if you're not curious. I can only hope that you read through this without skipping to the end. But my faith isn't worth much.]] }, ["sunbeams"] = { @@ -1115,26 +1237,73 @@ do ["damage_zone"] = { tooltip = "deals damage in a zone", popup_tutorial = - "This part tries to deal hitbox damage via the server. It may or may not be allowed because of server settings (pac_sv_damage_zone) and client consents (pac_client_damage_zone_consent), etc. Server owners can add or remove entity classes that can be damaged with pac_damage_zone_blacklist_entity_class, pac_damage_zone_whitelist_entity_class commands.\n".. - "Among NPCs it should include VJ and DRG base NPCs, but only if they have npc_ or drg_ in their name\n".. - "Most shapes should be self-explanatory but you can use the preview function to see what it should cover. There are some settings for raycasts which could come in handy for some niche use cases even if the basic ones you'll use most of the time (box, sphere, cone from spheres, ray) will not really use these.".. - "There are certain special damage types. the dissolves can disintegrate entities but can be restricted in the server, prevent_physics_force suppresses the corpse force, removenoragdoll removes the corpse.\n".. - "" +[[This part can deal hitbox-based damaged via the server. It might not be allowed. There are server settings (e.g. pac_sv_damage_zone) and client consents (pac_client_damage_zone_consent) for protecting the peace. +Server owners can add or remove entity classes that can be damaged with pac_damage_zone_blacklist_entity_class, pac_damage_zone_whitelist_entity_class commands. + +Most hitbox shapeas should be self-explanatory, but you can use the preview function to see what it should cover. +There are some settings for raycasts which could come in handy for some niche use cases, but you'll probably use one of the basic ones (box, sphere, cone from spheres, ray) +you can filter by certain types of targets like general classes like NPCs, which should include VJ and DRG base, as well as according to their "friendliness" which are related to dispositions (ally, enemy, neutral). + +Damage falloff: +reduces the damage depending on the distance (according to the hitbox shape), and you can set a power for a bumpier or sharper falloff + +Do Not Kill and Reverse Do Not Kill: +critical health is a point of comparison. +Do Not Kill prevents the damage zone from taking health below that point. when healing, it prevents from healing above that point. converge to the critical health. +Reverse Do Not Kill damages only if health is below critical health, heals only if health is above critical health. diverge from the critical health. + +Damage Scaling: +damage scaling applies a maxHP%-type damage, a fraction of max HP. 1 is 100% of HP. if a target has 150 HP and 100 max HP, it will deal 100 damage. +it doesn't always insta-kill, as damage multipliers can still take effect, but it's there to let you even the playing field with any NPC like nextbots with absurd HP + +There are certain "special" damage types. the dissolves can disintegrate entities but can be restricted in the server, prevent_physics_force suppresses the corpse force, removenoragdoll removes the corpse. +You can define your damage as a damage over time, which repeats the same damage a number of times. DOT time is the delay between ticks, DOT count is the number of ticks. + +You can link the damage zone to sounds to use as hit sounds or kill sounds. They should work without much issue. +You can set hit parts, which are like projectiles. they spawn on targets. It can be unreliable though!]] }, ["health_modifier"] = { tooltip = "modifies your health, armor", popup_tutorial = - "This part allows you to quickly change your max health, max armor, damage multiplier taken, and has the possibility to give you extra health bars that absorb damage before the main health gets damaged.\n".. - "For the extra bars, you need to set a layer priority to pick which ones get damaged first. Outer ones are higher layer values. But they're still invisible for now... events and proxies will come later...\n".. - "The part's usage may or may not be allowed by the server." - } +[[This part lets you change your max health, armor, add a multiplier for damage taken, and create extra health bars that take damage before the main health. + +the "follow health" and "follow armor" checkboxes mean the current health will follow your max health or armor on specific conditions. +It will increase alongside the max IF you're already at the max. And it will be brought down if you lower the max below what you currently have. + +For the extra bars, you can set a layer priority to pick which ones get damaged first. higher layer means it gets damaged first. +View the info bubble to view the part's current extra health. +available events or proxies for visualization: +healthmod_bar_hit detects when it gets updated +healthmod_bar_layertotal gets the total value for one layer +healthmod_bar_total gets the total value across all layers and parts +healthmod_bar_uidvalue gets the value of one part as an overall amount +healthmod_bar_remaining_bars(uid) gets the value of one part as a number of bars. + +Absorb Factor is a multiplier to the damage that goes to the main health when the extra bars get damaged. +1 will make the extra health basically ineffective, 0 is normal, -1 will heal the main health for every damage received. + +The part's usage may or may not be allowed by the server.]] + }, + + ["mesh_trail"] = { + tooltip = "produces a trail as a model", + popup_tutorial = +[[This part continuously creates a trail of segments linked as a continuous model. You'll see how it differs from beam-based trails. + +end position side represents where the trail's end tapers off, 1 is the tip, 0 is the center, and -1 the base + +you should know U and V are a vertex/point's texture coordinates, the trail can be customized with these values. U generally stretches out, V results in a shear + +This is still a work in progress. Only the rod mode is implemented, and the basis modes aren't implemented except time + +if you are using a custom texture, you can try to add "noclamp " before your link]] + }, + } - --print("we have defined the pace.TUTORIALS.PartInfos", pace.TUTORIALS.PartInfos) for i,v in pairs(pace.TUTORIALS.PartInfos) do - --print(i,v) if pace.PartTemplates then if pace.PartTemplates[i] then pace.PartTemplates[i].TutorialInfo = v diff --git a/lua/pac3/editor/client/proxy_function_tutorials.lua b/lua/pac3/editor/client/proxy_function_tutorials.lua new file mode 100644 index 000000000..34aa475f9 --- /dev/null +++ b/lua/pac3/editor/client/proxy_function_tutorials.lua @@ -0,0 +1,742 @@ +local Tutorials = {} + +--basic math functions +Tutorials["none"] = [[none(n) doesn't do anything. it passes the argument straight through. it's only used in easy setup to read an input without modifications.]] +Tutorials["sin"] = [[sin(rad) is the sine wave function. it has a range of [-1,1] and is cyclical. + +rad is radians. one full cycle takes 2*PI radians. pi is 3.1416... so a full cycle is around 6.283 radians + +sin(0) = 0 (zero crossing on the upward slope) +sin(PI/2) = 1 (the peak) +sin(PI) = 0 (zero crossing on the downward slope) +sin((3/2) * PI) = -1 (the trough / valley) +sin(2*PI) = 0 (zero crossing on the upward slope, starting another cycle) + +there are some interesting symmetries and regularities, but either you already know what a sine is, or you don't. I'll just give you some interesting setups + +the most typical use involves time. +sin(time()*10) + +UCM (uniform circular motion) +when you map a sine and cosine on the position of an object, you trace a circular path by definition. the sine is the height or Y, the cosine is the width or X position. +it's useful for orbits / revolutions +100*sin(3*time()), 100*cos(3*time()) + +power sines make shorter pulses. even powers make the range [0,1] (they convert every trough into a peak), while odd powers keep the range to [-1,1]. +compare these: +sin(time()*5)^10 +sin(time()*5)^15 + +also see nsin and nsin2]] + +Tutorials["cos"] = [[cos(rad) is the cosine wave function. it has a range of [-1,1] and is cyclical. + +rad is radians. one full cycle takes 2*PI radians. pi is 3.1416... so a full cycle is around 6.283 radians + +sin(0) = 1 (the peak) +sin(PI/2) = 0 (zero crossing on the downward slope) +sin(PI) = -1 (the trough / valley) +sin((3/2) * PI) = 0 (zero crossing on the upward slope) +sin(2*PI) = 1 (the peak) + +there are some interesting symmetries and regularities, but either you already know what a cosine is, or you don't. I'll just give you some interesting setups + +the most typical use involves time. +cos(time()*10) + +UCM (uniform circular motion) +when you map a sine and cosine on the position of an object, you trace a circular path by definition. the sine is the height or Y, the cosine is the width or X position. +it's useful for orbits / revolutions +100*sin(3*time()), 100*cos(3*time()) + +power sines make shorter pulses. even powers make the range [0,1] (they convert every trough into a peak), while odd powers keep the range to [-1,1]. +compare these: +cos(time()*5)^10 +cos(time()*5)^15 + +also see ncos and ncos2]] + +Tutorials["tan"] = [[tan(rad) is the tangent function. I don't have much to say about it but you can look more up if you wish. +the range is [-inf,inf], it's asymptotic, it represents the slope of a surface if it's perfectly vertical, the slope is practically infinite. + +rad is radians. one full cycle takes 2*PI radians. pi is 3.1416... so a full cycle is around 6.283 radians + +]] +Tutorials["abs"] = [[abs(n) takes the absolute value. it removes any negative sign. that's all. it's useless if you're always working with positive numbers.]] +Tutorials["sgn"] = [[sgn(n) takes the sign. +if n > 0 then sgn(n) = 1 +if n = 0 then sgn(n) = 0 +if n < 0 then sgn(n) = -1 + +idea: you can use sgn with random_once to randomly pick a side with sgn(random_once(0,-1,1)), then multiplying with whatever else you might've had.]] +Tutorials["acos"] = [[acos(cos) is the arc-cosine, the reverse of cos. it will give the corresponding angle in radians. +cos is a cosine value. we expect between -1 and 1]] +Tutorials["asin"] = [[asin(sin) is the arc-sine, the reverse of sin. it will give the corresponding angle in radians. +sin is a sine value. we expect between -1 and 1]] +Tutorials["atan"] = [[atan(tan) is the arc-tangent, the reverse of tan. it will give the corresponding angle in radians. +sin is a tangent value]] +Tutorials["atan2"] = [[atan2(tan) is an alternate arc-tangent, the reverse of tan. it will give the corresponding angle in radians. +tan is a tangent value]] +Tutorials["ceil"] = [[ceil(n) rounds the number up. +ceil(0) = 0 +ceil(0.001) = 1 +ceil(1) = 1]] +Tutorials["floor"] = [[floor(n) rounds the number down. +floor(0) = 0 +floor(0.999) = 0 +floor(1) = 1]] +Tutorials["round"] = [[round(n,dec) rounds the number up to a certain amount of decimals +dec is decimal magnitude. 0 if not provided (whole numbers), 1 is tenths, 2 is hundredths, -1 is tens, -2 is hundreds etc.]] +Tutorials["rand"] = [[rand() is math.random. it generates a random number from 0 to 1]] +Tutorials["randx"] = [[randx(a,b) is math.Rand(a,b). it generates a random number from a to b.]] +Tutorials["sqrt"] = [[sqrt(x) is just the square root. it's equivalent to x^0.5 +avoid negative values.]] +Tutorials["exp"] = [[exp(base,exponent) is an exponentiation. it's equivalent to base^exponent]] +Tutorials["log"] = [[log(x, base) is the logarithm on a base. logarithms are the reverse of the exponentiation operation. +e.g. since 10^3 = 1000, log(1000,10) = 3]] +Tutorials["log10"] = [[log10(x) is the logarithm on base ten. logarithms are the reverse of the exponentiation operation. +e.g. since 10^3 = 1000, log10(1000) = 3]] +Tutorials["deg"] = [[deg(rad) converts radians to degrees. PI radians = 180 degrees]] +Tutorials["rad"] = [[rad(deg) converts degrees to radians. PI radians = 180 degrees]] +Tutorials["clamp"] = [[clamp(x,min,max) restricts x within a minimum and maximum. if x goes above max, clamp will still return max. if x goes below min, clamp will still return min. +observe clamp(timeex(),0,1) or clamp(10*timeex(),0,50) +it was standard for fades and movement transitions but now ezfade and ezfade_4pt exist to make it easier]] + +Tutorials["nsin"] = [[nsin(radians) is the normalized sine. +it is simply 0.5 + 0.5*sin(radians) + +whereas sin has the codomain of [-1,1], we may sometimes want a normalized [0,1] for various reasons + +keep in mind sin(0) is 0 (the wave's zero-crossing going up), so nsin(0) will be 0.5, so you may want to use nsin2 if you want to start at 0]] + +Tutorials["nsin2"] = [[nsin2(radians) is another normalized sine, but phase-shifted to start at 0. +it is simply 0.5 + 0.5*sin(-PI/2 + radians) + +whereas sin has the codomain of [-1,1], we may sometimes want a normalized [0,1] for various reasons + +keep in mind sin(0) is 0 (the wave's zero-crossing going up), so nsin(0) will be 0.5, this is why nsin2 exists to start at 0 (the wave's trough) instead]] + +Tutorials["ncos"] = [[ncos(radians) is the normalized cosine. +it is simply 0.5 + 0.5*cos(radians) + +whereas cos has the codomain of [-1,1], we may sometimes want a normalized [0,1] for various reasons + +keep in mind sin(0) is 0, so nsin(0) will be 0.5, so you may want to use ncos2 if you want to start at 0]] + +Tutorials["ncos2"] = [[ncos2(radians) is another normalized cosine, but phase-shifted to start at 0. +it is simply 0.5 + 0.5*sin(-PI + radians) + +whereas cos has the codomain of [-1,1], we may sometimes want a normalized [0,1] for various reasons + +keep in mind cos(0) is 1 (the wave's peak), so ncos(0) will be 0.5, this is why you should use ncos2 if you want to start at 0 (the wave's trough) +but ncos2 is the same as nsin2. we forced them to have the wave starting at the same phase]] + +Tutorials["polynomial"] = +[[polynomial(x, a0, a1, a2, a3 ..., aN) + +computes a polynomial series, which means it takes the base x and sums over exponents for every coefficient provided a1*x + a2*x^2 + a3*x^3 ... + aN*x^N +x is the base +for any a(0) .. to a(N), a(n) is a coefficient and the sum will add a * x^n + +e.g. you might have polynomial(2,0,1,2,3) which is 34, since it is computed as 0*1 + 1*2 + 2*(2*2) + 3*(2*2*2)]] + + +--basic logic and commands +Tutorials["command"] = [[command(name) reads your own pac_proxy data (from console commands). + +name is the name of the value. it is optional but the alternative is weird. +without that argument, it will use the name of the proxy part. which wouldn't allow multiple values in the same expression. + +e.g. "pac_proxy my_number 1" means command("my_number") will be 1 +if you then run "pac_proxy my_number ++1" repeatedly, command("my_number") will be 2, then 3, then 4 ... +you can also do "pac_proxy my_number --5" etc. and enter vectors. +"pac_proxy my_number 1 2 3"]] + +Tutorials["property"] = +[[property(property_name, field) + +it takes a part's property. the part is the target entity + +property_name is a part's variable name. e.g. "Alpha" +field is an axis: "x", "y", "z", "p", "y", "r", "r", "g", "b"]] + +Tutorials["number_operator_alternative"] = [[number_operator_alternative(comp1, op, comp2, num1, num2) or if_else is a simple if statement to choose between two numbers depending on the compared input values + +comp1 is the first element to compare +op is the operator, it is a string / text. we expect quotes. you have most number-based operators written different ways + "=", "==", "equal" + ">", "above", "greater", "greater than" + ">=", "above or equal", "greater or equal", "greater than or equal" + "<", "below", "less", "less than" + "<=", "below or equal", "less or equal", "less than or equal" + "~=", "!=", "not equal" + +comp2 is the second element to compare. + +num1 is the result to give if the comparison was found to be true. that's the "if" case, it's optional, 1 if not provided +num2 is the result to give if the comparison was found to be false. that's the "else" case, it's optional, 0 if not provided + +thus we might have if_else(4, ">", 5, 1, -1), and we know that's not true so we shall take -1 as a result]] +Tutorials["if_else"] = Tutorials["number_operator_alternative"] + +Tutorials["sequenced_event_number"] = [[sequenced_event_number(name) reads your own pac_event_sequenced data for command events (from console commands). + +name is the base name of the sequenced event + +events will only be registered as sequences if you have a series of numbered command events like hat1, hat2, hat3, +or if you force it to register with e.g. "pac_event_sequenced_force_set_bounds color 1 5" + +keep in mind sequenced events are managed independently from normal command events, although they end up applied to the same source. +we will not change the code to force that. if you have a series of numbered events, they are not necessarily a sequence. e.g. I have togglepad0 to togglepad9 bound to my keypad, they are independent bind togglers, they shouldn't mess with each other. + +if they are a sequenced event, you should avoid triggering them from the event wheel or with normal pac_event commands. please use pac_event_sequenced to set your sequenced events so the function can update properly.]] + +Tutorials["feedback"] = [[feedback(), feedback_x(), feedback_y() and feedback_z() take the proxy's previous computed value of the main expression. +it is used in feedback controllers to maintain a memory that's adjustable with a command-controlled speed, and in feedback attractors to gravitate toward a changeable target number + +typical examples would be +feedback() + ftime()*command("speed") +feedback_x() + ftime()*(command("targetx") - feedback_x()), feedback_y() + ftime()*(command("targety") - feedback_y()), feedback_z() + ftime()*(command("targetz") - feedback_z()) +feedback() - 4*(command("target") - feedback()) + +extra expressions can't change feedbacks. they can read them from the previous frame though. feebacks are only computed from the main expression]] + +Tutorials["feedback_x"] = Tutorials["feedback"] +Tutorials["feedback_y"] = Tutorials["feedback"] +Tutorials["feedback_z"] = Tutorials["feedback"] + +local extravar_tutorial = [[the extra/var series range from 1 to 5, so you'll have extra1, extra2, extra3, extra4, extra5 or alternatively var1, var2, var3, var4, var5 + +var1(uid) for example takes the result of the first extra expression of the proxy referenced + +uid is a string argument corresponding to the Unique ID, partial UID or name of a proxy. +It's optional but you'll probably end up using it anyway because it's not hugely useful if it's gone. + +the two main uses for this function are: +1-without the uid argument: working inside the same proxy, compressing some math for readability. extra expressions are computed before the main expression. +2-with the uid argument: outsourcing / creating variables used by other proxies. defining some stuff outside is useful to make your proxies more meaningful and simpler down the line + +Keep in mind if you have feedback functions, feedback can only change based on the main expression. Put your main math in the main expression in that case. +You can simply put a feedback() in your extra expression and it'll work then. You could also do some minor reformatting on it. + +here's what you should do, e.g. with a standard feedback attractor setup. +main : feedback() + ftime()*(feedback) +extra1 : feedback()]] + +for i=1,5,1 do + Tutorials["var"..i] = extravar_tutorial + Tutorials["extra"..i] = extravar_tutorial +end + + +--sequences +Tutorials["hexadecimal_level_sequence"] = [[hexadecimal_level_sequence(freq, hex) converts a hexadecimal string into numbers, and animated as a sequence, but normalized to [0,1] ranges + +freq is the frequency of the sequence. how many times it should run every second. +hex is a hexadecimal (base 16) string / text. every letter corresponds to a frame, these are divided by 15 at the end. + +it's a weird one but it was a coin flip to decide whether people want a true hexadecimal or a normalized sequence maker. + +"0" = 0 +"1" = 1/15 (0.067) +... +"9" = 9/15 (0.6) +"a" = 10/15 (0.667) +"b" = 11/15 (0.733) +"c" = 12/15 (0.8) +"d" = 13/15 (0.866) +"e" = 14/15 (0.933) +"f" = 1 + +"0f" would be a simple binary flicker pulse +"000f00000f00f00f0f0ff00f0f0f0f" would be a semi-erratic flicker, might look good with 0.5 frequency. +"0123456789abcdefedcba987654321" would be a linear fadein-fadeout]] + +Tutorials["letters_level_sequence"] = [[letters_level_sequence(freq, str) converts a string of letters into numbers + +it's inspired by Source / Hammer light presets, but normalized to [0,1] ranges. + +freq is the frequency of the sequence. how many times it should run every second. +str is the letters. we expect quotes. + +"az" would be a simple binary flicker pulse +"abcdefghijklmnopqrstuvwxyz" would be a sawtooth-style pulse]] + +Tutorials["numberlist_level_sequence"] = [[numberlist_level_sequence(freq, ...) converts a "vararg" list of numbers and cycles through them regularly. no further processing is done to the numbers + +freq is the frequency of the sequence. how many times it should run every second. +... is the numbers, separated by commas e.g. a1, a2, a3 ... aN + +0,1 would be a simple binary flicker pulse +0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1 would be a sawtooth-style pulse]] + + + +--time +Tutorials["time"] = [[time() gives you world time, it is RealTime(). It's the server time unaffected by sv_timescale or other factors like pausing.]] +Tutorials["synced_time"] = [[synced_time() gives you CurTime(). It's the server time affected by sv_timescale and other factors like pausing.]] +Tutorials["systime"] = [[systime() gives you system time, it is SysTime()]] Tutorials["stime"] = Tutorials["systime"] +Tutorials["frametime"] = [[frametime() gives you FrameTime(), which is how long it took to draw the previous frame on your screen. +It is used when adjusting iteratively by addition while maintaining a steady speed]] Tutorials["ftime"] = Tutorials["frametime"] +Tutorials["framenumber"] = [["framenumber() gives you FrameNumber(), which is the amount of frames that were drawn since you loaded the map"]] Tutorials["fnumber"] = Tutorials["framenumber"] +Tutorials["timeex"] = [[timeex() is the time in seconds counted from when the proxy was shown, like a chronometer / stopwatch. It is a simple yet capital building block in an immense amount of the proxies you can think of.]] + +--randomness and interpolation +Tutorials["ezfade"] = [[ezfade(speed, starttime, endtime) creates standard fades flexibly. think of clamps. this is that, but a bit easier and quicker to set up. + +clamp(timeex(), 0, 1) = ezfade() = ezfade(1) +clamp(1 - timeex(), 0, 1) = ezfade(-1) +clamp(-9 + 2*timeex(), 0, 1) = ezfade(2, 4.5) +clamp(-1 + 0.5*timeex(),0 , 1)*clamp(2 - timeex()*0.5, 0, 1) = ezfade(0.5, 2, 6) + +speed (optional, default = 1) is how fast the fades should work. the speed is the same for the fade-in and the fade-out. 2 means it will last half a second. negative values gives you the simple fadeout setup +starttime (optional, default = 0) is when the fade should start happening, starting from showtime. if speed is negative, starttime will count as an endtime +endtime (optional) is when the fade-out should end. + +keep in mind we are still working in normalized space, we're within 0 and 1. it is suitable for float-based variables like Alpha. +but if you want to use that in another variable you might have to adjust by multiplying. + +it's by design. forget about putting your min and max inside the function. work in normalized terms to have a clear view of the time.]] + +Tutorials["ezfade_4pt"] = [[ezfade_4pt(in_starttime, in_endtime, out_starttime, out_endtime) creates a fadein-fadeout setup in normalized ranges i.e. [0,1] based on four time points rather than speeds + +in_starttime is the time when the fadein should start (where it starts moving from 0 to 1). +in_endtime is the time when the fadein should end (where it is 1) +out_starttime is the time when the fadeout should start (where it starts moving from 1 back to 0), it is optional if you wish to have only a fadein +out_endtime is the time when the fadeout should end (where it finally stops at 0)]] +Tutorials["random"] = [[random(min, max) gives you a random number every time it is called. it will flicker like mad. + +min and max are optional arguments. they are implicitly 0 and 1 if not specified. + +for held randomness, please review random_once(seed, min, max) and sample_and_hold(seed, duration, min, max, ease)]] +Tutorials["random_once"] = [[random_once(seed, min, max) gives you a random number every time the proxy is shown. it will reset when the proxy is hidden and shown. + +seed is an optional argument, it is implicitly 0 if not specified. seed will allow you to choose between independent or shared sources. they are not a true RNG seed (you still get new randomness every showtime) but they work similarly. +min and max are optional arguments. they are implicitly 0 and 1 if not specified. + +e.g. you might have random_once(1),random_once(2),random_once(2) resulting in 0.141, 0.684, 0.684 + +idea: you can use sgn to randomly pick a side with sgn(random_once(0,-1,1)) + +also see sample_and_hold(seed, duration, min, max, ease)]] +Tutorials["lerp"] = [[lerp(fraction, min, max) interpolates linearly between min and max, by the fraction provided. It is ike an adjustable middle point + +fraction is how far in the interpolation we are at. it is implicitly 0 if not provided +min is the start or minimum, it is optional, implicitly -1 if not specified +max is the end or maximum, it is optional, implicitly 1 if not specified + +the formula is (max - min) * frac + min + +e.g. you might have lerp(ezfade(),0,10) which would move from 0 to 10 in 1 second]] + +Tutorials["ease"] = [[eases are interpolations, but more special. +eases are fun. + +eases have several variations. +a typical ease is easeInSine(fraction, min, max) +it would be the same as ease_InSine(fraction, min, max) and InSine(fraction, min, max) +they interpolate between min and max, by the fraction provided, but with more dynamic curves. + +fraction is how far in the interpolation we are at. +min is the start or minimum, it is optional, implicitly 0 if not specified +max is the end or maximum, it is optional, implicitly 1 if not specified + +ease "flavors": Sine, Quad, Cubic, Quart, Circ, Expo, Sine, Back, Bounce, Elastic +For every ease "flavor", there is an "In", an "Out" and an "InOut" version. +you'll have eases written like easeOutBack, ease_InOutSine, InCirc etc. + +here's a quick tip. use ezfade to easily get a transition going. you can even multiply outside instead of putting your min and max inside the function. +e.g.20*easeInSine(ezfade())]] + +for ease,f in pairs(math.ease) do + if string.find(ease,"In") or string.find(ease,"Out") then + Tutorials[ease] = Tutorials["ease"] + Tutorials["ease_"..ease] = Tutorials["ease"] + Tutorials["ease"..ease] = Tutorials["ease"] + end +end + +Tutorials["sample_and_hold"] = [[sample_and_hold(seed, duration, min, max, ease) or samplehold or drift or random_drift + +it's a type of regularly-refreshing random emitter with extra steps. + +seed is like a RNG seed so you can decide whether you want a shared source or independent ones. +duration is how often in seconds the value should move to a new random value. it is 1 if not specified. +min and max are self-explanatory +ease is a string corresponding to the ease name. we expect quotes like "InSine" or "linear". without that argument, it is a sample and hold (no easing) + +reminder there are many variations of eases. here are some examples showing some flavors, the In\Out types and the different alternative ways of writing them. +"easeInSine", "ease_OutBack", "InOutQuad" +the full list of ease "flavors": Sine, Quad, Cubic, Quart, Circ, Expo, Sine, Back, Bounce, Elastic]] +Tutorials["samplehold"] = Tutorials["sample_and_hold"] +Tutorials["drift"] = Tutorials["sample_and_hold"] +Tutorials["random_drift"] = Tutorials["sample_and_hold"] + + +--voice +Tutorials["voice_volume"] = [[voice_volume() reads your voice volume, it has ranges of [0,1], but 1 is an absurdly high volume. please don't scream into the mic.]] +Tutorials["voice_volume_scale"] = [[voice_volume_scale() reads your voice volume scale setting which affects how much volume you transmit, it has ranges of [0,1].]] + + +--statistics +Tutorials["sum"] = [[sum(...) adds all the arguments]] +Tutorials["product"] = [[product(...) takes the product of the arguments]] +Tutorials["average"] = [[average(...) or mean takes the average of the arguments +it is the sum of the arguments divided by the number of arguments]] +Tutorials["mean"] = Tutorials["average"] +Tutorials["median"] = [[median(...) takes the median of the arguments + +it is like the middle element in a list when sorted +if there are an even number of arguments, the median is the average of the two middle elements]] +Tutorials["event_alternative"] = [[event_alternative(uid1, num1, num2) or if_event or if_else_event finds out whether an event is active, and returns num1 or num2 depending on whether it's on or off + +uid1 is a string (text), we expect quotes like "w_button", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +num1 is the value to return if the event is acive (hiding parts), it is optional, 0 by default. +num2 is the value to return if the event is inactive (showing parts), it is optional, 1 by default + +those default values are useful for boolean (true/false) variables if you want to link them to an event. they will reflect the state of the event without any fuss]] +Tutorials["if_event"] = Tutorials["event_alternative"] +Tutorials["if_else_event"] = Tutorials["event_alternative"] + + +--aim, eye position, visibility +Tutorials["owner_fov"] = [[owner_fov() gets your field of view in degrees]] +Tutorials["visible"] = +[[visible(radius) gives you whether the physical target is visible or not. + +radius is an optional argument. it is implicitly 16 if not specified. + +the physical target is usually the parent model, the visibility is considered if a radius circle would be "pixel-visible". +it will give 1 if visible, 0 if not visible. being non-visible happens with world objects, being outside of FOV and with pac drawables]] +Tutorials["eye_position_distance"] = [[eye_position_distance() takes the distance from the part's physical target (target or parent) to the viewer eye position + +it is not very suitable for fading camera effects based on distance, since if you put something in front of the eyes, it will be at near-zero distance + +see also part_distance(uid1,uid2)]] + +Tutorials["eye_angle_distance"] = [[eye_angle_distance() tells you how much of an "angle" you have, from the viewer eye angle to the line from the physical target (target or parent) eye position + +it's using a normalized vector dot products for this + +usually you'll have 0.5 if the viewer is looking straight at the target, down to 0 if looking away at 45 degrees or so]] + +Tutorials["aim_length"] = [[aim_length() takes the distance to the traced aimed point. It's how far you look.]] +Tutorials["aim_length_fraction"] = [[aim_length_fraction() takes the fractional distance to the traced aimed point. It's how far you look, but as a proportion of 16000.]] + +Tutorials["flat_dot_forward"] = [[flat_dot_forward() takes the dot product of the yaw of the owner/part's angles, against the forward angle from the viewer to the owner/part. + +to break it down, it just means to compare how the subject is oriented relative to the viewer. + +-1 is facing away from the viewer +0 is right angled orientation (left/right) +1 is facing toward the viewer + +a similar idea is used in the south park example pac for picking different 2D sprites based on where we're looking]] + +Tutorials["flat_dot_right"] = [[flat_dot_right() takes the dot product of the yaw of the owner/part's angles, against the right angle from the viewer to the owner/part. + +to break it down, it just means to compare how the subject is oriented relative to the viewer. + +0 is facing away or toward the viewer +-1 is when the subject is facing left +1 is when the subject is facing right + +a similar idea is used in the south park example pac for picking different 2D sprites based on where we're looking]] + +Tutorials["owner_eye_angle_pitch"] = [[owner_eye_angle_pitch() takes the upward eye angle +the ranges are about [0,1]. +you'll usually have root owner checked for this.]] +Tutorials["owner_eye_angle_yaw"] = [[owner_eye_angle_yaw() takes the sideways eye angle +the ranges are about [-2,2]. +you'll usually have root owner checked for this.]] +Tutorials["owner_eye_angle_roll"] = [[owner_eye_angle_roll() takes the tilt of eye angle. +you'll usually have root owner checked for this. +you normally won't have a roll in your eye angles.]] + + +--position, velocity, vectors +Tutorials["part_distance"] = [[part_distance(uid1, uid2) takes the distance between two base_movable parts like models + +uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid2 is optional, it is implicitly using the proxy's parent model for example. +that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which meant the set uid would match the old base part otherwise]] + +Tutorials["Vector"] = [[Vector(x,y,z) creates a vector. It has access to vector functions like Vector(0,0,1):Dot(Vector(2,0,0))]] + +Tutorials["owner_position"] = [[owner_position() gets the owner's world position. +the owner is either the parent model or the owning entity i.e. your player]] +Tutorials["owner_position_x"] = [[owner_position_x() gets the owner's world position on x. +the owner is either the parent model or the owning entity i.e. your player]] +Tutorials["owner_position_y"] = [[owner_position_y() gets the owner's world position on y. +the owner is either the parent model or the owning entity i.e. your player]] +Tutorials["owner_position_z"] = [[owner_position_z() gets the owner's world position on z. the owner is either the parent model or the owning entity i.e. your player]] + +Tutorials["part_pos"] = [[part_pos(uid1) takes the position of a base_movable part like models + +uid1 is a string (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid1 is optional, it is implicitly using the proxy's parent model for example]] +Tutorials["part_pos_x"] = [[part_pos_x(uid1) takes the X (perhaps north/south) world position of a base_movable part like models + +uid1 is a string (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid1 is optional, it is implicitly using the proxy's parent model for example]] + +Tutorials["part_pos_y"] = [[part_pos_y(uid1) takes the Y (perhaps east/west) world position of a base_movable part like models + +uid1 is a string (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid1 is optional, it is implicitly using the proxy's parent model for example]] + +Tutorials["part_pos_z"] = [[part_pos_z(uid1) takes the Z (up/down) world position of a base_movable part like models + +uid1 is a string (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid1 is optional, it is implicitly using the proxy's parent model for example]] + +Tutorials["delta_pos"] = [[delta_pos(uid1, uid2) takes the difference of world positions as a vector, between two base_movable parts like models. +mind the order. it is doing (pos2 - pos1) like a standard delta + +uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid2 is optional, it is implicitly using the proxy's parent model for example. +that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which means the set uid would match the old part otherwise]] + +Tutorials["delta_x"] = [[delta_x(uid1, uid2) takes the difference of X (perhaps north/south) world coordinates, between two base_movable parts like models +mind the order. it is doing (pos2.x - pos1.x) like a standard delta + +uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid2 is optional, it is implicitly using the proxy's parent model for example. +that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which means the set uid would match the old part otherwise]] + +Tutorials["delta_y"] = [[delta_y(uid1, uid2) takes the difference of Y (perhaps east/west) world coordinates, between two base_movable parts like models +mind the order. it is doing (pos2.y - pos1.y) like a standard delta + +uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid2 is optional, it is implicitly using the proxy's parent model for example. +that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which means the set uid would match the old part otherwise]] + +Tutorials["delta_z"] = [[delta_z(uid1, uid2) takes the difference of Z world coordinates (height), between two base_movable parts like models +mind the order. it is doing (pos2.z - pos1.z) like a standard delta + +uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case. +full UID can be copied from the part in the copy menu + +uid2 is optional, it is implicitly using the proxy's parent model for example. +that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which means the set uid would match the old part otherwise]] + +Tutorials["owner_velocity_length_increase"] = [[owner_velocity_length_increase() takes overall speed and takes it to gradually increase in value +it builds up, making it good for wheels and such, although the velocity is taken at its length so it won't go backwards. see owner_velocity_forward_increase.]] + +Tutorials["owner_velocity_forward_increase"] = [[owner_velocity_forward_increase() takes forward speed (velocity dotted with eye angles) and takes it to gradually increase or decrease in value +it builds up, making it good for wheels and such. although the wheels' angles would constantly change, so it might be weird with the dot product +so maybe you should use an invalidbone model as a source for the "mileage" variable and make it an extra expression updated at invalidbone and read on the wheels]] +Tutorials["owner_velocity_right_increase"] = [[owner_velocity_right_increase() takes right speed (velocity dotted with eye angles) and takes it to gradually increase or decrease in value +it builds up, making it good for wheels and such. although the wheels' angles would constantly change, so it might be weird with the dot product +so maybe you should use an invalidbone model as a source for the "mileage" variable and make it an extra expression updated at invalidbone and read on the wheels]] +Tutorials["owner_velocity_up_increase"] = [[owner_velocity_up_increase() takes up speed (velocity dotted with eye angles) and takes it to gradually increase or decrease in value +it builds up, making it good for wheels and such. although the wheels' angles would constantly change, so it might be weird with the dot product +so maybe you should use an invalidbone model as a source for the "mileage" variable and make it an extra expression updated at invalidbone and read on the wheels]] + +Tutorials["owner_velocity_world_forward_increase"] = [[owner_velocity_world_forward_increase() takes X speed (world coordinates) and takes it to gradually increase or decrease in value +it builds up, making it good for wheels and such. although we're in global coordinates so maybe not.]] +Tutorials["owner_velocity_world_right_increase"] = [[owner_velocity_world_right_increase() takes Y speed (world coordinates) and takes it to gradually increase or decrease in value +it builds up, making it good for wheels and such. although we're in global coordinates so maybe not.]] +Tutorials["owner_velocity_world_up_increase"] = [[owner_velocity_world_up_increase() takes X speed (world coordinates) and takes it to gradually increase or decrease in value.]] + +Tutorials["parent_velocity_length"] = [[parent_velocity_length() takes the physical target (target part or parent) overall speed]] +Tutorials["parent_velocity_forward"] = [[parent_velocity_forward() takes the physical target (target part or parent) velocity dotted against the forward of its angle]] +Tutorials["parent_velocity_right"] = [[parent_velocity_right() takes the physical target (target part or parent) velocity dotted against the right of its angle]] +Tutorials["parent_velocity_up"] = [[parent_velocity_up() takes the physical target (target part or parent) velocity dotted against the up of its angle]] + +Tutorials["owner_velocity_length"] = [[owner_velocity_length() takes owner's overall speed. +normal running will usually be 4.5, sprinting is 9, crouching is 1.3, walking is 2.2 + +this function uses the velocity roughness and reset velocities on hide from the behavior section +more roughness means less frame-by-frame smoothing and more direct readouts, although these readouts will be unreliable if the FPS varies. +reset velocities on hide clears the smoothing memory]] +Tutorials["owner_velocity_forward"] = [[owner_velocity_forward() takes owner's forward speed compared to the eye angles. +it's made "forward" by doing a dot product with the eye angles. it will be reduced if you look up or down. +if you want to ignore eye angles, you could set it up on a model part located on invalidbone while not using root owner, and use an extra expression to outsource the result elsewhere + +normal running will usually be -4.5, sprinting is -9, crouching is -1.3, walking is -2.2. going back will make these positive. + +this function uses the velocity roughness and reset velocities on hide from the behavior section +more roughness means less frame-by-frame smoothing and more direct readouts, although these readouts will be unreliable if the FPS varies. +reset velocities on hide clears the smoothing memory]] +Tutorials["owner_velocity_right"] = [[owner_velocity_right() takes owner's right speed compared to the eye angles. +it's made "right" by doing a dot product with the eye angles. it will be reduced if you look away, but normally it shouldn't happen if you're actively moving. it can happen if you fling yourself with noclip and look around. +if you want to ignore eye angles, you could set it up on a model part located on invalidbone while not using root owner, and use an extra expression to outsource the result elsewhere + +normal running will usually be -4.5, sprinting is -9, crouching is -1.3, walking is -2.2. going left will make these positive. + +this function uses the velocity roughness and reset velocities on hide from the behavior section +more roughness means less frame-by-frame smoothing and more direct readouts, although these readouts will be unreliable if the FPS varies. +reset velocities on hide clears the smoothing memory]] +Tutorials["owner_velocity_up"] = [[owner_velocity_up() takes owner's up speed compared to the eye angles. +it's made "up" by doing a dot product with the eye angles. it will be reduced if you look up or down. +if you want to ignore eye angles, you could set it up on a model part located on invalidbone while not using root owner, and use an extra expression to outsource the result elsewhere + +normal noclipping going up will usually be -12, falling at terminal velocity will usually be 20. + +this function uses the velocity roughness and reset velocities on hide from the behavior section +more roughness means less frame-by-frame smoothing and more direct readouts, although these readouts will be unreliable if the FPS varies. +reset velocities on hide clears the smoothing memory]] + +Tutorials["owner_velocity_world_forward"] = [[owner_velocity_world_forward() takes owner's X (north/south?) speed in terms of world (global) coordinates +not that it matters, but going to +X, normal running will usually be 4.5, sprinting is 9, crouching is 1.3, walking is 2.2.]] +Tutorials["owner_velocity_world_right"] = [[owner_velocity_world_right() takes owner's Y (east/west?) speed in terms of world (global) coordinates +not that it matters, but going to +Y, normal running will usually be 4.5, sprinting is 9, crouching is 1.3, walking is 2.2.]] +Tutorials["owner_velocity_world_up"] = [[owner_velocity_world_up() takes owner's Z (up/down) speed in terms of world (global) coordinates +normal noclipping going up will usually be -12, falling at terminal velocity will usually be 20.]] + + +--model parameters +Tutorials["pose_parameter"] = [[pose_parameter(name) takes the value of the owner's pose parameter. it can be in weird ranges. + +name is the name of the pose parameter to read. it's a string, we expect quotes e.g. pose_parameter("head_pitch") + +keep in mind most non-biped and most models not made as playermodels do not have the usual pose parameters. +if you're using a monster-type model as a PM, stay in your usual humanoid PM. +make the monster a model on invalidbone and use use root owner for your pose parameters + +also see pose_parameter_true for an alternative adjusted output that more accurately reflects the value of the pose parameter. +e.g. while pose_parameter("head_yaw") might range from [0.2,0.8], pose_parameter_true("head_yaw") would range from [-45,45]. +since most people want a symmetrical thing, they'd need a 45*(-1 + 2*pose_parameter("head_yaw")) style setup +I think it's more convenient to use pose_parameter_true("head_yaw")]] + +Tutorials["pose_parameter_true"] = [[pose_parameter_true(name) takes the value of the owner's pose parameter adjusted to get its "true value". + +name is the name of the pose parameter to read. it's a string, we expect quotes e.g. pose_parameter("head_pitch") + +keep in mind most non-biped and most models not made as playermodels do not have the usual pose parameters. +if you're using a monster-type model as a PM, stay in your usual humanoid PM. +make the monster a model on invalidbone and use use root owner for your pose parameters + +e.g. while pose_parameter("head_yaw") might range from [0.2,0.8], pose_parameter_true("head_yaw") would range from [-45,45]. +since most people want a symmetrical thing, they'd need a 45*(-1 + 2*pose_parameter("head_yaw")) style setup +I think it's more convenient to use pose_parameter_true("head_yaw")]] + +Tutorials["bodygroup"] = [[bodygroup(name, uid) or model_bodygroup reads the parent or the referenced part's bodygroup. + +name is the name of the bodygroup, it's a string, we expect quotes. +uid is the Unique ID or name of a part, it's a string, we expect quotes again]] +Tutorials["model_bodygroup"] = Tutorials["bodygroup"] + +Tutorials["parent_scale_x"] = [[parent_scale_x() takes the X scale (with size) of the physical target (target part or parent)]] +Tutorials["parent_scale_y"] = [[parent_scale_y() takes the Y scale (with size) of the physical target (target part or parent)]] +Tutorials["parent_scale_z"] = [[parent_scale_z() takes the Z scale (with size) of the physical target (target part or parent)]] + +Tutorials["owner_scale_x"] = [[owner_scale_x() takes owner's model scale on x. +it combines the Size and Scale from pac. if owner.pac_model_scale does not exist, it may use owner.GetModelScale +e.g. with a size of 2 and a scale of (3,2,1), it will be 6.]] +Tutorials["owner_scale_y"] = [[owner_scale_y() takes owner's model scale on y. +it combines the Size and Scale from pac. if owner.pac_model_scale does not exist, it may use owner.GetModelScale +e.g. with a size of 2 and a scale of (3,2,1), it will be 4.]] +Tutorials["owner_scale_z"] = [[owner_scale_z() takes owner's model scale on z. +it combines the Size and Scale from pac. if owner.pac_model_scale does not exist, it may use owner.GetModelScale +e.g. with a size of 2 and a scale of (3,2,1), it will be 2.]] + + +--lighting and color +Tutorials["light_amount"] = [[light_amount() reads the physical target (target part or parent) nearby lighting as a color-vector. components are in ranges of [0,1].]] +Tutorials["light_amount_r"] = [[light_amount_r() reads the physical target (target part or parent) nearby lighting's red component, ranges are [0,1].]] +Tutorials["light_amount_g"] = [[light_amount_g() reads the physical target (target part or parent) nearby lighting's green component, ranges are [0,1].]] +Tutorials["light_amount_b"] = [[light_amount_b() reads the physical target (target part or parent) nearby lighting's blue component, ranges are [0,1].]] +Tutorials["light_value"] = [[light_value() reads the physical target (target part or parent) nearby lighting and takes its value (brightness), ranges are [0,1].]] +Tutorials["ambient_light"] = [[ambient_light() reads the global ambient lighting as a color-vector (255,255,255). it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["ambient_light_r"] = [[ambient_light_r() reads the global ambient lighting's red component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["ambient_light_g"] = [[ambient_light_g() reads the global ambient lighting's green component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["ambient_light_b"] = [[ambient_light_b() reads the global ambient lighting's blue component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["hsv_to_color"] = [[hsv_to_color(h,s,v) reads a hue, saturation and value and expands it into an RGB color-vector. + +h is the hue, it's the color in terms of an angle, ranging from 0 to 360, it will take the remainder of 360 to loop back. +0 = red +30 = orange +60 = yellow +90 = lime green +120 = green +150 = teal +180 = cyan +210 = light blue +240 = dark blue +270 = purple +300 = magenta +330 = magenta-red +360 = red + +s is the saturation. at 0 it is white. at more than 1 it distorts the color + +v is the value, the brightness. at 0 it is black. at more than 1 it distorts the color]] + +--health and armor +Tutorials["owner_health"] = [[owner_health() reads your current player health.]] +Tutorials["owner_max_health"] = [[owner_max_health() reads your maximum player health.]] +Tutorials["owner_health_fraction"] = [[owner_health_fraction() reads your health as a fraction between your current health and maximum health. 50 of 200 is 0.25, 100 of 100 is 1]] +Tutorials["owner_armor"] = [[owner_armor() reads your current player HEV suit armor.]] +Tutorials["owner_max_armor"] = [[owner_max_armor() reads your maximum player HEV suit armor.]] +Tutorials["owner_armor_fraction"] = [[owner_armor_fraction() reads your HEV suit armor as a fraction between your current armor and maximum armor. 50 of 200 is 0.25, 100 of 100 is 1]] + + +--entity colors +Tutorials["player_color"] = [[player_color() reads the player color as a color-vector (255,255,255). it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["player_color_r"] = [[player_color_r() reads the player color's red component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["player_color_g"] = [[player_color_g() reads the player color's green component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["player_color_b"] = [[player_color_b() reads the player color's blue component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["weapon_color"] = [[weapon_color() reads the weapon color as a color-vector (255,255,255). it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["weapon_color_r"] = [[weapon_color_r() reads the weapon color's red component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["weapon_color_g"] = [[weapon_color_g() reads the weapon color's green component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["weapon_color_b"] = [[weapon_color_b() reads the weapon color's blue component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["ent_color"] = [[ent_color() reads the entity (root owner (true entity) or parent/target part (pac entity)) color as a color-vector (255,255,255). it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["ent_color_r"] = [[ent_color_r() reads the entity (root owner (true entity) or parent/target part (pac entity)) color's red component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["ent_color_g"] = [[ent_color_g() reads the entity (root owner (true entity) or parent/target part (pac entity)) color's green component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["ent_color_b"] = [[ent_color_b() reads the entity (root owner (true entity) or parent/target part (pac entity)) color's blue component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] +Tutorials["ent_color_a"] = [[ent_color_b() reads the entity (root owner (true entity) or parent/target part (pac entity)) color's alpha component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]] + + +--ammo +Tutorials["owner_total_ammo"] = [[owner_total_ammo(id) reads an ammo type's current ammo reserves. +id is a string for the ammo name. it's a string, we expect quotes. it is a name like "Pistol", "357", "SMG1", "SMG1_Grenade", "AR2", "AR2AltFire", etc.]] +Tutorials["weapon_primary_ammo"] = [[weapon_primary_ammo() reads your current clip's primary ammo on your active weapon.]] +Tutorials["weapon_primary_total_ammo"] = [[weapon_primary_total_ammo() reads your current primary ammo reserves on your active weapon.]] +Tutorials["weapon_primary_clipsize"] = [[weapon_primary_clipsize() reads your primary clip size on your active weapon.]] +Tutorials["weapon_secondary_ammo"] = [[weapon_secondary_ammo() reads your current clip's secondary ammo on your active weapon.]] +Tutorials["weapon_secondary_total_ammo"] = [[weapon_secondary_total_ammo() reads your current secondary ammo reserves on your active weapon.]] +Tutorials["weapon_secondary_clipsize"] = [[weapon_secondary_clipsize() reads your secondary clip size on your active weapon.]] + + +--server population +Tutorials["server_maxplayers"] = [[server_maxplayers() gets the server capacity.]] +Tutorials["server_playercount"] = [[server_playercount() or server_population, gets the server population (number of players).]] +Tutorials["server_population"] = Tutorials["server_playercount"] +Tutorials["server_botcount"] = [[server_botcount() gets the number of bot players connected to the server.]] +Tutorials["server_humancount"] = [[server_botcount() gets the number of human players connected to the server.]] + + +--health modifier extra health bars +Tutorials["pac_healthbars_total"] = [[pac_healthbars_total() or healthmod_bar_total gets the total amount of "extra health" granted by your health modifiers.]] +Tutorials["healthmod_bar_total"] = Tutorials["pac_healthbars_total"] +Tutorials["pac_healthbars_layertotal"] = [[pac_healthbars_layertotal(layer) or healthmod_bar_layertotal gets the total amount of "extra health" granted by your health modifiers on a certain layer. +layer should be a number, they are usually whole numbers from 0 to 15, with bigger numbers being damaged first]] +Tutorials["healthmod_bar_layertotal"] = Tutorials["healthmod_bar_layertotal"] +Tutorials["pac_healthbar_uidvalue"] = [[pac_healthbar_uidvalue(uid) or healthmod_bar_uidvalue gets the amount of "extra health" granted by one of your health modifier parts. +uid is a string corresponding to the name or Unique ID of the part, we expect quotes.]] +Tutorials["healthmod_bar_uidvalue"] = Tutorials["pac_healthbar_uidvalue"] +Tutorials["pac_healthbar_remaining_bars"] = [[healthmod_bar_remaining_bars(uid) or pac_healthbar_remaining_bars gets the remaining number of "extra health" bars granted by one of your health modifier parts. +uid is a string corresponding to the name or Unique ID of the part, we expect quotes.]] +Tutorials["healthmod_bar_remaining_bars"] = Tutorials["pac_healthbar_remaining_bars"] + +return Tutorials diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index d6272efe6..55108337d 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -674,7 +674,7 @@ end vgui.Register( "pace_settings", PANEL, "DPanel" ) -function pace.OpenSettings() +function pace.OpenSettings(tab) if IsValid(pace.settings_panel) then pace.settings_panel:Remove() end @@ -715,6 +715,9 @@ function pace.OpenSettings() timer.Simple(0.5, function() pace.cvar_changes = nil end) local pnl = vgui.Create("pace_settings", pnl) pnl:Dock(FILL) + if tab then + pnl.sheet:SwitchToName(tab) + end end concommand.Add("pace_settings", function() @@ -1385,33 +1388,39 @@ function pace.FillEditorSettings(pnl) elseif option_name == "paste" then return pace.MiscIcons.paste elseif option_name == "cut" then - return 'icon16/cut.png' + 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' + return"icon16/drive.png" elseif option_name == "bulk_apply_properties" then - return 'icon16/application_form.png' + return "icon16/application_form.png" elseif option_name == "bulk_select" then - return 'icon16/table_multiple.png' + return "icon16/table_multiple.png" elseif option_name == "spacer" then - return 'icon16/application_split.png' + return "icon16/application_split.png" elseif option_name == "hide_editor" then - return 'icon16/application_delete.png' + return "icon16/application_delete.png" elseif option_name == "expand_all" then - return 'icon16/arrow_down.png' + return "icon16/arrow_down.png" elseif option_name == "collapse_all" then - return 'icon16/arrow_in.png' + 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' + return "icon16/information.png" elseif option_name == "reorder_movables" then - return 'icon16/application_double.png' - end - return 'icon16/world.png' + 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" + end + return "icon16/world.png" end partmenu_choices:SetY(50) diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 2f7496c05..53136ade9 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -59,7 +59,10 @@ pace.PACActionShortcut_Dictionary = { "bulk_paste_properties_2", "bulk_hide", "help_info_popup", - "ultra_cleanup" + "ultra_cleanup", + "arraying_menu", + "bulk_morph", + "criteria_process" } pace.PACActionShortcut_Default = { @@ -447,7 +450,7 @@ function pace.DoShortcutFunc(action) if action == "redo" then pace.Redo(pace.current_part) pace.delayshortcuts = RealTime() end if action == "undo" then pace.Undo(pace.current_part) pace.delayshortcuts = RealTime() end if action == "delete" then pace.RemovePart(pace.current_part) end - if action == "hide" then pace.current_part:SetHide(not pace.current_part:GetHide()) end + if action == "hide" then pace.current_part:SetHide(not pace.current_part:GetHide()) pace.PopulateProperties(pace.current_part) end if action == "copy" then pace.Copy(pace.current_part) end if action == "cut" then pace.Cut(pace.current_part) end @@ -649,7 +652,7 @@ function pace.DoShortcutFunc(action) pace.PasteProperties(v) end end - if action == "bulk_hide" then pace.BulkHide() end + if action == "bulk_hide" then pace.BulkHide() pace.PopulateProperties(pace.current_part) end if action == "help_info_popup" then if pace.floating_popup_reserved then @@ -690,7 +693,7 @@ function pace.DoShortcutFunc(action) elseif popup_prefered_type == "cursor" then pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, input.GetCursorPos()) - elseif popup_prefered_type == "editor bar" then + elseif popup_prefered_type == "menu bar" then popup_setup_tbl.obj = pace.Editor pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) @@ -714,6 +717,17 @@ function pace.DoShortcutFunc(action) pace.UltraCleanup(pace.current_part) end + if action == "arraying_menu" then + pace.OpenArrayingMenu(pace.current_part) + end + + if action == "bulk_morph" then + pace.BulkMorphProperty() + end + + if action == "criteria_process" then + pace.PromptProcessPartsByCriteria(pace.current_part) + end end diff --git a/lua/pac3/editor/client/tools.lua b/lua/pac3/editor/client/tools.lua index 0445d31b1..6b77c62a0 100644 --- a/lua/pac3/editor/client/tools.lua +++ b/lua/pac3/editor/client/tools.lua @@ -373,7 +373,7 @@ pace.AddTool(L"import editor tool from file...", function() Derma_StringRequest(L"filename", L"relative to garrysmod/data/pac3_editor/tools/", "mytool.txt", function(toolfile) if file.Exists("pac3_editor/tools/" .. toolfile,"DATA") then local toolstr = file.Read("pac3_editor/tools/" .. toolfile,"DATA") - local ctoolstr = [[pace.AddTool(L"]] .. toolfile .. [[", function(part, suboption) ]] .. toolstr .. " end)" + local ctoolstr = [[pace.AddTool("]] .. toolfile .. [[", function(part, suboption) ]] .. toolstr .. " end)" RunStringEx(ctoolstr, "pac_editor_import_tool") pac.LocalPlayer:ConCommand("pac_editor") --close and reopen editor else @@ -391,7 +391,7 @@ pace.AddTool(L"import editor tool from url...", function() local function ToolDLSuccess(body) local toolname = pac.PrettifyName(toolurl:match(".+/(.-)%.")) local toolstr = body - local ctoolstr = [[pace.AddTool(L"]] .. toolname .. [[", function(part, suboption)]] .. toolstr .. " end)" + local ctoolstr = [[pace.AddTool("]] .. toolname .. [[", function(part, suboption)]] .. toolstr .. " end)" RunStringEx(ctoolstr, "pac_editor_import_tool") pac.LocalPlayer:ConCommand("pac_editor") --close and reopen editor end @@ -821,6 +821,10 @@ pace.AddTool(L"proxy/event: Engrave targets", function(part) end end) +pace.AddTool(L"Process by Criteria", function(part) + pace.PromptProcessPartsByCriteria(part) +end) + --aka pace.UltraCleanup pace.AddTool(L"Destroy hidden parts, proxies and events", function(part) diff --git a/lua/pac3/editor/client/util.lua b/lua/pac3/editor/client/util.lua index 598f349cb..4df3d63d2 100644 --- a/lua/pac3/editor/client/util.lua +++ b/lua/pac3/editor/client/util.lua @@ -145,12 +145,26 @@ function pace.MessagePrompt( strText, strTitle, strButtonText ) local DScrollPanel = vgui.Create( "DScrollPanel", Window ) DScrollPanel:Dock( FILL ) - local Text = DScrollPanel:Add("DLabel") - Text:SetText( strText or "Message Text" ) - Text:SetTextColor( color_white ) - Text:Dock(FILL) - Text:SetAutoStretchVertical(true) - Text:SetWrap(true) + if not pace.alternate_message_prompts and (strText and (#strText < 800)) then + local Text = DScrollPanel:Add("DLabel") + Text:SetText( strText or "Message Text" ) + Text:SetTextColor( color_white ) + Text:Dock(FILL) + Text:SetAutoStretchVertical(true) + Text:SetWrap(true) + else --hack for more text length / alternative style using RichText + local Text = DScrollPanel:Add("RichText") + Text:SetText("") + Text:AppendText(strText or "Message Text") + Text:SetBGColor(0,0,0,0) + Text:Dock(FILL) + Text:SetTall(240) + Text:SetFGColor(255,255,255,255) + function Text:PerformLayout() + Text:SetBGColor(0,0,0,0) + Text:SetFGColor(255,255,255,255) + end + end local Button = vgui.Create( "DButton", Window ) Button:SetText( strButtonText or "OK" ) @@ -194,7 +208,7 @@ function pace.MultilineStringRequest( strTitle, strText, strDefaultText, fnEnter TextEntry:SetMultiline(true) TextEntry:Dock(FILL) TextEntry:SetUpdateOnType(true) - TextEntry.OnChange = function(self) self:SetText(self:GetValue():gsub("\t", " ")) end + TextEntry.OnChange = function(self) local caret = self:GetCaretPos() self:SetText(self:GetValue():gsub("\t", " ")) self:SetCaretPos(caret) end TextEntry.OnEnter = function() Window:Close() fnEnter( TextEntry:GetValue() ) end local ButtonPanel = vgui.Create( "DPanel", Window ) @@ -227,4 +241,4 @@ function pace.MultilineStringRequest( strTitle, strText, strDefaultText, fnEnter return Window -end \ No newline at end of file +end From 43b6857afedc745b8f7f3846cc603dd718c39d3b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 25 Aug 2024 22:11:58 -0400 Subject: [PATCH 217/300] use part Notes as tree tooltips --- lua/pac3/editor/client/panels/tree.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index 2fd6dff24..8b2ae1673 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -501,7 +501,8 @@ function PANEL:PopulateParts(node, parts, children) fix_folder_funcs(part_node) - if part.Description then part_node:SetTooltip(L(part.Description)) end + if part.Description then part_node:SetTooltip(L(part.Description)) end --ok but have we ever had any Description other than "right click to add parts"? + if part.Notes ~= "" then part_node.Label:SetTooltip(part.Notes) end --idk if anyone uses Notes but tooltips are good if they have something on them. It can easily be overridden by other code anyway. part.pace_tree_node = part_node part_node.part = part From 983dc0da23d83e73d8a1501cc4096ed13cd926fa Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 27 Aug 2024 01:38:45 -0400 Subject: [PATCH 218/300] fixes prevent proxy set function if part reference is invalid in multi target part, and renamed some loop variables force property handlers into submission in bulk morph property --- lua/pac3/core/client/parts/proxy.lua | 10 ++-- lua/pac3/editor/client/parts.lua | 75 ++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index cb84f6e65..00c0e2892 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -1922,6 +1922,7 @@ function PART:OnThink(to_hide) if self.AffectChildren then if self.MultiTargetPart then for _,part2 in ipairs(self.MultiTargetPart) do + if not part2.GetProperty then continue end set(self, part2, x, y, z, true) end else @@ -1931,8 +1932,9 @@ function PART:OnThink(to_hide) end else if self.MultiTargetPart then - for i,v in ipairs(self.MultiTargetPart) do - set(self, v, x, y, z) + for _,part2 in ipairs(self.MultiTargetPart) do + if not part2.GetProperty then continue end + set(self, part2, x, y, z) end else set(self, part, x, y, z) @@ -1992,6 +1994,7 @@ function PART:OnThink(to_hide) if self.AffectChildren then if self.MultiTargetPart then for _,part2 in ipairs(self.MultiTargetPart) do + if not part2.GetProperty then continue end set(self, part2, num, nil, nil, true) end else @@ -2001,7 +2004,8 @@ function PART:OnThink(to_hide) end else if self.MultiTargetPart then - for i,part2 in ipairs(self.MultiTargetPart) do + for _,part2 in ipairs(self.MultiTargetPart) do + if not part2.GetProperty then continue end set(self, part2, num) end else diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index c8ac4cd04..cd53cf4cc 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1941,15 +1941,84 @@ do -- menu if success then start_value = pace.CreatePanel("properties_" .. property_type, properties_pnl) properties_pnl:AddKeyValue("StartValue",start_value) end_value = pace.CreatePanel("properties_" .. property_type, properties_pnl) properties_pnl:AddKeyValue("EndValue",end_value) - + if property_type == "vector" then + function start_value.OnValueChanged(val) + if isstring(val) then + if val == "" then return end + local x,y,z = unpack(string.Split(val, " ")) + val = Vector(x,y,z) + end + start_value:SetValue(val) + end + function end_value.OnValueChanged(val) + if isstring(val) then + if val == "" then return end + local x,y,z = unpack(string.Split(val, " ")) + val = Vector(x,y,z) + end + end_value:SetValue(val) + end + elseif property_type == "angle" then + function start_value.OnValueChanged(val) + if isstring(val) then + if val == "" then return end + local x,y,z = unpack(string.Split(val, " ")) + val = Angle(x,y,z) + end + start_value:SetValue(val) + end + function end_value.OnValueChanged(val) + if isstring(val) then + if val == "" then return end + local x,y,z = unpack(string.Split(val, " ")) + val = Angle(x,y,z) + end + end_value:SetValue(val) + end + elseif property_type == "color" then + function start_value.OnValueChanged(val) + if isstring(val) then + if val == "" then return end + local x,y,z = unpack(string.Split(val, " ")) + val = Color(x,y,z) + end + start_value:SetValue(val) + end + function end_value.OnValueChanged(val) + if isstring(val) then + if val == "" then return end + local x,y,z = unpack(string.Split(val, " ")) + val = Color(x,y,z) + end + end_value:SetValue(val) + end + elseif property_type == "number" then + function start_value.OnValueChanged(val) + start_value:SetValue(tonumber(val) or val) + end + function end_value.OnValueChanged(val) + end_value:SetValue(tonumber(val) or val) + end + end + else start_value = pace.CreatePanel("properties_label", properties_pnl) properties_pnl:AddKeyValue("ERROR",start_value) end_value = pace.CreatePanel("properties_label", properties_pnl) properties_pnl:AddKeyValue("ERROR",end_value) end --end) if start_value.Restart then start_value:Restart() end if end_value.Restart then end_value:Restart() end - if start_value.OnValueChanged then start_value.OnValueChanged(start_value:GetValue()) end - if start_value.OnValueChanged then end_value.OnValueChanged(end_value:GetValue()) end + if start_value.OnValueChanged then + local def = start_value:GetValue() + if pace.BulkSelectList[1] then def = pace.BulkSelectList[1][property] end + start_value.OnValueChanged(def) + start_value.OnValueChanged(start_value:GetValue()) + end + if end_value.OnValueChanged then + local def = end_value:GetValue() + if pace.BulkSelectList[1] then def = pace.BulkSelectList[1][property] end + end_value.OnValueChanged(def) + end_value.OnValueChanged(end_value:GetValue()) + end end local function setsingle(part, property_name, property_type, frac) From 8e2a1356454548087e8bef3a3ecfc680f2eb8017 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 31 Aug 2024 22:41:52 -0400 Subject: [PATCH 219/300] more dynamic property stuff proxies auto update the property and shows as colored (default is dark red) to show users what's being controlled base part Hide property will have a label to show when the part is actually being hidden and the container tooltip will say the reasons (it's not always the part's Hide flag!) which means the responsible parts that are either events, hidden parents or itself convar pac_special_property_update_dynamically convar pac_special_property_text_color I guess there were also some fixes for the popup modes using screen space or something, needed to send the x and y positions in case we need to specify them split cursor popup location into placing the popup at cursor once, or making the popup track the cursor continually base part notes creates the tree label tooltip --- lua/pac3/core/client/base_part.lua | 49 +++++++-- lua/pac3/core/client/parts/proxy.lua | 101 +++++++++++++++--- lua/pac3/editor/client/menu_bar.lua | 1 + lua/pac3/editor/client/panels/properties.lua | 68 +++++++++++- .../editor/client/popups_part_tutorials.lua | 8 +- lua/pac3/editor/client/shortcuts.lua | 3 + 6 files changed, 205 insertions(+), 25 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index 4c5a8a769..c30098494 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -143,6 +143,18 @@ function PART:SetUniqueID(id) end end +function PART:SetNotes(str) + self.Notes = str + if self:GetPlayerOwner() ~= pac.LocalPlayer then return end + if self.pace_tree_node and self.pace_tree_node.Label then + if str ~= "" then + self.pace_tree_node.Label:SetTooltip(str) + else + self.pace_tree_node.Label:SetTooltip() + end + end +end + local function set_info(msg, info_type) if not msg then return nil end local msg = tostring(msg) @@ -770,6 +782,7 @@ do -- hidden / events return found end + local extra_dynamic = CreateClientConVar("pac_special_property_update_dynamically", "1", true, false, "Whether proxies should refresh the properties, and some booleans may show more information.") function PART:CalcShowHide(from_rendering) local b = self:IsHidden() @@ -782,6 +795,24 @@ do -- hidden / events end self.last_hidden = b + if pace.IsActive() then + if self == pace.current_part then --update the hide property (show reasons why it's hidden) + if IsValid(self.hide_property_pnl) then + local reasons_hidden = self:GetReasonsHidden() + local pnl = self.hide_property_pnl:GetParent() + if not table.IsEmpty(reasons_hidden) and not self.reasons_hidden then + self.reasons_hidden = reasons_hidden + pnl:SetTooltip("Hidden by:" .. table.ToString(reasons_hidden, "", true)) + if not extra_dynamic:GetBool() then return end + pnl:CreateAlternateLabel("hidden") + else + pnl:CreateAlternateLabel(nil) --remove it + self.reasons_hidden = nil + pnl:SetTooltip() + end + end + end + end end function PART:IsHiddenCached() @@ -1221,8 +1252,14 @@ do function PART:AlwaysOnThink() end end +function PART:GetTutorial() + if pace and pace.TUTORIALS and pace.TUTORIALS[self.ClassName] then + return pace.TUTORIALS.PartInfos[self.ClassName].popup_tutorial or "" + end +end + --the popup system -function PART:SetupEditorPopup(str, force_open, tbl) +function PART:SetupEditorPopup(str, force_open, tbl, x, y) if pace.Editor == nil then return end if self.pace_tree_node == nil then return end local legacy_help_popup_hack = false @@ -1262,8 +1299,7 @@ function PART:SetupEditorPopup(str, force_open, tbl) local pnl --local pace = pace or {} - if tree_node then - tree_node.Label:SetTooltip(self.ClassName) + if tree_node and tree_node.Label then local part = self function tree_node:Think() @@ -1286,7 +1322,7 @@ function PART:SetupEditorPopup(str, force_open, tbl) tree_node:Think() end if not pnl then - pnl = pac.InfoPopup(info_string,popup_config_table) + pnl = pac.InfoPopup(info_string,popup_config_table, x, y) self.pace_tree_node.popupinfopnl = pnl end if pace then @@ -1296,11 +1332,12 @@ function PART:SetupEditorPopup(str, force_open, tbl) return pnl end -function PART:AttachEditorPopup(str, flash, tbl) - local pnl = self:SetupEditorPopup(str, flash, tbl) +function PART:AttachEditorPopup(str, flash, tbl, x, y) + local pnl = self:SetupEditorPopup(str, flash, tbl, x, y) if flash and pnl then pnl:MakePopup() end + return pnl end function PART:DetachEditorPopup() diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 00c0e2892..0a1cc7765 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -24,19 +24,19 @@ BUILDER:StartStorableVars() end return tbl - end}) + end, description = "What property of the target part should be changed.\nYou don't need to follow the list builder if you know what you're doing with target parts."}) BUILDER:GetSet("RootOwner", false) - BUILDER:GetSetPart("TargetPart") - BUILDER:GetSet("MultipleTargetParts", "") + BUILDER:GetSetPart("TargetPart", {description = "send output to an external part. supports name and uid"}) + BUILDER:GetSet("MultipleTargetParts", "", {description = "send output to multiple external partss.\npaste multiple UIDs or names here, separated by semicolons. With bulk select, you can select parts and right click to get that done quickly.."}) BUILDER:GetSetPart("OutputTargetPart", {hide_in_editor = true}) BUILDER:GetSet("AffectChildren", false) - BUILDER:GetSet("Expression", "") + BUILDER:GetSet("Expression", "", {description = "write math here. hit F1 for a tutorial or right click for examples.", editor_panel = "code_proxy"}) BUILDER:SetPropertyGroup("easy setup") - BUILDER:GetSet("Input", "time", {enums = function(part) return part.Inputs end}) - BUILDER:GetSet("Function", "sin", {enums = function(part) return part.Functions end}) - BUILDER:GetSet("Axis", "") + BUILDER:GetSet("Input", "time", {enums = function(part) return part.Inputs end, description = "base (inner) function for easy setup\nin sin(time()) it is time"}) + BUILDER:GetSet("Function", "sin", {enums = function(part) return part.Functions end, description = "processing (outer) function for easy setup.\nin sin(time()) it is sin"}) + BUILDER:GetSet("Axis", "", {description = "The direction where the output ends up.\nx,y,z for vectors, p,y,r or r,g,b for colors\nIf you provide an expression with vector notation content, it will expand to the next axes. for example \"0,1,2\" on y will put 0 on y and 1 on z, 2 will overflow to nowhere."}) BUILDER:GetSet("Min", 0) BUILDER:GetSet("Max", 1) BUILDER:GetSet("Offset", 0) @@ -53,12 +53,12 @@ BUILDER:StartStorableVars() BUILDER:GetSet("PreviewOutput", false, {description = "Previews the proxy's output (for yourself) next to the nearest owner entity in the game"}) BUILDER:SetPropertyGroup("extra expressions") - BUILDER:GetSet("ExpressionOnHide", "") - BUILDER:GetSet("Extra1", "") - BUILDER:GetSet("Extra2", "") - BUILDER:GetSet("Extra3", "") - BUILDER:GetSet("Extra4", "") - BUILDER:GetSet("Extra5", "") + BUILDER:GetSet("ExpressionOnHide", "", {description = "Math to apply once, when the proxy is hidden. It computes once, so it will not move."}) + BUILDER:GetSet("Extra1", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra1() or var1() to save space, or by another proxy as extra1(\"uid or name\") or var1(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra2", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra2() or var2() to save space, or by another proxy as extra2(\"uid or name\") or var2(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra3", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra3() or var3() to save space, or by another proxy as extra3(\"uid or name\") or var3(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra4", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra4() or var4() to save space, or by another proxy as extra4(\"uid or name\") or var4(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra5", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra5() or var5() to save space, or by another proxy as extra5(\"uid or name\") or var5(\"uid or name\")", editor_panel = "code_proxy"}) BUILDER:EndStorableVars() -- redirect @@ -1554,6 +1554,7 @@ end PART.Inputs.healthmod_bar_remaining_bars = PART.Inputs.pac_healthbar_remaining_bars +local proxy_verbosity = CreateConVar("pac_proxy_verbosity", 1, FCVAR_ARCHIVE, "whether to print info when running pac_proxy") net.Receive("pac_proxy", function() local ply = net.ReadEntity() local str = net.ReadString() @@ -1565,7 +1566,7 @@ net.Receive("pac_proxy", function() if ply:IsValid() then ply.pac_proxy_events = ply.pac_proxy_events or {} ply.pac_proxy_events[str] = {name = str, x = x, y = y, z = z} - if LocalPlayer() == ply then + if proxy_verbosity:GetBool() and pac.LocalPlayer == ply then pac.Message("pac_proxy -> command(\""..str.."\") is " .. x .. "," .. y .. "," .. z) end end @@ -1720,16 +1721,18 @@ function PART:OnShow() self.vec_additive = Vector() end +local extra_dynamic = CreateClientConVar("pac_special_property_update_dynamically", "1", true, false, "Whether proxies should refresh the properties, and some booleans may show more information.") local function set(self, part, x, y, z, children) local val = part:GetProperty(self.VariableName) + local original_x local T = type(val) + local vector_type = false if allowed[T] then if T == "boolean" then x = x or val == true and 1 or 0 local b = tonumber(x) > 0 - -- special case for hide to make it behave like events if self.VariableName == "Hide" then @@ -1754,13 +1757,28 @@ local function set(self, part, x, y, z, children) part:SetProperty(self.VariableName, b) end elseif T == "number" then + original_x = x x = x or val part:SetProperty(self.VariableName, tonumber(x) or 0) + self.using_x = true else + vector_type = true if self.Axis ~= "" and val[self.Axis] then val = val * 1 - val[self.Axis] = x + val[self.Axis] = x or 0 + if T == "Angle" then + self.using_x = self.Axis == "p" or self.Axis == "x" or self.Axis == "pitch" + self.using_y = self.Axis == "y" or self.Axis == "y" or self.Axis == "yaw" + self.using_z = self.Axis == "r" or self.Axis == "z" or self.Axis == "roll" + elseif T == "Vector" then + self.using_x = self.Axis == "x" + self.using_y = self.Axis == "y" + self.using_z = self.Axis == "z" + end else + self.using_x = x ~= nil + self.using_y = y ~= nil + self.using_z = z ~= nil if T == "Angle" then val = val * 1 val.p = x or val.p @@ -1783,6 +1801,57 @@ local function set(self, part, x, y, z, children) set(self, part, x, y, z, true) end end + + --update the property if this is the current part + if not extra_dynamic:GetBool() then return end + if pace:IsActive() then + if self:GetPlayerOwner() ~= pac.LocalPlayer then return end + if part ~= pace.current_part then return end + local property_pnl = part["pac_property_panel_"..self.VariableName] + if IsValid(property_pnl) then + local container = property_pnl:GetParent() + local math_description = "expression:\n"..self.Expression + if self.Expression == "" then math_description = "using " .. self.Function .. " and " .. self.Input end + if vector_type then + if self.using_x then + property_pnl.used_by_proxy = true + container = property_pnl.left + property_pnl.left.used_by_proxy = true + local num = x or 0 + property_pnl.left:SetValue(math.Round(tonumber(num),4)) + container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) + end + if self.using_y then + property_pnl.used_by_proxy = true + container = property_pnl.middle + property_pnl.middle.used_by_proxy = true + local num = y or x or 0 + property_pnl.left:SetValue(math.Round(tonumber(num),4)) + container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) + end + if self.using_z then + property_pnl.used_by_proxy = true + container = property_pnl.right + property_pnl.right.used_by_proxy = true + local num = z or x or 0 + property_pnl.right:SetValue(math.Round(tonumber(num),4)) + container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) + end + elseif T == "boolean" then + if x ~= nil then + property_pnl.used_by_proxy = true + property_pnl:SetValue(tonumber(x) > 0) + container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) + end + elseif original_x ~= nil then + property_pnl.used_by_proxy = true + property_pnl:SetValue(math.Round(tonumber(x) or 0,4)) + container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) + end + + + end + end end function PART:RunExpression(ExpressionFunc) diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index b53313c96..376f44665 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -155,6 +155,7 @@ local function populate_pac(menu) popup_pref_mode:AddOption(L"part label on tree", function() RunConsoleCommand("pac_popups_preferred_location", "pac tree label") end):SetImage('icon16/layout_content.png') popup_pref_mode:AddOption(L"menu bar", function() RunConsoleCommand("pac_popups_preferred_location", "menu bar") end):SetImage('icon16/layout_header.png') popup_pref_mode:AddOption(L"cursor", function() RunConsoleCommand("pac_popups_preferred_location", "cursor") end):SetImage('icon16/mouse.png') + popup_pref_mode:AddOption(L"tracking cursor", function() RunConsoleCommand("pac_popups_preferred_location", "tracking cursor") end):SetImage('icon16/mouse_add.png') popup_pref_mode:AddOption(L"screen", function() RunConsoleCommand("pac_popups_preferred_location", "screen") end):SetImage('icon16/monitor.png') diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 2e4a60c27..43012b7e5 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -2,7 +2,26 @@ local L = pace.LanguageString local languageID = CreateClientConVar("pac_editor_languageid", 1, true, false, "Whether we should show the language indicator inside of editable text entries.") local favorites_menu_expansion = CreateClientConVar("pac_favorites_try_to_build_asset_series", "0", true, false) - +local extra_dynamic = CreateClientConVar("pac_update_properties_dynamically", "1", true, false, "Whether proxies should refresh the properties, and some booleans may show more information.") +local special_property_text_color = CreateClientConVar("pac_special_property_text_color", "160 0 80", true, false, "R G B color of special property text\npac_special_property_text_color \"\" will make it not change the color\nSpecial contexts like proxies and hidden parts can show a different color to show that changes are happening in real time.") + +pace.special_property_text_color = Color(160,0,80) +if special_property_text_color:GetString() ~= "" then + local r,g,b = unpack(string.Split(special_property_text_color:GetString(), " ")) + r = tonumber(r) or 0 g = tonumber(g) or 0 b = tonumber(b) or 0 + pace.special_property_text_color = Color(r,g,b) +else + pace.special_property_text_color = nil +end +cvars.AddChangeCallback("pac_special_property_text_color", function(cvar, old, new) + if new ~= "" then + local r,g,b = unpack(string.Split(special_property_text_color:GetString(), " ")) + r = tonumber(r) or 0 g = tonumber(g) or 0 b = tonumber(b) or 0 + pace.special_property_text_color = Color(r,g,b) + else + pace.special_property_text_color = nil + end +end, "pac_change_special_property_text_color") local searched_cache_series_results = {} @@ -395,6 +414,24 @@ do -- container end end + function PANEL:CreateAlternateLabel(str) + if not str then + if self.alt_label then + if IsValid(self.alt_label) then + self.alt_label:Remove() + end + end + return + end + if str == "" then return end + self.alt_label = vgui.Create("DLabel", self) + self.alt_label:SetText("<" .. L(str) .. ">") + if pace.special_property_text_color then self.alt_label:SetTextColor(pace.special_property_text_color) end + self.alt_label:SetPos(60,-1) + self.alt_label:SetSize(200,20) + self.alt_label:SetFont(pace.CurrentFont) + end + pace.RegisterPanel(PANEL) end @@ -644,6 +681,16 @@ do -- list if ispanel(var) then pnl:SetContent(var) + pace.current_part["pac_property_panel_"..key] = var + + if key == "Hide" then + local reasons_hidden = pace.current_part:GetReasonsHidden() + if not table.IsEmpty(reasons_hidden) then + pnl:SetTooltip("Hidden by:" .. table.ToString(reasons_hidden, "", true)) + pnl:CreateAlternateLabel("hidden") + end + pace.current_part.hide_property_pnl = var + end end self.left:AddItem(btn) @@ -1057,6 +1104,7 @@ do -- base editable end function PANEL:Init(...) + self.pac_property_panel = self if DLabel and DLabel.Init then local status = DLabel.Init(self, ...) self:SetText('') @@ -1093,7 +1141,12 @@ do -- base editable end self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text) - if str == "" then self:SetTextColor(Color(160,0,80)) end + if str == "" or self.used_by_proxy then + if pace.special_property_text_color then + self:SetTextColor(pace.special_property_text_color) + end + end + self:SetFont(pace.CurrentFont) self:SetText(" " .. string.Trim(str,"\n")) -- ugh self:SizeToContents() @@ -2094,6 +2147,11 @@ do -- vector self.middle = middle self.right = right + self.pac_property_panel = self + left.pac_property_panel = self + middle.pac_property_panel = self + right.pac_property_panel = self + if self.MoreOptionsLeftClick then local btn = vgui.Create("DButton", self) btn:SetSize(16, 16) @@ -2411,6 +2469,7 @@ do -- number end function PANEL:OnCursorMoved() + if self.used_by_proxy then self:SetCursor("no") return end self:SetCursor("sizens") end @@ -2528,6 +2587,11 @@ do -- boolean self.chck:Toggle() self.chck:Toggle() self.lbl:SetText(L(tostring(b))) + if self.used_by_proxy then + if pace.special_property_text_color then + self.lbl:SetTextColor(pace.special_property_text_color) + end + end self.during_change = false end diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua index 4b58b66a6..d49512460 100644 --- a/lua/pac3/editor/client/popups_part_tutorials.lua +++ b/lua/pac3/editor/client/popups_part_tutorials.lua @@ -339,7 +339,10 @@ function pac.InfoPopup(str, tbl, x, y) elseif tbl.obj_type == "screen" then self:SetPos(x,y) - elseif tbl.obj_type == "cursor" then + --[[elseif tbl.obj_type == "cursor" then + self:SetPos(input.GetCursorPos())]] + + elseif tbl.obj_type == "tracking cursor" then self:SetPos(input.GetCursorPos()) elseif tbl.obj_type == "menu bar" then @@ -352,6 +355,9 @@ function pac.InfoPopup(str, tbl, x, y) end + if tbl.obj_type == "cursor" then + pnl:SetPos(input.GetCursorPos()) + end if tbl then pnl.tbl = tbl diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 53136ade9..112be90de 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -693,6 +693,9 @@ function pace.DoShortcutFunc(action) elseif popup_prefered_type == "cursor" then pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, input.GetCursorPos()) + elseif popup_prefered_type == "tracking cursor" then + pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, input.GetCursorPos()) + elseif popup_prefered_type == "menu bar" then popup_setup_tbl.obj = pace.Editor pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl) From 2cfd7e2ab5658721ef48826ae319ab63a9f51eb6 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 31 Aug 2024 23:26:55 -0400 Subject: [PATCH 220/300] health modifier update absorb factor sends the update to server counted hits: extra healthbar is depleted at 1 unit per hit no overflow: extra healthbar forgives breakthrough damage (e.g. if I have 20 extra HP and get hit with 100 damage, the barrier breaks and nothing else happens) options to execute on show, on wear register the part uids another way in case of uid collisions if players share an outfit manually do the table encoding for the healthbar updates to something less atrocious because net.WriteTable is unnecessarily heavy in case people use the quick setups HUD maker, when removing the part, remove the default hooks that might be used in case the HUD is there forever --- .../core/client/parts/health_modifier.lua | 62 +++++++--- lua/pac3/editor/client/parts.lua | 15 ++- lua/pac3/extra/shared/net_combat.lua | 108 +++++++++++++----- 3 files changed, 138 insertions(+), 47 deletions(-) diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index 5a498ceb7..cba16beed 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -6,6 +6,8 @@ PART.Group = "combat" PART.Icon = "icon16/heart.png" BUILDER:StartStorableVars() + BUILDER:GetSet("ActivateOnShow", true) + BUILDER:GetSet("ActivateOnWear", true) BUILDER:SetPropertyGroup("Health") BUILDER:GetSet("ChangeHealth", false) @@ -14,11 +16,13 @@ BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("ExtraHpBars") BUILDER:GetSet("FollowHealthBars", true, {description = "whether changing the extra health bars should try to update them at the same time"}) - BUILDER:GetSet("HealthBars", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,100)) end}) - BUILDER:GetSet("BarsAmount", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) - BUILDER:GetSet("BarsLayer", 1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,15)) end}) - BUILDER:GetSet("AbsorbFactor", 0, {editor_onchange = function(self,num) return math.Clamp(num,-1,1) end}) + BUILDER:GetSet("HealthBars", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,100)) end, description = "Extra health bars taking damage before the main health.\nThey work as multiple bars for convenience. The total will be bars * amount."}) + BUILDER:GetSet("BarsAmount", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end, description = "Extra health bars taking damage before the main health.\nThey work as multiple bars for convenience. The total will be bars * amount."}) + BUILDER:GetSet("BarsLayer", 1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,15)) end, description = "The layer decides which bars get damaged first. Outer layers are bigger numbers."}) + BUILDER:GetSet("AbsorbFactor", 0, {editor_onchange = function(self,num) return math.Clamp(num,-1,1) end, description = "How much damage to extra health bars should carry over to the main health. 1 is ineffective, 0 is normal, -1 is a healing conversion."}) BUILDER:GetSet("HPBarsResetOnHide", false) + BUILDER:GetSet("CountedHits", false, {description = "Instead of a quantity of HP points, make counted hits as a number.\nIt will spend 1 unit of the healthbar per hit."}) + BUILDER:GetSet("NoOverflow", false, {description = "When shield breaks, remaining damage will be forgiven.\nIt won't affect the main health or any remaining healthbar."}) BUILDER:SetPropertyGroup("Armor") BUILDER:GetSet("ChangeArmor", false) @@ -26,15 +30,23 @@ BUILDER:StartStorableVars() BUILDER:GetSet("MaxArmor", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end}) BUILDER:SetPropertyGroup("DamageMultipliers") - BUILDER:GetSet("DamageMultiplier", 1) - BUILDER:GetSet("ModifierId", "") + BUILDER:GetSet("DamageMultiplier", 1, {description = "Damage multiplier to the hits you take. They stack but might not help with hardcoded addons that directly edit your HP or something."}) + BUILDER:GetSet("ModifierId", "", {description = "Putting an ID lets you update a damage multiplier from multiple health modifier parts so they don't stack, without using proxies."}) BUILDER:GetSet("MultiplierResetOnHide", false) BUILDER:EndStorableVars() -local part_UID_caches = {} +pac.healthmod_part_UID_caches = {} +--wait a minute can we just assume uids will be unique? what if people give each other pacs, the uids will be the same +local function register_UID(self, str, ply) + pac.healthmod_part_UID_caches[ply] = pac.healthmod_part_UID_caches[ply] or {} + pac.healthmod_part_UID_caches[ply][str] = self +end function PART:SendModifier(str) + --pac.healthmod_part_UID_caches[string.sub(self.UniqueID,1,8)] = self + register_UID(self, string.sub(self.UniqueID,1,8), self:GetPlayerOwner()) + if self:IsHidden() then return end if LocalPlayer() ~= self:GetPlayerOwner() then return end if not GetConVar("pac_sv_health_modifier"):GetBool() then return end @@ -46,8 +58,10 @@ function PART:SendModifier(str) if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end end - part_UID_caches[self.UniqueID] = self - if self.Name ~= "" then part_UID_caches[self.Name] = self end + --pac.healthmod_part_UID_caches[self.UniqueID] = self + register_UID(self, self.UniqueID, self:GetPlayerOwner()) + if self.Name ~= "" then pac.healthmod_part_UID_caches[self.Name] = self end + register_UID(self, self.Name, self:GetPlayerOwner()) if str == "MaxHealth" and self.ChangeHealth then net.Start("pac_request_healthmod") @@ -83,6 +97,8 @@ function PART:SendModifier(str) net.WriteUInt(self.BarsLayer, 4) net.WriteFloat(self.AbsorbFactor) net.WriteBool(self.FollowHealthBars) + net.WriteBool(self.CountedHits) + net.WriteBool(self.NoOverflow) net.SendToServer() elseif str == "all" then @@ -113,6 +129,12 @@ function PART:SetBarsLayer(val) self:UpdateHPBars() end +function PART:SetAbsorbFactor(val) + self.AbsorbFactor = val + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end + self:SendModifier("HealthBars") +end + function PART:SetMaxHealth(val) self.MaxHealth = val if pac.LocalPlayer ~= self:GetPlayerOwner() then return end @@ -138,7 +160,11 @@ function PART:SetDamageMultiplier(val) end function PART:OnRemove() - part_UID_caches = {} --we'll need this part removed from the cache + --pac.healthmod_part_UID_caches = {} --we'll need this part removed from the cache + register_UID(nil, string.sub(self.UniqueID,1,8), self:GetPlayerOwner()) + register_UID(nil, self.UniqueID, self:GetPlayerOwner()) + register_UID(nil, self.Name, self:GetPlayerOwner()) + if pac.LocalPlayer ~= self:GetPlayerOwner() then return end if util.NetworkStringToID( "pac_request_healthmod" ) == 0 then return end local found_remaining_healthmod = false @@ -172,10 +198,17 @@ function PART:OnRemove() net.WriteBool(false) net.SendToServer() end + + hook.Remove("HUDPaint", "extrahealth_total") + hook.Remove("HUDPaint", "extrahealth_"..self.UniqueID) + hook.Remove("HUDPaint", "extrahealth_layer_"..self.BarsLayer) end function PART:OnShow() - self:SendModifier("all") + if self.ExecuteOnShow then self:SendModifier("all") end +end +function PART:OnWorn() + if self.ExecuteOnWear then self:SendModifier("all") end end function PART:OnHide() @@ -205,6 +238,8 @@ end function PART:Initialize() self.healthbar_index = 0 + --pac.healthmod_part_UID_caches[string.sub(self.UniqueID,1,8)] = self + register_UID(nil, string.sub(self.UniqueID,1,8), self:GetPlayerOwner()) if not GetConVar("pac_sv_health_modifier"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("health modifiers are disabled on this server!") end end @@ -223,8 +258,9 @@ end concommand.Add("pac_healthbar", function(ply, cmd, args) local uid_or_name = args[1] local num = tonumber(args[3]) or 0 - if part_UID_caches[uid_or_name] ~= nil and args[2] ~= nil then - local part = part_UID_caches[uid_or_name] + pac.healthmod_part_UID_caches[ply] = pac.healthmod_part_UID_caches[ply] or {} + if pac.healthmod_part_UID_caches[ply][uid_or_name] ~= nil and args[2] ~= nil then + local part = pac.healthmod_part_UID_caches[ply][uid_or_name] uid = part.UniqueID local action = args[2] or "" diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index cd53cf4cc..24852c041 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2000,7 +2000,6 @@ do -- menu end_value:SetValue(tonumber(val) or val) end end - else start_value = pace.CreatePanel("properties_label", properties_pnl) properties_pnl:AddKeyValue("ERROR",start_value) end_value = pace.CreatePanel("properties_label", properties_pnl) properties_pnl:AddKeyValue("ERROR",end_value) @@ -2909,7 +2908,7 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) end):SetIcon("icon16/user.png") collapses:AddOption("collapse legs", function() local group = pac.CreatePart("group") group:SetParent(obj) - local right = obj + local right = pac.CreatePart("bone3") right:SetBone("left thigh") right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right thigh") local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left thigh") end):SetIcon("icon16/user.png") @@ -3320,8 +3319,8 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) end):SetIcon("icon16/text_align_center.png") elseif obj.ClassName == "health_modifier" then main:AddOption("setup HUD display for extra health (total)", function() - local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") - local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") + local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") cmd_on:SetExecuteOnWear(true) + local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") cmd_off:SetExecuteOnWear(false) cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75}) surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true}) local x = 50 @@ -3338,8 +3337,8 @@ end)]]) main:AddOption("setup HUD display for extra health (this part only)", function() local function setup() - local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") - local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") + local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") cmd_on:SetExecuteOnWear(true) + local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") cmd_off:SetExecuteOnWear(false) cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75}) surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true}) local x = 50 @@ -3360,8 +3359,8 @@ end)]]) end):SetIcon("icon16/application_xp_terminal.png") main:AddOption("setup HUD display for extra health (this layer)", function() - local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") - local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") + local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") cmd_on:SetExecuteOnWear(true) + local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") cmd_off:SetExecuteOnWear(false) cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75}) surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true}) local x = 50 diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 44f7ee392..46839a502 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -559,7 +559,7 @@ if SERVER then --ply.pac_healthbars[layer] --ply.pac_healthbars[layer][part_uid] = healthvalue - local function UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) + local function UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow, counted_hits, no_overflow) local existing_uidlayer = true local healthvalue = 0 if ply.pac_healthbars == nil then @@ -583,21 +583,33 @@ if SERVER then ply.pac_healtbar_uid_absorbfactor = ply.pac_healtbar_uid_absorbfactor or {} ply.pac_healtbar_uid_absorbfactor[part_uid] = absorbfactor + ply.pac_healtbar_uid_info = ply.pac_healtbar_uid_info or {} + ply.pac_healtbar_uid_info[part_uid] = { + absorb_factor = absorbfactor, + counted_hits = counted_hits, + no_overflow = no_overflow + } + if num == 0 then --remove ply.pac_healthbars[layer] = nil - ply.pac_healtbar_uid_absorbfactor[part_uid] = nil + ply.pac_healtbar_uid_info[part_uid].absorbfactor = nil elseif num > 0 then --add if follow or created - ply.pac_healthbars[layer][part_uid] = healthvalue - ply.pac_healtbar_uid_absorbfactor[part_uid] = absorbfactor + if follow or not existing_uidlayer then + ply.pac_healthbars[layer][part_uid] = healthvalue + ply.pac_healtbar_uid_info[part_uid].absorbfactor = absorbfactor + end end for checklayer,tbl in pairs(ply.pac_healthbars) do + local layertotal = 0 for uid,value in pairs(tbl) do + layertotal = layertotal + value if layer ~= checklayer and part_uid == uid then ply.pac_healthbars[checklayer][uid] = nil + if table.IsEmpty(ply.pac_healthbars[checklayer]) then ply.pac_healthbars[checklayer] = nil end end end + if layertotal == 0 then ply.pac_healthbars[checklayer] = nil end end - end local function UpdateHealthBarsFromCMD(ply, action, num, part_uid) @@ -678,7 +690,7 @@ if SERVER then BARS_COPY[layer][uid] = math.max(0, value - remaining_dmg) end - local absorbfactor = ply.pac_healtbar_uid_absorbfactor[uid] + local absorbfactor = ply.pac_healtbar_uid_info[uid].absorbfactor side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor remaining_dmg = math.max(0,remaining_dmg - value) @@ -707,36 +719,75 @@ if SERVER then for uid,value in pairs(ply.pac_healthbars[layer]) do --check the healthbars by uid if value > 0 then --skip 0 HP healthbars + local counted_hits_mode = ply.pac_healtbar_uid_info[uid].counted_hits - local remainder = math.max(0,remaining_dmg - ply.pac_healthbars[layer][uid]) - - local breakthrough_dmg = math.min(remaining_dmg, value) + local absorbfactor = ply.pac_healtbar_uid_info[uid].absorbfactor + local breakthrough_dmg - if remaining_dmg > value then --break through one of the uid clusters - surviving_layer = layer - 1 - ply.pac_healthbars[layer][uid] = 0 + if counted_hits_mode then + ply.pac_healthbars[layer][uid] = ply.pac_healthbars[layer][uid] - 1 + breakthrough_dmg = remaining_dmg + remaining_dmg = 0 else - ply.pac_healthbars[layer][uid] = math.max(0, value - remaining_dmg) - end + --local remainder = math.max(0,remaining_dmg - ply.pac_healthbars[layer][uid]) - local absorbfactor = ply.pac_healtbar_uid_absorbfactor[uid] - side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor + --if the dmg is more than health value, we will have a breakthrough damage + breakthrough_dmg = math.min(remaining_dmg, value) - remaining_dmg = math.max(0,remaining_dmg - value) + if remaining_dmg > value then --break through one of the uid clusters + surviving_layer = layer - 1 + ply.pac_healthbars[layer][uid] = 0 + else --subtracting the health now + ply.pac_healthbars[layer][uid] = math.max(0, value - remaining_dmg) + end + if ply.pac_healtbar_uid_info[uid].no_overflow then + remaining_dmg = 0 + breakthrough_dmg = 0 + else + remaining_dmg = math.max(0,remaining_dmg - value) + end + end + side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor end end end end - return remaining_dmg,surviving_layer,side_effect_dmg end local function SendUpdateHealthBars(target) if not target:IsPlayer() or not target.pac_healthbars then return end + local table_copy = {} + local layers = 0 + + for layer=0,15,1 do --ok so we're gonna compress it + if not target.pac_healthbars[layer] then continue end + local tbl = target.pac_healthbars[layer] + layers = layer + table_copy[layer] = {} + for uid, value in pairs(tbl) do + table_copy[layer][string.sub(uid, 1, 8)] = math.Round(value) + end + end + --PrintTable(table_copy) net.Start("pac_update_healthbars") net.WriteEntity(target) - net.WriteTable(target.pac_healthbars) + net.WriteUInt(layers, 4) + for i=0,layers,1 do + --PrintTable(table_copy) + if not table_copy[i] then + net.WriteBool(true)--skip + continue + elseif not table.IsEmpty(table_copy[i]) then + net.WriteBool(false)--data exists + end + net.WriteUInt(math.Clamp(table.Count(table_copy[i]),0,15), 4) + for uid, value in pairs(table_copy[i]) do + net.WriteString(uid) --partial UID was written before + net.WriteUInt(value,24) + end + end net.Broadcast() end @@ -757,6 +808,7 @@ if SERVER then local cumulative_mult = GatherDamageScales(target) dmginfo:ScaleDamage(cumulative_mult) + local pretotal_hp_value,prebuilt_tbl = GatherExtraHPBars(target) local remaining_dmg,surviving_layer,side_effect_dmg = GetHPBarDamage(target, dmginfo:GetDamage()) if IsValid(dmginfo:GetInflictor()) then @@ -766,20 +818,21 @@ if SERVER then end local total_hp_value,built_tbl = GatherExtraHPBars(target) - if surviving_layer == nil or total_hp_value == 0 or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult + if surviving_layer == nil or (total_hp_value == 0 and pretotal_hp_value == total_hp_value) or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult if cumulative_mult < 0 then target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(dmginfo:GetDamage()),0,target:GetMaxHealth()))) return true else dmginfo:SetDamage(remaining_dmg) - if target.pac_healthbars then SendUpdateHealthBars(target) end + --if target.pac_healthbars then SendUpdateHealthBars(target) end end else --shields = use the calculated cumulative side effect damage from each uid's related absorbfactor if side_effect_dmg < 0 then target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(side_effect_dmg),0,target:GetMaxHealth()))) + SendUpdateHealthBars(target) return true else dmginfo:SetDamage(side_effect_dmg + remaining_dmg) @@ -787,6 +840,7 @@ if SERVER then end end + end end) @@ -826,8 +880,8 @@ if SERVER then local pac_sv_damage_zone_allow_dissolve = GetConVar("pac_sv_damage_zone_allow_dissolve"):GetBool() local pac_sv_prop_protection = global_combat_prop_protection:GetBool() - local inflictor = dmg_info:GetInflictor() - local attacker = dmg_info:GetAttacker() + local inflictor = dmg_info:GetInflictor() or ply + local attacker = dmg_info:GetAttacker() or ply local kill = false --whether a kill was done local hit = false --whether a hit was done @@ -1097,9 +1151,9 @@ if SERVER then dmg_info2:IsBulletDamage(tbl.Bullet) dmg_info2:SetDamageForce(Vector(0,0,0)) - dmg_info2:SetAttacker(attacker) + if IsValid(attacker) then dmg_info2:SetAttacker(attacker) end - dmg_info2:SetInflictor(inflictor) + if IsValid(inflictor) then dmg_info2:SetInflictor(inflictor) end ent:TakeDamageInfo(dmg_info2) max_dmg = math.max(max_dmg, dmg_info2:GetDamage()) @@ -2491,8 +2545,10 @@ if SERVER then local layer = net.ReadUInt(4) local absorbfactor = net.ReadFloat() local follow = net.ReadBool() + local counted_hits = net.ReadBool() + local no_overflow = net.ReadBool() - UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow) + UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow, counted_hits, no_overflow) elseif action == "OnRemove" then if ply.pac_damage_scalings then From 63f9450361b733b8f9a7f2d1959090fd76471b3c Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 31 Aug 2024 23:30:04 -0400 Subject: [PATCH 221/300] hotfix the new healthbars update net receiver from last commit --- lua/pac3/core/client/parts/event.lua | 48 +++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 81e154bd9..42de802c7 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -4295,31 +4295,69 @@ do end -net.Receive("pac_update_healthbars", function() +net.Receive("pac_update_healthbars", function(len) + pac.healthmod_part_UID_caches = pac.healthmod_part_UID_caches or {} local ent = net.ReadEntity() - local tbl = net.ReadTable() - + if ent:EntIndex() == 0 then return end + pac.healthmod_part_UID_caches[ent] = pac.healthmod_part_UID_caches[ent] or {} if not IsValid(ent) then return end + local layers = net.ReadUInt(4) + --local tbl = net.ReadTable() + local tbl = {} + for i=0,layers,1 do + local skip = net.ReadBool() + if skip then continue end + tbl[i] = {} + local number_parts = net.ReadUInt(4) + for j=1,number_parts,1 do + local partial_uid = net.ReadString() + local value = net.ReadUInt(24) + + local cached_part = pac.healthmod_part_UID_caches[ent][partial_uid] + if cached_part then + tbl[i][cached_part.UniqueID] = value + end + + end + end + --PrintTable(tbl) ent.pac_healthbars = tbl - ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {} - ent.pac_healthbars_uidtotals = ent.pac_healthbars_uidtotals or {} + local previous_total = ent.pac_healthbars_total or 0 + local previous_totals_layers = ent.pac_healthbars_layertotals or {} + local previous_totals_uids = ent.pac_healthbars_uidtotals or {} + + ent.pac_healthbars_layertotals = {} + ent.pac_healthbars_uidtotals = {} ent.pac_healthbars_total = 0 + ent.pac_healthbars_total_updated = {time = CurTime(), delta = 0} + ent.pac_healthbars_layers_updated = {time = CurTime(), deltas = {}} + ent.pac_healthbars_uids_updated = {time = CurTime(), deltas = {}} for layer=15,0,-1 do --go progressively inward in the layers ent.pac_healthbars_layertotals[layer] = 0 if tbl[layer] then for uid,value in pairs(tbl[layer]) do --check the healthbars by uid + value = math.Round(value) --so apparently some damage sources like dynamite can deal fractional damage + --dynamites made a giga ugly mess with decimals ruining my hud display ent.pac_healthbars_uidtotals[uid] = value ent.pac_healthbars_layertotals[layer] = ent.pac_healthbars_layertotals[layer] + value ent.pac_healthbars_total = ent.pac_healthbars_total + value + + ent.pac_healthbars_uids_updated.deltas[uid] = (previous_totals_uids[uid] or 0) - value + local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) if IsValid(part) and part.UpdateHPBars then part:UpdateHPBars() end end + ent.pac_healthbars_layers_updated.deltas[layer] = (previous_totals_layers[layer] or 0) - (ent.pac_healthbars_layertotals[layer] or 0) else ent.pac_healthbars_layertotals[layer] = nil end end + --delta is actually -delta but whatever + ent.pac_healthbars_total_updated.delta = previous_total - ent.pac_healthbars_total + + end) From b18c64402eff182464f9e3e033e8d8943e4cfc39 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 31 Aug 2024 23:41:58 -0400 Subject: [PATCH 222/300] jiggle update restructure the property categories initial velocity clamp angles: restrict the rotation to some amount of degrees --- lua/pac3/core/client/parts/jiggle.lua | 65 ++++++++++++++++++++------- lua/pac3/editor/client/parts.lua | 3 ++ 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/lua/pac3/core/client/parts/jiggle.lua b/lua/pac3/core/client/parts/jiggle.lua index ec5585a27..ef8b89899 100644 --- a/lua/pac3/core/client/parts/jiggle.lua +++ b/lua/pac3/core/client/parts/jiggle.lua @@ -12,6 +12,9 @@ PART.Group = 'model' PART.Icon = 'icon16/chart_line.png' BUILDER:StartStorableVars() + + BUILDER:SetPropertyGroup("orientation") + BUILDER:SetPropertyGroup("dynamics") BUILDER:GetSet("Strain", 0.5, {editor_onchange = function(self, num) self.sens = 0.25 num = tonumber(num) @@ -19,22 +22,27 @@ BUILDER:StartStorableVars() end}) BUILDER:GetSet("Speed", 1) BUILDER:GetSet("ConstantVelocity", Vector(0, 0, 0)) - BUILDER:GetSet("LocalVelocity", true) - BUILDER:GetSet("JiggleAngle", true) - BUILDER:GetSet("JigglePosition", true) - - BUILDER:GetSet("ConstrainPitch", false) - BUILDER:GetSet("ConstrainYaw", false) - BUILDER:GetSet("ConstrainRoll", false) + BUILDER:GetSet("InitialVelocity", Vector(0, 0, 0)) + BUILDER:GetSet("LocalVelocity", true, {description = "Whether Constant Velocity and Initial Velocity should use the jiggle part's angles instead of being applied by world coordinates"}) + BUILDER:GetSet("ResetOnHide", false, {description = "Reinitializes the container's position to the base part position, and its speeds to 0, when the part is shown."}) + BUILDER:GetSet("Ground", false) - BUILDER:GetSet("ConstrainX", false) - BUILDER:GetSet("ConstrainY", false) - BUILDER:GetSet("ConstrainZ", false) + BUILDER:SetPropertyGroup("angles") + BUILDER:GetSet("JiggleAngle", true) + BUILDER:GetSet("ClampAngles", false, {description = "Restrict the angles so it can't go beyond a certain amount too far from the jiggle's base angle. Components are below"}) + BUILDER:GetSet("AngleClampAmount", Vector(180,180,180)) + BUILDER:GetSet("ConstrainPitch", false, {description = "Do not jiggle the angles on the pitch component\nThe pitch that will remain is the jiggle's current or initial global pitch.\nThat only resets when the part is created, or if reset on hide, when shown; Or, it starts moving again when you turn this off."}) + BUILDER:GetSet("ConstrainYaw", false, {description = "Do not jiggle the angles on the yaw component\nThe yaw that will remain is the jiggle's current or initial global yaw.\nThat only resets when the part is created, or if reset on hide, when shown; Or, it starts moving again when you turn this off."}) + BUILDER:GetSet("ConstrainRoll", false, {description = "Do not jiggle the angles on the roll component\nThe roll that will remain is the jiggle's current or initial global roll.\nThat only resets when the part is created, or if reset on hide, when shown; Or, it starts moving again when you turn this off."}) + BUILDER:SetPropertyGroup("position") + BUILDER:GetSet("JigglePosition", true) BUILDER:GetSet("ConstrainSphere", 0) BUILDER:GetSet("StopRadius", 0) - BUILDER:GetSet("Ground", false) - BUILDER:GetSet("ResetOnHide", false) + BUILDER:GetSet("ConstrainX", false, {description = "Do not jiggle on X position coordinates"}) + BUILDER:GetSet("ConstrainY", false, {description = "Do not jiggle on Y position coordinates"}) + BUILDER:GetSet("ConstrainZ", false, {description = "Do not jiggle on Z position coordinates"}) + BUILDER:EndStorableVars() local math_AngleDifference = math.AngleDifference @@ -63,6 +71,15 @@ function PART:OnShow() if self.ResetOnHide then self:Reset() end + + if not self.InitialVelocity then return end + local ang = self:GetWorldAngles() + if self.LocalVelocity then + self.vel = self.InitialVelocity.x * ang:Forward() + self.InitialVelocity.y * ang:Right() + self.InitialVelocity.z * ang:Up() + else + self.vel = self.InitialVelocity + end + end local inf, ninf = math.huge, -math.huge @@ -89,7 +106,7 @@ function PART:OnDraw() self.vel = self.vel or VectorRand() self.pos = self.pos or pos * 1 - if self.StopRadius ~= 0 and self.pos and self.pos:Distance(pos) < self.StopRadius then + if self.StopRadius ~= 0 and self.pos and self.pos:DistToSqr(pos) < (self.StopRadius * self.StopRadius) then self.vel = Vector() return end @@ -160,18 +177,36 @@ function PART:OnDraw() if not self.ConstrainPitch then self.angvel.p = self.angvel.p + math_AngleDifference(ang.p, self.ang.p) self.ang.p = math_AngleDifference(self.ang.p, self.angvel.p * -speed) + if self.ClampAngles then + local p_angdiff = math_AngleDifference(self.ang.p, ang.p) + if p_angdiff > self.AngleClampAmount.x or p_angdiff < -self.AngleClampAmount.x then + self.ang.p = ang.p + math.Clamp(p_angdiff,-self.AngleClampAmount.x,self.AngleClampAmount.x) + end + end self.angvel.p = self.angvel.p * self.Strain end if not self.ConstrainYaw then self.angvel.y = self.angvel.y + math_AngleDifference(ang.y, self.ang.y) - self.ang.y = math_AngleDifference(self.ang.y, self.angvel.y * -speed) + self.ang.y = math_AngleDifference(self.ang.y, self.angvel.y * -speed) + if self.ClampAngles then + local y_angdiff = math_AngleDifference(self.ang.y, ang.y) + if y_angdiff > self.AngleClampAmount.y or y_angdiff < -self.AngleClampAmount.y then + self.ang.y = ang.y + math.Clamp(y_angdiff,-self.AngleClampAmount.y,self.AngleClampAmount.y) + end + end self.angvel.y = self.angvel.y * self.Strain end if not self.ConstrainRoll then self.angvel.r = self.angvel.r + math_AngleDifference(ang.r, self.ang.r) - self.ang.r = math_AngleDifference(self.ang.r, self.angvel.r * -speed) + self.ang.r = math_AngleDifference(self.ang.r, self.angvel.r * -speed) + if self.ClampAngles then + local r_angdiff = math_AngleDifference(self.ang.r, ang.r) + if r_angdiff > self.AngleClampAmount.x or r_angdiff < -self.AngleClampAmount.z then + self.ang.r = ang.r + math.Clamp(r_angdiff,-self.AngleClampAmount.z,self.AngleClampAmount.z) + end + end self.angvel.r = self.angvel.r * self.Strain end else diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 24852c041..b7ddabc40 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -3392,6 +3392,9 @@ end)]]) particle:SetGravity(Vector(0,0,0)) end):SetIcon("icon16/water.png") elseif obj.ClassName == "jiggle" then + main:AddOption("Limit Angles", function() + obj:SetClampAngles(true) obj:SetAngleClampAmount(Vector(50,50,50)) + end):SetIcon("icon16/compress.png") local named_part = obj.Parent if pace.recently_substituted_movable_part then if pace.recently_substituted_movable_part.Parent == obj then From 5907080213dda20e8aa02e6c299374caa29d6934 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 31 Aug 2024 23:50:15 -0400 Subject: [PATCH 223/300] model: fix bodygroup collisions with submaterials when a bodygroup has the same name as a submaterial, rather than changing the underlying code by handling bodygroups as yet another set of separate dynamic properties, I'll just append an underscore at the start of the bodygroup name and mark it as an exception and ignore that underscore when processing the bodygroups in drawing --- lua/pac3/core/client/parts/model.lua | 51 ++++++++++++++++++---------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/lua/pac3/core/client/parts/model.lua b/lua/pac3/core/client/parts/model.lua index 420a73c7f..576d23aed 100644 --- a/lua/pac3/core/client/parts/model.lua +++ b/lua/pac3/core/client/parts/model.lua @@ -97,23 +97,6 @@ function PART:GetDynamicProperties() } end - for _, info in ipairs(ent:GetBodyGroups()) do - if info.num > 1 then - tbl[info.name] = { - key = info.name, - set = function(val) - local tbl = self:ModelModifiersToTable(self:GetModelModifiers()) - tbl[info.name] = val - self:SetModelModifiers(self:ModelModifiersToString(tbl)) - end, - get = function() - return self:ModelModifiersToTable(self:GetModelModifiers())[info.name] or 0 - end, - udata = {editor_onchange = function(self, num) return math.Clamp(math.Round(num), 0, info.num - 1) end, group = "bodygroups"}, - } - end - end - if ent:GetMaterials() and #ent:GetMaterials() > 1 then for i, name in ipairs(ent:GetMaterials()) do name = name:match(".+/(.+)") or name @@ -138,6 +121,30 @@ function PART:GetDynamicProperties() end end + for _, info in ipairs(ent:GetBodyGroups()) do + if info.num > 1 then + local bodygroup_name = info.name + local exception = tbl[info.name] ~= nil --trouble! an existing material competes with the bodygroup, we should try renaming it? + if exception then + bodygroup_name = "_" .. info.name + self.bodygroup_exceptions[info.name] = true + end + + tbl[bodygroup_name] = { + key = bodygroup_name, + set = function(val) + local tbl = self:ModelModifiersToTable(self:GetModelModifiers()) + tbl[bodygroup_name] = val + self:SetModelModifiers(self:ModelModifiersToString(tbl)) + end, + get = function() + return self:ModelModifiersToTable(self:GetModelModifiers())[bodygroup_name] or 0 + end, + udata = {editor_onchange = function(self, num) return math.Clamp(math.Round(num), 0, info.num - 1) end, group = "bodygroups"}, + } + end + end + return tbl end @@ -199,11 +206,18 @@ function PART:SetModelModifiers(str) if not owner:GetBodyGroups() then return end self.draw_bodygroups = {} + self.bodygroup_exceptions = self.bodygroup_exceptions or {} + local dyn_props = self:GetDynamicProperties() for i, info in ipairs(owner:GetBodyGroups()) do local val = tbl[info.name] - if val then + if self.bodygroup_exceptions[info.name] then + val = dyn_props["_"..info.name].get() table.insert(self.draw_bodygroups, {info.id, val}) + else + if val then + table.insert(self.draw_bodygroups, {info.id, val}) + end end end end @@ -290,6 +304,7 @@ function PART:Initialize() self.Owner:SetNoDraw(true) self.Owner.PACPart = self self.material_count = 0 + self.bodygroup_exceptions = {} end function PART:OnShow() From 821933983321537b8cc95ecc056cd16d1e2c92b4 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 1 Sep 2024 07:51:17 -0400 Subject: [PATCH 224/300] hotfix the luapad-based code panel will be published later --- lua/pac3/core/client/parts/proxy.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 0a1cc7765..6a2c521c2 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -31,7 +31,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("MultipleTargetParts", "", {description = "send output to multiple external partss.\npaste multiple UIDs or names here, separated by semicolons. With bulk select, you can select parts and right click to get that done quickly.."}) BUILDER:GetSetPart("OutputTargetPart", {hide_in_editor = true}) BUILDER:GetSet("AffectChildren", false) - BUILDER:GetSet("Expression", "", {description = "write math here. hit F1 for a tutorial or right click for examples.", editor_panel = "code_proxy"}) + BUILDER:GetSet("Expression", "", {description = "write math here. hit F1 for a tutorial or right click for examples."}) BUILDER:SetPropertyGroup("easy setup") BUILDER:GetSet("Input", "time", {enums = function(part) return part.Inputs end, description = "base (inner) function for easy setup\nin sin(time()) it is time"}) @@ -54,11 +54,11 @@ BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("extra expressions") BUILDER:GetSet("ExpressionOnHide", "", {description = "Math to apply once, when the proxy is hidden. It computes once, so it will not move."}) - BUILDER:GetSet("Extra1", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra1() or var1() to save space, or by another proxy as extra1(\"uid or name\") or var1(\"uid or name\")", editor_panel = "code_proxy"}) - BUILDER:GetSet("Extra2", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra2() or var2() to save space, or by another proxy as extra2(\"uid or name\") or var2(\"uid or name\")", editor_panel = "code_proxy"}) - BUILDER:GetSet("Extra3", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra3() or var3() to save space, or by another proxy as extra3(\"uid or name\") or var3(\"uid or name\")", editor_panel = "code_proxy"}) - BUILDER:GetSet("Extra4", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra4() or var4() to save space, or by another proxy as extra4(\"uid or name\") or var4(\"uid or name\")", editor_panel = "code_proxy"}) - BUILDER:GetSet("Extra5", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra5() or var5() to save space, or by another proxy as extra5(\"uid or name\") or var5(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra1", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra1() or var1() to save space, or by another proxy as extra1(\"uid or name\") or var1(\"uid or name\")"}) + BUILDER:GetSet("Extra2", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra2() or var2() to save space, or by another proxy as extra2(\"uid or name\") or var2(\"uid or name\")"}) + BUILDER:GetSet("Extra3", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra3() or var3() to save space, or by another proxy as extra3(\"uid or name\") or var3(\"uid or name\")"}) + BUILDER:GetSet("Extra4", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra4() or var4() to save space, or by another proxy as extra4(\"uid or name\") or var4(\"uid or name\")"}) + BUILDER:GetSet("Extra5", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra5() or var5() to save space, or by another proxy as extra5(\"uid or name\") or var5(\"uid or name\")"}) BUILDER:EndStorableVars() -- redirect From bb66a11d35d4f0edf2d82fa5724554c2428e3cb9 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 1 Sep 2024 12:01:35 -0400 Subject: [PATCH 225/300] fix variable names --- lua/pac3/core/client/parts/health_modifier.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index cba16beed..e9dfbefdf 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -205,10 +205,10 @@ function PART:OnRemove() end function PART:OnShow() - if self.ExecuteOnShow then self:SendModifier("all") end + if self.ActivateOnShow then self:SendModifier("all") end end function PART:OnWorn() - if self.ExecuteOnWear then self:SendModifier("all") end + if self.ActivateOnWear then self:SendModifier("all") end end function PART:OnHide() From 4f8176a2378185c77701676bbbc44c20ae1f8fe8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 1 Sep 2024 14:01:54 -0400 Subject: [PATCH 226/300] Update proxy.lua --- lua/pac3/core/client/parts/proxy.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 6a2c521c2..44092057c 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -1826,7 +1826,7 @@ local function set(self, part, x, y, z, children) container = property_pnl.middle property_pnl.middle.used_by_proxy = true local num = y or x or 0 - property_pnl.left:SetValue(math.Round(tonumber(num),4)) + property_pnl.middle:SetValue(math.Round(tonumber(num),4)) container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) end if self.using_z then From 7a504535905c1487d961ed5c99670f03ca105207 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 8 Sep 2024 17:33:26 -0400 Subject: [PATCH 227/300] handful of fixes validity checks conceal the underscore hackery on bodygroup exceptions in property labels rename dynamic property convar for consistency with where it's used elsewhere (proxy and base_part) revert to derma line/altline text colors if special text color is empty add some quicksetup presets to interpolator --- lua/pac3/core/client/parts/model.lua | 8 +- lua/pac3/editor/client/panels/properties.lua | 14 +++- lua/pac3/editor/client/parts.lua | 87 +++++++++++++++++++- lua/pac3/editor/client/settings.lua | 18 ++-- 4 files changed, 114 insertions(+), 13 deletions(-) diff --git a/lua/pac3/core/client/parts/model.lua b/lua/pac3/core/client/parts/model.lua index 576d23aed..8619a82ab 100644 --- a/lua/pac3/core/client/parts/model.lua +++ b/lua/pac3/core/client/parts/model.lua @@ -212,8 +212,12 @@ function PART:SetModelModifiers(str) for i, info in ipairs(owner:GetBodyGroups()) do local val = tbl[info.name] if self.bodygroup_exceptions[info.name] then - val = dyn_props["_"..info.name].get() - table.insert(self.draw_bodygroups, {info.id, val}) + if dyn_props["_"..info.name] then + val = dyn_props["_"..info.name].get() + end + if val then + table.insert(self.draw_bodygroups, {info.id, val}) + end else if val then table.insert(self.draw_bodygroups, {info.id, val}) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 43012b7e5..2c90fe4af 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -2,7 +2,7 @@ local L = pace.LanguageString local languageID = CreateClientConVar("pac_editor_languageid", 1, true, false, "Whether we should show the language indicator inside of editable text entries.") local favorites_menu_expansion = CreateClientConVar("pac_favorites_try_to_build_asset_series", "0", true, false) -local extra_dynamic = CreateClientConVar("pac_update_properties_dynamically", "1", true, false, "Whether proxies should refresh the properties, and some booleans may show more information.") +local extra_dynamic = CreateClientConVar("pac_special_property_update_dynamically", "1", true, false, "Whether proxies should refresh the properties, and some booleans may show more information.") local special_property_text_color = CreateClientConVar("pac_special_property_text_color", "160 0 80", true, false, "R G B color of special property text\npac_special_property_text_color \"\" will make it not change the color\nSpecial contexts like proxies and hidden parts can show a different color to show that changes are happening in real time.") pace.special_property_text_color = Color(160,0,80) @@ -426,7 +426,8 @@ do -- container if str == "" then return end self.alt_label = vgui.Create("DLabel", self) self.alt_label:SetText("<" .. L(str) .. ">") - if pace.special_property_text_color then self.alt_label:SetTextColor(pace.special_property_text_color) end + if pace.special_property_text_color then self.alt_label:SetTextColor(pace.special_property_text_color) + else self.alt_label:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text) end self.alt_label:SetPos(60,-1) self.alt_label:SetSize(200,20) self.alt_label:SetFont(pace.CurrentFont) @@ -664,8 +665,15 @@ do -- list end btn:SetValue(L((udata and udata.editor_friendly or key):gsub("%u", " %1"):lower()):Trim()) + if udata then + if udata.group == "bodygroups" then + if key[1] == "_" then --bodygroup exceptions + btn.lbl:SetText(key:sub(2,-1)) + end + end + end end - + if obj then btn.key_name = key btn.part_namepart_name = obj.ClassName diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index b7ddabc40..d8f6b0e1f 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2536,7 +2536,7 @@ local part_classes_with_quicksetups = { health_modifier = true, hitscan = true, jiggle = true, - + interpolated_multibone = true, } --those are more to configure a part into common setups, might involve creating other parts @@ -3395,7 +3395,8 @@ end)]]) main:AddOption("Limit Angles", function() obj:SetClampAngles(true) obj:SetAngleClampAmount(Vector(50,50,50)) end):SetIcon("icon16/compress.png") - local named_part = obj.Parent + local named_part = obj.Parent or obj + if not IsValid(named_part) then named_part = obj end if pace.recently_substituted_movable_part then if pace.recently_substituted_movable_part.Parent == obj then named_part = pace.recently_substituted_movable_part @@ -3415,6 +3416,88 @@ end)]]) local event = pac.CreatePart("event") event:SetParent(proxy) event:SetEvent("command") event:SetArguments("jiggle_anchor_"..str) end):SetIcon("icon16/anchor.png") + elseif obj.ClassName == "interpolated_multibone" then + main:AddOption("rough demo: create random nodes", function() + 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) + 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) + obj["SetNode"..i](obj,newnode) + newnode:SetPosition(VectorRand()*100) newnode:SetAngles(AngleRand()) newnode:SetBone(obj.Bone) + end + local proxy = pac.CreatePart("proxy") + proxy:SetParent(obj) proxy:SetVariableName("LerpValue") proxy:SetExpression("time()%6") + end):SetIcon("icon16/anchor.png") + main:AddOption("add node at camera (local head)", function() + if obj.Parent.ClassName == "group" and obj.Parent ~= obj:GetRootPart() then + obj.recent_parent = obj.Parent + end + if not obj.recent_parent then + local group = pac.CreatePart("group") + group:SetParent(obj.Parent) + obj:SetParent(group) + obj.recent_parent = group + end + local index = 1 + for i=1,20,1 do + if not IsValid(obj["Node"..i]) then --free slot? + index = i + break + end + end + local newnode = pac.CreatePart("model2") newnode:SetParent(obj.Parent) newnode:SetModel("models/empty.mdl") + local localpos, localang = WorldToLocal(pace.ViewPos, pace.ViewAngles, newnode:GetWorldPosition(), newnode:GetWorldAngles()) + newnode:SetNotes("recorded FOV : " .. math.Round(pace.ViewFOV)) + newnode:SetName("cam_node_"..index) + obj["SetNode"..index](obj,newnode) + newnode:SetPosition(localpos) newnode:SetAngles(localang) + end):SetIcon("icon16/camera.png") + main:AddOption("add node at camera (entity invalidbone)", function() + local index = 1 + for i=1,20,1 do + if not IsValid(obj["Node"..i]) then --free slot? + index = i + break + end + end + local newnode = pac.CreatePart("model2") + newnode:SetParent(obj:GetRootPart()) + + newnode:SetModel("models/empty.mdl") + newnode:SetBone("invalidbone") + local localpos, localang = WorldToLocal(pace.ViewPos, pace.ViewAngles, newnode:GetWorldPosition(), newnode:GetWorldAngles()) + newnode:SetNotes("recorded FOV : " .. math.Round(pace.ViewFOV)) + newnode:SetName("cam_node_"..index) newnode:SetBone("invalidbone") + obj["SetNode"..index](obj,newnode) + newnode:SetPosition(localpos) newnode:SetAngles(localang) + end):SetIcon("icon16/camera.png") + if #pace.BulkSelectList > 0 then + main:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set nodes (overwrite)", function() + for i=1,20,1 do + if pace.BulkSelectList[i] then + obj["SetNode"..i](obj,pace.BulkSelectList[i]) + else + obj["SetNode"..i](obj,nil) + end + end + pace.PopulateProperties(obj) + end):SetIcon("icon16/pencil_delete.png") + main:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set nodes (append)", function() + for i=1,20,1 do + if not IsValid(obj["Node"..i]) then --free slot? + index = i + break + end + end + for i,part in ipairs(pace.BulkSelectList) do + obj["SetNode"..(index + i - 1)](obj,part) + end + pace.PopulateProperties(obj) + end):SetIcon("icon16/pencil_add.png") + end end end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 55108337d..e6ff678e2 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -580,7 +580,7 @@ local function encode_table_to_file(str) file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) elseif str == "pac_editor_partmenu_layouts" then data = pace.operations_order - file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data)) + file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data, true)) elseif str == "pac_part_categories" then data = pace.partgroups file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) @@ -593,7 +593,7 @@ local function encode_table_to_file(str) end elseif str == "eventwheel_colors" then data = pace.command_colors or {} - file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data)) + file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data, true)) end end @@ -619,10 +619,16 @@ local function decode_table_from_file(str) pace.operations_order = util.JSONToTable(data) elseif str == "pac_part_categories" then - pace.partgroups = util.KeyValuesToTable(data) - + pace.partgroups = util.KeyValuesToTable(data, false, true) + elseif str == "eventwheel_colors" then - pace.command_colors = util.KeyValuesToTable(data) + if not util.JSONToTable(data) then + if not table.IsEmpty(util.KeyValuesToTable(data)) then + pace.command_colors = util.KeyValuesToTable(data) + end + else + pace.command_colors = util.JSONToTable(data) + end end @@ -1992,7 +1998,7 @@ function pace.ConfigureEventWheelMenu() mid_panel:Dock(FILL) local scr_pnl = vgui.Create("DScrollPanel", mid_panel) - scr_pnl:SetSize(490,800) + scr_pnl:Dock(FILL) scr_pnl:SetPos(0,45) local list = vgui.Create("DListLayout", scr_pnl) list:Dock(FILL) From 9a0a7167277a661bdb87dfef7a93f4d2fb8ed357 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 15 Sep 2024 01:26:39 -0400 Subject: [PATCH 228/300] adjustments for event target part mode fix quick link to parent in part's quick actions (it was using the editor friendly name) reorder event target part code flow so it doesn't affect the parent as well advise the user if they use a target part with affect children only --- lua/pac3/core/client/parts/event.lua | 42 +++++++++++++++------------- lua/pac3/editor/client/parts.lua | 2 +- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 42de802c7..906d09b46 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -3192,6 +3192,7 @@ end function PART:TriggerEvent(b) self.event_triggered = b -- event_triggered is just used for the editor + local override = IsValid(self.DestinationPart) if self.AffectChildrenOnly then if self.MultiTargetPart then @@ -3203,30 +3204,33 @@ function PART:TriggerEvent(b) child:SetEventTrigger(self, b) end end + if override then self:SetWarning("The Affect Children Only checkbox should perhaps be turned off, because you have chosen a targeted part") end else - if self.MultiTargetPart then - for _,part2 in ipairs(self.MultiTargetPart) do - if part2.SetEventTrigger then part2:SetEventTrigger(self, b) end + if override then --single target part mode + if IsValid(self.previousdestinationpart) then + if self.DestinationPart ~= self.previousdestinationpart then --when editing, if we change the destination part we need to reset the old one + self.previousdestinationpart:SetEventTrigger(self, false) + end end - else - local parent = self:GetParent() - if parent:IsValid() then - parent:SetEventTrigger(self, b) + self.DestinationPart:SetEventTrigger(self, b) + self.previousdestinationpart = self.DestinationPart + else --normal parent mode + if IsValid(self.previousdestinationpart) then + if self.DestinationPart ~= self.previousdestinationpart then --when editing, if we change the destination part we need to reset the old one + self.previousdestinationpart:SetEventTrigger(self, false) + end end - end - end - if IsValid(self.DestinationPart) then --target part. the proper one. - if IsValid(self.previousdestinationpart) then - if self.DestinationPart ~= self.previousdestinationpart then --once we change the destination part we need to reset the old one - self.previousdestinationpart:SetEventTrigger(self, false) + if self.MultiTargetPart then + for _,part2 in ipairs(self.MultiTargetPart) do + if part2.SetEventTrigger then part2:SetEventTrigger(self, b) end + end + else + local parent = self:GetParent() + if parent:IsValid() then + parent:SetEventTrigger(self, b) + end end end - (self.DestinationPart):SetEventTrigger(self, b) - self.previousdestinationpart = (self.DestinationPart) - elseif IsValid(self.previousdestinationpart) then - if self.DestinationPart ~= self.previousdestinationpart then --once we change the destination part we need to reset the old one - self.previousdestinationpart:SetEventTrigger(self, false) - end end end diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index d8f6b0e1f..34f5f83fd 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -3634,7 +3634,7 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) end):SetIcon("icon16/star.png") end if not IsValid(obj.DestinationPart) then - menu:AddOption("engrave / quick-link to parent", function() obj:SetTargetedPart(obj:GetParent()) end):SetIcon("icon16/star.png") + menu:AddOption("engrave / quick-link to parent", function() obj:SetDestinationPart(obj:GetParent()) end):SetIcon("icon16/star.png") end end From 41a73ece6052697e67e184c0870ef51a445cc5dc Mon Sep 17 00:00:00 2001 From: Yagira Date: Mon, 16 Sep 2024 23:33:24 +0200 Subject: [PATCH 229/300] Add $allowdiffusemodulation --- lua/pac3/libraries/shader_params.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lua/pac3/libraries/shader_params.lua b/lua/pac3/libraries/shader_params.lua index 685a94df2..36d094ae4 100644 --- a/lua/pac3/libraries/shader_params.lua +++ b/lua/pac3/libraries/shader_params.lua @@ -624,6 +624,12 @@ return { default = false, description = "flag", }, + allowdiffusemodulation = { + type = "bool", + default = true, + friendly = "AllowDiffuseModulation", + description = "Prevents the material from being tinted.", + } }, ["bump map"] = { bumpmap = { From e9fb2238ca785dbb54531f99999f0c3d1b7af638 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 18 Sep 2024 19:42:39 -0400 Subject: [PATCH 230/300] hotfix --- lua/pac3/editor/client/parts.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 34f5f83fd..e2a282d4d 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -3280,17 +3280,13 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) end) end):SetIcon("icon16/calculator.png") lua_menu:AddOption("X-Ray hook (halos)", function() - obj:SetName("halos on") obj:SetString([["hook.Add("PreDrawHalos","xray_halos", function() halo.Add(ents.FindByClass("npc_combine_s"), Color(255,0,0), 5, 5, 5, true, true) end)"]]) - local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) obj:SetName("halos off") obj:SetString([["hook.Remove("PreDrawHalos","xray_halos")"]]) - obj:SetName("halos on") obj:SetString([["hook.Add("PreDrawHalos","xray_halos", function() halo.Add(ents.FindByClass("npc_combine_s"), Color(255,0,0), 5, 5, 5, true, true) end)"]]) - local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) obj:SetName("halos off") obj:SetString([["hook.Remove("PreDrawHalos","xray_halos")"]]) + obj:SetName("halos on") obj:SetString([[hook.Add("PreDrawHalos","xray_halos", function() halo.Add(ents.FindByClass("npc_combine_s"), Color(255,0,0), 5, 5, 5, true, true) end)]]) + local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) newobj:SetName("halos off") newobj:SetString([[hook.Remove("PreDrawHalos","xray_halos")]]) obj:SetUseLua(true) newobj:SetUseLua(true) end):SetIcon("icon16/shading.png") lua_menu:AddOption("X-Ray hook (ignorez)", function() - obj:SetName("ignoreZ on") obj:SetString([["hook.Add("PreDrawHalos","xray_halos", function() halo.Add(ents.FindByClass("npc_combine_s"), Color(255,0,0), 5, 5, 5, true, true) end)"]]) - local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) obj:SetName("halos off") obj:SetString([["hook.Remove("PreDrawHalos","xray_halos")"]]) - obj:SetName("ignoreZ off") obj:SetString([["hook.Add("PostDrawTranslucentRenderables","xray_ignorez", function() cam.IgnoreZ( true ) for i,ent in pairs(ents.FindByClass("npc_combine_s")) do ent:DrawModel() end cam.IgnoreZ( false ) end)"]]) - local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) obj:SetName("halos off") obj:SetString([["hook.Remove("PostDrawTranslucentRenderables","xray_ignorez")"]]) + obj:SetName("ignoreZ on") obj:SetString([[hook.Add("PostDrawTranslucentRenderables","xray_ignorez", function() cam.IgnoreZ( true ) for i,ent in pairs(ents.FindByClass("npc_combine_s")) do ent:DrawModel() end cam.IgnoreZ( false ) end)]]) + local newobj = pac.CreatePart("command") newobj:SetName("ignoreZ off") newobj:SetParent(obj.Parent) newobj:SetString([[hook.Remove("PostDrawTranslucentRenderables","xray_ignorez")]]) obj:SetUseLua(true) newobj:SetUseLua(true) end):SetIcon("icon16/shape_move_front.png") elseif obj.ClassName == "bone3" then From 7644e4f920a26541919a088ba91c01889a5e949b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 21 Sep 2024 17:50:23 -0400 Subject: [PATCH 231/300] Update net_combat.lua the help texts should've been one arg later --- lua/pac3/extra/shared/net_combat.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 46839a502..fb47afcba 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -2782,12 +2782,12 @@ if CLIENT then concommand.Add( "pac_stop_lock", function() net.Start("pac_signal_stop_lock") net.SendToServer() - end, "asks the server to breakup any lockpart hold on your player") + end, nil, "asks the server to breakup any lockpart hold on your player") concommand.Add( "pac_break_lock", function() net.Start("pac_signal_stop_lock") net.SendToServer() - end, "asks the server to breakup any lockpart hold on your player") + end, nil, "asks the server to breakup any lockpart hold on your player") net.Receive("pac_lock_imposecalcview", function() local authority_to_calcview = net.ReadBool() and GetConVar("pac_client_lock_camera_consent"):GetBool() From 04b4336d55de35c35bc586e218852c9128a93133 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 22 Sep 2024 16:19:42 -0400 Subject: [PATCH 232/300] projectile adjustments bump up some of the network-sent maximum bit depths supporting up to 80s delay and +/- 160 bounce --- lua/pac3/core/client/parts/projectile.lua | 8 ++++---- lua/pac3/extra/shared/projectiles.lua | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index 40a2276f5..d09a55c9c 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -24,7 +24,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("AddOwnerSpeed", false) BUILDER:GetSet("Spread", 0) BUILDER:GetSet("NumberProjectiles", 1) - BUILDER:GetSet("Delay", 0) + BUILDER:GetSet("Delay", 0, {editor_clamp = {0,80}}) BUILDER:GetSet("Maximum", 0) BUILDER:GetSet("RandomAngleVelocity", Vector(0,0,0)) BUILDER:GetSet("LocalAngleVelocity", Vector(0,0,0)) @@ -41,7 +41,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Collisions", true) BUILDER:GetSet("Sphere", false) BUILDER:GetSet("Radius", 1, {editor_panel = "projectile_radii"}) - BUILDER:GetSet("Bounce", 0) + BUILDER:GetSet("Bounce", 0, {editor_clamp = {-160,160}}) BUILDER:GetSet("Sticky", false) BUILDER:GetSet("CollideWithOwner", false) BUILDER:GetSet("CollideWithSelf", false) @@ -294,13 +294,13 @@ function PART:Shoot(pos, ang, multi_projectile_count) net.WriteInt(1000*self.Speed,18) net.WriteUInt(self.Maximum,7) net.WriteUInt(100*self.LifeTime,14) --might need decimals - net.WriteUInt(100*self.Delay,9) --might need decimals + net.WriteUInt(100*self.Delay,13) --might need decimals net.WriteUInt(self.Mass,16) net.WriteInt(100*self.Spread,10) net.WriteInt(100*self.Damping,20) --might need decimals net.WriteInt(self.Attract,14) net.WriteUInt(self.AttractRadius,10) - net.WriteInt(100*self.Bounce,8) --might need decimals + net.WriteInt(100*self.Bounce,15) --might need decimals net.SendToServer() else diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua index ea6890eae..cfcf0f14b 100644 --- a/lua/pac3/extra/shared/projectiles.lua +++ b/lua/pac3/extra/shared/projectiles.lua @@ -579,13 +579,13 @@ if SERVER then part.Speed = math.Clamp(net.ReadInt(18) / 1000, -pac_sv_projectile_max_speed:GetFloat(), pac_sv_projectile_max_speed:GetFloat()) part.Maximum = net.ReadUInt(7) part.LifeTime = net.ReadUInt(14) / 100 - part.Delay = net.ReadUInt(9) / 100 + part.Delay = net.ReadUInt(13) / 100 part.Mass = net.ReadUInt(16) part.Spread = net.ReadInt(10) / 100 part.Damping = net.ReadInt(20) / 100 part.Attract = net.ReadInt(14) part.AttractRadius = net.ReadUInt(10) - part.Bounce = net.ReadInt(8) / 100 + part.Bounce = net.ReadInt(15) / 100 local radius_limit = 2000 From 10f51cd4b7d5d6ff8f7456151b71f7c63ac2f40d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 25 Sep 2024 22:36:52 -0400 Subject: [PATCH 233/300] Update parts.lua this 2d text mode (with newline support) is not available on the text part yet --- 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 e2a282d4d..551d7c621 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2706,7 +2706,7 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) pace.Call("PartSelected", proxy) end):SetIcon("icon16/calculator_link.png") main:AddOption("quick large 2D text", function() - obj:SetDrawMode("DrawDrawText") + obj:SetDrawMode("SurfaceText") obj:SetFont("DermaLarge") end):SetIcon("icon16/text_letter_omega.png") main:AddOption("make HUD", function() From 4a70a721375f6bfb05a4143d8031fe3667059731 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 27 Sep 2024 21:43:49 -0400 Subject: [PATCH 234/300] color picker pick colors from the screen hold lshift to average the colors within a circle, useful when textures can have inconsistent coloring (noise), holes and other imperfections --- lua/pac3/editor/client/panels/properties.lua | 89 ++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 2c90fe4af..3541d40c9 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -2180,6 +2180,95 @@ do -- vector surface.SetDrawColor(self:GetSkin().Colours.Properties.Border) surface.DrawOutlinedRect(0,0,w,h) end + + --screen color picker + local btn2 = vgui.Create("DImageButton", self) + btn2:SetSize(16, 16) + btn2:Dock(RIGHT) btn2:DockPadding(0,0,16,0) + btn2:SetTooltip("Color picker") + btn2:SetImage("icon16/sitemap_color.png") + btn2.DoClick = function() + pace.FlashNotification("Hold Left Shift to open a circle to average nearby pixels") + local averaging_radius = 0 + local lock_x + local lock_y + pac.AddHook("DrawOverlay", "colorpicker", function() + render.CapturePixels() + local mx, my = input.GetCursorPos() + local cx = mx + local cy = my + + local r,g,b,a = render.ReadPixel(mx,my) + + --we may average on nearby pixels + if input.IsKeyDown(KEY_LSHIFT) then + lock_x = lock_x or mx + lock_y = lock_y or my + local dx = mx-lock_x + local dy = my-lock_y + + averaging_radius = math.floor(math.sqrt(dx^2 + dy^2)) + else + lock_x = nil + lock_y = nil + averaging_radius = 0 + end + if lock_x and lock_y then + cx = lock_x + cy = lock_y + end + local r_sum = 0 + local g_sum = 0 + local b_sum = 0 + + if averaging_radius > 0 then + local counted_pixels = 0 + for x=cx-averaging_radius,mx+averaging_radius,1 do + for y=cy-averaging_radius,my+averaging_radius,1 do + if x^2 + y^2 > averaging_radius^2 then + counted_pixels = counted_pixels + 1 + local r,g,b,a = render.ReadPixel(x,y) + r_sum = r_sum + r + g_sum = g_sum + g + b_sum = b_sum + b + end + end + end + if counted_pixels ~= 0 then + r = math.floor(r_sum / counted_pixels) + g = math.floor(g_sum / counted_pixels) + b = math.floor(b_sum / counted_pixels) + end + + if RealTime() % 0.2 > 0.1 then + surface.DrawCircle(cx, cy, averaging_radius, 0,0,0,255) + else + surface.DrawCircle(cx, cy, averaging_radius, 255,255,255,255) + end + draw.DrawText("(average color) radius = "..averaging_radius, "TargetID", cx + 15, cy + 15, picked_color, TEXT_ALIGN_LEFT) + end + + local color = Color(0,0,0) + if r + g + a < 400 then + color = Color(255,255,255) + end + + local picked_color = Color(r,g,b) + draw.RoundedBox(0,cx + 15,cy,120,18,color) + draw.DrawText(r .. " " .. g .. " " .. " " .. b, "TargetID", cx + 15, cy, picked_color, TEXT_ALIGN_LEFT) + if input.IsMouseDown(MOUSE_LEFT) then + pac.CopyValue(picked_color) + if pace.current_part.ProperColorRange then + self.OnValueChanged(Vector(picked_color.r/255,picked_color.g/255,picked_color.b/255)) + else + self.OnValueChanged(Vector(picked_color.r,picked_color.g,picked_color.b)) + end + + pac.RemoveHook("DrawOverlay", "colorpicker") + end + if input.IsKeyDown(KEY_ESCAPE) then pac.RemoveHook("DrawOverlay", "colorpicker") end + end) + end end end From 296b4094fbce1d881d3d1eb8cd67d656fe0dd8d3 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 28 Sep 2024 15:26:05 -0400 Subject: [PATCH 235/300] bulk select deploy command series : use equal operator apparently no one went beyond 9 in their command series to find out that the sequence being at 1 would also trigger a 10 --- lua/pac3/editor/client/parts.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 551d7c621..72524dc58 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -3867,6 +3867,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) str = string.gsub(str, " ", "") for i,v in ipairs(pace.BulkSelectList) do part = pac.CreatePart("event") + part:SetOperator("equal") part:SetParent(v) part.Event = "command" part.Arguments = str..i.."@@0@@0" From 107bffab362400bac1075ad4233cfec5a369e550 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Sep 2024 14:36:50 -0400 Subject: [PATCH 236/300] envmap fresnel --- lua/pac3/libraries/shader_params.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lua/pac3/libraries/shader_params.lua b/lua/pac3/libraries/shader_params.lua index 36d094ae4..2bec13b9f 100644 --- a/lua/pac3/libraries/shader_params.lua +++ b/lua/pac3/libraries/shader_params.lua @@ -159,6 +159,13 @@ return { } }, vertexlitgeneric = { + ["environment map"] = { + envmapfresnel = { + type = "float", + friendly = "Fresnel", + description = "like $fresnelreflection. requires phong.", + }, + }, wrinkle = { compress = { type = "texture", From 2abf3ee2f404b4c5334401ccbebdbb10ba289bfb Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 29 Sep 2024 22:16:03 -0400 Subject: [PATCH 237/300] changes to pac_event commands and events move pac_event_sequenced command to serverside instead of ping-pong networking command to define sequence bounds manually add random mode to pac_event to activate single-shots randomly outsource some button code to part pool more caching for lockpart and damagezone-related events add healthmod_bar_hit event add or_gate event --- lua/pac3/core/client/part_pool.lua | 49 +++- lua/pac3/core/client/parts/event.lua | 339 ++++++++++++++++----------- lua/pac3/core/server/event.lua | 126 ++++++++-- 3 files changed, 349 insertions(+), 165 deletions(-) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 04146425a..38abd57ef 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -645,16 +645,47 @@ function pac.EnablePartsByClass(classname, enable) end end +function pac.LinkSpecialTrackedPartsForEvent(part, ply) + part.erroring_cached_parts = {} + part.found_cached_parts = {} + + part.specialtrackedparts = {} + local tracked_classes = { + ["damage_zone"] = true, + ["lock"] = true + } + for _,part2 in pairs(all_parts) do + if ply == part2:GetPlayerOwner() and tracked_classes[part.ClassName] then + table.insert(part.specialtrackedparts,part2) + end + 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 +function pac.RegisterPartToCache(ply, name, part, remove) + ply["pac_part_cache_"..name] = ply["pac_part_cache_"..name] or {} + if remove then + ply["pac_part_cache_"..name][part] = nil + else + ply["pac_part_cache_"..name][part] = part + end +end + function pac.UpdateButtonEvents(ply, key, down) - for _,part in pairs(all_parts) do - if part:GetPlayerOwner() == ply and part.ClassName == "event" and part.Event == "button" then - part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {} - part.holdtime = part.holdtime or 0 - part.toggleimpulsekey = part.toggleimpulsekey or {} - part.toggleimpulsekey[key] = down - part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 - ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0 - part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime + local button_events = ply.pac_part_cache_button_events or {} + for _,part in pairs(button_events) do + if key ~= string.Split(part.Arguments, "@@")[1]:lower() then continue end + part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {} + part.toggleimpulsekey = part.toggleimpulsekey or {} + part.toggleimpulsekey[key] = down + part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 + ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0 + part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime + + if part.togglestate == nil then part.togglestate = false end + + if part.toggleimpulsekey[key] then + part.togglestate = not part.togglestate end end end diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 906d09b46..3be3896d1 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -51,7 +51,7 @@ function PART:register_command_event(str,b) local event = str local flush = b - local num = tonumber(string.sub(event, string.find(event,"[%d]+$") or 0)) or 0 + local num = tonumber(string.sub(event, string.find(event,"[%d]+$") or 0)) if string.find(event,"[%d]+$") then event = string.gsub(event,"[%d]+$","") @@ -62,13 +62,34 @@ function PART:register_command_event(str,b) ply.pac_command_event_sequencebases[event] = nil return end + local data = ply.pac_command_event_sequencebases[event] or {name = event} + local min = data.min or 1 + local max = data.max or 1 + if num then + if num < min then min = num end + if num > max then max = num end + end + + if min then data.min = min end + if max then data.max = max end - if ply.pac_command_event_sequencebases[event] and string.find(str,"[%d]+$") then - ply.pac_command_event_sequencebases[event].max = math.max(ply.pac_command_event_sequencebases[event].max,num) + if data and string.find(str,"[%d]+$") then + event_series_bounds[event] = event_series_bounds[event] or {} + event_series_bounds[event][1] = event_series_bounds[event][1] or min + event_series_bounds[event][1] = math.min(event_series_bounds[event][1], min) + event_series_bounds[event][2] = event_series_bounds[event][2] or max + event_series_bounds[event][2] = math.max(event_series_bounds[event][2], max) else - ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = num} + data = {name = event} end - + timer.Simple(0.3, function() + if stop_timer then return end + stop_timer = true + net.Start("pac_event_define_sequence_bounds") + net.WriteTable(event_series_bounds) + net.SendToServer() + end) + timer.Simple(0.5, function() stop_timer = false end) end function PART:fix_event_operator() @@ -128,26 +149,44 @@ function PART:AttachEditorPopup(str) self:SetupEditorPopup(str, true) end +local tracked_events = { + damage_zone_hit = true, + damagezone_kill = true, + lockpart_grabbing = true +} function PART:SetEvent(event) local reset = (self.Arguments == "") or (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) local owner = self:GetPlayerOwner() - if owner == pac.LocalPlayer then + if (owner == pac.LocalPlayer) and (not pace.processing) then if event == "command" then owner.pac_command_events = owner.pac_command_events or {} end - if not self.Events[event] then --invalid event? try a command event - if GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then + if not self.Events[event] then --invalid event? try a command event or button event + if pac.key_enums_reverse[event] then timer.Simple(0.2, function() if not self.pace_properties or self ~= pace.current_part then return end --now we'll use event as a command name - self:SetEvent("command") - self.pace_properties["Event"]:SetValue("command") + self:SetEvent("button") + self.pace_properties["Event"]:SetValue("button") self:SetArguments(event .. "@@0") self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") pace.PopulateProperties(self) end) return + else + if GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then + timer.Simple(0.2, function() + if not self.pace_properties or self ~= pace.current_part then return end + --now we'll use event as a command name + self:SetEvent("command") + self.pace_properties["Event"]:SetValue("command") + self:SetArguments(event .. "@@0") + self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") + pace.PopulateProperties(self) + end) + return + end end end end @@ -170,6 +209,11 @@ function PART:SetEvent(event) if not GetConVar("pac_editor_remember_divider_height"):GetBool() and IsValid(pace.Editor) then pace.Editor.div:SetTopHeight(ScrH() - 520) end end + --caching for some events + pac.RegisterPartToCache(owner, "button_events", self, event ~= "button") + if tracked_events[event] then + timer.Simple(1, function() pac.LinkSpecialTrackedPartsForEvent(self, owner) end) + end end function PART:Initialize() @@ -2218,9 +2262,9 @@ PART.OldEvents = { callback = function(self, ent, time, damage, uid) uid = uid or "" uid = string.gsub(uid, "\"", "") - local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then - for _,part in pairs(pac.GetLocalParts()) do + --for _,part in pairs(pac.GetLocalParts()) do + for _,part in ipairs(self.specialtrackedparts) do if part.ClassName == "damage_zone" then if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then if part.dmgzone_hit_done + time > CurTime() then @@ -2229,19 +2273,21 @@ PART.OldEvents = { end end end - elseif not valid_uid and err then - self:SetError("invalid part Unique ID\n"..err) - elseif valid_uid then - local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) - if part.ClassName == "damage_zone" then - if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then - if part.dmgzone_hit_done + time > CurTime() then - return true + else + local part = self:GetOrFindCachedPart(uid) + if not IsValid(part) then + self:SetError("invalid part Unique ID\n"..uid) + else + if part.ClassName == "damage_zone" then + if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then + if part.dmgzone_hit_done + time > CurTime() then + return true + end end + self:SetError() + else + self:SetError("You set a UID that's not a damage zone!") end - self:SetError() - else - self:SetError("You set a UID that's not a damage zone!") end end return false @@ -2266,9 +2312,9 @@ PART.OldEvents = { callback = function(self, ent, time, uid) uid = uid or "" uid = string.gsub(uid, "\"", "") - local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then - for _,part in pairs(pac.GetLocalParts()) do + --for _,part in pairs(pac.GetLocalParts()) do + for _,part in ipairs(self.specialtrackedparts) do if part.ClassName == "damage_zone" then if part.dmgzone_kill_done then if part.dmgzone_kill_done + time > CurTime() then @@ -2277,19 +2323,21 @@ PART.OldEvents = { end end end - elseif not valid_uid and err then - self:SetError("invalid part Unique ID\n"..err) - elseif valid_uid then - local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) - if part.ClassName == "damage_zone" then - if part.dmgzone_kill_done then - if part.dmgzone_kill_done + time > CurTime() then - return true + else + local part = self:GetOrFindCachedPart(uid) + if not IsValid(part) then + self:SetError("invalid part Unique ID\n"..uid) + else + if part.ClassName == "damage_zone" then + if part.dmgzone_kill_done then + if part.dmgzone_kill_done + time > CurTime() then + return true + end end + self:SetError() + else + self:SetError("You set a UID that's not a damage zone!") end - self:SetError() - else - self:SetError("You set a UID that's not a damage zone!") end end return false @@ -2323,24 +2371,27 @@ PART.OldEvents = { uid = string.gsub(uid, "\"", "") local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then - for _,part in pairs(pac.GetLocalParts()) do + --for _,part in pairs(pac.GetLocalParts()) do + for _,part in ipairs(self.specialtrackedparts) do if part.ClassName == "lock" then if part.grabbing then return IsValid(part.target_ent) end end end - elseif not valid_uid and err then - self:SetError("invalid part Unique ID\n"..err) - elseif valid_uid then - local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid) - if part.ClassName == "lock" then - if part.grabbing then - return IsValid(part.target_ent) - end - self:SetError() + else + local part = self:GetOrFindCachedPart(uid) + if not IsValid(part) then + self:SetError("invalid part Unique ID\n"..uid) else - self:SetError("You set a UID that's not a lock part!") + if part.ClassName == "lock" then + if part.grabbing then + return IsValid(part.target_ent) + end + self:SetError() + else + self:SetError("You set a UID that's not a lock part!") + end end end return false @@ -2440,6 +2491,68 @@ PART.OldEvents = { end }, + healthmod_bar_hit = { + operator_type = "number", preferred_operator = "above", + arguments = {{amount = "number"}, {layer = "string"}, {uid = "string"}, {time = "number"}}, + userdata = {{default = 0}, {default = ""}, {default = ""}, {default = 1}}, + callback = function(self, ent, amount, layer, uid, time) + if not ent.pac_healthbars or not ent.pac_healthbars_total_updated then return false end + local check_layer = layer ~= "" + layer = tonumber(layer) + local check_uid = uid ~= "" + --[[ + ent.pac_healthbars_total_updated = {time = CurTime(), delta = 0} + ent.pac_healthbars_layers_updated = {time = CurTime(), deltas = {}} + ent.pac_healthbars_uids_updated = {time = CurTime(), deltas = {}} + ]] + if not check_layer and not check_uid then + if ent.pac_healthbars_total_updated.time + time > CurTime() then + return self:NumberOperator(ent.pac_healthbars_total_updated.delta, amount) + end + elseif check_layer and (layer ~= nil) and ent.pac_healthbars_layers_updated.deltas[layer] then + if ent.pac_healthbars_layers_updated.time + time > CurTime() then + return self:NumberOperator(ent.pac_healthbars_layers_updated.deltas[layer], amount) + end + elseif check_uid and ent.pac_healthbars_uids_updated.deltas[uid] then + if ent.pac_healthbars_uids_updated.time + time > CurTime() then + return self:NumberOperator(ent.pac_healthbars_uids_updated.deltas[uid], amount) + end + end + return false + end + }, + + or_gate = { + operator_type = "none", preferred_operator = "find simple", + tutorial_explanation = "combines multiple events into an OR gate, the event will activate as soon as one of the events listed is activated (taking inverts into account)", + arguments = {{uids = "string"}}, + userdata = {{enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + for i, part in pairs(parts) do + if part.ClassName == "event" then + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID + end + end + return output + end}}, + callback = function(self, ent, uids) + if uids == "" then return false end + local uid_splits = string.Split(uids, ";") + for i,uid in ipairs(uid_splits) do + local part = self:GetOrFindCachedPart(uid) + if part then + local b = part.event_triggered + if part.Invert then b = not b end + if b then + return true + end + end + end + return false + end, + } + } @@ -2500,6 +2613,7 @@ do end pac.key_enums = enums + pac.key_enums_reverse = enums2 --@note button broadcast @@ -2528,8 +2642,8 @@ do end end end - pac.key_enums = enums + pac.key_enums_reverse = enums2 end key = pac.key_enums[key] or key @@ -2575,30 +2689,15 @@ do return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")" end, callback = function(self, ent, button, holdtime, toggle) - - local holdtime = holdtime or 0 + self.holdtime = holdtime or 0 local toggle = toggle or false - self.togglestate = self.togglestate or false - self.holdtime = holdtime - self.toggle = toggle - - self.toggleimpulsekey = self.toggleimpulsekey or {} - - if self.toggleimpulsekey[button] then - self.togglestate = not self.togglestate - self.toggleimpulsekey[button] = false - end - --print(button, "hold" ,self.holdtime) local ply = self:GetPlayerOwner() self.pac_broadcasted_buttons_holduntil = self.pac_broadcasted_buttons_holduntil or {} - if ply == pac.LocalPlayer then - ply.pac_broadcast_buttons = ply.pac_broadcast_buttons or {} - if not ply.pac_broadcast_buttons[button] then local val = enums2[button:lower()] if val then @@ -2608,20 +2707,14 @@ do end ply.pac_broadcast_buttons[button] = true end - - --print(button, ply.pac_broadcasted_buttons_holduntil[button], ply.pac_broadcast_buttons[button]) - --PrintTable(ply.pac_broadcast_buttons) - --PrintTable(self.pac_broadcasted_buttons_holduntil) end local buttons = ply.pac_buttons self.pac_broadcasted_buttons_holduntil[button] = self.pac_broadcasted_buttons_holduntil[button] or SysTime() - --print(button, self.toggle, self.togglestate) - --print(button,"until",self.pac_broadcasted_buttons_holduntil[button]) + if buttons then - --print("trying to compare " .. SysTime() .. " > " .. self.pac_broadcasted_buttons_holduntil[button] - 0.05) - if self.toggle then + if toggle then return self.togglestate elseif self.holdtime > 0 then return SysTime() < self.pac_broadcasted_buttons_holduntil[button] @@ -3188,6 +3281,7 @@ function PART:OnRemove() self.DestinationPart.active_events_ref_count = self.DestinationPart.active_events_ref_count - 1 self.DestinationPart:CalcShowHide() end + pac.RegisterPartToCache(self:GetPlayerOwner(), "button_events", self, true) end function PART:TriggerEvent(b) @@ -3519,82 +3613,47 @@ concommand.Add("pac_print_events", function(ply) PrintTable(ply.pac_command_events) end) -concommand.Add("pac_event_sequenced", function(ply, cmd, args) - - if not args[1] then return end - - local event = args[1] - local action = args[2] or "+" - local sequence_number = 0 - local set_target = args[3] or 1 - local found = false - +local sequence_verbosity = CreateConVar("pac_event_sequenced_verbosity", 1, FCVAR_ARCHIVE, "whether to print info when running pac_event_sequenced") +net.Receive("pac_event_set_sequence", function(len) + local ply = net.ReadEntity() + local event = net.ReadString() + local num = net.ReadUInt(8) ply.pac_command_events = ply.pac_command_events or {} - ply.pac_command_events[event..1] = ply.pac_command_events[event..1] or {name = event..1, time = 0, on = 1} - ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} + ply.pac_command_event_sequencebases[event] = ply.pac_command_event_sequencebases[event] or {name = event} + data = ply.pac_command_event_sequencebases[event] + local previous_sequencenumber = data.current or 0 - if not ply.pac_command_event_sequencebases[event] then - ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = 1} + --assuming we don't know which parts of the series are active, nil out all of them before setting one + for i=0,100,1 do + ply.pac_command_events[event..i] = nil end - local target_number = 1 - local min = 1 - local max = ply.pac_command_event_sequencebases[event].max - - for i=1,100,1 do - if ply.pac_command_events[event..i] then - if ply.pac_command_events[event..i].on == 1 then - if sequence_number == 0 then sequence_number = i end - found = true - end - --elseif ply.pac_command_events[event..i] == nil then - ply.pac_command_events[event..i] = {name = event..i, time = 0, on = 0} + if data.min then + if num < data.min then data.min = num end + else data.min = num end + if data.max then + if num > data.max then data.max = num end + else data.max = num end + + if ply.pac_command_event_sequencebases then + if ply.pac_command_event_sequencebases[event] then + ply.pac_command_events[event..num] = {name = event..num, time = pac.RealTime, on = 1} + ply.pac_command_event_sequencebases[event].current = num end end + if sequence_verbosity:GetBool() and (ply == pac.LocalPlayer) then pac.Message("sequencing event series: " .. event .. "\n\t" .. previous_sequencenumber .. "->" .. num .. " / " .. data.max) end +end) - if found then - if action == "+" or action == "forward" or action == "add" or action == "sequence+" or action == "advance" then - - ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 0} - if sequence_number == max then - target_number = min - else target_number = sequence_number + 1 end - - pac.Message("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) - ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} - - RunConsoleCommand("pac_event", event..sequence_number, "0") - RunConsoleCommand("pac_event", event..target_number, "1") - - - elseif action == "-" or action == "backward" or action == "sub" or action == "sequence-" then - - ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 0} - if sequence_number == min then - target_number = max - else target_number = sequence_number - 1 end - - print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. target_number .. " / " .. max, "action: "..action) - ply.pac_command_events[event..target_number] = {name = event..target_number, time = pac.RealTime, on = 1} - - RunConsoleCommand("pac_event", event..sequence_number, "0") - RunConsoleCommand("pac_event", event..target_number, "1") - - elseif action == "set" then - print("sequencing event series: " .. event .. "\n\t" .. sequence_number .. "->" .. set_target .. " / " .. max) - - sequence_number = set_target or 1 - for i=1,100,1 do - ply.pac_command_events[event..i] = nil - end - ply.pac_command_events[event..sequence_number] = {name = event..sequence_number, time = pac.RealTime, on = 1} - target_number = set_target - net.Start("pac_event_set_sequence") - net.WriteString(event) - net.WriteUInt(sequence_number,8) - net.SendToServer() +net.Receive("pac_event_update_sequence_bounds", function(len) + local ply = net.ReadEntity() + local tbl = net.ReadTable() + for cmd, bounds in pairs(tbl) do + local current = 0 + if ply.pac_command_event_sequencebases[cmd] then + current = ply.pac_command_event_sequencebases[cmd].current end + ply.pac_command_event_sequencebases[cmd] = {min = bounds[1], max = bounds[2], current = current} end end) @@ -4362,6 +4421,4 @@ net.Receive("pac_update_healthbars", function(len) --delta is actually -delta but whatever ent.pac_healthbars_total_updated.delta = previous_total - ent.pac_healthbars_total - - end) diff --git a/lua/pac3/core/server/event.lua b/lua/pac3/core/server/event.lua index 3ce200749..b2ef2ded0 100644 --- a/lua/pac3/core/server/event.lua +++ b/lua/pac3/core/server/event.lua @@ -2,16 +2,8 @@ util.AddNetworkString("pac_proxy") util.AddNetworkString("pac_event") util.AddNetworkString("pac_event_set_sequence") - -net.Receive("pac_event_set_sequence", function(len, ply) - local event = net.ReadString() - local num = net.ReadUInt(8) - ply.pac_command_events = ply.pac_command_events or {} - for i=1,100,1 do - ply.pac_command_events[event..i] = nil - end - ply.pac_command_events[event..num] = {name = event, time = pac.RealTime, on = 1} -end) +util.AddNetworkString("pac_event_define_sequence_bounds") +util.AddNetworkString("pac_event_update_sequence_bounds") -- event concommand.Add("pac_event", function(ply, _, args) @@ -30,6 +22,15 @@ concommand.Add("pac_event", function(ply, _, args) extra = ply.pac_event_toggles[event] and 1 or 0 end + if args[2] == "random" then + local min = tonumber(args[3]) + local max = tonumber(args[4]) + if isnumber(min) and isnumber(max) then + local append = math.floor(math.Rand(tonumber(args[3]), tonumber(args[4]) + 1)) + event = event .. append + end + end + net.Start("pac_event", true) net.WriteEntity(ply) net.WriteString(event) @@ -38,7 +39,99 @@ concommand.Add("pac_event", function(ply, _, args) ply.pac_command_events = ply.pac_command_events or {} ply.pac_command_events[event] = ply.pac_command_events[event] or {} ply.pac_command_events[event] = {name = event, time = pac.RealTime, on = extra} -end, nil, "pac_event triggers command events. it needs at least one argument.\nname: any name, preferably without spaces\nmode: a number.\n\t0 turns off\n\t1 turns on\n\t2 toggles on/off\n\twithout a second argument, the event is a single-shot\n\ne.g. pac_event light 2") +end, nil, +[[pac_event triggers command events. it needs at least one argument. +name: any name, preferably without spaces +modes: + 0 turns off + 1 turns on + 2 toggles on/off + without a second argument, the event is a single-shot + random followed by two numbers will run a single-shot randomly, the name will be the base name with the number at the end + +e.g. + pac_event light 2 + pac_event attack random 1 3]]) + +net.Receive("pac_event_define_sequence_bounds", function(len, ply) + local bounds = net.ReadTable() + if bounds == nil then return end + for event,tbl in pairs(bounds) do + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} + local current_seq_value = 1 + if ply.pac_command_event_sequencebases[event] then + if ply.pac_command_event_sequencebases[event].current then + current_seq_value = ply.pac_command_event_sequencebases[event].current + end + end + ply.pac_command_event_sequencebases[event] = {name = event, min = tbl[1], max = tbl[2], current = current_seq_value} + end + net.Start("pac_event_update_sequence_bounds") net.WriteEntity(ply) net.WriteTable(bounds) net.Broadcast() +end) + +concommand.Add("pac_event_sequenced_force_set_bounds", function(ply, cmd, args) + local event = args[1] + local min = args[2] + local max = args[3] + ply.pac_command_event_sequencebases[event] = {name = event, min = tonumber(min), max = tonumber(max), current = 0} + net.Start("pac_event_update_sequence_bounds") net.WriteEntity(ply) net.WriteTable(bounds) net.Broadcast() +end) + +concommand.Add("pac_event_sequenced", function(ply, cmd, args) + if args[1] == nil then + ply:PrintMessage(HUD_PRINTCONSOLE, "\npac_event_sequenced needs at least one* argument.\nname: the base name of your series of events\naction: set, forward, backward\nnumber: if using the set mode, the number to set\n\ne.g. pac_event_sequenced hat_style set 3\n") + return + end + + local event = args[1] + local action = args[2] or "+" + local sequence_number = 0 + local set_target = tonumber(args[3]) or 1 + + ply.pac_command_events = ply.pac_command_events or {} + + local data + if ply.pac_command_event_sequencebases ~= nil then + if ply.pac_command_event_sequencebases[event] == nil then ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = 1} end + else + ply.pac_command_event_sequencebases = {} + ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = 1} + end + + data = ply.pac_command_event_sequencebases[event] + sequence_number = data.current + + local target_number = 1 + local min = data.min + local max = data.max + + sequence_number = tonumber(data.current) or 1 + if action == "+" or action == "forward" or action == "add" or action == "sequence+" or action == "advance" then + if sequence_number >= max then + target_number = min + else target_number = sequence_number + 1 end + data.current = target_number + + elseif action == "-" or action == "backward" or action == "sub" or action == "sequence-" then + if sequence_number <= min then + target_number = max + else target_number = sequence_number - 1 end + data.current = target_number + + elseif action == "set" then + sequence_number = set_target or 1 + target_number = set_target + data.current = target_number + else + ply:PrintMessage(HUD_PRINTCONSOLE, "\npac_event_sequenced : wrong action name. Valid action names are:\nforward, +, add, sequence+ or advance\nbackward, -, sub, or sequence-\nset") + return + end + net.Start("pac_event_set_sequence") + net.WriteEntity(ply) + net.WriteString(event) + net.WriteUInt(target_number,8) + net.Broadcast() +end) concommand.Add("+pac_event", function(ply, _, args) if not args[1] then @@ -62,7 +155,7 @@ concommand.Add("+pac_event", function(ply, _, args) net.WriteString(args[1] .. "_on") net.Broadcast() end -end, nil, "activates a command event when bound. \ne.g. \"+pac_event name\" will run \"pac_event name_on\" when the button is held, \"pac_event name_off\" when the button is held. Take note these are instant commands, they would need a command event with duration.\nmeanwhile, \"+pac_event name 2\" will run \"pac_event name 1\" when the button is held, \"pac_event name 0\" when the button is held. Take note these are held commands.") +end, nil, "activates a command event. more effective when bound. \ne.g. \"+pac_event name\" will run \"pac_event name_on\" when the button is held, \"pac_event name_off\" when the button is held. Take note these are instant commands, they would need a command event with duration.\nmeanwhile, \"+pac_event name 2\" will run \"pac_event name 1\" when the button is held, \"pac_event name 0\" when the button is held. Take note these are held commands.") concommand.Add("-pac_event", function(ply, _, args) if not args[1] then @@ -136,6 +229,9 @@ concommand.Add("pac_proxy", function(ply, _, args) net.WriteFloat(y or 0) net.WriteFloat(z or 0) net.Broadcast() - - --PrintTable(ply.pac_proxy_events[str]) -end, nil, "pac_proxy sets the number of a command function in a proxy. it is typically accessed as command(\"value\") needs at least two arguments.\nname: any name, preferably without spaces\nnumbers: a number, or a series of numbers for a vector. increment notation is available, such as ++1 and --1.\ne.g. pac_proxy myvector 1 2 3\ne.g. pac_proxy value ++5") +end, nil, +[[pac_proxy sets the number of a command function in a proxy. it is typically accessed as command(\"value\") needs at least two arguments. +name: any name, preferably without spaces +numbers: a number, or a series of numbers for a vector. increment notation is available, such as ++1 and --1. +e.g. pac_proxy myvector 1 2 3 +e.g. pac_proxy value ++5]]) From 22392c048b671433387ad9bda9af84ea83ea5f53 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 30 Sep 2024 21:15:59 -0400 Subject: [PATCH 238/300] some checks --- lua/pac3/core/client/parts/event.lua | 2 ++ lua/pac3/core/server/event.lua | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 3be3896d1..6a46d9f4f 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -3648,6 +3648,8 @@ end) net.Receive("pac_event_update_sequence_bounds", function(len) local ply = net.ReadEntity() local tbl = net.ReadTable() + if not ply:IsPlayer() then return end + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} for cmd, bounds in pairs(tbl) do local current = 0 if ply.pac_command_event_sequencebases[cmd] then diff --git a/lua/pac3/core/server/event.lua b/lua/pac3/core/server/event.lua index b2ef2ded0..9460a933e 100644 --- a/lua/pac3/core/server/event.lua +++ b/lua/pac3/core/server/event.lua @@ -70,11 +70,13 @@ net.Receive("pac_event_define_sequence_bounds", function(len, ply) end) concommand.Add("pac_event_sequenced_force_set_bounds", function(ply, cmd, args) + if args[1] == nil then return end local event = args[1] local min = args[2] local max = args[3] + ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {} ply.pac_command_event_sequencebases[event] = {name = event, min = tonumber(min), max = tonumber(max), current = 0} - net.Start("pac_event_update_sequence_bounds") net.WriteEntity(ply) net.WriteTable(bounds) net.Broadcast() + net.Start("pac_event_update_sequence_bounds") net.WriteEntity(ply) net.WriteTable(ply.pac_command_event_sequencebases) net.Broadcast() end) concommand.Add("pac_event_sequenced", function(ply, cmd, args) From 7b0294fdb57a1c0df4c2a755f3307a93ede33914 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 30 Sep 2024 21:43:23 -0400 Subject: [PATCH 239/300] fix force damping old velocity can differ vastly between the physobj and the ent for some reason --- lua/pac3/extra/shared/net_combat.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index fb47afcba..829869260 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1454,6 +1454,7 @@ if SERVER then if is_player then if tbl.Players or (ent == ply and tbl.AffectSelf) then if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then + oldvel = ent:GetVelocity() phys_ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) end From b8d92c9c5f28c745a974c19aa12432f87335a6d6 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 1 Oct 2024 17:33:32 -0400 Subject: [PATCH 240/300] small check --- lua/pac3/core/client/part_pool.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 38abd57ef..3dbbe6e72 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -680,6 +680,8 @@ function pac.UpdateButtonEvents(ply, key, down) part.toggleimpulsekey[key] = down part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0 ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0 + + if part.holdtime == nil then part.holdtime = 0 end part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime if part.togglestate == nil then part.togglestate = false end From a31f36eda833acefb38f9521181a0df93672dbfc Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 3 Oct 2024 17:17:58 -0400 Subject: [PATCH 241/300] small fix create the cache table on init (it was created after a timer, but the part thinks before that) --- lua/pac3/core/client/parts/event.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 6a46d9f4f..d62ef59f1 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -217,6 +217,7 @@ function PART:SetEvent(event) end function PART:Initialize() + self.specialtrackedparts = {} self.ExtraHermites = {} if self:GetPlayerOwner() == LocalPlayer() then timer.Simple(0.2, function() From d45e9265d1f4adf0301945e22f557964550b6f3b Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 3 Oct 2024 20:00:58 -0400 Subject: [PATCH 242/300] Update part_pool.lua --- lua/pac3/core/client/part_pool.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 3dbbe6e72..9164f5206 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -663,6 +663,7 @@ 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 function pac.RegisterPartToCache(ply, name, part, remove) + if not IsValid(ply) then return end ply["pac_part_cache_"..name] = ply["pac_part_cache_"..name] or {} if remove then ply["pac_part_cache_"..name][part] = nil From 04839a7d8e5d21c91e9a532648191b552b9ba633 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 8 Oct 2024 21:44:31 -0400 Subject: [PATCH 243/300] sit-related events: fix errors with sit anywhere script etc. just a nil check --- lua/pac3/core/client/parts/event.lua | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index d62ef59f1..ec6b8b613 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2050,13 +2050,9 @@ PART.OldEvents = { if IsValid(vehicle) then --vehicle entity exists if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent - --print(vehicle:GetParent().PassengerSeats) - if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys return ent:IsDrivingSimfphys() and ent:GetVehicle() == ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat - elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too - --print(vehicle:GetParent().BaseClass.ClassName, #vehicle:GetParent().Seats) - --PrintTable(vehicle:GetParent().Seats[1]) + elseif vehicle:GetParent().BaseClass and vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too return vehicle == vehicle.wac_seatswitcher.seats[1] --first seat end elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we don't want bare seats or prisoner pod @@ -2082,7 +2078,7 @@ PART.OldEvents = { if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys return ent:IsDrivingSimfphys() and ent:GetVehicle() ~= ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat - elseif vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too + elseif vehicle:GetParent().BaseClass and vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too return vehicle ~= vehicle.wac_seatswitcher.seats[1] --first seat end elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we can count bare seats and prisoner pods as passengers From 238102d3a55e7b893ce3e9ea4900b0b9f56e423c Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 8 Oct 2024 22:54:11 -0400 Subject: [PATCH 244/300] fix damage zone fire damage dissolving on kill delineate the "special case" damage types and prevent bad damage id assignments --- lua/pac3/extra/shared/net_combat.lua | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 829869260..0ec38b10c 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -233,6 +233,17 @@ if SERVER then heal = -1, armor = -1, } + local special_damagetypes = { + fire = true, -- ent:Ignite(5) + -- env_entity_dissolver + dissolve_energy = 0, + dissolve_heavy_electrical = 1, + dissolve_light_electrical = 2, + dissolve_core_effect = 3, + + heal = true, + armor = true, + } local when_to_print_messages = {} local can_print = {} @@ -1920,7 +1931,15 @@ if SERVER then local kill = false local hit = false - dmg_info:SetDamageType(damage_types[tbl.DamageType]) + if damage_types[tbl.DamageType] then + if special_damagetypes[tbl.DamageType] then + dmg_info:SetDamageType(0) + else + dmg_info:SetDamageType(damage_types[tbl.DamageType]) + end + else + dmg_info:SetDamageType(0) + end local ratio if tbl.Radius == 0 then ratio = tbl.Length From 670d77af87165e1fe6c2945fac973a0ded760c19 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 13 Oct 2024 18:54:39 -0400 Subject: [PATCH 245/300] Update event.lua revert previous change and make alternate wiring modes (affect children, target part, multi target part) non-competing add descriptions if typing a number in event field, create a timerx --- lua/pac3/core/client/parts/event.lua | 108 ++++++++++++++------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index ec6b8b613..eaaff5213 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -28,16 +28,16 @@ BUILDER:StartStorableVars() end return output - end}) - BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end}) - BUILDER:GetSet("Arguments", "", {hidden = false}) - BUILDER:GetSet("Invert", true) + end, description = "The type of condition used to determine whether to hide or show parts.\nCommon events are button, command, timer, timerx, is_on_ground, health_lost, is_touching"}) + BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end, description = "How the event will compare its source data with your reference value. PAC will try automatically pick an appropriate operator based on the event.\n\nfind and find simple searches for a keyword match (applies to text only).\nequal looks for an exact match (applies for text and numbers)\nabove, below etc are number comparators and should be self-explanatory.\nmaybe does a coin flip ignoring everything"}) + BUILDER:GetSet("Arguments", "", {hidden = false, description = "The internal text representation of the event's arguments, how it gets saved.\nThe dynamic fields access that very same thing, but a text field is useful to review and copy all the arguments at once."}) + BUILDER:GetSet("Invert", true, {description = "invert: show when condition is met\nuninverted: hide when condition is met"}) BUILDER:GetSet("RootOwner", true) - BUILDER:GetSet("AffectChildrenOnly", false) + BUILDER:GetSet("AffectChildrenOnly", false, {description = "Instead of the parent, the event's children will be affected instead"}) BUILDER:GetSet("ZeroEyePitch", false) - BUILDER:GetSetPart("TargetPart", {editor_friendly = "ExternalOriginPart"}) - BUILDER:GetSetPart("DestinationPart", {editor_friendly = "TargetedPart"}) - BUILDER:GetSet("MultipleTargetParts", "") + BUILDER:GetSetPart("TargetPart", {editor_friendly = "ExternalOriginPart", description = "Only applies to some scale or velocity-related events, picks a different point as a reference for measurement.\nFormerly known as target part. If you remember it, forget this misnomer."}) + BUILDER:GetSetPart("DestinationPart", {editor_friendly = "TargetedPart", description = "Instead of the parent, targets a single part to show/hide."}) + BUILDER:GetSet("MultipleTargetParts", "", {description = "Instead of the parent, targets a list of parts to show/hide.\nThe list takes the form of UIDs or names separated by semicolons. You can use bulk select to quickly build the list."}) BUILDER:EndStorableVars() PART.Tutorials = {} @@ -162,27 +162,29 @@ function PART:SetEvent(event) if (owner == pac.LocalPlayer) and (not pace.processing) then if event == "command" then owner.pac_command_events = owner.pac_command_events or {} end - if not self.Events[event] then --invalid event? try a command event or button event - if pac.key_enums_reverse[event] then + if not self.Events[event] then --invalid event? try another event + if isnumber(tonumber(event)) then --timerx + timer.Simple(0.2, function() + if not self.pace_properties or self ~= pace.current_part then return end + self:SetEvent("timerx") + self:SetArguments(event .. "@@1@@0") + pace.PopulateProperties(self) + end) + return + elseif pac.key_enums_reverse[event] then --button timer.Simple(0.2, function() if not self.pace_properties or self ~= pace.current_part then return end - --now we'll use event as a command name self:SetEvent("button") - self.pace_properties["Event"]:SetValue("button") self:SetArguments(event .. "@@0") - self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") pace.PopulateProperties(self) end) return - else + else --command if GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then timer.Simple(0.2, function() if not self.pace_properties or self ~= pace.current_part then return end - --now we'll use event as a command name self:SetEvent("command") - self.pace_properties["Event"]:SetValue("command") self:SetArguments(event .. "@@0") - self.pace_properties["Arguments"]:SetValue(event .. "@@0@@0") pace.PopulateProperties(self) end) return @@ -255,25 +257,35 @@ end function PART:SetMultipleTargetParts(str) self.MultipleTargetParts = str + if str == "" then + if self.MultiTargetPart then + for _,part2 in ipairs(self.MultiTargetPart) do + if part2.SetEventTrigger then part2:SetEventTrigger(self, false) end + end + end + self.MultiTargetPart = nil self.ExtraHermites = nil + return + end self.MultiTargetPart = {} - if str == "" then self.MultiTargetPart = nil self.ExtraHermites = nil return end if not string.find(str, ";") then local part = self:GetOrFindCachedPart(str) if IsValid(part) then self:SetDestinationPart(part) self.MultipleTargetParts = "" + pace.PopulateProperties(self) else timer.Simple(3, function() local part = self:GetOrFindCachedPart(str) if part then self:SetDestinationPart(part) self.MultipleTargetParts = "" + pace.PopulateProperties(self) end end) end self.MultiTargetPart = nil else - self:SetDestinationPart() + --self:SetDestinationPart() self.MultiTargetPart = {} self.ExtraHermites = {} local uid_splits = string.Split(str, ";") @@ -3261,7 +3273,6 @@ function PART:SetAffectChildrenOnly(b) end end self.AffectChildrenOnly = b - end function PART:OnRemove() @@ -3283,43 +3294,34 @@ end function PART:TriggerEvent(b) self.event_triggered = b -- event_triggered is just used for the editor - local override = IsValid(self.DestinationPart) + local single_targetpart = IsValid(self.DestinationPart) - if self.AffectChildrenOnly then - if self.MultiTargetPart then - for _,part2 in ipairs(self.MultiTargetPart) do - if part2.SetEventTrigger then part2:SetEventTrigger(self, b) end - end - else - for _, child in ipairs(self:GetChildren()) do - child:SetEventTrigger(self, b) + if single_targetpart then + self.DestinationPart:SetEventTrigger(self, b) + self.previousdestinationpart = self.DestinationPart + else + if IsValid(self.previousdestinationpart) then + if self.DestinationPart ~= self.previousdestinationpart then --when editing, if we change the destination part we need to reset the old one + self.previousdestinationpart:SetEventTrigger(self, false) end end - if override then self:SetWarning("The Affect Children Only checkbox should perhaps be turned off, because you have chosen a targeted part") end + end + + if self.MultiTargetPart then + for _,part2 in ipairs(self.MultiTargetPart) do + if part2.SetEventTrigger then part2:SetEventTrigger(self, b) end + end + end + + if self.AffectChildrenOnly then + for _, child in ipairs(self:GetChildren()) do + child:SetEventTrigger(self, b) + end else - if override then --single target part mode - if IsValid(self.previousdestinationpart) then - if self.DestinationPart ~= self.previousdestinationpart then --when editing, if we change the destination part we need to reset the old one - self.previousdestinationpart:SetEventTrigger(self, false) - end - end - self.DestinationPart:SetEventTrigger(self, b) - self.previousdestinationpart = self.DestinationPart - else --normal parent mode - if IsValid(self.previousdestinationpart) then - if self.DestinationPart ~= self.previousdestinationpart then --when editing, if we change the destination part we need to reset the old one - self.previousdestinationpart:SetEventTrigger(self, false) - end - end - if self.MultiTargetPart then - for _,part2 in ipairs(self.MultiTargetPart) do - if part2.SetEventTrigger then part2:SetEventTrigger(self, b) end - end - else - local parent = self:GetParent() - if parent:IsValid() then - parent:SetEventTrigger(self, b) - end + if not single_targetpart and not self.MultiTargetPart then --normal parent mode should only happen if nothing is set + local parent = self:GetParent() + if parent:IsValid() then + parent:SetEventTrigger(self, b) end end end From 0507911f5a9fbb3d6e07ad630fdacfbe1399e7e6 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 13 Oct 2024 19:34:32 -0400 Subject: [PATCH 246/300] Update event.lua --- 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 eaaff5213..b652bdb5e 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -151,7 +151,7 @@ end local tracked_events = { damage_zone_hit = true, - damagezone_kill = true, + damage_zone_kill = true, lockpart_grabbing = true } function PART:SetEvent(event) From de10931efc9312a3767102c39cf7fd966608609f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 31 Oct 2024 18:47:04 -0400 Subject: [PATCH 247/300] some net combat checks and fixes add func_physbox to list of damageables hitmarker recycling no longer used by default (it was unreliable). now it's like projectiles, creating new parts every time --- lua/pac3/core/client/parts/damage_zone.lua | 69 ++++++++++++++++++++-- lua/pac3/extra/shared/net_combat.lua | 10 ++-- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index 2b2e51829..e391d7351 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -21,6 +21,8 @@ local renderhooks = { "PreDrawViewModel" } +local recycle_hitmark = CreateConVar("pac_damage_zone_recycle_hitmarkers", "0", FCVAR_ARCHIVE, "Whether to use the experimental recycling system to save performance on spawning already created hit markers.\nIf this is 0, it will be more reliable but more costly because it creates new parts every time.") + BUILDER:StartStorableVars() :SetPropertyGroup("Targets") @@ -121,9 +123,10 @@ 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, {description = "Repeats your damage a few times. Subject to serverside convar."}) - :GetSet("DOTTime", 0, {editor_clamp = {0,32}}) - :GetSet("DOTCount", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,127)) end}) + :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("NoInitialDOT", false, {description = "Skips the first instance (the instant one) of damage to achieve a delayed damage for example."}) :SetPropertyGroup("HitOutcome") :GetSetPart("HitSoundPart") @@ -649,7 +652,7 @@ function PART:SendNetMessage() 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)) - local using_hit_feedback = self.HitMarkerPart ~= nil or self.KillMarkerPart ~= nil + local using_hit_feedback = IsValid(self.HitMarkerPart) or IsValid(self.KillMarkerPart) net.WriteBool(using_hit_feedback) net.SendToServer() end @@ -699,6 +702,40 @@ function PART:SetAttachPartsToTargetEntity(b) end 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 + + ent.pac_draw_distance = 0 + + local tbl = part:ToTable() + + local group = pac.CreatePart("group", self:GetPlayerOwner()) + group:SetShowInEditor(false) + + local part_clone = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl)) + group:AddChild(part_clone) + + group:SetOwner(ent) + group.SetOwner = function(s) s.Owner = ent end + part_clone:SetHide(false) + + local id = group.Id + local owner_id = self:GetPlayerOwnerId() + if owner_id then + id = id .. owner_id + end + + ent:CallOnRemove("pac_hitmarker_" .. id, function() group:Remove() end) + group:CallRecursive("Think") + + ent.pac_hitmark_part = group + ent.pac_hitmark = self --that's just the launcher though + + return true +end + net.Receive("pac_hit_results", function(len) local uid = net.ReadString() or "" local self = part_partialUID_caches[uid] @@ -746,6 +783,30 @@ net.Receive("pac_hit_results", function(len) if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker --what if people employ a more roundabout method? CRACKDOWN! + + if not recycle_hitmark:GetBool() then + local ent = parent_ent + if not self.AttachPartsToTargetEntity then + ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") + ent:SetNoDraw(true) + ent:SetPos(pos) + end + self:LegacyAttachToEntity(killing and self.KillMarkerPart or self.HitMarkerPart, ent) + + timer.Simple(math.Clamp(killing and self.KillMarkerLifetime or self.HitMarkerLifetime, 0, 30), function() + if IsValid(ent) then + if ent.pac_hitmark_part and ent.pac_hitmark_part:IsValid() then + ent.pac_hitmark_part:Remove() + end + + timer.Simple(0.5, function() + SafeRemoveEntity(ent) + end) + end + end) + return + end + if not owner.hitparts then owner.hitparts = {} end if owner.stop_hit_markers_until then diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 0ec38b10c..88690ee07 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -79,6 +79,7 @@ if SERVER then ["npc_satchel"] = true, ["func_breakable_surf"] = true, ["func_breakable"] = true, + ["func_physbox"] = true, ["physics_cannister"] = true } @@ -1068,7 +1069,7 @@ if SERVER then end --this may benefit from some flattening treatment, lotta pyramids over here - if tbl.DamageType == "heal" then + if tbl.DamageType == "heal" and ent.Health then if ent:Health() < ent:GetMaxHealth() then if tbl.ReverseDoNotKill then --don't heal if health is below critical if ent:Health() > tbl.CriticalHealth then --default behavior @@ -1084,7 +1085,7 @@ if SERVER then end end end - elseif tbl.DamageType == "armor" then + elseif tbl.DamageType == "armor" and ent.Armor then if ent:Armor() < ent:GetMaxArmor() then if tbl.ReverseDoNotKill then --don't heal if armor is below critical if ent:Armor() > tbl.CriticalHealth then --default behavior @@ -2166,6 +2167,7 @@ if SERVER then local prop_protected, reason = IsPropProtected(targ_ent, ply) local owner = Try_CPPIGetOwner(targ_ent) + if not IsValid(owner) then return end local unconsenting_owner = owner ~= ply and (grab_consents[owner] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false)) @@ -2467,7 +2469,7 @@ if SERVER then local fraction = math.Clamp(1 - (1-bulletinfo.DamageFalloffFraction)*(distance / bulletinfo.DamageFalloffDistance),bulletinfo.DamageFalloffFraction,1) local ent = trc.Entity - if bulletinfo.dmgtype_str == "heal" then + if bulletinfo.dmgtype_str == "heal" and ent.Health then dmg:SetDamageType(0) if ent:Health() < ent:GetMaxHealth() then @@ -2476,7 +2478,7 @@ if SERVER then dmg:SetDamage(0) return - elseif bulletinfo.dmgtype_str == "armor" then + elseif bulletinfo.dmgtype_str == "armor" and ent.Armor then dmg:SetDamageType(0) if ent:Armor() < ent:GetMaxArmor() then From 4b2909b016736e7d85997991580ba8f0b8a76111 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 2 Nov 2024 17:10:08 -0400 Subject: [PATCH 248/300] lock part fixes --- lua/pac3/core/client/parts/lock.lua | 2 +- lua/pac3/extra/shared/net_combat.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index c9d747434..9e94152de 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -388,7 +388,7 @@ function PART:OnHide() pac.RemoveHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID) self.teleported = false self.grabbing = false - if self.target_ent == nil then return + if not IsValid(self.target_ent) then return else self.target_ent.IsGrabbed = false self.target_ent.IsGrabbedID = nil end if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end self:reset_ent_ang() diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 88690ee07..db13e89b6 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -2166,7 +2166,7 @@ if SERVER then local prop_protected, reason = IsPropProtected(targ_ent, ply) - local owner = Try_CPPIGetOwner(targ_ent) + local owner = Try_CPPIGetOwner(targ_ent) or targ_ent if not IsValid(owner) then return end From fa6eb8aa09cc2a728d0a106d53f1bf2be8829f0a Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 16 Nov 2024 00:21:48 -0500 Subject: [PATCH 249/300] properly add eases to sample_and_hold /drift proxy --- lua/pac3/core/client/parts/proxy.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 44092057c..dded649c3 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -417,6 +417,8 @@ PART.Inputs.lerp = function(self, m, a, b) return (b - a) * m + a end +--I'll be reusing these on sample and hold / drift +local ease_aliases = {} for ease,f in pairs(math.ease) do if string.find(ease,"In") or string.find(ease,"Out") then local f2 = function(self, frac, min, max) @@ -427,6 +429,9 @@ for ease,f in pairs(math.ease) do PART.Inputs["ease"..ease] = f2 PART.Inputs["ease_"..ease] = f2 PART.Inputs[ease] = f2 + ease_aliases[ease] = ease + ease_aliases["ease"..ease] = ease + ease_aliases["ease_"..ease] = ease end end From 67ba3954d04bc7c992f2054505a7e8d178578cb4 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 30 Nov 2024 19:04:33 -0500 Subject: [PATCH 250/300] labelling correction --- lua/pac3/editor/client/spawnmenu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index e5e35ca2c..d1428174f 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -198,7 +198,7 @@ function pace.AdminSettingsMenu(self) self:NumSlider(L"Max radius", "pac_sv_force_max_radius", 0, 32767, 0) self:NumSlider(L"Max length", "pac_sv_force_max_length", 0, 32767, 0) - self:Help(L"Force part"):SetFont("DermaDefaultBold") + self:Help(L"Health Modifier part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable health modifier", "pac_sv_health_modifier") self:CheckBox(L"Allow changing max health or armor", "pac_sv_health_modifier_allow_maxhp") self:NumSlider(L"Minimum combined damage scaling", "pac_sv_health_modifier_min_damagescaling", -10, 1, 2) From e5c3ac1d6bf402e843b0d035467c9710d4985752 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 8 Dec 2024 01:35:30 -0500 Subject: [PATCH 251/300] command part update, property stuff, luapad add "jump to" option on proxy-affected properties and part reference properties, to navigate to the relevant part directly add reset to booleans, font-related fix for the label resizes the label for when using large enough fonts add apply size to scales option (for models, sprites) to normalize size to 1 generic multiline text panel for notes code panels for commands and proxies, derived from the luapad used in the hidden script part command part features: most of these are to reduce event load and duplicate parts -OnHide command string -Delayed command string -pac_event presets for both mainline pac_events and pac_event_sequenced. Random mode will replace lua command randomizers (people have consistently brought up the issue that cs lua is not allowed on many servers) -Dynamic mode: run a command repeatedly when the appended number is changed (e.g. for post-processing gradual fading) -Safe Guard: option to delay execution by 1 frame in order to attempt to fix certain specific event logic issues, thereby preventing false triggers. a lua timer is probably better than timerx events constantly running --- lua/pac3/core/client/base_part.lua | 4 +- lua/pac3/core/client/parts/command.lua | 149 +++++- lua/pac3/core/client/parts/proxy.lua | 34 +- .../editor/client/panels/extra_properties.lua | 464 ++++++++++++++++++ lua/pac3/editor/client/panels/properties.lua | 117 ++++- 5 files changed, 749 insertions(+), 19 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index c30098494..c5434b78b 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -31,7 +31,7 @@ BUILDER :StartStorableVars() :SetPropertyGroup("generic") :GetSet("Name", "") - :GetSet("Notes", "") + :GetSet("Notes", "", {editor_panel = "generic_multiline"}) :GetSet("Hide", false) :GetSet("EditorExpand", false, {hidden = true}) :GetSet("UniqueID", "", {hidden = true}) @@ -1323,7 +1323,7 @@ function PART:SetupEditorPopup(str, force_open, tbl, x, y) end if not pnl then pnl = pac.InfoPopup(info_string,popup_config_table, x, y) - self.pace_tree_node.popupinfopnl = pnl + if IsValid(self.pace_tree_node) then self.pace_tree_node.popupinfopnl = pnl end end if pace then pace.legacy_floating_popup_reserved = pnl diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index f4852a878..e5b3c58f7 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -6,10 +6,43 @@ PART.Group = "advanced" PART.Icon = "icon16/application_xp_terminal.png" BUILDER:StartStorableVars() - BUILDER:GetSet("String", "", {editor_panel = "string"}) + +BUILDER:SetPropertyGroup("generic") + BUILDER:GetSet("String", "", {editor_panel = "code_script"}) BUILDER:GetSet("UseLua", false) BUILDER:GetSet("ExecuteOnWear", false) BUILDER:GetSet("ExecuteOnShow", true) + BUILDER:GetSet("SafeGuard", false, {description = "Delays the execution by 1 frame to attempt to prevent false triggers due to events' runtime quirks"}) + + --fading re-run mode + BUILDER:SetPropertyGroup("dynamic mode") + BUILDER:GetSet("DynamicMode", false, {description = "Dynamically assign an argument, adding the appended number to the string.\nWhen the appended number is changed, run the command again.\nFor example, it could be used with post processing fades. With pp_colormod 1, pp_colormod_color represents saturation multiplier. You could fade that to slowly fade to gray."}) + BUILDER:GetSet("AppendedNumber", 1, {description = "Argument to use. When it changes, the command will run again with the updated value."}) + + --common alternate activations + BUILDER:SetPropertyGroup("alternates") + BUILDER:GetSet("OnHideString", "", {description = "An alternate command when the part is hidden. Governed by execute on show", editor_panel = "code_script"}) + BUILDER:GetSet("DelayedString", "", {description = "An alternate command after a delay. Governed by execute on show", editor_panel = "code_script"}) + BUILDER:GetSet("Delay", 1) + + --we might as well have a section for command events since they are so useful for logic, and often integrated with command parts + --There should be a more convenient front-end for pac_event stuff and to fix the issue where people want to randomize their command (e.g. random attacks) when cs lua isn't allowed on some servers. + BUILDER:SetPropertyGroup("pac_event") + BUILDER:GetSet("CommandName", "", {description = "name of the pac_event to manage, or base name of the sequenced series.\n\nfor example, if you have commands hat1, hat2, hat3, and hat4:\n-the base name is hat\n-the minimum is 1\n-the maximum is 4"}) + BUILDER:GetSet("Action", "Default", {enums = { + ["Default: single-shot"] = "Default", + ["Default: On (1)"] = "On", + ["Default: Off (0)"] = "Off", + ["Default: Toggle (2)"] = "Toggle", + ["Sequence: forward"] = "Sequence+", + ["Sequence: back"] = "Sequence-", + ["Sequence: set"] = "SequenceSet", + ["Random"] = "Random", + ["Random (Sequence set)"] = "RandomSet", + }, description = "The Default series corresponds to the normal pac_event command modes. Minimum and maximum don't apply.\nSequences run the sequence command pac_event_sequenced with the corresponding mode.\nRandom will select a random number to append to the base name and run the pac_event as a single-shot. This is intended to replace the lua randomizer method when sv_allowcslua is disabled."}) + BUILDER:GetSet("Minimum", 1, {description = "The defined minimum for the pac_event if it's for a numbered series.\nOr, when using the sequence set action, this will be used.", editor_onchange = function(self, val) return math.floor(val) end}) + BUILDER:GetSet("Maximum", 1, {description = "The defined maximum for the pac_event if it's for a numbered series.", editor_onchange = function(self, val) return math.floor(val) end}) + BUILDER:EndStorableVars() local sv_allowcslua = GetConVar("sv_allowcslua") @@ -23,13 +56,54 @@ function PART:OnWorn() end end +function PART:SetMaximum(val) + self.Maximum = val + if self:GetPlayerOwner() == pac.LocalPlayer and self.CommandName ~= "" and self.Minimum ~= self.Maximum then + self:GetPlayerOwner():ConCommand("pac_event_sequenced_force_set_bounds " .. self.CommandName .. " " .. self.Minimum .. " " .. self.Maximum) + end +end + +function PART:SetMinimum(val) + self.Minimum = val + if self:GetPlayerOwner() == pac.LocalPlayer and self.CommandName ~= "" and self.Minimum ~= self.Maximum then + self:GetPlayerOwner():ConCommand("pac_event_sequenced_force_set_bounds " .. self.CommandName .. " " .. self.Minimum .. " " .. self.Maximum) + end +end + +function PART:SetAppendedNumber(val) + if self.AppendedNumber ~= val then + self.AppendedNumber = val + if self:GetPlayerOwner() == pac.LocalPlayer and self.DynamicMode then + self:Execute() + end + end + self.AppendedNumber = val +end + function PART:OnShow(from_rendering) - if pace.still_loading_wearing then return end if not from_rendering and self:GetExecuteOnShow() then - timer.Simple(0, function() - if self.Hide or self:IsHidden() then return end + if pace.still_loading_wearing then return end + + if self.SafeGuard then + timer.Simple(0,function() + if self.Hide or self:IsHidden() then return end + self:Execute() + end) + else self:Execute() - end) + end + + if self.DelayedString ~= "" then + timer.Simple(self.Delay, function() + self:Execute(self.DelayedString) + end) + end + end +end + +function PART:OnHide() + if self.ExecuteOnShow and self.OnHideString ~= "" then + self:Execute(self.OnHideString) end end @@ -62,10 +136,31 @@ function PART:GetNiceName() if self.UseLua then return ("lua: " .. self.String) end + if self.String == "" and self.CommandName ~= "" then + if self.Action == "Default" then + return "pac_event " .. self.CommandName + elseif self.Action == "On" then + return "pac_event " .. self.CommandName .. " 1" + elseif self.Action == "Off" then + return "pac_event " .. self.CommandName .. " 0" + elseif self.Action == "Toggle" then + return "pac_event " .. self.CommandName .. " 2" + elseif self.Action == "Sequence+" then + return "pac_event_sequenced " .. self.CommandName .. " +" + elseif self.Action == "Sequence-" then + return "pac_event_sequenced " .. self.CommandName .. " -" + elseif self.Action == "SequenceSet" then + return "pac_event_sequenced " .. self.CommandName .. " set " .. self.Minimum .. "[bounds:" .. self.Minimum .. ", " .. self.Maximum .."]" + elseif self.Action == "Random" then + return "pac_event " .. self.CommandName .. " " + elseif self.Action == "RandomSet" then + return "pac_event_sequenced " .. self.CommandName .. " set " .. "" + end + end return "command: " .. self.String end -function PART:Execute() +function PART:Execute(commandstring) local ent = self:GetPlayerOwner() if ent == pac.LocalPlayer then @@ -88,7 +183,47 @@ function PART:Execute() self:SetError("Concommand is blocked") return end - ent:ConCommand(self.String) + if self.String == "" and self.CommandName ~= "" then + --[[ + ["Default: single-shot"] = "Default", + ["Default: On (1)"] = "On", + ["Default: Off (0)"] = "Off", + ["Default: Toggle (2)"] = "Toggle", + ["Sequence: forward"] = "Sequence+", + ["Sequence: back"] = "Sequence-", + ["Sequence: set"] = "SequenceSet", + ["Random"] = "Random", + ["Random"] = "RandomSet", + ]] + if self.Action == "Default" then + ent:ConCommand("pac_event " .. self.CommandName) + elseif self.Action == "On" then + ent:ConCommand("pac_event " .. self.CommandName .. " 1") + elseif self.Action == "Off" then + ent:ConCommand("pac_event " .. self.CommandName .. " 0") + elseif self.Action == "Toggle" then + ent:ConCommand("pac_event " .. self.CommandName .. " 2") + elseif self.Action == "Sequence+" then + ent:ConCommand("pac_event_sequenced " .. self.CommandName .. " +") + elseif self.Action == "Sequence-" then + ent:ConCommand("pac_event_sequenced " .. self.CommandName .. " -") + elseif self.Action == "SequenceSet" then + ent:ConCommand("pac_event_sequenced " .. self.CommandName .. " set " .. self.Minimum) + elseif self.Action == "Random" then + local randnum = math.floor(math.Rand(self.Minimum, self.Maximum + 1)) + ent:ConCommand("pac_event " .. self.CommandName .. randnum) + elseif self.Action == "RandomSet" then + local randnum = math.floor(math.Rand(self.Minimum, self.Maximum + 1)) + ent:ConCommand("pac_event_sequenced " .. self.CommandName .. " set " .. randnum) + end + return + end + + if self.DynamicMode then + ent:ConCommand(self.String .. " " .. self.AppendedNumber) + return + end + ent:ConCommand(commandstring or self.String) end end end diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index dded649c3..8c320c51c 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -31,7 +31,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("MultipleTargetParts", "", {description = "send output to multiple external partss.\npaste multiple UIDs or names here, separated by semicolons. With bulk select, you can select parts and right click to get that done quickly.."}) BUILDER:GetSetPart("OutputTargetPart", {hide_in_editor = true}) BUILDER:GetSet("AffectChildren", false) - BUILDER:GetSet("Expression", "", {description = "write math here. hit F1 for a tutorial or right click for examples."}) + BUILDER:GetSet("Expression", "", {description = "write math here. hit F1 for a tutorial or right click for examples.", editor_panel = "code_proxy"}) BUILDER:SetPropertyGroup("easy setup") BUILDER:GetSet("Input", "time", {enums = function(part) return part.Inputs end, description = "base (inner) function for easy setup\nin sin(time()) it is time"}) @@ -54,11 +54,11 @@ BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("extra expressions") BUILDER:GetSet("ExpressionOnHide", "", {description = "Math to apply once, when the proxy is hidden. It computes once, so it will not move."}) - BUILDER:GetSet("Extra1", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra1() or var1() to save space, or by another proxy as extra1(\"uid or name\") or var1(\"uid or name\")"}) - BUILDER:GetSet("Extra2", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra2() or var2() to save space, or by another proxy as extra2(\"uid or name\") or var2(\"uid or name\")"}) - BUILDER:GetSet("Extra3", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra3() or var3() to save space, or by another proxy as extra3(\"uid or name\") or var3(\"uid or name\")"}) - BUILDER:GetSet("Extra4", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra4() or var4() to save space, or by another proxy as extra4(\"uid or name\") or var4(\"uid or name\")"}) - BUILDER:GetSet("Extra5", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra5() or var5() to save space, or by another proxy as extra5(\"uid or name\") or var5(\"uid or name\")"}) + BUILDER:GetSet("Extra1", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra1() or var1() to save space, or by another proxy as extra1(\"uid or name\") or var1(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra2", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra2() or var2() to save space, or by another proxy as extra2(\"uid or name\") or var2(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra3", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra3() or var3() to save space, or by another proxy as extra3(\"uid or name\") or var3(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra4", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra4() or var4() to save space, or by another proxy as extra4(\"uid or name\") or var4(\"uid or name\")", editor_panel = "code_proxy"}) + BUILDER:GetSet("Extra5", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra5() or var5() to save space, or by another proxy as extra5(\"uid or name\") or var5(\"uid or name\")", editor_panel = "code_proxy"}) BUILDER:EndStorableVars() -- redirect @@ -1818,10 +1818,14 @@ local function set(self, part, x, y, z, children) local math_description = "expression:\n"..self.Expression if self.Expression == "" then math_description = "using " .. self.Function .. " and " .. self.Input end if vector_type then + property_pnl.user_proxies = property_pnl.right.user_proxies or {} + property_pnl.user_proxies [self] = self if self.using_x then property_pnl.used_by_proxy = true container = property_pnl.left property_pnl.left.used_by_proxy = true + property_pnl.left.user_proxies = property_pnl.left.user_proxies or {} + property_pnl.left.user_proxies [self] = self local num = x or 0 property_pnl.left:SetValue(math.Round(tonumber(num),4)) container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) @@ -1830,6 +1834,8 @@ local function set(self, part, x, y, z, children) property_pnl.used_by_proxy = true container = property_pnl.middle property_pnl.middle.used_by_proxy = true + property_pnl.middle.user_proxies = property_pnl.middle.user_proxies or {} + property_pnl.middle.user_proxies [self] = self local num = y or x or 0 property_pnl.middle:SetValue(math.Round(tonumber(num),4)) container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) @@ -1838,6 +1844,8 @@ local function set(self, part, x, y, z, children) property_pnl.used_by_proxy = true container = property_pnl.right property_pnl.right.used_by_proxy = true + property_pnl.right.user_proxies = property_pnl.right.user_proxies or {} + property_pnl.right.user_proxies [self] = self local num = z or x or 0 property_pnl.right:SetValue(math.Round(tonumber(num),4)) container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) @@ -1845,11 +1853,15 @@ local function set(self, part, x, y, z, children) elseif T == "boolean" then if x ~= nil then property_pnl.used_by_proxy = true + property_pnl.user_proxies = property_pnl.user_proxies or {} + property_pnl.user_proxies [self] = self property_pnl:SetValue(tonumber(x) > 0) container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) end elseif original_x ~= nil then property_pnl.used_by_proxy = true + property_pnl.user_proxies = property_pnl.user_proxies or {} + property_pnl.user_proxies [self] = self property_pnl:SetValue(math.Round(tonumber(x) or 0,4)) container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description) end @@ -1960,11 +1972,19 @@ function PART:OnThink(to_hide) if not ok then self.error = true if self:GetPlayerOwner() == pac.LocalPlayer and self.Expression ~= self.LastBadExpression then - chat.AddText(Color(255,180,180),"============\n[ERR] PAC Proxy error on "..tostring(self)..":\n"..x.."\n============\n") + --don't spam the chat every time we type a single character in the luapad + if not (pace.ActiveSpecialPanel and pace.ActiveSpecialPanel.luapad) then + chat.AddText(Color(255,180,180),"============\n[ERR] PAC Proxy error on "..tostring(self)..":\n"..x.."\n============\n") + end self.LastBadExpression = self.Expression + self.Error = x --will be used by the luapad for its title end + + if not self.errors_override then self:SetError(self.Error) end return end + self.Error = nil + if not self.errors_override then self:SetError() end if x and not isnumber(x) then x = 0 end if y and not isnumber(y) then y = 0 end diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 3ed454f95..f86ab3130 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -604,6 +604,93 @@ do -- sound pace.RegisterPanel(PANEL) end +do --generic multiline text + local PANEL = {} + + PANEL.ClassName = "properties_generic_multiline" + PANEL.Base = "pace_properties_base_type" + + function PANEL:MoreOptionsLeftClick() + local pnl = vgui.Create("DFrame") + local DText = vgui.Create("DTextEntry", pnl) + local DButtonOK = vgui.Create("DButton", pnl) + DText:SetMaximumCharCount(50000) + + pnl:SetSize(1200,800) + 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) + DText:SetPos(5,25) + DText:SetSize(1190,700) + DText:SetMultiline(true) + DText:SetContentAlignment(7) + pnl:MakePopup() + DText:RequestFocus() + DText:SetText(pace.current_part[self.CurrentKey]) + + DButtonOK.DoClick = function() + local str = DText:GetText() + pace.current_part[self.CurrentKey] = str + if pace.current_part.ClassName == "sound2" then + pace.current_part.AllPaths = str + pace.current_part:UpdateSoundsFromAll() + end + pace.PopulateProperties(pace.current_part) + pnl:Remove() + end + end + + pace.RegisterPanel(PANEL) +end + +local lua_editor_fontsize = 16 +local function install_fontsize_buttons(frame, editor, add_execute) + local btn_fontplus = vgui.Create("DButton", frame) btn_fontplus:SetSize(20, 18) btn_fontplus:SetText("+") + local btn_fontminus = vgui.Create("DButton", frame) btn_fontminus:SetSize(20, 18) btn_fontminus:SetText("-") + function btn_fontplus:DoClick() + lua_editor_fontsize = math.Clamp(lua_editor_fontsize + 1, 6, 80) + surface.CreateFont("LuapadEditor", {font = "roboto mono", size = lua_editor_fontsize, weight = 400 } ) + surface.CreateFont("LuapadEditor_Bold", {font = "roboto mono", size = lua_editor_fontsize, weight = 800}) + surface.SetFont("LuapadEditor"); + editor.FontWidth, editor.FontHeight = surface.GetTextSize(" ") + end + function btn_fontminus:DoClick() + lua_editor_fontsize = math.Clamp(lua_editor_fontsize - 1, 6, 80) + surface.CreateFont("LuapadEditor", {font = "roboto mono", size = lua_editor_fontsize, weight = 400 } ) + surface.CreateFont("LuapadEditor_Bold", {font = "roboto mono", size = lua_editor_fontsize, weight = 800}) + surface.SetFont("LuapadEditor"); + editor.FontWidth, editor.FontHeight = surface.GetTextSize(" ") + end + local perflayout = frame.PerformLayout + local fthink = frame.Think + btn_fontplus:SetY(3) + btn_fontminus:SetY(3) + + if add_execute then + local btn_run = vgui.Create("DButton", frame) btn_run:SetSize(50, 18) btn_run:SetY(3) + btn_run:SetImage("icon16/bullet_go.png") btn_run:SetText(" run") + function btn_run:DoClick() + pace.current_part:Execute() + end + function frame:Think() + btn_run:SetX(self:GetWide() - 190 + 4) + btn_fontplus:SetX(self:GetWide() - 120 + 4) + btn_fontminus:SetX(self:GetWide() - 140 + 4) + fthink(self) + end + frame:RequestFocus() + else + function frame:PerformLayout() + btn_fontplus:SetX(self:GetWide() - 120 + 4) + btn_fontminus:SetX(self:GetWide() - 140 + 4) + perflayout(self) + end + end + +end + do -- script local PANEL = {} @@ -668,12 +755,389 @@ do -- script frame:SetTitle(title) end + add_fontsize_buttons(frame, editor) + pace.ActiveSpecialPanel = frame end pace.RegisterPanel(PANEL) end +local function install_edge_resizes(frame) + local function more_or_less(n1,n2) + return math.abs(n1-n2) < 10 + end + pac.AddHook("Think", frame, function() + local w,h = frame:GetSize() + local px,py = frame:GetPos() + local mx,my = input.GetCursorPos() + if not input.IsMouseDown(MOUSE_LEFT) then + frame.resizing = false + frame.resizing_down = false + frame.resizing_left = false + frame.resizing_right = false + elseif frame.resizing then + if frame.resizing_down then + frame:SetHeight(my-py) + elseif frame.resizing_left then + frame:SetWide(frame.target_edge - mx) + frame:SetX(mx) + elseif frame.resizing_right then + frame:SetWide(mx-px) + end + end + if more_or_less(px+w,mx) then --EDGE RIGHT + frame:SetCursor("sizewe") + if input.IsMouseDown(MOUSE_LEFT) then + frame.resizing = true + frame.resizing_right = true + end + end + if more_or_less(px,mx) then --EDGE LEFT + frame:SetCursor("sizewe") + frame.target_edge = px+w + if input.IsMouseDown(MOUSE_LEFT) then + frame.resizing = true + frame.resizing_left = true + end + end + if more_or_less(py+h,my) then --EDGE DOWN + frame:SetCursor("sizens") + if input.IsMouseDown(MOUSE_LEFT) then + frame.resizing = true + frame.resizing_down = true + end + end + end) +end + +do -- script command + local PANEL = {} + + PANEL.ClassName = "properties_code_script" + PANEL.Base = "pace_properties_base_type" + + function PANEL:SetValue(var, skip_encode) + if self.editing then return end + + local value = skip_encode and var or self:Encode(var) + if isnumber(value) then + -- visually round numbers so 0.6 doesn't show up as 0.600000000001231231 on wear + value = math.Round(value, 7) + end + local str = tostring(value) + local original_str = string.Trim(str,"\n") + local lines = string.Explode("\n", original_str) + if #lines > 1 then + if lines[#lines] ~= "" then + str = "" + elseif #lines > 2 then + str = "" + end + end + + self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text) + if str == "" then self:SetTextColor(Color(160,0,80)) end + self:SetFont(pace.CurrentFont) + self:SetText(" " .. str) -- ugh + self:SizeToContents() + + if #str > 10 then + self:SetTooltip(value) + else + self:SetTooltip() + end + + self.original_text = original_str + self.original_str = original_str + self.original_var = var + + if self.OnValueSet then + self:OnValueSet(var) + end + end + + function PANEL:MoreOptionsLeftClick() + local part = pace.current_part + pace.SafeRemoveSpecialPanel() + + local frame = vgui.Create("DFrame") + install_edge_resizes(frame) + frame:SetTitle(L"script") + pace.ShowSpecial(frame, self, 0) + frame:SetSizable(true) + + local editor = vgui.Create("pace_luapad", frame) + frame.luapad = editor + install_fontsize_buttons(frame, editor, true) + editor:Dock(FILL) + if pace.Editor:IsLeft() then + frame:SetSize(ScrW() - pace.Editor:GetX() - pace.Editor:GetWide(),200) + frame:SetPos(pace.Editor:GetWide() + pace.Editor:GetX(), select(2, self:LocalToScreen())) + else + frame:SetSize(pace.Editor:GetX(),200) + frame:SetPos(0, select(2, self:LocalToScreen())) + end + + + editor:SetText(pace.current_part:GetCode()) + editor.OnTextChanged = function(self) + pace.current_part:SetString(self:GetValue()) + end + + editor.last_error = "" + + function editor:CheckGlobal(str) + if not part:IsValid() then frame:Remove() return end + + return part:ShouldHighlight(str) + end + + function editor:Think() + if not part:IsValid() then frame:Remove() return end + + local title = L"script editor" + if part.UseLua then + title = L"command" .. " (lua)" + else + title = L"command" .. " (console)" + end + + if part.Error then + title = part.Error + + local line = tonumber(title:match("SCRIPT_ENV:(%d-):")) + + if line then + title = title:match("SCRIPT_ENV:(.+)") + if self.last_error ~= title then + editor:SetScrollPosition(line) + editor:SetErrorLine(line) + self.last_error = title + end + end + else + editor:SetErrorLine(nil) + + if part.script_printing then + title = part.script_printing + part.script_printing = nil + end + end + + frame:SetTitle(title) + end + + + + editor.FontWidth, editor.FontHeight = surface.GetTextSize(" ") + + pace.ActiveSpecialPanel = frame + end + + pace.RegisterPanel(PANEL) +end + +do -- script proxy + local PANEL = {} + + PANEL.ClassName = "properties_code_proxy" + PANEL.Base = "pace_properties_base_type" + + function PANEL:SetValue(var, skip_encode) + if self.editing then return end + + local value = skip_encode and var or self:Encode(var) + if isnumber(value) then + -- visually round numbers so 0.6 doesn't show up as 0.600000000001231231 on wear + value = math.Round(value, 7) + end + local str = tostring(value) + local original_str = string.Trim(str,"\n") + local lines = string.Explode("\n", str) + if #lines > 1 then + if lines[#lines] ~= "" then + str = "" + elseif #lines > 2 then + str = "" + end + end + str = string.gsub(str, "\n", "") + self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text) + if str == "" then self:SetTextColor(Color(160,0,80)) end + self:SetFont(pace.CurrentFont) + self:SetText(" " .. str) -- ugh + self:SizeToContents() + + if #str > 10 then + self:SetTooltip(str) + else + self:SetTooltip() + end + + self.original_text = original_str + self.original_str = original_str + self.original_var = var + + if self.OnValueSet then + self:OnValueSet(var) + end + end + + function PANEL:MoreOptionsLeftClick() + local key = self.CurrentKey + local part = pace.current_part + local prop = self + pace.SafeRemoveSpecialPanel() + + local frame = vgui.Create("DFrame") + install_edge_resizes(frame) + frame:SetTitle(L"proxy") + pace.ShowSpecial(frame, self, 0) + frame:SetSizable(true) + + local editor = vgui.Create("pace_luapad", frame) + frame.luapad = editor + install_fontsize_buttons(frame, editor) + editor:Dock(FILL) + if pace.Editor:IsLeft() then + frame:SetSize(ScrW() - pace.Editor:GetX() - pace.Editor:GetWide(),200) + frame:SetPos(pace.Editor:GetWide() + pace.Editor:GetX(), select(2, self:LocalToScreen())) + else + frame:SetSize(pace.Editor:GetX(),200) + frame:SetPos(0, select(2, self:LocalToScreen())) + end + + editor:SetText(part["Get"..key](part)) + editor.OnTextChanged = function(self) + part["Set"..key](part, self:GetValue()) + prop:SetValue(self:GetValue()) + end + + editor.last_error = "" + + function editor:ShouldHighlight(str) + return part.lib and part.lib[str] + end + + function editor:CheckGlobal(str) + if not part:IsValid() then frame:Remove() return end + + return self:ShouldHighlight(str) + end + + function editor:Think() + if not part:IsValid() then frame:Remove() return end + + local title = L"proxy editor" + + if part.Error then + title = part.Error + + local line = tonumber(title:match("SCRIPT_ENV:(%d-):")) + + if line then + title = title:match("SCRIPT_ENV:(.+)") + if self.last_error ~= title then + editor:SetScrollPosition(line) + editor:SetErrorLine(line) + self.last_error = title + end + end + else + editor:SetErrorLine(nil) + + if part.script_printing then + title = part.script_printing + part.script_printing = nil + end + end + + frame:SetTitle(title) + end + + pace.ActiveSpecialPanel = frame + end + + function PANEL:EditText() + local oldText = self:GetText() + self:SetText("") + + local pnl = vgui.Create("DTextEntry") + self.editing = pnl + pnl:SetFont(pace.CurrentFont) + pnl:SetDrawBackground(false) + pnl:SetDrawBorder(false) + local enc = self:EncodeEdit(self.original_str or "") + if enc == "" then + enc = self.original_str + end + pnl:SetText(enc) + pnl:SetKeyboardInputEnabled(true) + pnl:RequestFocus() + pnl:SelectAllOnFocus(true) + + pnl.OnTextChanged = function() oldText = pnl:GetText() end + + local hookID = tostring({}) + local textEntry = pnl + local delay = os.clock() + 0.5 + + pac.AddHook('Think', hookID, function(code) + if not IsValid(self) or not IsValid(textEntry) 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 + pac.RemoveHook('Think', hookID) + self.editing = false + pace.BusyWithProperties = NULL + textEntry:Remove() + self:SetText(oldText) + pnl:OnEnter() + end) + + --local x,y = pnl:GetPos() + --pnl:SetPos(x+3,y-4) + --pnl:Dock(FILL) + local x, y = self:LocalToScreen() + local inset_x = self:GetTextInset() + pnl:SetPos(x+5 + inset_x, y) + pnl:SetSize(self:GetSize()) + pnl:SetWide(ScrW()) + pnl:MakePopup() + + pnl.OnEnter = function() + pace.BusyWithProperties = NULL + self.editing = false + + pnl:Remove() + + self:SetText(tostring(self:Encode(self:DecodeEdit(pnl:GetText() or ""))), true) + self.OnValueChanged(self:Decode(self:GetText())) + end + + local old = pnl.Paint + pnl.Paint = function(...) + if not self:IsValid() then pnl:Remove() return end + + surface.SetFont(pnl:GetFont()) + local w = surface.GetTextSize(pnl:GetText()) + 6 + + surface.DrawRect(0, 0, w, pnl:GetTall()) + surface.SetDrawColor(self:GetSkin().Colours.Properties.Border) + surface.DrawOutlinedRect(0, 0, w, pnl:GetTall()) + + pnl:SetWide(w) + + old(...) + end + + pace.BusyWithProperties = pnl + end + + pace.RegisterPanel(PANEL) +end + do -- hull local PANEL = {} diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 3541d40c9..ffb09e4e0 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -39,6 +39,27 @@ function pace.FixMenu(menu) menu:SetPos(pace.Editor:GetPos() + pace.Editor:GetWide(), gui.MouseY() - (menu:GetTall() * 0.5)) end + +function pace.GoToPart(part) + pace.OnPartSelected(part, true) + local delay = 0 + if not IsValid(part.pace_tree_node) then --possible de-loaded node + delay = 0.5 + end + local parent = part:GetParent() + while IsValid(parent) and (parent:GetParent() ~= parent) do + parent:SetEditorExpand(true) + parent = parent:GetParent() + if parent:IsValid() then + parent:SetEditorExpand(true) + end + end + pace.RefreshTree(true) + + timer.Simple(delay, function() if IsValid(part.pace_tree_node) then + pace.tree:ScrollToChild(part.pace_tree_node) + end end) +end ---returns table --start_index is the first known index --continuous is whether it's continuous (some series have holes) @@ -214,6 +235,7 @@ local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight) btn:Dock(RIGHT) btn:SetText("...") btn.DoClick = function() callFuncLeft(self, self.CurrentKey) end + btn.PerformLayout = function() btn:SetWide(self:GetTall()) end if callFuncRight then btn.DoRightClick = function() callFuncRight(self, self.CurrentKey) end @@ -414,7 +436,7 @@ do -- container end end - function PANEL:CreateAlternateLabel(str) + function PANEL:CreateAlternateLabel(str, no_offset) if not str then if self.alt_label then if IsValid(self.alt_label) then @@ -428,9 +450,10 @@ do -- container self.alt_label:SetText("<" .. L(str) .. ">") if pace.special_property_text_color then self.alt_label:SetTextColor(pace.special_property_text_color) else self.alt_label:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text) end - self.alt_label:SetPos(60,-1) + self.alt_label:SetPos(no_offset and 0 or 60,-1) self.alt_label:SetSize(200,20) self.alt_label:SetFont(pace.CurrentFont) + return self.alt_label end pace.RegisterPanel(PANEL) @@ -665,6 +688,7 @@ do -- list end btn:SetValue(L((udata and udata.editor_friendly or key):gsub("%u", " %1"):lower()):Trim()) + pace.current_part["pac_property_label_"..key] = btn if udata then if udata.group == "bodygroups" then if key[1] == "_" then --bodygroup exceptions @@ -695,7 +719,19 @@ do -- list local reasons_hidden = pace.current_part:GetReasonsHidden() if not table.IsEmpty(reasons_hidden) then pnl:SetTooltip("Hidden by:" .. table.ToString(reasons_hidden, "", true)) - pnl:CreateAlternateLabel("hidden") + local label = pnl:CreateAlternateLabel("hidden") + label.DoRightClick = function() + local menu = DermaMenu() + menu:SetPos(input.GetCursorPos()) + for part,reason in pairs(tbl) do + if part ~= pace.current_part then + menu:AddOption("jump to " .. tostring(part), function() + pace.GoToPart(part) + end):SetImage("icon16/arrow_turn_right.png") + end + end + menu:MakePopup() + end end pace.current_part.hide_property_pnl = var end @@ -1224,6 +1260,24 @@ do -- base editable pace.multicopy_source = part end function PANEL:PopulateContextMenu(menu) + if self.user_proxies then + for _,part in pairs(self.user_proxies) do + menu:AddOption("jump to " .. tostring(part), function() + pace.GoToPart(part) + end):SetImage("icon16/arrow_turn_right.png") + end + end + + if self.udata and self.udata.editor_panel == "part" then + if self:GetValue() ~= "" then + local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), self:GetValue()) + if IsValid(part) then + menu:AddOption("jump to " .. tostring(part), function() + pace.GoToPart(part) + end):SetImage("icon16/arrow_turn_right.png") + end + end + end pace.clipboardtooltip = pace.clipboardtooltip or "" local copymenu, copypnl = menu:AddSubMenu(L"copy", function() @@ -1904,6 +1958,27 @@ do -- base editable self:SetValue(-val) self.OnValueChanged(self:GetValue()) end):SetImage("icon16/arrow_switch.png") + + if self.CurrentKey == "Size" then + if pace.current_part.ClassName == "sprite" then + menu:AddOption(L"apply size to scales", function() + local val = self:GetValue() + pace.current_part.SizeX = pace.current_part.SizeX * val + pace.current_part.SizeY = pace.current_part.SizeX * val + self:SetValue(1) + self.OnValueChanged(self:GetValue()) + pace.PopulateProperties(pace.current_part) + end):SetImage("icon16/arrow_down.png") + elseif pace.current_part.SetScale and pace.current_part.GetScale then + menu:AddOption(L"apply size to scales", function() + local val = self:GetValue() + pace.current_part:SetScale(val * pace.current_part:GetScale()) + self:SetValue(1) + self.OnValueChanged(self:GetValue()) + pace.PopulateProperties(pace.current_part) + end):SetImage("icon16/arrow_down.png") + end + end end menu:AddSpacer() @@ -2292,6 +2367,13 @@ do -- vector end function PANEL:PopulateContextMenu(menu) + if self.user_proxies then + for _,part in pairs(self.user_proxies) do + menu:AddOption("jump to " .. tostring(part), function() + pace.GoToPart(part) + end):SetImage("icon16/arrow_turn_right.png") + end + end pace.clipboardtooltip = pace.clipboardtooltip or "" local copymenu, copypnl = menu:AddSubMenu(L"copy", function() pace.clipboard = pac.CopyValue(self.vector) @@ -2668,6 +2750,25 @@ do -- boolean self.OnValueChanged(b) self.lbl:SetText(L(tostring(b))) end + chck.DoRightClick = function() + local menu = DermaMenu() + menu:SetPos(input.GetCursorPos()) + if self.user_proxies then + for _,part in pairs(self.user_proxies) do + menu:AddOption("jump to " .. tostring(part), function() + pace.GoToPart(part) + end):SetImage("icon16/arrow_turn_right.png") + end + end + 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]) + self:SetValue(val) + self.OnValueChanged(val) + end + end):SetImage(pace.MiscIcons.clear) + menu:MakePopup() + end self.chck = chck local lbl = vgui.Create("DLabel", self) @@ -2708,6 +2809,7 @@ do -- boolean self.lbl:CenterVertical() local w,h = self:GetParent():GetSize() self:SetSize(w-2,h) + self.lbl:SetSize(w-h-2,h) end pace.RegisterPanel(PANEL) @@ -2723,6 +2825,15 @@ local tree_search_excluded_vars = { } function pace.OpenTreeSearch() + --[[if GetConVar("pac_tree_lazymode"):GetBool() then + timer.Simple(0, function() + for i,v in pairs(pac.GetLocalParts()) do + v.no_populate = false + v.dormant_node = false + end + pace.RefreshTree(true) + end) + end]] if pace.tree_search_open then return end pace.Editor.y_offset = 24 pace.tree_search_open = true From d030e0e652208b98c353ea707c03ebf7628e94e8 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 16 Dec 2024 00:08:18 -0500 Subject: [PATCH 252/300] fixes for command part and luapad luapad QoL -"run" button now repositions with performlayout -add more buttons for whether to remember position/size, and whether to auto-kill the luapad panel when dragging the view -trim newlines to help the property's display -drag check on the resizes -prevent resizing to smaller than 100 pixels -compile only when pressing enter, the run button or exiting the window. no more errors spammed for every new character typed command update -make onshow, delayed strings work with lua -limit the error message for when sv_allowcslua 0 -error handling applied to luapad. it does change the compilestring handleError arg but there's not much lost here. end users don't care about the extra lines that were printed from the stacktrace (they were only UI backend functions, not the code worked on) --- lua/pac3/core/client/parts/command.lua | 112 ++++++++++-- lua/pac3/core/client/parts/proxy.lua | 6 + .../editor/client/panels/extra_properties.lua | 167 +++++++++++++++--- lua/pac3/editor/client/panels/properties.lua | 1 + lua/pac3/editor/client/view.lua | 2 +- 5 files changed, 249 insertions(+), 39 deletions(-) diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index e5b3c58f7..7f66f24b0 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -112,12 +112,79 @@ function PART:SetUseLua(b) self:SetString(self:GetString()) end -function PART:SetString(str) - if self.UseLua and canRunLua() and self:GetPlayerOwner() == pac.LocalPlayer then - self.func = CompileString(str, "pac_event") +function PART:HandleErrors(result, mode) + if isstring(result) then + pac.Message(result) + self.Error = "[" .. mode .. "] " .. result + self.erroring_mode = mode + self:SetError(result) + if pace.ActiveSpecialPanel and pace.ActiveSpecialPanel.luapad then + pace.ActiveSpecialPanel.special_title = self.Error + end + elseif isfunction(result) then + if pace.ActiveSpecialPanel and pace.ActiveSpecialPanel.luapad then + if not self.Error then --good compile + pace.ActiveSpecialPanel.special_title = "[" .. mode .. "] " .. "successfully compiled" + self.Error = nil + self:SetError() + elseif (self.erroring_mode~= nil and self.erroring_mode ~= mode) then --good compile but already had an error from somewhere else (there are 3 script areas: main, onhide, delayed) + pace.ActiveSpecialPanel.special_title = "successfully compiled, but another erroring script may remain at " .. self.erroring_mode + else -- if we fixed our previous error from the same mode + pace.ActiveSpecialPanel.special_title = "[" .. mode .. "] " .. "successfully compiled" + self.Error = nil + self:SetError() + end + end end +end +function PART:SetString(str) + str = string.Trim(str,"\n") + self.func = nil + if self.UseLua and canRunLua() and self:GetPlayerOwner() == pac.LocalPlayer and str ~= "" then + self.func = CompileString(str, "pac_event", false) + self:HandleErrors(self.func, "Main string") + end self.String = str + if self.UseLua and not canRunLua() then + self:SetError("clientside lua is disabled (sv_allowcslua 0)") + end +end + +function PART:SetOnHideString(str) + str = string.Trim(str,"\n") + self.onhide_func = nil + if self.erroring_mode == "OnHide string" then self.erroring_mode = nil end + if self.UseLua and canRunLua() and self:GetPlayerOwner() == pac.LocalPlayer and str ~= "" then + self.onhide_func = CompileString(str, "pac_event", false) + self:HandleErrors(self.onhide_func, "OnHide string") + end + self.OnHideString = str + if self.UseLua and not canRunLua() then + self:SetError("clientside lua is disabled (sv_allowcslua 0)") + end +end + +function PART:SetDelayedString(str) + str = string.Trim(str,"\n") + self.delayed_func = nil + if self.erroring_mode == "Delayed string" then self.erroring_mode = nil end + if self.UseLua and canRunLua() and self:GetPlayerOwner() == pac.LocalPlayer and str ~= "" then + self.delayed_func = CompileString(str, "pac_event", false) + self:HandleErrors(self.delayed_func, "Delayed string") + end + self.DelayedString = str + if self.UseLua and not canRunLua() then + self:SetError("clientside lua is disabled (sv_allowcslua 0)") + end +end + +function PART:Initialize() + --yield for the compile until other vars are available (UseLua) + timer.Simple(0, function() + self:SetOnHideString(self:GetOnHideString()) + self:SetDelayedString(self:GetDelayedString()) + end) end function PART:GetCode() @@ -160,22 +227,39 @@ function PART:GetNiceName() return "command: " .. self.String end +local function try_lua_exec(self, func) + if canRunLua() then + if isstring(func) then return end + local status, err = pcall(func) + + if not status then + self:SetError(err) + ErrorNoHalt(err .. "\n") + end + else + local msg = "clientside lua is disabled (sv_allowcslua 0)" + self:SetError(msg) + pac.Message(tostring(self) .. " - ".. msg) + end +end + function PART:Execute(commandstring) local ent = self:GetPlayerOwner() if ent == pac.LocalPlayer then - if self.UseLua and self.func then - if canRunLua() then - local status, err = pcall(self.func) - - if not status then - self:SetError(err) - ErrorNoHalt(err .. "\n") + if self.UseLua then + if (self.func or self.onhide_func or self.delayed_func) then + if commandstring == nil then --regular string + try_lua_exec(self, self.func) + else --other modes + if ((commandstring == self.OnHideString) and self.onhide_func) then + try_lua_exec(self, self.onhide_func) + elseif ((commandstring == self.DelayedString) and self.delayed_func) then + try_lua_exec(self, self.delayed_func) + elseif ((commandstring == self.String) and self.func) then + try_lua_exec(self, self.func) + end end - else - local msg = "clientside lua is disabled (sv_allowcslua 0)" - self:SetError(msg) - pac.Message(tostring(self) .. " - ".. msg) end else if hook.Run("PACCanRunConsoleCommand", self.String) == false then return end diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 8c320c51c..8fbb3f928 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -1595,6 +1595,7 @@ local allowed = { } function PART:SetExpression(str, slot) + str = string.Trim(str,"\n") if not slot then --that's the default expression self.Expression = str self.ExpressionFunc = nil @@ -1659,26 +1660,31 @@ function PART:SetExpression(str, slot) end function PART:SetExpressionOnHide(str) + str = string.Trim(str,"\n") self.ExpressionOnHide = str self:SetExpression(str, 0) end function PART:SetExtra1(str) + str = string.Trim(str,"\n") self.Extra1 = str self:SetExpression(str, 1) end function PART:SetExtra2(str) + str = string.Trim(str,"\n") self.Extra2 = str self:SetExpression(str, 2) end function PART:SetExtra3(str) + str = string.Trim(str,"\n") self.Extra3 = str self:SetExpression(str, 3) end function PART:SetExtra4(str) + str = string.Trim(str,"\n") self.Extra4 = str self:SetExpression(str, 4) end diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index f86ab3130..1e98faf65 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -645,8 +645,15 @@ do --generic multiline text pace.RegisterPanel(PANEL) end + +local calcdrag_remove_pnl = CreateClientConVar("pac_luapad_calcdrag_removal", "1", true, false, "whether dragging view should remove the luapad") + +local lua_editor_txt = "" +local lua_editor_previous_dimensions local lua_editor_fontsize = 16 -local function install_fontsize_buttons(frame, editor, add_execute) +local function install_fontsize_buttons(frame, editor, add_execute, key) + frame.ignore_saferemovespecialpanel = not calcdrag_remove_pnl:GetBool() + local btn_fontplus = vgui.Create("DButton", frame) btn_fontplus:SetSize(20, 18) btn_fontplus:SetText("+") local btn_fontminus = vgui.Create("DButton", frame) btn_fontminus:SetSize(20, 18) btn_fontminus:SetText("-") function btn_fontplus:DoClick() @@ -663,8 +670,43 @@ local function install_fontsize_buttons(frame, editor, add_execute) surface.SetFont("LuapadEditor"); editor.FontWidth, editor.FontHeight = surface.GetTextSize(" ") end + + + local btn_remember_dimensions = vgui.Create("DButton", frame) btn_remember_dimensions:SetSize(18, 18) btn_remember_dimensions:SetImage("icon16/computer_link.png") + btn_remember_dimensions:SetTooltip("Remember winbdow size") btn_remember_dimensions:SetY(3) + function btn_remember_dimensions:DoClick() + if lua_editor_previous_dimensions == nil then + local x,y = frame:LocalToScreen() + lua_editor_previous_dimensions = { + x = x, + y = y, + wide = frame:GetWide(), + tall = frame:GetTall() + } + frame.special_title = "will remember position and size" timer.Simple(3, function() if not IsValid(frame) then return end frame.special_title = nil end) + else + lua_editor_previous_dimensions = nil + frame.special_title = "will not remember window position and size" timer.Simple(3, function() if not IsValid(frame) then return end frame.special_title = nil end) + end + end + if lua_editor_previous_dimensions then + btn_remember_dimensions:SetImage("icon16/computer_delete.png") + end + + local btn_calcdrag_remove = vgui.Create("DButton", frame) btn_calcdrag_remove:SetSize(18, 18) btn_calcdrag_remove:SetImage("icon16/application_delete.png") + btn_calcdrag_remove:SetTooltip("Close window if dragging main view") btn_calcdrag_remove:SetY(3) + function btn_calcdrag_remove:DoClick() + calcdrag_remove_pnl:SetBool(not calcdrag_remove_pnl:GetBool()) + frame.ignore_saferemovespecialpanel = not calcdrag_remove_pnl:GetBool() + if calcdrag_remove_pnl:GetBool() then + frame.special_title = "will remove window if dragging main view" timer.Simple(3, function() if not IsValid(frame) then return end frame.special_title = nil end) + else + frame.special_title = "will not remove window if dragging main view" timer.Simple(3, function() if not IsValid(frame) then return end frame.special_title = nil end) + end + end + + local perflayout = frame.PerformLayout - local fthink = frame.Think btn_fontplus:SetY(3) btn_fontminus:SetY(3) @@ -672,17 +714,50 @@ local function install_fontsize_buttons(frame, editor, add_execute) local btn_run = vgui.Create("DButton", frame) btn_run:SetSize(50, 18) btn_run:SetY(3) btn_run:SetImage("icon16/bullet_go.png") btn_run:SetText(" run") function btn_run:DoClick() - pace.current_part:Execute() + if key then + if key == "String" then + pace.current_part:SetString(editor:GetValue()) + elseif key == "OnHideString" then + pace.current_part:SetOnHideString(editor:GetValue()) + elseif key == "DelayedString" then + pace.current_part:SetDelayedString(editor:GetValue()) + end + timer.Simple(0.2, function() pace.current_part:Execute(pace.current_part[key]) end) + else + pace.current_part:Execute() + end end - function frame:Think() + function frame:PerformLayout() + if lua_editor_previous_dimensions then + local x,y = frame:LocalToScreen() + lua_editor_previous_dimensions = { + x = x, + y = y, + wide = frame:GetWide(), + tall = frame:GetTall() + } + end + btn_calcdrag_remove:SetX(self:GetWide() - 230 + 4) + btn_remember_dimensions:SetX(self:GetWide() - 210 + 4) btn_run:SetX(self:GetWide() - 190 + 4) btn_fontplus:SetX(self:GetWide() - 120 + 4) btn_fontminus:SetX(self:GetWide() - 140 + 4) - fthink(self) + perflayout(self) end frame:RequestFocus() else function frame:PerformLayout() + if lua_editor_previous_dimensions then + local x,y = frame:LocalToScreen() + lua_editor_previous_dimensions = { + x = x, + y = y, + wide = frame:GetWide(), + tall = frame:GetTall() + } + end + btn_calcdrag_remove:SetX(self:GetWide() - 180 + 4) + btn_remember_dimensions:SetX(self:GetWide() - 160 + 4) btn_fontplus:SetX(self:GetWide() - 120 + 4) btn_fontminus:SetX(self:GetWide() - 140 + 4) perflayout(self) @@ -763,6 +838,7 @@ do -- script pace.RegisterPanel(PANEL) end + local function install_edge_resizes(frame) local function more_or_less(n1,n2) return math.abs(n1-n2) < 10 @@ -777,10 +853,11 @@ local function install_edge_resizes(frame) frame.resizing_left = false frame.resizing_right = false elseif frame.resizing then + if pace.dragging then return end if frame.resizing_down then - frame:SetHeight(my-py) + frame:SetHeight(math.max(my-py, 100)) elseif frame.resizing_left then - frame:SetWide(frame.target_edge - mx) + frame:SetWide(math.max(frame.target_edge - mx, 100)) frame:SetX(mx) elseif frame.resizing_right then frame:SetWide(mx-px) @@ -869,20 +946,54 @@ do -- script command local editor = vgui.Create("pace_luapad", frame) frame.luapad = editor - install_fontsize_buttons(frame, editor, true) + install_fontsize_buttons(frame, editor, true, self.CurrentKey) editor:Dock(FILL) - if pace.Editor:IsLeft() then - frame:SetSize(ScrW() - pace.Editor:GetX() - pace.Editor:GetWide(),200) - frame:SetPos(pace.Editor:GetWide() + pace.Editor:GetX(), select(2, self:LocalToScreen())) + + if lua_editor_previous_dimensions ~= nil then + frame:SetPos(lua_editor_previous_dimensions.x,lua_editor_previous_dimensions.y) + frame:SetSize(lua_editor_previous_dimensions.wide,lua_editor_previous_dimensions.tall) else - frame:SetSize(pace.Editor:GetX(),200) - frame:SetPos(0, select(2, self:LocalToScreen())) + if pace.Editor:IsLeft() then + frame:SetSize(ScrW() - pace.Editor:GetX() - pace.Editor:GetWide(),200) + frame:SetPos(pace.Editor:GetWide() + pace.Editor:GetX(), select(2, self:LocalToScreen())) + else + frame:SetSize(pace.Editor:GetX(),200) + frame:SetPos(0, select(2, self:LocalToScreen())) + end end - - editor:SetText(pace.current_part:GetCode()) - editor.OnTextChanged = function(self) - pace.current_part:SetString(self:GetValue()) + editor:SetText(part[self.CurrentKey]) + local pnl = self + if pnl.CurrentKey == "String" then + editor.OnTextChanged = function(self) + local str = self:GetValue():Trim("\n") + if input.IsButtonDown(KEY_ENTER) then part:SetString(str) end + pnl:SetValue(str) + end + editor.OnRemove = function(self) + local str = self:GetValue():Trim("\n") + part:SetString(str) + end + elseif pnl.CurrentKey == "OnHideString" then + editor.OnTextChanged = function(self) + local str = self:GetValue():Trim("\n") + if input.IsButtonDown(KEY_ENTER) then part:SetOnHideString(str) end + pnl:SetValue(str) + end + editor.OnRemove = function(self) + local str = self:GetValue():Trim("\n") + part:SetOnHideString(str) + end + elseif pnl.CurrentKey == "DelayedString" then + editor.OnTextChanged = function(self) + local str = self:GetValue():Trim("\n") + if input.IsButtonDown(KEY_ENTER) then part:SetDelayedString(str) end + pnl:SetValue(str) + end + editor.OnRemove = function(self) + local str = self:GetValue():Trim("\n") + part:SetDelayedString(str) + end end editor.last_error = "" @@ -902,6 +1013,9 @@ do -- script command else title = L"command" .. " (console)" end + if frame:GetTitle() == "successfully compiled" then + title = "(lua) successfully compiled" + end if part.Error then title = part.Error @@ -924,7 +1038,7 @@ do -- script command part.script_printing = nil end end - + title = frame.special_title or title frame:SetTitle(title) end @@ -1000,12 +1114,17 @@ do -- script proxy frame.luapad = editor install_fontsize_buttons(frame, editor) editor:Dock(FILL) - if pace.Editor:IsLeft() then - frame:SetSize(ScrW() - pace.Editor:GetX() - pace.Editor:GetWide(),200) - frame:SetPos(pace.Editor:GetWide() + pace.Editor:GetX(), select(2, self:LocalToScreen())) + if lua_editor_previous_dimensions ~= nil then + frame:SetPos(lua_editor_previous_dimensions.x,lua_editor_previous_dimensions.y) + frame:SetSize(lua_editor_previous_dimensions.wide,lua_editor_previous_dimensions.tall) else - frame:SetSize(pace.Editor:GetX(),200) - frame:SetPos(0, select(2, self:LocalToScreen())) + if pace.Editor:IsLeft() then + frame:SetSize(ScrW() - pace.Editor:GetX() - pace.Editor:GetWide(),200) + frame:SetPos(pace.Editor:GetWide() + pace.Editor:GetX(), select(2, self:LocalToScreen())) + else + frame:SetSize(pace.Editor:GetX(),200) + frame:SetPos(0, select(2, self:LocalToScreen())) + end end editor:SetText(part["Get"..key](part)) @@ -1052,7 +1171,7 @@ do -- script proxy part.script_printing = nil end end - + title = frame.special_title or title frame:SetTitle(title) end diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index ffb09e4e0..582d758eb 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -346,6 +346,7 @@ end pac.AddHook("GUIMousePressed", "pace_SafeRemoveSpecialPanel", function() local pnl = pace.ActiveSpecialPanel if pnl:IsValid() then + if pnl.ignore_saferemovespecialpanel then return end local x,y = input.GetCursorPos() local _x, _y = pnl:GetPos() if x < _x or y < _y or x > _x + pnl:GetWide() or y > _y + pnl:GetTall() then diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 1d36dab55..63ce185c4 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -159,7 +159,7 @@ local function CalcDrag() if pace.BusyWithProperties:IsValid() or - pace.ActiveSpecialPanel:IsValid() or + (pace.ActiveSpecialPanel:IsValid() and not pace.ActiveSpecialPanel.ignore_saferemovespecialpanel) or pace.editing_viewmodel or pace.editing_hands or pace.properties.search:HasFocus() From cc9ebcc935b4714e4ba2904bb2851631d2974d88 Mon Sep 17 00:00:00 2001 From: techbot Date: Mon, 16 Dec 2024 22:52:43 +0100 Subject: [PATCH 253/300] get rid of the unused linter workflow [skip ci] --- .github/workflows/update_workshop.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/update_workshop.yaml b/.github/workflows/update_workshop.yaml index 04d674a49..f8501c44e 100644 --- a/.github/workflows/update_workshop.yaml +++ b/.github/workflows/update_workshop.yaml @@ -8,12 +8,8 @@ on: workflow_dispatch: jobs: - linter: - uses: CapsAdmin/pac3/.github/workflows/lua_linter.yaml@develop - update-workshop: if: github.repository == 'CapsAdmin/pac3' - # needs: linter runs-on: ubuntu-latest steps: - name: Checkout From 154a8d3340b7aee0e1cc724585c3a8f4b23b1a85 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 23 Dec 2024 22:35:49 -0500 Subject: [PATCH 254/300] editor view features, fixes and more usable in shortcuts: -orthographic view -entity follow angle: forward and sideview variations -reset_eyeang, reset_eyeang_pitch orthographic view major culling issues usable in partmenu actions: -view go to: move view to a certain axis -view lock on: point camera to target. different pitch control modes (zero pitch, free pitch, direct, frame of reference) partmenu config menu: right click to insert an action at the beginning of the list new movement bind: roll view (unbound by default). drag to tilt, it wraps around the screen, tap to reset. roll applies post and doesn't impact pace.ViewAngles. there's issues with both this and the alternative, I chose the former. more quicksetups proxy: basic feedback controllers base_movables on camera bone: suggest use of event viewed_by_owner, suggest distance-based fade setup remember editor width option fix remember property divider position: preserve across games multipurpose pace.GoTo function: moves camera to point toward an object, or navigate to a part in the tree, or property in the properties fix plain disable editor cam (the camera part rework jank kinda broke it) --- lua/pac3/editor/client/menu_bar.lua | 3 + lua/pac3/editor/client/panels/editor.lua | 72 ++++ lua/pac3/editor/client/parts.lua | 184 +++++++++- lua/pac3/editor/client/settings.lua | 57 +++- lua/pac3/editor/client/shortcuts.lua | 53 +++ lua/pac3/editor/client/view.lua | 406 ++++++++++++++++++++++- 6 files changed, 752 insertions(+), 23 deletions(-) diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index 376f44665..aa4bb1a57 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -196,8 +196,11 @@ local function populate_options(menu) menu:AddCVar(L"Keyboard shortcuts: Legacy mode", "pac_editor_shortcuts_legacy_mode", "1", "0") menu:AddCVar(L"inverse collapse/expand controls", "pac_reverse_collapse", "1", "0") menu:AddCVar(L"enable shift+move/rotate clone", "pac_grab_clone", "1", "0") + menu:AddCVar(L"remember editor position", "pac_editor_remember_position", "1", "0") menu:AddCVar(L"remember divider position", "pac_editor_remember_divider_height", "1", "0") + menu:AddCVar(L"remember editor width", "pac_editor_remember_width", "1", "0") + menu:AddCVar(L"ask before loading autoload", "pac_prompt_for_autoload", "1", "0") local prop_pac_load_mode, pnlpplm = menu:AddSubMenu("(singleplayer only) How to handle prop/npc outfits", function() end) diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index 48292c139..1d7681c1b 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -17,7 +17,9 @@ local use_tabs = CreateClientConVar("pac_property_tabs", 1, true) local zoom_persistent = CreateClientConVar("pac_zoom_persistent", 0, true, false, 'Keep zoom between sessions.') local zoom_mousewheel = CreateClientConVar("pac_zoom_mousewheel", 0, true, false, 'Enable zooming with mouse wheel.') local zoom_smooth = CreateClientConVar("pac_zoom_smooth", 0, true, false, 'Enable smooth zooming.') + local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's vertical divider position") +local remember_width = CreateConVar("pac_editor_remember_width", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's width") function PANEL:Init() self:SetTitle("") @@ -102,6 +104,46 @@ function PANEL:Init() self.smoothlabel:SetWrap(true) self.smoothlabel:SetAutoStretchVertical(true) + self.orthocheckbox = vgui.Create("DCheckBoxLabel", self.zoomsettings) + self.orthocheckbox:SetText("Orthographic") + self.orthocheckbox:Dock(TOP) + self.orthocheckbox:SetDark(true) + self.orthocheckbox:DockMargin(0,SETTING_MARGIN_TOP,0,0) + self.orthocheckbox:SetConVar("pac_camera_orthographic") + self.orthocheckbox:SetTooltip("Orthographic view projects parallel rays perpendicular to a rectangle. Instead of degrees, it is in terms of distance units (Hammer Units)\n\nThere are still —possibly engine-related— issues where objects and world geometry can disapear if looking from the wrong angle due to culling. Especially worse in tight spaces.") + + self.ortholabel = vgui.Create("DLabel", self.zoomsettings) + self.ortholabel:Dock(TOP) + self.ortholabel:SetDark(true) + self.ortholabel:SetText("Enable orthographic view.") + self.ortholabel:SetWrap(true) + self.ortholabel:SetAutoStretchVertical(true) + + self.ortho_nearz = vgui.Create("DNumSlider", self.zoomsettings) + self.ortho_nearz:Dock(TOP) + self.ortho_nearz:SetMin( 0 ) + self.ortho_nearz:SetMax( 5000 ) + self.ortho_nearz:SetDecimals( 1 ) + self.ortho_nearz:SetText("NearZ") + self.ortho_nearz:SetDark(true) + self.ortho_nearz:SetDefaultValue( 0 ) + self.ortho_nearz:SetValue( 0 ) + + self.ortho_farz = vgui.Create("DNumSlider", self.zoomsettings) + self.ortho_farz:Dock(TOP) + self.ortho_farz:SetMin( 0 ) + self.ortho_farz:SetMax( 64000 ) + self.ortho_farz:SetDecimals( 1 ) + self.ortho_farz:SetText("FarZ") + self.ortho_farz:SetDark(true) + self.ortho_farz:SetDefaultValue( 64000 ) + self.ortho_farz:SetValue( 64000 ) + if not pace.camera_orthographic then + self.ortho_nearz:Hide() + self.ortho_farz:Hide() + end + + self.sliderpanel = vgui.Create("DPanel", self.zoomframe) self.sliderpanel:SetSize(180, 20) self.sliderpanel:Dock(TOP) @@ -113,6 +155,11 @@ function PANEL:Init() self.zoomslider:SetMax( 100 ) self.zoomslider:SetDecimals( 0 ) self.zoomslider:SetText("Camera FOV") + if pace.camera_orthographic then + self.zoomslider:SetText("Ortho. Width") + self.zoomslider:SetMin( -10000 ) + self.zoomslider:SetMax( 10000 ) + end self.zoomslider:SetDark(true) self.zoomslider:SetDefaultValue( 75 ) @@ -129,6 +176,13 @@ function PANEL:Init() self:SetCookieName("pac3_editor") self:SetPos(self:GetCookieNumber("x"), BAR_SIZE) + if remember_width:GetBool() then + self.init_w = math.max(self:GetCookieNumber("width"), 200) + end + if remember_divider:GetBool() then + pace.vertical_div_height = self:GetCookieNumber("y_divider") + end + self:MakeBar() self.lastTopBarHover = 0 self.rendertime_data = {} @@ -177,6 +231,10 @@ function PANEL:OnRemove() pace.vertical_div_height = self.div:GetTopHeight() end + if remember_width:GetBool() then + pace.editor_width = math.max(self:GetWide(), 200) + end + if self.menu_bar:IsValid() then self.menu_bar:Remove() end @@ -212,6 +270,12 @@ function PANEL:Think(...) self:SetTall(ScrH() - (self.y_offset or 0)) local w = math.max(self:GetWide(), 200) + + --wtf the GetWide isn't saved on Init??? I have to do this? + if self.init_w then + w = self.init_w + self.init_w = nil + end self:SetWide(w) self:SetPos(math.Clamp(self:GetPos(), 0, ScrW() - w), (self.y_offset or 0)) @@ -219,6 +283,14 @@ function PANEL:Think(...) self:SetCookie("x", x) self.last_x = x end + if w ~= self.last_w then + self:SetCookie("width", w) + self.last_w = w + end + if pace.vertical_div_height ~= self.last_vertical_div_height then + self:SetCookie("y_divider", pace.vertical_div_height) + self.last_vertical_div_height = pace.vertical_div_height + end if self.exit_button:IsValid() then diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 72524dc58..0e8081fba 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"} +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_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"} @@ -132,6 +132,7 @@ local function DrawHaloHighlight(tbl) if not pace.Active then pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights") end + if pace.camera_orthographic then return end --Find out the color and apply the halo local color_string = GetConVar("pac_hover_color"):GetString() @@ -2548,6 +2549,15 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) local main, pnlmain = menu:AddSubMenu("quick setups") pnlmain:SetIcon("icon16/basket_go.png") --base_movables can restructure, but nah bones aint it if obj.GetDrawPosition and obj.ClassName ~= "bone" and obj.ClassName ~= "bone2" and obj.ClassName ~= "bone3" then + if obj.Bone and obj.Bone == "camera" then + main:AddOption("camera bone suggestion: limit view to yourself", function() + local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(obj) + end):SetImage("icon16/star.png") + if obj.GetAlpha then main:AddOption("camera bone suggestion: fade with distance", function() + local model = pac.CreatePart("model2") model:SetModel("models/empty.mdl") model:SetParent(obj.Parent) model:SetName("head_position") + local proxy = pac.CreatePart("proxy") proxy:SetExpression("clamp(2 - (part_distance(\"head_position\")/100),0,1)") proxy:SetParent(obj) proxy:SetVariableName("Alpha") + end):SetImage("icon16/star.png") end + end local substitutes, pnl = main:AddSubMenu("Restructure / Create parent substitute", function() pace.SubstituteBaseMovable(obj, "create_parent") timer.Simple(20, function() if pace.recently_substituted_movable_part == obj then pace.recently_substituted_movable_part = nil end end) @@ -2654,6 +2664,61 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) end):SetIcon("icon16/asterisk_yellow.png") elseif obj.ClassName == "proxy" then pnlmain:SetTooltip("remember you also have a preset library by right clicking on the expression field") + main:AddOption("basic feedback controller setup", function() + Derma_StringRequest("What should we call this controller variable?", "Type a name for the commands.\nThese number ranges would be appropriate for positions\nIf you make more, name them something different", "speed", function(str) + if str == "" then return end if str == " " then return end + local cmdforward = pac.CreatePart("command") cmdforward:SetParent(obj) + cmdforward:SetString("pac_proxy " .. str .. " 100") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("up") btn:SetParent(cmdforward) + + local cmdback = pac.CreatePart("command") cmdback:SetParent(obj) + cmdback:SetString("pac_proxy " .. str .. " -100") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("down") btn:SetParent(cmdback) + + local cmdneutral = pac.CreatePart("command") cmdneutral:SetParent(obj) + cmdneutral:SetString("pac_proxy " .. str .. " 0") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("up") btn:SetParent(cmdneutral) btn:SetInvert(false) + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("down") btn:SetParent(cmdneutral) btn:SetInvert(false) + + obj:SetExpression("feedback() + ftime()*command(\"".. str .. "\")") + end) + end):SetIcon("icon16/joystick.png") + main:AddOption("2D feedback controller setup", function() + Derma_StringRequest("What should we call this controller variable?", "Type a name for the commands.\nThese number ranges would be appropriate for positions\nIf you make more, name them something different", "speed", function(str) + if str == "" then return end if str == " " then return end + local cmdforward = pac.CreatePart("command") cmdforward:SetParent(obj) + cmdforward:SetString("pac_proxy " .. str .. "_x" .. " 100") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("up") btn:SetParent(cmdforward) + + local cmdback = pac.CreatePart("command") cmdback:SetParent(obj) + cmdback:SetString("pac_proxy " .. str .. "_x" .. " -100") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("down") btn:SetParent(cmdback) + + local cmdneutral = pac.CreatePart("command") cmdneutral:SetParent(obj) + cmdneutral:SetString("pac_proxy " .. str .. "_x" .. " 0") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("up") btn:SetParent(cmdneutral) btn:SetInvert(false) + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("down") btn:SetParent(cmdneutral) btn:SetInvert(false) + + + local cmdright = pac.CreatePart("command") cmdright:SetParent(obj) + cmdright:SetString("pac_proxy " .. str .. "_y" .. " 100") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("right") btn:SetParent(cmdright) + + local cmdleft = pac.CreatePart("command") cmdleft:SetParent(obj) + cmdleft:SetString("pac_proxy " .. str .. "_y" .. " -100") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("left") btn:SetParent(cmdleft) + + local cmdneutral = pac.CreatePart("command") cmdneutral:SetParent(obj) + cmdneutral:SetString("pac_proxy " .. str .. "_y" .. " 0") + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("left") btn:SetParent(cmdneutral) btn:SetInvert(false) + local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("right") btn:SetParent(cmdneutral) btn:SetInvert(false) + obj:SetExpression( + "feedback_x() + ftime()*command(\"".. str .. "_x\")" + .. "," .. + "feedback_y() + ftime()*command(\"".. str .. "_y\")" + ) + end) + end):SetIcon("icon16/joystick.png") main:AddOption("command feedback attractor setup (-100, -50, 0, 50, 100)", function() Derma_StringRequest("What should we call this attractor?", "Type a name for the commands.\nThese number ranges would be appropriate for positions\nIf you make more, name them something different", "target_number", function(str) if str == "" then return end if str == " " then return end @@ -3960,6 +4025,123 @@ function pace.addPartMenuComponent(menu, obj, option_name) substitute:AddOption("Switch with another (select two parts with bulk select)", function() pace.SwapBaseMovables(pace.BulkSelectList[1], pace.BulkSelectList[2], false) end) substitute:AddOption("Recast into new class (warning!)", function() pace.SubstituteBaseMovable(obj, "cast") end) end + elseif option_name == "view_lockon" then + if not obj then return end + local function add_entity_version(obj, root_owner) + local root_owner = obj:GetRootPart():GetOwner() + local lockons, pnl2 = menu:AddSubMenu("lock on to " .. tostring(root_owner)) + local function viewlock(mode) + if mode ~= "toggle" then + pace.viewlock_mode = mode + else + if pace.viewlock then + if pace.viewlock ~= root_owner then + pace.viewlock = root_owner + return + end + pace.viewlock = nil + return + end + pace.viewlock = root_owner + end + if mode == "disable" then + pace.viewlock = nil + return + end + pace.viewlock_distance = pace.ViewPos:Distance(root_owner:GetPos() + root_owner:OBBCenter()) + pace.viewlock = root_owner + end + lockons:AddOption("direct", function() viewlock("direct") end):SetImage("icon16/arrow_right.png") + lockons:AddOption("free pitch", function() viewlock("free pitch") end):SetImage("icon16/arrow_refresh.png") + lockons:AddOption("zero pitch", function() viewlock("zero pitch") end):SetImage("icon16/arrow_turn_right.png") + lockons:AddOption("disable", function() viewlock("disable") end):SetImage("icon16/cancel.png") + pnl2:SetImage("icon16/zoom.png") + end + local function add_part_version(obj) + local lockons, pnl2 = menu:AddSubMenu("lock on to " .. tostring(obj)) + local function viewlock(mode) + if mode ~= "toggle" then + pace.viewlock_mode = mode + else + if pace.viewlock then + if pace.viewlock ~= obj then + pace.viewlock = obj + return + end + pace.viewlock = nil + return + end + pace.viewlock = obj + end + if mode == "disable" then + pace.viewlock = nil + return + end + pace.viewlock_distance = pace.ViewPos:Distance(obj:GetWorldPosition()) + pace.viewlock = obj + end + lockons:AddOption("direct", function() viewlock("direct") end):SetImage("icon16/arrow_right.png") + lockons:AddOption("free pitch", function() viewlock("free pitch") end):SetImage("icon16/arrow_refresh.png") + lockons:AddOption("zero pitch", function() viewlock("zero pitch") end):SetImage("icon16/arrow_turn_right.png") + lockons:AddOption("frame of reference (x)", function() pace.viewlock_axis = "x" viewlock("frame of reference") end):SetImage("icon16/arrow_branch.png") + lockons:AddOption("frame of reference (y)", function() pace.viewlock_axis = "y" viewlock("frame of reference") end):SetImage("icon16/arrow_branch.png") + lockons:AddOption("frame of reference (z)", function() pace.viewlock_axis = "z" viewlock("frame of reference") end):SetImage("icon16/arrow_branch.png") + lockons:AddOption("disable", function() viewlock("disable") end):SetImage("icon16/cancel.png") + pnl2:SetImage("icon16/zoom.png") + end + local is_root_entity = obj:GetOwner() == obj:GetRootPart():GetOwner() + if obj.ClassName == "group" then + if is_root_entity then + add_entity_version(obj, obj:GetRootPart():GetOwner()) + elseif obj:GetOwner().GetWorldPosition then + add_part_version(obj:GetOwner()) + end + elseif obj.GetWorldPosition then + add_part_version(obj) + end + elseif option_name == "view_goto" then + if not obj then return end + local is_root_entity = obj:GetOwner() == obj:GetRootPart():GetOwner() + if obj.ClassName == "group" then + if is_root_entity then + local gotos, pnl2 = menu:AddSubMenu("go to") + pnl2:SetImage("icon16/arrow_turn_right.png") + local axes = {"x","y","z","world_x","world_y","world_z"} + for _,ax in ipairs(axes) do + gotos:AddOption("+" .. ax, function() + pace.GoTo(obj:GetRootPart():GetOwner(), "view", {radius = 50, axis = ax}) + end):SetImage("icon16/arrow_turn_right.png") + gotos:AddOption("-" .. ax, function() + pace.GoTo(obj:GetRootPart():GetOwner(), "view", {radius = -50, axis = ax}) + end):SetImage("icon16/arrow_turn_right.png") + end + elseif obj:GetOwner().GetWorldPosition then + local gotos, pnl2 = menu:AddSubMenu("go to") + pnl2:SetImage("icon16/arrow_turn_right.png") + local axes = {"x","y","z","world_x","world_y","world_z"} + for _,ax in ipairs(axes) do + gotos:AddOption("+" .. ax, function() + pace.GoTo(obj, "view", {radius = 50, axis = ax}) + end):SetImage("icon16/arrow_turn_right.png") + gotos:AddOption("-" .. ax, function() + pace.GoTo(obj, "view", {radius = -50, axis = ax}) + end):SetImage("icon16/arrow_turn_right.png") + end + end + elseif obj.GetWorldPosition then + local gotos, pnl2 = menu:AddSubMenu("go to") + pnl2:SetImage("icon16/arrow_turn_right.png") + local axes = {"x","y","z","world_x","world_y","world_z"} + for _,ax in ipairs(axes) do + gotos:AddOption("+" .. ax, function() + pace.GoTo(obj, "view", {radius = 50, axis = ax}) + end):SetImage("icon16/arrow_turn_right.png") + gotos:AddOption("-" .. ax, function() + pace.GoTo(obj, "view", {radius = -50, axis = ax}) + end):SetImage("icon16/arrow_turn_right.png") + end + end + end end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index e6ff678e2..613919b7a 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -32,6 +32,7 @@ local convar_params_damage_zone = { {"pac_sv_damage_zone_max_length", "Max damage zone length", "", 0, 0, 32767}, {"pac_sv_damage_zone_max_damage", "Max damage zone damage", "", 0, 0, 268435455}, {"pac_sv_damage_zone_allow_dissolve", "Allow damage entity dissolvers", "", -1, 0, 200}, + {"pac_sv_damage_zone_allow_ragdoll_hitparts", "Allow ragdoll hitparts", "", -1, 0, 200}, } local convar_params_force = { {"pac_sv_force", "Allow force part", "", -1, 0, 200}, @@ -81,6 +82,7 @@ local convar_params_wearing_drawing = { {"pac_submit_spam", "Limit pac_submit to prevent spam", "", -1, 0, 200}, {"pac_submit_limit", "limit of pac_submits", "", 0, 0, 100}, {"pac_onuse_only_force", "Players need to +USE on others to reveal outfits", "", -1, 0, 200}, + {"pac_sv_prop_outfits", "allow prop / other player outfits", "0 = don't allow\n1 = allow applying outfits on props/npcs\n2 = allow applying outfits on other players", 0, 0, 2}, } local convar_params_misc = { {"sv_pac_webcontent_allow_no_content_length", "Players need to +USE on others to reveal outfits", "", -1, 0, 200}, @@ -1425,6 +1427,10 @@ function pace.FillEditorSettings(pnl) 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 @@ -1435,11 +1441,24 @@ function pace.FillEditorSettings(pnl) local pnl = vgui.Create("DButton", f) pnl:SetText(string.Replace(string.upper(v),"_"," ")) pnl:SetImage(FindImage(v)) + pnl:SetTooltip("Left click to add at the end\nRight click to insert at the beginning") function pnl:DoClick() table.insert(buildlist_partmenu,v) partmenu_previews:AddLine(#buildlist_partmenu,v) end + function pnl:DoRightClick() + table.insert(buildlist_partmenu,1,v) + local previous_list = {} + for i,v in ipairs(partmenu_previews:GetLines()) do + table.insert(previous_list,v:GetValue(2)) + end + ClearPartMenuPreviewList() + partmenu_previews:AddLine(1,v) + for i,v in ipairs(previous_list) do + partmenu_previews:AddLine(i+1,v) + end + end partmenu_choices:AddItem(pnl) pnl:SetHeight(18) pnl:SetWidth(200) @@ -1561,7 +1580,8 @@ function pace.FillEditorSettings2(pnl) ["up"] = pace.camera_up_bind, ["down"] = pace.camera_down_bind, ["slow"] = pace.camera_slow_bind, - ["speed"] = pace.camera_speed_bind + ["speed"] = pace.camera_speed_bind, + ["roll_drag"] = pace.camera_roll_drag_bind } ]] @@ -1589,74 +1609,91 @@ function pace.FillEditorSettings2(pnl) local forward_binder = vgui.Create("DBinder", LeftPanel) forward_binder:SetSize(40,40) forward_binder:SetPos(100,40) - forward_binder:SetTooltip("move forward") + forward_binder:SetTooltip("move forward" .. "\nbound to " .. pace.camera_movement_binds["forward"]:GetString()) forward_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["forward"]:GetString())) function forward_binder:OnChange(num) pace.camera_movement_binds["forward"]:SetString(input.GetKeyName( num )) + self:SetTooltip("move forward" .. "\nbound to " .. input.GetKeyName( num )) end local back_binder = vgui.Create("DBinder", LeftPanel) back_binder:SetSize(40,40) back_binder:SetPos(100,80) - back_binder:SetTooltip("move back") + back_binder:SetTooltip("move back" .. "\nbound to " .. pace.camera_movement_binds["back"]:GetString()) back_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["back"]:GetString())) function back_binder:OnChange(num) pace.camera_movement_binds["back"]:SetString(input.GetKeyName( num )) + self:SetTooltip("move back" .. "\nbound to " .. input.GetKeyName( num )) end local moveleft_binder = vgui.Create("DBinder", LeftPanel) moveleft_binder:SetSize(40,40) moveleft_binder:SetPos(60,80) - moveleft_binder:SetTooltip("move left") + moveleft_binder:SetTooltip("move left" .. "\nbound to " .. pace.camera_movement_binds["moveleft"]:GetString()) moveleft_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveleft"]:GetString())) function moveleft_binder:OnChange(num) pace.camera_movement_binds["moveleft"]:SetString(input.GetKeyName( num )) + self:SetTooltip("move left" .. "\nbound to " .. input.GetKeyName( num )) end local moveright_binder = vgui.Create("DBinder", LeftPanel) moveright_binder:SetSize(40,40) moveright_binder:SetPos(140,80) - moveright_binder:SetTooltip("move right") + moveright_binder:SetTooltip("move right" .. "\nbound to " .. pace.camera_movement_binds["moveright"]:GetString()) moveright_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveright"]:GetString())) function moveright_binder:OnChange(num) pace.camera_movement_binds["moveright"]:SetString(input.GetKeyName( num )) + self:SetTooltip("move right" .. "\nbound to " .. input.GetKeyName( num )) end local up_binder = vgui.Create("DBinder", LeftPanel) up_binder:SetSize(40,40) up_binder:SetPos(180,40) - up_binder:SetTooltip("move up") + up_binder:SetTooltip("move up" .. "\nbound to " .. pace.camera_movement_binds["up"]:GetString()) up_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["up"]:GetString())) function up_binder:OnChange(num) pace.camera_movement_binds["up"]:SetString(input.GetKeyName( num )) + self:SetTooltip("move up" .. "\nbound to " .. input.GetKeyName( num )) end local down_binder = vgui.Create("DBinder", LeftPanel) down_binder:SetSize(40,40) down_binder:SetPos(180,80) - down_binder:SetTooltip("move down") + down_binder:SetTooltip("move down" .. "\nbound to " .. pace.camera_movement_binds["down"]:GetString()) down_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["down"]:GetString())) function down_binder:OnChange(num) - print(num, input.GetKeyName( num )) pace.camera_movement_binds["down"]:SetString(input.GetKeyName( num )) + self:SetTooltip("move down" .. "\nbound to " .. input.GetKeyName( num )) end local slow_binder = vgui.Create("DBinder", LeftPanel) slow_binder:SetSize(40,40) slow_binder:SetPos(20,80) - slow_binder:SetTooltip("go slow") + slow_binder:SetTooltip("go slow" .. "\nbound to " .. pace.camera_movement_binds["slow"]:GetString()) slow_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["slow"]:GetString())) function slow_binder:OnChange(num) pace.camera_movement_binds["slow"]:SetString(input.GetKeyName( num )) + self:SetTooltip("go slow" .. "\nbound to " .. input.GetKeyName( num )) end local speed_binder = vgui.Create("DBinder", LeftPanel) speed_binder:SetSize(40,40) speed_binder:SetPos(20,40) - speed_binder:SetTooltip("go fast") + speed_binder:SetTooltip("go fast" .. "\nbound to " .. pace.camera_movement_binds["speed"]:GetString()) speed_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["speed"]:GetString())) function speed_binder:OnChange(num) pace.camera_movement_binds["speed"]:SetString(input.GetKeyName( num )) + self:SetTooltip("go fast" .. "\nbound to " .. input.GetKeyName( num )) + end + + local roll_binder = vgui.Create("DBinder", LeftPanel) + roll_binder:SetSize(40,40) + roll_binder:SetPos(60,40) + roll_binder:SetTooltip("roll drag (hold & drag to tilt, tap to reset)" .. "\nbound to " .. pace.camera_movement_binds["roll_drag"]:GetString()) + roll_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["roll_drag"]:GetString())) + function roll_binder:OnChange(num) + pace.camera_movement_binds["roll_drag"]:SetString(input.GetKeyName( num )) + self:SetTooltip("roll drag (hold & drag to tilt, tap to reset)" .. "\nbound to " .. input.GetKeyName( num )) end local Parts = pac.GetRegisteredParts() diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 112be90de..fceb4f78e 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -44,6 +44,12 @@ pace.PACActionShortcut_Dictionary = { "toolbar_view", "toolbar_options", "zoom_panel", + "view_orthographic", + "view_follow_entity", + "view_follow_entity_ang_frontback", + "view_follow_entity_sideview", + "reset_eyeang", + "reset_eyeang_pitch", "T_Pose", "bulk_select", "clear_bulkselect", @@ -620,6 +626,53 @@ function pace.DoShortcutFunc(action) if action == "zoom_panel" then pace.PopupMiniFOVSlider() end + if action == "view_orthographic" then + pace.OrthographicView() + end + if action == "view_follow_entity" then + GetConVar("pac_camera_follow_entity"):SetBool(not GetConVar("pac_camera_follow_entity"):GetBool()) + end + if action == "reset_eyeang" then + pace.ResetEyeAngles() + elseif action == "reset_eyeang_pitch" then + pace.ResetEyeAngles(true) + end + if action == "view_follow_entity_ang_frontback" then + pace.ResetEyeAngles(true) + local b = GetConVar("pac_camera_follow_entity_ang"):GetBool() + GetConVar("pac_camera_follow_entity_ang_use_side"):SetBool(false) + if not b then + pace.view_reversed = 1 + GetConVar("pac_camera_follow_entity_ang"):SetBool(true) + timer.Simple(0, function() pace.FlashNotification("view_follow_entity_ang_frontback (back)") end) + else + if pace.view_reversed == -1 then + GetConVar("pac_camera_follow_entity_ang"):SetBool(false) + timer.Simple(0, function() pace.FlashNotification("view_follow_entity_ang_frontback (disable)") end) + else + timer.Simple(0, function() pace.FlashNotification("view_follow_entity_ang_frontback (front)") end) + end + pace.view_reversed = -pace.view_reversed + end + end + if action == "view_follow_entity_sideview" then + pace.ResetEyeAngles(true) + local b = GetConVar("pac_camera_follow_entity_ang"):GetBool() + GetConVar("pac_camera_follow_entity_ang_use_side"):SetBool(true) + if not b then + pace.view_reversed = 1 + GetConVar("pac_camera_follow_entity_ang"):SetBool(true) + timer.Simple(0, function() pace.FlashNotification("view_follow_entity_sideview (left)") end) + else + if pace.view_reversed == -1 then + GetConVar("pac_camera_follow_entity_ang"):SetBool(false) + timer.Simple(0, function() pace.FlashNotification("view_follow_entity_sideview (disable)") end) + else + timer.Simple(0, function() pace.FlashNotification("view_follow_entity_sideview (right)") end) + end + pace.view_reversed = -pace.view_reversed + end + end if action == "T_Pose" then pace.SetTPose(not pace.GetTPose()) end diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 63ce185c4..9058f2400 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -12,6 +12,89 @@ acsfnc("Pos", Vector(5,5,5)) acsfnc("Angles", Angle(0,0,0)) acsfnc("FOV", 75) +function pace.GoTo(obj, mode, extra, alt_move) + if not obj then return end + if mode == "view" and (obj.GetWorldPosition or isentity(obj)) then + + extra = extra or {radius = 75} --if no 3rd arg, assume a basic 75 distance + extra.radius = extra.radius or 75 --if the table is wrong, force insert a default 75 distance + if alt_move ~= nil then --repeated hits = reverse? or come back? + if alt_move == true then + extra.radius = -extra.radius + elseif alt_move == false then + + end + end + local obj_pos + local angfunc + if obj.GetWorldPosition then + obj_pos = obj:GetWorldPosition() + elseif isentity(obj) then + obj_pos = obj:GetPos() + obj:OBBCenter() + end + pace.ViewAngles = (pace.ViewPos - obj_pos):Angle() + if extra.axis then + local vec + local sgn = extra.radius > 0 and 1 or -1 + if obj.GetWorldPosition then + if extra.axis == "x" then + vec = obj:GetWorldAngles():Forward() + elseif extra.axis == "y" then + vec = obj:GetWorldAngles():Right() + elseif extra.axis == "z" then + vec = obj:GetWorldAngles():Up() + elseif extra.axis == "world_x" then + vec = Vector(1,0,0) + elseif extra.axis == "world_y" then + vec = Vector(0,1,0) + elseif extra.axis == "world_z" then + vec = Vector(0,0,1) + end + elseif isentity(obj) then + local ang = obj:GetAngles() + ang.p = 0 + if extra.axis == "x" then + vec = ang:Forward() + elseif extra.axis == "y" then + vec = ang:Right() + elseif extra.axis == "z" then + vec = ang:Up() + elseif extra.axis == "world_x" then + vec = Vector(1,0,0) + elseif extra.axis == "world_y" then + vec = Vector(0,1,0) + elseif extra.axis == "world_z" then + vec = Vector(0,0,1) + end + end + vec = sgn * vec + local viewpos = obj_pos - vec * math.abs(extra.radius) + pace.ViewPos = viewpos + pace.ViewAngles = (obj_pos - viewpos):Angle() + pace.ViewAngles:Normalize() + return + end + pace.ViewAngles:Normalize() + pace.ViewPos = obj_pos + (obj_pos - pace.ViewPos):GetNormalized() * (extra.radius or 75) + elseif mode == "treenode" and (obj.pace_tree_node or ispanel(obj)) then + local part + if obj.pace_tree_node then part = obj elseif ispanel(obj) then part = obj.part end + local parent = part:GetParent() + while IsValid(parent) and (parent:GetParent() ~= parent) do + parent.pace_tree_node:SetExpanded(true) + parent = parent:GetParent() + if parent:IsValid() then + parent.pace_tree_node:SetExpanded(true) + end + end + if part.pace_tree_node then + pace.tree:ScrollToChild(part.pace_tree_node) + end + elseif mode == "property" then + pace.FlashProperty(obj, extra or "Name") + end +end + pace.camera_forward_bind = CreateClientConVar("pac_editor_camera_forward_bind", "w", true) pace.camera_back_bind = CreateClientConVar("pac_editor_camera_back_bind", "s", true) pace.camera_moveleft_bind = CreateClientConVar("pac_editor_camera_moveleft_bind", "a", true) @@ -21,6 +104,40 @@ pace.camera_down_bind = CreateClientConVar("pac_editor_camera_down_bind", "", tr pace.camera_slow_bind = CreateClientConVar("pac_editor_camera_slow_bind", "ctrl", true) pace.camera_speed_bind = CreateClientConVar("pac_editor_camera_speed_bind", "shift", true) +pace.camera_roll_drag_bind = CreateClientConVar("pac_editor_camera_roll_bind", "", true) +pace.roll_snapping = CreateClientConVar("pac_camera_roll_snap", "0", true) + +pace.camera_orthographic_cvar = CreateClientConVar("pac_camera_orthographic", "0", true) +pace.camera_orthographic = false +pace.viewlock_mode = "" + +function pace.OrthographicView(b) + if b == nil then b = not pace.camera_orthographic end + pace.camera_orthographic = b + pace.camera_orthographic_cvar:SetBool(tobool(b or false)) + if pace.Editor and pace.Editor.zoomslider then + if pace.camera_orthographic then + timer.Simple(1, function() pace.FlashNotification("Switched to orthographic mode") end) + pace.Editor.zoomslider:SetText("Ortho. Width") + pace.Editor.zoomslider:SetMax( 1000 ) + pace.Editor.zoomslider:SetValue(50) + pace.Editor.ortho_nearz:Show() + pace.Editor.ortho_farz:Show() + else + timer.Simple(1, function() pace.FlashNotification("Switched to normal FOV mode") end) + pace.Editor.zoomslider:SetText("Camera FOV") + pace.Editor.zoomslider:SetValue(75) + pace.Editor.zoomslider:SetMax( 100 ) + pace.Editor.ortho_nearz:Hide() + pace.Editor.ortho_farz:Hide() + end + end +end + +cvars.AddChangeCallback("pac_camera_orthographic", function(name, old, new) + pace.OrthographicView(tobool(new)) +end, "pac_update_ortho") + pace.camera_movement_binds = { ["forward"] = pace.camera_forward_bind, ["back"] = pace.camera_back_bind, @@ -29,7 +146,8 @@ pace.camera_movement_binds = { ["up"] = pace.camera_up_bind, ["down"] = pace.camera_down_bind, ["slow"] = pace.camera_slow_bind, - ["speed"] = pace.camera_speed_bind + ["speed"] = pace.camera_speed_bind, + ["roll_drag"] = pace.camera_roll_drag_bind } function pace.GetViewEntity() @@ -154,6 +272,24 @@ local function MovementBindDown(name) return input.IsButtonDown(input.GetKeyCode(pace.camera_movement_binds[name]:GetString())) end +local follow_entity_ang = CreateClientConVar("pac_camera_follow_entity_ang", "0", true) +local follow_entity_ang_side = CreateClientConVar("pac_camera_follow_entity_ang_use_side", "0", true) +local delta_y = 0 +local previous_delta_y = 0 + + +local rolling = false +local initial_roll = 0 +local initial_roll_x = 0 +local current_x = 0 +local roll_x = 0 +local start_x = 0 +local previous_roll = 0 +local roll_x_delta = 0 +local roll_release_time = 0 + + +local pitch_limit = 90 local function CalcDrag() if not pace.properties or not pace.properties.search then return end @@ -215,8 +351,56 @@ local function CalcDrag() mult = mult + 5 end + if MovementBindDown("roll_drag") then + local current_x,current_y = input.GetCursorPos() + if not rolling then + start_x,_ = input.GetCursorPos() + rolling = true + initial_roll = previous_roll + else + local wrapping = false + local x,_ = input.GetCursorPos() + if x >= ScrW()-1 then + input.SetCursorPos(2,current_y) + current_x = 2 + start_x = start_x - ScrW() + 2 + wrapping = true + end + if x <= 1 then + wrapping = true + input.SetCursorPos(ScrW()-2,current_y) + current_x = ScrW() - 2 + start_x = start_x + ScrW() - 2 + wrapping = true + end + + local snap = pace.roll_snapping:GetFloat() + roll_x_delta = x - start_x --current delta (modify) + if not wrapping then + pace.view_roll = (180 + math.Round(200 * (initial_roll + x - start_x) / ScrW(),2)) % 360 - 180 + pace.FlashNotification("view roll : " .. pace.view_roll .. " degrees (Ctrl to snap by " .. snap .. " degrees)") + end + + if snap ~= 0 and input.IsButtonDown(KEY_LCONTROL) and pace.view_roll ~= nil then + pace.view_roll = math.Round(pace.view_roll / snap,0) * snap + pace.FlashNotification("view roll : " .. pace.view_roll .. " (snapped to nearest " .. snap .. " degrees)") + end + --will be applied post + end + elseif rolling then + local x,_ = input.GetCursorPos() + previous_roll = initial_roll + roll_x_delta + if math.abs(start_x - x) < 5 then + pace.FlashNotification("view roll reset") + pace.view_roll = nil + previous_roll = 0 initial_roll = 0 start_x = 0 roll_x_delta = 0 + end + rolling = false + end + if not pace.IsSelecting then if mcode == MOUSE_LEFT then + pace.dragging = true local mpos = Vector(input.GetCursorPos()) if mpos.x >= ScrW() - 1 then @@ -225,24 +409,46 @@ local function CalcDrag() mpos = set_mouse_pos(ScrW() - 2, gui.MouseY()) end + local overflows = false if mpos.y >= ScrH() - 1 then mpos = set_mouse_pos(gui.MouseX(), 1) + overflows = 1 elseif mpos.y < 1 then mpos = set_mouse_pos(gui.MouseX(), ScrH() - 2) + overflows = -1 end local delta = (held_mpos - mpos) / 5 * math.rad(pace.ViewFOV) - pace.ViewAngles.p = math.Clamp(held_ang.p - delta.y, -90, 90) + pace.ViewAngles.p = math.Clamp(held_ang.p - delta.y, -pitch_limit, pitch_limit) pace.ViewAngles.y = held_ang.y + delta.x + if pace.viewlock and pace.viewlock_mode == "zero pitch" then + delta_y = (held_ang.p - delta.y) + if (previous_delta_y ~= delta_y) and (not overflows) then + pace.ViewPos = pace.ViewPos + Vector(0,0,delta_y - previous_delta_y) * pace.viewlock_distance / 300 + elseif overflows then + pace.ViewPos = pace.ViewPos + Vector(0,0,overflows) * pace.viewlock_distance / 300 + end + previous_delta_y = (held_ang.p - delta.y) + end + else + previous_delta_y = 0 + delta_y = 0 + pace.dragging = false end end - + local viewlock_direct = (pace.viewlock and not pace.dragging) and (pace.viewlock_mode == "direct") if pace.delaymovement < RealTime() then if MovementBindDown("forward") then pace.ViewPos = pace.ViewPos + pace.ViewAngles:Forward() * mult * ftime - elseif MovementBindDown("back") then + if pace.viewlock or follow_entity_ang:GetBool() then + pace.viewlock_distance = pace.viewlock_distance - mult * ftime + end + elseif MovementBindDown("back") then pace.ViewPos = pace.ViewPos - pace.ViewAngles:Forward() * mult * ftime + if pace.viewlock or follow_entity_ang:GetBool()then + pace.viewlock_distance = pace.viewlock_distance + mult * ftime + end end if MovementBindDown("moveright") then @@ -253,13 +459,28 @@ local function CalcDrag() if MovementBindDown("up") then if not IsValid(pace.timeline.frame) then + if viewlock_direct then + local up = pace.ViewAngles:Up() + mult = mult * up.z + end pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * mult * ftime end elseif MovementBindDown("down") then if not IsValid(pace.timeline.frame) then + if viewlock_direct then + local up = pace.ViewAngles:Up() + mult = mult * up.z + end pace.ViewPos = pace.ViewPos - pace.ViewAngles:Up() * mult * ftime end end + if viewlock_direct and pace.viewlock_mode ~= "frame of reference" then + local distance = pace.viewlock_distance or 75 + pace.ViewAngles = (pace.viewlock_pos - pace.ViewPos):Angle() + + local newpos = pace.viewlock_pos - distance * pace.ViewAngles:Forward() + pace.ViewPos = newpos + end end end @@ -271,10 +492,13 @@ cvars.AddChangeCallback("pac_enable_editor_view", function(name, old, new) pace.EnableView(true) else pace.CameraPartSwapView() + pac.RemoveHook("CalcView", "editor") end end, "pace_update_editor_view") local lastEntityPos +pace.view_reversed = 1 +pace.viewlock_distance = 75 function pace.CalcView(ply, pos, ang, fov) if pace.editing_viewmodel or pace.editing_hands then @@ -293,8 +517,128 @@ function pace.CalcView(ply, pos, ang, fov) lastEntityPos = nil end + if follow_entity_ang:GetBool() then + local ent = pace.GetViewEntity() + local ang = ent:GetAngles() + if follow_entity_ang_side:GetBool() then ang = ang:Right():Angle() end + local pos = ent:GetPos() + ent:OBBCenter() + pace.viewlock = nil + pace.viewlock_pos = pos + pace.viewlock_pos_deltaZ = pace.viewlock_pos + pace.viewlock_distance = pace.viewlock_distance or 75 + if pace.viewlock_distance > 10 then + pace.ViewAngles = (pace.view_reversed * ang:Forward()):Angle() + local newpos = pos - pace.viewlock_distance*pace.ViewAngles:Forward() + pace.ViewPos = newpos + else + pace.view_reversed = -pace.view_reversed --this will flip between front-facing and back-facing + pace.viewlock_distance = 75 --but for that to happen we need to move the imposed position forward + pace.delaymovement = RealTime() + 0.5 + pace.ViewPos = pace.ViewPos + 75*pace.ViewAngles:Forward() + end + end + local pos, ang, fov = pac.CallHook("EditorCalcView", pace.ViewPos, pace.ViewAngles, pace.ViewFOV) + if pace.viewlock then + local pitch = pace.ViewAngles.p + local viewlock_pos + if isvector(pace.viewlock) then + viewlock_pos = pace.viewlock + elseif isentity(pace.viewlock) then + viewlock_pos = pace.viewlock:GetPos() + pace.viewlock:OBBCenter() + elseif pace.viewlock.GetWorldPosition then + viewlock_pos = pace.viewlock:GetWorldPosition() + end + + pace.viewlock_pos = viewlock_pos + ang = ang or pace.ViewAngles + pos = pos or pace.ViewPos + local deltaZ = Vector(0,0,pace.ViewPos.z - viewlock_pos.z) + pace.viewlock_pos_deltaZ = pace.viewlock_pos + deltaZ + + + if pace.viewlock_distance < 10 then + pace.view_reversed = -pace.view_reversed --this will flip between front-facing and back-facing + pace.viewlock_distance = 75 --but for that to happen we need to move the imposed position forward + pos = pace.ViewPos - 75*pace.ViewAngles:Forward() + end + + if pace.viewlock_mode == "free pitch" then + pitch_limit = 90 + viewlock_pos = viewlock_pos + deltaZ + local distance = pace.viewlock_distance or viewlock_pos:Distance(pace.ViewPos) + if not pace.dragging then + ang = (-pace.ViewPos + viewlock_pos):Angle() + ang:Normalize() + pos = viewlock_pos - pace.view_reversed * distance * ang:Forward() + else + ang = (-pace.ViewPos + viewlock_pos):Angle() + ang:Normalize() + end + + ang.p = pitch + elseif pace.viewlock_mode == "zero pitch" then + viewlock_pos = viewlock_pos + deltaZ + local distance = pace.viewlock_distance or viewlock_pos:Distance(pace.ViewPos) + if not pace.dragging then + ang = (-pace.ViewPos + viewlock_pos):Angle() + ang:Normalize() + pos = viewlock_pos - pace.view_reversed * distance * ang:Forward() + else + ang = (-pace.ViewPos + viewlock_pos):Angle() + ang:Normalize() + end + + ang.p = 0 + elseif pace.viewlock_mode == "direct" then + pitch_limit = 89.9 + local distance = pace.viewlock_distance or viewlock_pos:Distance(pace.ViewPos) + local newpos + if pace.dragging then + newpos = viewlock_pos - distance * pace.ViewAngles:Forward() + pos = newpos + ang = (-newpos + pace.viewlock_pos):Angle() + ang:Normalize() + pace.ViewAngles = ang + else + newpos = viewlock_pos + pace.view_reversed * distance * pace.ViewAngles:Forward() + ang = (-pace.ViewPos + newpos):Angle() + ang:Normalize() + end + elseif pace.viewlock_mode == "frame of reference" then + pitch_limit = 90 + viewlock_pos = viewlock_pos + deltaZ + local distance = pace.viewlock_distance or viewlock_pos:Distance(pace.ViewPos) + if pace.viewlock and pace.viewlock.GetDrawPosition then + local _pos, _ang = pace.viewlock:GetDrawPosition() + local mat = Matrix() + mat:Rotate(_ang) + if pace.viewlock_axis == "x" then + --mat:Scale(Vector(-1,1,1)) + elseif pace.viewlock_axis == "y" then + mat:Rotate(Angle(0,90,0)) + elseif pace.viewlock_axis == "z" then + mat:Rotate(Angle(90,0,0)) + end + mat:Scale(Vector(pace.view_reversed,1,1)) + ang = mat:GetAngles() + ang.r = pace.view_reversed*ang.r + pos = _pos - distance * ang:Forward() + end + elseif pace.viewlock_mode == "disable" then + pitch_limit = 90 + pace.viewlock = nil + end + --we apply the reversion only once, so reset here + if pace.view_reversed == -1 and (pace.viewlock_mode ~= "frame of reference") then + pace.view_reversed = 1 + end + else + pitch_limit = 89.9 + --pace.ViewAngles.r = 0 + end + if pos then pace.ViewPos = pos end @@ -306,13 +650,40 @@ function pace.CalcView(ply, pos, ang, fov) if fov then pace.ViewFOV = fov end + + local viewang_final = Angle(pace.ViewAngles) - return - { - origin = pace.ViewPos, - angles = pace.ViewAngles, - fov = pace.ViewFOV, - } + if pace.view_roll then + pace.ViewAngles_postRoll = Angle(viewang_final) + pace.ViewAngles_postRoll:RotateAroundAxis(pace.ViewAngles:Forward(), pace.view_roll) + viewang_final = pace.ViewAngles_postRoll + end + + local orthoborder = pace.Editor.zoomslider:GetValue() / 1000 + if not pace.camera_orthographic then + return + { + origin = pace.ViewPos, + angles = viewang_final, + fov = pace.ViewFOV + } + else + return + { + origin = pace.ViewPos, + angles = viewang_final, + fov = pace.ViewFOV, + ortho = { + left = -orthoborder * ScrW(), + right = orthoborder * ScrW(), + top = -orthoborder * ScrH(), + bottom = orthoborder * ScrH() + }, + znear = pace.Editor.ortho_nearz:GetValue(), + zfar = pace.Editor.ortho_farz:GetValue() + } + end + end function pace.ShouldDrawLocalPlayer() @@ -619,7 +990,7 @@ end function pace.GetBreathing() return pace.breathing end -function pace.ResetEyeAngles() +function pace.ResetEyeAngles(pitch_only) local ent = pace.GetViewEntity() if ent:IsValid() then if ent:IsPlayer() then @@ -635,7 +1006,13 @@ function pace.ResetEyeAngles() end) end) - ent:SetEyeAngles(Angle(0, 0, 0)) + if not pitch_only then + ent:SetEyeAngles(Angle(0, 0, 0)) + else + local ang = ent:EyeAngles() + ang.p = 0 + ent:SetEyeAngles(ang) + end else ent:SetAngles(Angle(0, 0, 0)) end @@ -659,6 +1036,11 @@ function pace.PopupMiniFOVSlider() zoomframe.zoomslider:SetMax( 100 ) zoomframe.zoomslider:SetDecimals( 0 ) zoomframe.zoomslider:SetText("Camera FOV") + if pace.camera_orthographic then + zoomframe.zoomslider:SetText("Ortho. Width") + zoomframe.zoomslider:SetMin( -10000 ) + zoomframe.zoomslider:SetMax( 10000 ) + end zoomframe.zoomslider:SetDark(true) zoomframe.zoomslider:SetDefaultValue( 75 ) From 6546e1226ea170805d068135807daca64312c646 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 23 Dec 2024 23:01:39 -0500 Subject: [PATCH 255/300] hotfix there was one unaccounted edit and I didn't remember why it was there, but now I remember why. it's so the direct viewlock mode doesn't snap to some weird angle when clicking on a new starting position on the screen also revert to the default view pitch clamp limits at this other specific line (when not in a viewlock mode) --- lua/pac3/editor/client/view.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 9058f2400..cd4cbc80e 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -227,6 +227,7 @@ function pace.GUIMousePressed(mc) if mc == MOUSE_LEFT and not pace.editing_viewmodel then held_ang = pace.ViewAngles * 1 + held_ang:Normalize() held_mpos = Vector(input.GetCursorPos()) end @@ -635,8 +636,7 @@ function pace.CalcView(ply, pos, ang, fov) pace.view_reversed = 1 end else - pitch_limit = 89.9 - --pace.ViewAngles.r = 0 + pitch_limit = 90 end if pos then From 2c9dd525a50d092d4644b2d0c52f16ed54a560de Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 25 Dec 2024 01:00:45 -0500 Subject: [PATCH 256/300] proxy property fixes promote ExpressionOnHide to code_proxy panel class if luapad is active, mirror changes from normal property text field to the luapad screen (the link is now bidirectional) convert vectors when pasting so they work out of the box with commas and no unnecessary trailing zeros, this is also to prevent an error where it tried to string trim a vector --- lua/pac3/core/client/parts/proxy.lua | 14 ++++++++------ .../editor/client/panels/extra_properties.lua | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua index 8fbb3f928..eddbef0b6 100644 --- a/lua/pac3/core/client/parts/proxy.lua +++ b/lua/pac3/core/client/parts/proxy.lua @@ -53,7 +53,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("PreviewOutput", false, {description = "Previews the proxy's output (for yourself) next to the nearest owner entity in the game"}) BUILDER:SetPropertyGroup("extra expressions") - BUILDER:GetSet("ExpressionOnHide", "", {description = "Math to apply once, when the proxy is hidden. It computes once, so it will not move."}) + BUILDER:GetSet("ExpressionOnHide", "", {description = "Math to apply once, when the proxy is hidden. It computes once, so it will not move.", editor_panel = "code_proxy"}) BUILDER:GetSet("Extra1", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra1() or var1() to save space, or by another proxy as extra1(\"uid or name\") or var1(\"uid or name\")", editor_panel = "code_proxy"}) BUILDER:GetSet("Extra2", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra2() or var2() to save space, or by another proxy as extra2(\"uid or name\") or var2(\"uid or name\")", editor_panel = "code_proxy"}) BUILDER:GetSet("Extra3", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra3() or var3() to save space, or by another proxy as extra3(\"uid or name\") or var3(\"uid or name\")", editor_panel = "code_proxy"}) @@ -1596,6 +1596,13 @@ local allowed = { function PART:SetExpression(str, slot) str = string.Trim(str,"\n") + 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 + pace.ActiveSpecialPanel.luapad:SetText(str) + end + end + if not slot then --that's the default expression self.Expression = str self.ExpressionFunc = nil @@ -1660,31 +1667,26 @@ function PART:SetExpression(str, slot) end function PART:SetExpressionOnHide(str) - str = string.Trim(str,"\n") self.ExpressionOnHide = str self:SetExpression(str, 0) end function PART:SetExtra1(str) - str = string.Trim(str,"\n") self.Extra1 = str self:SetExpression(str, 1) end function PART:SetExtra2(str) - str = string.Trim(str,"\n") self.Extra2 = str self:SetExpression(str, 2) end function PART:SetExtra3(str) - str = string.Trim(str,"\n") self.Extra3 = str self:SetExpression(str, 3) end function PART:SetExtra4(str) - str = string.Trim(str,"\n") self.Extra4 = str self:SetExpression(str, 4) end diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 1e98faf65..512cf8486 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -1065,6 +1065,15 @@ do -- script proxy if isnumber(value) then -- visually round numbers so 0.6 doesn't show up as 0.600000000001231231 on wear value = math.Round(value, 7) + elseif isvector(var) then + var = math.Round(var.x,3) .. "," .. math.Round(var.y,3) .. "," .. math.Round(var.z,3) + value = var + elseif isangle(var) then + var = math.Round(var.p,3) .. "," .. math.Round(var.y,3) .. "," .. math.Round(var.r,3) + value = var + elseif IsColor(var) then + var = math.Round(var.r,3) .. "," .. math.Round(var.g,3) .. "," .. math.Round(var.b,3) + value = var end local str = tostring(value) local original_str = string.Trim(str,"\n") @@ -1111,6 +1120,15 @@ do -- script proxy frame:SetSizable(true) local editor = vgui.Create("pace_luapad", frame) + local slots = { + ["ExpressionOnHide"] = 0, + ["Extra1"] = 1, + ["Extra2"] = 2, + ["Extra3"] = 3, + ["Extra4"] = 4, + ["Extra5"] = 5, + } + editor.keynumber = slots[self.CurrentKey] frame.luapad = editor install_fontsize_buttons(frame, editor) editor:Dock(FILL) From 4a8523d33da1af2d78e5c6894c24e86ef80c8ed6 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 25 Dec 2024 19:19:06 -0500 Subject: [PATCH 257/300] don't run SafeRemoveEntityDelayed on the weapon part's weapon owner it's on remove(e.g. clearing outfit) but it happens in weirdly specific circumstances, with the "all" mode --- lua/pac3/core/client/parts/model/weapon.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/pac3/core/client/parts/model/weapon.lua b/lua/pac3/core/client/parts/model/weapon.lua index ba49afdc5..2a827cb18 100644 --- a/lua/pac3/core/client/parts/model/weapon.lua +++ b/lua/pac3/core/client/parts/model/weapon.lua @@ -158,4 +158,8 @@ function PART:OnHide() end end +function PART:OnRemove() + --overridden to prevent calling SafeRemoveEntityDelayed on a weapon entity (not a pac CS Ent) +end + BUILDER:Register() \ No newline at end of file From 78d1f05a003a7d865efb62685d9564572bd4aba3 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 25 Dec 2024 19:39:03 -0500 Subject: [PATCH 258/300] double-check to exit calcview hook if editor is not active specific camera part setups caused issues also, apply orthographic view preference before going in the editor --- lua/pac3/editor/client/view.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index cd4cbc80e..40740c4e3 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -108,7 +108,7 @@ pace.camera_roll_drag_bind = CreateClientConVar("pac_editor_camera_roll_bind", " pace.roll_snapping = CreateClientConVar("pac_camera_roll_snap", "0", true) pace.camera_orthographic_cvar = CreateClientConVar("pac_camera_orthographic", "0", true) -pace.camera_orthographic = false +pace.camera_orthographic = pace.camera_orthographic_cvar:GetBool() pace.viewlock_mode = "" function pace.OrthographicView(b) @@ -502,6 +502,7 @@ pace.view_reversed = 1 pace.viewlock_distance = 75 function pace.CalcView(ply, pos, ang, fov) + if not pace.IsActive() then pace.EnableView(false) return end if pace.editing_viewmodel or pace.editing_hands then pace.ViewPos = pos pace.ViewAngles = ang @@ -658,8 +659,7 @@ function pace.CalcView(ply, pos, ang, fov) pace.ViewAngles_postRoll:RotateAroundAxis(pace.ViewAngles:Forward(), pace.view_roll) viewang_final = pace.ViewAngles_postRoll end - - local orthoborder = pace.Editor.zoomslider:GetValue() / 1000 + if not pace.camera_orthographic then return { @@ -668,6 +668,7 @@ function pace.CalcView(ply, pos, ang, fov) fov = pace.ViewFOV } else + local orthoborder = pace.Editor.zoomslider:GetValue() / 1000 return { origin = pace.ViewPos, From e168426591a49a94a8c5e76ec905677f6205b286 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 2 Jan 2025 13:10:08 -0500 Subject: [PATCH 259/300] fire_bullets event fixes originally from https://github.com/CapsAdmin/pac3/pull/1350, we kind of forgot about it and OP deleted his repo the fixes consist of renaming the variable find injected in the callback function (stopping error spam as it was expected to be read as find_ammo) and adding some hackery to make the event work when in singleplayer also adding enums for the ammo names but many addon weapons I tested don't seem to work to begin with, maybe they don't call the FireBullets hook in the first place --- lua/pac3/core/client/parts/event.lua | 49 +++++++++++++++++++++------ lua/pac3/core/server/net_messages.lua | 14 ++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index b652bdb5e..65dbf0d75 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1589,13 +1589,31 @@ PART.OldEvents = { fire_bullets = { operator_type = "string", preferred_operator = "find simple", - tutorial_explanation = "fire_bullets supposedly checks what types of bullets you're firing", + tutorial_explanation = "fire_bullets checks what types of bullets you're firing.\nDoesn't seem to work with many addon weapons. The event relies on the FireBullets hook", arguments = {{find_ammo = "string"}, {time = "number"}}, - callback = function(self, ent, find, time) + userdata = {{default = "AR2", enums = function() + local tbl = {} + for i=-1,512,1 do + local name = game.GetAmmoName(i) + if name then + tbl[name .. " (ID ="..i..")"] = name + end + end + return tbl + end}, {default = 0.1}}, + callback = function(self, ent, find_ammo, time) time = time or 0.1 ent = try_viewmodel(ent) + if game.SinglePlayer() then + if self:GetPlayerOwner() == pac.LocalPlayer then + if ent.pac_hide_bullets ~= ent:GetNWBool("pac_hide_bullets", false) then + net.Start("pac_hide_bullets_get") net.WriteBool(ent.pac_hide_bullets) net.SendToServer() + end + end + end + local data = ent.pac_fire_bullets local b = false @@ -3561,16 +3579,27 @@ pac.AddHook("EntityEmitSound", "emit_sound", function(data) end end) -pac.AddHook("EntityFireBullets", "firebullets", function(ent, data) - if not ent:IsValid() or not ent.pac_has_parts then return end - ent.pac_fire_bullets = {name = data.AmmoType, time = pac.RealTime, reset = true} +if game.SinglePlayer() then + net.Receive("pac_fire_bullets_for_singleplayer", function() + local ent = net.ReadEntity() + if not ent:IsValid() or not ent.pac_has_parts then return end + local ammo_type = net.ReadUInt(8) + ent.pac_fire_bullets = {name = game.GetAmmoName(ammo_type), time = pac.RealTime, reset = true} - pac.CallRecursiveOnAllParts("OnFireBullets") + pac.CallRecursiveOnAllParts("OnFireBullets") + end) +else + pac.AddHook("EntityFireBullets", "firebullets", function(ent, data) + if not ent:IsValid() or not ent.pac_has_parts then return end + ent.pac_fire_bullets = {name = data.AmmoType, time = pac.RealTime, reset = true} - if ent.pac_hide_bullets then - return false - end -end) + pac.CallRecursiveOnAllParts("OnFireBullets") + + if ent.pac_hide_bullets then + return false + end + end) +end --for regaining focus on cameras from first person, hacky thing to not loop through localparts every time --only if the received command name matches that of a camera's linked command event diff --git a/lua/pac3/core/server/net_messages.lua b/lua/pac3/core/server/net_messages.lua index 2f37bc6b4..3a22e417f 100644 --- a/lua/pac3/core/server/net_messages.lua +++ b/lua/pac3/core/server/net_messages.lua @@ -3,6 +3,8 @@ util.AddNetworkString("pac.AllowPlayerButtons") util.AddNetworkString("pac.BroadcastPlayerButton") util.AddNetworkString("pac_chat_typing_mirror") util.AddNetworkString("pac_chat_typing_mirror_broadcast") +util.AddNetworkString("pac_fire_bullets_for_singleplayer") +util.AddNetworkString("pac_hide_bullets_get") do -- button event net.Receive("pac.AllowPlayerButtons", function(length, client) @@ -39,3 +41,15 @@ net.Receive("pac_chat_typing_mirror", function(len, ply) net.WriteEntity(ply) net.Broadcast() end) + +if game.SinglePlayer() then + hook.Add("EntityFireBullets", "pac_bullet_singleplayer_hack", function(ent, data) + if ent:IsPlayer() then + net.Start("pac_fire_bullets_for_singleplayer") net.WriteEntity(ent) net.WriteUInt(game.GetAmmoID(data.AmmoType),8) net.Broadcast() + end + if ent:GetNWBool("pac_hide_bullets", false) then return false end + end) + net.Receive("pac_hide_bullets_get", function(len, ply) + ply:SetNWBool("pac_hide_bullets",net.ReadBool()) + end) +end \ No newline at end of file From dd4a750b6b35ef55fdcbc8d1ed3e658ad0139e33 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 2 Jan 2025 18:46:05 -0500 Subject: [PATCH 260/300] fix parent_scale events Skip the parent.Type == "part" check because it was nil to begin with, it breaks the event's core function of reading part scales, making the input value always the default 1 add option to get the actual parent by default (tree structure is no longer a limitation because of target part modes) add more information on ammo and total_ammo events --- lua/pac3/core/client/parts/event.lua | 101 +++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 65dbf0d75..82afb9b14 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1364,6 +1364,7 @@ PART.OldEvents = { ammo = { operator_type = "number", preferred_operator = "above", + tutorial_explanation = "ammo compares the active weapon's current clip ammo on either the primary or secondary ammunition.", arguments = {{primary = "boolean"}, {amount = "number"}}, userdata = {{editor_onchange = function(part, num) return math.Round(num) end}}, callback = function(self, ent, primary, amount) @@ -1377,7 +1378,20 @@ PART.OldEvents = { }, total_ammo = { operator_type = "number", preferred_operator = "above", + tutorial_explanation = "total_ammo compares the ammo reserves with a certain amount.\n\nhaving primary or secondary as the ammo ID selects the active weapon.\n\nOtherwise, we expect an ammo ID number.\n\nbeware the ammo IDs are dynamic and might change depending on the server because they're loading different weapons with possible custom ammo.", arguments = {{ammo_id = "string"}, {amount = "number"}}, + userdata = {{default = "primary", enums = function() + local tbl = {} + tbl["primary"] = "primary" + tbl["secondary"] = "secondary" + for i=0,1000,1 do + local ammo_name = game.GetAmmoName(i) + if ammo_name ~= nil then + tbl[ammo_name .. " (ID="..i..")"] = tostring(i) + end + end + return tbl + end}}, callback = function(self, ent, ammo_id, amount) if ent.GetAmmoCount then ammo_id = tonumber(ammo_id) or ammo_id:lower() @@ -1904,16 +1918,35 @@ PART.OldEvents = { parent_scale_x = { operator_type = "number", preferred_operator = "above", - arguments = {{scale = "number"}}, - callback = function(self, ent, num) + arguments = {{scale = "number"},{default_to_grandparent = "boolean"}}, + userdata = {{default = 1}, {default = true}}, + callback = function(self, ent, num, default_to_grandparent) + local parent = self:GetParentEx() - if not self.TargetPart:IsValid() and parent:HasParent() then - parent = parent:GetParent() + if default_to_grandparent then --legacy behavior + if not self.TargetPart:IsValid() and parent:HasParent() then + parent = parent:GetParent() + end + else + --GetParentEx can differ from GetParent, but that only happens if we set TargetPart ("External origin part") + if parent ~= self:GetParent() then + if self.TargetPart ~= parent then + if not self.TargetPart:IsValid() and parent:HasParent() then + parent = parent:GetParent() + end + end + end end if parent:IsValid() then - return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.x * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.x) or (parent.GetModelScale and parent:GetModelScale()) or 1, num) + local value = (parent.Scale and parent.Scale.x * parent.Size) + or (parent.pac_model_scale and parent.pac_model_scale.x) + or (parent.GetModelScale and parent:GetModelScale()) + or 1 + value = math.Round(value,4) + self:SetInfo("selected parent : " .. tostring(parent) .. "\nx scale = " .. value) + return self:NumberOperator(value, num) end return 1 @@ -1921,16 +1954,35 @@ PART.OldEvents = { }, parent_scale_y = { operator_type = "number", preferred_operator = "above", - arguments = {{scale = "number"}}, - callback = function(self, ent, num) + arguments = {{scale = "number"},{default_to_grandparent = "boolean"}}, + userdata = {{default = 1}, {default = true}}, + callback = function(self, ent, num, default_to_grandparent) + local parent = self:GetParentEx() - if not self.TargetPart:IsValid() and parent:HasParent() then - parent = parent:GetParent() + if default_to_grandparent then --legacy behavior + if not self.TargetPart:IsValid() and parent:HasParent() then + parent = parent:GetParent() + end + else + --GetParentEx can differ from GetParent, but that only happens if we set TargetPart ("External origin part") + if parent ~= self:GetParent() then + if self.TargetPart ~= parent then + if not self.TargetPart:IsValid() and parent:HasParent() then + parent = parent:GetParent() + end + end + end end if parent:IsValid() then - return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.y * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.y) or (parent.GetModelScale and parent:GetModelScale()) or 1, num) + local value = (parent.Scale and parent.Scale.y * parent.Size) + or (parent.pac_model_scale and parent.pac_model_scale.y) + or (parent.GetModelScale and parent:GetModelScale()) + or 1 + value = math.Round(value,4) + self:SetInfo("selected parent : " .. tostring(parent) .. "\ny scale = " .. value) + return self:NumberOperator(value, num) end return 1 @@ -1938,16 +1990,35 @@ PART.OldEvents = { }, parent_scale_z = { operator_type = "number", preferred_operator = "above", - arguments = {{scale = "number"}}, - callback = function(self, ent, num) + arguments = {{scale = "number"},{default_to_grandparent = "boolean"}}, + userdata = {{default = 1}, {default = true}}, + callback = function(self, ent, num, default_to_grandparent) + local parent = self:GetParentEx() - if not self.TargetPart:IsValid() and parent:HasParent() then - parent = parent:GetParent() + if default_to_grandparent then --legacy behavior + if not self.TargetPart:IsValid() and parent:HasParent() then + parent = parent:GetParent() + end + else + --GetParentEx can differ from GetParent, but that only happens if we set TargetPart ("External origin part") + if parent ~= self:GetParent() then + if self.TargetPart ~= parent then + if not self.TargetPart:IsValid() and parent:HasParent() then + parent = parent:GetParent() + end + end + end end if parent:IsValid() then - return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.z * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.z) or (parent.GetModelScale and parent:GetModelScale()) or 1, num) + local value = (parent.Scale and parent.Scale.z * parent.Size) + or (parent.pac_model_scale and parent.pac_model_scale.z) + or (parent.GetModelScale and parent:GetModelScale()) + or 1 + value = math.Round(value,4) + self:SetInfo("selected parent : " .. tostring(parent) .. "\nz scale = " .. value) + return self:NumberOperator(value, num) end return 1 From 6eb2bfd9167265cdd528fd77904558d66ae967f8 Mon Sep 17 00:00:00 2001 From: textstack <46581273+textstack@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:37:39 -0500 Subject: [PATCH 261/300] Add admin checks to admin networking (#1371) * add admin checks to admin networking * apply admin checks to combat bans too --------- Co-authored-by: pingu7867 --- lua/pac3/editor/server/bans.lua | 8 ++++++++ lua/pac3/editor/server/combat_bans.lua | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/server/bans.lua b/lua/pac3/editor/server/bans.lua index f686c523b..f87a1113c 100644 --- a/lua/pac3/editor/server/bans.lua +++ b/lua/pac3/editor/server/bans.lua @@ -117,6 +117,10 @@ function pace.IsBanned(ply) end net.Receive("pac.BanUpdate", function(len, ply) + if not ply:IsAdmin() then + return + end + pac.Message("Received ban list update operation from : ", ply) pac.Message("Time : ", os.date( "%a %X %x", os.time() )) local playerlist = net.ReadTable() @@ -131,6 +135,10 @@ net.Receive("pac.BanUpdate", function(len, ply) end) net.Receive("pac.RequestBanStates", function(len,ply) + if not ply:IsAdmin() then + return + end + local archive = net.ReadBool() pac.Message("Received ban list request from : ", ply) pac.Message("Time : ", os.date( "%a %X %x", os.time() )) diff --git a/lua/pac3/editor/server/combat_bans.lua b/lua/pac3/editor/server/combat_bans.lua index 635f44c52..35a9a9b8b 100644 --- a/lua/pac3/editor/server/combat_bans.lua +++ b/lua/pac3/editor/server/combat_bans.lua @@ -46,7 +46,10 @@ if SERVER then end -net.Receive("pac.CombatBanUpdate", function() +net.Receive("pac.CombatBanUpdate", function(len, player) + if not player:IsAdmin() then + return + end --get old states first pac.old_tbl_on_file = get_combat_ban_states() @@ -80,6 +83,9 @@ net.Receive("pac.CombatBanUpdate", function() end) net.Receive("pac.RequestCombatBanStates", function(len, ply) + if not ply:IsAdmin() then + return + end pac.global_combat_whitelist = get_combat_ban_states() net.Start("pac.SendCombatBanStates") net.WriteTable(pac.global_combat_whitelist) From 27fdb2ef49135473c4e2cd8cb45adf4f326271ef Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 2 Jan 2025 22:19:33 -0500 Subject: [PATCH 262/300] some event fixes move some caching code around and rename one variable related to it this will allow lockpart_grabbing, damage_zone_hit, and damage_zone_kill to work with empty string argument again --- lua/pac3/core/client/part_pool.lua | 4 ++-- lua/pac3/core/client/parts/event.lua | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index 9164f5206..ae1f46c47 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -648,14 +648,14 @@ end function pac.LinkSpecialTrackedPartsForEvent(part, ply) part.erroring_cached_parts = {} part.found_cached_parts = {} - + part.specialtrackedparts = {} local tracked_classes = { ["damage_zone"] = true, ["lock"] = true } for _,part2 in pairs(all_parts) do - if ply == part2:GetPlayerOwner() and tracked_classes[part.ClassName] then + if ply == part2:GetPlayerOwner() and tracked_classes[part2.ClassName] then table.insert(part.specialtrackedparts,part2) end end diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 82afb9b14..bf0eea028 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -159,6 +159,13 @@ function PART:SetEvent(event) (self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event) local owner = self:GetPlayerOwner() + timer.Simple(1, function() + --caching for some events + pac.RegisterPartToCache(owner, "button_events", self, event ~= "button") + if tracked_events[event] then + pac.LinkSpecialTrackedPartsForEvent(self, owner) + end + end) if (owner == pac.LocalPlayer) and (not pace.processing) then if event == "command" then owner.pac_command_events = owner.pac_command_events or {} end @@ -211,11 +218,6 @@ function PART:SetEvent(event) if not GetConVar("pac_editor_remember_divider_height"):GetBool() and IsValid(pace.Editor) then pace.Editor.div:SetTopHeight(ScrH() - 520) end end - --caching for some events - pac.RegisterPartToCache(owner, "button_events", self, event ~= "button") - if tracked_events[event] then - timer.Simple(1, function() pac.LinkSpecialTrackedPartsForEvent(self, owner) end) - end end function PART:Initialize() @@ -2467,7 +2469,6 @@ PART.OldEvents = { callback = function(self, ent, uid) uid = uid or "" uid = string.gsub(uid, "\"", "") - local valid_uid, err = pcall(pac.GetPartFromUniqueID, pac.Hash(ent), uid) if uid == "" then --for _,part in pairs(pac.GetLocalParts()) do for _,part in ipairs(self.specialtrackedparts) do From b1310e91212d89a056c91348babc1f34b500a10d Mon Sep 17 00:00:00 2001 From: techbot Date: Fri, 3 Jan 2025 12:04:16 +0100 Subject: [PATCH 263/300] don't check extensions only, since some services don't provide a direct link to a file with an extension --- lua/pac3/editor/client/saved_parts.lua | 37 +++++++++++--------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua index 6a2004298..4ef455512 100644 --- a/lua/pac3/editor/client/saved_parts.lua +++ b/lua/pac3/editor/client/saved_parts.lua @@ -213,32 +213,25 @@ function pace.LoadParts(name, clear, override_part) pac.dprint("loading Parts %s", name) if name:find("https?://") then + local function callback(str) + if string.find( str, "" ) then + pace.MessagePrompt("Invalid URL, .txt expected, but the website returned a HTML file. If you're using Github then use the RAW option.", "URL Failed", "OK") + return + end - local ext = name:match("/.+%.(%a+)[%?&]?.-") - if ext == "txt" then - local function callback(str) - if string.find( str, "" ) then - pace.MessagePrompt("Invalid URL, the website returned a HTML file. If you're using Github then use the RAW option.", "URL Failed", "OK") - return - end - - local data, err = pace.luadata.Decode(str) - if not data then - local message = string.format("URL fail: %s : %s\n", name, err) - pace.MessagePrompt(message, "URL Failed", "OK") - return - end - - pace.LoadPartsFromTable(data, clear, override_part) + local data, err = pace.luadata.Decode(str) + if not data then + local message = string.format("Failed to load pac3 outfit from url: %s : %s\n", name, err) + pace.MessagePrompt(message, "URL Failed", "OK") + return end - pac.HTTPGet(name, callback, function(err) - pace.MessagePrompt(err, "HTTP Request Failed for " .. name, "OK") - end) - else - pace.MessagePrompt(".txt file expected, got" .. ext, "Invalid file", "OK") - return + pace.LoadPartsFromTable(data, clear, override_part) end + + pac.HTTPGet(name, callback, function(err) + pace.MessagePrompt(err, "HTTP Request Failed for " .. name, "OK") + end) else name = name:gsub("%.txt", "") From 9f7fbcba51ce26da6e0e8fc2aac14fd566193adf Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 12 Jan 2025 16:11:13 -0500 Subject: [PATCH 264/300] better way to handle bulk select in nested setups pac_bulk_select_subsume is a new option to toggle the behavior where selecting a part deselects its children and prevents selecting those children thereafter having it off will allow operations on arbitrary parts in nested setups, but it can still cause issues with "broad" operations if you, for example, copy a series of parts that are parents, grandparents, great-grandparents etc. subsuming would've prevented excess copies of children. or if you move / insert, the hierarchy can break. it'll make sense if you think about how it works. bulk select does single operations one by one on a list use flashnotification when doing bulk selections and deselections, in keeping with the general principle of explaining what's going on when editor actions are being done with that, also automatically add the bulk select partmenu option at the end, when there's a bulk selection and it's not already part of the menu. it's not a perfect solution to prevent confusion but it might be helpful for people when they accidentally discover bulk select. --- lua/pac3/editor/client/parts.lua | 73 +++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 0e8081fba..f84e7d895 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -22,8 +22,9 @@ local hover_color = CreateConVar( "pac_hover_color", "255 255 255", FCVAR_ARCHIV 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") -CreateConVar( "pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") -CreateConVar( "pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") +CreateConVar("pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") +CreateConVar("pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") +local bulk_select_subsume = CreateConVar("pac_bulk_select_subsume", "1", FCVAR_ARCHIVE, "Whether bulk-selecting a part implicitly deselects its children since they are covered by the parent already.\nWhile it can provide a clearer view of what's being selected globally which simplifies broad operations like deleting, moving and copying, it prevents targeted operations on nested parts like bulk property editing.") CreateConVar("pac_copilot_partsearch_depth", -1, FCVAR_ARCHIVE, "amount of copiloting in the searchable part menu\n-1:none\n0:auto-focus on the text edit for events\n1:bring up a list of clickable event types\nother parts aren't supported yet") CreateConVar("pac_copilot_make_popup_when_selecting_event", 1, FCVAR_ARCHIVE, "whether to create a popup so you can read what an event does") @@ -73,7 +74,11 @@ local function BulkSelectRefreshFadedNodes(part_trace) for _,v in ipairs(pace.BulkSelectList) do if not v:IsValid() then table.RemoveByValue(pace.BulkSelectList, v) elseif IsValid(v.pace_tree_node) then - v.pace_tree_node:SetAlpha( 150 ) + if bulk_select_subsume:GetBool() then + v.pace_tree_node:SetAlpha( 150 ) + else + v.pace_tree_node:SetAlpha( 255 ) + end end end end @@ -1375,25 +1380,33 @@ do -- menu pace.BulkSelectList = pace.BulkSelectList or {} if (table.HasValue(pace.BulkSelectList, obj)) then pace.RemoveFromBulkSelect(obj) + pace.FlashNotification("Bulk select: de-selected " .. tostring(obj)) selected_part_added = false elseif (pace.BulkSelectList[obj] == nil) then pace.AddToBulkSelect(obj) + pace.FlashNotification("Bulk select: selected " .. tostring(obj)) selected_part_added = true - for _,v in ipairs(obj:GetChildrenList()) do - pace.RemoveFromBulkSelect(v) + if bulk_select_subsume:GetBool() then + for _,v in ipairs(obj:GetChildrenList()) do + pace.RemoveFromBulkSelect(v) + end end end - --check parents and children - for _,v in ipairs(pace.BulkSelectList) do - if table.HasValue(v:GetChildrenList(), obj) then - --print("selected part is already child to a bulk-selected part!") - pace.RemoveFromBulkSelect(obj) - selected_part_added = false - elseif table.HasValue(obj:GetChildrenList(), v) then - --print("selected part is already parent to a bulk-selected part!") - pace.RemoveFromBulkSelect(v) - selected_part_added = false + if bulk_select_subsume:GetBool() then + --check parents and children + for _,v in ipairs(pace.BulkSelectList) do + if table.HasValue(v:GetChildrenList(), obj) then + --print("selected part is already child to a bulk-selected part!") + pace.RemoveFromBulkSelect(obj) + pace.FlashNotification("") + selected_part_added = false + elseif table.HasValue(obj:GetChildrenList(), v) then + --print("selected part is already parent to a bulk-selected part!") + pace.RemoveFromBulkSelect(v) + pace.FlashNotification("") + selected_part_added = false + end end end @@ -1426,7 +1439,9 @@ do -- menu function pace.RemoveFromBulkSelect(obj) table.RemoveByValue(pace.BulkSelectList, obj) - obj.pace_tree_node:SetAlpha( 255 ) + if IsValid(obj.pace_tree_node) then + obj.pace_tree_node:SetAlpha( 255 ) + end obj:SetInfo() --RebuildBulkHighlight() end @@ -1435,7 +1450,14 @@ do -- menu table.insert(pace.BulkSelectList, obj) if obj.pace_tree_node == nil then return end obj:SetInfo("selected in bulk select") - obj.pace_tree_node:SetAlpha( 150 ) + if IsValid(obj.pace_tree_node) then + if bulk_select_subsume:GetBool() then + obj.pace_tree_node:SetAlpha( 150 ) + else + obj.pace_tree_node:SetAlpha( 255 ) + end + + end --RebuildBulkHighlight() end function pace.BulkHide() @@ -2269,10 +2291,19 @@ do -- menu --default_operations_order --if not obj then obj = pace.current_part end if obj then pace.AddClassSpecificPartMenuComponents(menu, obj) end + local contained_bulk_select = false for _,option_name in ipairs(pace.operations_order) do + if option_name == "bulk_select" then + contained_bulk_select = true + end pace.addPartMenuComponent(menu, obj, option_name) end + if #pace.BulkSelectList >= 1 and not contained_bulk_select then + menu:AddSpacer() + pace.addPartMenuComponent(menu, obj, "bulk_select") + end + --[[if obj then if not obj:HasParent() then menu:AddOption(L"wear", function() @@ -3783,7 +3814,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) end end) elseif option_name == "arraying_menu" then - local arraying_menu, pnl = menu:AddSubMenu(L"arraying menu", function() pace.OpenArrayingMenu(obj) end) pnl:SetImage('icon16/table_multiple.png') + local arraying_menu, pnl = menu:AddSubMenu(L"arraying menu", function() pace.OpenArrayingMenu(obj) end) pnl:SetImage("icon16/table_multiple.png") if obj.GetWorldPosition then local icon = obj.pace_tree_node.ModelPath or obj.Icon if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end @@ -3849,6 +3880,8 @@ function pace.addPartMenuComponent(menu, obj, option_name) copied:AddOption(i .. " : " .. name_str .. " (" .. v.ClassName .. ")"):SetIcon(v.Icon) end end + local subsume_pnl = bulk_menu:AddCVar("bulk select subsume", "pac_bulk_select_subsume", "1", "0") + subsume_pnl:SetTooltip("Whether bulk select should take the hierarchy into account, deselecting children when selecting a part.\nEnable this if you commonly do broad operations like copying, deleting or moving parts.\nDisable this for targeted operations like property editing on nested model structures, for example.") local resetting_mode, resetpnl = bulk_menu:AddSubMenu("Clear selection after operation?") resetpnl:SetImage("icon16/table_delete.png") local resetting_mode1 = resetting_mode:AddOption("Yes") resetting_mode1:SetIsCheckable(true) resetting_mode1:SetRadio(true) @@ -3963,9 +3996,9 @@ function pace.addPartMenuComponent(menu, obj, option_name) pace.BulkMorphProperty() end):SetImage("icon16/chart_line_edit.png") - bulk_menu:AddOption(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end):SetImage('icon16/table_multiple.png') + bulk_menu:AddOption(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end):SetImage("icon16/application_form.png") - local arraying_menu, pnl = bulk_menu:AddSubMenu(L"arraying menu", function() pace.OpenArrayingMenu(obj) end) pnl:SetImage('icon16/table_multiple.png') + local arraying_menu, pnl = bulk_menu:AddSubMenu(L"arraying menu", function() pace.OpenArrayingMenu(obj) end) pnl:SetImage("icon16/table_multiple.png") if obj and obj.GetWorldPosition then local icon = obj.pace_tree_node.ModelPath or obj.Icon if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end From 8325af1b90c07adea19ed8b817a0a013a745d3ec Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 12 Jan 2025 17:52:16 -0500 Subject: [PATCH 265/300] further bulk select update pac_bulk_select_cursor_info option to draw text next to the cursor when there is a bulk selection or a bulk clipboard, this is to further inform users. something right on the cursor is one of the closest non-invasive ways to inform add a basic cvar-setting derma prompt to the bulk halo highlighting mode menu option (which was purely for information), use more concise text to describe the option deprecate the default bulk_copy shortcut keyword in the experimental presets since it wasn't used (the actual working keyword was copy_bulkselect), but now it's usable as a fallback --- lua/pac3/editor/client/parts.lua | 54 +++++++++++++++++++++++----- lua/pac3/editor/client/shortcuts.lua | 3 +- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index f84e7d895..34b34cbbf 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -25,6 +25,7 @@ CreateConVar( "pac_hover_halo_limit", 100, FCVAR_ARCHIVE, "max number of parts b CreateConVar("pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") CreateConVar("pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") local bulk_select_subsume = CreateConVar("pac_bulk_select_subsume", "1", FCVAR_ARCHIVE, "Whether bulk-selecting a part implicitly deselects its children since they are covered by the parent already.\nWhile it can provide a clearer view of what's being selected globally which simplifies broad operations like deleting, moving and copying, it prevents targeted operations on nested parts like bulk property editing.") +local bulkselect_cursortext = CreateConVar("pac_bulk_select_cursor_info", "1", FCVAR_ARCHIVE, "Whether to draw some info next to your cursor when there is a bulk selection") CreateConVar("pac_copilot_partsearch_depth", -1, FCVAR_ARCHIVE, "amount of copiloting in the searchable part menu\n-1:none\n0:auto-focus on the text edit for events\n1:bring up a list of clickable event types\nother parts aren't supported yet") CreateConVar("pac_copilot_make_popup_when_selecting_event", 1, FCVAR_ARCHIVE, "whether to create a popup so you can read what an event does") @@ -3841,13 +3842,16 @@ function pace.addPartMenuComponent(menu, obj, option_name) local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt() local info - if mode == 0 then info = "not halo-highlighted" - elseif mode == 1 then info = "automatically halo-highlighted" - elseif mode == 2 then info = "halo-highlighted on custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString() - elseif mode == 3 then info = "halo-highlighted on preset keypress: control" - elseif mode == 4 then info = "halo-highlighted on preset keypress: shift" end - - bulk_menu:AddOption(L"Bulk select info: "..info):SetImage(pace.MiscIcons.info) + if mode == 0 then info = "none" + elseif mode == 1 then info = "passive" + elseif mode == 2 then info = "custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString() + elseif mode == 3 then info = "preset keypress: control" + elseif mode == 4 then info = "preset keypress: shift" end + + bulk_menu:AddOption(L"Bulk select highlight mode: "..info, function() + Derma_StringRequest("Change bulk select halo highlighting mode", "0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.", + tostring(mode), function(str) RunConsoleCommand("pac_bulk_select_halo_mode", str) end) + end):SetImage(pace.MiscIcons.info) if #pace.BulkSelectList == 0 then bulk_menu:AddOption(L"Bulk select info: nothing selected"):SetImage(pace.MiscIcons.info) else @@ -3882,7 +3886,8 @@ function pace.addPartMenuComponent(menu, obj, option_name) end local subsume_pnl = bulk_menu:AddCVar("bulk select subsume", "pac_bulk_select_subsume", "1", "0") subsume_pnl:SetTooltip("Whether bulk select should take the hierarchy into account, deselecting children when selecting a part.\nEnable this if you commonly do broad operations like copying, deleting or moving parts.\nDisable this for targeted operations like property editing on nested model structures, for example.") - + bulk_menu:AddCVar("draw bulk select info next to cursor", "pac_bulk_select_cursor_info", "1", "0") + local resetting_mode, resetpnl = bulk_menu:AddSubMenu("Clear selection after operation?") resetpnl:SetImage("icon16/table_delete.png") local resetting_mode1 = resetting_mode:AddOption("Yes") resetting_mode1:SetIsCheckable(true) resetting_mode1:SetRadio(true) local resetting_mode2 = resetting_mode:AddOption("No") resetting_mode2:SetIsCheckable(true) resetting_mode2:SetRadio(true) @@ -5166,3 +5171,36 @@ tbl = { ]] +local bsel_main_icon = Material("icon16/table_multiple.png") +local bsel_clipboard_icon = Material("icon16/page_white_text.png") +local white = Color(255,255,255) + +pac.AddHook("DrawOverlay", "bulkselect_cursor_info", function() + if not bulkselect_cursortext:GetBool() then return end + if not pace then return end + if not pace.IsFocused() then return end + local mx, my = input.GetCursorPos() + + surface.SetFont("BudgetLabel") + surface.SetMaterial(Material("icon16/table_multiple.png")) + surface.SetTextColor(white) + surface.SetDrawColor(white) + local base_y = my + 8 + + if pace.BulkSelectList then + if #pace.BulkSelectList > 0 then + surface.DrawTexturedRect(mx + 10, base_y, 16, 16) + surface.SetTextPos(mx + 12 + 16, base_y) + surface.DrawText("bulk select [" .. #pace.BulkSelectList .."]") + base_y = base_y + 16 + end + end + if pace.BulkSelectClipboard then + if #pace.BulkSelectClipboard > 0 then + surface.SetMaterial(Material("icon16/page_white_text.png")) + surface.DrawTexturedRect(mx + 10, base_y, 16, 16) + surface.SetTextPos(mx + 12 + 16, base_y) + surface.DrawText("bulk clipboard [" .. #pace.BulkSelectClipboard .."]") + end + end +end) \ No newline at end of file diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index fceb4f78e..845c792e4 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -229,7 +229,7 @@ pace.PACActionShortcut_Experimental = { [1] = {"CTRL", "c"} }, - ["bulk_copy"] = { + ["copy_bulkselect"] = { [1] = {"SHIFT", "CTRL", "c"} }, @@ -680,6 +680,7 @@ function pace.DoShortcutFunc(action) pace.DoBulkSelect(pace.current_part) end if action == "clear_bulkselect" then pace.ClearBulkList() end + if action == "bulk_copy" then pace.BulkCopy(pace.current_part) end --deprecated keyword if action == "copy_bulkselect" then pace.BulkCopy(pace.current_part) end if action == "bulk_insert" then pace.BulkCutPaste(pace.current_part) end if action == "bulk_delete" then pace.BulkRemovePart() end From 708833b021498ee2a565656f8bec53e8b96101ef Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 12 Jan 2025 19:31:15 -0500 Subject: [PATCH 266/300] adjustments to is_touching_scalable event add world_only option rename extra_radius to radius (it was a mistake to not include the base entity bounds in the core event code, but backward compatibility must prevail on that one) and reflect the real radius in the part's nicename builder and the extra properties tester --- lua/pac3/core/client/parts/event.lua | 37 ++++++++++++------- .../editor/client/panels/extra_properties.lua | 27 +++++++++++++- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index bf0eea028..fce9a00fb 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -1295,9 +1295,9 @@ PART.OldEvents = { operator_type = "none", tutorial_explanation = "is_touching_life checks in a stretchable box (util.TraceHull) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others", - arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {nearest_model = "boolean"}}, - userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}}, - callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) + arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {nearest_model = "boolean"}, {world_only = "boolean"}}, + userdata = {{editor_panel = "is_touching", default = 15, editor_friendly = "radius"}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}, {default = false}}, + callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model, world_only) if nearest_model then ent = self:GetOwner() end if not IsValid(ent) then return false end extra_radius = extra_radius or 15 @@ -1314,25 +1314,36 @@ PART.OldEvents = { mins = mins * radius maxs = maxs * radius - local tr = util.TraceHull( { - start = startpos, - endpos = startpos, - maxs = maxs, - mins = mins, - filter = {self:GetRootPart():GetOwner(),ent} - } ) - return tr.Hit + if world_only then + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = function(ent) return ent:IsWorld() end + } ) + return tr.Hit + else + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {self:GetRootPart():GetOwner(),ent} + } ) + return tr.Hit + end end, nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model) if nearest_model then ent = self:GetOwner() end if not IsValid(ent) then return "" end - local radius = ent:BoundingRadius() + local radius = extra_radius if radius == 0 and IsValid(ent.pac_projectile) then radius = ent.pac_projectile:GetRadius() end - radius = math.Round(math.max(radius + extra_radius + 1, 1)) + radius = math.Round(math.max(extra_radius, 1),1) local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]" return str diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index 512cf8486..b658023a9 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -1416,7 +1416,7 @@ do -- event is_touching local startpos = ent:WorldSpaceCenter() local b = false - if part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable" then + if part:GetEvent() == "is_touching" then local tr = util.TraceHull( { start = startpos, endpos = startpos, @@ -1425,6 +1425,31 @@ do -- event is_touching filter = {part:GetRootPart():GetOwner(),ent} } ) b = tr.Hit + elseif part:GetEvent() == "is_touching_scalable" then + radius = math.max(extra_radius, 1) --oops, extra_radius is not accounted. but we need to keep backward compatibility + mins = Vector(-x_stretch,-y_stretch,-z_stretch) + maxs = Vector(x_stretch,y_stretch,z_stretch) + mins = mins * radius + maxs = maxs * radius + if part:GetProperty("world_only") then + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = function(ent) return ent:IsWorld() end + } ) + b = tr.Hit + else + local tr = util.TraceHull( { + start = startpos, + endpos = startpos, + maxs = maxs, + mins = mins, + filter = {part:GetRootPart():GetOwner(),ent} + } ) + b = tr.Hit + end elseif part:GetEvent() == "is_touching_life" then local found = false local ents_hits = ents.FindInBox(startpos + mins, startpos + maxs) From f2c5c352b2ed9d5dc7a2c1574d80fb0473f19cce Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 12 Jan 2025 20:15:10 -0500 Subject: [PATCH 267/300] text part reworks and new features rework the checks and make requirements more stringent for using custom fonts to ensure zero errors also fixed the font enum builders to show clearly which fonts are actually usable partial newline support (draw.DrawText supports it natively), optionally using escape characters promote Text property to generic multiline panel class truncate mode: gradually write letters or words, optionally delimited by certain definable "skip" characters proxy-able vector type data source allow custom brackets and separators for nicer formatting --- lua/pac3/core/client/parts/text.lua | 424 ++++++++++++++++++++++------ 1 file changed, 338 insertions(+), 86 deletions(-) diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua index 865113758..c79ce9239 100644 --- a/lua/pac3/core/client/parts/text.lua +++ b/lua/pac3/core/client/parts/text.lua @@ -22,6 +22,40 @@ net.Receive("pac_chat_typing_mirror_broadcast", function(len) ply.pac_mirrored_chat_text = text end) +local TTT_fonts = { + "DefaultBold", + "TabLarge", + "Trebuchet22", + "TraitorState", + "TimeLeft", + "HealthAmmo", + "cool_small", + "cool_large", + "treb_small" +} + +local sandbox_fonts = { + "GModToolName", + "GModToolSubtitle", + "GModToolHelp", + "GModToolScreen", + "ContentHeader", + "GModWorldtip", + "ContentHeader", +} + +--all "de facto" usables: +--base gmod fonts +--sandbox OR TTT gamemode fonts +--created fonts that passed all checks +local usable_fonts = {} + +--all base usable: +--base gmod fonts +--sandbox OR TTT gamemode fonts +local included_fonts = {} + + local default_fonts = { "BudgetLabel", "CenterPrintText", @@ -70,23 +104,59 @@ local default_fonts = { "GModNotify", "ScoreboardDefault", "ScoreboardDefaultTitle", - "GModToolName", - "GModToolSubtitle", - "GModToolHelp", - "GModToolScreen", - "ContentHeader", - "GModWorldtip", - "ContentHeader", - "DefaultBold", - "TabLarge", - "Trebuchet22", - "TraitorState", - "TimeLeft", - "HealthAmmo", - "cool_small", - "cool_large", - "treb_small" } + +if engine.ActiveGamemode() == "sandbox" then + for i,v in ipairs(sandbox_fonts) do + table.insert(default_fonts,v) + end +elseif engine.ActiveGamemode() == "ttt" then + for i,v in ipairs(TTT_fonts) do + table.insert(default_fonts,v) + end +end + +for i,v in ipairs(default_fonts) do + usable_fonts[v] = true + default_fonts[v] = v --I want string key lookup... + included_fonts[v] = v +end + +local gmod_basefonts = { + --key is ttf filename, value is the nice title + ["akbar"] = "Akbar", + ["coolvetica"] = "Coolvetica", + ["csd"] = "csd", + ["Roboto-Black"] = "Roboto Black", + ["Roboto-BlackItalic"] = "Roboto Black Italic", + ["Roboto-Bold"] = "Roboto Bold", + ["Roboto-BoldCondensed"] = "Roboto Bold Condensed", + ["Roboto-BoldCondensedItalic"] = "Roboto Bold Condensed Italic", + ["Roboto-Condensed"] = "Roboto Condensed", + ["Roboto-CondensedItalic"] = "Roboto Condensed Italic", + ["Roboto-Italic"] = "Roboto Italic", + ["Roboto-Light"] = "Roboto Light", + ["Roboto-LightItalic"] = "Roboto Light Italic", + ["Roboto-Medium"] = "Roboto Medium", + ["Roboto-MediumItalic"] = "Roboto Medium Italic", + ["Roboto-Regular"] = "Roboto Regular", + ["Roboto-Thin"] = "Roboto Thin", + ["Roboto-Thin"] = "Roboto Thin Italic", + ["Tahoma"] = "Tahoma" +} + +local buildable_basefonts = {} +--create some fonts +for k,v in pairs(gmod_basefonts) do + buildable_basefonts[v] = v + local newfont = v .. "_30" + surface.CreateFont(newfont, { + font = v, + size = 30 + }) + table.insert(default_fonts, newfont) + usable_fonts[newfont] = true +end PART.ClassName = "text" @@ -97,24 +167,34 @@ BUILDER:StartStorableVars() :SetPropertyGroup("generic") :PropertyOrder("Name") :PropertyOrder("Hide") - :GetSet("Text", "") - :GetSet("Font", "default", {enums = default_fonts}) + :GetSet("Text", "", {editor_panel = "generic_multiline"}) + :GetSet("Font", "DermaDefault", {enums = default_fonts}) :GetSet("Size", 1, {editor_sensitivity = 0.25}) :GetSet("DrawMode", "DrawTextOutlined", {enums = { ["draw.SimpleTextOutlined 3D2D"] = "DrawTextOutlined", ["draw.SimpleTextOutlined 2D"] = "DrawTextOutlined2D", - ["surface.DrawText"] = "SurfaceText" + ["surface.DrawText"] = "SurfaceText", + ["draw.DrawText"] = "DrawDrawText" }}) :SetPropertyGroup("text layout") :GetSet("HorizontalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Left"] = "0", ["Center"] = "1", ["Right"] = "2"}}) :GetSet("VerticalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Center"] = "1", ["Top"] = "3", ["Bottom"] = "4"}}) :GetSet("ConcatenateTextAndOverrideValue",false,{editor_friendly = "CombinedText"}) - :GetSet("TextPosition","Prefix", {enums = {["Prefix"] = "Prefix", ["Postfix"] = "Postfix"}},{editor_friendly = "ConcatenateMode"}) + :GetSet("TextPosition","Prefix", {enums = {["Prefix"] = "Prefix", ["Postfix"] = "Postfix"}, description = "where the base text will be relative to the data-sourced text. this only applies when using Combined Text mode"}) + :GetSet("SentenceNewlines", false, {description = "With the punctuation marks . ! ? make a newline. Newlines only work with DrawDrawText mode.\nThis variable is useful for the chat modes since you can't put newlines in chat.\nBut if you're not using these, you might as well use the multiline text editor on the main text's [...] button"}) + :GetSet("Truncate", false, {description = "whether to cut the string off until a certain position. This can be used with proxies to gradually write the text.\nSkip Characters should normally be spaces and punctuation\nTruncate Words splits into words"}) + :GetSet("TruncateWords", false, {description = "whether to cut the string off until a certain position. This can be used with proxies to gradually write the text"}) + :GetSet("TruncateSkipCharacters", "", {description = "Characters to skip during truncation, or to split into words with the TruncateWords mode.\nNormally it could be a space, but if you want to split your text by lines (i.e. write one whole line at a time), write the escape character \"\\n\""}) + :GetSet("TruncatePosition", 0, {editor_onchange = function(self, val) return math.floor(val) end}) + :GetSet("VectorBrackets", "()") + :GetSet("VectorSeparator", ",") + :GetSet("UseBracketsOnNonVectors", false) :SetPropertyGroup("data source") :GetSet("TextOverride", "Text", {enums = { ["Proxy value (DynamicTextValue)"] = "Proxy", + ["Proxy vector (DynamicVectorValue)"] = "ProxyVector", ["Text"] = "Text", ["Health"] = "Health", ["Maximum Health"] = "MaxHealth", @@ -147,6 +227,7 @@ BUILDER:StartStorableVars() ["Last Chat Sent"] = "ChatSent", }}) :GetSet("DynamicTextValue", 0) + :GetSet("DynamicVectorValue", Vector(0,0,0)) :GetSet("RoundingPosition", 2, {editor_onchange = function(self, num) return math.Round(num,0) end}) @@ -169,7 +250,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Translucent", true) :SetPropertyGroup("CustomFont") :GetSet("CreateCustomFont",false, {description = "Tries to create a custom font.\nHeavily throttled as creating fonts is an expensive process.\nSupport is limited because of the fonts' supported features and the limits of Lua strings.\nFont names include those stored in your operating system. for example: Comic Sans MS, Ink Free"}) - :GetSet("CustomFont", "DermaDefault") + :GetSet("CustomFont", "DermaDefault", {enums = buildable_basefonts}) :GetSet("FontSize", 13) :GetSet("FontWeight",500) :GetSet("FontBlurSize",0) @@ -187,6 +268,9 @@ BUILDER:EndStorableVars() function PART:GetNiceName() if self.TextOverride ~= "Text" then return self.TextOverride end + if string.find(self.Text, "\n") then + if self.DrawMode == "DrawDrawText" then return "multiline text" else return string.Replace(self.Text, "\n", "") end + end return 'Text: "' .. self:GetText() .. '"' end @@ -208,6 +292,23 @@ function PART:SetAlpha(n) self.Alpha = n end +function PART:SetTruncateSkipCharacters(str) + self.TruncateSkipCharacters = str + if str == "" then self.TruncateSkipCharacters_tbl = nil return end + self.TruncateSkipCharacters_tbl = {} + for i=1,#str,1 do + local char = str[i] + if char == [[\]] then + if str[i+1] == "n" then self.TruncateSkipCharacters_tbl["\n"] = true + elseif str[i+1] == "t" then self.TruncateSkipCharacters_tbl["\t"] = true + elseif str[i+1] == [[\]] then self.TruncateSkipCharacters_tbl["\\"] = true + end + elseif str[i-1] ~= [[\]] then + self.TruncateSkipCharacters_tbl[char] = true + end + end +end + function PART:SetOutlineColor(v) self.OutlineColorC = self.OutlineColorC or Color(255, 255, 255, 255) @@ -225,42 +326,144 @@ function PART:SetOutlineAlpha(n) self.OutlineAlpha = n end +local function GetReasonBadFont_At_CreationTime(str) + local reason + if #str < 20 then + if not included_fonts[str] then reason = str .. " is not a font that exists" end + if engine.ActiveGamemode() ~= "sandbox" then + if table.HasValue(sandbox_fonts,str) then + reason = str .. " is a sandbox-exclusive font not available in the gamemode " .. engine.ActiveGamemode() + end + end + if engine.ActiveGamemode() ~= "ttt" then + if table.HasValue(TTT_fonts,str) then + reason = str .. " is a TTT-exclusive font not available in the gamemode " .. engine.ActiveGamemode() + end + end + else --standard part UID length + if #str > 31 then + reason = "you cannot create fonts with the base font being longer than 31 letters" + end + end + if string.find(str, "http") then + reason = "urls are not supported" + end + return reason +end + +local function GetReasonBadFont_At_UseTime(str) + local reason + if #str < 20 then + if not included_fonts[str] then reason = str .. " is not a font that exists" end + if engine.ActiveGamemode() ~= "sandbox" then + if table.HasValue(sandbox_fonts,str) then + reason = str .. " is a sandbox-exclusive font not available in the gamemode " .. engine.ActiveGamemode() + end + end + if engine.ActiveGamemode() ~= "ttt" then + if table.HasValue(TTT_fonts,str) then + reason = str .. " is a TTT-exclusive font not available in the gamemode " .. engine.ActiveGamemode() + end + end + else --standard part UID length + reason = str .. " is possibly a pac custom font from another text part but it's not guaranteed to be created right now\nor maybe it doesn't exist" + end + if string.find(str, "http") then + reason = "urls are not supported" + end + return reason +end + + +function PART:CheckFontBuildability(str) + if string.find(str, "http") then + return false, "urls are not supported" + end + if #str > 31 then return false, "base font is too long" end + if buildable_basefonts[str] then return true, "base font recognized from gmod" end + if included_fonts[str] then return true, "default font" end + return false, "nonexistent base font" +end + + +--before using a font, we need to check if it exists +--font creation time should mark them function PART:SetFont(str) - self.UsedFont = str - if not self.CreateCustomFont then - if not pcall(surface_SetFont, str) then - if #self.Font > 20 then - - self.lastwarn = self.lastwarn or CurTime() - if self.lastwarn > CurTime() + 1 then - pac.Message(Color(255,150,0),str.." Font not found! Could be custom font, trying again in 4 seconds!") - self.lastwarn = CurTime() + self.Font = str + self:SetError() + self:CheckFont() +end + + +local lastfontcreationtime = 0 +function PART:CheckFont() + if self.CreateCustomFont then + if not self:CheckFontBuildability(self.CustomFont) then + self.UsedFont = "DermaDefault" + self:SetError(GetReasonBadFont_At_UseTime(self.CustomFont) .. "\nreverting to " .. self.UsedFont) + else + self:TryCreateFont() + end + else + if usable_fonts[self.Font] then + self.UsedFont = self.Font + else + self.UsedFont = "DermaDefault" + self:SetError(GetReasonBadFont_At_UseTime(self.Font) .. "\nreverting to " .. self.UsedFont) + end + end +end + +function PART:SetCustomFont(str) + self.CustomFont = str + local buildable, reason = self:CheckFontBuildability(str) + --suppress if not requesting custom font, and if name is too long + if buildable then self:TryCreateFont() else return end + if self:GetPlayerOwner() == pac.LocalPlayer then + if not self.pace_properties then return end + if pace.current_part == self then + local pnl = self["pac_property_label_CustomFont"] + if IsValid(pnl) then + if not included_fonts[str] and not default_fonts[str] then + --pnl:SetValue("") + pnl:Clear() + pnl:CreateAlternateLabel("bad font", true) + pnl:SetTooltip(GetReasonBadFont_At_CreationTime(str)) + else + pace.PopulateProperties(self) end - timer.Simple(4, function() - if not pcall(surface_SetFont, str) then - pac.Message(Color(255,150,0),str.." Font still not found! Reverting to DermaDefault!") - str = "DermaDefault" - self.UsedFont = str - end - end) - else - timer.Simple(5, function() - if not pcall(surface_SetFont, str) then - pac.Message(Color(255,150,0),str.." Font still not found! Reverting to DermaDefault!") - str = "DermaDefault" - self.UsedFont = str - end - end) end end + end - self.Font = self.UsedFont end -local lastfontcreationtime = 0 + +function PART:GetBrackets() + local bracket1 = "" + local bracket2 = "" + local bracks = tostring(self.VectorBrackets) + if #bracks % 2 == 1 then + bracket1 = string.sub(bracks,1, (#bracks + 1) / 2) or "" + bracket2 = string.sub(bracks, (#bracks + 1) / 2, #bracks) or "" + else + bracket1 = string.sub(bracks,1, #bracks / 2) or "" + bracket2 = string.sub(bracks, #bracks / 2 + 1, #bracks) or "" + end + return bracket1, bracket2 +end + +function PART:GetNiceVector(vec) + local bracket1, bracket2 = self:GetBrackets() + return bracket1..math.Round(vec.x,self.RoundingPosition)..self.VectorSeparator..math.Round(vec.y,self.RoundingPosition)..self.VectorSeparator..math.Round(vec.z,self.RoundingPosition)..bracket2 +end + + function PART:OnDraw() local pos, ang = self:GetDrawPosition() + self:CheckFont() - if not pcall(surface_SetFont, self.UsedFont) then return end + if not self.UsedFont then self.UsedFont = self.Font end + if not usable_fonts[self.UsedFont] then return end local DisplayText = self.Text or "" if self.TextOverride == "Text" then goto DRAW end @@ -289,15 +492,13 @@ function PART:OnDraw() DisplayText = math.Round(ent:GetVelocity():Length(),2) elseif self.TextOverride == "VelocityVector" then local ent = self:GetOwner() or self:GetRootPart():GetOwner() - local vec = ent:GetVelocity() - DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")" + DisplayText = self:GetNiceVector(ent:GetVelocity()) elseif self.TextOverride == "PositionVector" then local vec = self:GetDrawPosition() - DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")" + DisplayText = self:GetNiceVector(vec) elseif self.TextOverride == "OwnerPositionVector" then local ent = self:GetRootPart():GetOwner() - local vec = ent:GetPos() - DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")" + DisplayText = self:GetNiceVector(ent:GetPos()) elseif self.TextOverride == "SequenceName" then DisplayText = self:GetRootPart():GetOwner():GetSequenceName(self:GetPlayerOwner():GetSequence()) elseif self.TextOverride == "PlayerName" then @@ -377,6 +578,8 @@ function PART:OnDraw() else DisplayText = "not driving" end elseif self.TextOverride == "Proxy" then DisplayText = ""..math.Round(self.DynamicTextValue,self.RoundingPosition) + elseif self.TextOverride == "ProxyVector" then + DisplayText = self:GetNiceVector(self.DynamicVectorValue) elseif self.TextOverride == "ChatTyping" then if self:GetPlayerOwner() == pac.LocalPlayer and not pac.broadcast_chat_typing then pac.AddHook("ChatTextChanged", "broadcast_chat_typing", function(text) @@ -400,6 +603,13 @@ function PART:OnDraw() end end + if not string.find(self.TextOverride, "Vector") then + if self.UseBracketsOnNonVectors then + local bracket1, bracket2 = self:GetBrackets() + DisplayText = bracket1 .. DisplayText .. bracket2 + end + end + if self.ConcatenateTextAndOverrideValue then if self.TextPosition == "Prefix" then DisplayText = ""..self.Text..DisplayText @@ -409,8 +619,49 @@ function PART:OnDraw() end ::DRAW:: + if self.Truncate then + + if self.TruncateSkipCharacters_tbl then + local temp_string = "" + local char_pos = 1 + local temp_chunk = "" + for i=1,#DisplayText,1 do + local char = DisplayText[i] + local escaped_char = false + if char == "\n" or char == "\t" then escaped_char = true end + if not self.TruncateWords then --char by char, add to the string only if it's a non-skip character + if char_pos > self.TruncatePosition then break end + if self.TruncateSkipCharacters_tbl[char] or escaped_char then + temp_chunk = temp_chunk .. char + else + temp_string = temp_string .. temp_chunk .. char + temp_chunk = "" + char_pos = char_pos + 1 + end + else --word by word, add to the string once i reaches the end or reaches a boundary + if char_pos > self.TruncatePosition then break end + if not self.TruncateSkipCharacters_tbl[char] and (self.TruncateSkipCharacters_tbl[DisplayText[i+1]] or i == #DisplayText) then + temp_string = string.sub(DisplayText,0,i) + char_pos = char_pos + 1 + end + end + end + DisplayText = temp_string + else + DisplayText = string.sub(DisplayText, 0, self.TruncatePosition) + end + end + + if self.SentenceNewlines then + DisplayText = string.Replace(DisplayText,". ",".\n") + DisplayText = string.Replace(DisplayText,"! ","!\n") + DisplayText = string.Replace(DisplayText,"? ","?\n") + 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) @@ -438,16 +689,17 @@ function PART:OnDraw() DisableClipping(oldState) cam_End3D2D() cam_End3D() - elseif self.DrawMode == "SurfaceText" or self.DrawMode == "DrawTextOutlined2D" then + elseif self.DrawMode == "SurfaceText" or self.DrawMode == "DrawTextOutlined2D" or self.DrawMode == "DrawDrawText" then pac.AddHook("HUDPaint", "pac.DrawText"..self.UniqueID, function() - if not pcall(surface_SetFont, self.UsedFont) then return end - self:SetFont(self.UsedFont) + surface.SetFont(self.UsedFont) surface.SetTextColor(self.Color.r, self.Color.g, self.Color.b, 255*self.Alpha) - surface.SetFont(self.UsedFont) + 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 @@ -478,6 +730,8 @@ function PART:OnDraw() 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) @@ -486,6 +740,8 @@ function PART:OnDraw() 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 @@ -499,48 +755,44 @@ function PART:OnDraw() end function PART:Initialize() + if self.Font == "default" then self.Font = "DermaDefault" end self:TryCreateFont() + self.anotherwarning = false end -function PART:CheckFont() - if self.CreateCustomFont then - lastfontcreationtime = lastfontcreationtime or 0 - if lastfontcreationtime + 3 <= CurTime() then - self:TryCreateFont() - end - else - self:SetFont(self.Font) - end - -end -function PART:TryCreateFont() - if "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID == self.lastcustomfont then - self.UsedFont = "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID - return - end +function PART:TryCreateFont(force_refresh) + local newfont = "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID if self.CreateCustomFont then - local newfont = "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID + if usable_fonts[newfont] then self.UsedFont = newfont self.Font = newfont return end + local buildable, reason = self:CheckFontBuildability(self.CustomFont) + --if reason == "default font" then self.CustomFont = "default" end + if not buildable then + return + end + if lastfontcreationtime + 2 > CurTime() then return end surface.CreateFont( newfont, { font = self.CustomFont, -- Use the font-name which is shown to you by your operating system Font Viewer, not the file name extended = self.Extended, size = self.FontSize, - weight = self.Weight, - blursize = self.BlurSize, - scanlines = self.ScanLines, - antialias = self.Antialias, - underline = self.Underline, - italic = self.Italic, - strikeout = self.Strikeout, - symbol = self.Symbol, - rotary = self.Rotary, + weight = self.FontWeight, + blursize = self.FontBlurSize, + scanlines = self.FontScanLines, + antialias = self.FontAntialias, + underline = self.FontUnderline, + italic = self.FontItalic, + strikeout = self.FontStrikeout, + symbol = self.FontSymbol, + rotary = self.FontRotary, shadow = self.Shadow, - additive = self.Additive, + additive = self.FontAdditive, outline = self.Outline, } ) - self:SetFont(newfont) - self.lastcustomfont = newfont lastfontcreationtime = CurTime() + --base fonts are ok to derive from + usable_fonts[newfont] = true + self.UsedFont = newfont + self.Font = newfont end end From 0238b3a3ce684b84ef6d593eb988914c313460b0 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 16 Jan 2025 23:54:56 -0500 Subject: [PATCH 268/300] lock and hitscan fixes spelling issues add some descriptions to force part and allow force actions that only do damping, without other forces (stopping momentum is a valid action to send to the server, unlike otherwise zero-force actions) --- lua/pac3/core/client/parts/force.lua | 36 +++++++++++++++++++++----- lua/pac3/core/client/parts/hitscan.lua | 4 +-- lua/pac3/core/client/parts/lock.lua | 8 +++--- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index a78e4eda5..7c0c20c26 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -25,9 +25,31 @@ BUILDER:StartStorableVars() :GetSet("BaseForce", 0) :GetSet("AddedVectorForce", Vector(0,0,0)) :GetSet("Torque", Vector(0,0,0)) - :GetSet("BaseForceAngleMode","Radial",{enums = {["Radial"] = "Radial", ["Locus"] = "Locus", ["Local"] = "Local"}}) - :GetSet("VectorForceAngleMode", "Global", {enums = {["Global"] = "Global", ["Local"] = "Local", ["Radial"] = "Radial", ["RadialNoPitch"] = "RadialNoPitch"}}) - :GetSet("TorqueMode", "TargetLocal", {enums = {["Global"] = "Global", ["TargetLocal"] = "TargetLocal", ["Local"] = "Local", ["Radial"] = "Radial"}}) + :GetSet("BaseForceAngleMode","Radial",{enums = {["Radial"] = "Radial", ["Locus"] = "Locus", ["Local"] = "Local"}, + description = +[[Radial points the base force outward from the force part. To point in, use negative values + +Locus points out from the locus (external point) + +Local points forward (red arrow) from the force part]]}) + :GetSet("VectorForceAngleMode", "Global", {enums = {["Global"] = "Global", ["Local"] = "Local", ["Radial"] = "Radial", ["RadialNoPitch"] = "RadialNoPitch"}, + description = +[[Global applies the vector force on world coordinates + +Local applies it based on the force part's angles + +Radial gets the base directions from the targets to the force part + +RadialNoPitch gets the base directions from the targets to the force part, but making pitch horizon-level]]}) + :GetSet("TorqueMode", "TargetLocal", {enums = {["Global"] = "Global", ["TargetLocal"] = "TargetLocal", ["Local"] = "Local", ["Radial"] = "Radial"}, + description = +[[Global applies the angular force on world coordinates + +TargetLocal applies it on the target's local angles + +Local applies it based on the force part's angles + +Radial gets the base directions from the targets to the force part]]}) :GetSetPart("Locus", nil) :SetPropertyGroup("Behaviors") @@ -39,9 +61,9 @@ BUILDER:StartStorableVars() :GetSet("LevitationHeight", 0) :SetPropertyGroup("Damping") - :GetSet("Damping", 0, {editor_clamp = {0,1}, editor_sensitivity = 0.1}) - :GetSet("DampingFalloff", false, {description = "Whether the damping should fade with distance"}) - :GetSet("DampingReverseFalloff", false, {description = "Whether the damping should fade with distance but reverse"}) + :GetSet("Damping", 0, {editor_clamp = {0,1}, editor_sensitivity = 0.1, description = "Reduces the existing velocity before applying force, by way of multiplication by (1-damping). 0 doesn't change it, while 1 is a full negation of the initial speed."}) + :GetSet("DampingFalloff", false, {description = "Whether the damping should fade with distance (further is weaker influence)"}) + :GetSet("DampingReverseFalloff", false, {description = "Whether the damping should fade with distance but reverse (closer is weaker influence)"}) :SetPropertyGroup("Targets") :GetSet("AffectSelf",false) @@ -174,7 +196,7 @@ function PART:Impulse(on) end else locus_pos = self:GetWorldPosition() end - if self.BaseForce == 0 and not game.SinglePlayer() then + if self.BaseForce == 0 and not game.SinglePlayer() and self.Damping == 0 then if math.abs(self.AddedVectorForce.x) < 10 and math.abs(self.AddedVectorForce.y) < 10 and math.abs(self.AddedVectorForce.z) < 10 then if math.abs(self.Torque.x) < 10 and math.abs(self.Torque.y) < 10 and math.abs(self.Torque.z) < 10 then return diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index 8c68ec4c3..78be2ff2d 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -187,7 +187,7 @@ local tracer_ids = { ["HunterTracer"] = 7, ["StriderTracer"] = 8, ["GunshipTracer"] = 9, - ["ToolgunTracer"] = 10, + ["ToolTracer"] = 10, ["LaserTracer"] = 11 } @@ -217,7 +217,7 @@ function PART:SendNetMessage() net.WriteUInt(self.Force, 16) net.WriteUInt(self.MaxDistance, 16) net.WriteUInt(self.NumberBullets, 9) - net.WriteUInt(tracer_ids[self.TracerName], 4) + net.WriteUInt(tracer_ids[self.TracerName] or 0, 4) net.WriteBool(self.DistributeDamage) net.WriteBool(self.DamageFalloff) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 9e94152de..d4e949f18 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -225,7 +225,7 @@ do function PART:BreakLock(ent) self.forcebreak = true self.next_allowed_grab = CurTime() + 3 - if self.target_ent then self.target_ent.IsGrabbedID = nil end + if self.target_ent then self.target_ent.IsGrabbedByUID = nil end self.target_ent = nil self.grabbing = false pac.Message(Color(255, 50, 50), "lock break result:") @@ -291,7 +291,7 @@ do part:BreakLock(target_to_mark) --yes we will employ the aggressive lock break here else target_to_mark.IsGrabbed = successful_grab - target_to_mark.IsGrabbedID = uid + target_to_mark.IsGrabbedByUID = uid target_to_mark:SetGravity(0) end end) @@ -389,7 +389,7 @@ function PART:OnHide() self.teleported = false self.grabbing = false if not IsValid(self.target_ent) then return - else self.target_ent.IsGrabbed = false self.target_ent.IsGrabbedID = nil end + else self.target_ent.IsGrabbed = false self.target_ent.IsGrabbedByUID = nil end if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end self:reset_ent_ang() end @@ -505,7 +505,7 @@ function PART:CheckEntValidity() self.valid_ent = true end if self.target_ent ~= nil then - if self.target_ent.IsGrabbedID and self.target_ent.IsGrabbedID ~= self.UniqueID then self.valid_ent = false end + if self.target_ent.IsGrabbedByUID and self.target_ent.IsGrabbedByUID ~= self.UniqueID then self.valid_ent = false end end if not self.valid_ent then self.target_ent = nil end --print("ent check:",self.valid_ent) From ba7dc80f36a484040fc731349da4dca48e1c0b25 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 17 Jan 2025 00:30:39 -0500 Subject: [PATCH 269/300] remove unused code --- lua/pac3/extra/shared/net_combat.lua | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index db13e89b6..a695278e0 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1580,16 +1580,6 @@ if SERVER then local endpos = pos + ang:Forward()*tbl.Length ents_hits = ents.FindAlongRay(startpos, endpos) ProcessForcesList(ents_hits, tbl, pos, ang, ply) - - if tbl.Bullet then - local bullet = {} - bullet.Src = pos + ang:Forward() - bullet.Dir = ang:Forward()*50000 - bullet.Damage = -1 - bullet.Force = 0 - bullet.Entity = dmg_info:GetAttacker() - dmg_info:GetInflictor():FireBullets(bullet) - end end end From ba54007d1afb504c99c17ecc46e2a29b126f4a3f Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 17 Jan 2025 17:06:12 -0500 Subject: [PATCH 270/300] small material fixes unlit hack option for submaterial. because unlit materials from pac weren't being correctly used. works by forcing pac.Material lookup to replace the default fallback "Fix" material created by the submaterial. reapply the hide flag on material part on initialize. because the fact that hiding the part should prevent auto-applying the material on its parent wasn't being obeyed at weartime --- lua/pac3/core/client/parts/material.lua | 1 + lua/pac3/core/client/parts/submaterial.lua | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/lua/pac3/core/client/parts/material.lua b/lua/pac3/core/client/parts/material.lua index 04886206f..8543f7761 100644 --- a/lua/pac3/core/client/parts/material.lua +++ b/lua/pac3/core/client/parts/material.lua @@ -271,6 +271,7 @@ for shader_name, groups in pairs(shader_params.shaders) do self.rotation_angle = Angle(0, 0, 0) timer.Simple(0, function() self:SetbasetexturetransformAngle(self:GetbasetexturetransformAngle()) + self:SetHide(self:GetHide()) end) end diff --git a/lua/pac3/core/client/parts/submaterial.lua b/lua/pac3/core/client/parts/submaterial.lua index d3b53ae58..7e4c5df0c 100644 --- a/lua/pac3/core/client/parts/submaterial.lua +++ b/lua/pac3/core/client/parts/submaterial.lua @@ -23,6 +23,7 @@ BUILDER:StartStorableVars() return tbl end, }) + BUILDER:GetSet("UnlitMaterialHack", false, {description = "hacky fix if the material comes from a material part with unlitgeneric shader.\nBut it will break raw url textures!"}) BUILDER:GetSet("RootOwner", false, { hide_in_editor = true }) BUILDER:EndStorableVars() @@ -63,6 +64,7 @@ function PART:UpdateSubMaterialId(id, material) ent.pac_submaterials = ent.pac_submaterials or {} local mat = self.Materialm + if self.UnlitMaterialHack then mat = pac.Material(self.Material, self) end if not material then if self.Material and self.Material ~= "" and mat and not mat:IsError() then @@ -79,6 +81,11 @@ function PART:UpdateSubMaterialId(id, material) end end +function PART:SetUnlitMaterialHack(b) + self.UnlitMaterialHack = b + self:UpdateSubMaterialId() +end + function PART:PostApplyFixes() self:UpdateSubMaterialId() end From 65a22ff52d941100209736d524403bcab4b1e7e3 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Fri, 31 Jan 2025 01:40:22 -0500 Subject: [PATCH 271/300] more camera part quick setups (place camera from editor view) apply editor view - translates editor camera position into local space, with a series of sub-options to switch between editor or default camera FOV, and select bones centralized the zoom slider's bounds-setting functions into one Add an option on the fov slider to raise its fov limit. By default it should stay at 100, but expanding it to 179 would be used to mirror the camera part's limits to set up some of the more exaggerated camera shots we might see in stylized animation and such (e.g. high FOV close-up on a weapon to make it look bigger). converted some of the camera quick setup options into right clickable options that don't automatically close the menu, allowing a better experience in trying out different configs. I'm experimenting with that, would be good when there are occasionally options with multiple choices which would benefit from immediate 1-click feedback. I will tentatively explore this, but it should be used sparingly, for various reasons. misusing it might result in anti-QoL because most options introduce side effects that don't get undone by neighboring options, since they often do separate things try to look for an alternate head if the standard valvebiped head isn't found the base height used for calculation is now a traced hit to sample the top of the head instead of the base of the head bone. hopefully it works better. it's also manually adjustable (right clickable options refresh the height variable) to other alternates (between head bone's base, trace, hull height, eyepos), it's ultimately a micro difference but will be added for the sake of information and comparison add option to clone the camera position as a dummy model for interpolators --- lua/pac3/editor/client/panels/editor.lua | 38 ++- lua/pac3/editor/client/parts.lua | 363 +++++++++++++++++------ lua/pac3/editor/client/view.lua | 33 ++- 3 files changed, 336 insertions(+), 98 deletions(-) diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua index 1d7681c1b..9cae0b1df 100644 --- a/lua/pac3/editor/client/panels/editor.lua +++ b/lua/pac3/editor/client/panels/editor.lua @@ -21,6 +21,22 @@ local zoom_smooth = CreateClientConVar("pac_zoom_smooth", 0, true, false, 'Enabl local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's vertical divider position") local remember_width = CreateConVar("pac_editor_remember_width", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's width") +function pace.RefreshZoomBounds(zoomslider) + if pace.Editor then + if not zoomslider then + zoomslider = pace.Editor.zoomslider + end + if pace.camera_orthographic then + zoomslider:SetMin(-10000) + zoomslider:SetMax(10000) + else + zoomslider:SetMin(0) + zoomslider:SetMax(pace.max_fov) + timer.Simple(0, function() zoomslider:SetValue(math.Clamp(pace.ViewFOV, 0, pace.max_fov)) end) + end + end +end + function PANEL:Init() self:SetTitle("") self:SetSizable(true) @@ -104,6 +120,14 @@ function PANEL:Init() self.smoothlabel:SetWrap(true) self.smoothlabel:SetAutoStretchVertical(true) + self.limitfovcheckbox = vgui.Create("DCheckBoxLabel", self.zoomsettings) + self.limitfovcheckbox:SetText("Expanded FOV") + self.limitfovcheckbox:SetChecked(pace.max_fov ~= 100) + self.limitfovcheckbox:Dock(TOP) + self.limitfovcheckbox:SetDark(true) + self.limitfovcheckbox:DockMargin(0,SETTING_MARGIN_TOP,0,0) + + self.orthocheckbox = vgui.Create("DCheckBoxLabel", self.zoomsettings) self.orthocheckbox:SetText("Orthographic") self.orthocheckbox:Dock(TOP) @@ -151,15 +175,12 @@ function PANEL:Init() self.zoomslider = vgui.Create("DNumSlider", self.sliderpanel) self.zoomslider:DockPadding(4,0,0,0) self.zoomslider:SetSize(200, 20) - self.zoomslider:SetMin( 0 ) - self.zoomslider:SetMax( 100 ) self.zoomslider:SetDecimals( 0 ) self.zoomslider:SetText("Camera FOV") if pace.camera_orthographic then self.zoomslider:SetText("Ortho. Width") - self.zoomslider:SetMin( -10000 ) - self.zoomslider:SetMax( 10000 ) end + pace.RefreshZoomBounds(self.zoomslider) self.zoomslider:SetDark(true) self.zoomslider:SetDefaultValue( 75 ) @@ -168,6 +189,15 @@ function PANEL:Init() else self.zoomslider:SetValue( 75 ) end + local zoomslider = self.zoomslider + function self.limitfovcheckbox:OnChange(b) + if b then + pace.max_fov = 179 + else + pace.max_fov = 100 + end + pace.RefreshZoomBounds(zoomslider) + end self.btnClose.Paint = function() end diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 34b34cbbf..ccd117717 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2571,7 +2571,26 @@ local part_classes_with_quicksetups = { jiggle = true, interpolated_multibone = true, } - +local function AddOptionRightClickable(title, func, parent_menu) + local pnl = parent_menu:AddOption(title, func) + function pnl:Think() + if input.IsMouseDown(MOUSE_RIGHT) and self:IsHovered() then + if not self.clicked then func() end + self.clicked = true + else + self.clicked = false + end + end + --to communicate to the user that they can right click to activate without closing the parent menu + --tooltip may be set later, so we'll sandwich it with a timer + timer.Simple(0.5, function() + if not IsValid(pnl) then return end + if pnl:GetTooltip() == nil then + pnl:SetTooltip("You can right click this to use the option without exiting the menu") + end + end) + return pnl +end --those are more to configure a part into common setups, might involve creating other parts function pace.AddQuickSetupsToPartMenu(menu, obj) if not part_classes_with_quicksetups[obj.ClassName] and not obj.GetDrawPosition then @@ -3078,6 +3097,106 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) pace.RefreshTree(true) end):SetIcon("icon16/user.png") elseif obj.ClassName == "camera" then + menu:AddOption("clone position as a node for interpolators", function() + local newpart = pac.CreatePart("model2") + newpart:SetParent(obj:GetParent()) + newpart:SetModel("models/editor/camera.mdl") newpart:SetMaterial("models/wireframe") + newpart:SetPosition(obj:GetPosition()) + newpart:SetPositionOffset(obj:GetPositionOffset()) + newpart:SetAngles(obj:GetAngles()) + newpart:SetAngleOffset(obj:GetAngleOffset()) + newpart:SetBone(obj:GetBone()) + newpart:SetNotes("editor FOV: " .. math.Round(pace.ViewFOV,1) .. + "\ncamera FOV: " .. math.Round(obj:GetFOV(),1) .. + (obj.Name ~= "" and ("\ncamera name: " .. obj:GetName()) or "") .. + "\ncamera UID: " .. obj.UniqueID + ) + Derma_StringRequest("Set a name", "give a name to the camera position node", "camera_node", function(str) + newpart:SetName(str) + if newpart.pace_tree_node then + newpart.pace_tree_node:SetText(str) + end + end) + end):SetImage("icon16/find.png") + + local bone_parent = obj:GetParent() + if obj:GetOwner() ~= obj:GetRootPart():GetOwner() then + while not (bone_parent.Bone and bone_parent.GetWorldPosition) do + bone_parent = bone_parent:GetParent() + if bone_parent:GetOwner() == obj:GetRootPart():GetOwner() then + bone_parent = obj:GetRootPart():GetOwner() + end + end + else + bone_parent = obj:GetRootPart():GetOwner() + end + local function bone_reposition(bone) + local bone_pos, bone_ang = pac.GetBonePosAng(obj:GetOwner(), bone) + local pos, ang = WorldToLocal(pace.ViewPos, pace.view_roll and pace.ViewAngles_postRoll or pace.ViewAngles, bone_pos, bone_ang) + obj:SetPosition(pos) obj:SetAngles(ang) obj:SetEyeAnglesLerp(0) obj:SetBone(bone) + pace.PopulateProperties(obj) + end + local translate_from_view, pnl = main:AddSubMenu("Apply editor view", function() + bone_reposition(obj.Bone) + end) pnl:SetImage("icon16/arrow_redo.png") + + AddOptionRightClickable("apply FOV: " .. math.Round(pace.ViewFOV,1), function() + obj:SetFOV(math.Round(pace.ViewFOV,1)) + pace.PopulateProperties(obj) + end, translate_from_view):SetImage("icon16/zoom.png") + + AddOptionRightClickable("reset FOV" , function() + obj:SetFOV(-1) + pace.PopulateProperties(obj) + end, translate_from_view):SetImage("icon16/zoom_out.png") + + translate_from_view:AddOption("current bone: " .. obj.Bone, function() + bone_reposition(obj.Bone) + end):SetImage("icon16/arrow_redo.png") + translate_from_view:AddOption("no bone", function() + bone_reposition("invalidbone") + end):SetImage("icon16/arrow_redo.png") + + local bone_list = {} + if isentity(bone_parent) then + bone_list = pac.GetAllBones(bone_parent) + else + bone_list = pac.GetAllBones(bone_parent:GetOwner()) + end + local bonekeys = {} + local common_human_bones = { + "head", "neck", "spine", "spine 1", "spine 2", "spine 4", "pelvis", + "left clavicle", "left upperarm", "left forearm", "left hand", + "right clavicle", "right upperarm", "right forearm", "right hand", + "left thigh", "left calf", "left foot", "left toe", "right thigh", "right calf", "right foot", "right toe" + } + local sorted_bonekeys = {} + for i,v in pairs(bone_list) do + bonekeys[v.friendly] = v.friendly + end + for i,v in SortedPairs(bonekeys) do + table.insert(sorted_bonekeys, v) + end + + --basic humanoid bones + if bone_list["spine"] and bone_list["head"] and bone_list["left upperarm"] and bone_list["right upperarm"] then + local common_bones_menu, pnl = translate_from_view:AddSubMenu("shortened bone list (humanoid)") pnl:SetIcon("icon16/user.png") + for _,bonename in ipairs(common_human_bones) do + AddOptionRightClickable(bonename, function() + bone_reposition(bonename) + end, common_bones_menu):SetImage("icon16/user.png") + end + end + + translate_from_view:AddSpacer() + local full_bones_menu, pnl = translate_from_view:AddSubMenu("full bone list for " .. tostring(bone_parent)) pnl:SetIcon("icon16/user_add.png") + --full bone list + for _,bonename in ipairs(sorted_bonekeys) do + AddOptionRightClickable(bonename, function() + bone_reposition(bonename) + end, full_bones_menu):SetImage("icon16/connect.png") + end + local function extract_camera_from_jiggle() camera = obj if not IsValid(camera.recent_jiggle) then @@ -3090,7 +3209,14 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) local camparent = jig:GetParent() camera:SetParent(camparent) camera:SetBone(cambone) camera:SetAngles(camang) camera:SetPosition(campos) - jig:SetBone("head") jig:SetAngles(Angle(0,0,0)) jig:SetPosition(Vector(0,0,0)) + jig:Remove() + if not camera:IsHidden() then + if not camera.Hide then + timer.Simple(0, function() + camera:SetHide(true) camera:SetHide(false) + end) + end + end end local function insert_camera_into_jiggle() camera = obj @@ -3104,123 +3230,182 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) jig:SetBone(camera.Bone) jig:SetAngles(camera:GetAngles()) jig:SetPosition(camera:GetPosition()) camera:SetBone("head") camera:SetAngles(Angle(0,0,0)) camera:SetPosition(Vector(0,0,0)) camera:SetParent(jig) + if not camera:IsHidden() then + if not camera.Hide then + timer.Simple(0, function() + camera:SetHide(true) camera:SetHide(false) + end) + end + end + return jig end --helper variable to adjust relative to player height local ent = obj:GetRootPart():GetOwner() - local height = math.Round((ent:GetBonePosition(ent:LookupBone("ValveBiped.Bip01_Head1")) - ent:GetPos()).z,1) - main:AddOption("calculated head height : " .. height):SetIcon("icon16/help.png") + local default_headbone = ent:LookupBone("ValveBiped.Bip01_Head1") + if not default_headbone then + for i=0,ent:GetBoneCount(),1 do + if string.find(ent:GetBoneName(i), "head") or string.find(ent:GetBoneName(i), "Head") then + default_headbone = i + break + end + end + end - local fp, pnl = main:AddSubMenu("first person camera setups") pnl:SetImage("icon16/eye.png") - fp:AddOption("easy first person (head)", function() + if default_headbone then + local head_base_pos = ent:GetBonePosition(default_headbone) + local trace = util.QuickTrace(head_base_pos + Vector(0,0,50), Vector(0,0,-10000), function(ent2) return ent2 == ent end) + local mins, maxs = ent:GetHull() + + local height_headbase = math.Round((head_base_pos - ent:GetPos()).z,1) + local height_eyepos = math.Round((ent:EyePos() - ent:GetPos()).z,1) + local height_traced = math.Round((trace.HitPos - ent:GetPos()).z,1) + local height_hull = (maxs - mins).z + + local height = height_traced + if trace.Entity ~= ent then + height = height_headbase + end + + local info, pnl = main:AddSubMenu("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)") + info:AddOption("alternate height calculations"):SetImage("icon16/help.png") + info:SetTooltip("Due to lack of standardization on models' scales, heights are not guaranteed to be accurate or consistent\n\nThe unit conversion used is 1 Hammer Unit : 2.5 cm (1 inch)") + info:AddSpacer() + + AddOptionRightClickable("head bone's base position : " .. height_headbase .. " HU (" .. math.Round(height_headbase / 39,2) .." m)", function() + height = height_headbase + pnl:SetText("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)") + end, info):SetIcon("icon16/monkey.png") + AddOptionRightClickable("traced to top of the head: " .. height_traced .. " HU (" .. math.Round(height_traced / 39,2) .." m)", function() + height = height_traced + pnl:SetText("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)") + end, info):SetIcon("icon16/arrow_down.png") + AddOptionRightClickable("player eye position (ent:EyePos()) : " .. height_eyepos .. " HU (" .. math.Round(height_eyepos / 39,2) .." m)", function() + height = height_eyepos + pnl:SetText("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)") + end, info):SetIcon("icon16/eye.png") + AddOptionRightClickable("hull dimensions : " .. height_hull .. " HU (" .. math.Round(height_hull / 39,2) .." m)", function() + height = height_hull + pnl:SetText("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)") + end, info):SetIcon("icon16/collision_on.png") + + pnl:SetImage("icon16/help.png") + pnl:SetTooltip(ent:GetBoneName(default_headbone) .. "\n" .. ent:GetModel()) + local fp, pnl = main:AddSubMenu("first person camera setups") pnl:SetImage("icon16/eye.png") + AddOptionRightClickable("easy first person (head)", function() + extract_camera_from_jiggle() + obj:SetBone("head") + obj:SetPosition(Vector(5,-4,0)) obj:SetEyeAnglesLerp(1) obj:SetAngles(Angle(0,-90,-90)) + pace.PopulateProperties(obj) + end, fp):SetIcon("icon16/eye.png") + + AddOptionRightClickable("on neck + collapsed head", function() + extract_camera_from_jiggle() + obj:SetBone("neck") + obj:SetPosition(Vector(5,0,0)) obj:SetEyeAnglesLerp(1) obj:SetAngles(Angle(0,-90,-90)) + local bone = pac.CreatePart("bone3") + bone:SetScaleChildren(true) bone:SetSize(0) + bone:SetParent(obj) + local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(bone) + pace.PopulateProperties(obj) + end, fp):SetIcon("icon16/eye.png") + + AddOptionRightClickable("on neck + collapsed head + eyeang limiter", function() + extract_camera_from_jiggle() + obj:SetBone("neck") + obj:SetPosition(Vector(5,0,0)) obj:SetEyeAnglesLerp(0.7) obj:SetAngles(Angle(0,-90,-90)) + local bone = pac.CreatePart("bone3") + bone:SetScaleChildren(true) bone:SetSize(0) + bone:SetParent(obj) + local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(bone) + pace.PopulateProperties(obj) + end, fp):SetIcon("icon16/eye.png") + + AddOptionRightClickable("smoothen", function() + insert_camera_into_jiggle() + pace.PopulateProperties(obj) + end, main):SetIcon("icon16/chart_line.png") + AddOptionRightClickable("undo smoothen (extract from jiggle)", function() extract_camera_from_jiggle() - obj:SetBone("head") - obj:SetPosition(Vector(5,-4,0)) obj:SetEyeAnglesLerp(1) obj:SetAngles(Angle(0,-90,-90)) pace.PopulateProperties(obj) - end):SetIcon("icon16/eye.png") - - fp:AddOption("on neck + collapsed head", function() + end, main):SetIcon("icon16/chart_line_delete.png") + + AddOptionRightClickable("close up (zoomed on the face)", function() extract_camera_from_jiggle() - obj:SetBone("neck") - obj:SetPosition(Vector(5,0,0)) obj:SetEyeAnglesLerp(1) obj:SetAngles(Angle(0,-90,-90)) - local bone = pac.CreatePart("bone3") - bone:SetScaleChildren(true) bone:SetSize(0) - bone:SetParent(obj) - local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(bone) + obj:SetBone("head") obj:SetAngles(Angle(0,90,90)) obj:SetPosition(Vector(3,-20,0)) obj:SetEyeAnglesLerp(0) obj:SetFOV(45) pace.PopulateProperties(obj) - end):SetIcon("icon16/eye.png") - - fp:AddOption("on neck + collapsed head + eyeang limiter", function() + end, main):SetIcon("icon16/monkey.png") + + AddOptionRightClickable("Cowboy / medium shot (waist up) (relative to neck)", function() extract_camera_from_jiggle() - obj:SetBone("neck") - obj:SetPosition(Vector(5,0,0)) obj:SetEyeAnglesLerp(0.7) obj:SetAngles(Angle(0,-90,-90)) - local bone = pac.CreatePart("bone3") - bone:SetScaleChildren(true) bone:SetSize(0) - bone:SetParent(obj) - local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(bone) + obj:SetBone("neck") obj:SetAngles(Angle(0,120,90)) obj:SetPosition(Vector(14,-24,0)) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) pace.PopulateProperties(obj) - end):SetIcon("icon16/eye.png") - - main:AddOption("smoothen", function() - insert_camera_into_jiggle() - pace.PopulateProperties(obj) - end):SetIcon("icon16/chart_line.png") - - main:AddOption("close up (zoomed on the face)", function() - extract_camera_from_jiggle() - obj:SetBone("head") obj:SetAngles(Angle(0,90,90)) obj:SetPosition(Vector(3,-20,0)) obj:SetEyeAnglesLerp(0) obj:SetFOV(45) - pace.PopulateProperties(obj) - end):SetIcon("icon16/monkey.png") - - main:AddOption("Cowboy / medium shot (waist up) (relative to neck)", function() - extract_camera_from_jiggle() - obj:SetBone("neck") obj:SetAngles(Angle(0,120,90)) obj:SetPosition(Vector(14,-24,0)) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) - pace.PopulateProperties(obj) - end):SetIcon("icon16/user.png") - - main:AddOption("Cowboy / medium shot (waist up) (no bone) (20 + 0.6*height = " .. (20 + 0.6*height) .. ")", function() - extract_camera_from_jiggle() - obj:SetBone("invalidbone") obj:SetAngles(Angle(0,180,0)) obj:SetPosition(Vector(40,0,20 + 0.6*height)) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) - pace.PopulateProperties(obj) - end):SetIcon("icon16/user.png") - - main:AddOption("over the shoulder (no bone) (12 + 0.8*height = " .. 12 + 0.8*height .. ")", function() - extract_camera_from_jiggle() - obj:SetBone("invalidbone") obj:SetAngles(Angle(0,0,0)) obj:SetPosition(Vector(-30,15,12 + 0.8*height)) obj:SetEyeAnglesLerp(0.3) obj:SetFOV(-1) - pace.PopulateProperties(obj) - end):SetIcon("icon16/user_gray.png") - - main:AddOption("over the shoulder (with jiggle)", function() - local jiggle = insert_camera_into_jiggle() - jiggle:SetConstrainSphere(75) jiggle:SetSpeed(3) - obj:SetEyeAnglesLerp(0.7) obj:SetFOV(-1) - jiggle:SetBone("neck") jiggle:SetAngles(Angle(180,90,90)) jiggle:SetPosition(Vector(-2,18,-10)) - pace.PopulateProperties(obj) - end):SetIcon("icon16/user_gray.png") + end, main):SetIcon("icon16/user.png") + + AddOptionRightClickable("Cowboy / medium shot (waist up) (no bone) (20 + 0.6*height)", function() + extract_camera_from_jiggle() + obj:SetBone("invalidbone") obj:SetAngles(Angle(0,180,0)) obj:SetPosition(Vector(40,0,20 + 0.6*height)) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) + pace.PopulateProperties(obj) + end, main):SetIcon("icon16/user.png") + + AddOptionRightClickable("over the shoulder (no bone) (12 + 0.8*height)", function() + extract_camera_from_jiggle() + obj:SetBone("invalidbone") obj:SetAngles(Angle(0,0,0)) obj:SetPosition(Vector(-30,15,12 + 0.8*height)) obj:SetEyeAnglesLerp(0.3) obj:SetFOV(-1) + pace.PopulateProperties(obj) + end, main):SetIcon("icon16/user_gray.png") + + AddOptionRightClickable("over the shoulder (with jiggle)", function() + local jiggle = insert_camera_into_jiggle() + jiggle:SetConstrainSphere(75) jiggle:SetSpeed(3) + obj:SetEyeAnglesLerp(0.7) obj:SetFOV(-1) + jiggle:SetBone("neck") jiggle:SetAngles(Angle(180,90,90)) jiggle:SetPosition(Vector(-2,18,-10)) + pace.PopulateProperties(obj) + end, main):SetIcon("icon16/user_gray.png") - main:AddOption("full shot", function() - extract_camera_from_jiggle() - obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) - obj:SetBone("invalidbone") obj:SetAngles(Angle(6,180,0)) obj:SetPosition(Vector(height,-15,height * 0.7)) - pace.PopulateProperties(obj) - end):SetIcon("icon16/user_suit.png") + AddOptionRightClickable("full shot (0.7*height)", function() + extract_camera_from_jiggle() + obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) + obj:SetBone("invalidbone") obj:SetAngles(Angle(6,180,0)) obj:SetPosition(Vector(height,-15,height * 0.7)) + pace.PopulateProperties(obj) + end, main):SetIcon("icon16/user_suit.png") + end - main:AddOption("wide shot (with jiggle)", function() + AddOptionRightClickable("wide shot (with jiggle)", function() local jiggle = insert_camera_into_jiggle() jiggle:SetConstrainSphere(150) jiggle:SetSpeed(1) obj:SetEyeAnglesLerp(0.2) obj:SetFOV(-1) jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(0,15,120)) obj:SetPosition(Vector(-250,0,0)) pace.PopulateProperties(obj) - end):SetIcon("icon16/arrow_out.png") + end, main):SetIcon("icon16/arrow_out.png") - main:AddOption("extreme wide shot (with jiggle)", function() + AddOptionRightClickable("extreme wide shot (with jiggle)", function() local jiggle = insert_camera_into_jiggle() jiggle:SetConstrainSphere(0) jiggle:SetSpeed(0.3) obj:SetEyeAnglesLerp(0.1) obj:SetFOV(-1) jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(-500,0,200)) obj:SetPosition(Vector(0,0,0)) obj:SetAngles(Angle(15,0,0)) pace.PopulateProperties(obj) - end):SetIcon("icon16/map.png") + end, main):SetIcon("icon16/map.png") - main:AddOption("bird eye view (with jiggle)", function() + AddOptionRightClickable("bird eye view (with jiggle)", function() local jiggle = insert_camera_into_jiggle() jiggle:SetConstrainSphere(300) jiggle:SetSpeed(1) obj:SetEyeAnglesLerp(0.2) obj:SetFOV(-1) jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(-150,0,300)) obj:SetPosition(Vector(0,0,0)) obj:SetAngles(Angle(70,0,0)) pace.PopulateProperties(obj) - end):SetIcon("icon16/map_magnify.png") + end, main):SetIcon("icon16/map_magnify.png") - main:AddOption("Dutch shot (tilt)", function() + AddOptionRightClickable("Dutch shot (tilt)", function() local jiggle = insert_camera_into_jiggle() jiggle:SetConstrainSphere(150) jiggle:SetSpeed(1) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1) jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(0,15,50)) obj:SetPosition(Vector(-75,0,0)) obj:SetAngles(Angle(0,0,25)) pace.PopulateProperties(obj) - end):SetIcon("icon16/arrow_refresh.png") + end, main):SetIcon("icon16/arrow_refresh.png") elseif obj.ClassName == "faceposer" then if obj:GetDynamicProperties() == nil then main:AddOption("No flexes found!"):SetIcon("icon16/cancel.png") return end main:AddOption("reset expressions", function() @@ -3600,12 +3785,16 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) if obj.ClassName == "camera" then if not obj:IsHidden() then - if obj ~= pac.active_camera then - menu:AddOption("View this camera", function() + local remembered_view = {pace.ViewPos, pace.ViewAngles} + local view + local viewing = obj == pac.active_camera + local initial_name = viewing and "Unview this camera" or "View this camera" + view = AddOptionRightClickable(initial_name, function() + if not viewing then + remembered_view = {pace.ViewPos, pace.ViewAngles} pace.ManuallySelectCamera(obj, true) - end):SetIcon("icon16/star.png") - else - menu:AddOption("Unview this camera", function() + view:SetText("Unview this camera") + else pace.EnableView(true) pace.ResetView() pac.active_camera_manual = nil @@ -3618,8 +3807,12 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) end end end - end):SetIcon("icon16/camera_delete.png") - end + pace.ViewPos = remembered_view[1] + pace.ViewAngles = remembered_view[2] + view:SetText("View this camera") + end + viewing = obj == pac.active_camera + end, menu) view:SetIcon("icon16/star.png") else menu:AddOption("View this camera", function() local toggleable_command_events = {} diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua index 40740c4e3..7b103ccd4 100644 --- a/lua/pac3/editor/client/view.lua +++ b/lua/pac3/editor/client/view.lua @@ -104,6 +104,7 @@ pace.camera_down_bind = CreateClientConVar("pac_editor_camera_down_bind", "", tr pace.camera_slow_bind = CreateClientConVar("pac_editor_camera_slow_bind", "ctrl", true) pace.camera_speed_bind = CreateClientConVar("pac_editor_camera_speed_bind", "shift", true) +pace.max_fov = 100 pace.camera_roll_drag_bind = CreateClientConVar("pac_editor_camera_roll_bind", "", true) pace.roll_snapping = CreateClientConVar("pac_camera_roll_snap", "0", true) @@ -119,7 +120,6 @@ function pace.OrthographicView(b) if pace.camera_orthographic then timer.Simple(1, function() pace.FlashNotification("Switched to orthographic mode") end) pace.Editor.zoomslider:SetText("Ortho. Width") - pace.Editor.zoomslider:SetMax( 1000 ) pace.Editor.zoomslider:SetValue(50) pace.Editor.ortho_nearz:Show() pace.Editor.ortho_farz:Show() @@ -127,10 +127,10 @@ function pace.OrthographicView(b) timer.Simple(1, function() pace.FlashNotification("Switched to normal FOV mode") end) pace.Editor.zoomslider:SetText("Camera FOV") pace.Editor.zoomslider:SetValue(75) - pace.Editor.zoomslider:SetMax( 100 ) pace.Editor.ortho_nearz:Hide() pace.Editor.ortho_farz:Hide() end + pace.RefreshZoomBounds(pace.Editor.zoomslider) end end @@ -185,16 +185,24 @@ end function pace.SetZoom(fov, smooth) if smooth then - pace.ViewFOV = Lerp(FrameTime()*10, pace.ViewFOV, math.Clamp(fov,1,100)) + if pace.camera_orthographic then + pace.ViewFOV = Lerp(FrameTime()*10, pace.ViewFOV, math.Clamp(fov,-10000,10000)) + return + end + pace.ViewFOV = Lerp(FrameTime()*10, pace.ViewFOV, math.Clamp(fov,1,pace.max_fov)) else - pace.ViewFOV = math.Clamp(fov,1,100) + if pace.camera_orthographic then + pace.ViewFOV = math.Clamp(fov,-10000,10000) + return + end + pace.ViewFOV = math.Clamp(fov,1,pace.max_fov) end end function pace.ResetZoom() pace.zoom_reset = 75 end - + local worldPanel = vgui.GetWorldPanel(); function worldPanel.OnMouseWheeled( self, scrollDelta ) if IsValid(pace.Editor) then @@ -660,6 +668,16 @@ function pace.CalcView(ply, pos, ang, fov) viewang_final = pace.ViewAngles_postRoll end + + --[[ + local entpos = pace.GetViewEntity():WorldSpaceCenter() + local diff = pace.ViewPos - entpos + local MAX_CAMERA_DISTANCE = 300 + local backtrace = util.QuickTrace(entpos, diff*50000, ply) + local final_dist = math.min(diff:Length(), MAX_CAMERA_DISTANCE, (backtrace.HitPos - entpos):Length() - 10) + pace.ViewPos = entpos + diff:GetNormalized() * final_dist + ]] + if not pace.camera_orthographic then return { @@ -1033,15 +1051,12 @@ function pace.PopupMiniFOVSlider() zoomframe.zoomslider = vgui.Create("DNumSlider", zoomframe) zoomframe.zoomslider:DockPadding(4,0,0,0) zoomframe.zoomslider:SetSize(200, 20) - zoomframe.zoomslider:SetMin( 0 ) - zoomframe.zoomslider:SetMax( 100 ) zoomframe.zoomslider:SetDecimals( 0 ) zoomframe.zoomslider:SetText("Camera FOV") if pace.camera_orthographic then zoomframe.zoomslider:SetText("Ortho. Width") - zoomframe.zoomslider:SetMin( -10000 ) - zoomframe.zoomslider:SetMax( 10000 ) end + pace.RefreshZoomBounds(zoomframe.zoomslider) zoomframe.zoomslider:SetDark(true) zoomframe.zoomslider:SetDefaultValue( 75 ) From 5f50198b9ac90534b4b6d399158f74d38173bd41 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 2 Feb 2025 01:40:27 -0500 Subject: [PATCH 272/300] custom shortcuts update add reset zoom and reset view position ignore some weird persistent toggled keys add bracketed numbers indicating number of keybinds for each action in the combobox, and a richtext zone to provide information about active keybinds, it can filter --- lua/pac3/editor/client/settings.lua | 300 +++++++++++++++++++++++++-- lua/pac3/editor/client/shortcuts.lua | 21 +- 2 files changed, 298 insertions(+), 23 deletions(-) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 613919b7a..955434529 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -689,7 +689,7 @@ function pace.OpenSettings(tab) local pnl = vgui.Create("DFrame") pnl:SetTitle("pac settings") pace.settings_panel = pnl - pnl:SetSize(800,600) + pnl:SetSize(900,600) pnl:MakePopup() pnl:Center() pnl:SetSizable(true) @@ -1084,25 +1084,264 @@ function pace.FillEditorSettings(pnl) local shortcutaction_choices = vgui.Create("DComboBox", LeftPanel) shortcutaction_choices:SetText("Select a PAC action") - for _,name in ipairs(pace.PACActionShortcut_Dictionary) do - shortcutaction_choices:AddChoice(name) + shortcutaction_choices:SetSortItems(false) + local function rebuild_shortcut_box() + local display, active_action = shortcutaction_choices:GetSelected() + local active_action_count = 0 + if pace.PACActionShortcut[active_action] and (table.Count(pace.PACActionShortcut[active_action]) > 0) then + active_action_count = table.Count(pace.PACActionShortcut[active_action]) + end + for i=#pace.PACActionShortcut_Dictionary,1,-1 do + shortcutaction_choices:RemoveChoice(i) + end + for _,name in ipairs(pace.PACActionShortcut_Dictionary) do + local display_name = name + local binds_str = "" + if pace.PACActionShortcut[name] and (table.Count(pace.PACActionShortcut[name]) > 0) then + display_name = "[" .. table.Count(pace.PACActionShortcut[name]) .. "] " .. name + end + shortcutaction_choices:AddChoice(display_name, name) + end + if active_action then + if active_action_count > 0 then + timer.Simple(0, function() shortcutaction_choices:SetText("[" .. active_action_count .. "] " .. active_action) end) + end + end end + + local shortcut_dumps = {} + local shortcut_dumps_rawstring = "" + local function refresh_shortcut_dumps() + shortcut_dumps = {} + shortcut_dumps_rawstring = "" + for _,name in ipairs(pace.PACActionShortcut_Dictionary) do + local already_included_basename = false + if pace.PACActionShortcut[name] then + local binds_str = "" + for i=1,10,1 do + if pace.PACActionShortcut[name][i] then + if not already_included_basename then + shortcut_dumps_rawstring = shortcut_dumps_rawstring .. "\n" .. name .. " : " + already_included_basename = true + end + local raw_combo = {} + local combo_string = "["..i.."] = " + for j=1,10,1 do + if not pace.PACActionShortcut[name][i][j] then continue end + combo_string = combo_string .. pace.PACActionShortcut[name][i][j] .. " + " + table.insert(raw_combo, pace.PACActionShortcut[name][i][j]) + end + if not table.IsEmpty(raw_combo) then + shortcut_dumps_rawstring = shortcut_dumps_rawstring .. + " {" .. table.concat(raw_combo, "+") .. "}," + end + if combo_string ~= "" then + combo_string = string.TrimRight(combo_string, " + ") + binds_str = binds_str .. "\n" .. combo_string + end + end + end + shortcut_dumps[name] = string.Trim(binds_str,"\n") + end + shortcut_dumps_rawstring = string.Trim(shortcut_dumps_rawstring,"\n") + end + + local name, value = shortcutaction_choices:GetSelected() + shortcutaction_choices:SetTooltip(shortcut_dumps[value]) + rebuild_shortcut_box() + end + + local function get_common_keybind_groupings(filter) + if filter then + if table.IsEmpty(filter) then return get_common_keybind_groupings() end + end + + local ctrl = {} + local shift = {} + local alt = {} + local ctrl_shift = {} + local ctrl_alt = {} + local shift_alt = {} + local singles = {} + local pass_filter = {} + + for action,tbl in pairs(pace.PACActionShortcut) do + for i=1,10,1 do + if pace.PACActionShortcut[action][i] then + + local raw_combo = {} + local contains_ctrl = false + local contains_shift = false + local contains_alt = false + local fail_filter = true + local filter_match_number = 0 + + for j=1,10,1 do + key = pace.PACActionShortcut[action][i][j] + if not key then continue end + table.insert(raw_combo, pace.PACActionShortcut[action][i][j]) + if filter then + for _,k in ipairs(filter) do + if input.GetKeyCode(key) == k or key == k then + filter_match_number = filter_match_number + 1 + end + end + else + if input.GetKeyCode(key) == KEY_LCONTROL or input.GetKeyCode(key) == KEY_RCONTROL then + contains_ctrl = true + elseif input.GetKeyCode(key) == KEY_LSHIFT or input.GetKeyCode(key) == KEY_RSHIFT then + contains_shift = true + elseif input.GetKeyCode(key) == KEY_LALT or input.GetKeyCode(key) == KEY_RALT then + contains_alt = true + end + end + end + + if filter then + if filter_match_number == #filter then + table.insert(pass_filter, {raw_combo, action}) + end + end + + if not table.IsEmpty(raw_combo) then + if contains_ctrl then + table.insert(ctrl, {raw_combo, action}) + if contains_shift then + table.insert(shift, {raw_combo, action}) + table.insert(ctrl_shift, {raw_combo, action}) + if contains_alt then + table.insert(shift_alt, {raw_combo, action}) + end + end + if contains_alt then + table.insert(alt, {raw_combo, action}) + table.insert(ctrl_alt, {raw_combo, action}) + end + elseif contains_shift then + table.insert(shift, {raw_combo, action}) + if contains_alt then + table.insert(alt, {raw_combo, action}) + table.insert(shift_alt, {raw_combo, action}) + end + elseif contains_alt then + table.insert(alt, {raw_combo, action}) + else + table.insert(singles, {raw_combo, action}) + end + end + end + end + end + + return { + ctrl = ctrl, + shift = shift, + alt = alt, + ctrl_shift = ctrl_shift, + ctrl_alt = ctrl_alt, + shift_alt = shift_alt, + singles = singles, + pass_filter = pass_filter + } + end + + local output_panel_scroll = vgui.Create("DScrollPanel", LeftPanel) + output_panel_scroll:SetPos(430, 10) output_panel_scroll:SetSize(500, 500) + local output_panel + local function create_richtext() + if IsValid(output_panel) then output_panel:Remove() end + output_panel = vgui.Create("RichText", output_panel_scroll) + output_panel_scroll:AddItem(output_panel) + output_panel_scroll:SetVerticalScrollbarEnabled(true) + output_panel:SetSize(900 - 430 - 200,500) + output_panel:InsertColorChange(0,0,0, 255) + end + create_richtext() + output_panel:AppendText("keybind viewer\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<- Use this dropdown to filter") + + local test_cbox = vgui.Create("DComboBox", LeftPanel) + test_cbox:SetPos(300, 440) test_cbox:SetSize(120, 20) + test_cbox:SetSortItems(false) + test_cbox:SetText("filter keybinds") + local binder1_value + local binder2_value + local binder3_value + function test_cbox:OnSelect(i,val,data) + create_richtext() + local keybind_groupings = get_common_keybind_groupings() + + if keybind_groupings then + local str = "" + if val == "all actions" then + refresh_shortcut_dumps() + str = shortcut_dumps_rawstring + elseif val == "keybind categories" then + for groupname,group in pairs(keybind_groupings) do + str = str .. "Category: " .. groupname .. "\n" + for j,v in ipairs(group) do + str = str .. table.concat(v[1], "+") .. " : " .. v[2] .. "\n" + end + str = str .. "\n\n" + end + elseif val == "<<< filter" then + if not binder1_value then + output_panel:InsertColorChange(255,0,0, 255) + output_panel:AppendText("No binds to search! Use the three binder buttons\n") + output_panel:InsertColorChange(0,0,0, 255) + end + keybind_groupings = get_common_keybind_groupings({binder1_value, binder2_value, binder3_value}) + if table.IsEmpty(keybind_groupings["pass_filter"]) then + output_panel:InsertColorChange(100,100,100, 255) + output_panel:AppendText("\n") + output_panel:InsertColorChange(0,0,0, 255) + end + for i,v in ipairs(keybind_groupings["pass_filter"]) do + str = str .. table.concat(v[1], "+") .. " : " .. v[2] + str = str .. "\n" + end + else + for i,v in ipairs(keybind_groupings[val]) do + str = str .. table.concat(v[1], "+") .. " : " .. v[2] + str = str .. "\n" + end + end + output_panel:AppendText(str) + end + + end + test_cbox:AddChoice("<<< filter") test_cbox:AddChoice("all actions") test_cbox:AddChoice("keybind categories") test_cbox:AddChoice("singles") + test_cbox:AddChoice("ctrl") test_cbox:AddChoice("shift") test_cbox:AddChoice("alt") + test_cbox:AddChoice("ctrl_alt") test_cbox:AddChoice("ctrl_shift") test_cbox:AddChoice("shift_alt") + + rebuild_shortcut_box() shortcutaction_choices:SetX(10) shortcutaction_choices:SetY(400) shortcutaction_choices:SetWidth(170) shortcutaction_choices:SetHeight(20) shortcutaction_choices:ChooseOptionID(1) - + refresh_shortcut_dumps() + shortcutaction_choices:SetTooltip(shortcut_dumps_rawstring) + function shortcutaction_choices:OnMenuOpened(menu) + refresh_shortcut_dumps() + self:SetTooltip() + end function shortcutaction_choices:Think() self.next = self.next or 0 self.found = self.found or false if self.next < RealTime() then self.found = false end if self:IsHovered() then + if self:IsMenuOpen() then + self:SetTooltip() + end + if input.IsKeyDown(KEY_UP) then + refresh_shortcut_dumps() if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() + 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end elseif input.IsKeyDown(KEY_DOWN) then + refresh_shortcut_dumps() if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() - 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end else self.found = false end - else self.found = false + else + self.found = false end end @@ -1162,7 +1401,7 @@ function pace.FillEditorSettings(pnl) local function update_shortcutaction_choices_textCurrentShortcut(num) shortcutaction_choices_textCurrentShortcut:SetText("") num = tonumber(num) - local action, val = shortcutaction_choices:GetSelected() + local diplayname, action = shortcutaction_choices:GetSelected() local strs = {} if action and action ~= "" then @@ -1182,8 +1421,12 @@ function pace.FillEditorSettings(pnl) update_shortcutaction_choices_textCurrentShortcut(num) end - function shortcutaction_choices:OnSelect(i, action) + function shortcutaction_choices:OnSelect(i, displayname, action) shortcutaction_index:OnValueChanged(shortcutaction_index:GetValue()) + refresh_shortcut_dumps() + create_richtext() + output_panel:AppendText(action .. "\n") + output_panel:AppendText(shortcut_dumps[action] or "") end local binder1 = vgui.Create("DBinder", LeftPanel) @@ -1192,8 +1435,9 @@ function pace.FillEditorSettings(pnl) binder1:SetHeight(30) binder1:SetWidth(90) function binder1:OnChange( num ) - if not num or num == 0 then return end + if not num or num == 0 then binder1_value = nil return end if not input.GetKeyName( num ) then return end + binder1_value = num LocalPlayer():ChatPrint("New bound key 1: "..input.GetKeyName( num )) pace.FlashNotification("New bound key 1: "..input.GetKeyName( num )) end @@ -1204,8 +1448,9 @@ function pace.FillEditorSettings(pnl) binder2:SetHeight(30) binder2:SetWidth(90) function binder2:OnChange( num ) - if not num or num == 0 then return end + if not num or num == 0 then binder2_value = nil return end if not input.GetKeyName( num ) then return end + binder2_value = num LocalPlayer():ChatPrint("New bound key 2: "..input.GetKeyName( num )) pace.FlashNotification("New bound key 2: "..input.GetKeyName( num )) end @@ -1216,16 +1461,16 @@ function pace.FillEditorSettings(pnl) binder3:SetHeight(30) binder3:SetWidth(90) function binder3:OnChange( num ) - if not num or num == 0 then return end + if not num or num == 0 then binder3_value = nil return end if not input.GetKeyName( num ) then return end + binder2_value = num LocalPlayer():ChatPrint("New bound key 3: "..input.GetKeyName( num )) pace.FlashNotification("New bound key 3: "..input.GetKeyName( num )) end local function send_active_shortcut_to_assign(tbl) - local action = shortcutaction_choices:GetValue() + local display, action = shortcutaction_choices:GetSelected() local index = shortcutaction_index:GetValue() - if not tbl then pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {} pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {} @@ -1239,7 +1484,7 @@ function pace.FillEditorSettings(pnl) pace.PACActionShortcut[action][index] = nil end elseif not table.IsEmpty(tbl) then - pace.AssignEditorShortcut(shortcutaction_choices:GetValue(), tbl, shortcutaction_index:GetValue()) + pace.AssignEditorShortcut(action, tbl, shortcutaction_index:GetValue()) end encode_table_to_file("pac_editor_shortcuts") end @@ -1259,6 +1504,7 @@ function pace.FillEditorSettings(pnl) binder3:SetSelectedNumber(0) send_active_shortcut_to_assign() update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue()) + refresh_shortcut_dumps() end local bindoverwrite = vgui.Create("DButton", LeftPanel) @@ -1271,6 +1517,7 @@ function pace.FillEditorSettings(pnl) bindoverwrite:SetColor(Color(0,200,0)) bindoverwrite:SetIcon("icon16/disk.png") function bindoverwrite:DoClick() + local _, action = shortcutaction_choices:GetSelected() local tbl = {} local i = 1 --print(binder1:GetValue(), binder2:GetValue(), binder3:GetValue()) @@ -1278,9 +1525,9 @@ function pace.FillEditorSettings(pnl) if binder2:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder2:GetValue()) i = i + 1 end if binder3:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder3:GetValue()) end if not table.IsEmpty(tbl) then - pace.FlashNotification("Combo " .. shortcutaction_index:GetValue() .. " committed: " .. table.concat(tbl," ")) - if not pace.PACActionShortcut[shortcutaction_choices:GetValue()] then - pace.PACActionShortcut[shortcutaction_choices:GetValue()] = {} + pace.FlashNotification(action .. " " .. "Combo " .. shortcutaction_index:GetValue() .. " committed: " .. table.concat(tbl," ")) + if not pace.PACActionShortcut[action] then + pace.PACActionShortcut[action] = {} end send_active_shortcut_to_assign(tbl) update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue()) @@ -1315,6 +1562,7 @@ function pace.FillEditorSettings(pnl) bindcapture:SetWidth(90) pace.bindcapturelabel_text = "" function bindcapture:DoClick() + local previous_inputs_tbl = {} pace.delayshortcuts = RealTime() + 5 local input_active = {} local no_input = true @@ -1327,6 +1575,7 @@ function pace.FillEditorSettings(pnl) local inputs_tbl = {} inputs_str = "" for i=1,172,1 do --build bool list of all current keys + if pace.shortcuts_ignored_keys[i] then continue end if input.IsKeyDown(i) then input_active[i] = true inputs_tbl[i] = true @@ -1341,7 +1590,6 @@ function pace.FillEditorSettings(pnl) if previous_inputs_tbl and table.Count(previous_inputs_tbl) > 0 then if table.Count(inputs_tbl) < table.Count(previous_inputs_tbl) then pace.FlashNotification("ending input!" .. previous_inputs_str) - local tbl = {} local i = 1 for key,bool in pairs(previous_inputs_tbl) do @@ -1349,11 +1597,25 @@ function pace.FillEditorSettings(pnl) i = i + 1 end --print(shortcutaction_choices:GetValue(), shortcutaction_index:GetValue()) - pace.AssignEditorShortcut(shortcutaction_choices:GetValue(), tbl, shortcutaction_index:GetValue()) + local _, action = shortcutaction_choices:GetSelected() + pace.AssignEditorShortcut(action, tbl, shortcutaction_index:GetValue()) --pace.PACActionShortcut[shortcutaction_choices:GetValue()][shortcutaction_index:GetValue()] = tbl pace.delayshortcuts = RealTime() + 5 pace.bindcapturelabel_text = "Recorded input:\n" .. previous_inputs_str + previous_inputs_tbl = {} + inputs_tbl = {} pac.RemoveHook("Tick", "pace_buttoncapture_countdown") + if #tbl < 4 then + if tbl[1] then + binder1:SetValue(input.GetKeyCode(tbl[1])) + end + if tbl[2] then + binder2:SetValue(input.GetKeyCode(tbl[2])) + end + if tbl[3] then + binder3:SetValue(input.GetKeyCode(tbl[3])) + end + end end end previous_inputs_str = inputs_str @@ -1498,7 +1760,7 @@ function pace.FillEditorSettings(pnl) div:SetDividerWidth( 8 ) div:SetLeftMin( 50 ) div:SetRightMin( 50 ) - div:SetLeftWidth( 450 ) + div:SetLeftWidth( 700 ) partmenu_order_presets.OnSelect = function( self, index, value ) local temp_list = {"wear","save","load"} if value == "factory preset" then diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua index 845c792e4..8cf1892fc 100644 --- a/lua/pac3/editor/client/shortcuts.lua +++ b/lua/pac3/editor/client/shortcuts.lua @@ -44,6 +44,8 @@ pace.PACActionShortcut_Dictionary = { "toolbar_view", "toolbar_options", "zoom_panel", + "reset_zoom", + "reset_view_position", "view_orthographic", "view_follow_entity", "view_follow_entity_ang_frontback", @@ -338,10 +340,13 @@ local last = 0 pace.passthrough_keys = { [KEY_LWIN] = true, [KEY_RWIN] = true, - [KEY_CAPSLOCK] = true, - [KEY_CAPSLOCKTOGGLE] = true + [KEY_CAPSLOCK] = true +} +pace.shortcuts_ignored_keys = { + [KEY_CAPSLOCKTOGGLE] = true, + [KEY_NUMLOCKTOGGLE] = true, + [KEY_SCROLLLOCKTOGGLE] = true } - function pace.LookupShortcutsForAction(action, provided_inputs, do_it) pace.BulkSelectKey = input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString()) @@ -366,6 +371,7 @@ function pace.LookupShortcutsForAction(action, provided_inputs, do_it) local counterexample = false for key,bool in ipairs(inputs) do --check the input for counter-examples if input.IsKeyDown(key) then + if pace.shortcuts_ignored_keys[key] then continue end if not table.HasValue(combo, input.GetKeyName(key)) then --any keypress that is not in the combo invalidates the combo --some keys don't count as counterexamples?? --random windows or capslocktoggle keys being pressed screw up the input @@ -626,6 +632,12 @@ function pace.DoShortcutFunc(action) if action == "zoom_panel" then pace.PopupMiniFOVSlider() end + if action == "reset_zoom" then + pace.ResetZoom() + end + if action == "reset_view_position" then + pace.ResetView() + end if action == "view_orthographic" then pace.OrthographicView() end @@ -674,7 +686,7 @@ function pace.DoShortcutFunc(action) end end - if action == "T_Pose" then pace.SetTPose(not pace.GetTPose()) end + if action == "T_Pose" or action == "t_pose" then pace.SetTPose(not pace.GetTPose()) end if action == "bulk_select" then pace.DoBulkSelect(pace.current_part) @@ -906,6 +918,7 @@ function pace.CheckShortcuts() pace.shortcut_inputs_count = 0 for i=1,172,1 do --build bool list of all current keys if input.IsKeyDown(i) then + if pace.shortcuts_ignored_keys[i] then continue end if pace.passthrough_keys[i] or i == pace.BulkSelectKey then no_input_override = true end input_active[i] = true pace.shortcut_inputs_count = pace.shortcut_inputs_count + 1 From 026178ba7e75c9450a5eb71c74521962279a90a3 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 2 Feb 2025 02:30:33 -0500 Subject: [PATCH 273/300] view roll fix when doing lock on, using a "frame of reference" mode, it overrides pace.ViewAngles roll, but exiting these modes didn't reset it, so reset it --- lua/pac3/editor/client/parts.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index ccd117717..94d22515e 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -3237,7 +3237,7 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) end) end end - + return jig end @@ -4276,6 +4276,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) pace.viewlock = root_owner end if mode == "disable" then + pace.ViewAngles.r = 0 pace.viewlock = nil return end @@ -4305,6 +4306,7 @@ function pace.addPartMenuComponent(menu, obj, option_name) pace.viewlock = obj end if mode == "disable" then + pace.ViewAngles.r = 0 pace.viewlock = nil return end From 26ec84c0c1a8ba5423394d417965cab066a2ed60 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 2 Feb 2025 03:50:22 -0500 Subject: [PATCH 274/300] force part preview forces (draw lines) draws vector lines and their XYZ components reflecting the linear forces applied to targets, it doesn't render for non-owner players because of the calculations +stop sending force actions if no targets are selected --- lua/pac3/core/client/parts/force.lua | 323 ++++++++++++++++++++++++++- 1 file changed, 319 insertions(+), 4 deletions(-) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 7c0c20c26..ccd038c91 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -19,7 +19,8 @@ BUILDER:StartStorableVars() }}) :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) :GetSet("Radius", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) - :GetSet("Preview",false) + :GetSet("Preview",false, {description = "preview target selection boxes"}) + :GetSet("PreviewForces",false, {description = "preview the predicted forces"}) :SetPropertyGroup("BaseForces") :GetSet("BaseForce", 0) @@ -57,7 +58,7 @@ Radial gets the base directions from the targets to the force part]]}) :GetSet("AccountMass", false, {description = "Apply acceleration according to mass."}) :GetSet("Falloff", false, {description = "Whether the force to apply should fade with distance"}) :GetSet("ReverseFalloff", false, {description = "The reverse of the falloff means the force fades when getting closer."}) - :GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part"}) + :GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part.\nRequires vertical forces. Easiest way is to enter 0 0 500 in 'added vector force' with the Global vector mode which is already there by default."}) :GetSet("LevitationHeight", 0) :SetPropertyGroup("Damping") @@ -102,12 +103,325 @@ function PART:OnRemove() end +local white = Color(255,255,255) +local red = Color(255,0,0) +local green = Color(0,255,0) +local blue = Color(0,0,255) +local red2 = Color(255,100,100) +local red3 = Color(255,200,200) +local function draw_force_line(pos, amount) + local length = amount:Length() + local magnitude = length / 20 + amount:Normalize() + local x = amount.x + local y = amount.y + local z = amount.z + local dir = amount:Angle() + render.DrawLine( pos, 9 * magnitude * x * Vector(1,0,0) + pos, red, false) + render.DrawLine( pos, 9 * magnitude * y * Vector(0,1,0) + pos, green, false) + render.DrawLine( pos, 9 * magnitude * z * Vector(0,0,1) + pos, blue, false) + cam.IgnoreZ( true ) + for i=0,8,1 do + local scrolling = -i + math.floor((CurTime() % 1) * 8) + 2 + if scrolling == 0 then + render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red, false) + elseif scrolling == 1 then + render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red2, false) + elseif scrolling == 2 then + render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red3, false) + else + render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, white, false) + end + + end + cam.IgnoreZ( false ) +end + + +--convenience functions and tables from net_combat + +local pre_excluded_ent_classes = { + ["info_player_start"] = true, + ["aoc_spawnpoint"] = true, + ["info_player_teamspawn"] = true, + ["env_tonemap_controller"] = true, + ["env_fog_controller"] = true, + ["env_skypaint"] = true, + ["shadow_control"] = true, + ["env_sun"] = true, + ["predicted_viewmodel"] = true, + ["physgun_beam"] = true, + ["ambient_generic"] = true, + ["trigger_once"] = true, + ["trigger_multiple"] = true, + ["trigger_hurt"] = true, + ["info_ladder_dismount"] = true, + ["info_particle_system"] = true, + ["env_sprite"] = true, + ["env_fire"] = true, + ["env_soundscape"] = true, + ["env_smokestack"] = true, + ["light"] = true, + ["move_rope"] = true, + ["keyframe_rope"] = true, + ["env_soundscape_proxy"] = true, + ["gmod_hands"] = true, + ["env_lightglow"] = true, + ["point_spotlight"] = true, + ["spotlight_end"] = true, + ["beam"] = true, + ["info_target"] = true, + ["func_lod"] = true, + ["func_brush"] = true, + ["phys_bone_follower"] = true, +} + +local physics_point_ent_classes = { + ["prop_physics"] = true, + ["prop_physics_multiplayer"] = true, + ["prop_ragdoll"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true +} + +local function MergeTargetsByID(tbl1, tbl2) + for i,v in ipairs(tbl2) do + tbl1[v:EntIndex()] = v + end +end + +local function Is_NPC(ent) + return ent:IsNPC() or ent:IsNextBot() or ent.IsDrGEntity or ent.IsVJBaseSNPC +end + +local function ProcessForcesList(ents_hits, tbl, pos, ang, ply) + for i,v in pairs(ents_hits) do + if pre_excluded_ent_classes[v:GetClass()] then ents_hits[i] = nil end + end + local ftime = 0.016 --approximate tick duration + local BASEFORCE = 0 + local VECFORCE = Vector(0,0,0) + if tbl.Continuous then + BASEFORCE = tbl.BaseForce * ftime * 3.3333 --weird value to equalize how 600 cancels out gravity + VECFORCE = tbl.AddedVectorForce * ftime * 3.3333 + else + BASEFORCE = tbl.BaseForce + VECFORCE = tbl.AddedVectorForce + end + for _,ent in pairs(ents_hits) do + if ent:IsWeapon() or ent:GetClass() == "viewmodel" or ent:GetClass() == "func_physbox_multiplayer" then continue end + if ent:GetPos():Distance(ply:GetPos()) < 300 then + print(ent) + end + local phys_ent + local is_player = ent:IsPlayer() + local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner()))) + local is_npc = Is_NPC(ent) + + if is_npc and not tbl.NPC then continue end + if is_player and not (tbl.Players or ent == tbl:GetPlayerOwner()) then continue end + if is_player and not tbl.AffectSelf and ent == tbl:GetPlayerOwner() then continue end + if is_physics and not tbl.PhysicsProps then continue end + if not is_npc and not is_player and not is_physics then + if not tbl.PointEntities then continue end + end + + local is_phys = true + phys_ent = ent + is_phys = false + + local oldvel + + if IsValid(phys_ent) then + oldvel = phys_ent:GetVelocity() + else + oldvel = Vector(0,0,0) + end + + + local addvel = Vector(0,0,0) + local add_angvel = Vector(0,0,0) + + local ent_center = ent:WorldSpaceCenter() or ent:GetPos() + + local dir = ent_center - pos --part + local locus_pos = pos + if tbl.Locus ~= nil then + if tbl.Locus:IsValid() then + locus_pos = tbl.Locus:GetWorldPosition() + end + end + local dir2 = ent_center - locus_pos + + local dist_multiplier = 1 + local damping_dist_mult = 1 + local up_mult = 1 + local distance = (ent_center - pos):Length() + local height_delta = pos.z + tbl.LevitationHeight - ent_center.z + + --what it do + --if delta is -100 (ent is lower than the desired height), that means +100 adjustment direction + --height decides how much to knee the force until it equalizes at 0 + --clamp the delta to the ratio levitation height + + if tbl.Levitation then + up_mult = math.Clamp(height_delta / (5 + math.abs(tbl.LevitationHeight)),-1,1) + end + + if tbl.BaseForceAngleMode == "Radial" then --radial on self + addvel = dir:GetNormalized() * tbl.BaseForce + elseif tbl.BaseForceAngleMode == "Locus" then --radial on locus + addvel = dir2:GetNormalized() * tbl.BaseForce + elseif tbl.BaseForceAngleMode == "Local" then --forward on self + addvel = ang:Forward() * tbl.BaseForce + end + + if tbl.VectorForceAngleMode == "Global" then --global + addvel = addvel + tbl.AddedVectorForce + elseif tbl.VectorForceAngleMode == "Local" then --local on self + addvel = addvel + +ang:Forward()*tbl.AddedVectorForce.x + +ang:Right()*tbl.AddedVectorForce.y + +ang:Up()*tbl.AddedVectorForce.z + + elseif tbl.VectorForceAngleMode == "Radial" then --relative to locus or self + ang2 = dir:Angle() + addvel = addvel + +ang2:Forward()*tbl.AddedVectorForce.x + +ang2:Right()*tbl.AddedVectorForce.y + +ang2:Up()*tbl.AddedVectorForce.z + elseif tbl.VectorForceAngleMode == "RadialNoPitch" then --relative to locus or self + dir.z = 0 + ang2 = dir:Angle() + addvel = addvel + +ang2:Forward()*tbl.AddedVectorForce.x + +ang2:Right()*tbl.AddedVectorForce.y + +ang2:Up()*tbl.AddedVectorForce.z + end + + --[[if tbl.TorqueMode == "Global" then + add_angvel = tbl.Torque + elseif tbl.TorqueMode == "Local" then + add_angvel = ang:Forward()*tbl.Torque.x + ang:Right()*tbl.Torque.y + ang:Up()*tbl.Torque.z + elseif tbl.TorqueMode == "TargetLocal" then + add_angvel = tbl.Torque + elseif tbl.TorqueMode == "Radial" then + ang2 = dir:Angle() + addvel = ang2:Forward()*tbl.Torque.x + ang2:Right()*tbl.Torque.y + ang2:Up()*tbl.Torque.z + end]] + + local mass = 1 + if IsValid(phys_ent) then + if phys_ent.GetMass then + phys_ent:GetMass() + end + end + if is_phys and tbl.AccountMass then + if not is_npc then + addvel = addvel * (1 / math.max(mass,0.1)) + else + addvel = addvel + end + add_angvel = add_angvel * (1 / math.max(mass,0.1)) + end + + if tbl.Falloff then + dist_multiplier = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + if tbl.ReverseFalloff then + dist_multiplier = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + + if tbl.DampingFalloff then + damping_dist_mult = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + if tbl.DampingReverseFalloff then + damping_dist_mult = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + damping_dist_mult = damping_dist_mult + local final_damping = 1 - (tbl.Damping * damping_dist_mult) + + if tbl.Levitation then + addvel.z = addvel.z * up_mult + end + + addvel = addvel * dist_multiplier + draw_force_line(ent:WorldSpaceCenter(), addvel) + + end +end + +local function preview_process_ents(tbl) + ply = tbl:GetPlayerOwner() + local pos = tbl.pos + local ang = tbl.ang + + if tbl.HitboxMode == "Sphere" then + local ents_hits = ents.FindInSphere(pos, tbl.Radius) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Box" then + local mins + local maxs + if tbl.HitboxMode == "Box" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) + end + + local ents_hits = ents.FindInBox(mins, maxs) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cylinder" then + local ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cone" then + local ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + end +end + function PART:OnDraw() self.pos,self.ang = self:GetDrawPosition() - if not self.Preview then pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end + if not self.Preview and not self.PreviewForces then pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end - if self.Preview then + if self.Preview or self.PreviewForces then pac.AddHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID, function() + if self.PreviewForces then + --recalculating forces every drawframe is cringe for other players + if self:GetPlayerOwner() == pac.LocalPlayer then + if self.NPC or self.Players or self.AffectSelf or self.PhysicsProps or self.PointEntities then + preview_process_ents(self) + end + end + end + if not self.Preview then return end + if self.HitboxMode == "Box" then local mins = Vector(-self.Radius, -self.Radius, -self.Length) local maxs = Vector(self.Radius, self.Radius, self.Length) @@ -204,6 +518,7 @@ function PART:Impulse(on) end end + if not self.NPC and not self.Players and not self.AffectSelf and not self.PhysicsProps and not self.PointEntities then return end net.Start("pac_request_force", true) net.WriteVector(self:GetWorldPosition()) net.WriteAngle(self:GetWorldAngles()) From 66b5fdbce457658d7e490c6c9d8faee8f3d47bc5 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 3 Feb 2025 19:26:42 -0500 Subject: [PATCH 275/300] model/entity "edit materials" quick setup instead of load vmt, get some quick setups to auto add them and set them up in one click. the submenu root is clickable and deploys all submaterials but the sub-options are right clickable --- lua/pac3/editor/client/parts.lua | 117 ++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 94d22515e..91dff6f96 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -1363,7 +1363,7 @@ do -- menu function pace.ClearBulkList() for _,v in ipairs(pace.BulkSelectList) do - if v.pace_tree_node ~= nil then v.pace_tree_node:SetAlpha( 255 ) end + if IsValid(v.pace_tree_node) then v.pace_tree_node:SetAlpha( 255 ) end v:SetInfo() end pace.BulkSelectList = {} @@ -2652,6 +2652,78 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) end):SetIcon("icon16/table_multiple.png") end + local function install_submaterial_options(menu) + local mats = obj:GetOwner():GetMaterials() + local mats_str = table.concat(mats,"\n") + local dyn_props = obj:GetDynamicProperties() + local submat_togglers, pnl = main:AddSubMenu("create submaterial zone togglers (hide/show materials)", function() + Derma_StringRequest("submaterial togglers", "please input a submaterial name or a list of submaterial names with spaces\navailable materials:\n"..mats_str, "", function(str) + local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) event:SetEvent("command") event:SetArguments("materials_"..string.sub(obj.UniqueID,1,6)) + local proxy = pac.CreatePart("proxy") proxy:SetAffectChildren(true) proxy:SetVariableName("no_draw") proxy:SetExpression("0") proxy:SetExpressionOnHide("1") + event:SetParent(obj) proxy:SetParent(event) + for i, kw in ipairs(string.Split(str, " ")) do + for id,mat2 in ipairs(mats) do + if string.GetFileFromFilename(mat2) == kw then + local mat = pac.CreatePart("material_3d") mat:SetParent(proxy) + mat:SetName("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) + mat:SetLoadVmt(mat2) + dyn_props[kw].set("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) + end + end + end + end) + end) pnl:SetImage("icon16/picture_delete.png") pnl:SetTooltip("The sub-options are right clickable") + + local submat_toggler_proxy + local submat_toggler_event + local submaterials = {} + for i,mat2 in ipairs(mats) do + table.insert(submaterials,"") + local kw = string.GetFileFromFilename(mat2) + AddOptionRightClickable(kw, function() + if not submat_toggler_proxy then + local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) event:SetEvent("command") event:SetArguments("materials_"..string.sub(obj.UniqueID,1,6)) + local proxy = pac.CreatePart("proxy") proxy:SetAffectChildren(true) proxy:SetVariableName("no_draw") proxy:SetExpression("0") proxy:SetExpressionOnHide("1") + event:SetParent(obj) proxy:SetParent(event) + submat_toggler_proxy = proxy + end + local mat = pac.CreatePart("material_3d") mat:SetParent(submat_toggler_proxy) + mat:SetName("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) + mat:SetLoadVmt(mat2) + + submaterials[i] = "toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6) + obj:SetMaterials(table.concat(submaterials, ";")) + end, submat_togglers):SetIcon("icon16/paintcan.png") + end + + local edit_materials, pnl = main:AddSubMenu("edit all materials", function() + local materials = "" + obj:SetMaterial("") + for i,mat2 in ipairs(mats) do + local kw = string.GetFileFromFilename(mat2) + local mat = pac.CreatePart("material_3d") mat:SetParent(obj) + mat:SetName(kw.."_"..string.sub(obj.UniqueID,1,6)) + mat:SetLoadVmt(mat2) + submaterials[i] = kw.."_"..string.sub(obj.UniqueID,1,6) + + end + obj:SetMaterials(table.concat(submaterials, ";")) + end) pnl:SetImage("icon16/paintcan.png") + + for i,mat2 in ipairs(mats) do + local kw = string.GetFileFromFilename(mat2) + AddOptionRightClickable(kw, function() + obj:SetMaterial("") + + local mat = pac.CreatePart("material_3d") mat:SetParent(obj) + mat:SetName(kw.."_"..string.sub(obj.UniqueID,1,6)) + mat:SetLoadVmt(mat2) + + submaterials[i] = kw.."_"..string.sub(obj.UniqueID,1,6) + obj:SetMaterials(table.concat(submaterials, ";")) + end, edit_materials):SetIcon("icon16/paintcan.png") + end + end if obj.ClassName == "particles" then main:AddOption("bare 3D setup", function() obj:Set3D(true) obj:SetZeroAngle(false) obj:SetVelocity(0) obj:SetParticleAngleVelocity(Vector(0,0,0)) obj:SetGravity(Vector(0,0,0)) @@ -2928,26 +3000,8 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) end end end - main:AddOption("create submaterial zone togglers (hide/show materials)", function() - local mats = obj:GetOwner():GetMaterials() - local mats_str = table.concat(mats,"\n") - local dyn_props = obj:GetDynamicProperties() - Derma_StringRequest("submaterial togglers", "please input a submaterial name or a list of submaterial names with spaces\navailable materials:\n"..mats_str, "", function(str) - local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) event:SetEvent("command") event:SetArguments("materials_"..string.sub(obj.UniqueID,1,6)) - local proxy = pac.CreatePart("proxy") proxy:SetAffectChildren(true) proxy:SetVariableName("no_draw") proxy:SetExpression("0") proxy:SetExpressionOnHide("1") - event:SetParent(obj) proxy:SetParent(event) - for i, kw in ipairs(string.Split(str, " ")) do - for id,mat2 in ipairs(mats) do - if string.GetFileFromFilename(mat2) == kw then - local mat = pac.CreatePart("material_3d") mat:SetParent(proxy) - mat:SetName("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) - mat:SetLoadVmt(mat2) - dyn_props[kw].set("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) - end - end - end - end) - end):SetIcon("icon16/picture_delete.png") + install_submaterial_options(main) + elseif obj.ClassName == "model2" then local pm = pace.current_part:GetPlayerOwner():GetModel() local pm_selected = player_manager.TranslatePlayerModel(GetConVar("cl_playermodel"):GetString()) @@ -2995,26 +3049,7 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) end end - main:AddOption("create submaterial zone togglers (hide/show materials)", function() - local mats = obj:GetOwner():GetMaterials() - local mats_str = table.concat(mats,"\n") - local dyn_props = obj:GetDynamicProperties() - Derma_StringRequest("submaterial togglers", "please input a submaterial name or a list of submaterial names with spaces\navailable materials:\n"..mats_str, "", function(str) - local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) event:SetEvent("command") event:SetArguments("materials_"..string.sub(obj.UniqueID,1,6)) - local proxy = pac.CreatePart("proxy") proxy:SetAffectChildren(true) proxy:SetVariableName("no_draw") proxy:SetExpression("0") proxy:SetExpressionOnHide("1") - event:SetParent(obj) proxy:SetParent(event) - for i, kw in ipairs(string.Split(str, " ")) do - for id,mat2 in ipairs(mats) do - if string.GetFileFromFilename(mat2) == kw then - local mat = pac.CreatePart("material_3d") mat:SetParent(proxy) - mat:SetName("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) - mat:SetLoadVmt(mat2) - dyn_props[kw].set("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)) - end - end - end - end) - end):SetIcon("icon16/picture_delete.png") + install_submaterial_options(main) local collapses, pnl = main:AddSubMenu("bone collapsers") pnl:SetImage("icon16/compress.png") collapses:AddOption("collapse arms", function() From 32bb43a1045bb767be8b93974bade06c061182ca Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 3 Feb 2025 22:23:38 -0500 Subject: [PATCH 276/300] quicksetup materials fix + append multiple target parts from bulk select don't send the edited material to SetMaterials (internal dynamic properties) if only one material exists (the submaterial dynamic properties become invisible in that case) similar option to sending bulk select to proxy multi target, except it's appending, not overwriting existing ones, while preventing duplicate uids/names --- lua/pac3/editor/client/parts.lua | 37 +++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 91dff6f96..65fc31501 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -2692,7 +2692,12 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) mat:SetLoadVmt(mat2) submaterials[i] = "toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6) - obj:SetMaterials(table.concat(submaterials, ";")) + if #submaterials == 1 then + obj:SetMaterials("") obj:SetMaterial(submaterials[1]) + else + obj:SetMaterials(table.concat(submaterials, ";")) + end + end, submat_togglers):SetIcon("icon16/paintcan.png") end @@ -2707,7 +2712,11 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) submaterials[i] = kw.."_"..string.sub(obj.UniqueID,1,6) end - obj:SetMaterials(table.concat(submaterials, ";")) + if #submaterials == 1 then + obj:SetMaterials("") obj:SetMaterial(submaterials[1]) + else + obj:SetMaterials(table.concat(submaterials, ";")) + end end) pnl:SetImage("icon16/paintcan.png") for i,mat2 in ipairs(mats) do @@ -2720,7 +2729,11 @@ function pace.AddQuickSetupsToPartMenu(menu, obj) mat:SetLoadVmt(mat2) submaterials[i] = kw.."_"..string.sub(obj.UniqueID,1,6) - obj:SetMaterials(table.concat(submaterials, ";")) + if #submaterials == 1 then + obj:SetMaterials("") obj:SetMaterial(submaterials[1]) + else + obj:SetMaterials(table.concat(submaterials, ";")) + end end, edit_materials):SetIcon("icon16/paintcan.png") end end @@ -3909,6 +3922,24 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) end obj:SetMultipleTargetParts(table.concat(uid_tbl,";")) end):SetIcon("icon16/star.png") + if obj.MultipleTargetParts ~= "" then + menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Add to multiple target parts", function() + local anti_duplicate = {} + local uid_tbl = string.Split(obj.MultipleTargetParts,";") + + for i,uid in ipairs(uid_tbl) do + anti_duplicate[uid] = uid + end + for i,part in ipairs(pace.BulkSelectList) do + anti_duplicate[part.UniqueID] = part.UniqueID + end + uid_tbl = {} + for _,uid in pairs(anti_duplicate) do + table.insert(uid_tbl, uid) + end + obj:SetMultipleTargetParts(table.concat(uid_tbl,";")) + end):SetIcon("icon16/star.png") + end end elseif obj.ClassName == "beam" then if not IsValid(obj.TargetPart) and obj.MultipleEndPoints == "" then From ec58bf302ae145f5e43d55cd0959add121bf5682 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 5 Feb 2025 19:24:35 -0500 Subject: [PATCH 277/300] fix bulk select binder clear --- lua/pac3/editor/client/settings.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index 955434529..cb2d47261 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -1626,6 +1626,7 @@ function pace.FillEditorSettings(pnl) local bulkbinder = vgui.Create("DBinder", LeftPanel) function bulkbinder:OnChange( num ) + if num == 0 then GetConVar("pac_bulk_select_key"):SetString("") return end GetConVar("pac_bulk_select_key"):SetString(input.GetKeyName( num )) end bulkbinder:SetX(210) From 760ee7401719f2f633bee7b2deb33437d50747b7 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 6 Feb 2025 18:25:09 -0500 Subject: [PATCH 278/300] bulk select deselect option to deselect bulk selected parts if selecting a part without holding bulk select key I thought about whether it should be default, it's probably less annoying to handle accidental bulk selects and to deselect faster --- lua/pac3/editor/client/parts.lua | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 65fc31501..8f98a1603 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -25,6 +25,7 @@ CreateConVar( "pac_hover_halo_limit", 100, FCVAR_ARCHIVE, "max number of parts b CreateConVar("pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select") CreateConVar("pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.") local bulk_select_subsume = CreateConVar("pac_bulk_select_subsume", "1", FCVAR_ARCHIVE, "Whether bulk-selecting a part implicitly deselects its children since they are covered by the parent already.\nWhile it can provide a clearer view of what's being selected globally which simplifies broad operations like deleting, moving and copying, it prevents targeted operations on nested parts like bulk property editing.") +local bulk_select_deselect = CreateConVar("pac_bulk_select_deselect", "1", FCVAR_ARCHIVE, "Whether selecting a part without holding bulk select key will deselect the bulk selected parts") local bulkselect_cursortext = CreateConVar("pac_bulk_select_cursor_info", "1", FCVAR_ARCHIVE, "Whether to draw some info next to your cursor when there is a bulk selection") CreateConVar("pac_copilot_partsearch_depth", -1, FCVAR_ARCHIVE, "amount of copiloting in the searchable part menu\n-1:none\n0:auto-focus on the text edit for events\n1:bring up a list of clickable event types\nother parts aren't supported yet") @@ -360,9 +361,18 @@ local last_span_select_part local last_select_was_span = false local last_direction +pac.AddHook("VGUIMousePressed", "kecode_tracker", function(pnl, mc) + pace.last_mouse_code = mc +end) + function pace.OnPartSelected(part, is_selecting) pace.delaybulkselect = pace.delaybulkselect or 0 --a time updated in shortcuts.lua to prevent common pac operations from triggering bulk selection local bulk_key_pressed = input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) + + if (not bulk_key_pressed) and bulk_select_deselect:GetBool() then + if pace.last_mouse_code == MOUSE_LEFT then pace.ClearBulkList(true) end + end + if RealTime() > pace.delaybulkselect and bulk_key_pressed and not input.IsKeyDown(input.GetKeyCode("v")) and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then --jumping multi-select if holding shift + ctrl if bulk_key_pressed and input.IsShiftDown() then @@ -1361,13 +1371,13 @@ do -- menu pace.recently_substituted_movable_part = obj end - function pace.ClearBulkList() + function pace.ClearBulkList(silent) for _,v in ipairs(pace.BulkSelectList) do if IsValid(v.pace_tree_node) then v.pace_tree_node:SetAlpha( 255 ) end v:SetInfo() end pace.BulkSelectList = {} - pac.Message("Bulk list deleted!") + if not silent then pac.Message("Bulk list deleted!") end --surface.PlaySound('buttons/button16.wav') end //@note pace.DoBulkSelect @@ -4146,7 +4156,9 @@ function pace.addPartMenuComponent(menu, obj, option_name) local subsume_pnl = bulk_menu:AddCVar("bulk select subsume", "pac_bulk_select_subsume", "1", "0") subsume_pnl:SetTooltip("Whether bulk select should take the hierarchy into account, deselecting children when selecting a part.\nEnable this if you commonly do broad operations like copying, deleting or moving parts.\nDisable this for targeted operations like property editing on nested model structures, for example.") bulk_menu:AddCVar("draw bulk select info next to cursor", "pac_bulk_select_cursor_info", "1", "0") - + local deselect_pnl = bulk_menu:AddCVar("bulk select deselect", "pac_bulk_select_deselect", "1", "0") + deselect_pnl:SetTooltip("Deselect all bulk selects if you select a part without holding bulk select key") + local resetting_mode, resetpnl = bulk_menu:AddSubMenu("Clear selection after operation?") resetpnl:SetImage("icon16/table_delete.png") local resetting_mode1 = resetting_mode:AddOption("Yes") resetting_mode1:SetIsCheckable(true) resetting_mode1:SetRadio(true) local resetting_mode2 = resetting_mode:AddOption("No") resetting_mode2:SetIsCheckable(true) resetting_mode2:SetRadio(true) From db319f91d9231b705e3ed3d9c34f4b9c4d663660 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 19 Feb 2025 23:24:05 -0500 Subject: [PATCH 279/300] decay-related settings for some effect parts add "showtime dynamics" to sprites/beams, a series of options to make simple bursts and fades (multiply size or alpha over time) without proxies (it runs faster and would be more accessible to players) add decay, fractional chance and fire duration related to particle counts make particle count hardcoded clamp's limit (per emission) adjustable instead, add limiter for max active particles per emitter --- lua/pac3/core/client/parts/beam.lua | 88 +++++++++++++++++++++--- lua/pac3/core/client/parts/particles.lua | 59 ++++++++++++++-- lua/pac3/core/client/parts/sprite.lua | 38 +++++++++- lua/pac3/editor/client/spawnmenu.lua | 3 +- 4 files changed, 170 insertions(+), 18 deletions(-) diff --git a/lua/pac3/core/client/parts/beam.lua b/lua/pac3/core/client/parts/beam.lua index 673b18101..85919fcf6 100644 --- a/lua/pac3/core/client/parts/beam.lua +++ b/lua/pac3/core/client/parts/beam.lua @@ -92,6 +92,13 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Material", "cable/rope") BUILDER:GetSetPart("EndPoint") BUILDER:GetSet("MultipleEndPoints","") + BUILDER:GetSet("AutoHitpos", false, {description = "Create the endpoint at the hit position in front of the part (red arrow)"}) + BUILDER:GetSet("AutoHitposFilter", "standard", {enums = { + standard = "standard", + world_only = "world_only", + life = "life", + none = "none" + }, description = "the filter modes are as such: standard = exclude player, root owner and pac_projectile\nworld_only = only hit world\nlife = hit players, NPCs, Nextbots\nnone = hit anything"}) BUILDER:SetPropertyGroup("beam size") BUILDER:GetSet("Width", 1) BUILDER:GetSet("WidthBend", 0) @@ -105,6 +112,7 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Resolution", 16) BUILDER:GetSet("TextureStretch", 1) BUILDER:GetSet("TextureScroll", 0) + BUILDER:GetSet("ScrollRate", 0) BUILDER:SetPropertyGroup("orientation") BUILDER:SetPropertyGroup("appearance") BUILDER:GetSet("StartColor", Vector(255, 255, 255), {editor_panel = "color"}) @@ -113,6 +121,19 @@ BUILDER:StartStorableVars() BUILDER:GetSet("EndAlpha", 1) BUILDER:SetPropertyGroup("other") BUILDER:PropertyOrder("DrawOrder") + BUILDER:SetPropertyGroup("Showtime dynamics") + BUILDER:GetSet("EnableDynamics", false, {description = "If you want to make a fading effect, you can do it here instead of adding proxies."}) + BUILDER:GetSet("SizeFadeSpeed", 1) + BUILDER:GetSet("SizeFadePower", 1) + BUILDER:GetSet("IncludeWidthBend", true, {description = "whether to include the width bend in the dynamics fading of the overall width multiplier"}) + BUILDER:GetSet("DynamicsStartSizeMultiplier", 1, {editor_friendly = "StartSizeMultiplier"}) + BUILDER:GetSet("DynamicsEndSizeMultiplier", 1, {editor_friendly = "EndSizeMultiplier"}) + + BUILDER:GetSet("AlphaFadeSpeed", 1) + BUILDER:GetSet("AlphaFadePower", 1) + BUILDER:GetSet("DynamicsStartAlpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}, editor_friendly = "StartAlpha"}) + BUILDER:GetSet("DynamicsEndAlpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}, editor_friendly = "EndAlpha"}) + BUILDER:EndStorableVars() function PART:GetNiceName() @@ -265,10 +286,34 @@ function PART:SetMaterial(var) end end +function PART:OnShow() + self.starttime = CurTime() + self.scrolled_amount = 0 +end + function PART:OnDraw() local part = self.EndPoint - if self.Materialm and self.StartColorC and self.EndColorC and ((part:IsValid() and part.GetWorldPosition) or self.MultiEndPoint) then + local lifetime = (CurTime() - self.starttime) + self.scrolled_amount = self.scrolled_amount + FrameTime() * self.ScrollRate + + local fade_factor_w = math.Clamp(lifetime*self.SizeFadeSpeed,0,1) + local fade_factor_a = math.Clamp(lifetime*self.AlphaFadeSpeed,0,1) + + local final_alpha_mult = self.EnableDynamics and + self.DynamicsStartAlpha + (self.DynamicsEndAlpha - self.DynamicsStartAlpha) * math.pow(fade_factor_a,self.AlphaFadePower) + or 1 + + local StartColorA = self.StartColorC.a + local EndColorA = self.EndColorC.a + self.StartColorC.a = final_alpha_mult * StartColorA + self.EndColorC.a = final_alpha_mult * EndColorA + + local final_size_mult = self.EnableDynamics and + self.DynamicsStartSizeMultiplier + (self.DynamicsEndSizeMultiplier - self.DynamicsStartSizeMultiplier) * math.pow(fade_factor_w,self.SizeFadePower) + or 1 + + if self.Materialm and self.StartColorC and self.EndColorC and ((part:IsValid() and part.GetWorldPosition) or self.MultiEndPoint or self.AutoHitpos) then local pos, ang = self:GetDrawPosition() render.SetMaterial(self.Materialm) if self.MultiEndPoint then @@ -282,13 +327,13 @@ function PART:OnDraw() self.Bend, math.Clamp(self.Resolution, 1, 256), - self.Width, + self.Width * final_size_mult, self.StartColorC, self.EndColorC, self.Frequency, self.TextureStretch, - self.TextureScroll, - self.WidthBend, + self.TextureScroll - self.scrolled_amount, + self.IncludeWidthBend and final_size_mult * self.WidthBend or self.WidthBend, self.WidthBendSize, self.StartWidthMultiplier, self.EndWidthMultiplier, @@ -296,22 +341,44 @@ function PART:OnDraw() ) end else + if self.AutoHitpos then + local filter = {} + local playerowner = self:GetPlayerOwner() + local rootowner = self:GetRootPart():GetOwner() + if self.AutoHitposFilter == "standard" then + filter = function(ent) + if ent == playerowner then return false end + if ent == rootowner then return false end + if ent:GetClass() == "pac_projectile" then return false end + return true + end + elseif self.AutoHitposFilter == "world_only" then + filter = function(ent) + return ent:IsWorld() + end + elseif self.AutoHitposFilter == "life" then + filter = function(ent) return (ent:IsNPC() or (ent:IsPlayer() and ent ~= playerowner) or ent:IsNextBot()) end + else + filter = nil + end + self.hitpos = util.QuickTrace(pos, ang:Forward()*32000, filter).HitPos + end pac.DrawBeam( pos, - part:GetWorldPosition(), + self.AutoHitpos and self.hitpos or part:GetWorldPosition(), ang:Forward(), - part:GetWorldAngles():Forward(), + self.AutoHitpos and ang:Forward() or part:GetWorldAngles():Forward(), self.Bend, math.Clamp(self.Resolution, 1, 256), - self.Width, + self.Width * final_size_mult, self.StartColorC, self.EndColorC, self.Frequency, self.TextureStretch, - self.TextureScroll, - self.WidthBend, + self.TextureScroll - self.scrolled_amount, + self.IncludeWidthBend and final_size_mult * self.WidthBend or self.WidthBend, self.WidthBendSize, self.StartWidthMultiplier, self.EndWidthMultiplier, @@ -319,6 +386,9 @@ function PART:OnDraw() ) end end + + self.StartColorC.a = StartColorA + self.EndColorC.a = EndColorA end BUILDER:Register() diff --git a/lua/pac3/core/client/parts/particles.lua b/lua/pac3/core/client/parts/particles.lua index ad01513d8..2a8c71db9 100644 --- a/lua/pac3/core/client/parts/particles.lua +++ b/lua/pac3/core/client/parts/particles.lua @@ -17,9 +17,6 @@ BUILDER:StartStorableVars() BUILDER:PropertyOrder("ParentName") BUILDER:GetSet("Follow", false) BUILDER:GetSet("Additive", false) - BUILDER:GetSet("FireOnce", false) - BUILDER:GetSet("FireDelay", 0.2) - BUILDER:GetSet("NumberParticles", 1) BUILDER:GetSet("PositionSpread", 0) BUILDER:GetSet("PositionSpread2", Vector(0,0,0)) BUILDER:GetSet("DieTime", 3) @@ -29,6 +26,14 @@ BUILDER:StartStorableVars() BUILDER:GetSet("EndLength", 0) BUILDER:GetSet("ParticleAngle", Angle(0,0,0)) BUILDER:GetSet("AddFrametimeLife", false) + + BUILDER:SetPropertyGroup("particle emissions") + BUILDER:GetSet("FireDelay", 0.2) + BUILDER:GetSet("FireOnce", false) + BUILDER:GetSet("NumberParticles", 1, {editor_onchange = function(self,num) return math.Clamp(num,0,2000) end}) + BUILDER:GetSet("FireDuration", 0, {description = "how long to fire particles\n0 = infinite"}) + BUILDER:GetSet("Decay", 0, {description = "rate of decay for particle count, in particles per second\n0 = no decay\na positive number means simple decay starting at showtime\na negative number means delayed decay so that it reaches 0 at the time of 'fire duration'"}) + BUILDER:GetSet("FractionalChance", false, {description = "If 'number particles' has decimals, there is a chance to emit another particle\ne.g. 0.5 is 50% chance to emit a particle\ne.g. 1.25 is 25% chance to fire two / 75% to fire one particle)"}) BUILDER:SetPropertyGroup("stick") BUILDER:GetSet("AlignToSurface", true, {description = "requires 3D set to true"}) BUILDER:GetSet("StickToSurface", true, {description = "requires 3D set to true, and sliding set to false"}) @@ -72,7 +77,9 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() function PART:GetNiceName() - return pac.PrettifyName(("/".. self:GetMaterial()):match(".+/(.+)")) or "error" + local str = (self:GetMaterial()):match(".+/(.+)") or "" + --return pac.PrettifyName("/".. str) or "error" + return "[".. math.Round(self.number_particles or 0,2) .. "] " .. str end local function RemoveCallback(particle) @@ -133,8 +140,12 @@ function PART:SetDrawManual(b) self:GetEmitter():SetNoDraw(b) end +local max_active_particles = CreateClientConVar("pac_limit_particles_per_emitter", "8000") +local max_emit_particles = CreateClientConVar("pac_limit_particles_per_emission", "100") function PART:SetNumberParticles(num) - self.NumberParticles = math.Clamp(num, 0, 100) + local max = max_emit_particles:GetInt() + if num > max or num > 100 then self:SetWarning("You're trying to set the number of particles beyond the pac_limit_particles_per_emission limit, the default limit is 100.\nFor reference, the default max active particles for the emitter is around 8000 but can be further limited with pac_limit_particles_per_emitter") else self:SetWarning() end + self.NumberParticles = math.Clamp(num, 0, max) end function PART:Set3D(b) @@ -145,6 +156,7 @@ end function PART:OnShow(from_rendering) self.CanKeepFiring = true self.FirstShot = true + self.FirstShotTime = RealTime() if not from_rendering then self.NextShot = 0 local pos, ang = self:GetDrawPosition() @@ -153,7 +165,28 @@ function PART:OnShow(from_rendering) end function PART:OnDraw() - if not self.FireOnce then self.CanKeepFiring = true end + self.number_particles = self.NumberParticles + if not self.FireOnce then + if self.Decay == 0 then + self.number_particles = self.NumberParticles + elseif self.Decay > 0 then + self.number_particles = math.Clamp(self.NumberParticles - (RealTime() - self.FirstShotTime) * self.Decay,0,self.NumberParticles) + else + self.number_particles = math.Clamp(-self.FireDuration * self.Decay + self.NumberParticles - (RealTime() - self.FirstShotTime) * self.Decay,0,self.NumberParticles) + end + if self.FireDuration <= 0 then + self.CanKeepFiring = true + else + if RealTime() > self.FirstShotTime + self.FireDuration then self.number_particles = 0 end + end + if self.Decay ~= 0 then + if pace and pace.IsActive() and self.Name == "" then + if IsValid(self.pace_tree_node) then + self.pace_tree_node:SetText(self:GetNiceName()) + end + end + end + end local pos, ang = self:GetDrawPosition() local emitter = self:GetEmitter() @@ -226,7 +259,19 @@ function PART:EmitParticles(pos, ang, real_ang) double = 2 end - for _ = 1, self.NumberParticles do + local free_particles = math.max(max_active_particles:GetInt() - emt:GetNumActiveParticles(),0) + local max = math.min(free_particles, max_emit_particles:GetInt()) + --self.number_particles is self.NumberParticles with optional decay applied + local fractional_chance = 0 + if self.FractionalChance then + --e.g. treat 0.5 as 50% chance to emit or not + local delta = self.number_particles - math.floor(self.number_particles) + if math.random() < delta then + self.number_particles = self.number_particles + 1 + end + end + + for _ = 1, math.min(self.number_particles,max) do local mats = self.Material:Split(";") if #mats > 1 then self.Materialm = pac.Material(table.Random(mats), self) diff --git a/lua/pac3/core/client/parts/sprite.lua b/lua/pac3/core/client/parts/sprite.lua index 71e227fe0..4aa85dd2e 100644 --- a/lua/pac3/core/client/parts/sprite.lua +++ b/lua/pac3/core/client/parts/sprite.lua @@ -24,8 +24,25 @@ BUILDER:StartStorableVars() BUILDER:GetSet("Color", Vector(255, 255, 255), {editor_panel = "color"}) BUILDER:GetSet("Alpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}}) BUILDER:GetSet("Translucent", true) + + BUILDER:SetPropertyGroup("Showtime dynamics") + BUILDER:GetSet("EnableDynamics", false, {description = "If you want to make a fading effect, you can do it here instead of adding proxies."}) + BUILDER:GetSet("SizeFadeSpeed", 1) + BUILDER:GetSet("SizeFadePower", 1) + BUILDER:GetSet("DynamicsStartSizeMultiplier", 1, {editor_friendly = "StartSizeMultiplier"}) + BUILDER:GetSet("DynamicsEndSizeMultiplier", 1, {editor_friendly = "EndSizeMultiplier"}) + + BUILDER:GetSet("AlphaFadeSpeed", 1) + BUILDER:GetSet("AlphaFadePower", 1) + BUILDER:GetSet("DynamicsStartAlpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}, editor_friendly = "StartAlpha"}) + BUILDER:GetSet("DynamicsEndAlpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}, editor_friendly = "EndAlpha"}) + BUILDER:EndStorableVars() +function PART:OnShow() + self.starttime = CurTime() +end + function PART:GetNiceName() if not self:GetSpritePath() then return "error" @@ -110,6 +127,24 @@ function PART:OnDraw() end local old_alpha + + local lifetime = (CurTime() - self.starttime) + + local fade_factor_s = math.Clamp(lifetime*self.SizeFadeSpeed,0,1) + local fade_factor_a = math.Clamp(lifetime*self.AlphaFadeSpeed,0,1) + local final_alpha_mult = self.EnableDynamics and + self.DynamicsStartAlpha + (self.DynamicsEndAlpha - self.DynamicsStartAlpha) * math.pow(fade_factor_a,self.AlphaFadePower) + or 1 + + local final_size_mult = self.EnableDynamics and + self.DynamicsStartSizeMultiplier + (self.DynamicsEndSizeMultiplier - self.DynamicsStartSizeMultiplier) * math.pow(fade_factor_s,self.SizeFadePower) + or 1 + + if self.EnableDynamics then + if not self.ColorC then self:SetColor(self:GetColor()) end + self.ColorC.a = self.Alpha * 255 * final_alpha_mult + end + if pac.drawing_motionblur_alpha then if not self.ColorC then self:SetColor(self:GetColor()) end old_alpha = self.ColorC.a @@ -120,7 +155,7 @@ function PART:OnDraw() local pos = self:GetDrawPosition() render_SetMaterial(mat) - render_DrawSprite(pos, self.SizeX * self.Size, self.SizeY * self.Size, self.ColorC) + render_DrawSprite(pos, self.SizeX * self.Size * final_size_mult, self.SizeY * self.Size * final_size_mult, self.ColorC) if self.IgnoreZ then cam_IgnoreZ(false) @@ -129,6 +164,7 @@ function PART:OnDraw() if pac.drawing_motionblur_alpha then self.ColorC.a = old_alpha end + end end diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index d1428174f..7b2f5f505 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -138,7 +138,8 @@ function pace.ClientSettingsMenu(self) self:NumSlider(L"Shake draw distance: ", "pac_limit_shake_draw_distance", 0, 20000, 0) self:NumSlider(L"Shake max duration: ", "pac_limit_shake_duration", 0, 120, 0) self:NumSlider(L"Shake max amplitude: ", "pac_limit_shake_amplitude", 0, 1000, 0) - + self:NumSlider(L"Particles max per emission: ", "pac_limit_particles_per_emission", 0, 5000, 0) + self:NumSlider(L"Particles max per emitter: ", "pac_limit_particles_per_emitter", 0, 10000, 0) end function pace.AdminSettingsMenu(self) From 0efee70e4d95bd4e6bb753c08f14a6f76e146a77 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 24 Feb 2025 19:09:52 -0500 Subject: [PATCH 280/300] hotfix --- lua/pac3/core/client/parts/particles.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/particles.lua b/lua/pac3/core/client/parts/particles.lua index 2a8c71db9..ccb3603dd 100644 --- a/lua/pac3/core/client/parts/particles.lua +++ b/lua/pac3/core/client/parts/particles.lua @@ -76,6 +76,10 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() +function PART:Initialize() + self.number_particles = 0 +end + function PART:GetNiceName() local str = (self:GetMaterial()):match(".+/(.+)") or "" --return pac.PrettifyName("/".. str) or "error" @@ -165,10 +169,10 @@ function PART:OnShow(from_rendering) end function PART:OnDraw() - self.number_particles = self.NumberParticles + self.number_particles = self.NumberParticles or 0 if not self.FireOnce then if self.Decay == 0 then - self.number_particles = self.NumberParticles + self.number_particles = self.NumberParticles or 0 elseif self.Decay > 0 then self.number_particles = math.Clamp(self.NumberParticles - (RealTime() - self.FirstShotTime) * self.Decay,0,self.NumberParticles) else @@ -243,6 +247,7 @@ function PART:SetMaterial(var) end function PART:EmitParticles(pos, ang, real_ang) + self.number_particles = self.number_particles or 0 if self.FireOnce and not self.FirstShot then self.CanKeepFiring = false end local emt = self:GetEmitter() if not emt then return end From 06329e4bba78fd0c8296b0a13100be26375318af Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 24 Feb 2025 21:58:58 -0500 Subject: [PATCH 281/300] add showtime dynamics to sunbeams sunbeam multiplier: fade factor is multiplicative, with an additional initial "attack" factor (simple linear fade-in to avoid instant flashes while also allowing the fadeout) darken: fade factor is additive --- lua/pac3/core/client/parts/sunbeams.lua | 67 ++++++++++++++++++++----- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/lua/pac3/core/client/parts/sunbeams.lua b/lua/pac3/core/client/parts/sunbeams.lua index 2be731db7..50cdea0ff 100644 --- a/lua/pac3/core/client/parts/sunbeams.lua +++ b/lua/pac3/core/client/parts/sunbeams.lua @@ -11,11 +11,25 @@ local draw_distance = CreateClientConVar("pac_limit_sunbeams_draw_distance", "10 BUILDER:StartStorableVars() - BUILDER:GetSet("Darken", 0) - BUILDER:GetSet("Multiplier", 0.25, {editor_sensitivity = 0.25}) - BUILDER:GetSet("Size", 0.1, {editor_sensitivity = 0.25}) - BUILDER:GetSet("DrawDistance", 1000, {editor_onchange = function(self, val) return math.max(val,0) end}) - BUILDER:GetSet("Translucent", true) + BUILDER:GetSet("Darken", 0) + BUILDER:GetSet("Multiplier", 0.25, {editor_sensitivity = 0.25}) + BUILDER:GetSet("Size", 0.1, {editor_sensitivity = 0.25}) + BUILDER:GetSet("DrawDistance", 1000, {editor_onchange = function(self, val) return math.max(val,0) end}) + BUILDER:GetSet("Translucent", true) + + BUILDER:SetPropertyGroup("Showtime dynamics") + BUILDER:GetSet("EnableDynamics", false, {description = "If you want to make a fading effect, you can do it here instead of adding proxies.\nThe multiplier parts work multiplicatively, involving 3 terms: attack * multiplier * fade\nThe darken part works additively. It can add more darken on top of the existing darken value"}) + BUILDER:GetSet("EndMultiplier", 1) + BUILDER:GetSet("StartMultiplier", 1) + BUILDER:GetSet("MultiplierFadePower", 1) + BUILDER:GetSet("MultiplierFadeSpeed", 1) + BUILDER:GetSet("MultiplierAttack", 0, {description = "Additional fade-in time to optionally soften the flash. This is in terms of normalized speed"}) + + BUILDER:GetSet("EndDarken", 0) + BUILDER:GetSet("StartDarken", 0) + BUILDER:GetSet("DarkenFadeSpeed", 1) + BUILDER:GetSet("DarkenFadePower", 1) + BUILDER:EndStorableVars() function PART:GetNiceName() @@ -23,6 +37,10 @@ function PART:GetNiceName() return mult > 0 and "bright sunbeams" or mult < 0 and "dark sunbeams" or self.ClassName end +function PART:OnShow() + self.starttime = CurTime() +end + function PART:OnDraw() if not DrawSunbeams then DrawSunbeams = _G.DrawSunbeams end @@ -35,13 +53,38 @@ function PART:OnDraw() local dist_mult = - math.Clamp(pac.EyePos:Distance(pos) / distance, 0, 1) + 1 - DrawSunbeams( - self.Darken, - dist_mult * self.Multiplier * (math.Clamp(pac.EyeAng:Forward():Dot((pos - pac.EyePos):GetNormalized()) - 0.5, 0, 1) * 2) ^ 5, - self.Size, - spos.x / ScrW(), - spos.y / ScrH() - ) + if self.EnableDynamics then + local lifetime = (CurTime() - self.starttime) + + local init_mult = 1 + if self.MultiplierAttack > 0 then init_mult = math.Clamp(lifetime*self.MultiplierAttack,0,1) end + + local fade_factor_m = math.Clamp(lifetime*self.MultiplierFadeSpeed,0,1) + local fade_factor_d = math.Clamp(lifetime*self.DarkenFadeSpeed,0,1) + local final_mult_mult = self.EnableDynamics and + self.StartMultiplier + (self.EndMultiplier - self.StartMultiplier) * math.pow(fade_factor_m,self.MultiplierFadePower) + or self.Multiplier + + local final_darken_add = self.EnableDynamics and + self.StartDarken + (self.EndDarken - self.StartDarken) * math.pow(fade_factor_d,self.DarkenFadePower) + or 0 + + DrawSunbeams( + self.Darken + final_darken_add, + dist_mult * init_mult * self.Multiplier * final_mult_mult * (math.Clamp(pac.EyeAng:Forward():Dot((pos - pac.EyePos):GetNormalized()) - 0.5, 0, 1) * 2) ^ 5, + self.Size, + spos.x / ScrW(), + spos.y / ScrH() + ) + else + DrawSunbeams( + self.Darken, + dist_mult * self.Multiplier * (math.Clamp(pac.EyeAng:Forward():Dot((pos - pac.EyePos):GetNormalized()) - 0.5, 0, 1) * 2) ^ 5, + self.Size, + spos.x / ScrW(), + spos.y / ScrH() + ) + end cam.End2D() end From 301393a55a2c6d7e5e005f49898bcfbe8e5a1479 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 1 Mar 2025 10:30:18 -0500 Subject: [PATCH 282/300] hotfix --- lua/pac3/core/client/parts/particles.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/client/parts/particles.lua b/lua/pac3/core/client/parts/particles.lua index ccb3603dd..002a663af 100644 --- a/lua/pac3/core/client/parts/particles.lua +++ b/lua/pac3/core/client/parts/particles.lua @@ -158,6 +158,7 @@ function PART:Set3D(b) end function PART:OnShow(from_rendering) + self.number_particles = self.NumberParticles self.CanKeepFiring = true self.FirstShot = true self.FirstShotTime = RealTime() From 78f5491f87dbb48c6192217fae8c061a326176c6 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 1 Mar 2025 13:15:37 -0500 Subject: [PATCH 283/300] add "root owner" model option to model properties for stuff like vehicles and NPCs it also requests a spawnicon rebuild if it doesn't exist --- 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 582d758eb..ea3e6bd25 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -1640,6 +1640,33 @@ do -- base editable 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) From 8c8759e411d280110abd8a88ec8b598a2f34a723 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 1 Mar 2025 21:51:07 -0500 Subject: [PATCH 284/300] new lock part modes (aim) SetEyeang sets your eyeang to lock's world angle AimToPos sets eyeang to point from eyeang to lock's world pos and some small revisions for uniformity related to use of cached cvars --- lua/pac3/core/client/parts/lock.lua | 67 +++++++++++++++++++++++----- lua/pac3/editor/client/settings.lua | 1 + lua/pac3/editor/client/spawnmenu.lua | 3 +- lua/pac3/extra/shared/net_combat.lua | 1 + 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index d4e949f18..01e89cf89 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -22,12 +22,14 @@ local convar_lock = GetConVar("pac_sv_lock") local convar_lock_grab = GetConVar("pac_sv_lock_grab") local convar_lock_max_grab_radius = GetConVar("pac_sv_lock_max_grab_radius") local convar_lock_teleport = GetConVar("pac_sv_lock_teleport") +local convar_lock_aim = GetConVar("pac_sv_lock_aim") local convar_combat_enforce_netrate = GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") --sorcerous hack fix if convar_lock == nil then timer.Simple(10, function() convar_lock = GetConVar("pac_sv_lock") end) end if convar_lock_grab == nil then timer.Simple(10, function() convar_lock_grab = GetConVar("pac_sv_lock_grab") end) end if convar_lock_teleport == nil then timer.Simple(10, function() convar_lock_teleport = GetConVar("pac_sv_lock_teleport") end) end +if convar_lock_aim == nil then timer.Simple(10, function() convar_lock_aim = GetConVar("pac_sv_lock_aim") end) end if convar_lock_max_grab_radius == nil then timer.Simple(10, function() convar_lock_max_grab_radius = GetConVar("pac_sv_lock_max_grab_radius") end) end if convar_combat_enforce_netrate == nil then timer.Simple(10, function() convar_combat_enforce_netrate = GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") end) end @@ -41,7 +43,13 @@ PART.Icon = "icon16/lock.png" BUILDER:StartStorableVars() :SetPropertyGroup("Behaviour") - :GetSet("Mode", "None", {enums = {["None"] = "None", ["Grab"] = "Grab", ["Teleport"] = "Teleport"}}) + :GetSet("Mode", "None", {enums = { + ["None"] = "None", + ["Grab"] = "Grab", + ["Teleport"] = "Teleport", + ["SetEyeang"] = "SetEyeang", + ["AimToPos"] = "AimToPos" + }}) :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"}) :GetSet("RelativeGrab", false) :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before self.grabbing are re-applied"}) @@ -54,6 +62,11 @@ BUILDER:StartStorableVars() :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."}) :GetSet("Preview", false) + :SetPropertyGroup("AimMode") + :GetSet("AffectPitch", true) + :GetSet("AffectYaw", true) + :GetSet("ContinuousAim", true) + :SetPropertyGroup("TeleportSafety") :GetSet("ClampDistance", false, {description = "Prevents the teleport from going too far (By Radius amount). For example, if you use hitpos bone on a pac model, it can act as a safety in case the raycast falls out of bounds."}) :GetSet("SlopeSafety", false, {description = "Teleports a bit up in case you end up on a slope and get stuck."}) @@ -73,6 +86,28 @@ BUILDER:StartStorableVars() BUILDER:EndStorableVars() +local function set_eyeang(ply, self) + if ply ~= pac.LocalPlayer or self:GetPlayerOwner() ~= ply then return end + if not convar_lock_aim:GetBool() then + self:SetWarning("lock part aiming is disabled on this server!") + return + end + local plyang = ply:EyeAngles() + local pos, ang = self:GetDrawPosition() + + if self.Mode == "SetEyeang" then + ang.r = 0 + if not self.AffectPitch then ang.p = plyang.p end + if not self.AffectYaw then ang.y = plyang.y end + ply:SetEyeAngles(ang) + elseif self.Mode == "AimToPos" then + local ang = (pos - ply:EyePos()):Angle() + if not self.AffectPitch then ang.p = plyang.p end + if not self.AffectYaw then ang.y = plyang.y end + ply:SetEyeAngles(ang) + end +end + function PART:OnThink() if not convar_lock:GetBool() then return end if util.NetworkStringToID("pac_request_position_override_on_entity_grab") == 0 then self:SetError("This part is deactivated on the server") return end @@ -216,6 +251,12 @@ function PART:OnThink() self.grabbing = true self.teleported = false end + elseif self.Mode == "SetEyeang" then + if not self.ContinuousAim then return end + set_eyeang(self:GetPlayerOwner(), self) + elseif self.Mode == "AimToPos" then + if not self.ContinuousAim then return end + set_eyeang(self:GetPlayerOwner(), self) end --if self.is_first_time then print("lock " .. self.UniqueID .. "did its first clock") end self.is_first_time = false @@ -326,7 +367,7 @@ function PART:OnShow() origin_part = self end if origin_part == nil or not self.Preview or pac.LocalPlayer ~= self:GetPlayerOwner() then return end - local sv_dist = GetConVar("pac_sv_lock_max_grab_radius"):GetInt() + local sv_dist = convar_lock_max_grab_radius:GetInt() render.DrawLine(origin_part:GetWorldPosition(),origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount),Color(255,255,255)) @@ -342,7 +383,7 @@ function PART:OnShow() end) if self.Mode == "Teleport" then - if not GetConVar('pac_sv_lock_teleport'):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then return end + if not convar_lock_teleport:GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then return end if pace.still_loading_wearing then return end self.target_ent = nil @@ -363,7 +404,7 @@ function PART:OnShow() end end if self.SlopeSafety then teleport_pos_final = teleport_pos_final + Vector(0,0,30) end - if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not convar_combat_enforce_netrate:GetBool() then if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end end timer.Simple(0, function() @@ -381,6 +422,10 @@ function PART:OnShow() elseif self.Mode == "Grab" then self:DecideTarget() self:CheckEntValidity() + elseif self.Mode == "SetEyeang" then + set_eyeang(self:GetPlayerOwner(), self) + elseif self.Mode == "AimToPos" then + set_eyeang(self:GetPlayerOwner(), self) end end @@ -388,7 +433,7 @@ function PART:OnHide() pac.RemoveHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID) self.teleported = false self.grabbing = false - if not IsValid(self.target_ent) then return + if self.target_ent == nil then return else self.target_ent.IsGrabbed = false self.target_ent.IsGrabbedByUID = nil end if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end self:reset_ent_ang() @@ -401,7 +446,7 @@ function PART:reset_ent_ang() if reset_ent:IsValid() then timer.Simple(math.min(self.RestoreDelay,5), function() if pac.LocalPlayer == self:GetPlayerOwner() then - if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then + if not convar_combat_enforce_netrate:GetBool() then if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end end net.Start("pac_request_angle_reset_on_entity") @@ -521,21 +566,21 @@ end function PART:Initialize() self.default_ang = Angle(0,0,0) - if not GetConVar('pac_sv_lock_grab'):GetBool() then - if not GetConVar('pac_sv_lock_teleport'):GetBool() then + if not convar_lock_grab:GetBool() then + if not convar_lock_teleport:GetBool() then self:SetWarning("lock part grabs and teleports are disabled on this server!") else self:SetWarning("lock part grabs are disabled on this server!") end end - if not GetConVar('pac_sv_lock_teleport'):GetBool() then - if not GetConVar('pac_sv_lock_grab'):GetBool() then + if not convar_lock_teleport:GetBool() then + if not convar_lock_grab:GetBool() then self:SetWarning("lock part grabs and teleports are disabled on this server!") else self:SetWarning("lock part teleports are disabled on this server!") end end - if not GetConVar('pac_sv_lock'):GetBool() then self:SetError("lock parts are disabled on this server!") end + if not convar_lock:GetBool() then self:SetError("lock parts are disabled on this server!") end end diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index cb2d47261..b2db53375 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -21,6 +21,7 @@ local convar_params_lock = { {"pac_sv_lock", "Allow lock part", "", -1, 0, 200}, {"pac_sv_lock_teleport", "Allow lock part teleportation", "", -1, 0, 200}, {"pac_sv_lock_grab", "Allow lock part grabbing", "", -1, 0, 200}, + {"pac_sv_lock_aim", "Allow lock part aiming", "", -1, 0, 200}, {"pac_sv_lock_allow_grab_ply", "Allow grabbing players", "", -1, 0, 200}, {"pac_sv_lock_allow_grab_npc", "Allow grabbing NPCs", "", -1, 0, 200}, {"pac_sv_lock_allow_grab_ent", "Allow grabbing other entities", "", -1, 0, 200}, diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index 7b2f5f505..2199a8308 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -192,7 +192,8 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Enable lock part", "pac_sv_lock") self:CheckBox(L"Allow grab", "pac_sv_lock_grab") self:CheckBox(L"Allow teleport", "pac_sv_lock_teleport") - + self:CheckBox(L"Allow aiming", "pac_sv_lock_aim") + self:Help(L"Force part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable force part", "pac_sv_force") self:NumSlider(L"Max amount", "pac_sv_force_max_amount", 0, 10000000, 0) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index a695278e0..2f304076a 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -29,6 +29,7 @@ local damagezone_allow_ragdoll_networking_for_hitpart = CreateConVar("pac_sv_dam local lock_allow = CreateConVar("pac_sv_lock", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside") local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside") local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside") +local lock_allow_aim = CreateConVar("pac_sv_lock_aim", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part aim serverside") local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius") local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing players with lock part") local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part") From ad58c593ba2e13cd10cb2f404a6d8862004aa219 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 1 Mar 2025 22:52:55 -0500 Subject: [PATCH 285/300] lock part aim smoothing allows for smoothing / less aggressive control of the eye angles by doing an iterated lerp to target every frame --- lua/pac3/core/client/parts/lock.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 01e89cf89..98b1ba369 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -66,6 +66,8 @@ BUILDER:StartStorableVars() :GetSet("AffectPitch", true) :GetSet("AffectYaw", true) :GetSet("ContinuousAim", true) + :GetSet("SmoothAiming", false, {description = "Gradually ease into the target angle by only changing the angle by a fraction every frame instead of fully setting it immediately"}) + :GetSet("SmoothFraction", 0.05, {editor_clamp = {0,1}}) :SetPropertyGroup("TeleportSafety") :GetSet("ClampDistance", false, {description = "Prevents the teleport from going too far (By Radius amount). For example, if you use hitpos bone on a pac model, it can act as a safety in case the raycast falls out of bounds."}) @@ -99,11 +101,16 @@ local function set_eyeang(ply, self) ang.r = 0 if not self.AffectPitch then ang.p = plyang.p end if not self.AffectYaw then ang.y = plyang.y end - ply:SetEyeAngles(ang) elseif self.Mode == "AimToPos" then - local ang = (pos - ply:EyePos()):Angle() + ang = (pos - ply:EyePos()):Angle() if not self.AffectPitch then ang.p = plyang.p end if not self.AffectYaw then ang.y = plyang.y end + end + if self.SmoothAiming then + local lerped_ang = LerpAngle(self.SmoothFraction, plyang, ang) + lerped_ang.r = 0 + ply:SetEyeAngles(lerped_ang) + else ply:SetEyeAngles(ang) end end From b2211e5db98667c1b6b8c0b36fd7961a4216541d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 2 Mar 2025 22:11:22 -0500 Subject: [PATCH 286/300] event update add AND gate fix OR gate, add ignore_inverts add ratio_timer (allowing reset on hide and biased ratios e.g. different from 50/50) timer default interval is now 1 (0 caused spam issues, kinda annoying) add timerx2 (start and end times instead of needing to stack two timerx) timerx2 can be quickbuilt with a two-number event input e.g. "1 3" will automatically set start time to 1, end time to 3 bidirectional link between arguments and dynamic properties to reflect edits in real time --- lua/pac3/core/client/parts/event.lua | 213 +++++++++++++++++++++++++-- lua/pac3/editor/client/parts.lua | 59 ++++++++ 2 files changed, 260 insertions(+), 12 deletions(-) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index fce9a00fb..c7bf0b110 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -170,6 +170,16 @@ function PART:SetEvent(event) if (owner == pac.LocalPlayer) and (not pace.processing) then if event == "command" then owner.pac_command_events = owner.pac_command_events or {} end if not self.Events[event] then --invalid event? try another event + if #string.Split(event, " ") == 2 then --timerx2 + local strs = string.Split(event, " ") + timer.Simple(0.2, function() + if not self.pace_properties or self ~= pace.current_part then return end + self:SetEvent("timerx2") + self:SetArguments(strs[1] .. "@@" .. strs[2] .. "@@1@@0") + pace.PopulateProperties(self) + end) + return + end if isnumber(tonumber(event)) then --timerx timer.Simple(0.2, function() if not self.pace_properties or self ~= pace.current_part then return end @@ -220,7 +230,36 @@ function PART:SetEvent(event) end end +function PART:SetProperty(key, val) + if self["Set" .. key] ~= nil then + if self["Get" .. key](self) ~= val then + self["Set" .. key](self, val) + end + elseif self.GetDynamicProperties then + local info = self:GetDynamicProperties()[key] + if info and info then + if isnumber(val) then + val = math.Round(val, 7) + end + info.set(val) + if self:GetPlayerOwner() ~= pac.LocalPlayer then return end + if pace.IsActive() then + self.pace_properties["Arguments"]:SetText(" " .. self.Arguments) + self.pace_properties["Arguments"].original_str = self.Arguments + end + end + end +end + +function PART:SetArguments(str) + self.Arguments = str + if pace.IsActive() and pac.LocalPlayer == self:GetPlayerOwner() then + pace.PopulateProperties(self) + end +end + function PART:Initialize() + self.found_cached_parts = {} self.specialtrackedparts = {} self.ExtraHermites = {} if self:GetPlayerOwner() == LocalPlayer() then @@ -234,13 +273,21 @@ function PART:Initialize() end end) end + --force refresh + timer.Simple(10, function() + self.found_cached_parts = {} + end) end function PART:GetOrFindCachedPart(uid_or_name) local part = nil - self.found_cached_parts = self.found_cached_parts or {} - if self.found_cached_parts[uid_or_name] then return self.found_cached_parts[uid_or_name] end + 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 local owner = self:GetPlayerOwner() part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name) @@ -577,6 +624,36 @@ PART.OldEvents = { end, }, + timerx2 = { + operator_type = "none", + tutorial_explanation = "timerx2 is a dual timerx, a stopwatch that counts time since it's shown and determines whether it fits within the window defined by StartTime and EndTime", + arguments = {{StartTime = "number"}, {EndTime = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}}, + userdata = { + {default = 0.5, timerx_property = "StartTime"}, + {default = 1, timerx_property = "EndTime"}, + {default = true, timerx_property = "reset_on_hide"} + }, + nice = function(self, ent, seconds, seconds2) + return "timerx2: " .. ("%.2f"):format(self.number or 0, 2) .. " between " .. seconds .. " and " .. seconds2 .. " seconds" + end, + callback = function(self, ent, StartTime, EndTime, reset_on_hide, synced_time) + + local time = synced_time and CurTime() or RealTime() + + self.time = self.time or time + self.timerx_reset = reset_on_hide + + if self.AffectChildrenOnly and self:IsHiddenBySomethingElse() then + return false + end + self.number = time - self.time + + return self.number > StartTime and self.number < EndTime + + --return self:NumberOperator(self.number, seconds) + end, + }, + timersys = { operator_type = "number", preferred_operator = "above", tutorial_explanation = "like timerx, timersys is a stopwatch that counts time (it uses SysTime()) since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes", @@ -1549,9 +1626,32 @@ PART.OldEvents = { end, }, + ratio_timer = { + operator_type = "none", + arguments = {{interval = "number"}, {offset = "number"}, {ratio = "number"}, {reset_on_hide = "boolean"}}, + userdata = {{default = 1}, {default = 0}, {default = 0.5}, {default = false}}, + callback = function(self, ent, interval, offset, ratio, reset_on_hide) + interval = interval or 1 + offset = offset or 0 + final_offset = offset + ratio = math.Clamp(math.abs(ratio or 0.5), 0, 1) + + if interval == 0 or interval < FrameTime() then + self.timer_hack = not self.timer_hack + return self.timer_hack + end + + if reset_on_hide then + final_offset = -self.showtime + offset + end + return (CurTime() + final_offset) % interval < (interval * ratio) + end, + }, + timer = { operator_type = "none", arguments = {{interval = "number"}, {offset = "number"}}, + userdata = {{default = 1}, {default = 0}}, callback = function(self, ent, interval, offset) interval = interval or 1 offset = offset or 0 @@ -2358,7 +2458,7 @@ PART.OldEvents = { damage_zone_hit = { operator_type = "number", preferred_operator = "above", arguments = {{time = "number"}, {damage = "number"}, {uid = "string"}}, - userdata = {{default = 1}, {default = 0}, {enums = function(part) + userdata = {{default = 1}, {default = 0}, {default = "", enums = function(part) local output = {} local parts = pac.GetLocalParts() @@ -2408,7 +2508,7 @@ PART.OldEvents = { damage_zone_kill = { operator_type = "mixed", preferred_operator = "above", arguments = {{time = "number"}, {uid = "string"}}, - userdata = {{default = 1}, {enums = function(part) + userdata = {{default = 1}, {default = "", enums = function(part) local output = {} local parts = pac.GetLocalParts() @@ -2635,8 +2735,8 @@ PART.OldEvents = { or_gate = { operator_type = "none", preferred_operator = "find simple", tutorial_explanation = "combines multiple events into an OR gate, the event will activate as soon as one of the events listed is activated (taking inverts into account)", - arguments = {{uids = "string"}}, - userdata = {{enums = function(part) + arguments = {{uids = "string"}, {ignore_inverts = "boolean"}}, + userdata = {{default = "", enums = function(part) local output = {} local parts = pac.GetLocalParts() for i, part in pairs(parts) do @@ -2646,20 +2746,108 @@ PART.OldEvents = { end return output end}}, - callback = function(self, ent, uids) + callback = function(self, ent, uids, ignore_inverts) if uids == "" then return false end local uid_splits = string.Split(uids, ";") + local true_count = 0 for i,uid in ipairs(uid_splits) do local part = self:GetOrFindCachedPart(uid) - if part then - local b = part.event_triggered - if part.Invert then b = not b end + if part:IsValid() then + local raw = part.raw_event_condition + local b = false + if ignore_inverts then + b = raw + else + b = not part.event_triggered + end if b then - return true + true_count = true_count + 1 end end end - return false + return true_count > 0 + end, + }, + xor_gate = { + operator_type = "none", preferred_operator = "find simple", + tutorial_explanation = "combines multiple events into an XOR gate, the event will activate if one (and only one) of the two events is activated (taking inverts into account)", + arguments = {{uid1 = "string"},{uid2 = "string"}}, + userdata = { + {default = "", enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + for i, part in pairs(parts) do + if part.ClassName == "event" then + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID + end + end + return output + end}, + {default = "", enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + for i, part in pairs(parts) do + if part.ClassName == "event" then + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID + end + end + return output + end} + }, + callback = function(self, ent, uid1, uid2) + if uid1 == "" then return false end + if uid2 == "" then return false end + local part1 = self:GetOrFindCachedPart(uid1) + local part2 = self:GetOrFindCachedPart(uid2) + if not IsValid(part1) or not IsValid(part2) then return false end + local b1 = part1.event_triggered if part1.Invert then b1 = not b1 end + local b2 = part2.event_triggered if part2.Invert then b2 = not b2 end + return ((b1 and not b2) or (b2 and not b1)) and not (b1 and b2) + end, + nice = function(self, ent, uid1, uid2) + local part1 = self:GetOrFindCachedPart(uid1) + local part2 = self:GetOrFindCachedPart(uid2) + if not IsValid(part1) or not IsValid(part2) then return "xor_gate : [" .. uid1 .. ", " .. uid2 .. "] " end + local str = "xor_gate : [" .. part1:GetName() .. ", " .. part2:GetName() .. "]" + return str + end + }, + and_gate = { + operator_type = "none", preferred_operator = "find simple", + tutorial_explanation = "combines multiple events into an AND gate, the event will activate when all the events listed are activated (taking inverts into account)", + arguments = {{uids = "string"}, {ignore_inverts = "boolean"}}, + userdata = {{default = "", enums = function(part) + local output = {} + local parts = pac.GetLocalParts() + for i, part in pairs(parts) do + if part.ClassName == "event" then + output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID + end + end + return output + end}}, + callback = function(self, ent, uids, ignore_inverts) + if uids == "" then return false end + local uid_splits = string.Split(uids, ";") + for i,uid in ipairs(uid_splits) do + local part = self:GetOrFindCachedPart(uid) + if part:IsValid() then + local raw = part.raw_event_condition + local b = false + if ignore_inverts then + b = raw + else + b = not part.event_triggered + end + + if not b then + return false + end + else + return false + end + end + return true end, } @@ -3263,6 +3451,7 @@ local function should_trigger(self, ent, eventObject) else b = eventObject:Think(self, ent, self:GetParsedArgumentsForObject(eventObject)) or false end + self.raw_event_condition = b if self.Invert then b = not b diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 8f98a1603..0382ec3c3 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -3841,6 +3841,47 @@ end function pace.AddClassSpecificPartMenuComponents(menu, obj) if obj.Notes == "showhidetest" then menu:AddOption("(hide/show test) reset", function() obj:CallRecursive("OnShow") end):SetIcon("icon16/star.png") end + do --event reorganization + local full_events = true + for i,v in ipairs(pace.BulkSelectList) do + if v.ClassName ~= "event" then full_events = false end + end + if full_events then + menu:AddOption("reorganize into a non-ACO pocket", function() + for i,part in ipairs(pace.BulkSelectList) do + part:SetParent(part:GetRootPart()) + end + local prime_parent = obj:GetParent() + if prime_parent.ClassName == "event" or pace.BulkSelectList[1] == prime_parent then + prime_parent = obj:GetRootPart() + end + for i,part in ipairs(pace.BulkSelectList) do + part:SetParent() + part:SetAffectChildrenOnly(false) + part:SetDestinationPart() + end + obj:SetParent(prime_parent) + for i,part in ipairs(pace.BulkSelectList) do + part:SetParent(obj) + end + end):SetIcon("icon16/clock_link.png") + menu:AddOption("reorganize into an ACO downward tower", function() + local parent = obj:GetParent() + local grandparent = obj:GetParent() + if parent.Parent then grandparent = parent:GetParent() end + + for i,part in ipairs(pace.BulkSelectList) do + part:SetAffectChildrenOnly(true) + part:SetDestinationPart() + part:SetParent(parent) + parent = part + end + pace.BulkSelectList[1]:SetParent(obj:GetParent()) + obj:SetParent(parent) + end):SetIcon("icon16/clock_link.png") + end + end + if obj.ClassName == "camera" then if not obj:IsHidden() then local remembered_view = {pace.ViewPos, pace.ViewAngles} @@ -3994,6 +4035,24 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) end obj:SetMultipleTargetParts(table.concat(uid_tbl,";")) end):SetIcon("icon16/star.png") + if obj.Event == "and_gate" or obj.Event == "or_gate" then + menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set AND or OR gate arguments", function() + local uid_tbl = {} + for i,part in ipairs(pace.BulkSelectList) do + table.insert(uid_tbl, part.UniqueID) + end + obj:SetProperty("uids", table.concat(uid_tbl,";")) + end):SetIcon("icon16/clock_link.png") + end + if obj.Event == "xor_gate" and #pace.BulkSelectList == 2 then + menu:AddOption("(2 parts in Bulk select) Set XOR arguments", function() + local uid_tbl = {} + for i,part in ipairs(pace.BulkSelectList) do + table.insert(uid_tbl, part.UniqueID) + end + obj:SetArguments(table.concat(uid_tbl,"@@")) + end):SetIcon("icon16/clock_link.png") + end end if not IsValid(obj.DestinationPart) then menu:AddOption("engrave / quick-link to parent", function() obj:SetDestinationPart(obj:GetParent()) end):SetIcon("icon16/star.png") From d2ed2dca293e3a132ec8f891a8405717ec64adc2 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 2 Mar 2025 23:11:33 -0500 Subject: [PATCH 287/300] hotfix (quick actions - event reorganization) actually I want this at the end so that events quick access actions (triggers) are the priority also don't add the options if there's nothing in bulk select --- lua/pac3/editor/client/parts.lua | 82 ++++++++++++++++---------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua index 0382ec3c3..8d8dbd265 100644 --- a/lua/pac3/editor/client/parts.lua +++ b/lua/pac3/editor/client/parts.lua @@ -3841,47 +3841,6 @@ end function pace.AddClassSpecificPartMenuComponents(menu, obj) if obj.Notes == "showhidetest" then menu:AddOption("(hide/show test) reset", function() obj:CallRecursive("OnShow") end):SetIcon("icon16/star.png") end - do --event reorganization - local full_events = true - for i,v in ipairs(pace.BulkSelectList) do - if v.ClassName ~= "event" then full_events = false end - end - if full_events then - menu:AddOption("reorganize into a non-ACO pocket", function() - for i,part in ipairs(pace.BulkSelectList) do - part:SetParent(part:GetRootPart()) - end - local prime_parent = obj:GetParent() - if prime_parent.ClassName == "event" or pace.BulkSelectList[1] == prime_parent then - prime_parent = obj:GetRootPart() - end - for i,part in ipairs(pace.BulkSelectList) do - part:SetParent() - part:SetAffectChildrenOnly(false) - part:SetDestinationPart() - end - obj:SetParent(prime_parent) - for i,part in ipairs(pace.BulkSelectList) do - part:SetParent(obj) - end - end):SetIcon("icon16/clock_link.png") - menu:AddOption("reorganize into an ACO downward tower", function() - local parent = obj:GetParent() - local grandparent = obj:GetParent() - if parent.Parent then grandparent = parent:GetParent() end - - for i,part in ipairs(pace.BulkSelectList) do - part:SetAffectChildrenOnly(true) - part:SetDestinationPart() - part:SetParent(parent) - parent = part - end - pace.BulkSelectList[1]:SetParent(obj:GetParent()) - obj:SetParent(parent) - end):SetIcon("icon16/clock_link.png") - end - end - if obj.ClassName == "camera" then if not obj:IsHidden() then local remembered_view = {pace.ViewPos, pace.ViewAngles} @@ -4059,6 +4018,47 @@ function pace.AddClassSpecificPartMenuComponents(menu, obj) end end + do --event reorganization + local full_events = true + for i,v in ipairs(pace.BulkSelectList) do + if v.ClassName ~= "event" then full_events = false end + end + if #pace.BulkSelectList > 0 and full_events then + menu:AddOption("reorganize into a non-ACO pocket", function() + for i,part in ipairs(pace.BulkSelectList) do + part:SetParent(part:GetRootPart()) + end + local prime_parent = obj:GetParent() + if prime_parent.ClassName == "event" or pace.BulkSelectList[1] == prime_parent then + prime_parent = obj:GetRootPart() + end + for i,part in ipairs(pace.BulkSelectList) do + part:SetParent() + part:SetAffectChildrenOnly(false) + part:SetDestinationPart() + end + obj:SetParent(prime_parent) + for i,part in ipairs(pace.BulkSelectList) do + part:SetParent(obj) + end + end):SetIcon("icon16/clock_link.png") + menu:AddOption("reorganize into an ACO downward tower", function() + local parent = obj:GetParent() + local grandparent = obj:GetParent() + if parent.Parent then grandparent = parent:GetParent() end + + for i,part in ipairs(pace.BulkSelectList) do + part:SetAffectChildrenOnly(true) + part:SetDestinationPart() + part:SetParent(parent) + parent = part + end + pace.BulkSelectList[1]:SetParent(obj:GetParent()) + obj:SetParent(parent) + end):SetIcon("icon16/clock_link.png") + end + end + pace.AddQuickSetupsToPartMenu(menu, obj) end From 855064874220045cad332e3ace3d71901335003d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Tue, 4 Mar 2025 20:47:13 -0500 Subject: [PATCH 288/300] hotfix regarding the event arguments dynamic properties sync feature, I didn't account for the fact that properties can be edited from proxies --- lua/pac3/core/client/parts/event.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index c7bf0b110..9be37a281 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -244,6 +244,7 @@ function PART:SetProperty(key, val) info.set(val) if self:GetPlayerOwner() ~= pac.LocalPlayer then return end if pace.IsActive() then + if self ~= pace.current_part then return end self.pace_properties["Arguments"]:SetText(" " .. self.Arguments) self.pace_properties["Arguments"].original_str = self.Arguments end From fa93cd771b0c5539a38fa59892fe83a474e4c750 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 9 Mar 2025 04:40:05 -0400 Subject: [PATCH 289/300] small fix --- lua/pac3/core/client/parts/event.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 9be37a281..30d6165bb 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -255,6 +255,7 @@ end function PART:SetArguments(str) self.Arguments = str if pace.IsActive() and pac.LocalPlayer == self:GetPlayerOwner() then + if not self:GetShowInEditor() then return end pace.PopulateProperties(self) end end From c247ce7ff5b0bbc29363fb427bcfddba663a7c25 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sat, 15 Mar 2025 14:36:47 -0400 Subject: [PATCH 290/300] force part update split AffectSelf into two to distinguish between player owner and root owner (e.g. projectiles) for backward compatibility, AffectSelf will still act as both by default, but with AlternateAffectSelf the split occurs. you can then use AffectPlayerOwner to affect your player, and AffectSelf to affect the root part owner separately --- lua/pac3/core/client/parts/force.lua | 24 +++++++++++++++++------ lua/pac3/extra/shared/net_combat.lua | 29 ++++++++++++++++++---------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index ccd038c91..505d6993f 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -67,7 +67,9 @@ Radial gets the base directions from the targets to the force part]]}) :GetSet("DampingReverseFalloff", false, {description = "Whether the damping should fade with distance but reverse (closer is weaker influence)"}) :SetPropertyGroup("Targets") - :GetSet("AffectSelf",false) + :GetSet("AffectSelf", false, {description = "Affect Root Owner (i.e. it can be a projectile entity)"}) + :GetSet("AlternateAffectSelf", false, {description = "'Affect Self' was split into two. For backward compatibility reasons, this must be a setting.\nif this is off, affect self will affect both player owner and root owner, which may not be desired"}) + :GetSet("AffectPlayerOwner", false) :GetSet("Players",true) :GetSet("PhysicsProps", true) :GetSet("PointEntities",true, {description = "other entities not covered by physics props but with potential physics"}) @@ -85,6 +87,9 @@ end function PART:Initialize() self.next_impulse = CurTime() + 0.05 if not GetConVar("pac_sv_force"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("force parts are disabled on this server!") end + timer.Simple(0, function() + self.initialized = true + end) end function PART:OnShow() @@ -213,9 +218,6 @@ local function ProcessForcesList(ents_hits, tbl, pos, ang, ply) end for _,ent in pairs(ents_hits) do if ent:IsWeapon() or ent:GetClass() == "viewmodel" or ent:GetClass() == "func_physbox_multiplayer" then continue end - if ent:GetPos():Distance(ply:GetPos()) < 300 then - print(ent) - end local phys_ent local is_player = ent:IsPlayer() local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner()))) @@ -503,6 +505,8 @@ function PART:Impulse(on) if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end end + if not self.initialized then return end + local locus_pos = Vector(0,0,0) if self.Locus ~= nil then if self.Locus:IsValid() then @@ -518,7 +522,8 @@ function PART:Impulse(on) end end - if not self.NPC and not self.Players and not self.AffectSelf and not self.PhysicsProps and not self.PointEntities then return end + if not self.NPC and not self.Players and not self.AffectSelf and not self.AffectPlayerOwner and not self.PhysicsProps and not self.PointEntities then return end + net.Start("pac_request_force", true) net.WriteVector(self:GetWorldPosition()) net.WriteAngle(self:GetWorldAngles()) @@ -549,7 +554,14 @@ function PART:Impulse(on) net.WriteBool(self.DampingFalloff) net.WriteBool(self.DampingReverseFalloff) net.WriteBool(self.Levitation) - net.WriteBool(self.AffectSelf) + if self.AlternateAffectSelf then + net.WriteBool(self.AffectSelf) + net.WriteBool(self.AffectPlayerOwner) + else + net.WriteBool(self.AffectSelf) + net.WriteBool(self.AffectPlayerOwner or self.AffectSelf) + end + net.WriteBool(self.Players) net.WriteBool(self.PhysicsProps) net.WriteBool(self.PointEntities) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index 2f304076a..a098b34f0 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -1316,10 +1316,20 @@ if SERVER then if v.CPPICanPickup and not v:CPPICanPickup(ply) then ents_hits[i] = nil end if v.CPPICanPunt and not v:CPPICanPunt(ply) then ents_hits[i] = nil end if v:IsConstraint() then ents_hits[i] = nil end - if pre_excluded_ent_classes[v:GetClass()] or (Is_NPC(v) and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectSelf)) then ents_hits[i] = nil - else ent_count = ent_count + 1 end + + if v == ply then + if not tbl.AffectPlayerOwner then ents_hits[i] = nil end + elseif v == tbl.RootPartOwner then + if (not tbl.AffectSelf) and v == tbl.RootPartOwner then ents_hits[i] = nil end + end + + if pre_excluded_ent_classes[v:GetClass()] or (Is_NPC(v) and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectPlayerOwner)) then ents_hits[i] = nil + end + if ents_hits[i] ~= nil then + ent_count = ent_count + 1 + end end - if TooManyEnts(ent_count, ply) and not (tbl.AffectSelf and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end + if TooManyEnts(ent_count, ply) and not ((tbl.AffectSelf or tbl.AffectPlayerOwner) and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end for _,ent in pairs(ents_hits) do local phys_ent local ent_getphysobj = ent:GetPhysicsObject() @@ -1327,16 +1337,14 @@ if SERVER then local is_player = ent:IsPlayer() local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner()))) local is_npc = Is_NPC(ent) - - - if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner)) + if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner) or (tbl.AffectPlayerOwner and ent == ply)) and ( is_player or is_npc or is_physics or IsValid( ent_getphysobj ) ) then - + local is_phys = true if ent_getphysobj ~= nil then phys_ent = ent_getphysobj @@ -1465,8 +1473,8 @@ if SERVER then local unconsenting_owner = owner ~= ply and force_consents[owner] == false if is_player then - if tbl.Players or (ent == ply and tbl.AffectSelf) then - if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectSelf) then + if tbl.Players or (ent == ply and tbl.AffectPlayerOwner) then + if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectPlayerOwner) then oldvel = ent:GetVelocity() phys_ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) ent:SetVelocity(oldvel * (-1 + final_damping) + addvel) @@ -1513,7 +1521,7 @@ if SERVER then end end - elseif tbl.PointEntities then + elseif tbl.PointEntities or (tbl.AffectSelf and ent == tbl.RootPartOwner) then if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then phys_ent:SetVelocity(final_damping * oldvel + addvel) end @@ -1757,6 +1765,7 @@ if SERVER then tbl.DampingReverseFalloff = net.ReadBool() tbl.Levitation = net.ReadBool() tbl.AffectSelf = net.ReadBool() + tbl.AffectPlayerOwner = net.ReadBool() tbl.Players = net.ReadBool() tbl.PhysicsProps = net.ReadBool() tbl.PointEntities = net.ReadBool() From 14d79917eae283c68fb02246d3cef3c355fdcb28 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 17 Mar 2025 14:58:05 -0400 Subject: [PATCH 291/300] move active text entry alongside property list's scrolling it won't go off-screen or above the property divider accordingly, draw a rectangle with a text and arrows to indicate what property you're editing if out of bounds fix a narrow bug where the pace.PanelExists check interprets a "Container" bodygroup as the property container panel class. it created an error and dropped all further bodygroups --- .../editor/client/panels/extra_properties.lua | 4 +- lua/pac3/editor/client/panels/properties.lua | 49 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index b658023a9..de6c5e955 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -1256,7 +1256,9 @@ do -- script proxy local old = pnl.Paint pnl.Paint = function(...) if not self:IsValid() then pnl:Remove() return end - + local x, y = self:LocalToScreen() + y = math.Clamp(y,0,ScrH() - self:GetTall()) + pnl:SetPos(x + 5 + inset_x, y) surface.SetFont(pnl:GetFont()) local w = surface.GetTextSize(pnl:GetText()) + 6 diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index ea3e6bd25..9e826b143 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -839,7 +839,11 @@ do -- list if prop.udata and prop.udata.editor_panel then T = prop.udata.editor_panel or T elseif pace.PanelExists("properties_" .. prop.key:lower()) then - T = prop.key:lower() + --is it code bloat to fix weird edge cases like bodygroups on specific models??? + --idk but it's more egregious to allow errors just because of what bodygroups the model has + if prop.key:lower() ~= "container" then + T = prop.key:lower() + end elseif not pace.PanelExists("properties_" .. T) then T = "string" end @@ -2072,6 +2076,7 @@ do -- base editable local hookID = tostring({}) local textEntry = pnl local delay = os.clock() + 0.1 + 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 @@ -2090,7 +2095,6 @@ do -- base editable --pnl:SetPos(x+3,y-4) --pnl:Dock(FILL) local x, y = self:LocalToScreen() - local inset_x = self:GetTextInset() pnl:SetPos(x+5 + inset_x, y) pnl:SetSize(self:GetSize()) pnl:SetWide(ScrW()) @@ -2109,6 +2113,11 @@ do -- base editable local old = pnl.Paint pnl.Paint = function(...) if not self:IsValid() then pnl:Remove() return end + local x, y = self:LocalToScreen() + local _,prop_y = pace.properties:LocalToScreen(0,0) + y = math.Clamp(y,prop_y,ScrH() - self:GetTall()) + + pnl:SetPos(x + 5 + inset_x, y) surface.SetFont(pnl:GetFont()) local w = surface.GetTextSize(pnl:GetText()) + 6 @@ -2122,6 +2131,35 @@ do -- base editable old(...) end + local skincolor = self:GetSkin().Colours.Category.Line.Button + local col = Color(skincolor.r,skincolor.g,skincolor.b, 255) + + --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 + local _,prop_y = pace.properties:LocalToScreen(0,0) + local x, y = self:LocalToScreen() + local overflow = y < prop_y or y > ScrH() - self:GetTall() + if overflow then + local str = "" + if y > ScrH() then + "↓↓ " .. self.CurrentKey .. " ↓↓" + else + "↑↑ " .. self.CurrentKey .. " ↑↑" + end + local container = self:GetParent() + y = math.Clamp(y,prop_y,ScrH() - self:GetTall()) + surface.SetFont(pnl:GetFont()) + local w2 = surface.GetTextSize(str) + + surface.SetDrawColor(col) + surface.DrawRect(x - w2, y, w2, pnl:GetTall()) + surface.SetTextColor(self:GetSkin().Colours.Category.Line.Text) + surface.SetTextPos(x - w2, y) + surface.DrawText(str) + end + end) + pace.BusyWithProperties = pnl end @@ -2197,6 +2235,13 @@ do -- vector local left = pace.CreatePanel("properties_number", self) local middle = pace.CreatePanel("properties_number", self) local right = pace.CreatePanel("properties_number", self) + --a hack so that the scrolling out-of-bounds indicator rectangle with arrows has the key + timer.Simple(0, function() + if not IsValid(left) then return end + left.CurrentKey = self.CurrentKey + middle.CurrentKey = self.CurrentKey + right.CurrentKey = self.CurrentKey + end) left.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end middle.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end From e7de05b68dbe920961bf641c9ab7a721c4434724 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 17 Mar 2025 15:17:05 -0400 Subject: [PATCH 292/300] revert previous commit I'll investigate more later --- .../editor/client/panels/extra_properties.lua | 4 +- lua/pac3/editor/client/panels/properties.lua | 49 +------------------ 2 files changed, 3 insertions(+), 50 deletions(-) diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index de6c5e955..b658023a9 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -1256,9 +1256,7 @@ do -- script proxy local old = pnl.Paint pnl.Paint = function(...) if not self:IsValid() then pnl:Remove() return end - local x, y = self:LocalToScreen() - y = math.Clamp(y,0,ScrH() - self:GetTall()) - pnl:SetPos(x + 5 + inset_x, y) + surface.SetFont(pnl:GetFont()) local w = surface.GetTextSize(pnl:GetText()) + 6 diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index 9e826b143..ea3e6bd25 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -839,11 +839,7 @@ do -- list if prop.udata and prop.udata.editor_panel then T = prop.udata.editor_panel or T elseif pace.PanelExists("properties_" .. prop.key:lower()) then - --is it code bloat to fix weird edge cases like bodygroups on specific models??? - --idk but it's more egregious to allow errors just because of what bodygroups the model has - if prop.key:lower() ~= "container" then - T = prop.key:lower() - end + T = prop.key:lower() elseif not pace.PanelExists("properties_" .. T) then T = "string" end @@ -2076,7 +2072,6 @@ do -- base editable local hookID = tostring({}) local textEntry = pnl local delay = os.clock() + 0.1 - 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 @@ -2095,6 +2090,7 @@ do -- base editable --pnl:SetPos(x+3,y-4) --pnl:Dock(FILL) local x, y = self:LocalToScreen() + local inset_x = self:GetTextInset() pnl:SetPos(x+5 + inset_x, y) pnl:SetSize(self:GetSize()) pnl:SetWide(ScrW()) @@ -2113,11 +2109,6 @@ do -- base editable local old = pnl.Paint pnl.Paint = function(...) if not self:IsValid() then pnl:Remove() return end - local x, y = self:LocalToScreen() - local _,prop_y = pace.properties:LocalToScreen(0,0) - y = math.Clamp(y,prop_y,ScrH() - self:GetTall()) - - pnl:SetPos(x + 5 + inset_x, y) surface.SetFont(pnl:GetFont()) local w = surface.GetTextSize(pnl:GetText()) + 6 @@ -2131,35 +2122,6 @@ do -- base editable old(...) end - local skincolor = self:GetSkin().Colours.Category.Line.Button - local col = Color(skincolor.r,skincolor.g,skincolor.b, 255) - - --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 - local _,prop_y = pace.properties:LocalToScreen(0,0) - local x, y = self:LocalToScreen() - local overflow = y < prop_y or y > ScrH() - self:GetTall() - if overflow then - local str = "" - if y > ScrH() then - "↓↓ " .. self.CurrentKey .. " ↓↓" - else - "↑↑ " .. self.CurrentKey .. " ↑↑" - end - local container = self:GetParent() - y = math.Clamp(y,prop_y,ScrH() - self:GetTall()) - surface.SetFont(pnl:GetFont()) - local w2 = surface.GetTextSize(str) - - surface.SetDrawColor(col) - surface.DrawRect(x - w2, y, w2, pnl:GetTall()) - surface.SetTextColor(self:GetSkin().Colours.Category.Line.Text) - surface.SetTextPos(x - w2, y) - surface.DrawText(str) - end - end) - pace.BusyWithProperties = pnl end @@ -2235,13 +2197,6 @@ do -- vector local left = pace.CreatePanel("properties_number", self) local middle = pace.CreatePanel("properties_number", self) local right = pace.CreatePanel("properties_number", self) - --a hack so that the scrolling out-of-bounds indicator rectangle with arrows has the key - timer.Simple(0, function() - if not IsValid(left) then return end - left.CurrentKey = self.CurrentKey - middle.CurrentKey = self.CurrentKey - right.CurrentKey = self.CurrentKey - end) left.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end middle.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end From 5ad9beca15c13815583b5ee39645f23c0afb6072 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 17 Mar 2025 20:43:23 -0400 Subject: [PATCH 293/300] fix previous commit the parsing error was because these strings for the arrows and property key text at 2146 & 2148 weren't being assigned to the variable, free strings breaking syntax --- .../editor/client/panels/extra_properties.lua | 4 +- lua/pac3/editor/client/panels/properties.lua | 48 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua index b658023a9..de6c5e955 100644 --- a/lua/pac3/editor/client/panels/extra_properties.lua +++ b/lua/pac3/editor/client/panels/extra_properties.lua @@ -1256,7 +1256,9 @@ do -- script proxy local old = pnl.Paint pnl.Paint = function(...) if not self:IsValid() then pnl:Remove() return end - + local x, y = self:LocalToScreen() + y = math.Clamp(y,0,ScrH() - self:GetTall()) + pnl:SetPos(x + 5 + inset_x, y) surface.SetFont(pnl:GetFont()) local w = surface.GetTextSize(pnl:GetText()) + 6 diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua index ea3e6bd25..c57489c1a 100644 --- a/lua/pac3/editor/client/panels/properties.lua +++ b/lua/pac3/editor/client/panels/properties.lua @@ -839,7 +839,11 @@ do -- list if prop.udata and prop.udata.editor_panel then T = prop.udata.editor_panel or T elseif pace.PanelExists("properties_" .. prop.key:lower()) then - T = prop.key:lower() + --is it code bloat to fix weird edge cases like bodygroups on specific models??? + --idk but it's more egregious to allow errors just because of what bodygroups the model has + if prop.key:lower() ~= "container" then + T = prop.key:lower() + end elseif not pace.PanelExists("properties_" .. T) then T = "string" end @@ -2072,6 +2076,7 @@ do -- base editable local hookID = tostring({}) local textEntry = pnl local delay = os.clock() + 0.1 + 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 @@ -2090,7 +2095,6 @@ do -- base editable --pnl:SetPos(x+3,y-4) --pnl:Dock(FILL) local x, y = self:LocalToScreen() - local inset_x = self:GetTextInset() pnl:SetPos(x+5 + inset_x, y) pnl:SetSize(self:GetSize()) pnl:SetWide(ScrW()) @@ -2109,6 +2113,11 @@ do -- base editable local old = pnl.Paint pnl.Paint = function(...) if not self:IsValid() then pnl:Remove() return end + local x, y = self:LocalToScreen() + local _,prop_y = pace.properties:LocalToScreen(0,0) + y = math.Clamp(y,prop_y,ScrH() - self:GetTall()) + + pnl:SetPos(x + 5 + inset_x, y) surface.SetFont(pnl:GetFont()) local w = surface.GetTextSize(pnl:GetText()) + 6 @@ -2122,6 +2131,34 @@ do -- base editable old(...) end + local skincolor = self:GetSkin().Colours.Category.Line.Button + local col = Color(skincolor.r,skincolor.g,skincolor.b, 255) + + --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 + local _,prop_y = pace.properties:LocalToScreen(0,0) + local x, y = self:LocalToScreen() + local overflow = y < prop_y or y > ScrH() - self:GetTall() + if overflow then + local str = "" + if y > ScrH() then + str = "↓↓ " .. " " .. self.CurrentKey .. " " .. " ↓↓" + else + str = "↑↑ " .. " " .. self.CurrentKey .. " " .. " ↑↑" + end + y = math.Clamp(y,prop_y,ScrH() - self:GetTall()) + surface.SetFont(pnl:GetFont()) + local w2 = surface.GetTextSize(str) + + surface.SetDrawColor(col) + surface.DrawRect(x - w2, y, w2, pnl:GetTall()) + surface.SetTextColor(self:GetSkin().Colours.Category.Line.Text) + surface.SetTextPos(x - w2, y) + surface.DrawText(str) + end + end) + pace.BusyWithProperties = pnl end @@ -2197,6 +2234,13 @@ do -- vector local left = pace.CreatePanel("properties_number", self) local middle = pace.CreatePanel("properties_number", self) local right = pace.CreatePanel("properties_number", self) + --a hack so that the scrolling out-of-bounds indicator rectangle with arrows has the key + timer.Simple(0, function() + if not IsValid(left) then return end + left.CurrentKey = self.CurrentKey + middle.CurrentKey = self.CurrentKey + right.CurrentKey = self.CurrentKey + end) left.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end middle.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end From 79e2c3f140b055ebcc18815df2b0d33e8b240ca4 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 17 Mar 2025 23:20:24 -0400 Subject: [PATCH 294/300] buttons can bypass changes to their holdtime/toggle state updates if hidden and add more details to logic gates-based events' tutorial entries to clarify how to use uids --- lua/pac3/core/client/part_pool.lua | 3 +++ lua/pac3/core/client/parts/event.lua | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua index ae1f46c47..c2333916e 100644 --- a/lua/pac3/core/client/part_pool.lua +++ b/lua/pac3/core/client/part_pool.lua @@ -675,6 +675,9 @@ end function pac.UpdateButtonEvents(ply, key, down) local button_events = ply.pac_part_cache_button_events or {} for _,part in pairs(button_events) do + if part:GetProperty("ignore_if_hidden") then + if part:IsHidden() then continue end + end if key ~= string.Split(part.Arguments, "@@")[1]:lower() then continue end part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {} part.toggleimpulsekey = part.toggleimpulsekey or {} diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 30d6165bb..64a043152 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -2736,7 +2736,7 @@ PART.OldEvents = { or_gate = { operator_type = "none", preferred_operator = "find simple", - tutorial_explanation = "combines multiple events into an OR gate, the event will activate as soon as one of the events listed is activated (taking inverts into account)", + tutorial_explanation = "combines multiple events into an OR gate, the event will activate as soon as one of the events listed is activated (taking inverts into account).\n\nuids is a list (separated by semicolons) of part identifiers (UniqueIDs or names)\n\nAn easy way to gather them is to use bulk select (ctrl+click) and to right click back on the or_gate", arguments = {{uids = "string"}, {ignore_inverts = "boolean"}}, userdata = {{default = "", enums = function(part) local output = {} @@ -2772,7 +2772,7 @@ PART.OldEvents = { }, xor_gate = { operator_type = "none", preferred_operator = "find simple", - tutorial_explanation = "combines multiple events into an XOR gate, the event will activate if one (and only one) of the two events is activated (taking inverts into account)", + tutorial_explanation = "combines multiple events into an XOR gate, the event will activate if one (and only one) of the two events is activated (taking inverts into account).\n\nuid1 and uid2 are part identifiers (UniqueID or names) for events", arguments = {{uid1 = "string"},{uid2 = "string"}}, userdata = { {default = "", enums = function(part) @@ -2816,7 +2816,7 @@ PART.OldEvents = { }, and_gate = { operator_type = "none", preferred_operator = "find simple", - tutorial_explanation = "combines multiple events into an AND gate, the event will activate when all the events listed are activated (taking inverts into account)", + tutorial_explanation = "combines multiple events into an AND gate, the event will activate when all the events listed are activated (taking inverts into account)\n\nuids is a list (separated by semicolons) of part identifiers (UniqueIDs or names)\n\nAn easy way to gather them is to use bulk select (ctrl+click) and to right click back on the and_gate", arguments = {{uids = "string"}, {ignore_inverts = "boolean"}}, userdata = {{default = "", enums = function(part) local output = {} @@ -2965,7 +2965,7 @@ do PART.OldEvents.button = { operator_type = "none", - arguments = {{button = "string"}, {holdtime = "number"}, {toggle = "boolean"}}, + arguments = {{button = "string"}, {holdtime = "number"}, {toggle = "boolean"}, {ignore_if_hidden = "boolean"}}, userdata = {{enums = function() return enums end, default = "mouse_left"}, {default = 0}, {default = false}}, @@ -2988,7 +2988,7 @@ do return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")" end, - callback = function(self, ent, button, holdtime, toggle) + callback = function(self, ent, button, holdtime, toggle, ignore_if_hidden) self.holdtime = holdtime or 0 local toggle = toggle or false self.togglestate = self.togglestate or false From 3250394101e40ae37db6e7ac68ba94c1a28f8d11 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 23 Mar 2025 14:04:11 -0400 Subject: [PATCH 295/300] more health modifier cvars clamp max HP / max armor changes allow/disallow extra healthbars counted hits mode clamp total value of extra healthbars (it accounts for existing ones applied. can't cheat by adding multiple health modifier parts) health modifier part gets a nicename and harmonize some more of the other new convars (the settings manager code required a server-realm copy of the descriptions of the cvars to get proper updates) --- .../core/client/parts/health_modifier.lua | 36 +++++++++++++++++++ lua/pac3/editor/client/settings.lua | 3 ++ lua/pac3/editor/client/spawnmenu.lua | 11 ++++-- .../editor/server/pac_settings_manager.lua | 10 ++++-- lua/pac3/extra/shared/net_combat.lua | 27 ++++++++++++-- 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua index e9dfbefdf..7d6a69434 100644 --- a/lua/pac3/core/client/parts/health_modifier.lua +++ b/lua/pac3/core/client/parts/health_modifier.lua @@ -43,6 +43,42 @@ local function register_UID(self, str, ply) pac.healthmod_part_UID_caches[ply][str] = self end +function PART:GetNiceName() + ply = self:GetPlayerOwner() + local str = "health_modifier" + + if self.DamageMultiplier ~= 1 then + str = str .. " [dmg " .. self.DamageMultiplier .. "x]" + end + + if self.ChangeHealth then + if ply:Health() ~= self.MaxHealth then + str = str .. " [" .. ply:Health() .. " / " .. self.MaxHealth .. " health]" + else + str = str .. " [" .. self.MaxHealth .. " health]" + end + end + + if self.ChangeArmor then + if ply:Armor() ~= self.MaxArmor then + str = str .. " [" .. ply:Armor() .. " / " .. self.MaxArmor .. " armor]" + else + str = str .. " [" .. self.MaxArmor .. " armor]" + end + end + + if ply.pac_healthbars_uidtotals then + if ply.pac_healthbars_uidtotals[self.UniqueID] then + if self.HealthBars == 1 then + str = str .. " [" .. ply.pac_healthbars_uidtotals[self.UniqueID] .. " / " .. self.BarsAmount .. " EX]" + elseif self.HealthBars >= 1 then + str = str .. " [" .. ply.pac_healthbars_uidtotals[self.UniqueID] .. " EX (" .. (self.healthbar_index or "0") .. " / " .. self.HealthBars .. ")]" + end + end + end + return str +end + function PART:SendModifier(str) --pac.healthmod_part_UID_caches[string.sub(self.UniqueID,1,8)] = self register_UID(self, string.sub(self.UniqueID,1,8), self:GetPlayerOwner()) diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua index b2db53375..e2629afe2 100644 --- a/lua/pac3/editor/client/settings.lua +++ b/lua/pac3/editor/client/settings.lua @@ -60,8 +60,11 @@ local convar_params_projectile = { local convar_params_health_modifier = { {"pac_sv_health_modifier", "Allow health modifier part", "", -1, 0, 200}, {"pac_sv_health_modifier_allow_maxhp", "Allow changing max health and max armor", "", -1, 0, 200}, + {"pac_sv_health_modifier_max_hp_armor", "Maximum value for max health / armor modification", "", 0, 0, 100000000}, {"pac_sv_health_modifier_min_damagescaling", "Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.", "", 2, -10, 1}, {"pac_sv_health_modifier_extra_bars", "Allow extra healthbars", "What are those? It's like an armor layer that takes damage before it gets applied to the entity.", -1, 0, 200}, + {"pac_sv_health_modifier_allow_counted_hits", "Allow extra healthbars counted hits mode", "1 EX HP absorbs 1 whole hit.", -1, 0, 200}, + {"pac_sv_health_modifier_max_extra_bars_value", "Maximum combined value for extra healthbars", "", 0, 0, 100000000}, } local convar_params_modifiers = { diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua index 2199a8308..838809eba 100644 --- a/lua/pac3/editor/client/spawnmenu.lua +++ b/lua/pac3/editor/client/spawnmenu.lua @@ -161,6 +161,7 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Allow entity model modifier", "pac_modifier_model") self:CheckBox(L"Allow entity size modifier", "pac_modifier_size") self:CheckBox(L"Allow blood color modifier", "pac_allow_blood_color") + self:NumSlider(L"Allow prop / other player outfits", "pac_sv_prop_outfits", 0, 2, 0) self:Help(""):SetFont("DermaDefaultBold")--spacers self:Help(""):SetFont("DermaDefaultBold") @@ -182,6 +183,7 @@ function pace.AdminSettingsMenu(self) self:NumSlider(L"Max radius", "pac_sv_damage_zone_max_radius", 0, 32767, 0) self:NumSlider(L"Max length", "pac_sv_damage_zone_max_length", 0, 32767, 0) self:CheckBox(L"Enable damage zone dissolve", "pac_sv_damage_zone_allow_dissolve") + self:CheckBox(L"Enable ragdoll hitparts", "pac_sv_damage_zone_allow_ragdoll_hitparts") self:Help(L"Hitscan"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable hitscan part", "pac_sv_hitscan") @@ -193,19 +195,22 @@ function pace.AdminSettingsMenu(self) self:CheckBox(L"Allow grab", "pac_sv_lock_grab") self:CheckBox(L"Allow teleport", "pac_sv_lock_teleport") self:CheckBox(L"Allow aiming", "pac_sv_lock_aim") - + self:Help(L"Force part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable force part", "pac_sv_force") self:NumSlider(L"Max amount", "pac_sv_force_max_amount", 0, 10000000, 0) self:NumSlider(L"Max radius", "pac_sv_force_max_radius", 0, 32767, 0) self:NumSlider(L"Max length", "pac_sv_force_max_length", 0, 32767, 0) - self:Help(L"Health Modifier part"):SetFont("DermaDefaultBold") + self:Help(L"Health Modifier"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable health modifier", "pac_sv_health_modifier") self:CheckBox(L"Allow changing max health or armor", "pac_sv_health_modifier_allow_maxhp") + self:NumSlider(L"Maximum modified health or armor", "pac_sv_health_modifier_max_hp_armor", 0, 100000000, 0) self:NumSlider(L"Minimum combined damage scaling", "pac_sv_health_modifier_min_damagescaling", -10, 1, 2) self:CheckBox(L"Allow extra health bars", "pac_sv_health_modifier_extra_bars") - + self:CheckBox(L"Allow counted hits mode", "pac_sv_health_modifier_allow_counted_hits") + self:NumSlider(L"Maximum combined extra health value", "pac_sv_health_modifier_max_extra_bars_value", 0, 100000000, 0) + self:Help(L"Projectile part"):SetFont("DermaDefaultBold") self:CheckBox(L"Enable physical projectiles", "pac_sv_projectiles") self:CheckBox(L"Enable custom collide meshes for physical projectiles", "pac_sv_projectile_allow_custom_collision_mesh") diff --git a/lua/pac3/editor/server/pac_settings_manager.lua b/lua/pac3/editor/server/pac_settings_manager.lua index d9da6e2be..900cd63bd 100644 --- a/lua/pac3/editor/server/pac_settings_manager.lua +++ b/lua/pac3/editor/server/pac_settings_manager.lua @@ -18,6 +18,7 @@ local pac_server_cvars = { {"pac_sv_lock", "Allow lock part", "", -1, 0, 200}, {"pac_sv_lock_teleport", "Allow lock part teleportation", "", -1, 0, 200}, {"pac_sv_lock_grab", "Allow lock part grabbing", "", -1, 0, 200}, + {"pac_sv_lock_aim", "Allow lock part aiming", "", -1, 0, 200}, {"pac_sv_lock_allow_grab_ply", "Allow grabbing players", "", -1, 0, 200}, {"pac_sv_lock_allow_grab_npc", "Allow grabbing NPCs", "", -1, 0, 200}, {"pac_sv_lock_allow_grab_ent", "Allow grabbing other entities", "", -1, 0, 200}, @@ -28,11 +29,12 @@ local pac_server_cvars = { {"pac_sv_damage_zone_max_length", "Max damage zone length", "", 0, 0, 32767}, {"pac_sv_damage_zone_max_damage", "Max damage zone damage", "", 0, 0, 268435455}, {"pac_sv_damage_zone_allow_dissolve", "Allow damage entity dissolvers", "", -1, 0, 200}, + {"pac_sv_damage_zone_allow_ragdoll_hitparts", "Allow ragdoll hitparts", "", -1, 0, 200}, {"pac_sv_force", "Allow force part", "", -1, 0, 200}, {"pac_sv_force_max_radius", "Max force radius", "", 0, 0, 32767}, {"pac_sv_force_max_length", "Max force length", "", 0, 0, 32767}, - {"pac_sv_force_max_length", "Max force amount", "", 0, 0, 10000000}, + {"pac_sv_force_max_amount", "Max force amount", "", 0, 0, 10000000}, {"pac_sv_hitscan", "allow serverside bullets", "", -1, 0, 200}, {"pac_sv_hitscan_max_damage", "Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)", "", 0, 0, 268435455}, @@ -50,8 +52,11 @@ local pac_server_cvars = { {"pac_sv_health_modifier", "Allow health modifier part", "", -1, 0, 200}, {"pac_sv_health_modifier_allow_maxhp", "Allow changing max health and max armor", "", -1, 0, 200}, + {"pac_sv_health_modifier_max_hp_armor", "Maximum value for max health / armor modification", "", 0, 0, 100000000}, {"pac_sv_health_modifier_min_damagescaling", "Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.", "", 2, -10, 1}, {"pac_sv_health_modifier_extra_bars", "Allow extra healthbars", "What are those? It's like an armor layer that takes damage before it gets applied to the entity.", -1, 0, 200}, + {"pac_sv_health_modifier_allow_counted_hits", "Allow extra healthbars counted hits mode", "1 EX HP absorbs 1 whole hit.", -1, 0, 200}, + {"pac_sv_health_modifier_max_extra_bars_value", "Maximum combined value for extra healthbars", "", 0, 0, 100000000}, {"pac_modifier_blood_color", "Blood", "", -1, 0, 200}, @@ -70,6 +75,7 @@ local pac_server_cvars = { {"pac_submit_spam", "Limit pac_submit to prevent spam", "", -1, 0, 200}, {"pac_submit_limit", "limit of pac_submits", "", 0, 0, 100}, {"pac_onuse_only_force", "Players need to +USE on others to reveal outfits", "", -1, 0, 200}, + {"pac_sv_prop_outfits", "allow prop / other player outfits", "0 = don't allow\n1 = allow applying outfits on props/npcs\n2 = allow applying outfits on other players", 0, 0, 2}, {"sv_pac_webcontent_allow_no_content_length", "Players need to +USE on others to reveal outfits", "", -1, 0, 200}, {"pac_to_contraption_allow", "Allow PAC to contraption tool", "", -1, 0, 200}, @@ -102,5 +108,5 @@ net.Receive("pac_request_sv_cvars", function (len, ply) net.WriteTable(cvars_tbl) net.Send(ply) end) - + end) diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua index a098b34f0..663e7e0f3 100644 --- a/lua/pac3/extra/shared/net_combat.lua +++ b/lua/pac3/extra/shared/net_combat.lua @@ -44,6 +44,9 @@ local healthmod_allow = CreateConVar("pac_sv_health_modifier", master_default, C local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars") local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.") local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.") +local healthmod_allowed_counted_hits = CreateConVar("pac_sv_health_modifier_allow_counted_hits", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars counted hits mode (one hit = 1 HP)") +local healthmod_max_value = CreateConVar("pac_sv_health_modifier_max_hp_armor", 1000000, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "health modifier maximum value for health and armor") +local healthmod_max_extra_bars_value = CreateConVar("pac_sv_health_modifier_max_extra_bars_value", 1000000, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "health modifier maximum value for extra health bars (bars x amount)") local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on") cvars.AddChangeCallback("pac_sv_block_combat_features_on_next_restart", function() print("Remember that pac_sv_block_combat_features_on_next_restart is applied on server startup! Only do it if you know what you're doing. You'll need to restart the server.") end) @@ -649,8 +652,9 @@ if SERVER then end end - local function GatherExtraHPBars(ply) + local function GatherExtraHPBars(ply, filter) if ply.pac_healthbars == nil then return 0,nil end + local built_tbl = {} local total_hp_value = 0 @@ -658,6 +662,7 @@ if SERVER then built_tbl[layer] = {} local layer_total = 0 for uid,value in pairs(tbl) do + if uid == filter then continue end built_tbl[layer][uid] = value total_hp_value = total_hp_value + value layer_total = layer_total + value @@ -2529,6 +2534,7 @@ if SERVER then if action == "MaxHealth" then if not healthmod_allow:GetBool() then return end local num = net.ReadUInt(32) + num = math.Clamp(num,0,healthmod_max_value:GetInt()) local follow = net.ReadBool() if not healthmod_allow_change_maxhp:GetBool() then return end if ply:Health() == ply:GetMaxHealth() and follow then @@ -2544,6 +2550,7 @@ if SERVER then elseif action == "MaxArmor" then if not healthmod_allow:GetBool() then return end local num = net.ReadUInt(32) + num = math.Clamp(num,0,healthmod_max_value:GetInt()) local follow = net.ReadBool() if not healthmod_allow_change_maxhp:GetBool() then return end if ply:Armor() == ply:GetMaxArmor() and follow then @@ -2570,7 +2577,23 @@ if SERVER then local counted_hits = net.ReadBool() local no_overflow = net.ReadBool() - UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow, counted_hits, no_overflow) + if counted_hits and not healthmod_allowed_counted_hits:GetBool() then return end + + local requested_amount = num * barsize + + local current_bars_amount_without_this = GatherExtraHPBars(ply, part_uid) + local allowed_amount_without_this = healthmod_max_extra_bars_value:GetInt() - current_bars_amount_without_this + + if requested_amount >= allowed_amount_without_this then + requested_amount = math.Clamp(requested_amount,0,allowed_amount_without_this) + + barsize = math.floor(requested_amount / num) + num = math.floor(requested_amount / barsize) + + UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow, counted_hits, no_overflow) + else + UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow, counted_hits, no_overflow) + end elseif action == "OnRemove" then if ply.pac_damage_scalings then From a7589134c8e7967d5cbd7bdc15e229f2fb2b9c1d Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Wed, 26 Mar 2025 00:11:49 -0400 Subject: [PATCH 296/300] mini fix create this field on part initialization --- lua/pac3/core/client/parts/event.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 64a043152..991552d5b 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -261,6 +261,7 @@ function PART:SetArguments(str) end function PART:Initialize() + self.showtime = 0 self.found_cached_parts = {} self.specialtrackedparts = {} self.ExtraHermites = {} From b0d629d9d310280ee8fdc823918074e0b4263f49 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Thu, 3 Apr 2025 21:32:00 -0400 Subject: [PATCH 297/300] doubleclick actions some actions for base_class: rename, expand/collapse, show/hide, write notes some are class-specific: hitscan shoot, sound play/stop, command execute, event invert... some are event-specific: timerx reset, command trigger, flashlight toggle you can revert to only the base_class actions or ignore the base_class actions and only trigger actions when a specific action exists for the part the options are in menu bar and node expander --- lua/pac3/core/client/base_part.lua | 129 ++++++++++++++++++++ lua/pac3/core/client/parts/command.lua | 4 + lua/pac3/core/client/parts/damage_zone.lua | 6 + lua/pac3/core/client/parts/event.lua | 49 +++++++- lua/pac3/core/client/parts/force.lua | 6 + lua/pac3/core/client/parts/hitscan.lua | 6 + lua/pac3/core/client/parts/legacy/sound.lua | 12 ++ lua/pac3/core/client/parts/lock.lua | 7 ++ lua/pac3/core/client/parts/projectile.lua | 6 + lua/pac3/core/client/parts/sound.lua | 11 ++ lua/pac3/editor/client/menu_bar.lua | 18 +++ lua/pac3/editor/client/panels/pac_tree.lua | 5 +- lua/pac3/editor/client/panels/tree.lua | 19 +++ 13 files changed, 276 insertions(+), 2 deletions(-) diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua index c5434b78b..520e47d97 100644 --- a/lua/pac3/core/client/base_part.lua +++ b/lua/pac3/core/client/base_part.lua @@ -606,6 +606,135 @@ do -- scene graph end end + local pac_doubleclick_type = CreateClientConVar("pac_doubleclick_action", "expand", true, true, "What function should be run if you double-click on a part node.\n\nexpand : expand or collapse the node\nrename : rename the part\nnotes : write notes for the part\nshowhide : shows or hides the part\nspecific_only : only trigger class-specific actions, such as playing sounds, triggering hitscans, etc.\nnone : disable double-click actions") + + function PART:OnDoubleClickBaseClass() + pace.doubleclickfunc = pac_doubleclick_type:GetString() + if pace.doubleclickfunc == "specific_only" then return end + + pace.FlashNotification("double click action : " .. pace.doubleclickfunc) + + if pace.doubleclickfunc == "expand" then + if not self:HasChildren() then return end + self:SetEditorExpand(not self:GetEditorExpand()) + pace.RefreshTree() + elseif pace.doubleclickfunc == "rename" then + local pnl = vgui.Create("DTextEntry") + pnl:SetFont(pace.CurrentFont) + pnl:SetDrawBackground(false) + pnl:SetDrawBorder(false) + pnl:SetText(self:GetName()) + pnl:SetKeyboardInputEnabled(true) + pnl:RequestFocus() + pnl:SelectAllOnFocus(true) + + local hookID = tostring({}) + local textEntry = pnl + local delay = os.clock() + 0.3 + + local old_name = self:GetName() + + pac.AddHook('Think', hookID, function(code) + if not IsValid(textEntry) then return pac.RemoveHook('Think', hookID) end + if textEntry: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 + pac.RemoveHook('Think', hookID) + textEntry:Remove() + pnl:OnEnter() + end) + + --local x,y = pnl:GetPos() + --pnl:SetPos(x+3,y-4) + --pnl:Dock(FILL) + local x, y = self.pace_tree_node.Label:LocalToScreen() + local inset_x = self.pace_tree_node.Label:GetTextInset() + pnl:SetPos(x + inset_x, y) + pnl:SetSize(self.pace_tree_node.Label:GetSize()) + pnl:SetWide(ScrW()) + pnl:MakePopup() + + pnl.OnEnter = function() + local input_text = pnl:GetText() + pnl:Remove() + if old_name == input_text then return end + self:SetName(input_text) + if self.pace_tree_node then + if input_text ~= "" then + self.pace_tree_node:SetText(input_text) + else + timer.Simple(0, function() + self.pace_tree_node:SetText(self:GetName()) + end) + end + end + pace.PopulateProperties(self) + end + + local old = pnl.Paint + pnl.Paint = function(...) + if not self:IsValid() then pnl:Remove() return end + + surface.SetFont(pnl:GetFont()) + local w = surface.GetTextSize(pnl:GetText()) + 6 + + surface.DrawRect(0, 0, w, pnl:GetTall()) + surface.SetDrawColor(self.pace_tree_node:GetSkin().Colours.Properties.Border) + surface.DrawOutlinedRect(0, 0, w, pnl:GetTall()) + + pnl:SetWide(w) + + old(...) + end + elseif pace.doubleclickfunc == "notes" then + if IsValid(pace.notes_pnl) then + pace.notes_pnl:Remove() + end + local pnl = vgui.Create("DFrame") + pace.notes_pnl = pnl + local DText = vgui.Create("DTextEntry", pnl) + local DButtonOK = vgui.Create("DButton", pnl) + DText:SetMaximumCharCount(50000) + + pnl:SetSize(1200,800) + pnl:SetTitle("Long text with newline support for Notes.") + pnl:Center() + DButtonOK:SetText("OK") + DButtonOK:SetSize(80,20) + DButtonOK:SetPos(500, 775) + DText:SetPos(5,25) + DText:SetSize(1190,700) + DText:SetMultiline(true) + DText:SetContentAlignment(7) + pnl:MakePopup() + DText:RequestFocus() + DText:SetText(self:GetNotes()) + + DButtonOK.DoClick = function() + self:SetNotes(DText:GetText()) + pace.RefreshTree(true) + pnl:Remove() + end + elseif pace.doubleclickfunc == "showhide" then + self:SetHide(not self:GetHide()) + end + end + + local pac_doubleclick_specified = CreateClientConVar("pac_doubleclick_action_specified", "2", true, true, "Whether the base_part functions for double-click should be replaced by specific functions when available.\n\nset to 0 : only use base_class actions (expand, rename, notes, showhide)\nset to 1 : Use specific actions. most single-shot parts will trigger (sounds play, commands run, hitscans fire etc.), and events will invert\nset to 2 : When appropriate, some event types will have even more specific actions. command events trigger or toggle (depending on the time), is_flashlight_on will toggle the flashlight, timerx events will reset\n\nIf your selected base action is none, These won't trigger.\n\nIf you only want specific actions, you may select specific_only in the pac_doubleclick_action command if you only want specifics") + function PART:OnDoubleClickSpecified() + --override + end + + function PART:DoDoubleClick() + pace.doubleclickfunc = pac_doubleclick_type:GetString() + if pace.doubleclickfunc == "none" or pace.doubleclickfunc == "" then return end + if pac_doubleclick_specified:GetInt() ~= 0 and self.ImplementsDoubleClickSpecified then + pace.FlashNotification("double click action : class-specific") + self:OnDoubleClickSpecified() + else + self:OnDoubleClickBaseClass() + end + end end do -- hidden / events diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua index 7f66f24b0..041af61fc 100644 --- a/lua/pac3/core/client/parts/command.lua +++ b/lua/pac3/core/client/parts/command.lua @@ -5,6 +5,8 @@ PART.ClassName = "command" PART.Group = "advanced" PART.Icon = "icon16/application_xp_terminal.png" +PART.ImplementsDoubleClickSpecified = true + BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") @@ -312,4 +314,6 @@ function PART:Execute(commandstring) end end +PART.OnDoubleClickSpecified = PART.Execute + BUILDER:Register() diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index e391d7351..e3e084963 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -6,6 +6,8 @@ PART.ClassName = "damage_zone" PART.Group = "combat" PART.Icon = "icon16/package.png" +PART.ImplementsDoubleClickSpecified = true + local renderhooks = { "PostDraw2DSkyBox", "PostDrawOpaqueRenderables", @@ -682,6 +684,10 @@ function PART:OnShow() end end +function PART:OnDoubleClickSpecified() + self:SendNetMessage() +end + local dmgzone_requesting_corpses = {} function PART:SetAttachPartsToTargetEntity(b) self.AttachPartsToTargetEntity = b diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua index 991552d5b..d8f6beb77 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -17,6 +17,8 @@ PART.ThinkTime = 0 PART.AlwaysThink = true PART.Icon = 'icon16/clock.png' +PART.ImplementsDoubleClickSpecified = true + BUILDER:StartStorableVars() BUILDER:GetSet("Event", "", {enums = function(part) local output = {} @@ -45,6 +47,36 @@ PART.Tutorials = {} local registered_command_event_series = {} local event_series_bounds = {} +function PART:OnDoubleClickSpecified() + if GetConVar("pac_doubleclick_specified"):GetInt() == 1 then + self:SetInvert(not self:GetInvert()) + pace.PopulateProperties(self) + return + end + + if self.Event == "command" then + local cmd, time, hide = self:GetParsedArgumentsForObject(self.Events.command) + if time == 0 then --toggling mode + pac.LocalPlayer.pac_command_events[cmd] = pac.LocalPlayer.pac_command_events[cmd] or {name = cmd, time = pac.RealTime, on = 0} + ----MORE PAC JANK?? SOMETIMES, THE 2 NOTATION DOESN'T CHANGE THE STATE YET + if pac.LocalPlayer.pac_command_events[cmd].on == 1 then + RunConsoleCommand("pac_event", cmd, "0") + else + RunConsoleCommand("pac_event", cmd, "1") + end + else + RunConsoleCommand("pac_event", cmd) + end + elseif self.Event == "is_flashlight_on" then + RunConsoleCommand("impulse", "100") + elseif self.Event == "timerx" or self.Event == "timerx2" then + self.time = nil + else + self:SetInvert(not self:GetInvert()) + pace.PopulateProperties(self) + end +end + function PART:register_command_event(str,b) local ply = self:GetPlayerOwner() @@ -3411,12 +3443,27 @@ function PART:GetParentEx() return self:GetParent() end +function PART:GetTargetingModePrefix() + local modes = {} + if self.AffectChildrenOnly then + table.insert(modes, "ACO") + end + if IsValid(self:GetDestinationPart()) then + table.insert(modes, "TP") + end + if self.MultiTargetPart then + table.insert(modes, "MTP") + end + if table.IsEmpty(modes) then return "" end + return "[" .. table.concat(modes, " ") .. "] " +end + function PART:GetNiceName() local event_name = self:GetEvent() if not PART.Events[event_name] then return "unknown event" end - return PART.Events[event_name]:GetNiceName(self, get_owner(self)) + return self:GetTargetingModePrefix() .. PART.Events[event_name]:GetNiceName(self, get_owner(self)) end local function is_hidden_by_something_else(part, ignored_part) diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 505d6993f..9ace6c4c7 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -7,6 +7,8 @@ PART.Icon = "icon16/database_go.png" PART.ManualDraw = true PART.HandleModifiersManually = true +PART.ImplementsDoubleClickSpecified = true + BUILDER:StartStorableVars() :SetPropertyGroup("AreaShape") :GetSet("HitboxMode", "Box", {enums = { @@ -97,6 +99,10 @@ function PART:OnShow() self:Impulse(true) end +function PART:OnDoubleClickSpecified() + self:Impulse(true) +end + function PART:OnHide() pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) self:Impulse(false) diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua index 78be2ff2d..111dc91f0 100644 --- a/lua/pac3/core/client/parts/hitscan.lua +++ b/lua/pac3/core/client/parts/hitscan.lua @@ -9,6 +9,8 @@ PART.ClassName = "hitscan" PART.Group = "combat" PART.Icon = "icon16/user_gray.png" +PART.ImplementsDoubleClickSpecified = true + BUILDER:StartStorableVars() :GetSet("ServerBullets", true, {description = "serverside bullets can do damage and exert a physical impact force"}) :SetPropertyGroup("bullet properties") @@ -130,6 +132,10 @@ function PART:Shoot() end end +function PART:OnDoubleClickSpecified() + self:Shoot() +end + --NOT THE ACTUAL DAMAGE TYPES. UNIQUE IDS TO COMPRESS NET MESSAGES local damage_ids = { diff --git a/lua/pac3/core/client/parts/legacy/sound.lua b/lua/pac3/core/client/parts/legacy/sound.lua index dce2c2024..166f1bee0 100644 --- a/lua/pac3/core/client/parts/legacy/sound.lua +++ b/lua/pac3/core/client/parts/legacy/sound.lua @@ -8,6 +8,8 @@ PART.ThinkTime = 0 PART.Group = 'effects' PART.Icon = 'icon16/sound.png' +PART.ImplementsDoubleClickSpecified = true + BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") BUILDER:GetSet("Sound", "") @@ -329,6 +331,16 @@ function PART:StopSound(force_stop) end end +function PART:OnDoubleClickSpecified() + if self.playing then + self:StopSound(true) + self.playing = false + else + self:PlaySound() + self.playing = true + end +end + local channels = { CHAN_AUTO = 0, diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua index 98b1ba369..2d428b208 100644 --- a/lua/pac3/core/client/parts/lock.lua +++ b/lua/pac3/core/client/parts/lock.lua @@ -40,6 +40,7 @@ PART.ClassName = "lock" PART.Group = "combat" PART.Icon = "icon16/lock.png" +PART.ImplementsDoubleClickSpecified = true BUILDER:StartStorableVars() :SetPropertyGroup("Behaviour") @@ -436,6 +437,12 @@ function PART:OnShow() end end +function PART:OnDoubleClickSpecified() + if self.Mode ~= "Grab" then + self:OnShow() + end +end + function PART:OnHide() pac.RemoveHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID) self.teleported = false diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua index d09a55c9c..5d461e46d 100644 --- a/lua/pac3/core/client/parts/projectile.lua +++ b/lua/pac3/core/client/parts/projectile.lua @@ -18,6 +18,8 @@ PART.ClassName = "projectile" PART.Group = {"advanced", "combat"} PART.Icon = "icon16/bomb.png" +PART.ImplementsDoubleClickSpecified = true + BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("Firing") BUILDER:GetSet("Speed", 1) @@ -446,6 +448,10 @@ function PART:Shoot(pos, ang, multi_projectile_count) end end +function PART:OnDoubleClickSpecified() + self:Shoot() +end + function PART:SetRadius(val) self.Radius = val local sv_dist = GetConVar("pac_sv_projectile_max_phys_radius"):GetInt() diff --git a/lua/pac3/core/client/parts/sound.lua b/lua/pac3/core/client/parts/sound.lua index 23ca2dccc..c60d49fec 100644 --- a/lua/pac3/core/client/parts/sound.lua +++ b/lua/pac3/core/client/parts/sound.lua @@ -8,6 +8,7 @@ PART.ClassName = "sound2" PART.Icon = 'icon16/music.png' PART.Group = 'effects' +PART.ImplementsDoubleClickSpecified = true BUILDER:StartStorableVars() BUILDER:SetPropertyGroup("generic") @@ -359,6 +360,16 @@ function PART:OnShow(from_rendering) self.stopsound = false end +function PART:OnDoubleClickSpecified() + if self.playing then + self:StopSound(true) + self.playing = false + else + self:PlaySound() + self.playing = true + end +end + function PART:OnHide() self:StopSound() end diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua index aa4bb1a57..c348eac3a 100644 --- a/lua/pac3/editor/client/menu_bar.lua +++ b/lua/pac3/editor/client/menu_bar.lua @@ -201,6 +201,24 @@ local function populate_options(menu) menu:AddCVar(L"remember divider position", "pac_editor_remember_divider_height", "1", "0") menu:AddCVar(L"remember editor width", "pac_editor_remember_width", "1", "0") + menu:AddSpacer() + + local menu1, pnl = menu:AddSubMenu(L"double click actions") pnl:SetIcon("icon16/cursor.png") + menu1.GetDeleteSelf = function() return false end + local menu2, pnl = menu1:AddSubMenu(L"generic") pnl:SetIcon("icon16/world.png") + menu2.GetDeleteSelf = function() return false end + menu2:AddOption("expand / collapse", function() RunConsoleCommand("pac_doubleclick_action", "expand") end):SetImage('icon16/arrow_down.png') + menu2:AddOption("rename", function() RunConsoleCommand("pac_doubleclick_action", "rename") end):SetImage('icon16/text_align_center.png') + menu2:AddOption("write notes", function() RunConsoleCommand("pac_doubleclick_action", "notes") end):SetImage('icon16/page_white_edit.png') + menu2:AddOption("show / hide", function() RunConsoleCommand("pac_doubleclick_action", "showhide") end):SetImage('icon16/clock_red.png') + menu2:AddOption("only when specifed actions exist", function() RunConsoleCommand("pac_doubleclick_action", "specific_only") end):SetImage('icon16/application_xp_terminal.png') + menu2:AddOption("none", function() RunConsoleCommand("pac_doubleclick_action", "none") end):SetImage('icon16/collision_off.png') + local menu2, pnl = menu1:AddSubMenu(L"specific") pnl:SetIcon("icon16/application_xp_terminal.png") + menu2.GetDeleteSelf = function() return false end + menu2:AddOption("use generic actions only", function() RunConsoleCommand("pac_doubleclick_action_specified", "0") end):SetImage('icon16/world.png') + menu2:AddOption("use specific actions when available", function() RunConsoleCommand("pac_doubleclick_action_specified", "1") end):SetImage('icon16/cog.png') + menu2:AddOption("use even more specific actions (events)", function() RunConsoleCommand("pac_doubleclick_action_specified", "2") end):SetImage('icon16/clock.png') + menu:AddCVar(L"ask before loading autoload", "pac_prompt_for_autoload", "1", "0") local prop_pac_load_mode, pnlpplm = menu:AddSubMenu("(singleplayer only) How to handle prop/npc outfits", function() end) diff --git a/lua/pac3/editor/client/panels/pac_tree.lua b/lua/pac3/editor/client/panels/pac_tree.lua index 994497cc0..7462b7d26 100644 --- a/lua/pac3/editor/client/panels/pac_tree.lua +++ b/lua/pac3/editor/client/panels/pac_tree.lua @@ -143,7 +143,10 @@ function PANEL:Init() self.Label = vgui.Create("pac_dtree_node_button", self) self.Label:SetDragParent(self) self.Label.DoClick = function() self:InternalDoClick() end - self.Label.DoDoubleClick = function() self:InternalDoClick() end + self.Label.DoDoubleClick = function() + self:InternalDoClick() + self.part:DoDoubleClick() + end self.Label.DoRightClick = function() self:InternalDoRightClick() end self.Label.DragHover = function(s, t) self:DragHover(t) end diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua index 8b2ae1673..1e6fe5fd1 100644 --- a/lua/pac3/editor/client/panels/tree.lua +++ b/lua/pac3/editor/client/panels/tree.lua @@ -380,6 +380,25 @@ local function install_expand(node) node.part:CallRecursive('SetEditorExpand', true) pace.RefreshTree(true) end):SetImage('icon16/arrow_down.png') + + menu:AddSpacer() + + local menu1, pnl = menu:AddSubMenu(L"double click actions") pnl:SetIcon("icon16/cursor.png") + menu1.GetDeleteSelf = function() return false end + local menu2, pnl = menu1:AddSubMenu(L"generic") pnl:SetIcon("icon16/world.png") + menu2.GetDeleteSelf = function() return false end + menu2:AddOption("expand / collapse", function() RunConsoleCommand("pac_doubleclick_action", "expand") end):SetImage('icon16/arrow_down.png') + menu2:AddOption("rename", function() RunConsoleCommand("pac_doubleclick_action", "rename") end):SetImage('icon16/text_align_center.png') + menu2:AddOption("write notes", function() RunConsoleCommand("pac_doubleclick_action", "notes") end):SetImage('icon16/page_white_edit.png') + menu2:AddOption("show / hide", function() RunConsoleCommand("pac_doubleclick_action", "showhide") end):SetImage('icon16/clock_red.png') + menu2:AddOption("only when specifed actions exist", function() RunConsoleCommand("pac_doubleclick_action", "specific_only") end):SetImage('icon16/application_xp_terminal.png') + menu2:AddOption("none", function() RunConsoleCommand("pac_doubleclick_action", "none") end):SetImage('icon16/collision_off.png') + local menu2, pnl = menu1:AddSubMenu(L"specific") pnl:SetIcon("icon16/application_xp_terminal.png") + menu2.GetDeleteSelf = function() return false end + menu2:AddOption("use generic actions only", function() RunConsoleCommand("pac_doubleclick_action_specified", "0") end):SetImage('icon16/world.png') + menu2:AddOption("use specific actions when available", function() RunConsoleCommand("pac_doubleclick_action_specified", "1") end):SetImage('icon16/cog.png') + menu2:AddOption("use even more specific actions (events)", function() RunConsoleCommand("pac_doubleclick_action_specified", "2") end):SetImage('icon16/clock.png') + end end end From 277de7c176ef8a35ccec3ee79ade3d46292e3dfa Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Sun, 13 Apr 2025 20:00:50 -0400 Subject: [PATCH 298/300] fix misspelled cvar --- 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 d8f6beb77..f7afde25a 100644 --- a/lua/pac3/core/client/parts/event.lua +++ b/lua/pac3/core/client/parts/event.lua @@ -48,7 +48,7 @@ local registered_command_event_series = {} local event_series_bounds = {} function PART:OnDoubleClickSpecified() - if GetConVar("pac_doubleclick_specified"):GetInt() == 1 then + if GetConVar("pac_doubleclick_action_specified"):GetInt() == 1 then self:SetInvert(not self:GetInvert()) pace.PopulateProperties(self) return From 36db0e44582da9a779f48519e22d6fd8fed205d0 Mon Sep 17 00:00:00 2001 From: pingu7867 Date: Mon, 14 Apr 2025 21:01:24 -0400 Subject: [PATCH 299/300] temporary fix I will rework my damagezone hitparts code with more consideration later --- lua/pac3/core/client/parts/damage_zone.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua index e3e084963..f54933f4e 100644 --- a/lua/pac3/core/client/parts/damage_zone.lua +++ b/lua/pac3/core/client/parts/damage_zone.lua @@ -792,8 +792,10 @@ net.Receive("pac_hit_results", function(len) if not recycle_hitmark:GetBool() then local ent = parent_ent + local cs_ent = false if not self.AttachPartsToTargetEntity then ent = pac.CreateEntity("models/props_junk/popcan01a.mdl") + cs_ent = true ent:SetNoDraw(true) ent:SetPos(pos) end @@ -805,9 +807,9 @@ net.Receive("pac_hit_results", function(len) ent.pac_hitmark_part:Remove() end - timer.Simple(0.5, function() - SafeRemoveEntity(ent) - end) + if cs_ent then + SafeRemoveEntityDelayed(ent, 0.5) + end end end) return From 209a1f7780bd323d9fe63791aa7a95867ef21cae Mon Sep 17 00:00:00 2001 From: thegrb93 Date: Tue, 15 Apr 2025 21:48:42 -0400 Subject: [PATCH 300/300] Improve net error handling (#1398) * Improve network error handling --- lua/pac3/editor/client/wear.lua | 34 ++++++++++++--------- lua/pac3/editor/server/wear.lua | 54 ++++++++++++++++----------------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua index 038b8123e..8a03a5d73 100644 --- a/lua/pac3/editor/client/wear.lua +++ b/lua/pac3/editor/client/wear.lua @@ -63,17 +63,10 @@ end do -- to server local function net_write_table(tbl) - local buffer = pac.StringStream() buffer:writeTable(tbl) - local data = buffer:getString() - local ok, err = pcall(net.WriteStream, data) - - if not ok then - return ok, err - end - + net.WriteStream(data) return #data end @@ -104,10 +97,11 @@ do -- to server net.Start("pac_submit") - local bytes, err = net_write_table(data) + local ok, bytes = pcall(net_write_table, data) - if not bytes then - pace.Notify(false, "unable to transfer data to server: " .. tostring(err or "too big"), pace.pac_show_uniqueid:GetBool() and string.format("%s (%s)", part:GetName(), part:GetPrintUniqueID()) or part:GetName()) + if not ok then + net.Abort() + pace.Notify(false, "unable to transfer data to server: " .. tostring(bytes or "too big"), pace.pac_show_uniqueid:GetBool() and string.format("%s (%s)", part:GetName(), part:GetPrintUniqueID()) or part:GetName()) return false end @@ -127,8 +121,9 @@ do -- to server end net.Start("pac_submit") - local ret, err = net_write_table(data) - if ret == nil then + local ok, err = pcall(net_write_table, data) + if not ok then + net.Abort() pace.Notify(false, "unable to transfer data to server: " .. tostring(err or "too big"), name) return false end @@ -236,6 +231,13 @@ end net.Receive("pac_submit", function() if not pac.IsEnabled() then return end + local owner = net.ReadEntity() + if owner:IsValid() and owner:IsPlayer() then + pac.Message("Receiving outfit from ", owner) + else + return + end + net.ReadStream(ply, function(data) if not data then pac.Message("message from server timed out") @@ -243,7 +245,11 @@ net.Receive("pac_submit", function() end local buffer = pac.StringStream(data) - local data = buffer:readTable() + local ok, data = pcall(buffer.readTable, buffer) + if not ok then + pac.Message("received invalid message from server!?") + return + end if type(data.owner) ~= "Player" or not data.owner:IsValid() then pac.Message("received message from server but owner is not valid!? typeof " .. type(data.owner) .. " || ", data.owner) diff --git a/lua/pac3/editor/server/wear.lua b/lua/pac3/editor/server/wear.lua index a003a5ab4..6a35acfe7 100644 --- a/lua/pac3/editor/server/wear.lua +++ b/lua/pac3/editor/server/wear.lua @@ -7,6 +7,7 @@ local isfunction = isfunction local ProtectedCall = ProtectedCall pace.StreamQueue = pace.StreamQueue or {} +pace.MaxStreamQueue = 32 -- Max queued outfits per player timer.Create("pac_check_stream_queue", 0.1, 0, function() local item = table.remove(pace.StreamQueue) @@ -46,17 +47,10 @@ local function make_copy(tbl, input) end local function net_write_table(tbl) - local buffer = pac.StringStream() buffer:writeTable(tbl) - local data = buffer:getString() - local ok, err = pcall(net.WriteStream, data) - - if not ok then - return ok, err - end - + net.WriteStream(data) return #data end @@ -272,17 +266,19 @@ function pace.SubmitPartNow(data, filter) local ret = pac.CallHook("SendData", players, data) if ret == nil then net.Start("pac_submit") - local bytes, err = net_write_table(data) + net.WriteEntity(owner) + local ok, err = pcall(net_write_table, data) - if not bytes then + if ok then + net.Send(players) + else + net.Abort() local errStr = tostring(err) ErrorNoHalt("[PAC3] Outfit broadcast failed for " .. tostring(owner) .. ": " .. errStr .. "\n") if owner and owner:IsValid() then owner:ChatPrint("[PAC3] ERROR: Could not broadcast your outfit: " .. errStr) end - else - net.Send(players) end end @@ -299,18 +295,22 @@ end -- Inserts the given part into the StreamQueue function pace.SubmitPart(data, filter, callback) - if istable(data.part) then - pac.dprint("queuing part %q from %s", data.part.self.Name, tostring(data.owner)) - table.insert(pace.StreamQueue, { - data = data, - filter = filter, - callback = callback - }) - - return "queue" + if not ((istable(data.part) or isstring(data.part)) and IsValid(data.owner)) then return end + local owner = data.owner + local count = 0 + for _, v in ipairs(pace.StreamQueue) do + if v.data.owner == owner then + if count == pace.MaxStreamQueue then return end + count = count + 1 + end end - return pace.SubmitPartNow(data, filter) + if data.part.self then pac.dprint("queuing part %q from %s", data.part.self.Name, tostring(data.owner)) end + table.insert(pace.StreamQueue, { + data = data, + filter = filter, + callback = callback + }) end -- Inserts the given part into the StreamQueue, and notifies when it completes @@ -402,15 +402,15 @@ pace.PCallNetReceive(net.Receive, "pac_submit", function(len, ply) return end local buffer = pac.StringStream(data) - pace.HandleReceivedData(ply, buffer:readTable()) + local ok,tbl = pcall(buffer.readTable, buffer) + if ok then + pace.HandleReceivedData(ply, tbl) + end end) end) function pace.ClearOutfit(ply) - local uid = pac.Hash(ply) - - pace.SubmitPart({part = "__ALL__", uid = pac.Hash(ply), owner = ply}) - pace.CallHook("RemoveOutfit", ply) + pace.RemovePart({part = "__ALL__", uid = pac.Hash(ply), owner = ply}) end function pace.RequestOutfits(ply)