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 > useTimefor USE mode,> aidTimefor 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 modifier | Effect |
|---|---|
None | No change |
Heal | target.life.serverSetBleeding(false) |
Cut | target.life.serverSetBleeding(true) |
performBrokenBones
| Bones modifier | Effect |
|---|---|
None | No change |
Heal | target.life.serverSetLegsBroken(false) |
Break | target.life.serverSetLegsBroken(true) |
UseableRefill — Water Container System
Water Type State
The current water type is stored in player.equipment.state[0] as ERefillWaterType:
| Value | State | Effect on Drink |
|---|---|---|
| 0 | EMPTY | No effect |
| 1 | CLEAN | Positive stats (health, food, water) |
| 2 | SALTY | Dehydrates |
| 3 | DIRTY | Contaminates |
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: ifisUseable, 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 full → newWaterType = 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 → DIRTYServer-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 clientsStat Modification (simulate)
After the drink animation completes, simulate() applies stat changes based on refillWaterType:
| Stat | CLEAN | SALTY | DIRTY |
|---|---|---|---|
| Stamina | cleanStamina | saltyStamina | dirtyStamina |
| Oxygen | cleanOxygen | saltyOxygen | dirtyOxygen |
| Health | cleanHealth | saltyHealth | dirtyHealth |
| Food | cleanFood | saltyFood | dirtyFood |
| Water | cleanWater | saltyWater | dirtyWater |
| Virus | cleanVirus | saltyVirus | dirtyVirus |
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:
IsRubbleNullOrAllAlivemust be true — destroyed ruble blocks cannot be used. - Container source check: Tank must have
source == WATERor 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 clientsThe 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
WaterVolume (underwater point)
- Detected via
WaterUtility.isPointUnderwater(point, out volume). volume == null→ ocean water → SALTY.volume != null→volume.waterType(typically CLEAN for rivers/lakes).- Pour mode (
mode=true): always blocked — you cannot pour into the ocean.
- Detected via
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).
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).
InteractableObjectResource (water interactable)
- Level object (e.g., water pump, fountain, well).
- Must have
interactability == WATERand 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 \ To | EMPTY | CLEAN | SALTY | DIRTY |
|---|---|---|---|---|
| EMPTY | — | Take from barrel/tank/volume | Take from ocean | Take from resource |
| CLEAN | Drink/Pour into barrel/tank/resource | — | (impossible) | (impossible) |
| SALTY | Drink | (impossible) | — | (impossible) |
| DIRTY | Drink | (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
| RPC | Direction | Reliability | Payload | Purpose |
|---|---|---|---|---|
SendPlayConsume | Server → Client (excl. owner) | Unreliable | EConsumeMode | Play consume animation |
sendRaycast (Aid) | Client → Server | In input packet | Hit info | Find aid target |
getInput | Server read | N/A | InputInfo | Validate hit |
Refill RPCs
| RPC | Direction | Reliability | Payload | Purpose |
|---|---|---|---|---|
SendPlayUse | Server → Client (excl. owner) | Unreliable | None | Play drink animation |
SendPlayRefill | Server → Client (excl. owner) | Unreliable | None | Play 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, triggeringReceiveUpdateState→useable.updateState().player.equipment.updateQuality()is called for quality changes (refill sets 100 for clean, 0 for dirty).
Plugin Events
| Event | Signature | Phase |
|---|---|---|
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) |
