1
0
mirror of https://github.com/IkarusHD/ICF-3.git synced 2025-03-04 03:03:12 -05:00

Merge pull request #426 from marchc1/baseplate-ents

Add baseplate entities
This commit is contained in:
thecraftianman 2024-12-23 22:15:47 -05:00 committed by GitHub
commit 387945561c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 476 additions and 14 deletions

View File

@ -0,0 +1,87 @@
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
local bpConvertibleModelPaths = {
{
startWith = "models/sprops/rectangles",
addAngles = Angle(0, 0, 0)
}
}
function ACF.ConvertEntityToBaseplate(Player, Target)
if not AdvDupe2 then return false, "Advanced Duplicator 2 is not installed" end
if not IsValid(Target) then return false, "Invalid target" end
local Owner = Target:CPPIGetOwner()
if not IsValid(Owner) or Owner ~= Player then return false, "You do not own this entity" end
local PhysObj = Target:GetPhysicsObject()
if not IsValid(PhysObj) then return false, "Entity is not physical" end
if Target:GetClass() ~= "prop_physics" then return false, "Entity must be typeof 'prop_physics'" end
local foundTranslation
local targetModel = Target:GetModel()
for _, v in ipairs(bpConvertibleModelPaths) do
if string.StartsWith(targetModel, v.startWith) then
foundTranslation = v
break
end
end
if not foundTranslation then return false, "Incompatible model '" .. targetModel .. "'" end
local AMi, AMa = PhysObj:GetAABB()
local BoxSize = AMa - AMi
-- Duplicate the entire thing
local Entities, Constraints = AdvDupe2.duplicator.Copy(Player, Target, {}, {}, vector_origin)
-- 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.Length = w
Baseplate.Width = l
Baseplate.Thickness = t
Baseplate.PhysicsObjects[0].Angle = Baseplate.PhysicsObjects[0].Angle + foundTranslation.addAngles
-- 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_origin, angle_zero, vector_origin, 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 true, NewBaseplate
end

View File

@ -307,7 +307,7 @@ do -- ASSUMING DIRECT CONTROL
local Ent = self:GetEntity()
-- Required due for AD2 support, if this isn't present then entities will never get set to their required weight on dupe paste
if Ent.IsACFEntity then Contraption.SetMass(Ent, Ent.ACF.Mass) return end
if Ent.IsACFEntity and not Ent.ACF_UserWeighable then Contraption.SetMass(Ent, Ent.ACF.Mass) return end
if Ent.ACF_OnMassChange then
Ent:ACF_OnMassChange(self:GetMass(), Mass)

View File

