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.DontSaveand is not an asset. Destroy it when you are done, as you would anyScriptableObjectyou created. - Values are stored as given, not normalised. Bounds are applied on read —
RollsMinfloors at zero andRollsMaxnever reports belowRollsMin, 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
LootResultargument. 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) areinternal. Add them from the component menu; do not reference the types. - The RNG providers (
SeededRandomProvider,UnityRandomProvider) are internal. UseUseDeterministicRng/UseUnityRng, or supply your ownIRandomProvidertoLootRollerdirectly. LootEntry's fields are public and mutable because the struct is serialized for the inspector.LootTable.Entrieshands 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.