Skip to content

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

OffsetSizeFieldPurpose
04planteduint 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

  1. Provider.time growth — Uses unscaled server time to prevent manipulation via time scale.
  2. Rain accelerationPOST_DRIZZLE lighting event + roof check via BLOCK_WIND raycast.
  3. Double yield — Agriculture skill mastery determines chance via Random.value < mastery.
  4. Limited harvests — 2 damage per harvest against 5 base health means 2–3 harvests.
  5. 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

FeatureInteractableFarmOil PumpGeneratorWater Well
Fuel/power requiredNoYes (generator via wire)Fuel cansNo (but needs generator pump)
Growth/production timeConfigurable per crop assetContinuousN/A (consumption)N/A (static)
Player interactionHarvest by handRefuel + collectFill + toggleFill + collect
Skill scalingAgriculture masteryNoneNoneNone
Weather accelerationRain (POST_DRIZZLE)NoNoNo
Per-harvest damageYes (2 per harvest)No (indestructible while powered)No (only fuel drain)No
Spawnable itemYes (crop yield)Fuel outputPower outputWater output
Model switchingFoliage_0 → Foliage_1Single modelEngine on/offSingle model
ReplantingManual replant requiredN/ARefuelNone

Failure Modes and Common Mistakes

  1. Timezone/clock drift with Provider.timeProvider.time is 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.

  2. Rain detection failing under glass roofs — The BLOCK_WIND raycast uses Physics.Raycast(transform.position + Vector3.up, Vector3.up, 32f, RayMasks.BLOCK_WIND). If the roof material is not in the BLOCK_WIND layer 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.

  3. Double-yield not applying to harvest rewards — The agriculture skill double-yield applies to the primary grow item, but not to the harvestRewardsList. A farm with a harvestRewardsList containing bonus items only grants those once per harvest, regardless of skill level. Players with high agriculture see double primary yields but single bonus rewards.

  4. BarricadeManager.damage desync — The ReceiveHarvestRequest method calls BarricadeManager.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.

  5. Spawn table fallback producing unexpected items — When farmAsset.grow == 0, the code resolves through SpawnTableTool.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.time units (unscaled seconds). The relationship between real time and Provider.time depends on the server's tick rate and time scale. On a server with Time.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 grow field is an asset-configurable ID that can change per crop, and the fallback through SpawnTableTool.ResolveLegacyId means 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:

  • GrowAfterRealtime coroutine: one WaitForSecondsRealtime per unripe crop on the client, garbage-collected after completion.
  • onRainUpdated event: 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.time comparison and ReceiveHarvestRequest validation.

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

Document history