TinyMUX

Attribute System

Hardcode

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 VAVZ 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:

CodeFlagMeaning
+LOCKLocked; does not change ownership on @chown.
$NO_COMMANDNot checked for $-commands.
CCASE$-command matching is case-insensitive (requires R).
ENO_EVALContents are not evaluated when retrieved via u(), @trigger, etc.
HHTMLEmits from this attribute are not HTML-escaped.
INO_INHERITNot inherited by children via the parent chain.
MDARKOnly wizards and royalty can see this attribute.
NNO_NAMESuppress enactor’s name on @o-attribute output.
PNO_PARSE$-command and ^-listen matching uses the raw string.
RREGEXP$-command matching uses PCRE regular expressions.
TTRACEGenerate trace output when this attribute runs.
VVISUALAnyone who examines the object can see this attribute.
WWIZARDOnly 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 the ITER_PARENTS macro. 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 uses WITHOUT ROWID for 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_size in the configuration) is the single most impactful tuning parameter for attribute performance.
  • Bulk preload: The cache_preload mechanism 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_map from attribute names to numbers provides O(1) lookup for name resolution, so the number of user-defined attributes does not degrade lookup speed.