Gestion des zones¶
Source : src/CTLD_zone.lua
Entités : CTLDTroopZone, CTLDLogisticZone
Singleton : CTLDZoneManager
Les zones sont les ancres spatiales de CTLD : c'est là que les troops embarquent et se déploient, que les objectifs de mission sont comptabilisés, que les crates et les véhicules peuvent être servis, et que les transports IA chargent et déchargent leur cargaison. Un unique manager possède chaque zone ; les entités portent l'état propre à chaque zone et les tests de géométrie.
CTLDZoneManager suit l'idiome singleton décrit dans
Architecture — CTLDZoneManager = class(), une _instance privée et une
fabrique getInstance() qui exécute init() au premier accès. Les deux classes d'entité sont
de simples types class() instanciés via :new(data).
Types de zone¶
| Type | Préfixe / source | Entité | Rôle |
|---|---|---|---|
| TRZ | zone de trigger DCS TRZ_ |
CTLDTroopZone |
Pickup de troops, objectif d'extract, mixte, ou marqueur inerte |
| WPZ | zone de trigger DCS WPZ_ |
CTLDTroopZone (isWaypoint) |
Destination de marche pour les troops déployées |
| LGZ | zone de trigger DCS LGZ_ |
CTLDLogisticZone |
Services logistiques crate / véhicule |
| AIZ | table de config aiZones |
CTLDTroopZone (isAIPickup / isAIDropoff) |
Pickup / dropoff réservé au transport IA (Feature S/T) |
| Legacy | config troopZones / wpZones / logisticUnits |
CTLDTroopZone / CTLDLogisticZone |
Rétrocompatibilité avec les anciennes conventions PKZ/IAZ/WPZ/EXZ |
| EXZ (dynamique) | createExtractZone() à l'exécution |
CTLDTroopZone |
Extract zone créée depuis un DO SCRIPT de mission |
Les zones TRZ, WPZ et LGZ sont découvertes en parcourant env.mission.triggers.zones à
l'init. Les zones AIZ sont chargées depuis ctld.gs("aiZones") et référencent par leur nom une
zone de trigger existante de l'éditeur de mission (dcsZoneName), résolue via
trigger.misc.getZone. Les zones legacy proviennent des tables de config ctld.gs(...) et sont
chargées en dernier afin qu'une définition moderne TRZ_/LGZ_ l'emporte toujours : les
entrées existantes ne sont jamais écrasées.
Convention de nommage TRZ¶
Une troop zone encode tout son comportement dans le nom de la zone de trigger — un schéma positionnel strict où les cinq champs sont tous requis :
TRZ_<name>_<A|R|B|N>_<stock>_<flag>_<target>
| Position | Champ | Type | Valeurs | Signification |
|---|---|---|---|---|
| 1 | TRZ_ |
préfixe | littéral | Identifiant du type de zone |
| 2 | name |
string | quelconque, sans underscore, hors mots réservés | Identifiant de zone (zoneName) |
| 3 | coalition |
enum | A R B N |
A=all, R=RED, B=BLUE, N=NEUTRAL |
| 4 | stock |
entier 0–999 | 0=pas de pickup, 1–998=limité, 999=illimité |
Capacité d'embarquement des troops |
| 5 | flag |
string | nom de flag DCS, ou nil |
Flag d'objectif incrémenté à l'extract ; nil=pas d'objectif |
| 6 | target |
entier ≥0 | 0=pas de condition de victoire, N≥1=seuil de soldats |
Nombre de soldats requis pour compléter l'objectif |
Mots réservés qui ne peuvent pas être utilisés comme name : nil, A, R, B, N.
Sémantique des champs¶
Le parser convertit les valeurs des champs affichés à l'écran vers un stockage interne
volontairement différent, de sorte que nil puisse signifier « capacité absente » :
Valeur stock |
pickMaxStock interne |
Signification |
|---|---|---|
0 |
nil |
La zone ne peut pas embarquer de troops |
1–998 |
1–998 |
Stock limité, décrémenté au chargement |
999 |
0 |
Pickup illimité |
Valeur flag |
objectiveFlag interne |
Signification |
|---|---|---|
nil |
nil |
Pas de flag d'objectif |
| n'importe quelle string | la string | Nom de flag DCS, incrémenté du nombre de soldats à l'extract |
Valeur target |
objectiveTarget interne |
Signification |
|---|---|---|
0 |
nil |
Aucun seuil de victoire défini |
N≥1 |
N |
Nombre de soldats requis pour compléter l'objectif |
Utilisez
999pour un pickup illimité — et non0.0signifie « cette zone ne peut pas embarquer de troops ».
Pour la coalition, A→0, R→coalition.side.RED (1), B→coalition.side.BLUE (2),
N→coalition.side.NEUTRAL (0). A et N stockent tous deux 0 ; en interne, CTLD traite 0
comme « accepte toutes les coalitions », et le multijoueur DCS n'a pas de joueurs NEUTRAL, donc
les deux se comportent de manière identique.
Rôles de zone¶
Une TRZ peut jouer n'importe quelle combinaison de :
- Pickup (
stock > 0) : les troops embarquent ici ; l'embarquement décrémente le stock. - Extract (
flag ≠ nil) : les troops déployées ici incrémentent le flag d'objectif du nombre de soldats. - Mixte (les deux) : prend en charge l'embarquement et l'extract d'objectif.
- Inerte (
stock=0,flag=nil) : parsée mais sans fonction — un marqueur nommé.
Exemples¶
| Nom de zone | Rôle | Embarquement | Objectif |
|---|---|---|---|
TRZ_base_B_50_nil_0 |
Pickup | 50 soldats | — |
TRZ_depot_A_999_nil_0 |
Pickup ∞ | illimité | — |
TRZ_exfil_R_0_rescue_0 |
Extract | — | flag rescue, pas de seuil |
TRZ_lz_B_0_secure_100 |
Extract | — | flag secure, 100 soldats |
TRZ_fob_N_30_defend_50 |
Mixte | 30 soldats | flag defend, 50 soldats |
TRZ_marker_B_0_nil_0 |
Inerte | — | — |
TRZ _ fob _ N _ 30 _ defend _ 50
│ │ │ │ │ │
│ │ │ │ │ └─ target : 50 soldats pour l'objectif
│ │ │ │ └──────────── flag : "defend" (nom de flag DCS)
│ │ │ └─────────────────────── stock : 30 troops max (limité)
│ │ └────────────────────────────── coalition : NEUTRAL
│ └───────────────────────────────────── name : "fob"
└───────────────────────────────────────────── préfixe TRZ
Validation du parser¶
CTLDZoneManager:_parseTRZ(name) retourne une table parsée en cas de succès, ou nil plus une
chaîne d'erreur en cas d'échec :
| Violation | Message d'erreur |
|---|---|
| Préfixe ≠ TRZ | "not a TRZ" |
| Name manquant ou vide | "missing zoneName" |
| Name est un mot réservé | "zoneName cannot be a reserved word: <word>" |
| Coalition manquante | "missing coalition (A|R|B|N)" |
| Coalition ≠ A/R/B/N | "invalid coalition '<x>' — expected A, R, B or N" |
| Stock manquant / non entier / hors 0–999 | "invalid stock '<x>' — expected integer 0-999 …" |
| Flag manquant | "missing flag (DCS flag name or 'nil')" |
| Flag est un nombre | "flag must be a string or 'nil', not a number" |
| Target manquant / négatif / non entier | "invalid target '<x>' — expected integer ≥0 …" |
WPZ_ et LGZ_ utilisent la forme plus simple <PREFIX>_<name>_[R|B|N] (coalition
optionnelle, défaut 0) ; _parseWPZ partage _parseSimpleZone et _parseLGZ est autonome.
Algorithme de découverte¶
CTLDZoneManager:init() enregistre S_EVENT_DEAD sur le bridge d'événements DCS, puis exécute
les phases suivantes dans cet ordre :
_validateZoneNames()— parcourt chaque zone de trigger et la configaiZones, accumule les erreurs et les avertissements (voir plus bas), et les reporte une fois dans le log DCS et à l'écran._discoverTRZ()— pour chaque zone de trigger commençant parTRZ_, parse le nom avec_parseTRZ; en cas de succès, construit uneCTLDTroopZone(géométrie issue de la zone de trigger,smokeissu detroopZoneSmokeColor[coalition]) et remet à 0 toutobjectiveFlag._loadAIZonesFromConfig()— litctld.gs("aiZones"); chaque entrée valide devient uneCTLDTroopZonemarquéeisAIPickup/isAIDropoff, avec un stock par template/par type._discoverWPZ()— les zones de trigger commençant parWPZ_deviennent des waypoint troop zones (isWaypoint = true)._discoverLGZ()— les zones de trigger commençant parLGZ_deviennent des objetsCTLDLogisticZone(rayon issu dedynamicZoneRadius, défaut 200 m)._loadLegacyZones()— passe de rétrocompatibilité sur les tables de configtroopZones,wpZonesetlogisticUnits._scheduleSmoke()— démarre la boucle récurrente de rafraîchissement du smoke.
Enfin, init() publie un OnLogisticZoneUpdated initial et logge le nombre de zones. Chaque
phase de découverte se protège par if not self._troopZones[name] / _logisticZones[name],
donc l'ordre de priorité ci-dessus est ce qui fait que les définitions modernes l'emportent sur
les legacy.
Détails du fallback legacy¶
- Les entrées
troopZonesprennent en charge à la fois les zones de trigger DCS et les noms d'unités de navire — sitrigger.misc.getZoneéchoue, le loader se rabat surUnit.getByName, capturant la position du navire à l'init comme point de pickup mobile. Les troop zones legacy dérivent automatiquement unstockFlagNamede la forme<zoneName>_count, reflétantpickCurrentStockvers ce flag DCS. - Les entrées
logisticUnitssont résolues viaStaticObject.getByNameouUnit.getByNameet deviennent des logistic zones dynamiques liées à cet objet (rayon issu demaximumDistanceLogistic, défaut 500 m).
CTLDTroopZone¶
Construite à partir d'une table data. Requis : dcsName, zoneName, coalition, center
(vec3), radius. Optionnels : verticies, pickMaxStock, objectiveFlag, objectiveTarget,
stockFlagName, smoke (une valeur trigger.smokeColor.* ou -1), active, les flags de
rôle isWaypoint / isDropoff / isAIPickup / isAIDropoff, et les champs IA aiDropMode,
aiCargoType, troopTemplates, vehicleTypes, _aiTroopStock, _aiVehicleStock.
Prédicats de rôle¶
| Méthode | Vrai quand |
|---|---|
zone:hasPickup() |
pickMaxStock ~= nil |
zone:hasExtract() |
objectiveFlag ~= nil |
zone:hasWaypoint() |
isWaypoint == true |
zone:hasDropoff() |
isDropoff == true (auto-drop legacy IAZ) |
zone:hasAIPickup() |
isAIPickup == true (rôle P d'AIZ) |
zone:hasAIDropoff() |
isAIDropoff == true (rôle D d'AIZ) |
Géométrie¶
zone:isInZone(point) teste les zones polygonales (verticies avec ≥3 sommets) via un
ray-cast de Jordan sur le static privé CTLDTroopZone._raycast, sinon se rabat sur un test de
rayon circulaire. À noter que verticies[i].x/.y sont des coordonnées de fichier de mission où
le Y de mission égale le Z monde. zone:getCenter() retourne le center stocké.
Gestion du stock¶
zone:consumeStock(n) -- décrémente pickCurrentStock de n ; true en cas de succès, illimité toujours true
zone:restoreStock(n) -- restaure jusqu'à pickMaxStock ; no-op pour les zones illimitées ou sans pickup
Les deux appellent _syncStockFlag(), qui écrit pickCurrentStock dans stockFlagName via
trigger.action.setUserFlag lorsque ce flag est défini. Un stock illimité (pickMaxStock == 0)
signifie que consumeStock réussit toujours et que restoreStock est un no-op.
Stock IA par template / par type (Feature T)¶
Les zones AIZ peuvent porter un stock granulaire au lieu d'un simple compteur. _aiTroopStock
et _aiVehicleStock sont des tables de la forme
{ isAll = bool, init = {name=N}, current = {name=N} }, où N = -1 signifie illimité. La clé
de config spéciale "All" positionne isAll = true (chaque template/type, illimité).
zone:aiPickTroopTemplate(teams, typeName, unitName, tm) -- meilleur template éligible, ou nil
zone:aiConsumeTroopStock(templateName) -- décrémente une unité de troop
zone:aiRestoreTroopStock(templateName, n) -- restaure n (défaut 1), plafonné à init
zone:aiPickVehicleEntry() -- { type, isScene, isAASystem } ou nil
zone:aiConsumeVehicleStock(typeName) -- décrémente un véhicule
zone:aiRestoreVehicleStock(typeName) -- restaure un, plafonné à init
aiPickTroopTemplate filtre les templates qui sont en stock et qui rentrent dans la
capacité du transport (tm:_canEmbark), puis privilégie le stock courant le plus élevé,
choisissant au hasard en cas d'égalité. aiPickVehicleEntry résout le provider de chaque type
par ordre de priorité CTLDSceneManager (scene) > CTLDCrateAssemblyManager (AA system) > DCS
natif, retournant nil lorsque isAll est positionné (l'appelant utilise alors le chemin
legacy de scan physique).
Objectif¶
zone:incrementObjective(soldierCount) -- → incremented(bool), valueBefore, valueAfter
Ajoute soldierCount à objectiveFlag (mode : +N par soldat, pas par groupe ni par
opération) et logge une ligne INFO lorsque objectiveTarget est atteint. CTLD ne fait que
incrémenter le flag ; il remet objectiveFlag à 0 une seule fois à la découverte, et le mission
maker définit la condition de victoire dans l'éditeur DCS avec un trigger tel que
if flag >= target → victory.
zone:activate() / zone:deactivate() basculent le flag active.
CTLDLogisticZone¶
Construite à partir de data. Requis : name, coalition, center, radius (défaut 200).
Optionnels : linkedUnit (la zone suit alors cette unité/static DCS), active, et une table
services qui vaut par défaut { cratesPickup = true, cratesDropoff = true, vehicleSpawn = true }.
| Méthode | Comportement |
|---|---|
zone:getCenter() |
Point live de l'unité liée si elle existe encore, sinon le center stocké |
zone:isDynamic() |
Vrai si ancrée à une unité DCS mobile (_linkedUnit ~= nil) |
zone:isAlive() |
Vrai sauf si l'unité liée n'existe plus (toujours vrai pour une static) |
zone:isInZone(point) |
Test de rayon circulaire uniquement — les logistic zones sont toujours circulaires |
zone:activate() / zone:deactivate() |
Bascule active |
Scheduler de smoke¶
_scheduleSmoke() se réarme toutes les smokeRefreshInterval secondes (défaut 300). Lorsque
disableAllSmoke est positionné, il se re-planifie sans rien faire. Sinon, il fume chaque troop
zone active dont smoke >= 0 et chaque logistic zone active qui a une couleur dans
logisticZoneSmokeColor[coalition], puis publie OnZoneSmokeRefreshed avec un instantané des
deux ensembles de zones (positions, stock, progression d'objectif, couleurs).
Événements¶
Publiés via EventDispatcher (voir Architecture) :
| Événement | Publié quand |
|---|---|
OnZoneSmokeRefreshed |
Toutes les smokeRefreshInterval secondes par la boucle de smoke |
OnLogisticZoneUpdated |
À l'init, à la mort d'une unité dynamique, et lors d'un register / unregister / (dé)activation de FOB / logistique |
Le manager enregistre onDead pour S_EVENT_DEAD via CTLDDCSEventBridge : lorsque l'unité
liée d'une logistic zone dynamique meurt, la zone est retirée et un OnLogisticZoneUpdated est
publié avec le retrait.
API de requête — troop zones¶
zm:getTroopZone(zoneName) -- → CTLDTroopZone | nil
zm:getTroopZonesForCoalition(coalition) -- zones actives pour la coalition (0 = les deux)
zm:getTroopZoneAtPoint(point, coalition) -- zone contenante ; exclut les pickup zones réservées à l'IA
zm:getTroopZoneForUnit(unitName) -- zone sous la position d'une unité live
zm:getWaypointZoneAt(point, coalition) -- WPZ active contenant point
zm:getNearestWaypointZone(point, coalition) -- WPZ la plus proche (point n'a pas besoin d'être à l'intérieur)
zm:getDropoffZoneAt(point, coalition) -- zone d'auto-drop legacy IAZ active au point
zm:getAIPickupZoneAt(point, coalition) -- zone AIZ P active au point (le plus petit rayon l'emporte)
zm:getAIDropoffZoneAt(point, coalition) -- zone AIZ D active au point (le plus petit rayon l'emporte)
Le filtrage par coalition suit partout la même règle : une zone correspond quand
coalition == 0 (l'appelant accepte tout), ou zone.coalition == 0 (la zone sert tout le
monde), ou les deux camps sont égaux. getNearestWaypointZone sous-tend la Feature I
(gotoNearestWPZ), et les getters de pickup/dropoff IA sous-tendent la boucle de transport IA
(_checkAIStatus).
API de requête — logistic zones¶
zm:getLogisticZone(name) -- → CTLDLogisticZone | nil
zm:getLogisticZonesForCoalition(coalition) -- zones actives + vivantes pour la coalition
zm:getLogisticZoneAtPoint(point, coalition) -- première zone contenante
zm:getLogisticZoneForUnit(unitName) -- zone sous une unité / static
zm:getLogisticZonesAtPoint(point, coalition, key) -- TOUTES les zones contenantes ; filtre services optionnel
getLogisticZonesAtPoint retourne chaque zone correspondante (pas seulement la première). Le
serviceKey optionnel (par ex. "cratesPickup") ne conserve que les zones où
zone.services[key] n'est pas false.
Enregistrement à l'exécution et API rétrocompatible¶
zm:registerFOBAsLogistic(fobName, point, radius, coalitionId) -- ajoute un FOB construit comme LGZ
zm:unregisterLogistic(name) -- retire une LGZ dynamique (FOB détruit)
zm:deactivateLogisticZone(name) -- désactivation réversible (capture / perte)
zm:activateLogisticZone(name) -- réactive
zm:setTroopZoneActive(zoneName, active) -- active / désactive une TRZ
-- Rétrocompatible (appelé par les wrappers de l'API legacy) :
zm:isUnitInZone(unitName, zoneType) -- zoneType : "extract" | "pickup" | nil (toute zone active)
zm:createExtractZone(zoneName, flag, smoke) -- EXZ à l'exécution depuis une zone de trigger DCS
zm:removeExtractZone(zoneName, flag) -- flag accepté mais ignoré
zm:activateWaypointZone(zoneName) -- délègue à setTroopZoneActive(…, true)
zm:deactivateWaypointZone(zoneName) -- délègue à setTroopZoneActive(…, false)
zm:changeRemainingGroups(zoneName, amount) -- ajuste pickCurrentStock de ±amount (plancher à 0)
Les logistic zones désactivées sont ignorées par chaque getter jusqu'à réactivation, ce qui permet à une mission de simuler un point de ravitaillement capturé ou temporairement perdu sans le détruire.
Priorité de déchargement : extract avant RTB¶
Lorsqu'un joueur décharge des troops, la zone contenante est évaluée dans cet ordre (géré par le sous-système troop, pas par le zone manager lui-même) :
- TRZ avec
objectiveFlag→ extract (déploie les troops, incrémente le flag). - TRZ avec pickup seulement → RTB (les troops rentrent, stock restauré).
- Sinon → combat deploy (troops déployées, pas de flag, pas de changement de stock).
Cet ordre garantit qu'une zone mixte déclenche son objectif lorsqu'elle est utilisée comme point d'extract plutôt que de simplement renvoyer les troops au stock.
Comportement du menu F10¶
Le menu de commande des troops est reconstruit sur S_EVENT_LAND / S_EVENT_TAKEOFF à partir
de la position courante du joueur et de l'état de sa cargaison :
- En vol : le sous-menu « Troop Commands » est vide.
- Au sol dans une pickup TRZ : une option « Load from
<zoneName>» apparaît. - Au sol avec des troops à bord : une option « Unload / Extract » apparaît.
- Au sol hors de toute TRZ : aucune option de chargement.
Validation des noms de zone¶
_validateZoneNames() s'exécute en premier dans init() et produit un unique rapport groupé
(trigger.action.outText + log DCS + env.warning). Il couvre :
- TRZ / WPZ / LGZ : toute zone de trigger portant le préfixe qui échoue à son parser est reportée comme une erreur.
- Config AIZ (Features S/T) : des vérifications par entrée accumulent erreurs et
avertissements, indexées par
dcsZoneNameafin que seules les entrées invalides soient ignorées dans_loadAIZonesFromConfig.
| Règle | Sévérité | Condition |
|---|---|---|
| — | error | dcsZoneName manquant, dcsZoneName en double, zone absente de l'éditeur de mission, ou coalition manquante/invalide |
| G1 | error | ni isPickup ni isDropoff — la zone ne ferait rien |
| G5 | error | cargoType véhicule (V/TV) sur une pickup zone mais aucun aéronef n'a canTransportWholeVehicle |
| — | warn | cargoType invalide (défaut à T) ou aiDropMode invalide (défaut à GP) |
| G3 | warn | pickup + cargo troop mais troopStock indéfini — pickup de troop désactivé |
| G6 | warn | pickup + cargo véhicule mais vehicleStock indéfini — pickup de véhicule désactivé |
| G2 | warn | chaque entrée de troopTemplates est inconnue (des avertissements par nom sont aussi émis) |
| G4 | warn | chaque entrée de vehicleTypes est inconnue dans les listes de véhicules chargeables |
| — | warn | une pickup zone chevauche une dropoff zone de la même coalition — risque de boucle instantanée pickup/dropoff |
Lorsqu'il n'y a aucune anomalie, il logge CTLDZoneManager: zone config valid à la place.