Skip to content

UseableConsumeable — Eating and Drinking

Overview

UseableConsumeable (526 lines) at Unturned/Useable/UseableConsumeable.cs handles consumable items: food, water, medical supplies, and any item subclassing ItemConsumeableAsset. It supports self-use and a secondary aid mode for applying items to other players. UseableRefill (658 lines) at Unturned/Useable/UseableRefill.cs handles water container refilling from sources (rain barrels, tanks, water volumes, interactable objects). Both use animation-timed consumption with stat modification on the simulation tick.

Supporting types: EConsumeMode (USE, AID), ERefillMode (USE, REFILL), ERefillWaterType (EMPTY, CLEAN, SALTY, DIRTY).


UseableConsumeable Architecture

State Machine

equip() → sets useTime (animation length of "Use") and aidTime (animation length of "Aid")
startPrimary() / startSecondary() → sets isUsing = true, consumeMode, plays animation
simulate() → when isUsing && isUseable → performUseOnSelf() or performAid()
  • isUseable: realtimeSinceStartup - startedUse > useTime for USE mode, > aidTime for AID mode.
  • EConsumeMode.USE: self-consumption (eating/drinking).
  • EConsumeMode.AID: applying to another player (bandaging, medicating).

Primary Action (Self-Use)

csharp
override bool startPrimary()
{
    isBusy = true;
    startedUse = Time.realtimeSinceStartup;
    isUsing = true;
    consumeMode = USE;
    consume(); // play animation + sound + alert
    SendPlayConsume(USE) // broadcast to other clients
}

Secondary Action (Aid)

csharp
override bool startSecondary()
{
    if (!hasAid) return false;
    // Client-side raycast (3m range)
    Ray ray = new Ray(aim.position, aim.forward);
    RaycastInfo info = DamageTool.raycast(ray, 3f, DAMAGE_CLIENT);
    player.input.sendRaycast(info, ERaycastInfoUsage.ConsumeableAid);
    // On server:
    if (info.type == PLAYER && info.player != null)
    {
        enemy = info.player;
        isBusy = true;
        consumeMode = AID;
        consume();
    }
}

Self-Consumption (performUseOnSelf)

Owner + Server (predicted and replicated)

csharp
player.life.askRest(asset.energy);                    // stamina
player.life.askView( // vision (hallucination)
    Max(currentVision, asset.vision * (1 - Immunity_mastery))
);
player.life.simulatedModifyOxygen(asset.oxygen);
player.life.simulatedModifyWarmth(asset.warmth);

Server-Only

csharp
invokeConsumeRequested(asset); // plugin gate
asset.GrantQuestRewards(player);
asset.itemRewards.grantItems(player, CRAFT);

// Health
performHealth(player, asset.health);
// Bleeding
performBleeding(player, asset.bleedingModifier);
// Broken bones
performBrokenBones(player, asset.bonesModifier);

// Food/Water
player.life.askEat(asset.food * (quality / 100));
player.life.askDrink(asset.water * (quality / 100));

// Virus
player.life.askInfect(asset.virus * (1 - Immunity_mastery * 0.5));
player.life.askDisinfect(asset.disinfectant * (1 + Healing_mastery * 0.5));

// Low-quality penalty
if (quality < 50)
    infect += (food + water) * 0.5 * (1 - quality/50) * Immunity_skill;

// Rewards
invokeConsumePerformed(asset);

// Delete or dequip
if (asset.shouldDeleteAfterUse) use(); else dequip();

// Explosive consumables
if (asset.IsExplosive)
{
    EffectManager.triggerEffect(explosionEffect);
    DamageTool.explode(position, asset.range, ...);
    if (damage > 0.5f) player.damage(101); // suicide
}

Quality Modifier

Quality (player.equipment.quality / 100f) scales food and water restoration. Low-quality items (< 50 quality) apply an infection penalty proportional to (food + water) * 0.5.


Aid Application (performAid)

When applying a consumable to another player:

csharp
onPerformingAid?.Invoke(player, enemy, asset, ref shouldAllow); // plugin gate

enemy.life.askHeal(health * (1 + instigator.Healing_mastery * 0.5), false, false);
performBleeding(enemy, asset.bleedingModifier);
performBrokenBones(enemy, asset.bonesModifier);

