Loot — Integration Surfaces¶
The seams Loot exposes, what each is for, and what belongs on your side of them.
The four seams¶
| Interface | Answers | Shipped implementation |
|---|---|---|
ILootInventoryAdapter | Where do item awards go? | LootInventoryAdapter |
ILootCurrencyAdapter | Where do currency awards go? | LootCurrencyAdapter |
ILootPickupSpawner | How does an award become a thing in the world? | LootPickupSpawner |
ILootModifier | Should this award be changed before delivery? | none — yours |
The first three are assigned on the LootService component as serialized MonoBehaviour fields: Inventory Adapter, Currency Adapter, Pickup Spawner. Leave one empty and that award kind is simply not delivered.
The shipped adapters are internal and are added from RevFramework ▸ Integrations ▸ Loot ▸ <System> ▸ … in the Add Component menu. They are components, not API — you wire them in the inspector rather than constructing them, which is why they do not appear in the public surface.
Binding is by component, and re-bound lazily¶
LootService.Bind() runs at the head of every public entry point and resolves again whenever an assigned component changes, so an adapter assigned at runtime is picked up on the next call rather than requiring a restart. TryGetInventoryAdapter and TryGetCurrencyAdapter let editor tooling and your own code ask what is actually bound.
An absent adapter is not an error. It is the supported configuration for a project that installed Loot without Currency, and the reason the roll is unchanged by it: what you won is a fact about the table, not about what happened to be installed.
ILootModifier — the seam you are most likely to implement¶
Discovered on the owner's parent chain, so a modifier on a player's root affects everything that player receives. Ordered by Priority, lowest first.
public sealed class DoubleGoldWeekend : MonoBehaviour, ILootModifier
{
public int Priority => 0;
public LootItemGrant ModifyItem(GameObject owner, in LootItemGrant grant) => grant;
public LootCurrencyGrant ModifyCurrency(GameObject owner, in LootCurrencyGrant grant)
=> grant.currencyId == "gold"
? new LootCurrencyGrant(grant.currencyId, grant.amount * 2)
: grant;
}
It runs against the result, not the table. You can change what was won; you cannot change what was likely. See System Boundaries for why, and for what that rules out.
This is also the seam where item properties become reachable. The roller knows only a GUID, but your modifier lives in your assembly, so it can resolve the ItemDefinition, read rarity or level, and swap the award accordingly.
Pickups — a payload carrier, not a prefab per item¶
Pickups has no spawn-by-payload API: a pickup is a prefab carrying a PickupEffect. Authoring one prefab per possible award stops being workable past a handful of entries.
So LootPickupSpawner spawns one prefab, and the spawned instance carries the rolled award on a LootPickupPayload which applies it on collect.
The constraint that shaped this, and that will catch you if you build your own spawner: TriggerRelay3D resolves its IPickupTriggerReceiver during Awake, which has already run by the time Instantiate returns. The payload component must therefore be on the prefab, not added to the instance afterwards — added late, it is never found and the pickup does nothing on collect.
Health — LootDropOnDeath¶
Subscribes to Died, rolls the assigned table and grants it. Configure the table, the service, the recipient and the container on the component.
It guards against dropping twice. Health can raise Died more than once across a revive cycle, and a table that rolled twice would double a boss's drop. ResetDropGuard() exists for pooled enemies that genuinely should drop again after being revived and re-killed.
Choosing a container¶
Item awards go to a container named by string, defaulting to LootService.DefaultContainer ("Backpack" unless you change it). Names are canonicalised, so casing never selects a different one.
Pass a container explicitly when a drop should bypass the default — a quest reward into a quest bag, a crafting output into a workbench.
What not to build here¶
- Do not put drop-decision logic in a modifier. Modifiers see one award at a time and cannot see the table, so "guarantee at least one rare per chest" is not expressible. That belongs in the code that chooses which table to roll.
- Do not implement
ILootInventoryAdapterto reach a non-inventory store. It is typed around container handles and exact adds; a shop, bank or mailbox wants your own delivery step afterRoll, not an adapter pretending to be an inventory. - Do not grant from inside a modifier. It runs mid-delivery; granting there re-enters the service with a half-processed result.
- Do not use
Grantto hand out a freshly rolled result when you meantRollAndGrant.Grantnever spawns, so a full bag means the award is lost rather than dropped at the player's feet.