Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add baseplate entities #426

Draft
wants to merge 15 commits into
base: dev
Choose a base branch
from
65 changes: 65 additions & 0 deletions lua/acf/compatibility/baseplate_convert_sv.lua
Original file line number Diff line number Diff line change
@@ -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
181 changes: 178 additions & 3 deletions lua/acf/core/classes/entities/registration.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "<NIL>") .. "' didn't have a Type!") end
if not isstring(v.Type) then error("Argument '" .. tostring(k or "<NIL>") .. "' has a non-string Type! (" .. tostring(v.Type) .. ")") end
if not ArgumentTypes[v.Type] then error("Argument '" .. tostring(k or "<NIL>") .. "' 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 "<NIL>") .. "'") 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_<something> 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

Expand Down
70 changes: 70 additions & 0 deletions lua/acf/menu/items_cl/baseplates.lua
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion lua/acf/menu/items_cl/turret_menu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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

Expand Down
Loading
Loading