The gamemode decides what a pickup does and when it is consumed
Status: Accepted (2026-06-26)
Context
A pickup has two halves. Its existence is platform work: placing it, streaming it to the players near it, checking that a touch is genuine, removing and respawning it. Its meaning is gameplay: whether it heals, arms or does something else entirely, and whether a touch should consume it at all. If the core stored the reward and applied it on touch, every new kind of pickup and every rule ("not at full health") would grow inside the core. The platform should raise events and own elements; the gamemode should decide what an interaction means.
This builds on the server facade (ADR 0009), the grant verbs (ADR 0013, ADR 0014) and the event stream that carries player actions to resources (ADR 0048). The wire did not change.
Decision
- The core owns the lifecycle; the gamemode owns the effect and the decision to consume. A resource creates a pickup with
dlmp.create_pickup(spec)(model, position, scale, highlight,behavior"collectible" or "static",respawn_ms, world and interior); the pickup belongs to that resource and is removed when the resource stops. A map loaded withdlmp.load_mapcreates pickups too and hands their attributes to the mode. The core streams the pickup, validates eachPickupTouch(reliable order, spawned and alive player, active pickup streamed to that player, last known position within the radius) and raisespickup_interact(player, pickup, info), whereinfoholdsbehaviorandmodel. The event is sourced at the pickup, so it reaches the owning resource'sdlmp.resource_roothandlers anddlmp.roothandlers. - The core never consumes a collectible by itself. The gamemode consumes it with
dlmp.collect_pickup(pickup), which removes it and schedules its respawn. Until then the client keeps re-touching at a low rate and the event fires again on each accepted touch. A static pickup is never consumed; it fires once per entry per player. - The core stores no effect. The gamemode keeps its own map from pickup id to effect and applies the effect with named facade verbs:
player:give_weapon(weapon, ammo, set_current),player:add_health(amount, max),player:add_armor(amount, max). A pickup the gamemode does not know does nothing. Grantis not a public type. The facade speaks in verbs; the grant enum is its internal delivery primitive. Additive grants (never lower) and the absoluteset_health/set_armor(may lower) stay separate verbs, because they express different intents.- The full-vital rule is gamemode logic. The shipped
demoresource declines a health or armor pickup while the player's vital (player:health()/player:armor(), from the server's ledger) is already at the effect's ceiling: it neither consumes the pickup nor grants anything. The pickup stays active, the client keeps re-touching it, and it is collected as soon as the vital drops. Weapon pickups and pickups without an effect are not gated.
Consequences
- New kinds of pickups, new rewards and new rules need no change in the core.
- The core raises the interaction on every accepted touch of an unconsumed collectible, so a gamemode handler must be idempotent until it consumes the pickup (
collect_pickupreturnsnil, errfor a pickup already consumed). - The operator console has no reward pickups; rewards exist only where a resource defines them.
Alternatives considered
- An opaque tag stored on the core pickup, mapped to an effect by the gamemode. Rejected: even an opaque tag is per-pickup gamemode state in the core; all gamemode state stays in the gamemode.
- The core consumes a collectible on touch and the full-vital rule is dropped. Tried and rejected after an in-game check: additive grants made the touch harmless, but the pickup visibly vanished and respawned at full health, which is wrong for the game.
- The core applies the full-vital rule, knowing the effect's ceiling. Rejected: it would bring effect knowledge back into the core.
