From 650e6a29eae2f7dc7d1f535dafde03d9f1279846 Mon Sep 17 00:00:00 2001 From: David Pierron Date: Fri, 17 Jul 2026 14:21:56 +0200 Subject: [PATCH 01/17] fix(ci): correct luacheck globals for plugin scenes and test spies ctld is writable (plugin scenes populate ctld.i18n at load); the design-time gate spec spies on CTLDObjectRegistry; the vendored dcs_stubs.lua defines the DCS globals. Configure luacheck accordingly so CI passes. --- .luacheckrc | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 9aafa10..8bb1c79 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -11,10 +11,14 @@ self = false exclude_files = { "tools/**", - "vendor/**", -- vendored CTLD.lua build (not ours to lint) - "tests/data/**", -- generated datamine type set + "vendor/**", -- vendored CTLD.lua build (not ours to lint) + "tests/data/**", -- generated datamine type set + "tests/helpers/dcs_stubs.lua", -- vendored DCS API stubs (defines the DCS globals) } +-- `ctld` is writable: plugin scenes populate ctld.i18n[...] at load. +globals = { "ctld" } + read_globals = { -- DCS World API "env", "world", "coalition", "country", "timer", "trigger", @@ -23,7 +27,6 @@ read_globals = { "Weapon", "Runway", "Warehouse", "require", "dofile", "loadfile", "loadstring", "io", "os", -- CTLD runtime (provided by CTLD, consumed by plugins) - "ctld", "CTLDObjectRegistry", "CTLDSceneManager", "CtldScene", "CTLDCrateManager", "CTLDCrateAssemblyManager", "CTLDPlayerManager", "CTLDPlayer", @@ -33,7 +36,9 @@ read_globals = { } files["tests/"] = { + -- busted globals + the CTLD singletons a test may spy on (mutate). globals = { "describe", "it", "setup", "teardown", "before_each", "after_each", - "assert", "spy", "mock", "stub" }, + "assert", "spy", "mock", "stub", + "CTLDObjectRegistry", "CTLDSceneManager" }, } From 06012729dff564309c884d009754b502ddfc9299 Mon Sep 17 00:00:00 2001 From: David Pierron Date: Fri, 17 Jul 2026 14:27:39 +0200 Subject: [PATCH 02/17] feat(template): reference _template plugin scene exercising all extension points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plugins/_template is a heavily-commented reference scene to copy when authoring a plugin. It exercises the full extension surface: i18n (4 languages), ObjectRegistry declarations, a scene model with polar + func steps, a crate injected into Request Equipment, and an F10 radio submenu wired via deferMenuSection — plus requiresCtld and (empty) modTypes metadata. The radio submenu is the point Metal FARP alone did not cover: loaded after CTLD init, its deferMenuSection routes straight to registerMenuSection (CTLD's load-position-independent fix), so the submenu still attaches. A busted smoke test asserts registration, crate, metadata and the wired menu section; the asset gate now validates the template's types too. CI runs busted over tests/ and plugins/ so per-plugin tests are discovered. Catalogue: an authoring "Template" page (EN/FR). SCENE-PLUGINS ticket 09. --- .github/workflows/ci.yml | 2 +- docs/plugins/template.fr.md | 24 +++ docs/plugins/template.md | 21 +++ mkdocs.yml | 2 + plugins/_template/src/CTLD_templateScene.lua | 159 +++++++++++++++++++ plugins/_template/tests/template_spec.lua | 50 ++++++ tests/scene_asset_gate_spec.lua | 1 + 7 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 docs/plugins/template.fr.md create mode 100644 docs/plugins/template.md create mode 100644 plugins/_template/src/CTLD_templateScene.lua create mode 100644 plugins/_template/tests/template_spec.lua diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d569c0a..fe91fd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: sudo apt-get update && sudo apt-get install -y lua5.1 liblua5.1-dev luarocks sudo luarocks install busted - name: Run busted - run: busted tests/ + run: busted tests/ plugins/ # Build every plugin into dist/.lua and upload as artifacts. build: diff --git a/docs/plugins/template.fr.md b/docs/plugins/template.fr.md new file mode 100644 index 0000000..5302e99 --- /dev/null +++ b/docs/plugins/template.fr.md @@ -0,0 +1,24 @@ +# Template (pour les auteurs de plugins) + +`plugins/_template/` est une **scène de référence**, pas un plugin déployable. Copiez-la pour +démarrer un nouveau plugin — elle exerce tous les points d'extension d'une scène, abondamment +commentée : + +- **i18n** dans les quatre langues obligatoires (en / fr / es / ko) ; +- les déclarations **ObjectRegistry** de chaque type DCS spawné ; +- un **modèle de scène** avec des steps `polar` et `func` ; +- une **caisse** injectée dans le menu *Request Equipment* de CTLD ; +- un **sous-menu radio F10** câblé via `deferMenuSection` (fonctionne que la scène soit chargée + avant ou après l'init de CTLD — le contrat d'indépendance à la position de chargement) ; +- les métadonnées `requiresCtld` (version CTLD minimale) et `modTypes` (types hors-stock déclarés). + +## Checklist de création + +1. Copiez `plugins/_template/` vers `plugins//`, renommez le fichier et le `name` du + modèle. +2. Déclarez chaque type spawné dans le BLOCK 2. Si l'un est un type **mod**, ajoutez-le à + `model.modTypes` (et renseignez `requiresMod` pour le catalogue). Tous les autres types doivent + être stock. +3. `busted tests/ plugins/` — le gate d'assets échoue sur tout type inconnu/non déclaré. +4. `tools/build/merge_plugin.ps1 -Plugin ` → `dist/.lua`. +5. Ajoutez une page de catalogue sous `docs/plugins/`. diff --git a/docs/plugins/template.md b/docs/plugins/template.md new file mode 100644 index 0000000..da8e61c --- /dev/null +++ b/docs/plugins/template.md @@ -0,0 +1,21 @@ +# Template (for plugin authors) + +`plugins/_template/` is a **reference scene**, not a deployable plugin. Copy it to start a new +plugin — it exercises every extension point a scene can use, heavily commented: + +- **i18n** in the four mandatory languages (en / fr / es / ko); +- **ObjectRegistry** declarations for every spawned DCS type; +- a **scene model** with `polar` and `func` steps; +- a **crate** injected into the CTLD *Request Equipment* menu; +- an **F10 radio submenu** wired via `deferMenuSection` (works whether the scene is loaded before + or after CTLD init — the load-position-independent contract); +- `requiresCtld` (minimum CTLD version) and `modTypes` (declared non-stock types) metadata. + +## Authoring checklist + +1. Copy `plugins/_template/` to `plugins//`, rename the file and the model `name`. +2. Declare every spawned type in BLOCK 2. If any is a **mod** type, add it to `model.modTypes` + (and set `requiresMod` for the catalogue). All other types must be stock. +3. `busted tests/ plugins/` — the asset gate fails on any undeclared/unknown type. +4. `tools/build/merge_plugin.ps1 -Plugin ` → `dist/.lua`. +5. Add a catalogue page under `docs/plugins/`. diff --git a/mkdocs.yml b/mkdocs.yml index d314dfd..b50e1ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,3 +51,5 @@ nav: - Home: index.md - Plugins: - Metal FARP: plugins/metal-farp.md + - For authors: + - Template: plugins/template.md diff --git a/plugins/_template/src/CTLD_templateScene.lua b/plugins/_template/src/CTLD_templateScene.lua new file mode 100644 index 0000000..339aafa --- /dev/null +++ b/plugins/_template/src/CTLD_templateScene.lua @@ -0,0 +1,159 @@ +---@diagnostic disable +-- CTLD_templateScene.lua — REFERENCE plugin scene. +-- +-- Copy this file to start a new plugin. It exercises EVERY extension point a scene can use, so +-- whatever your scene needs, there is an example here. It is load-position-independent: the exact +-- same source works whether merged into CTLD.lua or loaded as a plugin after CTLD. +-- +-- A plugin scene is loaded from a MISSION START trigger, AFTER CTLD. Structure (by convention): +-- BLOCK 1 i18n — the 4 mandatory languages for every user-facing string +-- BLOCK 2 ObjectRegistry — declare every DCS type the scene spawns +-- BLOCK 3 scene model — metadata, crate, steps +-- BLOCK 4 menu section — optional F10 radio submenu +-- BLOCK 5 self-registration (always last) +-- +-- Dependencies (all provided by the CTLD runtime): ctld, CTLDObjectRegistry, CTLDSceneManager, +-- CTLDPlayerManager, ctld.MenuManager, ctld.tr, ctld.utils. +-- ==================================================================================================== + +-- ==================================================================================================== +-- BLOCK 1 : i18n — every user-facing string, in en / fr / es / ko. +-- ==================================================================================================== +ctld.i18n["en"]["Template Crate"] = "Template Crate" +ctld.i18n["fr"]["Template Crate"] = "Caisse Template" +ctld.i18n["es"]["Template Crate"] = "Caja Plantilla" +ctld.i18n["ko"]["Template Crate"] = "템플릿 화물" + +ctld.i18n["en"]["Deploy Template"] = "Deploy Template" +ctld.i18n["fr"]["Deploy Template"] = "Déployer le Template" +ctld.i18n["es"]["Deploy Template"] = "Desplegar Plantilla" +ctld.i18n["ko"]["Deploy Template"] = "템플릿 배치" + +ctld.i18n["en"]["Template"] = "Template" +ctld.i18n["fr"]["Template"] = "Template" +ctld.i18n["es"]["Template"] = "Plantilla" +ctld.i18n["ko"]["Template"] = "템플릿" + +ctld.i18n["en"]["Template: say hello"] = "Template: say hello" +ctld.i18n["fr"]["Template: say hello"] = "Template : dire bonjour" +ctld.i18n["es"]["Template: say hello"] = "Plantilla: saludar" +ctld.i18n["ko"]["Template: say hello"] = "템플릿: 인사하기" + +ctld.i18n["en"]["Hello from %1!"] = "Hello from %1!" +ctld.i18n["fr"]["Hello from %1!"] = "Bonjour de la part de %1 !" +ctld.i18n["es"]["Hello from %1!"] = "¡Hola de parte de %1!" +ctld.i18n["ko"]["Hello from %1!"] = "%1이(가) 인사합니다!" + +ctld.i18n["en"]["--- Template deployed by %1 ---"] = "--- Template deployed by %1 ---" +ctld.i18n["fr"]["--- Template deployed by %1 ---"] = "--- Template déployé par %1 ---" +ctld.i18n["es"]["--- Template deployed by %1 ---"] = "--- Plantilla desplegada por %1 ---" +ctld.i18n["ko"]["--- Template deployed by %1 ---"] = "--- %1이(가) 템플릿을 배치했습니다 ---" + +-- ==================================================================================================== +-- BLOCK 2 : ObjectRegistry — declare every DCS type the scene spawns. +-- registerIfAbsent is a no-op if the key already exists (CTLD pre-registers many common ones). +-- Use only STOCK types, OR declare mod types in model.modTypes (BLOCK 3) so the asset gate accepts +-- them while still validating every other type. +-- ==================================================================================================== +CTLDObjectRegistry.registerIfAbsent("Windsock", { + groupType = "STATIC", namePrefix = "Windsock", type = "Windsock", category = "Fortifications", +}) +CTLDObjectRegistry.registerIfAbsent("FARP_Tent", { + groupType = "STATIC", namePrefix = "Tent", type = "FARP Tent", category = "Fortifications", +}) + +-- ==================================================================================================== +-- BLOCK 3 : scene model — metadata + crate + steps. +-- ==================================================================================================== +local templateScene = {} +templateScene.name = "Template" + +-- Minimum CTLD version providing the plugin machinery; CTLD warns at load if it is older. +templateScene.requiresCtld = "2.0.0" + +-- Non-stock (mod) DCS types this scene spawns. Empty here (all stock). If your scene uses a mod +-- static/unit, list its exact type name(s): the asset gate then accepts them while still catching +-- typos in every stock type. Also set requiresMod = "" for the catalogue. +templateScene.modTypes = {} + +-- Crate: auto-injected into the CTLD "Request Equipment" menu (weight is the 1001.xx handle). +templateScene.crate = { + weight = 1001.50, + i18nKey = "Template Crate", + deployKey = "Deploy Template", + cratesRequired = 1, + side = nil, + showSets = false, +} + +templateScene.steps = { + -- polar step: deterministic position relative to the trigger unit snapshot. + { + registryKey = "Windsock", + polar = { distance = 15, angle = 0 }, + relativeHeadingInDegrees = 0, + relativeAltitudeInMeters = 0, + delayAfterPreviousStep = 0, + }, + { + registryKey = "FARP_Tent", + polar = { distance = 20, angle = 90 }, + relativeHeadingInDegrees = 0, + relativeAltitudeInMeters = 0, + delayAfterPreviousStep = 1, + }, + -- func step: no spawn, a post-spawn hook. ctx.scene / ctx.unit / ctx.spawnedObj are available. + { + delayAfterPreviousStep = 0, + func = function(ctx) + if trigger and trigger.action and trigger.action.outText then + trigger.action.outText( + ctld.tr("--- Template deployed by %1 ---", ctx.unit:getName()), 10) + end + end, + }, +} + +-- ==================================================================================================== +-- BLOCK 4 : F10 radio submenu (optional). +-- deferMenuSection works whether the scene loads before or after CTLD init (load-position- +-- independent). buildTemplateSection creates the container once per player; refreshTemplateSection +-- (re)fills it — called on menu build and on land. +-- ==================================================================================================== +function templateScene:refreshTemplateSection(playerObj) + local mm = ctld.MenuManager:getInstance() + local menu = mm:getMenuByGroupId(playerObj.groupId) + if not menu then return end + + local root = ctld.tr("CTLD") + local sub = ctld.tr("Template") + menu:clearBranch({ root, sub }) + + menu:addCommand({ root, sub }, ctld.tr("Template: say hello"), + function(arg) + local u = Unit.getByName(arg.unitName) + local who = (u and u:isExist()) and u:getName() or arg.unitName + trigger.action.outText(ctld.tr("Hello from %1!", who), 10) + end, + { unitName = playerObj.unitName }) +end + +function templateScene:buildTemplateSection(playerObj, menu) + local root = ctld.tr("CTLD") + local sub = ctld.tr("Template") + menu:addSubMenu({ root }, sub, { order = 80 }) + self:refreshTemplateSection(playerObj) +end + +-- ==================================================================================================== +-- BLOCK 5 : self-registration (always last). +-- ==================================================================================================== +CTLDSceneManager.getInstance():registerSceneModel(templateScene) + +CTLDPlayerManager.deferMenuSection({ + key = "template_section", + manager = templateScene, + method = "buildTemplateSection", + refreshMethod = "refreshTemplateSection", + order = 80, +}) diff --git a/plugins/_template/tests/template_spec.lua b/plugins/_template/tests/template_spec.lua new file mode 100644 index 0000000..1ac0bde --- /dev/null +++ b/plugins/_template/tests/template_spec.lua @@ -0,0 +1,50 @@ +---@diagnostic disable +-- plugins/_template/tests/template_spec.lua +-- Smoke test for the reference template scene: it registers its model, injects a crate, and wires a +-- radio submenu — proving the full extension surface (and CTLD's load-position-independent +-- deferMenuSection) works for a plugin loaded after CTLD. +-- ============================================================ + +local ROOT = debug.getinfo(1, "S").source:match("^@(.+)plugins[\\/]") or "" + +describe("template plugin scene", function() + + setup(function() + dofile(ROOT .. "plugins/_template/src/CTLD_templateScene.lua") + end) + + -- Sections live on the instance (_instance._menuSections) once CTLD is initialised, or in the + -- class-level pre-init queue (_deferredSections) if not. deferMenuSection routes to whichever + -- applies (the load-position-independent fix), so accept either. + local function menuSectionWired(key) + local inst = CTLDPlayerManager._instance + if inst and inst._menuSections then + for _, s in ipairs(inst._menuSections) do if s.key == key then return true end end + end + for _, s in ipairs(CTLDPlayerManager._deferredSections or {}) do + if s.key == key then return true end + end + return false + end + + it("registers the Template scene model", function() + assert.is_true(CTLDSceneManager.getInstance():isSceneEnabled("Template")) + end) + + it("declares a crate for the Request Equipment menu", function() + local model = CTLDSceneManager.getInstance():getModel("Template") + assert.is_not_nil(model.crate) + assert.is_not_nil(model.crate.weight) + end) + + it("declares requiresCtld and an (empty) modTypes list", function() + local model = CTLDSceneManager.getInstance():getModel("Template") + assert.equals("2.0.0", model.requiresCtld) + assert.equals(0, #model.modTypes) + end) + + it("wires the F10 radio submenu via deferMenuSection", function() + assert.is_true(menuSectionWired("template_section")) + end) + +end) diff --git a/tests/scene_asset_gate_spec.lua b/tests/scene_asset_gate_spec.lua index 5401e93..ddbd106 100644 --- a/tests/scene_asset_gate_spec.lua +++ b/tests/scene_asset_gate_spec.lua @@ -20,6 +20,7 @@ local ROOT = debug.getinfo(1, "S").source:match("^@(.+)tests[\\/][^\\/]+_spec%.l local PLUGIN_SCENES = { "plugins/metal-farp/src/CTLD_metalFarpScene.lua", + "plugins/_template/src/CTLD_templateScene.lua", } local function spawnedTypesOf(desc) From c707381d191fb1f15d6cf822afaa7af64cfc107e Mon Sep 17 00:00:00 2001 From: David Pierron Date: Fri, 17 Jul 2026 15:58:38 +0200 Subject: [PATCH 03/17] chore(vendor): refresh CTLD.lua baseline (Metal FARP removed from core) CTLD SCENE-PLUGINS ticket 05 removed Metal FARP from the core deliverable; refresh the vendored test runtime so the metal-farp plugin registers cleanly (no duplicate) and the gate runs against the real baseline. --- vendor/CTLD.lua | 330 ------------------------------------------------ 1 file changed, 330 deletions(-) diff --git a/vendor/CTLD.lua b/vendor/CTLD.lua index ce00718..611ede4 100644 --- a/vendor/CTLD.lua +++ b/vendor/CTLD.lua @@ -6090,21 +6090,6 @@ CTLDObjectRegistry._db = { rate = 100, }, - ["Farp_FG_Petit_Helipad"] = { -- specific mod - groupType = "STATIC", - namePrefix = "FARP_Helipad", - type = "Farp_FG_Petit_Helipad", - category = "Heliports", - shape_name = "Farp_FG_Petit_Helipad.edm", - heliport_frequency = "127.5", - heliport_callsign_id = 1, - heliport_modulation = 0, - -- DCS scripting API limitation: for custom mod heliports, getDesc().life == 0 whether the mod - -- is installed or not (identical to an invalid type). No reliable discriminant exists. - -- probeSkip suppresses the false NOT FOUND alarm; the mod cannot be validated at runtime. - probeSkip = true, - }, - -- ------------------------------------------------------------------ -- FORTIFICATIONS -- ------------------------------------------------------------------ @@ -23434,321 +23419,6 @@ CTLDSceneManager.getInstance():registerSceneModel(farpAlphaScene) -- End : scenes/CTLD_farpAlphaScene.lua -- ==================================================================================================== --- Start : scenes/CTLD_metalFarpScene.lua ----@diagnostic disable --- CTLD_metalFarpScene.lua --- Metal FARP deployment scene — compact forward arming/refueling point using the --- Farp_FG_Petit_Helipad mod (visible metallic helipad platform). --- --- Requires the Farp_FG_Petit_Helipad mod to be installed on all clients. --- probeSkip=true is set on the registry entry — the mod cannot be validated at runtime --- (DCS getDesc().life == 0 whether the mod is installed or not). --- --- Layout (all offsets from trigger unit position): --- Farp_FG_Petit_Helipad heliport — 58 m ahead of trigger unit --- Fuel truck — 35 m / 8° heading 90° (t+5 s) --- Repair truck — 35 m / 11° heading 90° (t+5 s) --- Tent — 35 m / 10° heading 90° (t+5.5 s) --- Ammo cargo — 75 m / 346° (t+10 s) --- M92 light panel — 75 m / 355° alt+4 m (t+15 s) --- Windsock — 73 m / 346° (t+15 s) --- Warehouse stocking — 10 000 L × 4 fuel types (t+20 s) --- --- Dependencies: CTLDObjectRegistry, CTLDSceneManager, CTLDUtils --- ==================================================================================================== - --- ==================================================================================================== --- BLOCK 1 : i18n -- 4 mandatory languages --- ==================================================================================================== - -ctld.i18n["en"]["Metal FARP Crate"] = "Metal FARP Crate" -ctld.i18n["fr"]["Metal FARP Crate"] = "Caisse FARP Métal" -ctld.i18n["es"]["Metal FARP Crate"] = "Caja FARP Metal" -ctld.i18n["ko"]["Metal FARP Crate"] = "메탈 FARP 화물" - -ctld.i18n["en"]["Deploy Metal FARP"] = "Deploy Metal FARP" -ctld.i18n["fr"]["Deploy Metal FARP"] = "Déployer FARP Métal" -ctld.i18n["es"]["Deploy Metal FARP"] = "Desplegar FARP Metal" -ctld.i18n["ko"]["Deploy Metal FARP"] = "메탈 FARP 배치" - -ctld.i18n["en"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Metal FARP Deployment by %1 : Complete! ---" -ctld.i18n["fr"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Déploiement FARP Métal par %1 : Terminé ! ---" -ctld.i18n["es"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Despliegue FARP Metal por %1 : ¡Completo! ---" -ctld.i18n["ko"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- %1에 의한 메탈 FARP 배치 완료! ---" - --- ==================================================================================================== --- BLOCK 2 : Registry entries required by this scene. --- registerIfAbsent() is a no-op when the key already exists. --- ==================================================================================================== - -CTLDObjectRegistry.registerIfAbsent("Farp_FG_Petit_Helipad", { - groupType = "STATIC", - namePrefix = "FARP_Helipad", - type = "Farp_FG_Petit_Helipad", - category = "Heliports", - shape_name = "Farp_FG_Petit_Helipad.edm", - heliport_frequency = "127.5", - heliport_callsign_id = 1, - heliport_modulation = 0, - -- DCS scripting API limitation: getDesc().life == 0 whether the mod is installed or not. - -- probeSkip suppresses the false NOT FOUND alarm from CTLDModValidator. - probeSkip = true, -}) - -CTLDObjectRegistry.registerIfAbsent("Fuel_Truck", { - groupType = "GROUND", - namePrefix = "Fuel_Truck_Grp", - task = "Ground Nothing", - category = Unit.Category.GROUND_UNIT, - units = { - { - namePrefix = "Fuel_Truck_Unit", - unitType = function(cid) - return cid == coalition.side.RED and "ATZ-10" or "M978 HEMTT Tanker" - end, - playerCanDrive = false, - dx = 0, dz = 0, dh = 0, - }, - }, -}) - -CTLDObjectRegistry.registerIfAbsent("repare_Truck", { - groupType = "GROUND", - namePrefix = "repare_Truck_Grp", - task = "Ground Nothing", - category = Unit.Category.GROUND_UNIT, - units = { - { - namePrefix = "repare_Truck_Unit", - unitType = function(cid) - return cid == coalition.side.RED and "Ural-375" or "M 818" - end, - playerCanDrive = false, - dx = 0, dz = 0, dh = 0, - }, - }, -}) - -CTLDObjectRegistry.registerIfAbsent("FARP_Tent", { - groupType = "STATIC", - namePrefix = "FARP_Tent", - type = "FARP Tent", - category = "Fortifications", -}) - -CTLDObjectRegistry.registerIfAbsent("ammo_cargo", { - groupType = "STATIC", - namePrefix = "ammo_box_cargo", - type = "ammo_cargo", - category = "Cargos", - shape_name = "ammo_box_cargo", - rate = 1, -}) - -CTLDObjectRegistry.registerIfAbsent("NF-2_LightOn", { - groupType = "STATIC", - namePrefix = "LightOn", - type = "NF-2_LightOn", - category = "Fortifications", - shape_name = "M92_NF-2_LightOn", - rate = 100, -}) - -CTLDObjectRegistry.registerIfAbsent("Windsock", { - groupType = "STATIC", - namePrefix = "Windsock", - type = "Windsock", - category = "Fortifications", - shape_name = "H-Windsock_RW", - rate = 3, -}) - --- "us carrier shooter" is already registered in the global CTLDObjectRegistry default entries. - --- ==================================================================================================== --- BLOCK 3 : scene model + crate descriptor --- ==================================================================================================== - -local metalFarpScene = {} -metalFarpScene.name = "Metal FARP" -metalFarpScene.requiresMod = "Farp_FG_Petit_Helipad" -- human-readable required-mod label (docs/catalogue) --- Non-stock (mod) DCS types this scene spawns. Added to the known set by the design-time --- asset hard-gate (datamine ∪ modTypes) so validation still catches typos in every stock type. -metalFarpScene.modTypes = { "Farp_FG_Petit_Helipad" } - -metalFarpScene.crate = { - weight = 1001.26, - i18nKey = "Metal FARP Crate", - deployKey = "Deploy Metal FARP", - groundKey = "You must be on the ground to deploy a FARP.", - cratesRequired = 1, - side = nil, - showSets = false, -} - -metalFarpScene.steps = { - - -- ---------------------------------------------------------------- - -- Step 1: Farp_FG_Petit_Helipad heliport (delay=0). - -- Spawned 50 m ahead of the trigger unit to avoid overlapping it. - -- Saves the spawned airbase name for the warehouse-stocking step. - -- critical=true: if the mod is absent the helipad cannot spawn; abort the whole scene - -- rather than deploying trucks and a tent with no landing pad. - -- ---------------------------------------------------------------- - { - polar = { distance = 58, angle = 0 }, - delayAfterPreviousStep = 0, - relativeHeadingInDegrees = 0, - relativeAltitudeInMeters = 0, - registryKey = "Farp_FG_Petit_Helipad", - critical = true, - func = function(ctx) - if not ctx.spawnedObj then return false end - ctx.scene._params.farpName = ctx.spawnedObj:getName() - return true - end, - }, - - -- ---------------------------------------------------------------- - -- Step 2: Fuel truck — right side under tent (t0 + 5 s). - -- ---------------------------------------------------------------- - { - polar = { distance = 60, angle = 342.5 }, - delayAfterPreviousStep = 5, - relativeHeadingInDegrees = 90, - relativeAltitudeInMeters = 0, - registryKey = "Fuel_Truck", - }, - - -- ---------------------------------------------------------------- - -- Step 3: Repair truck — left side under tent (t0 + 5 s). - -- ---------------------------------------------------------------- - { - polar = { distance = 61, angle = 340.5 }, - delayAfterPreviousStep = 0, - relativeHeadingInDegrees = 90, - relativeAltitudeInMeters = 0, - registryKey = "repare_Truck", - }, - - -- ---------------------------------------------------------------- - -- Step 4: Tent — over both trucks (t0 + 5.5 s). - -- ---------------------------------------------------------------- - { - polar = { distance = 61, angle = 341 }, - delayAfterPreviousStep = 0.5, - relativeHeadingInDegrees = 90, - relativeAltitudeInMeters = 0, - registryKey = "FARP_Tent", - }, - - -- ---------------------------------------------------------------- - -- Step 5: Ammo cargo (t0 + 10 s). - -- ---------------------------------------------------------------- - { - polar = { distance = 75, angle = 346 }, - delayAfterPreviousStep = 4.5, - relativeHeadingInDegrees = 0, - relativeAltitudeInMeters = 0, - registryKey = "ammo_cargo", - }, - - -- ---------------------------------------------------------------- - -- Step 6: M92 light panel at tent height (t0 + 15 s). - -- ---------------------------------------------------------------- - { - polar = { distance = 75, angle = 355 }, - delayAfterPreviousStep = 5, - relativeHeadingInDegrees = 310, - relativeAltitudeInMeters = 4, - registryKey = "NF-2_LightOn", - }, - - -- ---------------------------------------------------------------- - -- Step 7: Windsock near the light, same timing (t0 + 15 s). - -- ---------------------------------------------------------------- - { - polar = { distance = 73, angle = 346 }, - delayAfterPreviousStep = 0, - relativeHeadingInDegrees = 220, - relativeAltitudeInMeters = 0, - registryKey = "Windsock", - }, - - -- ---------------------------------------------------------------- - -- Step 8: Carrier Seaman on the helipad (t0 + 15 s). - -- ---------------------------------------------------------------- - { - polar = { distance = 67, angle = 2 }, - delayAfterPreviousStep = 0, - relativeHeadingInDegrees = 90, - relativeAltitudeInMeters = 0, - registryKey = "us carrier shooter", - }, - - -- ---------------------------------------------------------------- - -- Step 9: Stock warehouse + completion message (t0 + 20 s). - -- Fills all fuel types so aircraft can refuel/rearm at this point. - -- ---------------------------------------------------------------- - { - delayAfterPreviousStep = 5, - func = function(ctx) - local farpName = ctx.scene._params and ctx.scene._params.farpName - if farpName then - local ab = Airbase.getByName(farpName) - if ab then - local w = ab:getWarehouse() - -- If this is a redeployed FARP, restore the snapshot; otherwise stock defaults. - local snap = ctx.scene._params.repackData - and ctx.scene._params.repackData.warehouseSnapshot - if snap and snap.liquid then - for fuelType = 0, 3 do - w:setLiquidAmount(fuelType, snap.liquid[fuelType] or 0) - end - else - w:addLiquid(0, 10000) -- jet fuel - w:addLiquid(1, 10000) -- aviation gasoline - w:addLiquid(2, 10000) -- MW50 - w:addLiquid(3, 10000) -- diesel - end - end - end - trigger.action.outText( - ctld.tr("--- Metal FARP Deployment by %1 : Complete! ---", ctx.unit:getName()), 10) - return true - end, - }, -} - --- ==================================================================================================== --- BLOCK 4 : onRepack — called by CTLDSceneManager:packScene before objects are destroyed. --- Captures the current warehouse fuel levels so they can be restored on next deployment. --- ==================================================================================================== - -metalFarpScene.onRepack = function(scene, repackData) - local farpName = scene._params and scene._params.farpName - if not farpName then return end - local ab = Airbase.getByName(farpName) - if not ab then return end - local w = ab:getWarehouse() - repackData.warehouseSnapshot = { - liquid = { - [0] = w:getLiquidAmount(0), -- jet fuel - [1] = w:getLiquidAmount(1), -- aviation gasoline - [2] = w:getLiquidAmount(2), -- MW50 - [3] = w:getLiquidAmount(3), -- diesel - } - } -end - --- ==================================================================================================== --- BLOCK 5 : self-registration --- ==================================================================================================== - -CTLDSceneManager.getInstance():registerSceneModel(metalFarpScene) - --- End : scenes/CTLD_metalFarpScene.lua --- ==================================================================================================== -- Start : CTLD_core.lua -- ============================================================ -- CTLD_core.lua From f67368870b8a66099f0315ca60d899828de94ffb Mon Sep 17 00:00:00 2001 From: David Pierron Date: Fri, 17 Jul 2026 16:49:01 +0200 Subject: [PATCH 04/17] fix(gate): validate GROUND descriptor unit types (unitType per coalition) The asset gate read desc.units[].type and so silently skipped GROUND groups, whose unit types come from unitType(coalitionId). It now resolves both coalitions (mirrors CTLD's CTLDTypeCollector.typesOfDescriptor). metal-farp's Fuel_Truck / repare_Truck GROUND types are now validated (all in the datamine set). ASSET-VALIDATION-REVAMP ticket 01 (plugins side). --- tests/scene_asset_gate_spec.lua | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/scene_asset_gate_spec.lua b/tests/scene_asset_gate_spec.lua index ddbd106..0bb8d96 100644 --- a/tests/scene_asset_gate_spec.lua +++ b/tests/scene_asset_gate_spec.lua @@ -23,15 +23,33 @@ local PLUGIN_SCENES = { "plugins/_template/src/CTLD_templateScene.lua", } +-- STATIC → desc.type; GROUND → desc.units[i].unitType(coalitionId) (a per-coalition function), +-- with a static desc.units[i].type fallback. Kept self-contained so this gate stays copyable; +-- CTLD ships the same logic as CTLDTypeCollector.typesOfDescriptor. local function spawnedTypesOf(desc) - local out = {} + local out, seen = {}, {} if type(desc) ~= "table" then return out end - if desc.groupType == "GROUND" and type(desc.units) == "table" then + local function push(tn) + if type(tn) == "string" and tn ~= "" and not seen[tn] then + seen[tn] = true + out[#out + 1] = tn + end + end + if desc.groupType == "STATIC" then + push(desc.type) + elseif desc.groupType == "GROUND" and type(desc.units) == "table" then for _, u in ipairs(desc.units) do - if type(u) == "table" and u.type then out[#out + 1] = u.type end + if type(u) == "table" then + if type(u.unitType) == "function" then + for _, cid in ipairs({ 1, 2 }) do + local ok, tn = pcall(u.unitType, cid) + if ok then push(tn) end + end + else + push(u.type) + end + end end - elseif desc.type then - out[#out + 1] = desc.type end return out end From e2860632396c2936cdb1a61becc71487853c895f Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:36:12 +0200 Subject: [PATCH 05/17] docs(adr): add grilling context and architecture decision records Add CONTEXT.md glossary and two ADRs: - 0001: plugin README as single doc source of truth - 0002: mike-versioned docs (latest/dev on one site) Co-Authored-By: Claude Sonnet 4.6 --- CONTEXT.md | 49 +++++++++++++++++++ .../adr/0001-readme-as-doc-source-of-truth.md | 14 ++++++ docs/adr/0002-mike-versioned-docs.md | 14 ++++++ 3 files changed, 77 insertions(+) create mode 100644 CONTEXT.md create mode 100644 docs/adr/0001-readme-as-doc-source-of-truth.md create mode 100644 docs/adr/0002-mike-versioned-docs.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..a0c5cb1 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,49 @@ +# CONTEXT.md — CTLD_plugins + +Glossaire et décisions de domaine. Ne contient pas de détails d'implémentation. + +--- + +## Glossaire + +**Plugin** +Un fichier `.lua` unique qui s'auto-enregistre dans CTLD au chargement. Un plugin = une scène. Les plugins vivent dans `plugins/{nom}/src/`. + +**Scène** +Ce qu'un plugin construit en jeu (ex : un FARP métallique). Terme runtime DCS/CTLD. + +**Mission maker** +Utilisateur final du plugin : il télécharge le `.lua`, le charge dans l'éditeur de mission DCS, et installe les mods requis sur sa machine. + +**Mod DCS** +Extension tierce pour DCS World fournissant des types d'objets non-stock. Un plugin qui utilise un mod le déclare dans `modTypes` (runtime) et dans `modUrls` (doc). + +**modUrls** +Table de paires `{mod, url}` dans le front-matter YAML du `README.md` d'un plugin. Source de vérité documentaire pour les liens vers les mods requis. Distinct de `modTypes` (runtime Lua uniquement). + +**README plugin** +Fichier `plugins/{nom}/README.md`. Source de vérité unique pour la documentation d'un plugin. Contient un front-matter YAML (métadonnées structurées dont `modUrls`) et une prose en français. Suit le template `plugins/_template/README.md`. + +**Doc générée** +Les fichiers `docs/plugins/{nom}.md` (EN) et `docs/plugins/{nom}.fr.md` (FR) sont générés par un skill Claude interactif à partir du README plugin. Ils ne sont jamais édités à la main. + +**Skill generate-plugin-doc** +Skill Claude (à créer) qui lit `plugins/{nom}/README.md` + le template de doc, assemble et traduit FR→EN, et produit les deux fichiers `docs/plugins/`. + +**Catalogue** +Page d'index (`docs/index.md` / `docs/index.fr.md`) listant tous les plugins disponibles avec leur bouton de téléchargement. + +**Bouton download** +Lien direct vers le fichier `.lua` brut sur GitHub : +`https://raw.githubusercontent.com/VEAF/CTLD_plugins/{branch}/plugins/{nom}/src/{fichier}.lua` +où `{branch}` est `master` pour la version stable, `develop` pour la version dev. +Présent dans le tableau du catalogue ET dans la page de description du plugin. + +**{branch}** +Variable injectée à la génération des docs. Vaut `master` (site `latest`) ou `develop` (site `dev`). Détermine vers quelle version du fichier `.lua` pointent les boutons download. + +**Site docs versionné** +Site MkDocs Material géré par **mike**. Deux versions coexistent sur GitHub Pages : +- `latest` → branch `master` (stable) +- `dev` → branch `develop` (recette) +Chaque version a ses propres liens download cohérents avec sa branche. diff --git a/docs/adr/0001-readme-as-doc-source-of-truth.md b/docs/adr/0001-readme-as-doc-source-of-truth.md new file mode 100644 index 0000000..60257aa --- /dev/null +++ b/docs/adr/0001-readme-as-doc-source-of-truth.md @@ -0,0 +1,14 @@ +# README plugin comme source de vérité documentaire + +Chaque plugin possède un `plugins/{nom}/README.md` (front-matter YAML + prose FR) qui est l'unique source de vérité pour sa documentation. Les fichiers `docs/plugins/{nom}.md` et `{nom}.fr.md` sont **générés** par un skill Claude interactif — jamais édités à la main. + +## Alternatives considérées + +- **`meta.yaml` séparé + prose dans `docs/`** : deux fichiers à maintenir en sync, risque de désynchronisation. +- **Métadonnées dans le `.lua`** : le `.lua` tourne dans DCS — y coller des URLs GitHub pollue le code runtime avec de la pure métadonnée documentaire sans valeur à l'exécution. + +## Conséquences + +- Les `docs/plugins/` doivent être régénérés après chaque modification du `README.md` d'un plugin. +- Le `plugins/_template/README.md` fait loi comme template de référence pour les auteurs. +- La traduction FR→EN est produite par Claude à la demande du concepteur (interactif), pas par la CI. diff --git a/docs/adr/0002-mike-versioned-docs.md b/docs/adr/0002-mike-versioned-docs.md new file mode 100644 index 0000000..ad25c92 --- /dev/null +++ b/docs/adr/0002-mike-versioned-docs.md @@ -0,0 +1,14 @@ +# Versioning du site docs avec mike (latest / dev) + +Le site MkDocs Material est géré par **mike**, avec deux versions coexistant sur GitHub Pages : `latest` (branch `master`) et `dev` (branch `develop`). Chaque version injecte sa propre valeur de `{branch}` dans les boutons download, garantissant qu'un testeur sur `dev` télécharge bien le `.lua` de `develop` et non de `master`. + +## Alternatives considérées + +- **Un seul site (master uniquement)** : impossible de tester la doc et les liens download en recette sans bricolage — le testeur aurait cliqué "télécharger" et obtenu la version stable, pas celle en cours de test. +- **Deux sites séparés (`/` et `/dev/`)** : fonctionne mais perd le sélecteur de version intégré, déjà validé dans l'orga VEAF (`veaf.github.io/documentation/`). + +## Conséquences + +- Le workflow `docs.yml` appelle `mike deploy dev` sur push `develop` et `mike deploy latest` sur push `master`. +- `{branch}` est une variable injectée à la génération des docs (skill Claude) — valeur `develop` ou `master`. +- Le `versions.json` géré par mike doit rester en `gh-pages`, ne pas être commité dans `develop`/`master`. From 9de0580ab2e14b1790786870fc07f5693fd597d5 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:36:16 +0200 Subject: [PATCH 06/17] feat(catalogue): add download buttons in catalogue and plugin template pages Closes #5 Co-Authored-By: Claude Sonnet 4.6 --- docs/index.fr.md | 6 +++--- docs/index.md | 6 +++--- docs/plugins/template.fr.md | 8 +++++++- docs/plugins/template.md | 7 ++++++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/index.fr.md b/docs/index.fr.md index 89de7a7..6e4ba3a 100644 --- a/docs/index.fr.md +++ b/docs/index.fr.md @@ -16,6 +16,6 @@ mission ne les embarque que si elle le décide. ## Plugins disponibles -| Plugin | Ce qu'il construit | Requiert | -|--------|--------------------|----------| -| [Metal FARP](plugins/metal-farp.md) | Un FARP à hélisurface métallique | Mod DCS `Farp_FG_Petit_Helipad` | +| Plugin | Ce qu'il construit | Requiert | Télécharger | +|--------|--------------------|----------|-------------| +| [Metal FARP](plugins/metal-farp.md) | Un FARP à hélisurface métallique | Mod DCS `Farp_FG_Petit_Helipad` | [⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button } | diff --git a/docs/index.md b/docs/index.md index 40559a1..39b40dc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,6 @@ only pays for them if it opts in. ## Available plugins -| Plugin | What it builds | Requires | -|--------|----------------|----------| -| [Metal FARP](plugins/metal-farp.md) | A metal-helipad FARP | DCS mod `Farp_FG_Petit_Helipad` | +| Plugin | What it builds | Requires | Download | +|--------|----------------|----------|----------| +| [Metal FARP](plugins/metal-farp.md) | A metal-helipad FARP | DCS mod `Farp_FG_Petit_Helipad` | [⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button } | diff --git a/docs/plugins/template.fr.md b/docs/plugins/template.fr.md index 5302e99..cc93900 100644 --- a/docs/plugins/template.fr.md +++ b/docs/plugins/template.fr.md @@ -21,4 +21,10 @@ commentée : être stock. 3. `busted tests/ plugins/` — le gate d'assets échoue sur tout type inconnu/non déclaré. 4. `tools/build/merge_plugin.ps1 -Plugin ` → `dist/.lua`. -5. Ajoutez une page de catalogue sous `docs/plugins/`. +5. Copiez `plugins/_template/README.md` vers `plugins//README.md`. Renseignez le + front-matter (`modUrls` pour chaque mod requis) et rédigez la prose de description en **français**. + La section `modUrls` peut être supprimée entièrement si le plugin n'utilise aucun mod. +6. Demandez à Claude d'exécuter le skill `generate-plugin-doc` pour générer + `docs/plugins/.md` et `docs/plugins/.fr.md` depuis votre README. + Committez les fichiers générés. +7. Ajoutez le plugin dans le tableau du catalogue dans `docs/index.md` et `docs/index.fr.md`. diff --git a/docs/plugins/template.md b/docs/plugins/template.md index da8e61c..fe7198a 100644 --- a/docs/plugins/template.md +++ b/docs/plugins/template.md @@ -18,4 +18,9 @@ plugin — it exercises every extension point a scene can use, heavily commented (and set `requiresMod` for the catalogue). All other types must be stock. 3. `busted tests/ plugins/` — the asset gate fails on any undeclared/unknown type. 4. `tools/build/merge_plugin.ps1 -Plugin ` → `dist/.lua`. -5. Add a catalogue page under `docs/plugins/`. +5. Copy `plugins/_template/README.md` to `plugins//README.md`. Fill in the + front-matter (`modUrls` for each required mod) and write the description prose in **French**. + The `modUrls` section can be removed entirely if the plugin uses no mods. +6. Ask Claude to run the `generate-plugin-doc` skill to generate `docs/plugins/.md` + and `docs/plugins/.fr.md` from your README. Commit the generated files. +7. Add the plugin to the catalogue table in `docs/index.md` and `docs/index.fr.md`. From 494db4111fe3e4d7b04cc9fa3e96240c45d9bda9 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:36:24 +0200 Subject: [PATCH 07/17] feat(template): add plugins/_template/README.md for plugin authors Provides front-matter structure (modUrls) and French prose sections as a model for new plugin READMEs. Closes #6 Co-Authored-By: Claude Sonnet 4.6 --- plugins/_template/README.md | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 plugins/_template/README.md diff --git a/plugins/_template/README.md b/plugins/_template/README.md new file mode 100644 index 0000000..4af506d --- /dev/null +++ b/plugins/_template/README.md @@ -0,0 +1,42 @@ +--- +# Métadonnées documentaires du plugin. +# Ce fichier est la source de vérité pour la génération des pages docs/plugins/. +# Les fichiers docs/plugins/{nom}.md et {nom}.fr.md sont générés à partir d'ici +# via le skill generate-plugin-doc — ne pas les éditer à la main. + +# Mods DCS requis pour ce plugin. +# Supprimer la section modUrls entière si le plugin n'utilise aucun mod. +# Chaque entrée : nom du type DCS tel que déclaré dans modTypes du .lua, + URL de téléchargement. +modUrls: + - mod: NomDuTypeDCS + url: https://github.com/auteur/repo/tree/main/dossier-du-mod +--- + +# Nom du plugin (titre affiché sur la page de description) + + + +Courte description de ce que construit le plugin (1-2 phrases). Par exemple : "Construit un FARP +autour d'une hélisurface métallique, avec le mobilier FARP habituel." + +## Prérequis + +- **CTLD** ≥ X.Y.Z chargé en premier. +- Le mod DCS **`NomDuTypeDCS`** installé sur **tous** les clients. + Sans lui, [décrire l'effet visible : l'objet X ne peut pas apparaître]. + + + +## Installation + +1. Téléchargez le fichier `.lua` du plugin (bouton ci-dessus). +2. Dans l'éditeur de mission, ajoutez un déclencheur `DO SCRIPT FILE` au **démarrage de la + mission**, **après** le déclencheur qui charge `CTLD.lua`. +3. [Décrire comment l'utilisateur active la scène — ex : "La scène ajoute une caisse dans le menu + *Request Equipment* de CTLD ; déployez-la comme n'importe quelle caisse de scène FARP."] + +## Remarques + + + +[Avertissements, limitations connues, comportements non évidents pour le mission maker.] From 7dcf8e88c88e7cd0085dbaacfced9ed0435f7e34 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:36:30 +0200 Subject: [PATCH 08/17] feat(metal-farp): add README, align mod name, regenerate doc pages - plugins/metal-farp/README.md: authored prose + modUrls front-matter - CTLD_metalFarpScene.lua: rename Farp_FG_Petit_Helipad -> FG_small_Helipad - docs/plugins/metal-farp.{md,fr.md}: regenerated via generate-plugin-doc skill - .claude/skills/generate-plugin-doc: skill definition for doc generation Closes #7 Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/generate-plugin-doc/SKILL.md | 47 ++++++++ docs/plugins/metal-farp.fr.md | 29 +++-- docs/plugins/metal-farp.md | 33 +++--- plugins/metal-farp/README.md | 33 ++++++ .../metal-farp/src/CTLD_metalFarpScene.lua | 110 +++++++++--------- 5 files changed, 168 insertions(+), 84 deletions(-) create mode 100644 .claude/skills/generate-plugin-doc/SKILL.md create mode 100644 plugins/metal-farp/README.md diff --git a/.claude/skills/generate-plugin-doc/SKILL.md b/.claude/skills/generate-plugin-doc/SKILL.md new file mode 100644 index 0000000..e15a33c --- /dev/null +++ b/.claude/skills/generate-plugin-doc/SKILL.md @@ -0,0 +1,47 @@ +--- +name: generate-plugin-doc +description: Generates docs/plugins/{name}.md (EN) and docs/plugins/{name}.fr.md (FR) for a CTLD plugin, from its plugins/{name}/README.md (optional) and metadata extracted from its .lua source. Use when a plugin author wants to publish or refresh the documentation pages for their plugin. +--- + + + +Generate the documentation pages for a CTLD plugin. + +## Inputs to gather + +1. **Plugin name** — infer from context or ask. +2. **`plugins/{name}/README.md`** — read if it exists (front-matter YAML + French prose). Optional. +3. **`plugins/{name}/src/*.lua`** — extract `requiresCtld`, `modTypes`, `requiresMod`, and the scene `name`. +4. **`plugins/_template/README.md`** — reference for front-matter structure. + +## What to produce + +Two files, both beginning with the `do not edit` comment: + +``` + +``` + +### `docs/plugins/{name}.fr.md` + +- Title: `# {scene name}` (from `.lua`) +- Download button (primary): `[⬇ {name}.lua]({raw_url}){ .md-button .md-button--primary }` + where `raw_url = https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/{name}/src/{lua_file}` +- Sections: Description, Prérequis, Installation, Remarques (omit if empty) +- Content: the French prose from `README.md` body (if present), otherwise minimal generated text +- Mod links: for each entry in `modUrls`, add a link `[⬇ {mod}]({url}){ .md-button }` in the Prérequis section + +### `docs/plugins/{name}.md` + +- Same structure as the FR file, fully translated to English +- Translate all prose, section headings, and UI labels +- Keep code literals, type names, version numbers, and URLs untouched + +## Rules + +- Never edit `docs/plugins/` files by hand after generation — they are owned by this skill. +- If `README.md` is absent: generate minimal pages from `.lua` metadata only (no prose, no modUrls). +- `{branch}` in download URLs is always `master` for now (mike versioning handles dev separately). +- After writing both files, remind the author to add the plugin to `docs/index.md` and `docs/index.fr.md` if not already present. + + diff --git a/docs/plugins/metal-farp.fr.md b/docs/plugins/metal-farp.fr.md index 68d1e6a..d1250af 100644 --- a/docs/plugins/metal-farp.fr.md +++ b/docs/plugins/metal-farp.fr.md @@ -1,25 +1,24 @@ + + # Metal FARP -Construit un FARP autour d'une hélisurface métallique du mod DCS **`Farp_FG_Petit_Helipad`**, avec -le mobilier FARP habituel (camion carburant, camion de réparation, tente, munitions, éclairage, -manche à air). +[⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button .md-button--primary } + +Ce plugin permet via CTLD de faire apparaître un héliport métallique de façon progressive pour +simuler sa construction, en y ajoutant des objets pour obtenir un héliport "décoré" plus réaliste. ## Prérequis -- **CTLD** ≥ 2.0.0 chargé en premier (le plugin prévient en jeu si CTLD est plus ancien). -- Le mod DCS fournissant le type statique **`Farp_FG_Petit_Helipad`**, installé sur **tous** les - clients. Sans lui, l'hélisurface ne peut pas apparaître. +- **CTLD** ≥ 2.0.0 chargé en premier. +- Le mod DCS **`FG_small_Helipad`** installé sur **tous** les clients. + Sans lui, l'héliport ne peut pas apparaître. + + [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button } ## Installation -1. Téléchargez `metal-farp.lua`. +1. Téléchargez le fichier `.lua` du plugin (bouton ci-dessus). 2. Dans l'éditeur de mission, ajoutez un déclencheur `DO SCRIPT FILE` au **démarrage de la mission**, **après** le déclencheur qui charge `CTLD.lua`. -3. La scène ajoute une caisse dans le menu *Request Equipment* de CTLD ; déployez-la comme n'importe - quelle caisse de scène FARP. - -## Remarques - -La validation au design-time ne peut pas vérifier que le client a réellement le mod installé — -seulement que le nom de type est un mod déclaré. S'assurer que le mod est présent sur tous les -clients relève de la responsabilité du créateur de mission. +3. La scène ajoute une caisse dans le menu *Request Equipment* de CTLD ; déployez-la comme + n'importe quelle caisse de scène FARP. diff --git a/docs/plugins/metal-farp.md b/docs/plugins/metal-farp.md index 6ee96bc..ccf4e37 100644 --- a/docs/plugins/metal-farp.md +++ b/docs/plugins/metal-farp.md @@ -1,24 +1,25 @@ + + # Metal FARP -Builds a FARP around a metal helipad from the DCS mod **`Farp_FG_Petit_Helipad`**, plus the usual -FARP furniture (fuel truck, repair truck, tent, ammo, lighting, windsock). +[⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button .md-button--primary } + +This plugin uses CTLD to progressively deploy a metal helipad, simulating its construction, +and surrounds it with equipment objects to create a more realistic "dressed" forward arming +and refuelling point. ## Prerequisites -- **CTLD** ≥ 2.0.0 loaded first (the plugin warns in-game if CTLD is older). -- The DCS mod providing the static type **`Farp_FG_Petit_Helipad`**, installed on **every** client. - Without it the FARP helipad will not spawn. +- **CTLD** ≥ 2.0.0 loaded first. +- The DCS mod **`FG_small_Helipad`** installed on **all** clients. + Without it, the helipad cannot spawn. -## Install + [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button } -1. Download `metal-farp.lua`. -2. In the Mission Editor, add a `DO SCRIPT FILE` trigger at **MISSION START**, **after** the trigger - that loads `CTLD.lua`. -3. The scene registers a crate in the CTLD *Request Equipment* menu; deploy it like any other FARP - scene crate. - -## Notes +## Install -Design-time validation cannot check whether a client actually has the mod installed — only that the -type name is a declared mod. Ensuring the mod is present on all clients is the mission maker's -responsibility. +1. Download the plugin `.lua` file (button above). +2. In the Mission Editor, add a `DO SCRIPT FILE` trigger at **MISSION START**, **after** the + trigger that loads `CTLD.lua`. +3. The scene registers a crate in the CTLD *Request Equipment* menu; deploy it like any other + FARP scene crate. diff --git a/plugins/metal-farp/README.md b/plugins/metal-farp/README.md new file mode 100644 index 0000000..878cf63 --- /dev/null +++ b/plugins/metal-farp/README.md @@ -0,0 +1,33 @@ +--- +# Métadonnées documentaires du plugin. +# Ce fichier est la source de vérité pour la génération des pages docs/plugins/. +# Les fichiers docs/plugins/{nom}.md et {nom}.fr.md sont générés à partir d'ici +# via le skill generate-plugin-doc — ne pas les éditer à la main. + +# Mods DCS requis pour ce plugin. +# Supprimer la section modUrls entière si le plugin n'utilise aucun mod. +# Chaque entrée : nom du type DCS tel que déclaré dans modTypes du .lua, + URL de téléchargement. +modUrls: + - mod: FG_small_Helipad + url: https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad +--- + +# Metal FARP + +Ce plugin permet via CTLD de faire apparaitre un héliport métallique de façon progressive pour simuler sa construction, en y ajoutant des objets pour obtenir un héliport "décoré" plus réaliste. + +## Prérequis + +- **CTLD** ≥ 2.0.0 chargé en premier. +- Le mod DCS **`FG_small_Helipad`** installé sur **tous** les clients. + Sans lui, l'héliport ne peut pas apparaître. + +## Installation + +1. Téléchargez le fichier `.lua` du plugin (bouton ci-dessus). +2. Dans l'éditeur de mission, ajoutez un déclencheur `DO SCRIPT FILE` au **démarrage de la + mission**, **après** le déclencheur qui charge `CTLD.lua`. +3. La scène ajoute une caisse dans le menu + *Request Equipment* de CTLD ; déployez-la comme n'importe quelle caisse de scène FARP. + +## Remarques diff --git a/plugins/metal-farp/src/CTLD_metalFarpScene.lua b/plugins/metal-farp/src/CTLD_metalFarpScene.lua index 3215cab..84eba1e 100644 --- a/plugins/metal-farp/src/CTLD_metalFarpScene.lua +++ b/plugins/metal-farp/src/CTLD_metalFarpScene.lua @@ -1,14 +1,14 @@ ---@diagnostic disable -- CTLD_metalFarpScene.lua -- Metal FARP deployment scene — compact forward arming/refueling point using the --- Farp_FG_Petit_Helipad mod (visible metallic helipad platform). +-- FG_small_Helipad mod (visible metallic helipad platform). -- --- Requires the Farp_FG_Petit_Helipad mod to be installed on all clients. +-- Requires the FG_small_Helipad mod to be installed on all clients. -- probeSkip=true is set on the registry entry — the mod cannot be validated at runtime -- (DCS getDesc().life == 0 whether the mod is installed or not). -- -- Layout (all offsets from trigger unit position): --- Farp_FG_Petit_Helipad heliport — 58 m ahead of trigger unit +-- FG_small_Helipad heliport — 58 m ahead of trigger unit -- Fuel truck — 35 m / 8° heading 90° (t+5 s) -- Repair truck — 35 m / 11° heading 90° (t+5 s) -- Tent — 35 m / 10° heading 90° (t+5.5 s) @@ -24,32 +24,32 @@ -- BLOCK 1 : i18n -- 4 mandatory languages -- ==================================================================================================== -ctld.i18n["en"]["Metal FARP Crate"] = "Metal FARP Crate" -ctld.i18n["fr"]["Metal FARP Crate"] = "Caisse FARP Métal" -ctld.i18n["es"]["Metal FARP Crate"] = "Caja FARP Metal" -ctld.i18n["ko"]["Metal FARP Crate"] = "메탈 FARP 화물" +ctld.i18n["en"]["Metal FARP Crate"] = "Metal FARP Crate" +ctld.i18n["fr"]["Metal FARP Crate"] = "Caisse FARP Métal" +ctld.i18n["es"]["Metal FARP Crate"] = "Caja FARP Metal" +ctld.i18n["ko"]["Metal FARP Crate"] = "메탈 FARP 화물" -ctld.i18n["en"]["Deploy Metal FARP"] = "Deploy Metal FARP" -ctld.i18n["fr"]["Deploy Metal FARP"] = "Déployer FARP Métal" -ctld.i18n["es"]["Deploy Metal FARP"] = "Desplegar FARP Metal" -ctld.i18n["ko"]["Deploy Metal FARP"] = "메탈 FARP 배치" +ctld.i18n["en"]["Deploy Metal FARP"] = "Deploy Metal FARP" +ctld.i18n["fr"]["Deploy Metal FARP"] = "Déployer FARP Métal" +ctld.i18n["es"]["Deploy Metal FARP"] = "Desplegar FARP Metal" +ctld.i18n["ko"]["Deploy Metal FARP"] = "메탈 FARP 배치" -ctld.i18n["en"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Metal FARP Deployment by %1 : Complete! ---" -ctld.i18n["fr"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Déploiement FARP Métal par %1 : Terminé ! ---" -ctld.i18n["es"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Despliegue FARP Metal por %1 : ¡Completo! ---" -ctld.i18n["ko"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- %1에 의한 메탈 FARP 배치 완료! ---" +ctld.i18n["en"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Metal FARP Deployment by %1 : Complete! ---" +ctld.i18n["fr"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Déploiement FARP Métal par %1 : Terminé ! ---" +ctld.i18n["es"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- Despliegue FARP Metal por %1 : ¡Completo! ---" +ctld.i18n["ko"]["--- Metal FARP Deployment by %1 : Complete! ---"] = "--- %1에 의한 메탈 FARP 배치 완료! ---" -- ==================================================================================================== -- BLOCK 2 : Registry entries required by this scene. -- registerIfAbsent() is a no-op when the key already exists. -- ==================================================================================================== -CTLDObjectRegistry.registerIfAbsent("Farp_FG_Petit_Helipad", { +CTLDObjectRegistry.registerIfAbsent("FG_small_Helipad", { groupType = "STATIC", namePrefix = "FARP_Helipad", - type = "Farp_FG_Petit_Helipad", + type = "FG_small_Helipad", category = "Heliports", - shape_name = "Farp_FG_Petit_Helipad.edm", + shape_name = "FG_small_Helipad.edm", heliport_frequency = "127.5", heliport_callsign_id = 1, heliport_modulation = 0, @@ -63,14 +63,16 @@ CTLDObjectRegistry.registerIfAbsent("Fuel_Truck", { namePrefix = "Fuel_Truck_Grp", task = "Ground Nothing", category = Unit.Category.GROUND_UNIT, - units = { + units = { { - namePrefix = "Fuel_Truck_Unit", - unitType = function(cid) + namePrefix = "Fuel_Truck_Unit", + unitType = function(cid) return cid == coalition.side.RED and "ATZ-10" or "M978 HEMTT Tanker" end, playerCanDrive = false, - dx = 0, dz = 0, dh = 0, + dx = 0, + dz = 0, + dh = 0, }, }, }) @@ -80,14 +82,16 @@ CTLDObjectRegistry.registerIfAbsent("repare_Truck", { namePrefix = "repare_Truck_Grp", task = "Ground Nothing", category = Unit.Category.GROUND_UNIT, - units = { + units = { { - namePrefix = "repare_Truck_Unit", - unitType = function(cid) + namePrefix = "repare_Truck_Unit", + unitType = function(cid) return cid == coalition.side.RED and "Ural-375" or "M 818" end, playerCanDrive = false, - dx = 0, dz = 0, dh = 0, + dx = 0, + dz = 0, + dh = 0, }, }, }) @@ -132,17 +136,17 @@ CTLDObjectRegistry.registerIfAbsent("Windsock", { -- BLOCK 3 : scene model + crate descriptor -- ==================================================================================================== -local metalFarpScene = {} -metalFarpScene.name = "Metal FARP" -metalFarpScene.requiresMod = "Farp_FG_Petit_Helipad" -- human-readable required-mod label (docs/catalogue) +local metalFarpScene = {} +metalFarpScene.name = "Metal FARP" +metalFarpScene.requiresMod = "FG_small_Helipad" -- human-readable required-mod label (docs/catalogue) -- Non-stock (mod) DCS types this scene spawns. Added to the known set by the design-time -- asset hard-gate (datamine ∪ modTypes) so validation still catches typos in every stock type. -metalFarpScene.modTypes = { "Farp_FG_Petit_Helipad" } +metalFarpScene.modTypes = { "FG_small_Helipad" } -- Minimum CTLD version providing the plugin-scene machinery (load-position-independent menus, -- requiresCtld check). CTLD warns at load if it is older. metalFarpScene.requiresCtld = "2.0.0" -metalFarpScene.crate = { +metalFarpScene.crate = { weight = 1001.26, i18nKey = "Metal FARP Crate", deployKey = "Deploy Metal FARP", @@ -152,10 +156,10 @@ metalFarpScene.crate = { showSets = false, } -metalFarpScene.steps = { +metalFarpScene.steps = { -- ---------------------------------------------------------------- - -- Step 1: Farp_FG_Petit_Helipad heliport (delay=0). + -- Step 1: FG_small_Helipad heliport (delay=0). -- Spawned 50 m ahead of the trigger unit to avoid overlapping it. -- Saves the spawned airbase name for the warehouse-stocking step. -- critical=true: if the mod is absent the helipad cannot spawn; abort the whole scene @@ -166,9 +170,9 @@ metalFarpScene.steps = { delayAfterPreviousStep = 0, relativeHeadingInDegrees = 0, relativeAltitudeInMeters = 0, - registryKey = "Farp_FG_Petit_Helipad", - critical = true, - func = function(ctx) + registryKey = "FG_small_Helipad", + critical = true, + func = function(ctx) if not ctx.spawnedObj then return false end ctx.scene._params.farpName = ctx.spawnedObj:getName() return true @@ -183,7 +187,7 @@ metalFarpScene.steps = { delayAfterPreviousStep = 5, relativeHeadingInDegrees = 90, relativeAltitudeInMeters = 0, - registryKey = "Fuel_Truck", + registryKey = "Fuel_Truck", }, -- ---------------------------------------------------------------- @@ -194,7 +198,7 @@ metalFarpScene.steps = { delayAfterPreviousStep = 0, relativeHeadingInDegrees = 90, relativeAltitudeInMeters = 0, - registryKey = "repare_Truck", + registryKey = "repare_Truck", }, -- ---------------------------------------------------------------- @@ -205,7 +209,7 @@ metalFarpScene.steps = { delayAfterPreviousStep = 0.5, relativeHeadingInDegrees = 90, relativeAltitudeInMeters = 0, - registryKey = "FARP_Tent", + registryKey = "FARP_Tent", }, -- ---------------------------------------------------------------- @@ -216,7 +220,7 @@ metalFarpScene.steps = { delayAfterPreviousStep = 4.5, relativeHeadingInDegrees = 0, relativeAltitudeInMeters = 0, - registryKey = "ammo_cargo", + registryKey = "ammo_cargo", }, -- ---------------------------------------------------------------- @@ -227,7 +231,7 @@ metalFarpScene.steps = { delayAfterPreviousStep = 5, relativeHeadingInDegrees = 310, relativeAltitudeInMeters = 4, - registryKey = "NF-2_LightOn", + registryKey = "NF-2_LightOn", }, -- ---------------------------------------------------------------- @@ -238,7 +242,7 @@ metalFarpScene.steps = { delayAfterPreviousStep = 0, relativeHeadingInDegrees = 220, relativeAltitudeInMeters = 0, - registryKey = "Windsock", + registryKey = "Windsock", }, -- ---------------------------------------------------------------- @@ -249,7 +253,7 @@ metalFarpScene.steps = { delayAfterPreviousStep = 0, relativeHeadingInDegrees = 90, relativeAltitudeInMeters = 0, - registryKey = "us carrier shooter", + registryKey = "us carrier shooter", }, -- ---------------------------------------------------------------- @@ -266,16 +270,16 @@ metalFarpScene.steps = { local w = ab:getWarehouse() -- If this is a redeployed FARP, restore the snapshot; otherwise stock defaults. local snap = ctx.scene._params.repackData - and ctx.scene._params.repackData.warehouseSnapshot + and ctx.scene._params.repackData.warehouseSnapshot if snap and snap.liquid then for fuelType = 0, 3 do w:setLiquidAmount(fuelType, snap.liquid[fuelType] or 0) end else - w:addLiquid(0, 10000) -- jet fuel - w:addLiquid(1, 10000) -- aviation gasoline - w:addLiquid(2, 10000) -- MW50 - w:addLiquid(3, 10000) -- diesel + w:addLiquid(0, 10000) -- jet fuel + w:addLiquid(1, 10000) -- aviation gasoline + w:addLiquid(2, 10000) -- MW50 + w:addLiquid(3, 10000) -- diesel end end end @@ -291,7 +295,7 @@ metalFarpScene.steps = { -- Captures the current warehouse fuel levels so they can be restored on next deployment. -- ==================================================================================================== -metalFarpScene.onRepack = function(scene, repackData) +metalFarpScene.onRepack = function(scene, repackData) local farpName = scene._params and scene._params.farpName if not farpName then return end local ab = Airbase.getByName(farpName) @@ -299,10 +303,10 @@ metalFarpScene.onRepack = function(scene, repackData) local w = ab:getWarehouse() repackData.warehouseSnapshot = { liquid = { - [0] = w:getLiquidAmount(0), -- jet fuel - [1] = w:getLiquidAmount(1), -- aviation gasoline - [2] = w:getLiquidAmount(2), -- MW50 - [3] = w:getLiquidAmount(3), -- diesel + [0] = w:getLiquidAmount(0), -- jet fuel + [1] = w:getLiquidAmount(1), -- aviation gasoline + [2] = w:getLiquidAmount(2), -- MW50 + [3] = w:getLiquidAmount(3), -- diesel } } end From 454d01b78edb3acdee27b7a953408970b7d355b4 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:36:36 +0200 Subject: [PATCH 09/17] feat(ci): add validate-docs job for README/generated-doc coherence - tools/ci/validate_docs.py: checks front-matter, modUrls structure, and presence of docs/plugins/{name}.{fr.}md for every plugin - .github/workflows/ci.yml: new validate-docs job (pip pyyaml + script) - README.md: document the CI gate and plugin authoring steps Closes #8 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 11 ++++ README.md | 17 ++++++ tools/ci/validate_docs.py | 113 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 tools/ci/validate_docs.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe91fd0..eb7fa54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,17 @@ jobs: - name: Run busted run: busted tests/ plugins/ + # Documentation coherence gate: README front-matter + generated doc pages. + validate-docs: + name: Validate plugin docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install PyYAML + run: pip install pyyaml + - name: Check README / generated docs coherence + run: python tools/ci/validate_docs.py + # Build every plugin into dist/.lua and upload as artifacts. build: name: Build plugins diff --git a/README.md b/README.md index f57d657..3fc7dc5 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,20 @@ docs/ mkdocs bilingual (EN + FR) catalogue The vendored `vendor/CTLD.lua` and `tests/data/dcs_types.lua` are pinned to a CTLD baseline; refresh them together when bumping the supported CTLD version. + +- **Document a plugin:** create `plugins//README.md` (use `plugins/_template/README.md` as + model), then run the `generate-plugin-doc` skill in Claude to produce `docs/plugins/.md` + and `docs/plugins/.fr.md`. Finally add the plugin row to `docs/index.md` and + `docs/index.fr.md`. + +### CI gate — `validate-docs` + +The `validate-docs` job runs `tools/ci/validate_docs.py` on every push and PR. It: + +- **Warns** (exit 0) when a plugin has no `README.md` (docs will have no prose, but the build is not blocked). +- **Errors** (exit 1) when: + - `README.md` is present but its YAML front-matter is absent or malformed. + - `modUrls` exists but is not a list of `{mod, url}` pairs. + - `docs/plugins/.md` or `docs/plugins/.fr.md` is absent. + +Run it locally before pushing: `python tools/ci/validate_docs.py` (requires `pyyaml`). diff --git a/tools/ci/validate_docs.py b/tools/ci/validate_docs.py new file mode 100644 index 0000000..8b3294c --- /dev/null +++ b/tools/ci/validate_docs.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +validate_docs.py — CI gate for plugin documentation coherence. + +For each plugin directory under plugins/ (excluding _template): + - WARNING if plugins/{name}/README.md is absent + - ERROR if README.md is present but front-matter YAML is malformed + - ERROR if README.md is present and modUrls is not a list of {mod, url} pairs + - ERROR if docs/plugins/{name}.md or docs/plugins/{name}.fr.md is absent +""" + +import sys +import os +import re + +PLUGINS_DIR = "plugins" +DOCS_DIR = os.path.join("docs", "plugins") +TEMPLATE = "_template" + +errors = [] +warnings = [] + + +def parse_frontmatter(path): + """Return parsed front-matter dict, or None if absent/malformed.""" + with open(path, encoding="utf-8") as f: + content = f.read() + + # Strip comment lines from inside the YAML front-matter before parsing + match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL) + if not match: + return None + + raw_yaml = match.group(1) + # Remove YAML comment lines + yaml_lines = [l for l in raw_yaml.splitlines() if not l.strip().startswith("#")] + clean_yaml = "\n".join(yaml_lines) + + try: + import yaml + return yaml.safe_load(clean_yaml) or {} + except Exception as e: + return e # signal parse error + + +def validate_mod_urls(mod_urls): + """Return error message if modUrls is malformed, else None.""" + if not isinstance(mod_urls, list): + return "modUrls must be a list" + for i, entry in enumerate(mod_urls): + if not isinstance(entry, dict): + return f"modUrls[{i}] must be a dict with 'mod' and 'url' keys" + if "mod" not in entry or "url" not in entry: + return f"modUrls[{i}] is missing 'mod' or 'url' key" + return None + + +def main(): + if not os.path.isdir(PLUGINS_DIR): + print(f"ERROR: {PLUGINS_DIR}/ directory not found — run from repo root") + sys.exit(1) + + plugins = [ + p for p in os.listdir(PLUGINS_DIR) + if os.path.isdir(os.path.join(PLUGINS_DIR, p)) and p != TEMPLATE + ] + + if not plugins: + print("No plugins found (excluding _template) — nothing to validate.") + sys.exit(0) + + for plugin in sorted(plugins): + readme_path = os.path.join(PLUGINS_DIR, plugin, "README.md") + doc_en = os.path.join(DOCS_DIR, f"{plugin}.md") + doc_fr = os.path.join(DOCS_DIR, f"{plugin}.fr.md") + + # README absent → warning only + if not os.path.isfile(readme_path): + warnings.append(f"[{plugin}] README.md absent — doc pages will have no narrative prose") + else: + fm = parse_frontmatter(readme_path) + if fm is None: + errors.append(f"[{plugin}] README.md has no YAML front-matter") + elif isinstance(fm, Exception): + errors.append(f"[{plugin}] README.md front-matter is malformed: {fm}") + else: + mod_urls = fm.get("modUrls") + if mod_urls is not None: + err = validate_mod_urls(mod_urls) + if err: + errors.append(f"[{plugin}] README.md modUrls invalid: {err}") + + # Generated doc pages must exist + for doc_path in (doc_en, doc_fr): + if not os.path.isfile(doc_path): + errors.append(f"[{plugin}] missing generated doc: {doc_path}") + + # Report + for w in warnings: + print(f"WARNING: {w}") + for e in errors: + print(f"ERROR: {e}") + + if errors: + print(f"\n{len(errors)} error(s) — fix before merging.") + sys.exit(1) + else: + print(f"OK — {len(plugins)} plugin(s) checked, {len(warnings)} warning(s), 0 errors.") + sys.exit(0) + + +if __name__ == "__main__": + main() From 3890e6b5be2eafc3ba989ef3bd50dd382db73fa4 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:45:35 +0200 Subject: [PATCH 10/17] feat(docs): versioned site with mike (latest/dev) - docs.yml: triggers on master+develop, mike deploy instead of gh-deploy master -> version 'master' aliased 'latest' (set as default) develop -> version 'develop' aliased 'dev' - Inject GITHUB_REF_NAME into /master/ download URLs before build so dev docs link to develop-branch sources - mkdocs.yml: add version selector (mike provider, default: latest) Closes #9 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docs.yml | 34 +++++++++++++++++++++++++--------- mkdocs.yml | 5 +++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 597cd6f..75929c3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,7 @@ name: Docs on: push: - branches: [develop] + branches: [develop, master] paths: ['docs/**', 'mkdocs.yml', '.github/workflows/docs.yml'] workflow_dispatch: @@ -10,8 +10,8 @@ permissions: contents: write jobs: - build-deploy: - name: Build & deploy catalogue + deploy: + name: Deploy versioned docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -20,10 +20,26 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.x' - - name: Install mkdocs-material + i18n - run: pip install mkdocs-material mkdocs-static-i18n - - name: Build - run: mkdocs build --strict - - name: Deploy to gh-pages + - name: Install dependencies + run: pip install mkdocs-material mkdocs-static-i18n mike + - name: Configure git for mike + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + - name: Inject branch into download URLs + # Replace /master/ with the actual branch in raw download links, + # so the dev docs point to develop-branch sources. + run: | + branch="${GITHUB_REF_NAME}" + if [ "$branch" != "master" ]; then + find docs -name "*.md" -exec sed -i \ + "s|/CTLD_plugins/master/|/CTLD_plugins/${branch}/|g" {} \; + fi + - name: Deploy latest (master → latest) + if: github.ref == 'refs/heads/master' + run: | + mike deploy --push --update-aliases master latest + mike set-default --push latest + - name: Deploy dev (develop → dev) if: github.ref == 'refs/heads/develop' - run: mkdocs gh-deploy --force + run: mike deploy --push --update-aliases develop dev diff --git a/mkdocs.yml b/mkdocs.yml index b50e1ae..ce52020 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,11 @@ plugins: name: Français build: true +extra: + version: + provider: mike + default: latest + nav: - Home: index.md - Plugins: From 2b69d5cc10897ff2af9153c456795717e0178938 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:50:51 +0200 Subject: [PATCH 11/17] fix: align mod name FG_small_Helipad in catalogue + clearer CI error - docs/index.md, index.fr.md: Farp_FG_Petit_Helipad -> FG_small_Helipad - validate_docs.py: distinguish absent front-matter (None) from malformed structure (Exception) for a clearer error message Co-Authored-By: Claude Sonnet 4.6 --- docs/index.fr.md | 2 +- docs/index.md | 2 +- tools/ci/validate_docs.py | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/index.fr.md b/docs/index.fr.md index 6e4ba3a..f1612ad 100644 --- a/docs/index.fr.md +++ b/docs/index.fr.md @@ -18,4 +18,4 @@ mission ne les embarque que si elle le décide. | Plugin | Ce qu'il construit | Requiert | Télécharger | |--------|--------------------|----------|-------------| -| [Metal FARP](plugins/metal-farp.md) | Un FARP à hélisurface métallique | Mod DCS `Farp_FG_Petit_Helipad` | [⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button } | +| [Metal FARP](plugins/metal-farp.md) | Un FARP à hélisurface métallique | Mod DCS `FG_small_Helipad` | [⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button } | diff --git a/docs/index.md b/docs/index.md index 39b40dc..3036060 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,4 +17,4 @@ only pays for them if it opts in. | Plugin | What it builds | Requires | Download | |--------|----------------|----------|----------| -| [Metal FARP](plugins/metal-farp.md) | A metal-helipad FARP | DCS mod `Farp_FG_Petit_Helipad` | [⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button } | +| [Metal FARP](plugins/metal-farp.md) | A metal-helipad FARP | DCS mod `FG_small_Helipad` | [⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button } | diff --git a/tools/ci/validate_docs.py b/tools/ci/validate_docs.py index 8b3294c..23ebc35 100644 --- a/tools/ci/validate_docs.py +++ b/tools/ci/validate_docs.py @@ -22,14 +22,17 @@ def parse_frontmatter(path): - """Return parsed front-matter dict, or None if absent/malformed.""" + """Return parsed front-matter dict, None if absent, or Exception if malformed.""" with open(path, encoding="utf-8") as f: content = f.read() - # Strip comment lines from inside the YAML front-matter before parsing + if not content.startswith("---\n"): + return None # front-matter truly absent + + # Opening --- found: expect a closing --- too match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL) if not match: - return None + return Exception("opening '---' found but closing '---' is missing or malformed") raw_yaml = match.group(1) # Remove YAML comment lines From f977181892723ac23ea3d25c6b1b49c05d7b2b40 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:53:09 +0200 Subject: [PATCH 12/17] fix(test): align modTypes assertion with renamed mod FG_small_Helipad Co-Authored-By: Claude Sonnet 4.6 --- tests/scene_asset_gate_spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scene_asset_gate_spec.lua b/tests/scene_asset_gate_spec.lua index 0bb8d96..1ca1259 100644 --- a/tests/scene_asset_gate_spec.lua +++ b/tests/scene_asset_gate_spec.lua @@ -130,7 +130,7 @@ describe("plugin scene asset hard-gate", function() end) it("metalFarp's mod type is declared via modTypes", function() - assert.is_true(modUnion["Farp_FG_Petit_Helipad"] == true) + assert.is_true(modUnion["FG_small_Helipad"] == true) end) end) From 18ca51eabc29d915f453d519ab21684870ced713 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:56:58 +0200 Subject: [PATCH 13/17] docs(adr): add versioned site URLs table to ADR-0002 Co-Authored-By: Claude Sonnet 4.6 --- docs/adr/0002-mike-versioned-docs.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/adr/0002-mike-versioned-docs.md b/docs/adr/0002-mike-versioned-docs.md index ad25c92..399c5b4 100644 --- a/docs/adr/0002-mike-versioned-docs.md +++ b/docs/adr/0002-mike-versioned-docs.md @@ -7,6 +7,14 @@ Le site MkDocs Material est géré par **mike**, avec deux versions coexistant s - **Un seul site (master uniquement)** : impossible de tester la doc et les liens download en recette sans bricolage — le testeur aurait cliqué "télécharger" et obtenu la version stable, pas celle en cours de test. - **Deux sites séparés (`/` et `/dev/`)** : fonctionne mais perd le sélecteur de version intégré, déjà validé dans l'orga VEAF (`veaf.github.io/documentation/`). +## URLs du site + +| Version | URL | Déclencheur | +|---------|-----|-------------| +| Production (`latest`) | | push `master` | +| Recette (`dev`) | | push `develop` | +| Racine | | redirige vers `latest` | + ## Conséquences - Le workflow `docs.yml` appelle `mike deploy dev` sur push `develop` et `mike deploy latest` sur push `master`. From c97b1c2f155b0a7d53d9306fc3321ab65195e9ac Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:58:26 +0200 Subject: [PATCH 14/17] docs(readme): add versioned documentation site URLs Co-Authored-By: Claude Sonnet 4.6 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3fc7dc5..02e7918 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,11 @@ The `validate-docs` job runs `tools/ci/validate_docs.py` on every push and PR. I - `docs/plugins/.md` or `docs/plugins/.fr.md` is absent. Run it locally before pushing: `python tools/ci/validate_docs.py` (requires `pyyaml`). + +## Documentation site + +| Version | URL | +|---------|-----| +| Production (`latest`, from `master`) | | +| Recette (`dev`, from `develop`) | | +| Racine (redirects to `latest`) | | From f95abd1c13f2859655b188cac8ec07fe96bbaf83 Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:00:25 +0200 Subject: [PATCH 15/17] fix(metal-farp): correct spelling apparaitre -> apparaitre in README Co-Authored-By: Claude Sonnet 4.6 --- plugins/metal-farp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/metal-farp/README.md b/plugins/metal-farp/README.md index 878cf63..b955830 100644 --- a/plugins/metal-farp/README.md +++ b/plugins/metal-farp/README.md @@ -14,7 +14,7 @@ modUrls: # Metal FARP -Ce plugin permet via CTLD de faire apparaitre un héliport métallique de façon progressive pour simuler sa construction, en y ajoutant des objets pour obtenir un héliport "décoré" plus réaliste. +Ce plugin permet via CTLD de faire apparaître un héliport métallique de façon progressive pour simuler sa construction, en y ajoutant des objets pour obtenir un héliport "décoré" plus réaliste. ## Prérequis From ba8a829638a3586a12338197aedade7a3d64591a Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:56:45 +0200 Subject: [PATCH 16/17] fix(docs): fix download buttons and migrate all deliverables to English - CI: copy plugin .lua files to docs/downloads/ before MkDocs build so they are served same-origin, making the download attribute effective in all browsers (closes #12) - Docs: switch .lua buttons to relative downloads/ path + download attr; add target="ext-{mod}" rel="noopener" on mod buttons; add target="_blank" rel="noopener" on external prose links (closes #13) - Domain: update CONTEXT.md (Bouton download, Mod button, External prose link entries); add ADR-0003 for docs/downloads/ serving decision (closes #14) - Skill: update generate-plugin-doc template with new button/link rules (closes #15) - Migrate CONTEXT.md and ADR 0001-0003 from French to English Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/generate-plugin-doc/SKILL.md | 11 ++-- .github/workflows/docs.yml | 6 +- CONTEXT.md | 58 +++++++++++-------- .../adr/0001-readme-as-doc-source-of-truth.md | 18 +++--- docs/adr/0002-mike-versioned-docs.md | 30 +++++----- .../adr/0003-serve-lua-from-docs-downloads.md | 19 ++++++ docs/index.fr.md | 2 +- docs/index.md | 2 +- docs/plugins/metal-farp.fr.md | 4 +- docs/plugins/metal-farp.md | 4 +- 10 files changed, 93 insertions(+), 61 deletions(-) create mode 100644 docs/adr/0003-serve-lua-from-docs-downloads.md diff --git a/.claude/skills/generate-plugin-doc/SKILL.md b/.claude/skills/generate-plugin-doc/SKILL.md index e15a33c..82a356a 100644 --- a/.claude/skills/generate-plugin-doc/SKILL.md +++ b/.claude/skills/generate-plugin-doc/SKILL.md @@ -25,11 +25,12 @@ Two files, both beginning with the `do not edit` comment: ### `docs/plugins/{name}.fr.md` - Title: `# {scene name}` (from `.lua`) -- Download button (primary): `[⬇ {name}.lua]({raw_url}){ .md-button .md-button--primary }` - where `raw_url = https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/{name}/src/{lua_file}` +- Download button (primary): `[⬇ {name}.lua](../downloads/{lua_file}){ .md-button .md-button--primary download }` - Sections: Description, Prérequis, Installation, Remarques (omit if empty) - Content: the French prose from `README.md` body (if present), otherwise minimal generated text -- Mod links: for each entry in `modUrls`, add a link `[⬇ {mod}]({url}){ .md-button }` in the Prérequis section +- Mod links: for each entry in `modUrls`, add a link in the Prérequis section: + `[⬇ {mod}]({url}){ .md-button target="ext-{mod}" rel="noopener" }` +- External prose links: any hyperlink pointing to an external site must use `target="_blank" rel="noopener"` ### `docs/plugins/{name}.md` @@ -41,7 +42,7 @@ Two files, both beginning with the `do not edit` comment: - Never edit `docs/plugins/` files by hand after generation — they are owned by this skill. - If `README.md` is absent: generate minimal pages from `.lua` metadata only (no prose, no modUrls). -- `{branch}` in download URLs is always `master` for now (mike versioning handles dev separately). -- After writing both files, remind the author to add the plugin to `docs/index.md` and `docs/index.fr.md` if not already present. +- Download buttons always use a relative path (`../downloads/{lua_file}`) — never an absolute URL. +- After writing both files, remind the author to add the plugin to `docs/index.md` and `docs/index.fr.md` if not already present. In those index files, the download button path is `downloads/{lua_file}` (no leading `../`). diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 75929c3..0d4c0ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: Docs on: push: branches: [develop, master] - paths: ['docs/**', 'mkdocs.yml', '.github/workflows/docs.yml'] + paths: ['docs/**', 'plugins/**/src/*.lua', 'mkdocs.yml', '.github/workflows/docs.yml'] workflow_dispatch: permissions: @@ -26,6 +26,10 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + - name: Copy plugin .lua files to docs/downloads/ + run: | + mkdir -p docs/downloads + cp plugins/*/src/*.lua docs/downloads/ - name: Inject branch into download URLs # Replace /master/ with the actual branch in raw download links, # so the dev docs point to develop-branch sources. diff --git a/CONTEXT.md b/CONTEXT.md index a0c5cb1..96d3ed8 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,49 +1,57 @@ # CONTEXT.md — CTLD_plugins -Glossaire et décisions de domaine. Ne contient pas de détails d'implémentation. +Glossary and domain decisions. Contains no implementation details. --- -## Glossaire +## Glossary **Plugin** -Un fichier `.lua` unique qui s'auto-enregistre dans CTLD au chargement. Un plugin = une scène. Les plugins vivent dans `plugins/{nom}/src/`. +A single `.lua` file that self-registers in CTLD when loaded. One plugin = one scene. Plugins live in `plugins/{name}/src/`. -**Scène** -Ce qu'un plugin construit en jeu (ex : un FARP métallique). Terme runtime DCS/CTLD. +**Scene** +What a plugin builds in-game (e.g. a metal FARP). Runtime DCS/CTLD term. **Mission maker** -Utilisateur final du plugin : il télécharge le `.lua`, le charge dans l'éditeur de mission DCS, et installe les mods requis sur sa machine. +End user of a plugin: downloads the `.lua`, loads it in the DCS Mission Editor, and installs any required mods on their machine. -**Mod DCS** -Extension tierce pour DCS World fournissant des types d'objets non-stock. Un plugin qui utilise un mod le déclare dans `modTypes` (runtime) et dans `modUrls` (doc). +**DCS mod** +Third-party extension for DCS World providing non-stock object types. A plugin that requires a mod declares it in `modTypes` (runtime) and in `modUrls` (documentation). **modUrls** -Table de paires `{mod, url}` dans le front-matter YAML du `README.md` d'un plugin. Source de vérité documentaire pour les liens vers les mods requis. Distinct de `modTypes` (runtime Lua uniquement). +Table of `{mod, url}` pairs in the YAML front-matter of a plugin's `README.md`. Authoritative source for links to required mods. Distinct from `modTypes` (Lua runtime only). -**README plugin** -Fichier `plugins/{nom}/README.md`. Source de vérité unique pour la documentation d'un plugin. Contient un front-matter YAML (métadonnées structurées dont `modUrls`) et une prose en français. Suit le template `plugins/_template/README.md`. +**Plugin README** +File `plugins/{name}/README.md`. Single source of truth for a plugin's documentation. Contains a YAML front-matter (structured metadata including `modUrls`) and French prose. Follows the template at `plugins/_template/README.md`. -**Doc générée** -Les fichiers `docs/plugins/{nom}.md` (EN) et `docs/plugins/{nom}.fr.md` (FR) sont générés par un skill Claude interactif à partir du README plugin. Ils ne sont jamais édités à la main. +**Generated doc** +The files `docs/plugins/{name}.md` (EN) and `docs/plugins/{name}.fr.md` (FR) generated by an interactive Claude skill from the plugin README. Never edited by hand. **Skill generate-plugin-doc** -Skill Claude (à créer) qui lit `plugins/{nom}/README.md` + le template de doc, assemble et traduit FR→EN, et produit les deux fichiers `docs/plugins/`. +Claude skill that reads `plugins/{name}/README.md` and the doc template, assembles and translates FR→EN, and produces the two `docs/plugins/` files. **Catalogue** -Page d'index (`docs/index.md` / `docs/index.fr.md`) listant tous les plugins disponibles avec leur bouton de téléchargement. +Index page (`docs/index.md` / `docs/index.fr.md`) listing all available plugins with their download button. -**Bouton download** -Lien direct vers le fichier `.lua` brut sur GitHub : -`https://raw.githubusercontent.com/VEAF/CTLD_plugins/{branch}/plugins/{nom}/src/{fichier}.lua` -où `{branch}` est `master` pour la version stable, `develop` pour la version dev. -Présent dans le tableau du catalogue ET dans la page de description du plugin. +**Download button** +Link to the `.lua` file served from `docs/downloads/` (same origin as the docs site). +Relative URL: `downloads/{file}.lua` (from the index) or `../downloads/{file}.lua` (from a plugin page). +Carries the `download` attribute to trigger a native browser file download. +Present in both the catalogue table and the plugin description page. + +**Mod button** +Link in the Prerequisites section of a plugin page, pointing to the external mod page. +Carries `target="ext-{mod}" rel="noopener"`: multiple clicks reuse the same named browser tab rather than opening a new one each time. + +**External prose link** +Hyperlink in generated prose pointing to an external site (e.g. a mod's GitHub repository). +Carries `target="_blank" rel="noopener"` to open an anonymous new tab. **{branch}** -Variable injectée à la génération des docs. Vaut `master` (site `latest`) ou `develop` (site `dev`). Détermine vers quelle version du fichier `.lua` pointent les boutons download. +Variable injected at doc generation time. Value is `master` (site `latest`) or `develop` (site `dev`). Not applicable to download buttons (which point to `docs/downloads/`, copied from the current branch at CI build time). -**Site docs versionné** -Site MkDocs Material géré par **mike**. Deux versions coexistent sur GitHub Pages : +**Versioned docs site** +MkDocs Material site managed by **mike**. Two versions coexist on GitHub Pages: - `latest` → branch `master` (stable) -- `dev` → branch `develop` (recette) -Chaque version a ses propres liens download cohérents avec sa branche. +- `dev` → branch `develop` (staging) +Each version has its own download links coherent with its branch. diff --git a/docs/adr/0001-readme-as-doc-source-of-truth.md b/docs/adr/0001-readme-as-doc-source-of-truth.md index 60257aa..30a5bd9 100644 --- a/docs/adr/0001-readme-as-doc-source-of-truth.md +++ b/docs/adr/0001-readme-as-doc-source-of-truth.md @@ -1,14 +1,14 @@ -# README plugin comme source de vérité documentaire +# Plugin README as documentation source of truth -Chaque plugin possède un `plugins/{nom}/README.md` (front-matter YAML + prose FR) qui est l'unique source de vérité pour sa documentation. Les fichiers `docs/plugins/{nom}.md` et `{nom}.fr.md` sont **générés** par un skill Claude interactif — jamais édités à la main. +Each plugin has a `plugins/{name}/README.md` (YAML front-matter + French prose) that is the single source of truth for its documentation. The files `docs/plugins/{name}.md` and `{name}.fr.md` are **generated** by an interactive Claude skill — never edited by hand. -## Alternatives considérées +## Alternatives considered -- **`meta.yaml` séparé + prose dans `docs/`** : deux fichiers à maintenir en sync, risque de désynchronisation. -- **Métadonnées dans le `.lua`** : le `.lua` tourne dans DCS — y coller des URLs GitHub pollue le code runtime avec de la pure métadonnée documentaire sans valeur à l'exécution. +- **Separate `meta.yaml` + prose in `docs/`**: two files to keep in sync, risk of divergence. +- **Metadata embedded in the `.lua`**: the `.lua` runs inside DCS — embedding GitHub URLs there pollutes runtime code with pure documentary metadata that has no value at execution time. -## Conséquences +## Consequences -- Les `docs/plugins/` doivent être régénérés après chaque modification du `README.md` d'un plugin. -- Le `plugins/_template/README.md` fait loi comme template de référence pour les auteurs. -- La traduction FR→EN est produite par Claude à la demande du concepteur (interactif), pas par la CI. +- `docs/plugins/` files must be regenerated after every change to a plugin's `README.md`. +- `plugins/_template/README.md` is the authoritative template for plugin authors. +- The FR→EN translation is produced by Claude on demand from the designer (interactive), not by CI. diff --git a/docs/adr/0002-mike-versioned-docs.md b/docs/adr/0002-mike-versioned-docs.md index 399c5b4..98f8af5 100644 --- a/docs/adr/0002-mike-versioned-docs.md +++ b/docs/adr/0002-mike-versioned-docs.md @@ -1,22 +1,22 @@ -# Versioning du site docs avec mike (latest / dev) +# Versioned docs site with mike (latest / dev) -Le site MkDocs Material est géré par **mike**, avec deux versions coexistant sur GitHub Pages : `latest` (branch `master`) et `dev` (branch `develop`). Chaque version injecte sa propre valeur de `{branch}` dans les boutons download, garantissant qu'un testeur sur `dev` télécharge bien le `.lua` de `develop` et non de `master`. +The MkDocs Material site is managed by **mike**, with two versions coexisting on GitHub Pages: `latest` (branch `master`) and `dev` (branch `develop`). Each version injects its own `{branch}` value into download buttons, ensuring that a tester on `dev` downloads the `.lua` from `develop` and not from `master`. -## Alternatives considérées +## Alternatives considered -- **Un seul site (master uniquement)** : impossible de tester la doc et les liens download en recette sans bricolage — le testeur aurait cliqué "télécharger" et obtenu la version stable, pas celle en cours de test. -- **Deux sites séparés (`/` et `/dev/`)** : fonctionne mais perd le sélecteur de version intégré, déjà validé dans l'orga VEAF (`veaf.github.io/documentation/`). +- **Single site (master only)**: impossible to test the docs and download links in staging without workarounds — the tester would have clicked "download" and received the stable version, not the one under test. +- **Two separate sites (`/` and `/dev/`)**: works but loses the built-in version selector, already validated in the VEAF org (`veaf.github.io/documentation/`). -## URLs du site +## Site URLs -| Version | URL | Déclencheur | -|---------|-----|-------------| -| Production (`latest`) | | push `master` | -| Recette (`dev`) | | push `develop` | -| Racine | | redirige vers `latest` | +| Version | URL | Trigger | +| ------- | --- | ------- | +| Production (`latest`) | | push to `master` | +| Staging (`dev`) | | push to `develop` | +| Root | | redirects to `latest` | -## Conséquences +## Consequences -- Le workflow `docs.yml` appelle `mike deploy dev` sur push `develop` et `mike deploy latest` sur push `master`. -- `{branch}` est une variable injectée à la génération des docs (skill Claude) — valeur `develop` ou `master`. -- Le `versions.json` géré par mike doit rester en `gh-pages`, ne pas être commité dans `develop`/`master`. +- The `docs.yml` workflow calls `mike deploy dev` on push to `develop` and `mike deploy latest` on push to `master`. +- `{branch}` is a variable injected at doc generation time (Claude skill) — value is `develop` or `master`. +- The `versions.json` managed by mike must stay on `gh-pages`, not committed to `develop`/`master`. diff --git a/docs/adr/0003-serve-lua-from-docs-downloads.md b/docs/adr/0003-serve-lua-from-docs-downloads.md new file mode 100644 index 0000000..952ed54 --- /dev/null +++ b/docs/adr/0003-serve-lua-from-docs-downloads.md @@ -0,0 +1,19 @@ +# Serve plugin `.lua` files from `docs/downloads/` instead of `raw.githubusercontent.com` + +Plugin `.lua` files are copied into `docs/downloads/` by CI before the MkDocs build, so they are served from the same origin as the docs site (`veaf.github.io`). Download buttons point to this relative path and carry the `download` attribute, guaranteeing a native browser file download in all browsers. + +## Problem + +The HTML `download` attribute is silently ignored by Chrome (and Chromium-based browsers) when the link points to a cross-origin URL, even if CORS headers are present. `raw.githubusercontent.com` is a different origin from `veaf.github.io`: clicking the button was opening the file in the browser instead of downloading it. + +## Alternatives considered + +- **`download` attribute on the raw URL**: ignored by Chrome for cross-origin links — the problem persists. +- **Link to the GitHub blob page (`/blob/`)**: requires two clicks; the mission maker lands on the GitHub UI before being able to download. +- **Package `.lua` as a GitHub Release asset**: requires a dedicated release workflow and adds disproportionate operational complexity. + +## Consequences + +- `.lua` files are duplicated in the built site (negligible size). +- Branch coherence is automatic: CI copies the files from the branch currently being built. +- The existing `sed` branch-injection step in `docs.yml` no longer applies to `.lua` download URLs (harmless: these URLs are now relative paths). diff --git a/docs/index.fr.md b/docs/index.fr.md index f1612ad..e326a01 100644 --- a/docs/index.fr.md +++ b/docs/index.fr.md @@ -18,4 +18,4 @@ mission ne les embarque que si elle le décide. | Plugin | Ce qu'il construit | Requiert | Télécharger | |--------|--------------------|----------|-------------| -| [Metal FARP](plugins/metal-farp.md) | Un FARP à hélisurface métallique | Mod DCS `FG_small_Helipad` | [⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button } | +| [Metal FARP](plugins/metal-farp.md) | Un FARP à hélisurface métallique | Mod DCS `FG_small_Helipad` | [⬇ metal-farp.lua](downloads/CTLD_metalFarpScene.lua){ download .md-button } | diff --git a/docs/index.md b/docs/index.md index 3036060..4d288c3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,4 +17,4 @@ only pays for them if it opts in. | Plugin | What it builds | Requires | Download | |--------|----------------|----------|----------| -| [Metal FARP](plugins/metal-farp.md) | A metal-helipad FARP | DCS mod `FG_small_Helipad` | [⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button } | +| [Metal FARP](plugins/metal-farp.md) | A metal-helipad FARP | DCS mod `FG_small_Helipad` | [⬇ metal-farp.lua](downloads/CTLD_metalFarpScene.lua){ download .md-button } | diff --git a/docs/plugins/metal-farp.fr.md b/docs/plugins/metal-farp.fr.md index d1250af..859b8d4 100644 --- a/docs/plugins/metal-farp.fr.md +++ b/docs/plugins/metal-farp.fr.md @@ -2,7 +2,7 @@ # Metal FARP -[⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button .md-button--primary } +[⬇ metal-farp.lua](../downloads/CTLD_metalFarpScene.lua){ download .md-button .md-button--primary } Ce plugin permet via CTLD de faire apparaître un héliport métallique de façon progressive pour simuler sa construction, en y ajoutant des objets pour obtenir un héliport "décoré" plus réaliste. @@ -13,7 +13,7 @@ simuler sa construction, en y ajoutant des objets pour obtenir un héliport "dé - Le mod DCS **`FG_small_Helipad`** installé sur **tous** les clients. Sans lui, l'héliport ne peut pas apparaître. - [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button } + [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button target="ext-FG_small_Helipad" rel="noopener" } ## Installation diff --git a/docs/plugins/metal-farp.md b/docs/plugins/metal-farp.md index ccf4e37..dbcd567 100644 --- a/docs/plugins/metal-farp.md +++ b/docs/plugins/metal-farp.md @@ -2,7 +2,7 @@ # Metal FARP -[⬇ metal-farp.lua](https://raw.githubusercontent.com/VEAF/CTLD_plugins/master/plugins/metal-farp/src/CTLD_metalFarpScene.lua){ .md-button .md-button--primary } +[⬇ metal-farp.lua](../downloads/CTLD_metalFarpScene.lua){ download .md-button .md-button--primary } This plugin uses CTLD to progressively deploy a metal helipad, simulating its construction, and surrounds it with equipment objects to create a more realistic "dressed" forward arming @@ -14,7 +14,7 @@ and refuelling point. - The DCS mod **`FG_small_Helipad`** installed on **all** clients. Without it, the helipad cannot spawn. - [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button } + [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button target="ext-FG_small_Helipad" rel="noopener" } ## Install From 51b64cc5b16f1b8a4625710fc3cca61e1c79f17c Mon Sep 17 00:00:00 2001 From: FullGas1 <51051389+FullGas1@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:07:37 +0200 Subject: [PATCH 17/17] fix(docs): fix download filename and external link new-tab behaviour - Add hooks.py: MkDocs hook that adds target="ext-{id}" rel="noopener" to all external links at build time (attr_list does not reliably apply target to inline links in Python-Markdown) - Register hook in mkdocs.yml - Switch download attribute to download="{name}.lua" to force correct filename instead of browser defaulting to "download" - Remove target/rel from attr_list in doc files and skill (hook owns this) Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/generate-plugin-doc/SKILL.md | 6 +-- docs/plugins/metal-farp.fr.md | 4 +- docs/plugins/metal-farp.md | 4 +- hooks.py | 48 +++++++++++++++++++++ mkdocs.yml | 3 ++ 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 hooks.py diff --git a/.claude/skills/generate-plugin-doc/SKILL.md b/.claude/skills/generate-plugin-doc/SKILL.md index 82a356a..2e46673 100644 --- a/.claude/skills/generate-plugin-doc/SKILL.md +++ b/.claude/skills/generate-plugin-doc/SKILL.md @@ -25,12 +25,12 @@ Two files, both beginning with the `do not edit` comment: ### `docs/plugins/{name}.fr.md` - Title: `# {scene name}` (from `.lua`) -- Download button (primary): `[⬇ {name}.lua](../downloads/{lua_file}){ .md-button .md-button--primary download }` +- Download button (primary): `[⬇ {name}.lua](../downloads/{lua_file}){ download="{name}.lua" .md-button .md-button--primary }` - Sections: Description, Prérequis, Installation, Remarques (omit if empty) - Content: the French prose from `README.md` body (if present), otherwise minimal generated text - Mod links: for each entry in `modUrls`, add a link in the Prérequis section: - `[⬇ {mod}]({url}){ .md-button target="ext-{mod}" rel="noopener" }` -- External prose links: any hyperlink pointing to an external site must use `target="_blank" rel="noopener"` + `[⬇ {mod}]({url}){ .md-button }` +- External prose links: write them as plain markdown links — the MkDocs hook (`hooks.py`) automatically adds `target` and `rel="noopener"` to all external links at build time ### `docs/plugins/{name}.md` diff --git a/docs/plugins/metal-farp.fr.md b/docs/plugins/metal-farp.fr.md index 859b8d4..981bb7a 100644 --- a/docs/plugins/metal-farp.fr.md +++ b/docs/plugins/metal-farp.fr.md @@ -2,7 +2,7 @@ # Metal FARP -[⬇ metal-farp.lua](../downloads/CTLD_metalFarpScene.lua){ download .md-button .md-button--primary } +[⬇ metal-farp.lua](../downloads/CTLD_metalFarpScene.lua){ download="metal-farp.lua" .md-button .md-button--primary } Ce plugin permet via CTLD de faire apparaître un héliport métallique de façon progressive pour simuler sa construction, en y ajoutant des objets pour obtenir un héliport "décoré" plus réaliste. @@ -13,7 +13,7 @@ simuler sa construction, en y ajoutant des objets pour obtenir un héliport "dé - Le mod DCS **`FG_small_Helipad`** installé sur **tous** les clients. Sans lui, l'héliport ne peut pas apparaître. - [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button target="ext-FG_small_Helipad" rel="noopener" } + [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button } ## Installation diff --git a/docs/plugins/metal-farp.md b/docs/plugins/metal-farp.md index dbcd567..cf5e7ea 100644 --- a/docs/plugins/metal-farp.md +++ b/docs/plugins/metal-farp.md @@ -2,7 +2,7 @@ # Metal FARP -[⬇ metal-farp.lua](../downloads/CTLD_metalFarpScene.lua){ download .md-button .md-button--primary } +[⬇ metal-farp.lua](../downloads/CTLD_metalFarpScene.lua){ download="metal-farp.lua" .md-button .md-button--primary } This plugin uses CTLD to progressively deploy a metal helipad, simulating its construction, and surrounds it with equipment objects to create a more realistic "dressed" forward arming @@ -14,7 +14,7 @@ and refuelling point. - The DCS mod **`FG_small_Helipad`** installed on **all** clients. Without it, the helipad cannot spawn. - [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button target="ext-FG_small_Helipad" rel="noopener" } + [⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button } ## Install diff --git a/hooks.py b/hooks.py new file mode 100644 index 0000000..7f29c14 --- /dev/null +++ b/hooks.py @@ -0,0 +1,48 @@ +""" +MkDocs hook: add target and rel attributes to external links. + +- External links get target="ext-{stable-id}" so that multiple clicks + on the same URL reuse the same browser tab instead of opening new ones. +- Internal links (same site) are left untouched. +""" + +import re +import urllib.parse + + +def _url_target(href: str) -> str: + """Return a stable, sanitised target name derived from the URL.""" + parsed = urllib.parse.urlparse(href) + raw = (parsed.netloc + parsed.path).strip("/") + safe = re.sub(r"[^a-zA-Z0-9]", "-", raw) + safe = re.sub(r"-{2,}", "-", safe).strip("-") + return "ext-" + safe[:48] + + +def on_page_content(html, page, config, **kwargs): + site = config.get("site_url", "") + + def process(match): + tag = match.group(0) + attrs = match.group(1) + + href_m = re.search(r'href="([^"]*)"', attrs) + if not href_m: + return tag + + href = href_m.group(1) + + # Skip non-HTTP links and links to the same site + if not href.startswith(("http://", "https://")): + return tag + if site and href.startswith(site): + return tag + + # Skip if target is already set + if "target=" in attrs: + return tag + + target = _url_target(href) + return f'' + + return re.sub(r"]+)>", process, html) diff --git a/mkdocs.yml b/mkdocs.yml index ce52020..a8532dd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,9 @@ plugins: name: Français build: true +hooks: + - hooks.py + extra: version: provider: mike