diff --git a/lua/acf/compatibility/baseplate_convert_sv.lua b/lua/acf/compatibility/baseplate_convert_sv.lua new file mode 100644 index 000000000..cd775f4ab --- /dev/null +++ b/lua/acf/compatibility/baseplate_convert_sv.lua @@ -0,0 +1,65 @@ +local ACF = ACF + +local RecursiveEntityRemove +function RecursiveEntityRemove(ent, track) + track = track or {} + if track[ent] == true then return end + local constrained = constraint.GetAllConstrainedEntities(ent) + ent:Remove() + track[ent] = true + for k, _ in pairs(constrained) do + if k ~= ent then RecursiveEntityRemove(k, track) end + end +end + +function ACF.ConvertEntityToBaseplate(Player, Target) + if not IsValid(Target) then return end + + local Owner = Target:CPPIGetOwner() + if not IsValid(Owner) or Owner ~= Player then return end + + local PhysObj = Target:GetPhysicsObject() + if not IsValid(PhysObj) then return end + + if Target:GetClass() ~= "prop_physics" then return end + + local AMi, AMa = PhysObj:GetAABB() + local BoxSize = AMa - AMi + + -- Duplicate the entire thing + local Entities, Constraints = AdvDupe2.duplicator.Copy(Player, Target, {}, {}, Vector(0, 0, 0)) + + -- Find the baseplate + local Baseplate = Entities[Target:EntIndex()] + + -- Setup the dupe table to convert it to a baseplate + local w, l, t = BoxSize.x, BoxSize.y, BoxSize.z + Baseplate.Class = "acf_baseplate" + Baseplate.Width = w + Baseplate.Length = l + Baseplate.Thickness = t + + -- Delete everything now + for k, _ in pairs(Entities) do + local e = Entity(k) + if IsValid(e) then e:Remove() end + end + + -- Paste the stuff back to the dupe + local Ents = AdvDupe2.duplicator.Paste(Owner, Entities, Constraints, Vector(0, 0, 0), Angle(0, 0, 0), Vector(0, 0, 0), true) + -- Try to find the baseplate + local NewBaseplate + for _, v in pairs(Ents) do + if v:GetClass() == "acf_baseplate" and v:GetPos() == Baseplate.Pos then + NewBaseplate = v + break + end + end + + undo.Create("acf_baseplate") + undo.AddEntity(NewBaseplate) + undo.SetPlayer(Player) + undo.Finish() + + return NewBaseplate +end \ No newline at end of file diff --git a/lua/acf/core/classes/entities/registration.lua b/lua/acf/core/classes/entities/registration.lua index 2c11b50ba..c2a2d966f 100644 --- a/lua/acf/core/classes/entities/registration.lua +++ b/lua/acf/core/classes/entities/registration.lua @@ -20,9 +20,10 @@ local function GetEntityTable(Class) if not Data then Data = { - Lookup = {}, - Count = 0, - List = {}, + Lookup = {}, + Count = 0, + List = {}, + Restrictions = {} } Entries[Class] = Data @@ -55,11 +56,185 @@ local function AddArguments(Entity, Arguments) return List end +local ArgumentTypes = {} + +local function AddArgumentRestrictions(Entity, ArgumentRestrictions) + local Restrictions = Entity.Restrictions + + for k, v in pairs(ArgumentRestrictions) do + if not v.Type then error("Argument '" .. tostring(k or "") .. "' didn't have a Type!") end + if not isstring(v.Type) then error("Argument '" .. tostring(k or "") .. "' has a non-string Type! (" .. tostring(v.Type) .. ")") end + if not ArgumentTypes[v.Type] then error("Argument '" .. tostring(k or "") .. "' has a non-registered Type! (" .. tostring(v.Type) .. ")") end + + Restrictions[k] = v + end +end + + +--- Adds an argument type and verifier to the ArgumentTypes dictionary. +--- @param Type string The type of data +--- @param Verifier function The verification function. Arguments are: Value:any, Restrictions:table. Must return a Value of the same type and NOT nil! +function Entities.AddArgumentType(Type, Verifier) + if ArgumentTypes[Type] then return end + + ArgumentTypes[Type] = Verifier +end + +Entities.AddArgumentType("Number", function(Value, Specs) + if not isnumber(Value) then Value = ACF.CheckNumber(Value, Specs.Default or 0) end + + if Specs.Decimals then Value = math.Round(Value, Specs.Decimals) end + if Specs.Min then Value = math.max(Value, Specs.Min) end + if Specs.Max then Value = math.min(Value, Specs.Max) end + + return Value +end) + +--- Adds extra arguments to a class which has been created via Entities.AutoRegister() (or Entities.Register() with no arguments) +--- @param Class string A class previously registered as an entity class +--- @param DataKeys table A key-value table, where key is the name of the data and value defines the type and restrictions of the data. +function Entities.AddStrictArguments(Class, DataKeys) + if not isstring(Class) then return end + + local Entity = GetEntityTable(Class) + local Arguments = table.GetKeys(DataKeys) + local List = AddArguments(Entity, Arguments) + AddArgumentRestrictions(Entity, DataKeys) + + if Entity.Spawn then + duplicator.RegisterEntityClass(Class, Entity.Spawn, "Pos", "Angle", "Data", unpack(List)) + end +end + +function Entities.AutoRegister(ENT) + if ENT == nil then ENT = _G.ENT end + if not ENT then error("Called Entities.AutoRegister(), but no entity was in the process of being created.") end + + local Class = string.Split(ENT.Folder, "/"); Class = Class[#Class] + ENT.ACF_Class = Class + + local Entity = GetEntityTable(Class) + Entities.AddStrictArguments(Class, ENT.ACF_DataKeys or {}) + + if CLIENT then return end + + if isnumber(ENT.ACF_Limit) then + CreateConVar( + "sbox_max_" .. Class, + ENT.ACF_Limit, + FCVAR_ARCHIVE + FCVAR_NOTIFY, + "Maximum amount of " .. (ENT.PluralName or (Class .. " entities")) .. " a player can create." + ) + end + + -- Verification function + local function VerifyClientData(ClientData) + local Entity = GetEntityTable(Class) + local List = Entity.List + local Restrictions = Entity.Restrictions + + for _, argName in ipairs(List) do + if Restrictions[argName] then + local RestrictionSpecs = Restrictions[argName] + if not ArgumentTypes[RestrictionSpecs.Type] then error("No verification function for type '" .. tostring(RestrictionSpecs.Type or "") .. "'") end + ClientData[argName] = ArgumentTypes[RestrictionSpecs.Type](ClientData[argName], RestrictionSpecs) + end + end + + if ENT.ACF_OnVerifyClientData then ENT.ACF_OnVerifyClientData(ClientData) end + end + + local function UpdateEntityData(self, ClientData) + local Entity = GetEntityTable(Class) + local List = Entity.List + + if self.ACF_PreUpdateEntityData then self:ACF_PreUpdateEntityData(ClientData) end + self.ACF = self.ACF or {} + for _, v in ipairs(List) do + self[v] = ClientData[v] + end + + if self.ACF_PostUpdateEntityData then self:ACF_PostUpdateEntityData(ClientData) end + + ACF.Activate(self, true) + end + + function ENT:Update(ClientData) + VerifyClientData(ClientData) + + hook.Run("ACF_OnEntityLast", Class, self) + + ACF.SaveEntity(self) + UpdateEntityData(self, ClientData) + ACF.RestoreEntity(self) + + hook.Run("ACF_OnEntityUpdate", Class, self, ClientData) + if self.UpdateOverlay then self:UpdateOverlay(true) end + net.Start("ACF_UpdateEntity") + net.WriteEntity(self) + net.Broadcast() + + return true, (self.PrintName or Class) .. " updated successfully!" + end + + function Entity.Spawn(Player, Pos, Angle, ClientData) + if ENT.ACF_Limit then + if isfunction(ENT.ACF_Limit) then + if not ENT.ACF_Limit() then return end + elseif isnumber(ENT.ACF_Limit) then + if not Player:CheckLimit("_" .. Class) then return false end + end + end + + local CanSpawn = hook.Run("ACF_PreEntitySpawn", Class, Player, ClientData) + if CanSpawn == false then return false end + + local New = ents.Create(Class) + if not IsValid(New) then return end + + VerifyClientData(ClientData) + + New:SetPos(Pos) + New:SetAngles(Angle) + if New.ACF_PreSpawn then + New:ACF_PreSpawn(Player, Pos, Angle, ClientData) + end + + New:SetPlayer(Player) + New:Spawn() + Player:AddCount("_" .. Class, New) + Player:AddCleanup("_" .. Class, New) + New.Owner = Player -- MUST be stored on ent for PP + New.DataStore = Entities.GetArguments(Class) + + hook.Run("ACF_OnEntitySpawn", Class, New, ClientData) + + if New.ACF_PostSpawn then + New:ACF_PostSpawn(Player, Pos, Angle, ClientData) + end + + New:ACF_UpdateEntityData(ClientData) + if New.UpdateOverlay then New:UpdateOverlay(true) end + ACF.CheckLegal(New) + + return New + end + + ENT.ACF_VerifyClientData = VerifyClientData + ENT.ACF_UpdateEntityData = UpdateEntityData +end + --- Registers a class as a spawnable entity class --- @param Class string The class to register --- @param Function fun(Player:entity, Pos:vector, Ang:angle, Data:table):Entity A function defining how to spawn your class (This should be your MakeACF_ function) --- @param ... any #A vararg of arguments to attach to the entity function Entities.Register(Class, Function, ...) + if Class == nil and Function == nil then + -- Calling Entities.Register with no arguments performs an automatic registration + Entities.AutoRegister(ENT) + return + end + if not isstring(Class) then return end if not isfunction(Function) then return end diff --git a/lua/acf/menu/items_cl/baseplates.lua b/lua/acf/menu/items_cl/baseplates.lua new file mode 100644 index 000000000..192eea741 --- /dev/null +++ b/lua/acf/menu/items_cl/baseplates.lua @@ -0,0 +1,70 @@ +local ACF = ACF + +local gridMaterial = CreateMaterial("acf_bp_vis_spropgrid1", "VertexLitGeneric", { + ["$basetexture"] = "sprops/sprops_grid_12x12", + ["$model"] = 1, + ["$translucent"] = 1, + ["$vertexalpha"] = 1, + ["$vertexcolor"] = 1 +}) + +local function CreateMenu(Menu) + ACF.SetToolMode("acf_menu", "Spawner", "Baseplate") + ACF.SetClientData("PrimaryClass", "acf_baseplate") + ACF.SetClientData("SecondaryClass", "N/A") + + Menu:AddTitle("Baseplate Settings") + + Menu:AddLabel("The root entity of all ACF contraptions.") + local SizeX = Menu:AddSlider("Plate Width (gmu)", 36, 96, 2) + local SizeY = Menu:AddSlider("Plate Length (gmu)", 36, 420, 2) + local SizeZ = Menu:AddSlider("Plate Thickness (gmu)", 1, 3, 2) + + Menu:AddLabel("Comparing the current dimensions with a 105mm Howitzer:") + local Vis = Menu:AddModelPreview("models/howitzer/howitzer_105mm.mdl", true) + Vis:SetSize(30, 300) + function Vis:PreDrawModel(_) + local w, h, t = SizeX:GetValue(), SizeY:GetValue(), SizeZ:GetValue() + self.CamDistance = math.max(w, h, 60) * 1 + + render.SetMaterial(gridMaterial) + render.DrawBox(Vector(0, 0, 0), Angle(0, 0, 0), Vector(-h / 2, -w / 2, -t / 2), Vector(h / 2, w / 2, t / 2), color_white) + end + + SizeX:SetClientData("Width", "OnValueChanged") + SizeX:DefineSetter(function(Panel, _, _, Value) + local X = math.Round(Value, 2) + + Panel:SetValue(X) + + return X + end) + + SizeY:SetClientData("Length", "OnValueChanged") + SizeY:DefineSetter(function(Panel, _, _, Value) + local Y = math.Round(Value, 2) + + Panel:SetValue(Y) + + return Y + end) + + SizeZ:SetClientData("Thickness", "OnValueChanged") + SizeZ:DefineSetter(function(Panel, _, _, Value) + local Z = math.Round(Value, 2) + + Panel:SetValue(Z) + + return Z + end) + + Menu:AddLabel("You can hold SHIFT while left-clicking to replace an existing entity with an ACF Baseplate. " .. + "This will, to the best of its abilities (given you're using a cubical prop, with the long side facing forwards), replace the entity you're looking at with " .. + "a new ACF baseplate.\n\nIt works by taking an Advanced Duplicator 2 copy of the entire contraption from the target entity, replacing the target entity " .. + "in the dupe's class to acf_baseplate, setting the size based off the physical size of the target entity, then removing all entities and re-pasting the dupe. " .. + "\n\nYou will need to manually re-copy the contraption with the Adv. Dupe 2 tool before using it again, but after that, everything should be converted. This is " .. + "an experimental tool, so if something breaks with an ordinary setup, report it at https://github.com/ACF-Team/ACF-3/issues." + ) +end + +ACF.AddMenuItem(0, "Entities", "Baseplates", "shape_square", CreateMenu) \ No newline at end of file diff --git a/lua/acf/menu/items_cl/turret_menu.lua b/lua/acf/menu/items_cl/turret_menu.lua index a8808cee3..91469f3e1 100644 --- a/lua/acf/menu/items_cl/turret_menu.lua +++ b/lua/acf/menu/items_cl/turret_menu.lua @@ -4,7 +4,7 @@ local Turrets = ACF.Classes.Turrets local function CreateMenu(Menu) local Entries = Turrets.GetEntries() - ACF.SetToolMode("acf_menu", "Spawner", "Component") + ACF.SetToolMode("acf_menu", "Spawner", "Turret") ACF.SetClientData("PrimaryClass", "N/A") ACF.SetClientData("SecondaryClass", "N/A") @@ -31,6 +31,7 @@ local function CreateMenu(Menu) ClassDesc:SetText(Data.Description or "No description provided.") + ACF.SetToolMode("acf_menu", "Spawner", Data.ID) ACF.LoadSortedList(ComponentClass, Data.Items, "Name") end diff --git a/lua/acf/menu/operations/acf_menu.lua b/lua/acf/menu/operations/acf_menu.lua index ce89d0703..d29bb5079 100644 --- a/lua/acf/menu/operations/acf_menu.lua +++ b/lua/acf/menu/operations/acf_menu.lua @@ -219,7 +219,8 @@ do -- Generic Spawner/Linker operation creator --- @param Name string The name of the link type performed by the toolgun (e.g. Weapon, Engine, etc.) --- @param Primary string The type of the entity to be spawned on left click (purely aesthetical) --- @param Secondary string | nil The type of entity to be spawned on shift + right click (purely aesthetical) - function ACF.CreateMenuOperation(Name, Primary, Secondary) + --- @param OnRightClick table | nil If provided, a table with a Text and Func parameter for when right clicking + function ACF.CreateMenuOperation(Name, Primary, Secondary, OnRightClick) if not isstring(Name) then return end if not isstring(Primary) then return end @@ -229,12 +230,12 @@ do -- Generic Spawner/Linker operation creator -- These basically setup the tool information display you see on the top left of your screen ACF.RegisterOperation("acf_menu", "Spawner", Name, { OnLeftClick = SpawnEntity, - OnRightClick = function(Tool, Trace) + OnRightClick = OnRightClick and OnRightClick.Func or function(Tool, Trace) local Entity = Trace.Entity -- The call to SelectEntity will switch the mode to the linker return SelectEntity(Entity, Name, Tool) - end, + end }) ACF.RegisterToolInfo("acf_menu", "Spawner", Name, { @@ -252,7 +253,7 @@ do -- Generic Spawner/Linker operation creator ACF.RegisterToolInfo("acf_menu", "Spawner", Name, { name = "right", - text = "Select the entity you want to link or unlink.", + text = OnRightClick and OnRightClick.Text or "Select the entity you want to link or unlink." }) end @@ -326,4 +327,17 @@ ACF.CreateMenuOperation("Missile", "rack", "ammo crate") ACF.CreateMenuOperation("Engine", "engine", "fuel tank") ACF.CreateMenuOperation("Component", "component") ACF.CreateMenuOperation("Gearbox", "gearbox") -ACF.CreateMenuOperation("Sensor", "sensor") \ No newline at end of file +ACF.CreateMenuOperation("Sensor", "sensor") + +ACF.CreateMenuOperation("1-Turret", "turret") +ACF.CreateMenuOperation("2-Motor", "turret motor") +ACF.CreateMenuOperation("3-Gyro", "turret gyroscope") +ACF.CreateMenuOperation("4-Computer", "turret computer") + +ACF.CreateMenuOperation("Baseplate", "baseplate", nil, { + Text = "Convert the target entity into a baseplate. Works best on SProps rectangles (either normal, thin, or superthin). Other models not supported.", + Func = function(Tool, Trace) + if CLIENT then return end + ACF.ConvertEntityToBaseplate(Tool.SWEP:GetOwner(), Trace.Entity) + end +}) diff --git a/lua/entities/acf_baseplate/cl_init.lua b/lua/entities/acf_baseplate/cl_init.lua new file mode 100644 index 000000000..8f7bb4649 --- /dev/null +++ b/lua/entities/acf_baseplate/cl_init.lua @@ -0,0 +1,47 @@ +include("shared.lua") + +function ENT:Update() end + +local HideInfo = ACF.HideInfoBubble + +local COLOR_Black = Color(0, 0, 0) +local COLOR_Red = Color(255, 50, 40) +local COLOR_Green = Color(40, 255, 50) + +function ENT:DrawGizmos() + cam.IgnoreZ(true) + + local Pos = self:GetPos() + local Size = self.Size + + render.SetColorMaterial() + render.DrawBeam(Pos, self:LocalToWorld(Vector(Size.x / 2, 0, 0)), 2, 0, 1, COLOR_Black) + render.DrawBeam(Pos, self:LocalToWorld(Vector(Size.x / 2, 0, 0)), 1, 0, 1, COLOR_Red) + render.DrawBeam(Pos, self:LocalToWorld(Vector(0, -Size.y / 2, 0)), 2, 0, 1, COLOR_Black) + render.DrawBeam(Pos, self:LocalToWorld(Vector(0, -Size.y / 2, 0)), 1, 0, 1, COLOR_Green) + + cam.IgnoreZ(false) +end + +function ENT:Draw() + -- Partial from base_wire_entity, need the tooltip but without the model drawing since we're drawing our own + local LocalPlayer = LocalPlayer() + local Weapon = LocalPlayer:GetActiveWeapon() + local LookedAt = self:BeingLookedAtByLocalPlayer() + + if LookedAt then + self:DrawEntityOutline() + end + + self:DrawModel() + + if LookedAt and not HideInfo() then + self:AddWorldTip() + + if not LocalPlayer:InVehicle() and IsValid(Weapon) and Weapon:GetClass() == "weapon_physgun" then + self:DrawGizmos() + end + end +end + +ACF.Classes.Entities.Register() \ No newline at end of file diff --git a/lua/entities/acf_baseplate/init.lua b/lua/entities/acf_baseplate/init.lua new file mode 100644 index 000000000..92552846d --- /dev/null +++ b/lua/entities/acf_baseplate/init.lua @@ -0,0 +1,38 @@ +AddCSLuaFile("shared.lua") +AddCSLuaFile("cl_init.lua") +include("shared.lua") + +local ACF = ACF +local Classes = ACF.Classes +local Entities = Classes.Entities + +ENT.ACF_Limit = 16 + +function ENT.ACF_OnVerifyClientData(ClientData) + ClientData.Size = Vector(ClientData.Width, ClientData.Length, ClientData.Thickness) +end +function ENT:ACF_PostUpdateEntityData(ClientData) + self:SetSize(ClientData.Size) +end + +function ENT:ACF_PreSpawn(_, _, _, _) + self:SetScaledModel("models/holograms/cube.mdl") + self:SetMaterial("sprops/sprops_grid_12x12") +end + +function ENT:ACF_PostSpawn(_, _, _, ClientData) + local EntMods = ClientData.EntityMods + if EntMods and EntMods.mass then + ACF.Contraption.SetMass(self, self.ACF.Mass or 1) + else + ACF.Contraption.SetMass(self, 1000) + end +end + +local Text = "Baseplate Size: %dx%dx%d" + +function ENT:UpdateOverlayText() + return Text:format(self.Size[1], self.Size[2], self.Size[3]) +end + +Entities.Register() \ No newline at end of file diff --git a/lua/entities/acf_baseplate/shared.lua b/lua/entities/acf_baseplate/shared.lua new file mode 100644 index 000000000..976b8da56 --- /dev/null +++ b/lua/entities/acf_baseplate/shared.lua @@ -0,0 +1,13 @@ +DEFINE_BASECLASS "acf_base_scalable" + +ENT.PrintName = "ACF Baseplate" +ENT.WireDebugName = "ACF Baseplate" +ENT.PluralName = "ACF Baseplates" +ENT.IsACFEntity = true +ENT.IsACFBaseplate = true + +ENT.ACF_DataKeys = { + ["Width"] = {Type = "Number", Min = 36, Max = 96, Default = 36, Decimals = 2}, + ["Length"] = {Type = "Number", Min = 36, Max = 480, Default = 36, Decimals = 2}, + ["Thickness"] = {Type = "Number", Min = 1, Max = 3, Default = 3, Decimals = 2} +} \ No newline at end of file