Skip to content

Entities, Doors and Property

Ownership is the backbone of Applejack's social layer. It is also its largest single library.

Sources: libraries/sv_entity.lua (812 lines, ~40 functions), libraries/cl_entity.lua, plugins/doors/, derma/cl_door.lua, derma/cl_access.lua, entities/entities/cider_padlock/, sv_commands.lua.


1. The ownership model

Any entity can be made ownablecider.entity.makeOwnable(entity). Doors, vehicles and containers are the common cases, but the mechanism is generic.

An ownable entity carries:

entity._Owner = {
    name    = "Nobody",   -- display name, mirrored to NWString "cider_ownerName"
    owner   = NULL,       -- the owner (see §1.1)
    access  = {},         -- additional accessors (see §1.2)
    master  = NULL,       -- link to a master entity  (double doors)
    slaves  = {},         -- entities this one controls
}

1.1 Three kinds of owner

An owner is one of:

Kind Represented as Set by
Player the player entity setOwnerPlayer(entity, player)
Team (a job) the numeric team index setOwnerTeam(entity, teamid)
Gang (a faction within a group) the string "group;gang" setOwnerGang(entity, group, gang)

That the three are stored in one field with three different runtime types is exactly the sort of thing C# will not tolerate, and the new model uses a discriminated owner type.

The gang encoding matters: hasAccess builds tag = "group;gang" and also tag2 = "group;0", and grants access if the owner matches either. Gang 0 means the whole group. So a door owned by "1;0" is open to every job in group 1 (all Officials), while "1;2" is one gang within it. A neat hierarchical shorthand, implemented as string concatenation.

1.2 Access lists

entity._Owner.access is a flat array whose entries may be a player, a team index, or a "group;gang" tag — the same three kinds again. giveAccessPlayer / giveAccessTeam / giveAccessGang and their take counterparts maintain it.

cider.entity.hasAccess(entity, player) returns true if the player is the owner, matches the owner's team/gang tag, or appears in the access list under any of the three forms. Unowned entities are accessible to nobody — hasAccess returns false early.

1.3 Access survives disconnect

cider.entity.saveAccess(player) and restoreAccess(player) snapshot a disconnecting player's entries so that a brief drop does not cost them their property. This is an important quality-of-life behaviour and must be reproduced — but properly, as part of the persistence layer rather than as an in-memory side table.


2. Doors

2.1 State as a bitfield

Door state is packed into ent:GetDTInt(3) (sh_enumerations.lua:22-28):

Flag Value Meaning
OBJ_LOCKED 1 Locked
OBJ_SEALED 2 Sealed — permanently locked, cannot be opened at all
OBJ_PADLOCKED 4 A cider_padlock entity is attached
OBJ_CONTAINER 8 Entity is a container
OBJ_INUSE 16 Currently being used

Plus NWString "Name" for the door's display name and NWString "cider_ownerName" for the owner's. libraries/cl_entity.lua decodes all of this client-side to drive the ESP overlay.

This is D-16: an untyped bitfield in a numbered slot, with the meaning of slot 3 held by convention across two files.

2.2 Lock and unlock

makeOwnable attaches Lock and UnLock methods directly to the entity as closures. Both:

  • accept a delay and re-schedule themselves via timer.Simple if given one;
  • refuse while _Jammed unless override is passed;
  • fire the map entity's native lock / unlock input for real doors and vehicles;
  • maintain both _Locked and the OBJ_LOCKED bit.

Note the duplicated state: _Locked and the bitfield must agree, and are updated separately. Two sources of truth for one fact.

2.3 Opening, closing, jamming and breaching

openDoor(ent, delay, unlock, sound, jam) / closeDoor(ent, delay, lock).

The jam path is the interesting one. A breached door — see the cider_breach entity — is jammed open for config["Jam Time"] (default 60 seconds), during which Lock and UnLock refuse without an override. On expiry, unbreach() restores the door's autoclose behaviour by writing the map keyvalue back:

  • func_doorwait
  • prop_doorreturndelay

set to ent._Autoclose or config["Door Autoclose Time"] (default 10 seconds).

Breaching a door and having it stay open for a minute is excellent tension. Preserve it.

2.4 Master / slave linking

Double doors are two entities that must behave as one. setMaster(entity, master) links them; getSlaves, hasSlaves, updateSlaves, giveSlave, takeSlave maintain the graph. Ownership and access are read from the master. The /setmaster command (sv_commands.lua:1406) exposes this to players with the right access.

This is a real requirement (every map has double doors) and generalises: a property group of entities sharing one ownership record. The new design models it as an explicit group rather than a parent pointer plus a child array kept manually in sync.

2.5 Padlocks

entities/entities/cider_padlock/ — a physical entity attached to a door, reflected by OBJ_PADLOCKED. A padlock is a real object that can be seen, and therefore attacked or picked, which is much better than an invisible lock flag.


3. Buying and selling doors

Setting Default Source
Door Cost 150 sh_config.lua
Door Tax true
Door Tax Amount 50 Charged per door held
Maximum Doors 5
Jam Time 60 s
Door Autoclose Time 10 s

meta:GiveDoor / meta:TakeDoor (metatables/sv_player.lua:158). Selling refunds Door Cost / 2 — 50%, consistent with item sales.

The recurring tax is the mechanic worth highlighting: property is not a one-off purchase but an ongoing cost, so hoarding doors is actively punished. Combined with the 5-door cap, this is a deliberate anti-monopoly design. Preserve both.

The doors plugin (plugins/doors/sv_init.lua, 241 lines) persists per-map door metadata to doors/<map>.txt and provides the hooks EntityNameSet, EntityMasterSet, EntitySealed, EntityOwnerSet, PlayerCanOwnDoor, PlayerCanJamDoor.


4. Commands

Command Line in sv_commands.lua Purpose
/door 939 Buy, sell, and manage a door
/entity 1068 The same for any ownable entity
/setmaster 1406 Link a door to a master
/seal 1424 Seal a door permanently shut
/setname 1447 Name an entity
/setowner 1461 Transfer ownership

Client UI is derma/cl_door.lua and derma/cl_access.lua, the latter a mass access-editing panel driven by the cider_massAccessSystem usermessage.


5. Housing

There is no housing system. "Housing" in Applejack is emergent: a house is a set of doors you happen to own, with an access list you happen to have configured. There is no rent (only the door tax), no leases, no property boundaries, no keys as items.

The master plan's Phase 16 (Housing: ownership, permissions, storage, rent, keys, locks, containers, persistence) is therefore mostly new design. The pieces that exist to build on are: entity ownership, access lists, the door tax as a rent analogue, containers as storage, and padlocks as physical locks.

Design note: the emergent version is arguably better than an explicit housing system — it means any building on any map is ownable without map-side authoring. Prefer generalising the ownership model over introducing a separate "property" concept.


6. What survives

Survives: ownership by player / team / gang; hierarchical gang access (group;0); per-entity access lists; access surviving disconnect; door tax and the ownership cap; 50% sale refund; breaching with a timed jam; autoclose; master/slave door groups; padlocks as physical entities; owner and status visible on the door itself.

Does not survive: the untyped bitfield; three owner kinds in one field; methods attached to entities as closures; duplicated _Locked state; a 812-line library; per-map GLON text files.

Redesign: Architecture/08_DATA_MODEL.md (ownership schema) and the world-object components described in Architecture/10_INTERACTION_FRAMEWORK.md.