How this addon retrofits Enfusion Persistence Framework (EPF) onto a third-party mod's prefabs without forking the upstream mod. Written up as a reference pattern for anyone doing the same against Growing Weed (or any other mod that spawns entities via action-configurable prefab references).
The pattern applies whenever:
- You do not own the upstream mod and cannot modify its prefabs.
- The upstream prefabs are spawned dynamically at runtime via
Resource.Load+SpawnEntityPrefab, driven by attributes on scripted user-actions / components. - You need those spawned entities (and optionally items given to inventory) to survive server restarts.
If those don't hold, you probably want to add EPF_PersistenceComponent directly to the upstream prefabs (owner-side) instead.
The problem
Upstream Growing Weed ships six plant prefabs (2 species × 3 stages) plus a filled pot, an empty pot, and two seed items. All state changes flow through four actions and one component:
Class | Spawns | Attribute holding target prefab |
|---|---|---|
| Filled pot |
|
| Stage-1 seedling |
|
| Next-stage plant |
|
| Post-harvest filled pot |
|
| Post-uproot empty pot / filled pot |
|
None of the upstream prefabs carry EPF_PersistenceComponent, so nothing survives a restart.
The approach
Four pieces:
- Persistent prefab variants, each inheriting from an upstream prefab and only adding
EPF_PersistenceComponent. No behavioural changes. - A static redirect map (
FF_GW_PersistenceRedirect) mapping each upstream resource name to its persistent variant. modded classaction/component overrides that funnel every spawn through the redirect map by rewriting the resource-name attribute right before callingsuper.- A component save-data class for
GW_GrowthComponentso that growth-timer progress (m_fElapsedInGameHours) round-trips through EPF alongside the entity itself.
Because upstream reads its own prefabs from its own attributes (which point at upstream resource GUIDs), and downstream stages are chained through those same attributes on the previous stage, redirecting a single link in the chain isn't enough — you have to redirect every action's spawn. But once every spawn goes through the redirect, chained transitions between persistent variants become the default: a persistent Stage-1's m_rNextStagePrefab still nominally points to upstream's Stage-2, but our modded GW_GrowthComponent.EOnInit rewrites it to our persistent Stage-2 before ticking starts.
Piece 1 — Inherited persistent prefabs
An inherited .et that adds nothing but EPF_PersistenceComponent:
GenericEntity : "{UPSTREAM_GUID}Prefabs/Plants/GW_WeedPlant_Sativa_Stage1.et" {
ID "6A5AF14330000021"
components {
EPF_PersistenceComponent "6A5AF14330000022" {
m_bStorageRoot 1
m_pSaveData EPF_ItemSaveDataClass "6A5AF14330000023" {
m_aComponents {
GW_GrowthComponentSaveDataClass "6A5AF14330000024" {
}
}
}
}
}
}Companion .meta:
MetaFileClass {
Name "{6A5AF14330000020}Prefabs/Plants/GW_WeedPlant_Sativa_Stage1_Persistent.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass XBOX_ONE : PC {}
EntityTemplateResourceClass XBOX_SERIES : PC {}
EntityTemplateResourceClass PS4 : PC {}
EntityTemplateResourceClass PS5 : PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}Two attribute choices matter:
m_bStorageRoot—1for world-placed entities (plants, filled pot on the ground);0for entities that live inside another storage root (inventory items).m_pSaveDataclass — useEPF_ItemSaveDataClasseven for world-placed non-item entities.EPF_EntitySaveDataClasslooks like it should be the right base class but EPF rejects it at runtime with "Missing or invalid save-data type in persistence component".EPF_ItemSaveDataClassis the concrete class the framework actually recognises.
Piece 2 — Redirect map
class FF_GW_PersistenceRedirect
{
static const ResourceName UP_SATIVA_1 = "{4673B6D9CFD85841}Prefabs/Plants/GW_WeedPlant_Sativa_Stage1.et";
static const ResourceName OUR_SATIVA_1 = "{6A5AF14330000020}Prefabs/Plants/GW_WeedPlant_Sativa_Stage1_Persistent.et";
// ...one pair per upstream prefab...
static ResourceName Get(ResourceName input)
{
if (input == UP_SATIVA_1) return OUR_SATIVA_1;
// ...
return input;
}
}Idempotent by design: passing in a persistent variant returns it unchanged. Safe to call twice.
Piece 3 — Modded action/component overrides
Every place that reads a ResourceName attribute and hands it to Resource.Load gets a one-line override that rewrites the attribute through the redirect map before delegating to super:
modded class GW_FillPotAction
{
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
{
m_rFilledPotPrefab = FF_GW_PersistenceRedirect.Get(m_rFilledPotPrefab);
super.PerformAction(pOwnerEntity, pUserEntity);
}
}Same pattern for GW_PlantAction.m_rSeedlingPrefab, GW_HarvestAction.m_rReplacementPrefab, GW_UprootAction.m_rReplacementPrefab. The growth component redirects at EOnInit instead so that the field is fixed once before ticking begins:
modded class GW_GrowthComponent
{
override void EOnInit(IEntity owner)
{
m_rNextStagePrefab = FF_GW_PersistenceRedirect.Get(m_rNextStagePrefab);
super.EOnInit(owner);
}
}Mutation is per-instance (each action/component instance is owned by one entity), so rewriting the attribute is safe and permanent for the entity's lifetime.
Cross-variant matching
GW_PlantAction gates on prefabName == m_rRequiredSeedItem — a plain string compare. Once the player carries a persistent seed but upstream's filled-pot prefab still holds the upstream seed in m_rRequiredSeedItem, the compare fails. Fix: extend the matcher to also accept the persistent equivalent of the required seed:
modded class GW_PlantAction
{
override bool IsSeedMatch(IEntity item)
{
if (super.IsSeedMatch(item))
return true;
if (!item || m_rRequiredSeedItem.IsEmpty())
return false;
EntityPrefabData prefabData = item.GetPrefabData();
if (!prefabData)
return false;
ResourceName persistentEquivalent = FF_GW_PersistenceRedirect.Get(m_rRequiredSeedItem);
if (persistentEquivalent == m_rRequiredSeedItem)
return false;
return prefabData.GetPrefabName() == persistentEquivalent;
}
}Any equality check against a hard-coded prefab reference in upstream needs the same treatment.
Piece 4 — Component save-data class
For growth-timer persistence, EPF needs to know how to serialise and restore GW_GrowthComponent.m_fElapsedInGameHours. That means a paired attribute-class / runtime-class:
[EPF_ComponentSaveDataType(GW_GrowthComponent), BaseContainerProps()]
class GW_GrowthComponentSaveDataClass : EPF_ComponentSaveDataClass
{
}
class GW_GrowthComponentSaveData : EPF_ComponentSaveData
{
float m_fElapsedInGameHours;
override EPF_EReadResult ReadFrom(IEntity owner, GenericComponent component, EPF_ComponentSaveDataClass attributes)
{
GW_GrowthComponent gc = GW_GrowthComponent.Cast(component);
if (!gc)
return EPF_EReadResult.ERROR;
m_fElapsedInGameHours = gc.GetElapsedInGameHours();
return EPF_EReadResult.OK;
}
override EPF_EApplyResult ApplyTo(IEntity owner, GenericComponent component, EPF_ComponentSaveDataClass attributes)
{
GW_GrowthComponent gc = GW_GrowthComponent.Cast(component);
if (!gc)
return EPF_EApplyResult.ERROR;
gc.SetElapsedInGameHours(m_fElapsedInGameHours);
return EPF_EApplyResult.OK;
}
override bool Equals(notnull EPF_ComponentSaveData other)
{
GW_GrowthComponentSaveData o = GW_GrowthComponentSaveData.Cast(other);
if (!o) return false;
return m_fElapsedInGameHours == o.m_fElapsedInGameHours;
}
}Upstream m_fElapsedInGameHours is protected, so we expose read/write accessors via a modded class:
modded class GW_GrowthComponent
{
float GetElapsedInGameHours() { return m_fElapsedInGameHours; }
void SetElapsedInGameHours(float value)
{
m_fElapsedInGameHours = value;
m_fLastAbsoluteHour = -1; // rebase delta accumulator so ticking resumes cleanly
}
}Wire the save-data class into the persistent prefab's EPF_ItemSaveDataClass.m_aComponents (shown in Piece 1 above). EPF picks it up via the EPF_ComponentSaveDataType(GW_GrowthComponent) attribute.
EPF-attribute gotchas we hit
The exact attribute forms drift between EPF versions. In the version we're on:
[EPF_ComponentSaveDataType(GW_GrowthComponent, "")]— wrong. Compiler error "Too many parameters for 'EPF_ComponentSaveDataType' method". Drop the second argument.[EDF_DbName.Automatic()]on the runtime class — wrong. Compiler error "Expected attribute call". Omit the attribute; the DB name is auto-derived from the class name when it's absent.
Both errors surface at script-reload time, not later, so you'll see them immediately.
Runtime flow
Server boot, player fills an empty pot, plants a Sativa seed, timer ticks, server restarts:
GW_FillPotAction.PerformAction— modded override rewritesm_rFilledPotPrefabfrom upstream filled-pot to our persistent variant, then callssuperwhich spawns it.GW_PlantAction.PerformAction— modded override rewritesm_rSeedlingPrefabfrom upstream Sativa-Stage-1 to our persistent variant.IsSeedMatchaccepts the persistent seed the player is carrying.superconsumes the seed and spawns the persistent Stage-1.- Persistent Stage-1's
EPF_PersistenceComponentregisters with EPF atOnPostInit. EPF starts snapshotting it, including the component save-data pulled fromGW_GrowthComponent(elapsed hours). - Timer ticks. On EPF's next snapshot the persistent Stage-1 entity + growth save-data are written to the DB.
- Server shuts down.
- Server restarts. EPF spawns the persistent Stage-1 at the saved transform,
ApplyTowritesm_fElapsedInGameHoursback before ticking begins,EOnInitrewritesm_rNextStagePrefabto our persistent Stage-2, ticking resumes from the saved elapsed hours.
The same pipeline covers uproot (both empty-pot and filled-pot replacement), harvest (filled-pot replacement), and inventory items (empty pot / seeds sold by the trader as persistent variants directly).
Limitations
- World-editor-placed upstream prefabs are not persistent. Only entities spawned via the mod's own action pipeline flow through the redirect. If a scenario places a Stage-3 upstream plant for raid loot, it will disappear on restart. Placing the persistent variant in the editor instead would fix it, but this addon does not do that.
- New upstream prefabs are not automatically covered. If Growing Weed ships a new strain, you need to add another pair of persistent variants + redirect-map entries. Same for any new spawn attribute added to an existing action.
m_iSupplyCostand other arsenal-side attributes are not inherited. Inherited prefabs pick up base attributes on the parent, but if you need to override attribute values on the persistent variant, do it explicitly.
