Skip to content

ItemRefillAsset — Refillable Water Containers

ItemRefillAsset defines refillable water containers — canteens, water bottles, and other portable vessels that can be filled from environmental sources and consumed for hydration. At 402 lines it is the largest of the vehicle service asset classes and one of the most complex ItemAsset subclasses. It carries 18 stat fields (6 stats × 3 water types) and a sophisticated water source detection and consumption system.

Source code location: Unturned/Bundles/ItemRefillAsset.cs

Inheritance Chain

ItemAsset
  → ItemRefillAsset

Class Definition

csharp
public enum ERefillWaterType { EMPTY, CLEAN, SALTY, DIRTY }

public class ItemRefillAsset : ItemAsset
{
    protected AudioClip _use;
    public AudioClip use => _use;

    [Obsolete("Replaced by separate stats for each water type")]
    public byte water => MathfEx.RoundAndClampToByte(cleanWater);

    // 6 stats × 3 water types = 18 fields
    public float cleanHealth { get; protected set; }
    public float saltyHealth { get; protected set; }
    public float dirtyHealth { get; protected set; }

    public float cleanFood { get; protected set; }
    public float saltyFood { get; protected set; }
    public float dirtyFood { get; protected set; }

    public float cleanWater { get; protected set; }
    public float saltyWater { get; protected set; }
    public float dirtyWater { get; protected set; }

    public float cleanVirus { get; protected set; }
    public float saltyVirus { get; protected set; }
    public float dirtyVirus { get; protected set; }

    public float cleanStamina { get; protected set; }
    public float saltyStamina { get; protected set; }
    public float dirtyStamina { get; protected set; }

    public float cleanOxygen { get; protected set; }
    public float saltyOxygen { get; protected set; }
    public float dirtyOxygen { get; protected set; }
}

Water Type System

ERefillWaterType

csharp
public enum ERefillWaterType
{
    EMPTY,  // No contents, can be filled
    CLEAN,  // Safe water source (rain, purifier)
    SALTY,  // Ocean/seawater
    DIRTY   // Puddle, swamp, contaminated source
}
TypeSource ExamplesQuality
EMPTYInitial stateNo contents
CLEANRain catcher, water purifier, admin spawnFull benefits, no penalties
SALTYOcean, seawaterPartial benefits, virus penalty
DIRTYPuddles, swamp, non-clean objectsPartial benefits, virus penalty

Six Stat Categories per Type

Each water type carries six float stat fields:

StatCLEANSALTYDIRTY
HealthcleanHealthsaltyHealthdirtyHealth
FoodcleanFoodsaltyFooddirtyFood
WatercleanWatersaltyWaterdirtyWater
ViruscleanVirussaltyVirusdirtyVirus
StaminacleanStaminasaltyStaminadirtyStamina
OxygencleanOxygensaltyOxygendirtyOxygen

State Management

The water type is stored as a single byte in the item state:

csharp
public override byte[] getState(EItemOrigin origin)
{
    byte[] state = new byte[1];

    if (origin == EItemOrigin.ADMIN)
        state[0] = (byte)ERefillWaterType.CLEAN;
    else
        state[0] = (byte)ERefillWaterType.EMPTY;

    return state;
}
OriginState
ADMINCLEAN
All othersEMPTY

The state changes when the player fills the container from a water source. The type is determined by the source's water quality.

Type-Aware Getters

Six getter methods resolve the correct stat value based on the current water type:

csharp
public float GetRefillHealth(ERefillWaterType refillWaterType)
{
    switch (refillWaterType)
    {
        case ERefillWaterType.CLEAN: return cleanHealth;
        case ERefillWaterType.SALTY: return saltyHealth;
        case ERefillWaterType.DIRTY: return dirtyHealth;
        default: return 0.0f;
    }
}

Identical patterns exist for GetRefillFood, GetRefillWater, GetRefillVirus, GetRefillStamina, and GetRefillOxygen. The EMPTY type returns 0.0 for all stats.

PopulateAsset — Default Value System

The parsing uses a cascading default system. CLEAN values are explicit; SALTY and DIRTY default to fractions of the CLEAN values:

csharp
float legacyWaterValue = p.data.ParseFloat("Water");
const float defaultSaltyFactor = 0.25f;
const float defaultDirtyFactor = 0.6f;

cleanHealth = p.data.ParseFloat("Clean_Health", 0.0f);
saltyHealth = p.data.ParseFloat("Salty_Health", cleanHealth * defaultSaltyFactor);
dirtyHealth = p.data.ParseFloat("Dirty_Health", cleanHealth * defaultDirtyFactor);

