Skip to main content

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.

info

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.

Definition

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>
warning

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/Carpet gets called:
    • base prefab Base/Flooring gets ready for call:
      • arg WalkSpeed ← mapped hardcoded value 1.2
    • base prefab Base/Flooring gets called:
      • 2nd base prefab Base/Renderable gets ready for call:
        • arg Position ← right from Penal Engineer UI (where a player clicked)
        • arg Sprite ← mapped hardcoded reference to a sprite namespaced ID MyMod/Floors/carpet
      • 2nd base prefab Base/Renderable gets called:
        • no base prefabs defined
        • runs own factory, creating an entity with properly filled components Position and Sprite
      • Base/Flooring doesn't define any more 2nd base prefabs
      • Base/Flooring runs own factory, adding the component WalkSpeed to the entity
    • MyMod/Carpet doesn't define any more base prefabs
    • MyMod/Carpet doesn't define own factory – done

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.

Important technical details

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.

info

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.

warning

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:

  1. Allow the original MainContent/Prisoner prefab to construct the entity (prisoner).
  2. After it, add a new component named Fight with some initial value (let's deduce it from the inner prisoner's trait Brutality).

Put a new Lua script returning the factory function somewhere inside your mod folder (say Scripts subfolder):

Scripts/prisoner_factory_patch_need_fight.lua
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:

index.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.