Skip to content

Loot — Public API

The supported surface. Anything not listed here is internal and may change without a major version — including the shipped adapter components, which are wired in the inspector rather than constructed.

This page is kept against Tests/EditMode/ApiSnapshots/Lib/Goldens/RevFramework.Loot.PublicApi.txt. If the two disagree, the golden is right and this page is stale.

Supported surface at a glance

Type Kind What it is
LootTable ScriptableObject The asset: entries, mode, roll count
LootEntry struct One row of a table
LootEntryKind enum Item, Currency, Table, Nothing
LootTableMode enum Weighted, IndependentChance
LootRoller static class Pure rolling
LootResult class What was won
LootItemGrant / LootCurrencyGrant readonly struct One award
LootService MonoBehaviour Rolling plus delivery
ILootInventoryAdapter / ILootCurrencyAdapter / ILootPickupSpawner interface Delivery seams
ILootInventoryRefusalReporter / ILootCurrencyRefusalReporter interface Optional: say why a delivery was refused
LootDeliveryRefusal enum None, Unavailable, AuthorityDenied
ILootModifier interface Adjust an award before delivery
LootPickupPayload MonoBehaviour Carries a rolled award on a spawned pickup

LootTable

IReadOnlyList<LootEntry> Entries { get; }
LootTableMode Mode { get; }
int RollsMin { get; }
int RollsMax { get; }
bool AllowDuplicates { get; }
float TotalWeight { get; }

static LootTable Create(LootTableMode mode, int rollsMin, int rollsMax,
                        bool allowDuplicates, params LootEntry[] entries);

Read-only once built. Nothing in the supported surface mutates an existing table.

Create — building a table in code

Authoring a .asset is the recommended route, and this does not compete with it — odds that live in code are odds a designer cannot open. It exists because the alternative denies the capability altogether: with no public construction route, a table cannot be built outside the framework at all, and "verify drop rates without a scene or Play Mode" would hold only for code with internals access.

Use it for deterministic tests of your own tables, editor tooling, and procedural generation:

var table = LootTable.Create(
    LootTableMode.Weighted, rollsMin: 1, rollsMax: 1, allowDuplicates: true,
    new LootEntry { kind = LootEntryKind.Item, itemGuid = "sword", weight = 1f },
    new LootEntry { kind = LootEntryKind.Nothing, weight = 3f });

var result = LootRoller.Roll(table, new MySeededRng(12345));

Three things worth knowing:

  • The instance is runtime-only. It is marked HideFlags.DontSave and is not an asset. Destroy it when you are done, as you would any ScriptableObject you created.
  • Values are stored as given, not normalised. Bounds are applied on read — RollsMin floors at zero and RollsMax never reports below RollsMin, so inverted bounds roll the minimum.
  • The entry array is copied. Mutating your array afterwards does not change the table.

A cycle cannot be built this way: a LootEntry holds a table reference, so the table pointed at must exist first, and nothing public edits a built table. Cycles are reachable only through authored assets, which is what LootRoller's guard is for.

TotalWeight is the sum across entries and exists for tooling and display. It is not a probability denominator you can reason with across nesting — a nested table rolls its own weights independently.

LootRoller

static LootResult Roll(LootTable table, IRandomProvider rng);
static void Roll(LootTable table, IRandomProvider rng,
                 List<LootItemGrant> items, List<LootCurrencyGrant> currency);
const int MaxNestingDepth = 8;

Pure. Grants nothing, touches no scene. The list overload exists so a caller rolling in a loop — the debugger sampling thousands of times — can reuse buffers instead of allocating a LootResult per roll.

MaxNestingDepth caps recursion; a table nesting deeper warns and stops. Cycles are detected by the path walked, so the same table appearing in two different branches is legal while a table reaching itself is not.

LootResult

IReadOnlyList<LootItemGrant> Items { get; }
IReadOnlyList<LootCurrencyGrant> Currency { get; }
bool IsEmpty { get; }
static LootResult Empty { get; }

A value. Holding one after a roll is safe and is exactly how pickups defer delivery.

LootService

LootResult Roll(LootTable table, GameObject owner);
LootResult RollAndGrant(LootTable table, GameObject owner,
                        string container = null, Vector3? spawnAt = null);
bool Grant(LootResult result, GameObject owner, string container = null);

bool TryGetInventoryAdapter(out ILootInventoryAdapter adapter);
bool TryGetCurrencyAdapter(out ILootCurrencyAdapter adapter);

void UseDeterministicRng(int seed);
void UseUnityRng();

string DefaultContainer { get; }
bool SpawnPickups { get; }

event Action<GameObject, LootResult> Rolled;
event Action<GameObject, LootResult> Granted;
event Action<GameObject, LootResult> Spawned;
event Action<GameObject, LootResult> Undelivered;

RollAndGrant

