Player tracking¶
CTLD tracks connected human players without MIST. Two cooperating singletons cover the concern:
| Singleton | Source | Responsibility |
|---|---|---|
CTLDPlayerTracker |
src/CTLD_core.lua |
Lightweight identity index — who occupies which slot |
CTLDPlayerManager |
src/CTLD_player.lua |
Per-player state (CTLDPlayer entities), F10 menu, cargo tracking |
Both follow the X = class() + getInstance() idiom described in
Architecture and consume DCS slot events through the single
CTLDDCSEventBridge handler rather than registering with world directly.
CTLDPlayerTracker — slot identity index¶
CTLDPlayerTracker maintains a double index of connected humans, with no dependency on
MIST:
self._byUnit = {} -- unitName → playerName
self._byPlayer = {} -- playerName → { unitName, coalition }
It is populated from three sources, in order of authority:
| Source | Role |
|---|---|
S_EVENT_PLAYER_ENTER_UNIT / S_EVENT_PLAYER_LEAVE_UNIT |
Primary, event-driven add/remove |
S_EVENT_BIRTH |
Backup — catches the first joiner if PLAYER_ENTER_UNIT was missed |
coalition.getPlayers() scan |
Safety net; _scanAllSlots() runs immediately at init, then every 30 s for the first 3 minutes |
The scan is idempotent (it only inserts slots not already indexed), so the three sources never conflict. After 3 minutes the event stream is considered sufficient and the periodic scan stops.
Public API¶
| Method | Returns |
|---|---|
getPlayerByUnit(unitName) |
playerName occupying the unit, or nil if AI/unoccupied |
getUnitByPlayer(playerName) |
{ unitName, coalition }, or nil if not connected |
getAllPlayers() |
list of { playerName, unitName, coalition } |
isPlayerUnit(unitName) |
true if the unit is currently occupied by a human |
CTLDPlayerTracker.getInstance() requires CTLDDCSEventBridge to be initialised first, because
init() registers its handlers on the bridge.
CTLDPlayer — per-player entity¶
CTLDPlayer = class() is an identity snapshot plus mutable cargo state, constructed by
CTLDPlayerManager when a human enters a slot. Its fields, set in CTLDPlayer:init(data):
{
unitName = "UH-1H-1", -- DCS unit name (the map key in CTLDPlayerManager)
groupId = 9901, -- group:getID() (used for F10 menu ops)
groupName = "Grp_1", -- group:getName()
coalition = 2, -- coalition.side.BLUE
typeName = "UH-1H", -- unit:getTypeName()
isTransport = true, -- detected capability (see below)
canCarryVehicles = false, -- detected capability (see below)
loadedTroops = {}, -- list
loadedCrates = {}, -- list of CTLDCrate
loadedVehicles = {}, -- list of CTLDVehicle
}
Cargo lists are mutated by identity-matched helpers on the entity:
addLoadedCrate(crate)/removeLoadedCrate(crate)addLoadedVehicle(vehicle)/removeLoadedVehicle(vehicle)
loadedTroops is initialised but not maintained by these helpers — troops in transit are owned
by CTLDTroopManager and queried via getInTransit(unitName) (see the "Check Cargo" command
below).
Capability detection¶
CTLDPlayerManager:_detectCapabilities(unit) derives the two capability flags from
ctld.gs("capabilitiesByType") keyed by unit:getTypeName():
isTransport—truewhen the type has an entry incapabilitiesByType.canCarryVehicles—truewhen that entry setscanTransportWholeVehicle == true.
CTLDPlayerManager — lifecycle & menu owner¶
The manager keeps its own map, distinct from the tracker's identity index:
self._players = {} -- unitName → CTLDPlayer
Lifecycle¶
| DCS event | Handler | Action |
|---|---|---|
S_EVENT_PLAYER_ENTER_UNIT |
onPlayerEnterUnit(event) |
Skip AI (no getPlayerName()), apply the pilot-name gate, build a CTLDPlayer, store it, call buildMenu(playerObj) |
S_EVENT_PLAYER_LEAVE_UNIT |
onPlayerLeaveUnit(event) |
Remove the player's DCS menu items and drop it from _players |
S_EVENT_TAKEOFF |
onTakeoff(event) |
Set _isFlying = true, refresh flight-dependent menu sections |
S_EVENT_LAND |
onLand(event) |
Clear _isFlying, refresh menu sections after a 1 s settle delay |
getPlayer(unitName) returns the tracked CTLDPlayer, or nil.
_scanExistingPlayers() is a multiplayer safety net: it runs once at the end of
ctld.initialize() and reschedules every 30 s, synthesising onPlayerEnterUnit for any occupied
slot not yet in _players (slot switches, AI takeover, late joiners).
Pilot-name gate: when ctld.gs("addPlayerAircraftByType") == false, only unit names listed in
ctld.gs("transportPilotNames") receive a CTLD menu; all others are logged and skipped.
Flight-state poller¶
Because S_EVENT_TAKEOFF / S_EVENT_LAND fire with a 3–5 s delay for helicopters, init() also
starts a 0.5 s timer.scheduleFunction poller. It reads ctld.utils.inAir(unit) per tracked
player and, once a new state holds for two consecutive ticks (DEBOUNCE_TICKS, ~1 s), commits it
to playerObj._isFlying and runs the same menu-refresh chain. The onTakeoff / onLand handlers
still fire, but by then _isFlying already matches and the refresh is a fast no-op.
Cargo state via the event bus¶
The manager subscribes to EventDispatcher (see Events) to keep each
CTLDPlayer's cargo lists current, then refreshes the affected menus:
| Subscribed event | Effect |
|---|---|
OnVehicleLoaded |
addLoadedVehicle, then refreshForUnit |
OnVehicleUnloaded |
removeLoadedVehicle, then refreshForUnit |
OnCrateLoaded |
addLoadedCrate, refreshForUnit, and refresh the crate's Unpack section |
OnFOBDeployed |
Refresh Request Equipment / JTAC sections for every grounded player |
CTLDPlayerManager publishes no events.
F10 menu¶
CTLDPlayerManager owns the CTLD F10 menu. Managers contribute sections through
registerMenuSection(sectionDef) (idempotent by key); scene files loaded before the singleton
exists may pre-queue sections with deferMenuSection(sectionDef), flushed during init().
buildMenu(playerObj) wipes and reconstructs the menu atomically via ctld.MenuManager. It
always adds a Check Cargo command that sums the transport's current load — crates (grouped by
descriptor), troops in transit (CTLDTroopManager:getInTransit), and whole vehicles
(CTLDVehicleSpawner:findLoadedVehicles, weighted from ctld.gs("groundVehicleWeights"), default
2500 kg) — and reports a per-type breakdown plus a total in kilograms. Registered sections are
then rendered in ascending order, each gated by its optional configKey
(ctld.gs(configKey) == true).
refreshForUnit(unitName) and refreshAll() request a deferred menu refresh through
ctld.MenuManager for one or all tracked players. Cargo weight is only ever displayed — there
is no per-transport weight cap or load rejection in this subsystem.