Prefabs
As you know from Basic concepts: ECS, prefabs is the central pillar of Penal Engineer extensibility. It may seem quite a complex subject, but it's crucial to understand how prefabs work in Penal Engineer, because you as a modder will supply or alter indeed prefabs most of the time.
Buckle up!
What exactly is a prefab?
First and foremost, you should realize that in Penal Engineer a prefab is nothing more than just a code function (more precisely, a C# lambda function) which constructs an entity (by way of constructing components and adding them to the entity).
The outcome of a prefab execution is always an entity with zero or more components.
Having read that, you may think that the only way to define a new prefab is to write relevant C# code for a new lambda. But this is where the power of Penal Engineer prefabs derivation system comes to rescue.
Derived prefabs
Penal Engineer is already bundled with a lot of prefabs defined. Actually, Penal Engineer comes with a whole hierarchy of prefabs, where some prefabs re-use others.
Those pre-bundled prefabs are coded in C# and already carry out all the necessary mechanics of creation of components based on input arguments and adding these components to a newly created entity.
Some of the pre-bundled prefabs are written with the sole purpose to get extended. We name them abstract prefabs.
For example, there is a bundled prefab Base/Flooring which can (and should!) be used to define a new Floor Tile.
This prefab accepts such arguments: WalkSpeed, Position, and Sprite (latter two comes from Base/Renderable which,
in turn, is a base prefab for Base/Flooring). In your new Floor Tile prefab, you can specify the Base/Flooring
prefab as your base prefab, and explicitly map arguments WalkSpeed and Sprite to some hardcoded values.
A new prefab which calls some another prefab as its base is named a derived prefab.
That said, your new Floor Tile prefab becomes derived from Base/Flooring.
When a player clicks on the map pointing out where to construct an entity, Penal Engineer only passes Position to
your prefab. If you don't map Position explicitly to anything else, your prefab shall simply pass this argument as came
from UI down to Base/Flooring, and for WalkSpeed and Sprite your prefab shall do mapping of hardcoded values and
also pass them down to Base/Flooring.
Thanks to this mechanism, your prefab can have zero program code. All the internal work shall be carried out by
Base/Flooring. Your prefab only substitutes values for its base input arguments.
As your new prefab doesn't need to contain any actual program code, it makes it a perfect fit for the simple declarative syntax:
<Prefab>
<NamespacedId Value="MyMod/Carpet" />
<BasePrefabs>
<BasePrefab>
<NamespacedId Value="Base/Flooring" />
<ArgsMapping xmlns:arg="https://penalengineer.com/schema/base/flooring/args">
<arg:Sprite NamespacedId="MyMod/Floors/carpet" />
<arg:WalkSpeed Value="1.2" />
</ArgsMapping>
</BasePrefab>
</BasePrefabs>
</Prefab>
Various base prefabs define different sets of arguments. Therefore, when specifying <ArgsMapping>, you must ensure a
correct XML namespace for each argument tag.
This is what will happen when a player clicks on the map hoping to lay a tile of your hairy piece of carpet:
MyMod/Carpetgets called:- base prefab
Base/Flooringgets ready for call:- arg
WalkSpeed← mapped hardcoded value1.2
- arg
- base prefab
Base/Flooringgets called:- 2nd base prefab
Base/Renderablegets ready for call:- arg
Position← right from Penal Engineer UI (where a player clicked) - arg
Sprite← mapped hardcoded reference to a sprite namespaced IDMyMod/Floors/carpet
- arg
- 2nd base prefab
Base/Renderablegets called:- no base prefabs defined
- runs own factory, creating an entity with properly filled components
PositionandSprite
Base/Flooringdoesn't define any more 2nd base prefabsBase/Flooringruns own factory, adding the componentWalkSpeedto the entity
- 2nd base prefab
MyMod/Carpetdoesn't define any more base prefabsMyMod/Carpetdoesn't define own factory – done
- base prefab
Multiple base prefabs
Your prefab may specify more than one base prefab. For every base prefab, you point out certain arguments mapping.
Multiple base prefabs shall be invoked in the exact order you specified them. It's okay to have an argument with a same name to get mapped in multiple base prefabs.
Example: an undercover prisoner, i.e. an actual law enforcement officer (but not a prison guard) settling in a prison
pretending to be a standard criminal. He must inherit components from MainContent/LawEnforcer as well as from
MainContent/Prisoner.
<Prefab>
<NamespacedId Value="MyMod/UndercoverPrisoner" />
<BasePrefabs>
<BasePrefab>
<NamespacedId Value="MainContent/Prisoner" />
<ArgsMapping xmlns:arg="https://penalengineer.com/schema/main/characters/prisoner/args">
<arg:Sprite NamespacedId="MyMod/Characters/prisoner_undercover" />
<arg:Brutality Value=".5" />
</ArgsMapping>
</BasePrefab>
<BasePrefab>
<NamespacedId Value="MainContent/LawEnforcer" />
<ArgsMapping xmlns:arg="https://penalengineer.com/schema/main/characters/law/args">
<arg:LawsAwareness xmlns="https://penalengineer.com/schema/main/characters/law/args">
<Criminal Value="1.0" />
<Civil Value=".2" />
</arg:LawsAwareness>
</ArgsMapping>
</BasePrefab>
</BasePrefabs>
</Prefab>
Patching prefabs
A completely another way of modding is to patch an already existing prefab. For example, to replace a sprite for every grass tile.
This can be achieved by defining a new prefab taking the same ID as an existing one.
In Penal Engineer, patching a prefab doesn't replace an original one. The original prefab function does not magically disappear. Instead, the new prefab definition gets chained, or layered on top of the original prefab.
When a prefab with the ID is called, Penal Engineer does call every prefab function registered for this ID in the chain, one by one, passing the resulting entity from an older prefab to a next one.
Thanks to that, you don't need to resemble the original prefab behavior. Your new prefab should only define a nitpick change of the result of the original prefab execution.
That's why it's named patching (not substitution, replacing or overriding).
Let's consider the outlined example: you want to replace a sprite for every grass tile. Assume you already have your new
beautiful sprite defined under ID MyMod/Floors/grass_enhanced. This is how you can deal with your endeavor:
<Prefab>
<NamespacedId Value="MainContent/Grass" /> <!-- Pay attention: take the original prefab ID -->
<Factory>
<ReplaceComponents xmlns:component="https://penalengineer.com/schema/base/flooring/components">
<component:Sprite NamespacedId="MyMod/Floors/grass_enhanced" />
</ReplaceComponents>
</Factory>
</Prefab>
This looks like a normal prefab definition. But the trick is in the <NamespacedId> which matches the already existing
prefab. When Penal Engineer mod loader encounters that, it realizes the supplied prefab is not a new but a patch.
Absence of an alien namespace claim doesn't prevent patching alien prefabs.
The <Factory> is normally supposed to point at a function (either in Lua or C#) which modifies the entity resulted
in the execution of the original prefab. But it also allows for the <ReplaceComponents> tag instead of a full-fledged
script. This is exactly what we have leveraged in the sample above for sake of keeping the mod purely XML-wise.
Make sure you properly declared dependencies in your mod metadata! It's essential to ensure the correct order of applying patches supplied by various mods and plugins.
When there is no definitive chaining path based on declared dependencies, a player is in control over the ordering.
Note that the declarative XML syntax doesn't allow for really complex patching. If you need to do stuff like conditional
components replacing or injecting new components, you should refer to scripting (see below) and leave XML
<ReplaceComponents> aside.
Howling at Lua
Derivation of prefabs is powerful enough to let you supply completely new entities. But besides that, Penal Engineer has a lot more to suggest.
-
Patching existing prefabs (advanced)
We already showed off this capability with XML. But it feels not enough. While simple replacing of components can be achieved with XML, advanced patching may regard to adding new components (such as a new Need injected to all Prisoners).
-
Conditional arguments mapping
Mapping of constants to arguments of base prefabs may be not enough sometimes. You might want to provide a computed value for a base argument given the current world state or conditions.
-
Defining prefabs with typed arguments
When defining a new prefab (be it a completely new or based on some other prefab), you can introduce new arguments with concrete bound types. You can also mark them required/optional and provide default values. The engine shall validate those constraints when some code (say, another plugin) tries to call your prefab.
-
Defining new abstract base prefabs allowing for extension by other mods/plugins
You can write a "meta plugin" in extensible manner, define new XML schemas for arguments mapping.
However, none of these capabilities is exposed via the XML pure declarative syntax. This is because all these features make no use without writing your own program code, either in a Lua script or even by wiring up with a bunch of C# code.
Fortunately, the Penal Engineer Mod Scripting API in Lua is rich enough to let you go without sinking in .NET world.
Patching prefabs (advanced)
We already demonstrated above how an existing prefab can be easily patched with XML.
Now let's consider a more challenging example. You want to inject a completely new need to all prisoners, say Fight
reflecting a prisoner's natural need to smash someone's face out of his inner brutality. Nothing could be easier:
- Allow the original
MainContent/Prisonerprefab to construct the entity (prisoner). - After it, add a new component named
Fightwith some initial value (let's deduce it from the inner prisoner's traitBrutality).
Put a new Lua script returning the factory function somewhere inside your mod folder (say Scripts subfolder):
local function prisonerFactoryPatch(world, entity, args)
entity:add("MyMod/Needs/Fight", {
Title = "Fight",
Value = entity:get("MainContent/Components/Traits/Brutality").value * math.random()
})
end
return prisonerFactoryPatch
Define the patch in your mod XML:
<Prefab>
<NamespacedId Value="MainContent/Prisoner" /> <!-- Pay attention: take the original prefab ID -->
<Factory>
<Script Path="Scripts/prisoner_factory_patch_need_fight.lua" />
</Factory>
</Prefab>
That's it! All prisoners now have the new need Fight on them. Now it's worth to code a new system which shall increase
this need per prisoner with running time and decrease on him taking violence. But this is a topic for another chapter.