Rolls, then delivers. Returns what the table produced, whether or not every award could be delivered — so the return value tells you a roll happened and nothing about where the awards went.

To know what landed, use the events. This is not a subtlety: it is the difference between a correct "you received" toast and one that lies.

Grant

Delivers a result rolled earlier. Returns true when at least one award actually reached the owner — not merely that there was something to deliver.

Never spawns pickups, whatever SpawnPickups is set to, because the path a collected pickup grants through must not produce another pickup.

The four events

Event Fires with When
Rolled The full roll After rolling, before anything is granted
Granted Only what the owner received After delivery, when at least one award reached them
Spawned Only what went to the world When an award was dropped instead of handed over
Undelivered Only what the service could not place When an award reached neither the owner nor the world

More than one can fire for a single call: delivery is per-award, so a roll can be partly received, partly dropped and partly undelivered.

Undelivered usually means the award is gone, but it is a report about the service, not a guarantee of loss. A caller holding the award keeps it — LootPickupPayload fires this event on a full-bag collect and stays in the world still carrying its drop. Check before mailing or re-granting.

Drive a "you received" feed from Granted alone. A dropped award is announced by Spawned and then by Granted when the player collects it — the collect path grants through the same service, so treating a drop as a receipt counts every spawned award twice.

Wiring them in the Inspector

Each of the four has a serialized mirror on the component — On Rolled, On Granted, On Spawned, On Undelivered — under Events (Inspector – designer hooks). They exist so a scene can respond without a script: drop a method onto On Granted and the "you received" panel opens.

They are not a second event system. Each mirror is raised from the same place as the event it names, with the same (GameObject owner, LootResult result), immediately before it — so everything in the table above applies to the mirror unchanged, including the double-count trap. Code should still subscribe to the C# event; the mirror is for the inspector.

Two behaviours worth knowing before you rely on one:

  • A throwing listener is contained. An exception from an inspector-wired method is logged and does not reach the roll, the delivery, or the C# event raised after it. That is deliberate: these fire after awards have already changed hands, so an escaping exception would report a failure for work that happened.
  • Unity cannot show a LootResult argument. The inspector can only bind a method whose signature matches, so a no-argument method works and a method taking (GameObject, LootResult) receives both. There is no way to author a static argument for these.

ILootModifier

int Priority { get; }
LootItemGrant ModifyItem(GameObject owner, in LootItemGrant grant);
LootCurrencyGrant ModifyCurrency(GameObject owner, in LootCurrencyGrant grant);

Discovered on the owner's parent chain, applied lowest Priority first, run against the rolled result. See Integration Surfaces.

ILootInventoryRefusalReporter / ILootCurrencyRefusalReporter

public enum LootDeliveryRefusal { None, Unavailable, AuthorityDenied }

bool TryAddExact(object handle, string itemGuid, int quantity, out LootDeliveryRefusal refusal);
bool TryCredit(GameObject owner, string currencyId, long amount, string reason,
               out LootDeliveryRefusal refusal);

Optional companions to ILootInventoryAdapter and ILootCurrencyAdapter. An adapter that implements one is asked through it instead; one that does not keeps working unchanged, with every refusal read as Unavailable. Both shipped adapters implement them.

Only one thing turns on the answer, and it is the reason these exist:

An authority denial is refused, not re-routed

A refused delivery normally falls back to spawning the award into the world — that is what most games do with a full bag, and it is recoverable. An authority denial is different: spawning after one takes an award your IInventoryAuthority or ICurrencyAuthority has just refused for this owner and puts it on the ground, where anyone can collect it. A fail-closed gate would be redirecting the mutation to an ungated surface rather than stopping it.

So AuthorityDenied skips the spawner entirely and reports through Undelivered. Every other refusal keeps the fallback it has always had.

This is reachable without writing a denying binder at all: an authority-wrapped currency stack that cannot resolve an authority answers Unauthorized by design.

Implement one only if you wrote your own adapter and gate loot delivery through an authority. If you do, return AuthorityDenied for authority refusals only — a cap, a full container or a missing service is Unavailable, and re-routing those is correct.

LootPickupPayload

Carries a rolled award on a spawned pickup and applies it on collect. Must be present on the prefab, not added after Instantiate — see Integration Surfaces for the Awake ordering reason.

Explicitly not supported

  • The shipped adapters (LootInventoryAdapter, LootCurrencyAdapter) are internal. Add them from the component menu; do not reference the types.
  • The RNG providers (SeededRandomProvider, UnityRandomProvider) are internal. Use UseDeterministicRng / UseUnityRng, or supply your own IRandomProvider to LootRoller directly.
  • LootEntry's fields are public and mutable because the struct is serialized for the inspector. LootTable.Entries hands out copies, so mutating one changes nothing — do not build on it.
  • Reproducibility across builds. A seed repeats within a build, not across versions of a table.