RunAction does the deed.
srp:give_map on the NPCs that should have it, in the dashboard,
like any other action.
Stocking an NPC
What an NPC has to give is your script’s to set, never the dashboard’s:uses_stock locks at zero, and every delivered deed (a shop deliver
included) takes its share. Up to 32 items per NPC, each named as your
inventory spells it (a-z, 0-9, _, ., -, at most 48 characters), with
a whole count up to 1,000,000 or 'unlimited'. An NPC that is not this
server’s, or a shelf outside those bounds, is refused and nothing changes.
The dashboard shows the current shelf on the NPC’s page, read-only.
What happens: a player who asks for the map before handing over the amulet is
refused in the NPC’s own words (“Not until that amulet is in my hand.”) and
nothing reaches your script. After your hook reports
item_given { item = 'amulet' } the NPC may answer with the tag,
[srp:give_map copies=2] Here, take it., and RunAction receives
('give_map', source, npcCoords, { player_id = 12, copies = 2, item = 'treasure_map' }).
What fails: a declaration with a mistake is refused at registration, in your
console, with the field at fault —
[humalike] actions provider from my-adapter rejected: unknown observation in requirement 1 of action give_map
— and nothing is reported. A tag whose values do not fit the declaration
(copies=3 here) is dropped before it is spoken or remembered, so your script
never sees a value it did not declare.
A shop counter
For NPCs that sell, declare aCatalog instead of one action per product and
HumaLike runs the whole sale:
[srp:order water=10 pistol=1]); HumaLike checks the lines against the shelf
and the per-item limits, prices them and tells the NPC the total. Payment is
the reported item_given of the currency, in whole units: the payment
observation declares quantity = 'integer' (a number is refused at
registration), and a reported quantity of zero or less buys nothing. Once the money on the counter
covers the total, HumaLike calls deliver itself on the NPC’s next reply to
that player, with the basket and the change (164 paid on 163 → 1 back),
records the sale in plain words (“you handed over 10 x water for 50 cash, 1
back as change”) and takes every line off the shelf. The NPC is told, in that
reply’s prompt, that the goods go out with this reply unless it cancels.
Hand over everything or nothing: return false when the customer cannot
take it, and nothing is recorded — order and money stay on the counter, the
NPC is told the hand-over failed and HumaLike tries again on its next reply
to that player. The retry carries the same invocation id for as long as
the same order stands: the id is derived from the order (its record and its
basket), not from the money, so a top-up or a tip in between does not change
it and a RunAction that did deliver but lost the answer can recognise the
repeat instead of handing over twice. A new [srp:order ...] is a new deed
with a new id. A refund is identified the same way by the order it cancels
(by the payments themselves when money sits on the counter with no order).
If the shelf ran out in the meantime (another customer took the last unit),
the hand-over is not attempted; the NPC is told and can cancel for a refund.
Underpaid (160) → nothing moves, the NPC is told what is owed.
One counter deed at a time per customer: while a deliver or a refund for
a player has not answered yet, HumaLike neither retries it nor accepts a
[srp:cancel_order] for that player, and the NPC is told the hand-over is
pending — a slow callback can never pay out twice. Answer RunAction
promptly; HumaLike waits up to 5 seconds. An answer that never arrives (a
timeout, a dropped connection, an HTTP 5xx) is sent again at once, up to
three attempts in all, under the same invocation id — for every action,
not only the counter’s. Keep the ids you accepted and answer a repeat from
that record, and a lost reply never runs the deed twice. A false is an
answer: a rejected deed is not retried automatically.
If the customer backs out, the NPC’s [srp:cancel_order] calls refund with
exactly what is on the counter — and wins over a pending delivery in the
same reply, wherever in the line it was written, so what the NPC says and
what happens never diverge. A refund your script refuses (false) is
remembered too: the NPC is told the money did not go back, HumaLike retries
the refund on its next reply to that player, and the goods do not go out
meanwhile — the sale was called off out loud. A new [srp:order ...] while
one is open replaces it; the money on the counter stays and counts towards
the new total. An order the counter refuses (a line not sold, not enough on
the shelf, over a per-player limit) is recorded with its reason, so the NPC
reads why on its next turn instead of quoting a price for a sale it cannot
make. The model never authors a price, a quantity delivered or an amount
returned. A server with its own shop menu skips the conversation:
exports.humalike:PlaceOrder(npcId, playerId, { water = 2 }) sends the
picked lines, which HumaLike prices the same way against the live shelf; the
NPC then announces the total. A menu order does not replace one the player
has already paid for: while their paid order awaits hand-over it is refused
(recorded as order_refused with reason order_open), and the NPC says so.
A spoken re-take waits the same way while a hand-over is pending.
Money on the counter never expires: an unspent payment is the player’s until
a deliver or a refund spends it, so a customer who could not take the
goods and comes back hours later still has their money on the counter and
[srp:cancel_order] stays open to give it back. Only the order ages out
after an hour (yesterday’s basket is not what today’s money is for); the
money then sits on the counter with no order, and the NPC is told so. An
observation’s spent field is HumaLike’s own (it names the facts a deed
spent) and cannot be declared or reported. Declare deliver and refund
bare — no params, requires, limit, uses_stock, auto or
params_from — the counter fills them.
The counter’s keys are reserved in your namespace: you cannot declare
order, cancel_order, order_placed or order_refused as an action or an
observation, nor deliver/refund as observations, and no key may be both
an action and an observation.
Declaration
Keys are namespaced on the wire (
srp:give_map) and local in RunAction
(give_map). Everything is re-declared on every capability report, so a
changed or removed action takes effect on the next resource start; an NPC that
had a removed action enabled simply stops being offered it.
How the gate works
- A player saying the amulet was handed over is talk. Only the observation your script reports counts, and only for the player the NPC is answering.
- While a condition is unmet the action stays listed as something the NPC can
do, but the tag is not accepted this turn and the NPC is told to say what has
to happen first. Nothing reaches
RunAction. - The check runs again when the model writes the tag, so a fact that expired while it was answering does not slip through.
- A delivered deed is written into the NPC’s transcript as a world event
naming the facts it spent, so the NPC remembers doing it and every
consumecondition — and the shop counter — knows those facts are gone. RunActionreturningfalsemarks the invocation rejected; the model may write the tag again on its next reply (the per-action cooldown is released), and that is a new deed with a new invocation id. Only a lost answer is retried under the same id, by HumaLike, at once. Prefer expressing state as observations over refusing at run time: by then the NPC has spoken.- The NPC’s gate reads its recent conversation plus, beside it, the newest
of its server-namespaced world events (your reported facts, its performed
deeds, the counter’s records) however old they are, so a busy NPC forgets
neither an unpaid order nor a spent fact because a conversation scrolled
past it. Money on the counter does not expire: an unspent payment is
refundable until a settlement spends it, and
[srp:cancel_order]stays open while any unspent payment exists; only an order ages out (after an hour).
Debugging
- A refused declaration is printed by
RegisterProviderin your console with the field at fault. - The dashboard’s NPC page lists the server’s declared actions with their conditions; the transcript view shows the observations and the deeds.
- With
humalike_debug 1, the resource logs every inbound push (inbound push: npc=… action=srp:give_map) and its result.

