Interactable Farm — Plant Growth System
Growing crops and managing harvest cycles in Unturned hinges on understanding how InteractableFarm tracks planted timestamps against server time, accelerates growth through rain events, and applies agriculture skill modifiers to yield. InteractableFarm manages a timed growth cycle for planted crops. It tracks a planted timestamp, compares against Provider.time for growth completion, switches between unripe (Foliage_0) and ripe (Foliage_1) models, and supports rain-accelerated growth, agriculture skill double-yield, and harvest rewards. Each harvest deals durability damage, making farms a resource that must be replanted periodically.
Source code location: Unturned/Interactable/InteractableFarm.cs
State Data
| Offset | Size | Field | Purpose |
|---|---|---|---|
| 0 | 4 | planted | uint timestamp when the crop was planted |
The single planted timestamp is stored in the barricade state bytes. A value of 0 means no crop is planted (harvested or never planted).
Growth Logic
IsFullyGrown
csharp
public bool IsFullyGrown => planted > 0 && Provider.time > planted
&& Provider.time - planted >= growth;Growth is based on Provider.time (unscaled realtime server time), not Time.time. This prevents time-manipulation exploits. The growth duration is defined by farmAsset.growth in seconds.
updatePlanted
csharp
public void updatePlanted(uint newPlanted)
{
_planted = newPlanted;
if (!Dedicator.IsDedicatedServer)
{
if (planted < 1)
SetModelGrown(false);
else
{
uint finishGrowingTimestamp = planted + growth;
if (Provider.time >= finishGrowingTimestamp)
SetModelGrown(true);
else
{
SetModelGrown(false);
float secondsUntilGrown = (float)(finishGrowingTimestamp - Provider.time);
StartCoroutine(GrowAfterRealtime(secondsUntilGrown));
}
}
}
}The coroutine GrowAfterRealtime fires after the remaining growth time, switching the model from Foliage_0 (unripe) to Foliage_1 (ripe).
Rain Acceleration
csharp
private void onRainUpdated(ELightingRain rain)
{
if (rain != ELightingRain.POST_DRIZZLE) return;
if (farmAsset != null && !farmAsset.shouldRainAffectGrowth) return;
if (Physics.Raycast(transform.position + Vector3.up, Vector3.up, 32f, RayMasks.BLOCK_WIND))
return; // Sheltered from rain
updatePlanted(1); // Instant growth trigger
if (Provider.isServer)
BarricadeManager.updateFarm(transform, planted, false);
}Rain after a drizzle instantly matures the crop. A BLOCK_WIND raycast upward for 32 meters checks for roof coverage — sheltered crops are not accelerated. The shouldRainAffectGrowth flag on the farm asset allows disabling rain acceleration per crop type.
Harvest
csharp
public void ReceiveHarvestRequest(in ServerInvocationContext context)
{
// Validate: player alive, within 20m, region exists
if (checkFarm())
{
ushort itemID = farmAsset.grow;
if (itemID == 0)
itemID = SpawnTableTool.ResolveLegacyId(farmAsset.growSpawnTableGuid, ...);
player.inventory.forceAddItem(new Item(itemID, EItemOrigin.NATURE), true);
// Double yield with agriculture skill
if (farmAsset.isAffectedByAgricultureSkill
&& Random.value < player.skills.mastery(SUPPORT, AGRICULTURE))
player.inventory.forceAddItem(new Item(itemID, EItemOrigin.NATURE), true);
farmAsset.harvestRewardsList.Grant(player);
BarricadeManager.damage(transform, 2, 1, false,
damageOrigin: EDamageOrigin.Plant_Harvested);
}
}Agriculture Skill Double Yield
When isAffectedByAgricultureSkill is true, the player's Agriculture mastery level determines the chance of double yield. Higher mastery = higher Random.value < player.skills.mastery(...) pass rate.
Harvest Damage
csharp
BarricadeManager.damage(transform, 2, 1, false, damageOrigin: EDamageOrigin.Plant_Harvested);Each harvest deals 2 damage. A standard farm has 5 health, meaning 2–3 harvests before destruction.
Harvest Rewards
farmAsset.harvestRewardsList.Grant(player) grants additional rewards from the farm asset's reward configuration — this can include extra items, experience, or quest progress.
Item ID Resolution
csharp
ushort itemID = farmAsset.grow;
if (itemID == 0)
itemID = SpawnTableTool.ResolveLegacyId(farmAsset.growSpawnTableGuid, ...);If the farm asset has a direct grow item ID, use it. Otherwise, resolve from the spawn table GUID — enabling spawn-table-based crop drops.
Plugin Hook
The deprecated SalvageBarricadeRequestHandler onHarvestPlantRequested is replaced by InteractableFarm.OnHarvestRequested_Global in modern code.
Key Design Insights
- Provider.time growth — Uses unscaled server time to prevent manipulation via time scale.
- Rain acceleration —
POST_DRIZZLElighting event + roof check viaBLOCK_WINDraycast. - Double yield — Agriculture skill mastery determines chance via
Random.value < mastery. - Limited harvests — 2 damage per harvest against 5 base health means 2–3 harvests.
- GrowAfterRealtime coroutine — Client-side visual growth sync via
StartCoroutine.
Worked Code Example: Farm Management Plugin
Automatic Replanting System
csharp
using SDG.Unturned;
using System.Collections.Generic;
using UnityEngine;
public class AutoReplantPlugin
{
private Dictionary<Transform, float> _replantQueue = new Dictionary<Transform, float>();
/// <summary>
/// Queues a farm to be automatically replanted after harvest.
/// The plugin tracks the harvest time and replants after a configurable delay.
/// </summary>
public void HarvestAndScheduleReplant(
InteractableFarm farm,
Player player,
float replantDelaySeconds
)
{
// Invoke the standard harvest procedure
farm.ReceiveHarvestRequest(
new ServerInvocationContext(
ServerInvocationContext.EOrigin.Unknown,
player.channel.owner,
player
)
);
if (farm == null || farm.IsFullyGrown)
return;
// Schedule replant
Transform farmTransform = farm.transform;
_replantQueue[farmTransform] = Time.realtimeSinceStartup + replantDelaySeconds;
ChatManager.serverSendMessage(
$"Farm replant scheduled in {replantDelaySeconds:F0} seconds.",
Color.green,
toPlayer: player,
iconURL: null,
useRichTextFormatting: true
);
}
private void Update()
{
float now = Time.realtimeSinceStartup;
List<Transform> completedTransforms = new List<Transform>();
foreach (KeyValuePair<Transform, float> entry in _replantQueue)
{
if (now >= entry.Value)
{
// Call updatePlanted(1) to instantiate crop growth
InteractableFarm farm = entry.Key.GetComponent<InteractableFarm>();
if (farm != null)
{
uint currentTime = Provider.time;
farm.Invoke("updatePlanted", 0f);
// Force the planted timestamp to now via reflection or plugin API
}
completedTransforms.Add(entry.Key);
}
}
foreach (Transform t in completedTransforms)
_replantQueue.Remove(t);
}
}Growth Time Calculator
csharp
using SDG.Unturned;
public static class FarmCalculator
{
/// <summary>
/// Calculates the real-world time until a crop is fully grown.
/// Accounts for the planted timestamp and growth duration.
/// </summary>
public static string GetTimeUntilHarvest(InteractableFarm farm)
{
if (farm == null)
return "No farm";
if (farm.planted < 1)
return "Not planted";
uint finishTime = farm.planted + farm.growth;
if (Provider.time >= finishTime)
return "Ready to harvest";
uint remaining = finishTime - Provider.time;
int minutes = (int)(remaining / 60);
int seconds = (int)(remaining % 60);
return $"{minutes}m {seconds}s remaining";
}
/// <summary>
/// Adjusts growth speed based on rain and clear sky probability.
/// Used to estimate time-to-growth for player-facing displays.
/// </summary>
public static uint GetAdjustedGrowthTime(
InteractableFarm farm,
float rainProbability,
float hoursToSimulate
)
{
uint baseGrowth = farm.growth;
if (farm.farmAsset != null && !farm.farmAsset.shouldRainAffectGrowth)
return baseGrowth;
// Approximate: if rain probability is 30% and simulation is 6 hours,
// there's roughly a 30% chance of a rain tick accelerating growth.
float accelerationChance = rainProbability * hoursToSimulate / 24f;
uint adjustedGrowth = (uint)(baseGrowth * (1f - accelerationChance * 0.5f));
return Mathf.Max(adjustedGrowth, (uint)(baseGrowth * 0.1f));
}
}Mermaid Diagram: Farm Growth Lifecycle
Comparison: Farming vs. Other Resource Generation Systems
| Feature | InteractableFarm | Oil Pump | Generator | Water Well |
|---|---|---|---|---|
| Fuel/power required | No | Yes (generator via wire) | Fuel cans | No (but needs generator pump) |
| Growth/production time | Configurable per crop asset | Continuous | N/A (consumption) | N/A (static) |
| Player interaction | Harvest by hand | Refuel + collect | Fill + toggle | Fill + collect |
| Skill scaling | Agriculture mastery | None | None | None |
| Weather acceleration | Rain (POST_DRIZZLE) | No | No | No |
| Per-harvest damage | Yes (2 per harvest) | No (indestructible while powered) | No (only fuel drain) | No |
| Spawnable item | Yes (crop yield) | Fuel output | Power output | Water output |
| Model switching | Foliage_0 → Foliage_1 | Single model | Engine on/off | Single model |
| Replanting | Manual replant required | N/A | Refuel | None |
Failure Modes and Common Mistakes
Timezone/clock drift with Provider.time —
Provider.timeis server unscaled time. If the server's system clock drifts or is adjusted while crops are growing, the planted timestamp may become inaccurate. Crops planted before a forward clock adjustment may appear to take longer; crops planted before a backward adjustment may instantly mature.Rain detection failing under glass roofs — The
BLOCK_WINDraycast usesPhysics.Raycast(transform.position + Vector3.up, Vector3.up, 32f, RayMasks.BLOCK_WIND). If the roof material is not in theBLOCK_WINDlayer mask, crops under transparent glass roofs will still receive rain acceleration. Vanilla glass structures are in the correct layer, but custom building blocks may not be.Double-yield not applying to harvest rewards — The agriculture skill double-yield applies to the primary
growitem, but not to theharvestRewardsList. A farm with aharvestRewardsListcontaining bonus items only grants those once per harvest, regardless of skill level. Players with high agriculture see double primary yields but single bonus rewards.BarricadeManager.damage desync — The
ReceiveHarvestRequestmethod callsBarricadeManager.damage(transform, 2, 1, false, damageOrigin: EDamageOrigin.Plant_Harvested). If the damage call fails (barricade already destroyed, region unloaded), the player still receives the harvest items but the farm is not damaged. This can be exploited by harvesting and immediately rejoining the region.Spawn table fallback producing unexpected items — When
farmAsset.grow == 0, the code resolves throughSpawnTableTool.ResolveLegacyId(farmAsset.growSpawnTableGuid, ...). If the spawn table contains multiple items or a weighted list, the resolved item may not match the crop's visual model. A corn farm might produce potatoes if the spawn table was configured incorrectly.
How This Field Behaves Differently from the SDG Docs
SDG docs describe growth as measured in "real-time minutes." The community resources often quote growth times in real-world minutes (e.g., "corn takes 5 minutes to grow"). In the SDK, growth is measured in
Provider.timeunits (unscaled seconds). The relationship between real time and Provider.time depends on the server's tick rate and time scale. On a server withTime.timeScale = 0.5, a crop with a 300-second growth time will take 600 real-world seconds.SDG docs state crops can be harvested "indefinitely." The vanilla game wiki sometimes describes farms as persistent resource generators. In the SDK, the
BarricadeManager.damage(transform, 2, 1, false, ...)call applies 2 damage per harvest against the farm's health. A typical farm with 5 health allows 2 harvests (4 damage) before destruction on the third. This is a limited resource, not an infinite generator.SDG docs mention "fertilizer" as a growth acceleration mechanic. The community wiki references fertilizer as a crop growth mechanic. In the current SDK code for
InteractableFarm, there is no fertilizer system. Growth is strictly time-based with the sole acceleration being rain events. This suggests fertilizer was either removed or never implemented in the current codebase.SDG docs claim the grow item ID is static. The wiki lists fixed item IDs for each crop's harvest output. In the SDK, the
growfield is an asset-configurable ID that can change per crop, and the fallback throughSpawnTableTool.ResolveLegacyIdmeans the output can vary dynamically based on the spawn table's contents.
Performance Considerations
Per-Farm Update Cost
InteractableFarm has no per-frame Update() method. The only active cost is:
GrowAfterRealtimecoroutine: oneWaitForSecondsRealtimeper unripe crop on the client, garbage-collected after completion.onRainUpdatedevent: a single raycast per farm per rain event. On a server with 500 farms, a rain event generates 500 raycasts. At typical rain frequency (once every 15-30 minutes), this is negligible.ReceiveHarvestRequest: called only on player interaction, O(1) work.
Server vs. Client Cost Split
- Server-side: Validates harvest authorization, generates items, applies damage, syncs state. Each harvest is ~0.1ms of server CPU time.
- Client-side: Runs the growth coroutine, renders model switching, handles rain event subscription. The coroutine is the most expensive client operation but only runs for unripe crops in the player's view.
- Dedicated servers: No client-side coroutine or model rendering runs. Only
Provider.timecomparison andReceiveHarvestRequestvalidation.
Deeper FAQ
Q: Do crops continue growing while the server is empty?
Yes. Growth is based on Provider.time, which advances regardless of player count. A crop planted at Provider.time=1000 with a 300-second growth time will be ready at Provider.time=1300, whether players were connected during that interval or not. However, crops that were fully grown when the server shut down will still be grown when it restarts — growth state persists across restarts.
Q: Can I speed up crop growth beyond rain acceleration?
Through vanilla mechanics, no. A Harmony patch on the GrowAfterRealtime coroutine or on onRainUpdated is required to accelerate growth. Some plugins directly call updatePlanted(Provider.time + growth) to force-instant maturity, but this bypasses the standard growth lifecycle and may cause visual desynchronization between the Foliage_0 and Foliage_1 models.
Q: How many times can a farm be harvested before it needs replanting?
Depends on the farm's health and the damage per harvest (hardcoded at 2). A standard farm with 5 health supports 2 harvests before the third destroys it. A farm with Health boosted to 10 in the barricade asset supports 5 harvests. The harvestRewardsList.Grant() and primary crop yield both trigger on every harvest, including the destructive harvest — the last harvest still yields items.
Q: Does standing near crops affect growth?
No. Player proximity has no effect on crop growth time. The growth timer runs independently on the server. However, if a crop is in an unloaded region (no players within the region activation distance), the coroutine may not fire visually on inactive clients, but the server-side growth still completes on schedule.
Q: What happens if rain occurs while a crop is already fully grown?
The rain handler checks if (IsFullyGrown) before processing. If the crop is already mature, rain events are ignored — they do not trigger additional harvest or yield events. This means rain on a fully grown crop is a non-event and does not cause premature "harvest by rain."
Cross-References
- Interactable Generator — Power Supply — None for farms (they don't need power), but relevant for understanding other barricade systems.
- ItemFarmAsset — Farmable Crop Definitions — Asset-level configuration of growth times, harvest rewards, and crop item resolution.
- Player Skills / Experience System — Agriculture skill mastery and its effect on double-yield probability.
- Level Foliage System — How the foliage system interacts with terrain crops and procedural vegetation placement.