enemy.life.askEat(asset.food * qualityMultiplier);
enemy.life.askDrink(water clamped to (postEat - preEat) when foodConstrainsWater);

enemy.life.askInfect(asset.virus * (1 - enemy.Immunity_mastery * 0.5));
enemy.life.askDisinfect(asset.disinfectant * (1 + enemy.Healing_mastery * 0.5));

if (instigator.quality < 50)
    infect penalty on enemy;

// XP and reputation for aid
xp: + (healthDelta)/2 + (virusDelta)/2; +15 for bleed heal; +15 for bone heal
rep: +1 per stat improved (up to 4)

enemy.life.serverModifyHallucination(asset.vision * (1 - enemy.Immunity_mastery));
enemy.life.serverModifyStamina(asset.energy);
enemy.life.serverModifyWarmth(asset.warmth);
enemy.skills.ServerModifyExperience(asset.experience);

onPerformedAid?.Invoke(player, enemy);

The Healing skill of the instigator increases healing output by up to 50%. The Immunity skill of the target reduces hallucination and infection effects by up to 50%.


Perform Helpers

performHealth

csharp
float instigatorHealingSkillMultiplier = 1f + (player.skills.mastery(SUPPORT.HEALING) * 0.5f);
int roundedDelta = Mathf.RoundToInt(delta * instigatorHealingSkillMultiplier);
target.life.askHeal((byte)roundedDelta, false, false);

performBleeding

Bleeding modifierEffect
NoneNo change
Healtarget.life.serverSetBleeding(false)
Cuttarget.life.serverSetBleeding(true)

performBrokenBones

Bones modifierEffect
NoneNo change
Healtarget.life.serverSetLegsBroken(false)
Breaktarget.life.serverSetLegsBroken(true)

UseableRefill — Water Container System

Water Type State

The current water type is stored in player.equipment.state[0] as ERefillWaterType:

ValueStateEffect on Drink
0EMPTYNo effect
1CLEANPositive stats (health, food, water)
2SALTYDehydrates
3DIRTYContaminates

Quality maps directly: CLEAN = 100 quality, DIRTY = 0 quality.

Animation Timing

  • useTime: length of "Use" animation (drinking).
  • refillTime: length of "Refill" animation (filling from source).
  • startPrimary: if isUseable, attempts to deposit (pour) into a source or drink if non-empty.
  • startSecondary: attempts to withdraw (take) from a source.

Source Detection (fire(bool mode, out newWaterType))

Client-Side Detection (prediction)

csharp
Ray ray = new Ray(aim.position, aim.forward, 3f, DAMAGE_CLIENT);
if (WaterUtility.isPointUnderwater(point, out volume))
    → newWaterType = volume == null ? SALTY : volume.waterType
else if (barrel = hit.GetComponent<InteractableRainBarrel>())
    → pour: must be CLEAN and barrel not full → newWaterType = EMPTY
    → take: must not be CLEAN and barrel is fullnewWaterType = CLEAN
else if (tank = hit.GetComponent<InteractableTank>())
    → same pattern with tank.source == WATER
else if (resource = hit.GetComponentInParent<InteractableObjectResource>())
    → must be WATER interactability, rubble alive
pour: non-empty liquid → EMPTY
take: EMPTY and resource has water → DIRTY

Server-Side Validation (authoritative)

Same logic, but uses player.input.getInput() for authority. After determining validity:

  • Barrel pour: BarricadeManager.updateRainBarrel(barrel, isFull: true) / false for take.
  • Tank pour: tank.ServerSetAmount(amount + 1) / amount - 1.
  • Resource pour: ObjectManager.updateObjectResource(resource, amount + 1) / amount - 1.

Each source interaction updates the barricade/object state, which replicates to all clients.

Consume/Drink Flow

csharp
startPrimary()
    if (isUseable && fire(true, out newWaterType)) → start(newWaterType) // poured into source
    else if (waterType != EMPTY) → drink:
        isBusy = true
        refillMode = USE
        play("Use")
        state[0] = EMPTY
        updateState()
        SendPlayUse to other clients

Stat Modification (simulate)

After the drink animation completes, simulate() applies stat changes based on refillWaterType:

StatCLEANSALTYDIRTY
StaminacleanStaminasaltyStaminadirtyStamina
OxygencleanOxygensaltyOxygendirtyOxygen
HealthcleanHealthsaltyHealthdirtyHealth
FoodcleanFoodsaltyFooddirtyFood
WatercleanWatersaltyWaterdirtyWater
ViruscleanVirussaltyVirusdirtyVirus

