-
Notifications
You must be signed in to change notification settings - Fork 0
CustomItemRegistry
Namespace: ScavLib.item
Registers custom items into the game's global item registry
(Item.GlobalItems) and makes them fully spawnable at runtime by
intercepting Utils.Create and cloning a vanilla template Prefab.
The recommended entry point is the chainable CustomItemBuilder.
Definition-only registration via RegisterItem / RegisterSimpleItem is
preserved for backwards compatibility but marked [Obsolete].
Injection timing is automatic: ScavLib applies a Harmony Postfix to
Item.SetupItems(), and any registration issued before the world loads is
safely queued and flushed at the correct moment.
using ScavLib.item;
using ScavLib.event_bus;
using ScavLib.event_bus.events;
private void Awake()
{
CustomItemBuilder.Create("mymod_salve", "MyMod", ItemTemplate.Bandage)
.Category("medical")
.Weight(0.3f).Value(15)
.Tags("medicine", "dressing")
.DisplayName("Herbal Salve")
.Description("A pungent paste that soothes burns and shallow cuts.")
.UsableOnLimb((limb, item) =>
{
limb.skinHealAmount += 15f;
limb.pain -= 20f;
item.condition -= 0.25f;
}, replace: false) // append after vanilla useLimbAction
.Register(out var err);
}The call to Register() is safe at Awake() time — registrations made
before the world exists are queued and applied when Item.SetupItems()
runs.
CustomItemBuilder is a chainable builder. Every configuration method
returns this, so the typical usage is a single fluent statement ending in
.Register(out var err).
The recommended factory takes the item id, owner mod name, and template in
one call. The template determines both the cloned vanilla prefab and the
ItemInfo subtype (ItemInfo / LiquidItemInfo / BatteryInfo):
CustomItemBuilder.Create(string id, string owner, ItemTemplate template)
CustomItemBuilder.Create(string id, string owner, string vanillaResourceId)| Parameter | Description |
|---|---|
id |
Unique item ID. Prefix with your mod name (e.g. "mymod_salve"). |
owner |
Mod name recorded in the Owner Ledger. Match ModInfo.Name. |
template |
Which vanilla prefab to clone — see the Template Catalog below. |
vanillaResourceId |
Escape hatch: clone any vanilla resource id directly (e.g. "materialpouch"). Use this when no ItemTemplate enum value matches. |
The id is also the key used by Utils.Create(id, ...),
GameUtil.SpawnItem(id, ...), and ItemUtil.IsKnownId(id).
For the common item families, ScavLib ships single-call convenience factories that pick the right template for you:
CustomItemBuilder.Pistol("mymod_pistol", "MyMod")
.DisplayName("Custom Pistol")
.Register(out _);
CustomItemBuilder.Helmet("mymod_helmet", "MyMod")
.Wearable(VanillaLimb.Head, VanillaWearSlot.Hat, armor: 5f)
.Register(out _);The full list of convenience factories:
| Family | Available factories |
|---|---|
| Weapons |
Pistol / Rifle / Shotgun / Magazine
|
| Ammo / Batteries |
SmallBattery / MediumBattery / LargeBattery
|
| Liquid containers | Canteen |
| Light / utility |
Flashlight / Backpack / Explosive
|
| Wearable — Head |
Helmet / BikeHelmet / RiotHelmet / DustMask / Goggles / Balaclava / Scarf
|
| Wearable — Torso |
Hoodie / Shirt / TorsoArmor / BellyArmor / Belt / Bandolier / FannyPack
|
| Wearable — Limbs |
Gloves / TacticalGloves / ArmWarmers / LimbWraps / KneePads / Sneakers / Boots / MaterialPouch / LegPouch
|
There is also a static factory specifically for "build a wearable from scratch":
CustomItemBuilder.Wearable(string id, string owner,
ItemTemplate template, VanillaLimb limb, VanillaWearSlot slot)This pre-fills wearable=true, desiredWearLimb, and wearSlotId. For
adding wearable settings to an existing builder chain, use the
instance method .Wearable(...) below.
Each ItemTemplate value is cloned from a real vanilla prefab. The clone
inherits the prefab's full component chain and ItemInfo; your fluent
overrides apply on top. Pick the entry that most closely matches your item's
behavior — fields you do not override keep the vanilla template's values
(rather than reverting to C# defaults).
| Category | ItemTemplate |
Vanilla clone source |
|---|---|---|
| Generic | SimpleItem |
geofruit |
| Generic | ScrapMaterial |
scrapmetal |
| Medical | Bandage |
bandage |
| Food | FoodSimple |
nutrientbar |
| Tools |
Tool / Cleaver / Knife
|
wrench / machete / flimsyknife
|
| Liquid |
WaterContainer / Canteen
|
canteen |
| Liquid | CraftingBottle |
craftingbottle |
| Guns |
Pistol / Rifle / Shotgun / MakeshiftRifle
|
pistol / rifle / shotgun / makeshiftrifle
|
| Ammo |
SmallMagazine / RifleMagazine
|
smallmagazine / riflemagazine
|
| Ammo |
Round9mm / Round556 / Gauge12 / BoxOf12Gauge
|
9mmround / 556round / 12gauge / boxof12gauge
|
| Batteries |
SmallBattery / MediumBattery / LargeBattery
|
smallbattery / mediumbattery / largebattery
|
| Containers |
SmallPack / Duffelbag / SlingBag / BigPack / LegPouch / FoliageBag
|
matching ids |
| Wearable — Head |
Helmet / BikeHelmet
|
bikehelmet |
| Wearable — Head | RiotHelmet |
riothelmet |
| Wearable — Head |
DustMask / Goggles / Balaclava / Scarf
|
matching ids |
| Wearable — Torso | Hoodie |
hoodie |
| Wearable — Torso | Shirt |
tornshirt |
| Wearable — Torso | TorsoArmor |
carapace |
| Wearable — Torso |
BellyArmor / Belt / Bandolier / FannyPack
|
matching ids |
| Wearable — Limbs | Gloves |
latexgloves |
| Wearable — Limbs |
TacticalGloves / ArmWarmers / LimbWraps / KneePads
|
matching ids |
| Wearable — Limbs |
Sneakers / Boots
|
sneakers |
| Wearable — Limbs | MaterialPouch |
materialpouch |
| Light |
Flashlight / Lantern / MakeshiftHeadlamp
|
matching ids |
| Explosive | Explosive |
dynamite |
| Misc |
Plushie / Watch / MP3Player / GeigerCounter / GrapplingHook / Blueprint / Sleepingbag
|
matching ids |
| Resources |
Plastic / Wood / ScrapCube
|
plasticchunk / woodcube / scrapcube
|
Some
ItemTemplatevalues intentionally share a clone source —HelmetandBikeHelmetboth clonebikehelmet,SneakersandBootsboth clonesneakers. They exist as separate enum values for call-site readability; the resulting prefab is identical until your fluent overrides apply.
.Category(string category)
.Weight(float kg)
.Value(int currency)
.Tags(params string[] tags)
.Recognition(int level)
.SlotRotation(float degrees)
.DecayMinutes(float minutes)
.RotSpeed(float speed)Tags are forwarded so they participate in ItemInfo.HasTag(...) lookups
exactly like vanilla items. Tags affecting gameplay behaviour
("cangetwet", "food", etc.) must be declared here — the game reads them
in Item.Start().
.Combineable(bool value = true)
.DestroyAtZeroCondition(bool value = true)
.ScaleWeightWithCondition(bool value = true)
.OnlyHoldInHands(bool value = true)
.AutoAttack(bool value = true)
.UsableWithLMB(bool value = true)
.IgnoreDepression(bool value = true).Usable(Action<Body, Item> onUse, bool replace = true)
.UsableOnLimb(Action<Limb, Item> onUseLimb, bool replace = true)
.OnSpawn(Action<GameObject> onSpawn)0.7.2 documentation fix: the
replaceparameter defaults to true. Earlier wiki revisions claimedfalse; that was always wrong relative to source.
replace: true (default) overwrites the vanilla template's useAction
/ useLimbAction entirely. Use this when you want pure custom behaviour and
the template's vanilla action would only get in your way.
replace: false appends your callback after the vanilla action, so the
template's built-in effect still runs first. This is the right choice for
templates whose vanilla useAction is the core behaviour you want to keep
— guns (vanilla useAction handles firing), batteries / flashlights (vanilla
useAction toggles charge state), dynamite (vanilla useAction is the fuse),
and bandages (vanilla useAction does the actual healing). On templates with
no vanilla useAction at all, replace: false degenerates to the same
behaviour as replace: true.
OnSpawn fires on every spawned instance after the prefab is configured.
Multiple OnSpawn calls accumulate; user-supplied hooks always run last so
they observe the prefab in its final template-configured state. As of 0.7.2,
OnSpawn runs after the clone's Unity Awake — see
How Spawning Works below.
For wearable items, use the strongly-typed Wearable chain rather than
writing raw slot strings.
CustomItemBuilder.Wearable(string id, string owner,
ItemTemplate template, VanillaLimb limb, VanillaWearSlot slot)Equivalent to Create(id, owner, template) plus pre-applied
wearable=true, desiredWearLimb, and wearSlotId. Use this when you are
starting a wearable item and want the wearable settings baked in from the
first call.
.Wearable(VanillaLimb desiredWearLimb, VanillaWearSlot wearSlotId,
float armor = 0f, float isolation = 0f)Available on any builder chain (typically one started with a non-wearable
factory like CustomItemBuilder.Helmet(...) or Create(...)). One call
sets wearable=true, desiredWearLimb, wearSlotId, and optionally
wearableArmor / wearableIsolation. armor / isolation of 0f are
treated as "do not override" — the cloned template's value carries over.
.WearableArmor(float armor)
.WearableIsolation(float isolation)
.WearableHitDurabilityLossMultiplier(float multiplier)
.WearableVisualOffset(int pixels)
.WearableCanBeHeld(bool value = true)
.JumpHeightMultChange(float change)
.WearSlot(VanillaLimb limb, VanillaWearSlot slot) // rewrite slot onlyThe VanillaLimb and VanillaWearSlot enums (in ScavLib.item) make typos
fail at compile time. The full slot map:
VanillaWearSlot |
Matching VanillaLimb
|
Vanilla items in this slot |
|---|---|---|
Hat |
Head |
bikehelmet, riothelmet, makeshifthelmet, headlamp, holidayhat
|
Eyes |
Head |
safetyglasses, goggles, autozoomgoggles
|
Mouth |
Head |
dustmask |
Blindfold |
Head |
blindfold |
Balaclava |
Head |
balaclava |
Neck |
UpTorso |
scarf |
OuterTorso |
UpTorso |
hoodie, striderpelt, carapace
|
Torso |
UpTorso |
tornshirt |
TorsoFront |
DownTorso |
bellyarmor, traumarig, fannypack, scubadivinggear
|
Back |
UpTorso |
smallpack, duffelbag, slingbag, bigpack, jetpack
|
Bandolier |
UpTorso |
bandolier |
Belt |
DownTorso |
belt |
Arms |
DownArmF |
armwarmers |
Wraps |
DownArmF |
limbwraps |
Hands |
HandF |
latexgloves, tacticalgloves, climbingclaws
|
Thigh |
ThighF |
legpouch |
ThighBack |
ThighB |
materialpouch |
Knees |
CrusF |
kneepads |
Feet |
FootF |
sneakers, tacticalboots, woodsandals
|
Some templates spawn with extra components beyond Item. The builder offers
direct chainable setters for the most common ones:
// Battery
.MaxCharge(float capacity)
// Ammo (rounds and magazines)
.AmmoType(GunScript.AmmoType type)
.AmmoMaxRounds(int rounds)
// Gun
.GunMagCapacity(int cap)
.GunDamage(float structureDamage, float animalDamage)
.GunShotsPerFire(int shots)
.GunVerticalSpread(float spread)
.GunSprites(Sprite normal, Sprite racked, Sprite normalNoMag, Sprite rackedNoMag)
// Container (backpacks, fanny packs, etc.)
.ContainerCapacity(float maxWeight, float maxWeightPerItem)
.ContainerTagRestriction(params string[] tags)
.ContainerItemsVisible(bool visible)
.ContainerEncumberance(float multiplier)These accumulate into an internal hook list that runs at spawn time,
ensuring components are configured before any user OnSpawn callback
observes the GameObject.
Meaningful only when the chosen template clones a liquid container
(Canteen, WaterContainer, CraftingBottle):
.Capacity(float capacity)
.AutoFill(bool value = true)
.DefaultContents(params (string liquidId, float amount)[] contents)
.LiquidFillSprite(Sprite sprite)Calling these on a non-liquid template logs a warning and is ignored.
.DisplayName(string en)
.DisplayName(IDictionary<string, string> byLanguage)
.Description(string en)
.Description(IDictionary<string, string> byLanguage)These are thin wrappers over LocaleManager.RegisterItem(...) (see the
i18n page). For bulk or runtime-driven translations, ship a
lang/<lang>.json next to your DLL and call
LocaleManager.AutoRegister(modId, pluginFolder) once in Awake().
.Register()
.Register(out string error)If Register() is called twice with the same id by the same owner,
ScavLib treats it as an idempotent overwrite (typical when re-registering
content across run boundaries) and logs a warning. Registration by a
different owner is rejected with a non-empty error. The Owner Ledger
records the original owner and conflicts surface in scavlib check.
True conflicts with vanilla Item.GlobalItems entries (an id that exists
only in GlobalItems, not in ScavLib's ledger) are still rejected.
Vanilla Utils.Create is a thin wrapper around Resources.Load:
public static GameObject Create(string id, Vector2 pos, float rot)
=> Object.Instantiate(Resources.Load(id), pos, Quaternion.Euler(0,0,rot)) as GameObject;
public static GameObject Create(string id, Transform trans)
=> Object.Instantiate(Resources.Load(id), trans) as GameObject;For an unknown id this returns null, so without intervention custom items
could not be spawned — there is no Prefab in Resources/ to load.
ScavLib applies a Priority.Low Harmony Prefix to both overloads. The
Prefix runs before the original method and:
- Looks the id up in the
CustomItemRegistrytemplate map. - If found, instantiates the recorded clone template, re-stamps its
Item.idto the custom id, applies the registeredItemInfo, runs all accumulatedOnSpawnhooks, and skips the original method. - If not found, falls through to the original method untouched.
The Priority.Low weighting matters for cross-mod compatibility: other
frameworks that patch Utils.Create get to run first, and ScavLib only
intervenes for ids it actually owns.
A subtle problem hit several vanilla Item-attached MonoBehaviours: their
Awake() reads Item.Stats (which dereferences Item.GlobalItems[this.id])
to cache ItemInfo-derived state. Because Unity's Object.Instantiate
runs Awake synchronously on an active prefab, that read fired with the
template id (e.g. "canteen") before ScavLib could swap it for the
mod id. The clone ended up cached against the wrong ItemInfo, and there
was usually no second pass to fix it. Most visible symptom: a custom
canteen template losing its LiquidItemInfo.defaultContents fill.
ScavLib 0.7.2 fixes this by temporarily disabling the source prefab around
Instantiate. The clone inherits the inactive state, so its Awake is
suspended; ScavLib then writes id, sprite, and the CustomItemTag onto
the still-inactive clone, then SetActive(true)s it. Awake fires once,
with the final mod id already in place. The prefab's active flag is
restored in a finally block.
A practical consequence: OnSpawn now runs after the clone's Awake, so
user code can rely on every component's Awake-time state being settled.
If you need to intervene before Awake — e.g. swapping a serialized
prefab field that Awake reads — you must still ship a custom prefab via
your own AssetBundle.
The pre-builder entry point is retained for source compatibility:
[Obsolete("Use CustomItemBuilder.Create(...).Register() instead.")]
public static void RegisterItem(string id, ItemInfo info, bool overwrite = false);It still registers the ItemInfo into Item.GlobalItems, but does not
bind a clone template, so calling Utils.Create("legacy_id", ...) on
items registered this way still returns null. To make a legacy
registration spawnable, re-declare it through CustomItemBuilder.
Prefix your item ids with a short mod identifier:
mymod_salve OK
super_bandage too generic — may collide
bandage conflicts with a built-in item
The Owner Ledger records the registering mod name. When two mods race for
the same id, scavlib check lists both owners and flags the conflict.
ItemUtil.IsKnownId("mymod_salve");
CustomItemRegistry.Contains("mymod_salve");
CustomItemRegistry.GetOwner("mymod_salve");
CustomItemRegistry.GetAllRegistered(); // IReadOnlyList<(id, owner)>
CustomItemRegistry.GetAllIds();Run scavlib check in the developer console for a human-readable summary
covering custom items, recipes, liquids, Harmony patches, and commands.
using ScavLib.item;
private void Awake()
{
CustomItemBuilder.Helmet("mymod_helmet", "MyMod")
.DisplayName(new Dictionary<string, string>
{
{ "EN", "Reinforced Helmet" },
{ "zh-CN", "强化头盔" }
})
.Category("custom")
.Weight(1.2f).Value(40)
.Recognition(4)
.Wearable(
desiredWearLimb: VanillaLimb.Head,
wearSlotId: VanillaWearSlot.Hat,
armor: 5f,
isolation: 0.2f)
.WearableHitDurabilityLossMultiplier(0.5f)
.Register(out _);
}The resulting item drops into the player's hat slot, applies armor and
cold isolation, and survives hits at twice the normal durability rate.
using ScavLib.item;
private void Awake()
{
CustomItemBuilder.Backpack("mymod_medbag", "MyMod")
.DisplayName("Medical Pack")
.Category("custom")
.Weight(0.6f).Value(35)
.Recognition(3)
.ContainerCapacity(maxWeight: 8f, maxWeightPerItem: 3f)
.ContainerTagRestriction("medical")
.ContainerEncumberance(0.8f)
.Register(out _);
}Only items tagged "medical" will fit, the encumbrance penalty is reduced,
and total payload is capped at 8 kg.
using ScavLib.item;
private void Awake()
{
CustomItemBuilder.Pistol("mymod_pistol", "MyMod")
.DisplayName("Service Pistol")
.Category("weapon")
.Weight(0.9f).Value(120)
.GunDamage(structureDamage: 8f, animalDamage: 14f)
.GunMagCapacity(12)
// Append our custom side-effect *after* vanilla firing logic.
.Usable((body, item) =>
{
// e.g. log every shot
ScavLib.util.GameUtil.Log("Fired Service Pistol");
}, replace: false)
.Register(out _);
}replace: false is the right default for guns — the vanilla useAction
is the firing logic; replacing it would give you a gun that does
nothing.