Garry's Mod Wiki

Revision Difference

Fake_Scripted_Entity_Creation#514977

<cat>Dev.GameModes</cat>⤶ Sometimes you may need to create a new entity based on an engine entity (this means that it is not scripted). You may want to do this if you want to keep interesting properties of an engine entity but you want to modify it by adding or modifying methods.⤶ ⤶ In my example, you will not be able to use ENTITY hooks. Any hook has to be hard-coded in the footer of the code so it is triggered, like for any other engine entity. For example, you will be unable to simply add an <page>ENTITY:Use</page> or an <page>ENTITY:Think</page> method for them to work: you have to make the code that will trigger them. (I may add some examples in the future.)⤶ ⤶ In this example I create a new class `prop_vehicle_mr` based on a `prop_dynamic`. I could not create a real scripted entity because if I do so and I parent it to another entity (to a `prop_vehicle_jeep` for example), the physics object does not move with it! So I needed to use a new class derivated from `prop_dynamic`, because the physics object will by glued the entity itself.⤶ ⤶ In addition, this fake SENT footer is written in a way that can replace any method included in the metatable and that cannot be replaced in a standard SENT. In this example I replace the <page>Entity:SetMaterial</page> method in order to change the default material when resetting it.⤶ ⤶ Please note: the method and properties are only available **serverside**! This means that it will be seen **clientside** simply as an entity of its base class, except the classname: the methods and the properties will be that of the base class. (I may modify this in the future through a hook.)⤶ ⤶ Here I show you step by step how to write a fake scripted entity. This may to be done in a scripted entity file but it is supposed to work in any other place. The file of this example is: `lua/entities/prop_vehicle_mr.lua`⤶ ⤶ ----⤶ ⤶ **First step: the header**⤶ ⤶ The headers prepares the structure of the fake scripted entity.⤶ ⤶ ```⤶ ENT.Type = "anim"⤶ local ENT = {}⤶ local BaseClass = FindMetaTable("Entity")⤶ local BaseClassName = "prop_dynamic"⤶ local ClassName = "prop_vehicle_mr"⤶ ```⤶ ⤶ First we define the Type of the fake SENT. It is certainly useless.⤶ ⤶ Then we create a clean ENT table, so nothing will be inherited from the SENT system.⤶ ⤶ We load the BaseClass which contains all default methods and all default properties for entities. It will be modified in the footer.⤶ ⤶ We specify the BaseClassName and the ClassName so the script knows what it has to deal with.⤶ ⤶ ----⤶ ⤶ **Second step: the classic <page>Global.AddCSLuaFile</page>**⤶ ⤶ ⤶ ```⤶ AddCSLuaFile()⤶ ```⤶ ⤶ ⤶ ----⤶ ⤶ **Third step: the content**⤶ ⤶ Here is where you will put default properties, methods and hooks for your fake SENT.⤶ ⤶ Stay aware of what you are doing while replacing methods from the metatable (BaseClass): it is dangerous and it might conflict with something else.⤶ ⤶ ```⤶ local old_SetMaterial = BaseClass.SetMaterial⤶ function ENT:SetMaterial (materialName)⤶ if materialName != nil and materialName != "" then⤶ return old_SetMaterial(self, materialName)⤶ else⤶ if self.DefaultMaterial != nil then⤶ return old_SetMaterial(self, self.DefaultMaterial)⤶ else⤶ return old_SetMaterial(self, "")⤶ end⤶ end⤶ end⤶ ```⤶ ⤶ Somewhere else, after creating an entity of this class, I may change the default material for this entity:⤶ ⤶ ```⤶ MyEntity.DefaultMaterial = "materialname"⤶ ```⤶ ⤶ Here are some examples of hooks that you may use. Remember: hooks that are not in this example are not implemented, it is your job.⤶ ⤶ ```⤶ function ENT:Initialize ()⤶ end⤶ ```⤶ ⤶ In this example the implementation of the hook <page>ENTITY:Initialize</page> in the footer makes that it is **not** called when you use <page>Entity:Spawn</page>. Instead it is called immediatly when using <page>ents.Create</page>.⤶ ⤶ Avoid using <page>Entity:Spawn</page> on the fake SENT itself in this hook as this should be done by the script that created the entity. The risk is that <page>Entity:Spawn</page> may be called twice.⤶ ⤶ ----⤶ ⤶ **Fourth step: the footer**⤶ ⤶ The footer is the most important part of the fake SENT: it modifies the Lua functions to create the expected entity.⤶ ⤶ ```⤶ local old_FindByClass = ents.FindByClass⤶ function ents.FindByClass (class, ...)⤶ if class == ClassName then -- Look for ClassName excluding anything else.⤶ local entities = {}⤶ for _, ent in pairs(old_FindByClass(BaseClassName)) do⤶ if ent:GetClass() == ClassName then⤶ entities[#entities+1] = ent⤶ end⤶ end⤶ return entities⤶ elseif class == BaseClassName then -- Look for BaseClassName excluding ClassName.⤶ local entities = {}⤶ for _, ent in pairs(old_FindByClass(BaseClassName)) do⤶ if ent:GetClass() != ClassName then⤶ entities[#entities+1] = ent⤶ end⤶ end⤶ return entities⤶ else⤶ return old_FindByClass(class, ...)⤶ end⤶ end⤶ ```⤶ ⤶ When calling <page>ents.FindByClass</page>("prop_vehicle_mr") we want to get a table containing all entities of the class `prop_vehicle_mr`. Without the code, the table would be always empty.⤶ ⤶ When calling <page>ents.FindByClass</page>("prop_dynamic") we want to get a table containing all entities of the class `prop_dynamic`, except that of the class `prop_vehicle_mr`. Without the code, the table would also contain entities of the class `prop_vehicle_mr`.⤶ ⤶ In other cases, <page>ents.FindByClass</page> will work like before.⤶ ⤶ ```⤶ local old_GetClass = BaseClass.GetClass⤶ function BaseClass:GetClass (...)⤶ -- if self[ClassName] then -- Non-networked classname⤶ if self:GetNWBool(ClassName, false) then -- Networked classname⤶ return ClassName⤶ else⤶ return old_GetClass(self, ...)⤶ end⤶ end⤶ ```⤶ ⤶ We change the <page>Entity:GetClass</page> method in the Entity datatable so the proper value is returned for fake SENTs.⤶ ⤶ For some reason, the fake SENT class may need not to be shared with the client. In this case, use comment and uncomment the proper lines here and in the `ents.Create` block below.⤶ ⤶ ```⤶ local SENT_values = {}⤶ for FuncName, Func in pairs(ENT) do⤶ if isfunction(Func) then -- This is a function value.⤶ local old_Func = BaseClass[FuncName]⤶ if isfunction(old_Func) then -- The method is re-defined in the base class.⤶ BaseClass[FuncName] = function (self, ...)⤶ if self:GetClass() == ClassName then⤶ return Func(self, ...)⤶ else⤶ return old_Func(self, ...)⤶ end⤶ end⤶ else -- The method is not defined in the base class.⤶ SENT_values[FuncName] = Func⤶ end⤶ else -- This is a value that is not a function.⤶ SENT_values[FuncName] = Func⤶ end⤶ end⤶ ```⤶ ⤶ We modify all methods that exist in the Entity datatable that are defined in the content.⤶ ⤶ We add all methods that do not exist in the Entity datatable in a table to be added by <page>ents.Create</page>.⤶ ⤶ We add all properties in a table to be added by <page>ents.Create</page>.⤶ ⤶ ```⤶ local old_Create = ents.Create⤶ function ents.Create (class, ...)⤶ if class == ClassName then⤶ local ent = old_Create(BaseClassName, ...)⤶ if IsValid(ent) then⤶ -- ent[ClassName] = true -- Non-networked classname⤶ ent:SetNWBool(ClassName, true) -- Networked classname⤶ for k, v in pairs(SENT_values) do⤶ ent[k] = v⤶ end⤶ if isfunction(ent.Initialize) then⤶ ent:Initialize()⤶ end⤶ end⤶ return ent⤶ else⤶ return old_Create(class, ...)⤶ end⤶ end⤶ ```⤶ ⤶ We modify the <page>ents.Create</page> function. It adds a flag to mark the name of the fake SENT and it adds all properties and methods that are in the `SENT_values` table.⤶ ⤶ ----⤶ ⤶ Here is the complete example, free to use:⤶ ⤶ ```⤶ -- This is not a real scripted class. It is derivated from prop_dynamic and it will be recognized as this by the engine and by some Lua functions.⤶ -- The prop_dynamic class has a fixed physics that cannot move without the prop itself. This is what I need.⤶ ⤶ ⤶ --## Header data ##--⤶ ENT.Type = "anim"⤶ local ENT = {}⤶ local BaseClass = FindMetaTable("Entity")⤶ local BaseClassName = "prop_dynamic"⤶ local ClassName = "prop_vehicle_mr"⤶ ⤶ ⤶ --## Code that would be nearly written for a normal scripted entity ##--⤶ AddCSLuaFile()⤶ ⤶ -- If the default material is not the model's one, then it should be set again when a Material tool tries to set the default material.⤶ local old_SetMaterial = BaseClass.SetMaterial⤶ function ENT:SetMaterial (materialName)⤶ if materialName != nil and materialName != "" then⤶ return old_SetMaterial(self, materialName)⤶ else⤶ if self.DefaultMaterial != nil then⤶ return old_SetMaterial(self, self.DefaultMaterial)⤶ else⤶ return old_SetMaterial(self, "")⤶ end⤶ end⤶ end⤶ ⤶ function ENT:Initialize ()⤶ end⤶ ⤶ ⤶ --## Footer treatment ##--⤶ local old_FindByClass = ents.FindByClass⤶ function ents.FindByClass (class, ...)⤶ if class == ClassName then -- Look for ClassName excluding anything else.⤶ local entities = {}⤶ for _, ent in pairs(old_FindByClass(BaseClassName)) do⤶ if ent:GetClass() == ClassName then⤶ entities[#entities+1] = ent⤶ end⤶ end⤶ return entities⤶ elseif class == BaseClassName then -- Look for BaseClassName excluding ClassName.⤶ local entities = {}⤶ for _, ent in pairs(old_FindByClass(BaseClassName)) do⤶ if ent:GetClass() != ClassName then⤶ entities[#entities+1] = ent⤶ end⤶ end⤶ return entities⤶ else⤶ return old_FindByClass(class, ...)⤶ end⤶ end⤶ local old_GetClass = BaseClass.GetClass⤶ function BaseClass:GetClass (...)⤶ -- if self[ClassName] then -- Non-networked classname⤶ if self:GetNWBool(ClassName, false) then -- Networked classname⤶ return ClassName⤶ else⤶ return old_GetClass(self, ...)⤶ end⤶ end⤶ local SENT_values = {}⤶ for FuncName, Func in pairs(ENT) do⤶ if isfunction(Func) then -- This is a function value.⤶ local old_Func = BaseClass[FuncName]⤶ if isfunction(old_Func) then -- The method is re-defined in the base class.⤶ BaseClass[FuncName] = function (self, ...)⤶ if self:GetClass() == ClassName then⤶ return Func(self, ...)⤶ else⤶ return old_Func(self, ...)⤶ end⤶ end⤶ else -- The method is not defined in the base class.⤶ SENT_values[FuncName] = Func⤶ end⤶ else -- This is a value that is not a function.⤶ SENT_values[FuncName] = Func⤶ end⤶ end⤶ local old_Create = ents.Create⤶ function ents.Create (class, ...)⤶ if class == ClassName then⤶ local ent = old_Create(BaseClassName, ...)⤶ if IsValid(ent) then⤶ -- ent[ClassName] = true -- Non-networked classname⤶ ent:SetNWBool(ClassName, true) -- Networked classname⤶ for k, v in pairs(SENT_values) do⤶ ent[k] = v⤶ end⤶ end⤶ return ent⤶ else⤶ return old_Create(class, ...)⤶ end⤶ end⤶ ```⤶ ⤶ ⤶ ⤶ ⤶