Values come from ItemRefillAsset fields per water type. simulatedModifyStamina/Oxygen runs on owner + server; serverModifyHealth/Food/Water/Virus runs on server only.

UI Messages

csharp
switch (waterType):
    EMPTY  → "Empty"
    CLEAN  → "Clean"
    SALTY  → "Salty"
    DIRTY  → "Dirty"
    else   → "Full"

Displayed via PlayerUI.message() on equip and after refill.


Edge Cases and Safeguards

  • Distance validation: Server rejects refill actions farther than 7m from aim point (> 49 sqrMagnitude).
  • Water volume priority: Volumes override all other source types when the target point is underwater.
  • Ocean water: When underwater and not in a water volume (ocean), defaults to SALTY.
  • Full container blocking: Cannot take from a barrel that is not full, cannot pour into a barrel that is already full.
  • Resource rubble check: IsRubbleNullOrAllAlive must be true — destroyed ruble blocks cannot be used.
  • Container source check: Tank must have source == WATER or refill is rejected.
  • ShouldDeleteAfterUse: Some consumables (bandages, syringes) are consumed and removed; others (food cans) are not.
  • Explosive consumables: The item explodes after consumption, potentially killing the user if playerDamage > 0.5.

Animation Timing — Detailed Analysis

Animation Length Resolution

csharp
public override void equip()
{
    useTime = player.animator.GetAnimationLength("Use");
    if (hasAid)
        aidTime = player.animator.GetAnimationLength("Aid");
}

The GetAnimationLength method reads the animation clip's duration from the player's animator controller. The return value is in seconds. useTime and aidTime are stored as float seconds and compared against Time.realtimeSinceStartup - startedUse in the isUseable property.

Animation Speed and Sync

The animator plays "Use" or "Aid" at normal speed (no speed modifiers). Unlike weapon reload/hammer animations, consumable animations are not affected by skills — the consumption timer is purely animation-driven.

Client-Server Animation Sync

csharp
// Client (prediction):
startedUse = Time.realtimeSinceStartup; // Local timestamp
isUsing = true;
consumeMode = USE;
consume(); // Plays animation locally

// Server (authority):
SendPlayConsume.Invoke(GetNetId(), ENetReliability.Unreliable, ..., consumeMode);
// → ReceivePlayConsume(consumeMode):
//   consume(); // Plays animation on non-owning clients

The server broadcasts SendPlayConsume to all clients excluding the owner (who already predicted locally). The unreliable delivery is acceptable because the consumption outcome (stats change) is server-authoritative and the animation is purely cosmetic.

Aid Mode Timing

Aid mode uses a separate aidTime from the "Aid" animation. If hasAid is false, aidTime is never set and startSecondary() returns false immediately:

csharp
public override bool startSecondary()
{
    if (!hasAid) return false;
    // ...
}

Refill System — Environment Source Catalogue

The fire(bool mode, out newWaterType) method in UseableRefill checks five possible water sources in priority order:

Source Priority Order

  1. WaterVolume (underwater point)

    • Detected via WaterUtility.isPointUnderwater(point, out volume).
    • volume == null → ocean water → SALTY.
    • volume != nullvolume.waterType (typically CLEAN for rivers/lakes).
    • Pour mode (mode=true): always blocked — you cannot pour into the ocean.
  2. InteractableRainBarrel

    • Barricade that collects rainwater.
    • Take: barrel must be full (barrel.isFull), current container must not already have CLEAN water.
    • Pour: current container must have CLEAN water, barrel must not be full.
    • Results in barrel state change via BarricadeManager.updateRainBarrel(barrel, isFull: bool).
  3. InteractableTank (with source == ETankSource.WATER)

    • Water storage tank (plumbed water system).
    • Take: tank must have amount > 0, current container must not have CLEAN water.
    • Pour: current container must have CLEAN water, tank must not be at capacity.
    • Updates via tank.ServerSetAmount(amount ± 1).
  4. InteractableObjectResource (water interactable)

    • Level object (e.g., water pump, fountain, well).
    • Must have interactability == WATER and no destroyed rubble (IsRubbleNullOrAllAlive).
    • Take: resource must have amount > 0, container must be EMPTY → receives DIRTY water.
    • Pour: container must have non-EMPTY water, resource must not be at capacity → pours out.
    • Updates via ObjectManager.updateObjectResource(resource, amount ± 1).

