Skip to content

CustomLiquidBuilder

QinShenYu edited this page Jun 7, 2026 · 3 revisions

CustomLiquidBuilder

Namespace: ScavLib.liquid

Registers custom liquid types into the game's Liquids.Registry dictionary. Custom liquids participate fully in the existing systems: they can be poured into WaterContainerItems, used as RecipeBuilder ingredients or results, drunk, and (optionally) injected.

Registration is safe at Awake() time — entries are queued and applied during the same Harmony Postfix that flushes custom items and recipes.


Quick Start

using ScavLib.liquid;
using UnityEngine;

private void Awake()
{
    CustomLiquidBuilder.Create("mymod_tonic")
        .DisplayName("Bitter Tonic")
        .Color(new Color(0.35f, 0.55f, 0.20f))
        .ValuePerLiter(8f)
        .Quality("medicine", 0.4f)
        .OnDrink((ml, body) =>
        {
            body.painShock      -= 10f * (ml / 100f);
            body.sicknessAmount  = Mathf.Max(0f, body.sicknessAmount - ml * 0.05f);
        })
        .HealthUsable()
        .OnHealthUse((ml, limb) =>
        {
            limb.skinHealAmount += 5f * (ml / 50f);
        })
        .Register();
}

CustomLiquidBuilder

A chainable builder. Each configuration method returns this; commit with .Register().

Creation

CustomLiquidBuilder.Create(string liquidId)
Parameter Type Description
liquidId string Unique liquid ID. Prefix with your mod name (e.g. "mymod_tonic"). This is the key used by Liquids.Registry, WaterContainerItem.AddLiquid(id, ml), and RecipeBuilder.IngredientLiquid(id, ml).

Display

.DisplayName(string en)
.DisplayName(string lang, string text)
.Color(Color color)                          // LiquidType.color
.LocaleFromItem(bool value = true)           // LiquidType.localeFromItem

DisplayName is a thin wrapper over LocaleRegistry; call it once per language, or use LocaleRegistry.AddItems(...) for bulk loading.

When LocaleFromItem(true) is set, the game pulls the display string from whichever item is acting as the container instead of the liquid's own locale entry, matching the vanilla localeFromItem flag.

Economy

.ValuePerLiter(float value)                  // LiquidType.valuePerLiter

Qualities

.Quality(string qualityId, float amount)
.Quality(CraftingQuality quality)

Adds an entry to LiquidType.qualities. Qualities are how the game identifies a liquid for crafting-by-quality matches — e.g. tagging a custom liquid with "disinfectant" (amount 0.4f) makes it eligible for any recipe declared via IngredientLiquidByQuality("disinfectant", ...).

Quality amounts are interpreted per liter. At craft time the game calls LiquidType.GetScaledQualities(ml) to scale them down to the actually- consumed volume.

Consumption Hooks

.OnDrink(LiquidType.OnDrink callback)
//   void (float ml, Body body)

.HealthUsable(bool value = true)
.OnHealthUse(LiquidType.OnHealthUse callback)
//   void (float ml, Limb limb)

Delegate signatures are exactly the game's:

public delegate void OnDrink(float ml, Body body);
public delegate void OnHealthUse(float ml, Limb limb);

The ml parameter is the volume actually consumed in this invocation, not the liquid's total amount in the container — multiplying status effects by ml / 100f is the typical pattern.

HealthUsable(true) must be called for OnHealthUse to be reachable; the game checks LiquidType.healthUsable before exposing the limb-treatment UI for the liquid.

Injection

.Injectable(bool value = true)               // LiquidType.injectable
.InjectionSickness(float multiplier)         // LiquidType.injectionSickness; default 1f

Injectable liquids can be administered through syringes / IVs. The injectionSickness multiplier scales the sickness penalty applied when the liquid is injected, mirroring the vanilla field of the same name.

Registration

.Register()
.Register(bool overwrite)

Register(overwrite: true) replaces an existing Liquids.Registry entry with the same ID. Duplicate registration without overwrite logs a warning and is ignored.


Worked Example — Antiseptic

A custom antiseptic that doubles as a "disinfectant" quality for crafting and inflicts mild sickness when injected:

using ScavLib.liquid;
using UnityEngine;

private void Awake()
{
    CustomLiquidBuilder.Create("mymod_antiseptic")
        .DisplayName("Antiseptic Solution")
        .DisplayName("zh-CN", "消毒液")
        .Color(new Color(0.85f, 0.95f, 0.95f))
        .ValuePerLiter(12f)
        .Quality("disinfectant", 0.6f)
        .HealthUsable()
        .OnHealthUse((ml, limb) =>
        {
            limb.skinDirtyness   = Mathf.Max(0f, limb.skinDirtyness - ml * 0.02f);
            limb.infectionAmount = Mathf.Max(0f, limb.infectionAmount - ml * 0.1f);
        })
        .OnDrink((ml, body) =>
        {
            // Drinking antiseptic is a bad idea.
            body.sicknessAmount += ml * 0.2f;
            body.venomCurrent   += ml * 0.05f;
        })
        .Injectable()
        .InjectionSickness(2f)
        .Register();
}

Pair this with a RecipeBuilder.IngredientLiquidByQuality("disinfectant", 30f) and the new liquid is immediately accepted everywhere vanilla disinfectants are.


Inspecting Liquids

CustomLiquidBuilder.GetOwner("mymod_tonic");
CustomLiquidBuilder.GetAllRegistered();   // IReadOnlyDictionary<string, string>

scavlib check lists every ScavLib-registered liquid alongside its owner mod and flags duplicate IDs.


Notes & Limitations

  • ScavLib does not ship a custom liquid container Prefab. To pre-fill a bottle with your liquid at world-load time, spawn a vanilla craftingbottle (or any WaterContainerItem) and call waterContainer.AddLiquid("mymod_tonic", ml).
  • The vanilla LiquidType struct exposes no "color blend" hook; the registered Color is used wholesale for the in-container fluid render.
  • Custom liquid IDs survive language switches. Locale.ChangeLanguage() reloads the active scene, but Liquids.Registry is a static dictionary and ScavLib re-applies its queue on the very next Recipes.SetUpRecipes post-load.

Clone this wiki locally