ADR-0004 — Item definitions as GameResource assets¶
Status: ✅ Accepted Date: 2026-07-27 Supersedes: — Superseded by: —
Context¶
The original defines each item as a Lua file assigning fields to a pre-created ITEM global.
items/food/cake.lua in its entirety:
ITEM.Name = "Delicious Cake";
ITEM.Cost = 2500;
ITEM.Model = "models/foodnhouseholditems/cake.mdl";
ITEM.Store = true;
ITEM.Plural = "Delicious Cake's";
ITEM.Description = "WOW what a delicious cake! Great for parties!";
ITEM.Hunger = 50;
ITEM.Base = "food";
UniqueID comes from the filename, Category from the directory. There are 139 such files
across 15 categories.
This authoring experience is genuinely good and is a large part of why the original accumulated so much content. Eight lines, no ceremony, obvious. Any replacement that makes adding a sandwich harder than this has failed, regardless of its other merits.
But it is executable code, and that has costs the original paid repeatedly:
- Field names are string keys, so a casing mistake is silent.
ITEM.Maxis read asitem.maxinsv_inventory.lua, so no item has ever had a working stack limit (D-03). - Inheritance is a
table.Mergeguarded by falsiness, so an item that deliberately setsStore = falsehas the base's value copied over it; and the multi-base branch iteratesself.basewhile testingself.Base, so array inheritance silently does nothing (D-04). - Definitions call
GM:GetPlugin(...)at file scope, making load order load-bearing (D-08). - A content author must be trusted with the ability to execute arbitrary code on the server.
- Nothing can be validated ahead of time, and there is no editor support.
S&box provides a direct answer:
custom GameResource assets —
inherit GameResource, decorate with [GameResource] to claim a file extension and an editor
icon, and [Property] members serialize to JSON, get an inspector, and hot-reload in-game.
Decision¶
Item definitions are
GameResourceassets (.item), authored in the S&box inspector or as JSON, with typed properties. Item behaviour is components, referenced by the definition — not code embedded in it.
/// <summary>
/// The authored template for an item. Shared by every <see cref="ItemInstance"/> created
/// from it; contains no per-instance state.
/// </summary>
[GameResource("Item", "item", "An Applejack item definition.", Icon = "inventory_2")]
public sealed class ItemDefinition : GameResource
{
/// <summary>Display name, singular. Shown in inventories and tooltips.</summary>
[Property] public string Name { get; set; } = "";
/// <summary>Display name, plural. Used when reporting quantities.</summary>
[Property] public string PluralName { get; set; } = "";
/// <summary>
/// Space consumed in an inventory. <b>Negative values grant capacity</b> -- this is how
/// pockets work, and it is deliberately preserved from the original.
/// See Documentation/Legacy/03_INVENTORY_AND_ITEMS.md §1.2.
/// </summary>
[Property] public int? Size { get; set; }
/// <summary>Purchase price. Sale refunds <see cref="EffectiveRefundValue"/>, defaulting to half.</summary>
[Property] public int? Cost { get; set; }
/// <summary>Whether multiple copies occupy a single inventory entry. See ADR-0006.</summary>
[Property] public bool? IsStackable { get; set; }
/// <summary>Maximum per stack. Zero means unlimited. Enforced -- unlike the original.</summary>
[Property] public int? MaximumStack { get; set; }
/// <summary>Behaviours attached to instances of this item, e.g. Edible, Equippable.</summary>
[Property] public List<ItemBehaviour> Behaviours { get; set; } = [];
}
Implementation note (Code/Items/ItemDefinition.cs): Size, Cost, IsStackable,
MaximumStack, Category are nullable in the actual type, not plain bool/int as shown
above. A plain bool/int loaded from JSON cannot distinguish "explicitly set to false/0" from
"never touched, sitting at its C# default" — exactly the distinction this ADR's own D-04
compliance requirement depends on — and this offline environment cannot verify whether
GameResource's real serialisation tracks per-property "was this explicitly authored" (no
compiler, no editor). Null means "unset, inherit"; any value, including false/0, is
explicit and final. Every other caller reads the computed Effective* accessors
(EffectiveSize, EffectiveCost, …); only ItemRegistry.ResolveInheritance touches the raw
nullable properties. Name/PluralName/Description/Model stay plain strings — empty is a
low-stakes "unset" sentinel for text, and D-04's concern is meaningful falsy values, not
display copy.
Inheritance is explicit and validated. A definition may name a parent; resolution happens once at load, distinguishes unset from set to a falsy value, and an unresolvable parent is a hard load failure naming the asset — never a silent empty merge.
Behaviour is composition, not inheritance. The original's nine bases become behaviour
components: Edible (restores a need), Equippable (the timed equip flow), Container
(holds an inventory), Contraband (spawns an entity, counts against a cap). An item is what
it does, and it can do several things.
Categories remain first-class, because jobs gate manufacturing rights on them (Legacy/05) — but a category becomes an explicit property rather than the containing directory.
The same decision applies to jobs and recipes, which have the same problem more acutely:
cider.team.add takes fifteen positional parameters.
Alternatives considered¶
Plain JSON files loaded by hand¶
Data-driven, no engine coupling, easy to generate. Rejected: it forgoes the inspector, the
asset browser, hot reload, reference validation and compile-time typing — all of which
GameResource provides for free. We would be reimplementing the engine's asset pipeline.
C# classes, one per item¶
Fully typed, refactorable, debuggable, and IDE-supported.
Genuinely tempting, and rejected on the founding requirement: adding a sandwich must not require a compiler, and content authoring must not require a programmer. 139 item classes is also 139 files that recompile the game.
Hybrid — assets for data, C# for behaviour¶
This is the decision. Data is authored; behaviour is a small set of reusable components referenced by the data. Called out explicitly because it is easy to read the decision as "assets only" and then discover there is nowhere to put a bespoke behaviour.
A scripting language for item callbacks¶
Restores the original's ability to write arbitrary onUse logic per item. Rejected: it
reintroduces the untyped, unvalidated, arbitrary-code-execution surface this ADR exists to
remove, and it means shipping and sandboxing an interpreter.
Consequences¶
Positive¶
- Adding an item needs no compiler and no programmer.
- Typed properties, so the D-03 and D-04 casing and merge classes of bug cannot recur.
- Inspector editing, hot reload in-game, and asset-browser discovery for free.
- Definitions are validated at load with named failures.
- Content authors need no code-execution trust.
- Behaviours compose, so an item can be edible and a container.
Negative¶
- Bespoke one-off behaviour needs a new component, where the original allowed an inline
onUse. This is the real cost: some items in the original do genuinely strange, specific things. Mitigation is a good library of parameterised behaviours; the residual cases need a programmer. - Behaviour components must be designed to be composable, which is more up-front thought than writing a callback.
- Migrating 139 legacy items becomes a content task — but it was always going to be, and it is deliberately deferred until the systems exist.
Neutral¶
- Item IDs stay
snake_casematching the original, soDocumentation/Legacy/remains directly usable as a content specification. - Definitions live under
Assets/, notCode/.
Compliance¶
D04_ItemWithUnresolvableBaseFailsLoudly— a named hard failure, not a silent merge.D03_PerItemStackLimitIsEnforced.- Inheritance distinguishes unset from set-to-default: a test asserts that a definition
explicitly setting
IsStackable = falseis not overwritten by a parent'strue. - Load-time validation: every definition's model, category and parent resolve, or startup fails naming the asset.
- No
ItemDefinitionproperty may beinternalor a field — the serialiser only sees public get/set properties.
Notes¶
Revisit if the behaviour-component library proves insufficient in practice — the signal would be a proliferation of single-use components, each serving one item. That would suggest either better parameterisation or, as a last resort, a constrained expression language for simple conditions.