cleanWater = p.data.ParseFloat("Clean_Water", legacyWaterValue);
saltyWater = p.data.ParseFloat("Salty_Water", cleanWater * defaultSaltyFactor);
dirtyWater = p.data.ParseFloat("Dirty_Water", cleanWater * defaultDirtyFactor);

cleanVirus = p.data.ParseFloat("Clean_Virus", 0.0f);
saltyVirus = p.data.ParseFloat("Salty_Virus", cleanWater * (-1.0f + defaultSaltyFactor));
dirtyVirus = p.data.ParseFloat("Dirty_Virus", cleanWater * (-1.0f + defaultDirtyFactor));

Default Value Rationale

  • SALTY = 25%: Salty water provides about a quarter of the clean benefits.
  • DIRTY = 60%: Dirty water provides about 60% of clean benefits.
  • Virus negative: The virus default for salty/dirty is negative (adds virus): cleanWater * (-1 + factor). For dirty water: cleanWater * (-1 + 0.6) = cleanWater * -0.4.

Legacy Water Key

The legacy Water key (deprecated) provides the default for Clean_Water:

csharp
float legacyWaterValue = p.data.ParseFloat("Water");
cleanWater = p.data.ParseFloat("Clean_Water", defaultValue: legacyWaterValue);

BuildDescription

The description renders water-type-specific information with full color coding:

  1. Water type label: DISPLAYS "Empty," "Clean," "Salty," or "Dirty" based on state byte.
  2. Per-stat display: Each non-zero stat appears with green/red color:
    • Positive: green, DescSort_RefillStat + DescSort_Beneficial.
    • Negative: red, DescSort_RefillStat + DescSort_Detrimental.
    • Zero: omitted.
  3. Values rounded: All stat values are rounded to integers for display.
csharp
ERefillWaterType waterType = (ERefillWaterType)itemInstance.state[0];
string waterKey = waterType switch
{
    ERefillWaterType.EMPTY => "Empty",
    ERefillWaterType.CLEAN => "Clean",
    ERefillWaterType.SALTY => "Salty",
    ERefillWaterType.DIRTY => "Dirty",
    _ => "Full"
};

builder.Append(localization.format("Refill", localization.format(waterKey)), DescSort_Important);

UseableRefill Runtime Behavior

Water Source Detection

The UseableRefill class detects water sources through multiple methods:

Source TypeDetection MethodResulting Type
Rain barrelBarricade raycast + EBuild.BARREL_RAIN checkCLEAN
Water purifierGameObject tag checkCLEAN
Clean tank (ETankSource.WATER)InteractableTank checkCLEAN
Ocean waterWaterUtility.isInWater + ocean biomeSALTY
Swamp/puddleWaterUtility.isInWater + biome checkDIRTY
Dirty tankInteractableTank source checkDIRTY

Fill and Consumption

  1. Fill: Player faces a water source with an empty or partially filled container.
  2. State update: The state byte changes to the source's water type.
  3. Consumption: Player left-clicks to drink from the container.
  4. Stat application: Stats from the current water type are applied to the player.
  5. Persistence: The container is not deleted after drinking — the state changes from CLEAN/SALTY/DIRTY to EMPTY.
  6. Refill: An empty container can be refilled at any water source.

Water Quality Comparison (Default Values)

Assuming Clean_Water=30:

QualityHealthFoodWaterVirusStaminaOxygen
CLEAN00+30000
SALTY00+7.5-22.500
DIRTY00+18-1200

The virus penalty for salty water is severe (30 * -0.75 = -22.5), making it dangerous without purification. Dirty water's penalty is moderate (30 * -0.4 = -12).

Common Issues

  1. Refill stat legacy field — The deprecated water field provides cleanWater for backwards compatibility. Mods that set only Water instead of Clean_Water will have correct clean values but default salty/dirty values (via the * 0.25 / * 0.6 fallback).
  2. Refill item reuseItemRefillAsset does not have a shouldDeleteAfterUse field. All refill items persist after use (the state changes from CLEAN to EMPTY rather than the item being consumed). An empty refill item can be refilled at any water source.
  3. Salty water virus default — The default virus for salty water is cleanWater * (-1 + 0.25) = cleanWater * -0.75. This can produce unexpectedly high virus values. Modders who set only Clean_Water may not realize the auto-calculated salty virus value.
  4. Empty state shows full stat potential — The description UI shows stats based on the current water type, not the potential. An empty container shows all stats as zero, making it hard to compare containers in inventory.
  5. No quality tracking — Unlike ItemConsumeableAsset, refill items have no showQuality or mold system. Water type and purity are determined by the source, not by quality decay.