Skip to content

CustomItemRegistry

QinShenYu edited this page Jun 16, 2026 · 6 revisions

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.


Quick Start

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

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).

Creation

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).

Convenience Factories

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.

Template Catalog

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 ItemTemplate values intentionally share a clone source — Helmet and BikeHelmet both clone bikehelmet, Sneakers and Boots both clone sneakers. They exist as separate enum values for call-site readability; the resulting prefab is identical until your fluent overrides apply.

Core Stats

.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().

Behaviour Flags

.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)

Action Callbacks

.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 replace parameter defaults to true. Earlier wiki revisions claimed false; 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.

Wearable Configuration

For wearable items, use the strongly-typed Wearable chain rather than writing raw slot strings.

Static factory — "build a wearable from scratch"

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.

Instance method — "add wearable settings to an existing builder"

.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.

Fine-grained wearable knobs

.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 only

The 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

Component-Level Configuration

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.

Liquid Overrides (LiquidItemInfo templates only)

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.

i18n Shortcuts

.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().

Registration

.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.


How Spawning Works

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:

  1. Looks the id up in the CustomItemRegistry template map.
  2. If found, instantiates the recorded clone template, re-stamps its Item.id to the custom id, applies the registered ItemInfo, runs all accumulated OnSpawn hooks, and skips the original method.
  3. 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.

Deferred-Awake clone (0.7.2)

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.


Manual Registration (Legacy / Obsolete)

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.


ID Naming Convention

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.


Inspecting Registered Items

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.


Worked Example — Wearable Helmet

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.


Worked Example — Themed Backpack

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.


Worked Example — Custom Pistol that keeps vanilla firing

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.

Clone this wiki locally