Skip to content

CustomItemRegistry

QinShenYu edited this page Jun 7, 2026 · 6 revisions

CustomItemRegistry

Namespace: ScavLib.item

Registers custom items into the game's global item registry (Item.GlobalItems) and, as of 0.7.0, 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;

[Subscribe]
private void OnWorldLoaded(WorldLoadedEvent e)
{
    // Already-registered items will spawn fine; this is just for verification.
    bool ok = ItemUtil.IsKnownId("mymod_salve");
    GameUtil.Log($"mymod_salve ready: {ok}");
}

private void Awake()
{
    CustomItemBuilder.Create("mymod_salve")
        .FromTemplate(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;
        })
        .Register();
}

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

Creation

CustomItemBuilder.Create(string id)
Parameter Type Description
id string Unique item ID. Prefix with your mod name to avoid collisions (e.g. "mymod_salve").

The ID is also the key used by Utils.Create(id, ...), GameUtil.SpawnItem(id, ...), and ItemUtil.IsKnownId(id).

Template Selection

.FromTemplate(ItemTemplate template)
.FromTemplate(string vanillaId)

Selects which vanilla item Prefab to clone as the visual / physical base for your item. The ItemTemplate enum covers the common bases:

ItemTemplate Vanilla ID Use for
Generic a small generic loose item Default fallback. Used when FromTemplate is omitted.
Bandage bandage Medical consumables, dressings, salves.
Pills a pill bottle Anything granular / pillular.
Bottle craftingbottle Containers that should also work as a WaterContainerItem.
Food a generic edible Edible consumables (drink hook + tags).
Tool a small handheld tool Tools, instruments, repair items.

The string overload FromTemplate("some_vanilla_id") lets you clone any existing item by ID — useful when you want an exact copy of a less-common vanilla item.

Core Stats

.Category(string category)        // default: "custom"
.Weight(float kg)                 // default: 1f
.Value(int currency)              // default: 1
.Tags(params string[] tags)       // applied via ItemInfo.SetTags(); supports e.g. "medicine", "dressing", "cangetwet"
.Recognition(int level)           // wraps new Recognition(level)

Tags are forwarded through ItemInfo.SetTags(...) 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

.Usable(bool value = true)                  // ItemInfo.usable
.UsableOnLimb(bool value = true)            // ItemInfo.usableOnLimb
.Combineable(bool value = true)             // ItemInfo.combineable
.DestroyAtZeroCondition(bool value = true)  // ItemInfo.destroyAtZeroCondition

Action Callbacks

.UseAction(Action<Item> onUse)                   // ItemInfo.useAction
.UseLimbAction(Action<Limb, Item> onUseLimb)     // ItemInfo.useLimbAction

These map 1:1 to the matching ItemInfo callbacks the game already invokes from its inventory UI; the builder does not wrap, schedule, or guard them.

i18n Shortcuts

.DisplayName(string en)                     // adds LocaleRegistry "EN" item entry
.DisplayName(string lang, string text)      // adds for a specific language
.Description(string en)                     // adds locale "other" entry under "<id>_desc"
.Description(string lang, string text)

These are thin wrappers over LocaleRegistry. For more than two languages, use LocaleRegistry.AddItems(...) directly. See the LocaleRegistry page for the full bulk-loading API.

Registration

.Register()                                 // commits the item; returns void
.Register(bool overwrite)                   // overwrite: replace an existing ID instead of failing

Register(overwrite: true) replaces an existing entry in Item.GlobalItems and rebinds the clone template. Useful for rebalancing a vanilla item or for two mods that mutually consent to one overriding the other.

If Register() is called twice with the same ID without overwrite: true, the second call logs a warning and is ignored. The Owner Ledger records the first registration's owner; conflicts surface in scavlib check.


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, which is why pre-0.7.0 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, and skips the original method (returning the new GameObject).
  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. Unknown IDs are still handled by the vanilla Resources.Load path, so non-ScavLib custom-item frameworks continue to function alongside ScavLib without conflict.


Manual Registration (Legacy / Obsolete)

The pre-0.7.0 entry points are retained for source compatibility:

[Obsolete("Use CustomItemBuilder.Create(...).Register() instead.")]
public static void RegisterItem(string id, ItemInfo info, bool overwrite = false);

[Obsolete("Use CustomItemBuilder.Create(...).Register() instead.")]
public static void RegisterSimpleItem(string id, string category = "custom",
    float weight = 1f, int value = 1, string tags = "");

Both still register the ItemInfo into Item.GlobalItems. They do not bind a clone template, so calling Utils.Create("legacy_id", ...) on items registered this way will still return 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       ✔
super_bandage     ✗  (too generic, may collide)
bandage           ✗  (conflicts with a built-in item)

The Owner Ledger records the registering mod name (taken from the BepInPlugin GUID or, if ModRegistry.Register was called, from ModInfo.Name). When two mods race for the same ID, scavlib check will list both owners and flag the conflict.


Inspecting Registered Items

ItemUtil.IsKnownId("mymod_salve");      // does Item.GlobalItems contain it?
CustomItemRegistry.GetOwner("mymod_salve");   // which mod registered it?
CustomItemRegistry.GetAllRegistered();        // IReadOnlyDictionary<string, string>

Run scavlib check in the developer console for a human-readable summary covering custom items, recipes, liquids, Harmony patches, and commands.


Worked Example

A toggleable healing salve that consumes herbs and bandages, ships English and Chinese names, and is fully spawnable from the dev console:

using ScavLib.item;
using ScavLib.locale;
using UnityEngine;

private void Awake()
{
    // Optional: extra languages beyond DisplayName / Description.
    LocaleRegistry.AddItem("mymod_salve", "fr-FR", "Onguent aux Herbes");

    CustomItemBuilder.Create("mymod_salve")
        .FromTemplate(ItemTemplate.Bandage)
        .Category("medical")
        .Weight(0.3f)
        .Value(15)
        .Tags("medicine", "dressing", "cangetwet")
        .Recognition(3)
        .DisplayName("Herbal Salve")
        .DisplayName("zh-CN", "草药膏")
        .Description("A pungent paste that soothes burns and shallow cuts.")
        .Description("zh-CN", "气味刺鼻的药膏,能舒缓烧伤与浅切口")
        .UsableOnLimb((limb, item) =>
        {
            limb.skinHealAmount += 15f;
            limb.pain           -= 20f;
            item.condition      -= 0.25f;
        })
        .DestroyAtZeroCondition(true)
        .Register();
}

After WorldLoadedEvent, you can spawn one for testing via the dev console or GameUtil.SpawnItemAt("mymod_salve", PlayerCamera.main.body.transform).

Clone this wiki locally