-
Notifications
You must be signed in to change notification settings - Fork 0
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 flushed
during ItemSetupItemsPatch.Prefix, which runs after Liquids' static
constructor and before LiquidItemInfo builds its default contents. As of
0.7.1, late registration mid-game is also supported.
using ScavLib.liquid;
using UnityEngine;
private void Awake()
{
CustomLiquidBuilder.Create("mymod_tonic", "MyMod")
.LocaleName("Bitter Tonic")
.Tint(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();
}A chainable builder. Each configuration method returns this; commit with
.Register().
CustomLiquidBuilder.Create(string id, string owner)| Parameter | Type | Description |
|---|---|---|
id |
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). ID matching is case-insensitive. |
owner |
string |
The owning mod's identifier. Used by the registry to detect cross-mod ID collisions: re-registering the same id from a different owner will fail; re-registering from the same owner is idempotent. |
.LocaleName(string name) // LiquidType.localeName
.LocaleFromItem(bool value = true) // LiquidType.localeFromItemLocaleName sets the LiquidType.localeName field directly. For
multi-language display strings, register them through
LocaleRegistry (e.g. LocaleRegistry.AddItems(...)) —
the builder itself does not handle localization.
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.
.Tint(UnityEngine.Color color) // LiquidType.colorThe method is named
Tintrather thanColorto avoid CS0119 ambiguity with theUnityEngine.Colortype when called as.Color(new Color(...)).
.ValuePerLiter(float value) // LiquidType.valuePerLiter.Quality(string qualityId, float amount = 1f)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.
Call .Quality(...) multiple times to attach more than one quality tag.
.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.
.Injectable(bool value = true) // LiquidType.injectable
.InjectionSickness(float multiplier) // LiquidType.injectionSicknessInjectable 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.
.MarkDangerous(bool value = true)When set, the liquid's ID is appended to Liquids.DangerList upon
successful registration. The vanilla code uses this list for AI/warning
behavior around hazardous fluids (acids, fuels, contaminated water, etc.).
.Register()
.Register(out string error)Both overloads return bool. The out-string overload populates error
with a human-readable reason on failure. Failure conditions:
-
idis null or empty. - The internal
LiquidTypeis null. - The
idwas already registered by a differentowner.
There is currently no
overwriteoverload. Re-registering the sameidfrom the same owner silently succeeds (idempotent); a conflict with a different owner is rejected and logged viaScavLibPlugin.Log.
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", "MyMod")
.LocaleName("Antiseptic Solution")
.Tint(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)
.MarkDangerous()
.Register(out string err);
if (!string.IsNullOrEmpty(err))
Debug.LogError($"[MyMod] antiseptic register failed: {err}");
}For multi-language display names, register them through LocaleRegistry:
LocaleRegistry.AddItem("mymod_antiseptic", "en", "Antiseptic Solution");
LocaleRegistry.AddItem("mymod_antiseptic", "zh-CN", "消毒液");Pair this with a RecipeBuilder.IngredientLiquidByQuality("disinfectant", 30f)
and the new liquid is immediately accepted everywhere vanilla disinfectants
are.
CustomLiquidRegistry.Contains("mymod_tonic"); // boolContains returns true for any ID that has been routed through
CustomLiquidBuilder / CustomLiquidRegistry, regardless of whether the
flush into Liquids.Registry has already happened.
Late registration (0.7.1+). If Register() is called after the
vanilla ItemSetupItems flush has already run (e.g. another mod loads
late, or you register from gameplay code rather than Awake()), the
builder writes directly into Liquids.Registry instead of waiting for the
flush. This prevents KeyNotFoundException when hovering items that
reference the new liquid.
KrokoshaCasualtiesMP (KrokMP) bridge. When the
KrokoshaCasualtiesMP plugin is detected via
BepInEx.Bootstrap.Chainloader.PluginInfos, late-registered liquids are
also synchronized into KrokMP's LiquidIdRegistry and
LiquidNetIdToId tables via reflection, assigning a fresh byte
network ID. This keeps custom liquids serializable in multiplayer
sessions without requiring a hard dependency on KrokMP at compile time.
The
bytenet ID space is capped at 255 entries. If your mod stack approaches that limit, expect later registrations to silently collide.
- 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 anyWaterContainerItem) and callwaterContainer.AddLiquid("mymod_tonic", ml). - The vanilla
LiquidTypestruct exposes no "color blend" hook; theTintcolor is used wholesale for the in-container fluid render. - Liquid IDs are matched case-insensitively
(
StringComparer.OrdinalIgnoreCase). - Custom liquid IDs survive language switches.
Locale.ChangeLanguage()reloads the active scene, butLiquids.Registryis a static dictionary; ScavLib re-applies its queue on the nextRecipes.SetUpRecipespost-load. - Registration is not thread-safe. Call the builder from the main Unity / BepInEx thread.