Attribute System
Attributes are named key-value pairs attached to objects in TinyMUX. Every object—player, room, exit, or thing—can carry any number of attributes. Each attribute has a name, a text value, an owner, and a set of flags that control visibility and behavior.
Built-in vs User-defined Attributes
TinyMUX divides attributes into two classes based on their numeric identifier.
Built-in attributes (numbers 0—255) are defined at compile time in the
AttrTable array in db.cpp. These include standard names like Desc,
Succ, Fail, Ahear, Listen, Alias, and the VA–VZ general-purpose
slots. Each built-in has a fixed attribute number constant (e.g., A_DESC,
A_SUCC) and a default set of flags. A handful of special internal attributes
(*PASSWORD, *PRIVILEGES, *MONEY) use names starting with * to make
them inaccessible from softcode.
User-defined attributes begin at number 256 (A_USER_START). When a
player sets an attribute with &MY_ATTR object=value and no built-in by that
name exists, the server allocates the next available attribute number via
vattr_alloc_LEN() in vattr.cpp. The name-to-number mapping is stored both
in memory (an unordered_map keyed by name) and persisted to the attrnames
table in SQLite.
Attribute Naming Rules
Attribute names may contain letters, numbers, and the characters
-_.@#$^&*~?=+|. They must start with a letter. Names of user-defined
attributes cannot be abbreviated—you must use the exact name when getting
or setting them. Built-in attributes support abbreviation because they are
looked up through the standard attribute table.
Attribute Flags
Each attribute carries a bitmask of flags that affect its visibility,
modifiability, and behavior. The flags visible on examine output are shown
with single-letter codes:
| Code | Flag | Meaning |
|---|---|---|
+ | LOCK | Locked; does not change ownership on @chown. |
$ | NO_COMMAND | Not checked for $-commands. |
C | CASE | $-command matching is case-insensitive (requires R). |
E | NO_EVAL | Contents are not evaluated when retrieved via u(), @trigger, etc. |
H | HTML | Emits from this attribute are not HTML-escaped. |
I | NO_INHERIT | Not inherited by children via the parent chain. |
M | DARK | Only wizards and royalty can see this attribute. |
N | NO_NAME | Suppress enactor’s name on @o-attribute output. |
P | NO_PARSE | $-command and ^-listen matching uses the raw string. |
R | REGEXP | $-command matching uses PCRE regular expressions. |
T | TRACE | Generate trace output when this attribute runs. |
V | VISUAL | Anyone who examines the object can see this attribute. |
W | WIZARD | Only wizards can modify this attribute. |
Additional internal flags such as AF_CONST (nobody can change it),
AF_GOD (only God can modify), AF_INTERNAL, and AF_IS_LOCK are used by
the server but cannot be set from softcode.
Attribute Ownership and Permissions
Each attribute value on an object has its own owner, stored separately from
the object’s owner. Normally the object’s owner owns all of its attributes,
but attribute ownership can diverge through @chown obj/attr. The LOCK flag
(+) on an attribute prevents it from being automatically chowned when the
object changes hands, and prevents the object’s owner from claiming it.
Write permission depends on the attribute’s flags and the actor’s privilege
level. Attributes flagged AF_WIZARD require wizard privileges to modify.
Attributes flagged AF_GOD require God. Attributes flagged AF_CONST cannot
be modified by anyone through normal commands.
Attribute Lookup and the Parent Chain
TinyMUX provides two attribute retrieval paths:
- Direct lookup (
atr_get) reads only the attribute stored directly on the target object. - Parent-chain lookup (
atr_pget) walks the parent chain using theITER_PARENTSmacro. Starting with the object itself, it checks each ancestor in turn until it finds a non-empty value or exhausts the chain.
The parent chain search respects two controls. If the attribute definition has
AF_PRIVATE set, the search stops at the first object—the attribute is
never inherited. If a specific attribute value on a parent has the AF_PRIVATE
flag in its per-instance flags, that value is skipped rather than returned to
children. The maximum parent depth is controlled by the parent_recursion_limit
configuration option, which defaults to 10.
Most softcode functions that read attributes (e.g., get(), u(),
@trigger) use the parent-chain path.
Storage Backend
All attribute data is persisted in a SQLite database (.sqlite file). The
schema uses two relevant tables:
attributes– stores attribute values keyed by(object, attrnum). Each row holds the value as a BLOB, plus the attribute owner and per-instance flags. The table usesWITHOUT ROWIDfor compact clustered storage.attrnames– maps user-defined attribute numbers to their names and default flags.
Writes use a write-through policy: every cache_put() call writes to SQLite
immediately via PutAttribute(), then updates the in-memory LRU cache. Reads
check the LRU cache first; on a miss, the value is loaded from SQLite and
inserted into the cache. The cache evicts the least-recently-used entries when
it exceeds the configured max_cache_size.
When a player connects or moves into a room, cache_preload() bulk-loads all
built-in attributes (numbers below 256) for that object into the LRU cache in
a single SQLite query, avoiding a burst of individual cache misses.
Cache statistics—hit count, miss count, entry count, and byte size—are
available to administrators via the @list cache_stats command, which calls
list_cache_stats() in attrcache.cpp.
The @attribute Command
The @attribute command manages the global attribute name registry for
user-defined attributes. It allows wizards to pre-define attribute names with
specific default flags before any player uses them. This is useful for
establishing server-wide conventions—for example, pre-defining a HELP
attribute with the AF_VISUAL flag so it is visible on all objects that set it.
Setting a user-defined attribute with &name object=value automatically
registers the name if it does not already exist, using no special default flags.
The @dbclean command, which historically renumbered attribute identifiers to
close gaps in the numbering, is a no-op under the SQLite backend. As the
message in vattr.cpp explains: “Attribute numbers are indexed keys; gaps
cost nothing.”
Performance Considerations
- LRU cache sizing: The attribute cache avoids repeated SQLite reads for
hot attributes. Sizing it appropriately (via
max_cache_sizein the configuration) is the single most impactful tuning parameter for attribute performance. - Bulk preload: The
cache_preloadmechanism reduces latency spikes when objects are first touched by front-loading their built-in attributes. - Parent chain depth: Deep parent chains multiply the number of attribute lookups per access. Keeping parent hierarchies shallow (well within the default limit of 10) avoids unnecessary database hits.
- Attribute value size: Values are capped at
LBUF_SIZE(8000 bytes). The cache tracks total byte usage, so many large attribute values will cause more frequent evictions. - Name map: The in-memory
unordered_mapfrom attribute names to numbers provides O(1) lookup for name resolution, so the number of user-defined attributes does not degrade lookup speed.