Command Parser and Targeting

Audience: Engine Developer Status: ✅ Ready

How a typed line becomes an action: the parser turns input into a verb + arguments, resolves a target using the classic Diku targeting grammar, and — for messages the action produces — routes perspective-aware output through act(). This page covers targeting and act(); the dispatch model (the four-namespace verb precedence, abbreviation, and rank gating) is documented on Builder Commands.

Related: Zone Runtime & Actor Model, Entity Component Model, Abilities & Effects.

The targeting grammar

Classic Diku (targeting.go):

sword        → first visible match for "sword"
2.sword      → the 2nd match
all.coin     → every match for "coin"
all          → everything in scope
long sword   → an entity whose keywords include BOTH words (isname prefix)

parseTargetSpec produces a TargetSpec{all, bare, index, keywords}. Key rules:

  • The all. / N. selector prefix applies only to the first word; the rest are additional isname keywords.
  • all. sets all; a numeric N. sets index. An explicit 0.x maps to index = -1, an “explicit-but-no-match” sentinel distinct from the unqualified index = 0. A non-numeric prefix like a.sword is treated literally (the dot stays part of the keyword).
  • Bare all (no keyword) means everything in scope.
  • Untrusted-input hardening: atoiBounded accepts only digits and caps at six (< 1e6), so a pasted mega-number can’t drive large work or overflow; work is bounded by the scope population, never by input length.

isname matching (matchesKeywords): every typed word must be a case-insensitive prefix of one of the candidate’s keywords. bare matches everything; an empty spec matches nothing.

Search scopes

Scope values: ScopeInventory, ScopeEquipment, ScopeRoomLiving, ScopeRoomItems, ScopeContainer. scopeCandidates concatenates candidate lists in the order the verb passes them, so a verb searching room-then-inventory finds the floor item before the carried one.

Scope Candidates
ScopeInventory actor.contents
ScopeRoomLiving room occupants with a Living, excluding the actor
ScopeRoomItems room contents without a Living, excluding the actor
ScopeEquipment the actor’s Wearer.worn, surfaced in content wear-slot order
ScopeContainer resolved explicitly by get x from yresolveInContainer

Resolve pulls candidates, filters to visible matches (via canSee, below) in scope order, then applies Diku selection: all → every hit; index > 0 → the Nth (1-based) or nil; index < 0nil (the explicit-no-match sentinel); default → first hit.

Visibility is part of targeting

Resolve calls z.canSee(actor, candidate) before matching. canSee delegates to visibleTo — the single visibility chokepoint shared by targeting and act()’s leak surface. It honors: self/nil always visible; holylight sees all; invisible unless the viewer has detect_invis; a dark room unless the viewer has light/infravision; hidden unless sense_hidden; and staff wizinvis by trust rank. These are open-string flags the engine reads and content sets (holylight and the other trust flags are reserved — content cannot self-grant them; see Trust Tier Model).

Not a parser token: me / self is not part of the command targeting grammar. A player’s only keyword is its own character name, and the room scopes exclude the actor — so a player cannot target themselves by name in a room scope. The self/target synonyms live only in the ability/effect-op layer and in Lua handler binds, not in the player command grammar.

act() — perspective messaging

The Diku idiom: one call emits the right variant to the actor, the victim, and the room, with $-substitutions per recipient (act.go). Recipient selection (ActTo): ToActor, ToVictim, ToRoom, ToRoomExceptActor.

Substitution tokens (this is the complete set — a hand-written single-pass scanner, not Sprintf, so there is no format-string injection path):

Token Expands to
$n / $N actor name / victim name
$p / $P object name / second object name
$t / $T literal text args (copied verbatim, never re-scanned)
$$ a literal $

Not implemented: the pronoun tokens $s / $e / $m (possessive / subjective / objective) do not exist — there is no pronoun handling and no gender field to feed one.

nameFor is the per-recipient name resolver and the leak guard: "You" when the viewer is the entity, and "Someone" / "someone" when the viewer can’t see the actor — the same canSee predicate as targeting, so messaging can never leak what targeting hid.

The sound-vs-presence split

Two concealment behaviors, both keyed on canSee:

  • Presence lines (arrivals/departures) use actConceal: a room recipient who can’t see the actor gets nothing at all. A hidden, sneaking, or dark-room mover moves silently.
  • Sound / masked lines (speech, combat) use plain act(): the line is delivered, but the unseen actor is masked as "Someone".

The reasoning: a plain act() “Someone arrives.” would leak that something concealed is present, so presence uses actConceal while ongoing sound stays audible-but-anonymous. (Presence-line stealth deliberately does not evade an aggressive mob’s aggro-on-entry — that is a separate, intentional choice.)