@ -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,188 @@ 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)
return List
end
-- Automatically registers an entity. This MUST be the last line in entity/init.lua for everything to work properly
-- Can be passed with an ENT table if you have some weird usecase, but auto defaults to _G.ENT
--- @param ENT table A scripted entity class definition (see https://wiki.facepunch.com/gmod/Structures/ENT)
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)
local ArgsList = 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
local ACF_Limit = ENT.ACF_Limit
function Entity.Spawn(Player, Pos, Angle, ClientData)
if ACF_Limit then
if isfunction(ACF_Limit) then
if not ACF_Limit() then return end
elseif isnumber(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
duplicator.RegisterEntityClass(Class, Entity.Spawn, "Pos", "Angle", "Data", unpack(ArgsList))
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

View File

@ -0,0 +1,71 @@
local ACF = ACF
local gridMaterial = CreateMaterial("acf_bp_vis_grid1", "VertexLitGeneric", {
["$basetexture"] = "hunter/myplastic",
["$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 BaseplateBase = Menu:AddCollapsible("Baseplate Information")
local SizeX = BaseplateBase:AddSlider("Plate Width (gmu)", 36, 96, 2)
local SizeY = BaseplateBase:AddSlider("Plate Length (gmu)", 36, 420, 2)
local SizeZ = BaseplateBase:AddSlider("Plate Thickness (gmu)", 0.5, 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_origin, angle_zero, 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 right click on an entity 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, ex. a SProps plate), 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)

View File

@ -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")
@ -29,6 +29,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

View File

@ -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,20 @@ ACF.CreateMenuOperation("Missile", "rack", "ammo crate")
ACF.CreateMenuOperation("Engine", "engine", "fuel tank")
ACF.CreateMenuOperation("Component", "component")
ACF.CreateMenuOperation("Gearbox", "gearbox")
ACF.CreateMenuOperation("Sensor", "sensor")
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 = "Attempts to convert the target entity into a baseplate.",
Func = function(Tool, Trace)
if CLIENT then return end
local success, msg = ACF.ConvertEntityToBaseplate(Tool.SWEP:GetOwner(), Trace.Entity)
if not success then
ACF.SendNotify(Tool:GetOwner(), err, "[ACF] Could not convert baseplate: " .. msg)
end
end
})

View File

@ -170,6 +170,8 @@ DefineScannerType("primitive_airfoil", "Primitive Airfoil", Color(
drawMesh = true
})
DefineScannerType("acf_baseplate", "ACF Baseplate", Color(255, 65, 160), "ABP", {drawBounds = true, drawMarker = true})
local function NetStart(n)
net_Start("ACF_Scanning_NetworkPacket")
net_WriteString(n)
@ -1344,9 +1346,11 @@ if CLIENT then
end
for _, ent in ipairs(baseplates) do
drawEntityNoOutline(ent, baseplateC.colorEntityInside)
drawPhysMesh(ent, baseplateC.color)
drawBounds(ent, baseplateC)
if ent:GetClass() ~= "acf_baseplate" then
drawEntityNoOutline(ent, baseplateC.colorEntityInside)
drawPhysMesh(ent, baseplateC.color)
drawBounds(ent, baseplateC)
end
VisualizeClips(ent)
end
render.DepthRange(0, 1)
@ -1529,7 +1533,7 @@ if CLIENT then
end
for _, ent in ipairs(baseplates) do
if IsValid(ent) then
if IsValid(ent) and ent:GetClass() ~= "acf_baseplate" then
local pXY = ent:GetPos():ToScreen()
local pX, pY = pXY.x, pXY.y
DrawMarker(baseplateC, pX, pY, ent)

View File

@ -0,0 +1,52 @@
include("shared.lua")
function ENT:Update() end
local HideInfo = ACF.HideInfoBubble
local ColorBlack = Color(0, 0, 0)
local ColorRed = Color(255, 50, 40)
local ColorGreen = 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, ColorBlack)
render.DrawBeam(Pos, self:LocalToWorld(Vector(Size.x / 2, 0, 0)), 1, 0, 1, ColorRed)
render.DrawBeam(Pos, self:LocalToWorld(Vector(0, -Size.y / 2, 0)), 2, 0, 1, ColorBlack)
render.DrawBeam(Pos, self:LocalToWorld(Vector(0, -Size.y / 2, 0)), 1, 0, 1, ColorGreen)
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 not LookedAt then return end
if HideInfo() then return end
self:AddWorldTip()
if LocalPlayer:InVehicle() then return end
if not IsValid(Weapon) then return end
local class = Weapon:GetClass()
if class ~= "weapon_physgun" and (class ~= "gmod_tool" or Weapon.current_mode ~= "acf_menu") then return end
self:DrawGizmos()
end
ACF.Classes.Entities.Register()

View File

@ -0,0 +1,39 @@
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
ENT.ACF_UserWeighable = true
function ENT.ACF_OnVerifyClientData(ClientData)
ClientData.Size = Vector(ClientData.Length, ClientData.Width, 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("hunter/myplastic")
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: %.1f x %.1f x %.1f"
function ENT:UpdateOverlayText()
return Text:format(self.Size[1], self.Size[2], self.Size[3])
end
Entities.Register()

View File

@ -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 = 0.5, Max = 3, Default = 3, Decimals = 2}
}