Rejected States

Both client and server reject refills when:

  • Aim distance > 3m on client, > 7m on server.
  • The source type does not match any of the five accepted types.
  • Pouring into a source that is already full.
  • Taking from a source that is empty.
  • Attempting to fill a container that already has the target water type (e.g., taking CLEAN when already holding CLEAN).

Water Type Transition Matrix

From \ ToEMPTYCLEANSALTYDIRTY
EMPTYTake from barrel/tank/volumeTake from oceanTake from resource
CLEANDrink/Pour into barrel/tank/resource(impossible)(impossible)
SALTYDrink(impossible)(impossible)
DIRTYDrink(impossible)(impossible)

The only way to get CLEAN water is from a rain barrel, fresh water tank, or fresh water volume. The only way to empty a container is to drink from it or pour it into a source.


Quality and Food Constrain Water

Quality Modifier

csharp
byte water = (byte)(asset.water * (player.equipment.quality / 100f));

At 100 quality: 100% of asset.water. At 50 quality: 50%. At 0 quality: 0 water restored.

foodConstrainsWater

When enabled, water restoration cannot exceed the amount of food actually eaten:

csharp
if (asset.foodConstrainsWater)
{
    water = (byte)Mathf.Min(water, postEat - preEat);
}

This prevents items with high water values from providing water even when the player is at max food (since postEat - preEat would be 0). Used for items like soup where the water content is tied to the food matrix.


Explosive Consumable — Complete Flow

Some consumable items double as explosives (configured in ItemConsumeableAsset):

csharp
if (asset.IsExplosive)
{
    EffectAsset explosionEffect = asset.FindExplosionEffectAsset();
    if (explosionEffect != null)
    {
        TriggerEffectParameters params = new TriggerEffectParameters(explosionEffect);
        params.relevantDistance = EffectManager.LARGE;
        params.position = explosionPosition;
        params.reliable = true;
        EffectManager.triggerEffect(params);
    }

    List<EPlayerKill> kills;
    DamageTool.explode(explosionPosition, asset.range, EDeathCause.CHARGE,
        killer, playerDamage, zombieDamage, animalDamage,
        barricadeDamage, structureDamage, vehicleDamage,
        resourceDamage, objectDamage, out kills,
        damageOrigin: EDamageOrigin.Food_Explosion);

    if (asset.playerDamageMultiplier.damage > 0.5f)
    {
        player.life.askDamage(101, Vector3.up, EDeathCause.CHARGE,
            ELimb.SPINE, owner.steamID, out kill);
    }
}

The suicide clause (askDamage(101)) ensures the player dies from their own explosive consumable if it deals enough damage to be lethal to others. The explosion uses EDamageOrigin.Food_Explosion for logging and differentiation from grenade/rocket explosions.


Network Protocol Summary

Consumeable RPCs

RPCDirectionReliabilityPayloadPurpose
SendPlayConsumeServer → Client (excl. owner)UnreliableEConsumeModePlay consume animation
sendRaycast (Aid)Client → ServerIn input packetHit infoFind aid target
getInputServer readN/AInputInfoValidate hit

Refill RPCs

RPCDirectionReliabilityPayloadPurpose
SendPlayUseServer → Client (excl. owner)UnreliableNonePlay drink animation
SendPlayRefillServer → Client (excl. owner)UnreliableNonePlay refill animation

State Update

For both systems, after consumption/refill:

  • player.equipment.state[0] is updated with new water type.
  • player.equipment.updateState() is called, triggering ReceiveUpdateStateuseable.updateState().
  • player.equipment.updateQuality() is called for quality changes (refill sets 100 for clean, 0 for dirty).

Plugin Events

EventSignaturePhase
onConsumeRequested(Player, ItemConsumeableAsset, ref bool shouldAllow)Pre-consume (server)
onConsumePerformed(Player, ItemConsumeableAsset)Post-consume (server)
onPerformingAid(Player instigator, Player target, ItemConsumeableAsset, ref bool shouldAllow)Pre-aid (server)
onPerformedAid(Player instigator, Player target)Post-aid (server)