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 ownable — cider.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
delayand re-schedule themselves viatimer.Simpleif given one; - refuse while
_Jammedunlessoverrideis passed; - fire the map entity's native
lock/unlockinput for real doors and vehicles; - maintain both
_Lockedand theOBJ_LOCKEDbit.
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_door→waitprop_door→returndelay
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.