docs: community mapmaking documentation (7-file SDK guide)

- docs/mapmaking/00-index.md — SDK overview with workflow diagram
- docs/mapmaking/01-getting-started.md — template setup & first map
- docs/mapmaking/02-building-geometry.md — CSG guide & prefab reference
- docs/mapmaking/03-lighting-and-env.md — LightmapGI baking guide
- docs/mapmaking/04-validation.md — validator CLI & CI/CD usage
- docs/mapmaking/05-packaging-and-shipping.md — .pck pipeline
- docs/mapmaking/06-faq-and-troubleshooting.md — 30+ common issues
- README.md — add mapmaking SDK link to features list
This commit is contained in:
2026-07-01 18:46:10 -04:00
parent 159c554a86
commit 34507f9043
8 changed files with 1382 additions and 1 deletions
+2 -1
View File
@@ -5,6 +5,7 @@ A competitive, round-based tactical FPS built in Godot 4.
- 128-tick authoritative dedicated servers
- GDExtension (C++) simulation core
- Community-hostable, self-hosted server browser
- Map-making SDK for community maps
- [Map-making SDK](docs/mapmaking/00-index.md) for community maps
See [PLANS/PROJECT_PLAN.md](PLANS/PROJECT_PLAN.md) for the full build plan.
See [Mapmaking SDK docs](docs/mapmaking/00-index.md) to build and ship custom maps.
+104
View File
@@ -0,0 +1,104 @@
# Tactical Shooter — Mapmaking SDK
Everything you need to build, validate, package, and ship competitive maps for
Tactical Shooter. This SDK is designed for the community — no C++, no GDextension
tinkering required. You only need Godot 4 (any 4.x) and a text editor.
## Quick Start
```bash
# Copy the template project
cp -r client/map_template/ maps/my_first_map/
# Rename the demo scene
mv maps/my_first_map/template_map.tscn maps/my_first_map/de_my_map.tscn
# Open in Godot
godot maps/my_first_map/project.godot
```
Then read the [Getting Started guide](01-getting-started.md).
## What You Can Build
- **Competitive defuse maps** — A-site, B-site, CT/T spawns, buy zones
- **Custom game modes** — The template is gameplay-agnostic; your map logic
is in Godot groups, not hardcoded
- **Community servers** — Ship maps as `.pck` files via the registry server
## SDK Components
| Component | Location | Purpose |
|-----------|----------|---------|
| [Template project](01-getting-started.md) | `client/map_template/` | Standalone Godot project with CSG prefabs and demo scene |
| [CSG Prefabs](02-building-geometry.md) | `client/map_template/assets/prefabs/` | 6 drop-in node types (spawns, sites, zones) |
| [Editor Validator](04-validation.md) | `client/map_template/template_map.gd` | Auto-runs when scene opened in editor |
| [Headless Validator](04-validation.md) | `client/tools/validate_map.gd` | 4-module CLI validator for CI/CD |
| [Packaging](05-packaging-and-shipping.md) | `scripts/map_packaging/pack_map.gd` | Exports `.tscn``.pck` addon pack |
| [Registry Server](05-packaging-and-shipping.md) | `scripts/map_packaging/map_registry_server.py` | Serves `.pck` files over HTTP |
| [Client Downloader](05-packaging-and-shipping.md) | `client/scripts/map_downloader.gd` | Autoload that downloads + loads `.pck` at runtime |
## Map Authoring Workflow
```
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 1. Build │ ──→ │ 2. Validate │ ──→ │ 3. Package │
│ - CSG geometry │ │ - Editor check │ │ - pack_map.gd │
│ - Prefab nodes │ │ - CLI validator │ │ - → .pck file │
│ - Lighting │ │ - Fix warnings │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 6. Load & Play │ ←── │ 5. Download │ ←── │ 4. Upload │
│ - Auto-download │ │ - Registry HTTP │ │ - map_registry │
│ - changelevel │ │ - Cache to disk │ │ - .pck + .json │
└──────────────────┘ └──────────────────┘ └──────────────────┘
```
## Performance Targets
Maps built with the SDK must meet these budgets:
| Metric | Target | Enforced by |
|------------------|-------------|--------------------------------|
| Triangle count | ≤ 50,000 | `validate_polycount.gd` |
| Texture size | ≤ 1024×1024 | `validate_textures.gd` |
| Dynamic lights | ≤ 4 | `validate_lights.gd` |
| LightmapGI | ✓ Baked | `validate_lights.gd` |
| ReflectionProbe | ≥ 1 | `validate_lights.gd` (warning) |
| FPS target | 60+ | Project rendering settings |
## File Structure Reference
```
tactical-shooter/
├── client/
│ ├── project.godot # Main game project
│ ├── map_template/ # ← YOU ARE HERE (template project)
│ │ ├── project.godot # Standalone Godot project
│ │ ├── template_map.tscn # Demo scene (rename me)
│ │ ├── template_map.gd # Editor validation script
│ │ └── assets/prefabs/ # CSG prefab nodes
│ ├── tools/
│ │ ├── validate_map.gd # CLI validator entry point
│ │ └── validate_map/ # 4 validator modules
│ │ ├── validate_scene.gd
│ │ ├── validate_polycount.gd
│ │ ├── validate_textures.gd
│ │ └── validate_lights.gd
│ └── scripts/
│ └── map_downloader.gd # Client autoload for .pck loading
├── scripts/
│ └── map_packaging/
│ ├── pack_map.gd # Godot editor → .pck exporter
│ └── map_registry_server.py # Python HTTP registry server
└── docs/mapmaking/
├── 00-index.md # This document
├── 01-getting-started.md # Template project setup
├── 02-building-geometry.md # CSG & prefab guide
├── 03-lighting-and-env.md # Lighting & baking
├── 04-validation.md # Validator usage
├── 05-packaging-and-shipping.md# Publishing pipeline
└── 06-faq-and-troubleshooting.md
```
+135
View File
@@ -0,0 +1,135 @@
# Getting Started — Your First Map
This guide walks you through setting up the map template project and creating
a basic playable map.
## Prerequisites
- Godot 4.0 or later (any 4.x stable)
- The Tactical Shooter repository cloned
- Familiarity with Godot's editor (scene tree, inspector, 3D viewport)
## 1. Copy the Template
The template project lives at `client/map_template/`. Copy it somewhere outside
the main project (or keep it adjacent — it's a standalone Godot project):
```bash
# From the tactical-shooter repository root
cp -r client/map_template/ maps/my_first_map/
cd maps/my_first_map/
mv template_map.tscn de_dust2.tscn
```
## 2. Open in Godot
Launch Godot and click **Import** (not Open). Navigate to your copied directory
and select `project.godot`. The map template project will open.
> **Why "Import"?** Because template is a standalone project with its own
> `project.godot`. You're importing a new project into the Godot project list,
> not opening a scene within an existing project.
## 3. Explore the Demo Scene
Open `de_dust2.tscn` (the renamed `template_map.tscn`). You'll see:
- **Floor**: A green CSGBox3D pad (16×20 units)
- **Walls**: Orange CSGBox3D walls forming a rough box
- **Spawn pads**: Green (CT) and red (T) Marker3D spawn points
- **Bomb sites**: Semi-transparent orange floor panels
- **Buy zones**: Semi-transparent yellow trigger areas
- **Lighting**: DirectionalLight3D (sun), OmniLight3D fill, LightmapGI
- **Environment**: WorldEnvironment with sky
The scene also contains a `template_map.gd` editor tool script. When you open
this scene, the **Output** panel (bottom dock) shows auto-validation results
for the map structure.
## 4. Build Your First Room
1. **Select the floor** — Click `Floor` in the scene tree
2. **Scale it** — With the scale tool (R), drag the red/green/blue handles
to resize the floor to roughly 12×16 units
3. **Add walls** — Select `Walls` → right-click → **Instantiate Child Scene**
select `assets/prefabs/map_bounds.tscn`. Size them to enclose the floor
4. **Add a ceiling** — Duplicate (Ctrl+D) the floor, move it up 4 units,
set **Operation** to Union (0) so it acts as a solid ceiling
> **Tip:** Use **View → Perspective** and rotate around to check your geometry
> from all angles. Walls should overlap slightly to prevent light leaks.
## 5. Place Spawn Points
Spawn points tell the game where players appear at round start.
**CT Spawns:**
- Drag `assets/prefabs/ct_spawn.tscn` from the FileSystem dock into the scene
- Position it on the floor at one end of the map
- The green pad's +Z arrow shows the direction players face on spawn
- Duplicate for 5 players (competitive standard)
**T Spawns:**
- Same process with `assets/prefabs/t_spawn.tscn` (red pads)
- Place at the opposite end of the map
## 6. Define Bomb Sites
A competitive defuse map needs two bomb sites, A and B.
1. Drag `assets/prefabs/bomb_site.tscn` into the scene
2. Rename it to `BombsiteA`
3. Resize the CollisionShape3D to cover the intended site area
4. In the **Node** dock → **Groups** tab, ensure it has:
- `bomb_site` (required — game logic finds sites by this group)
- `bombsite_a` (informational — helps identify which site)
5. Repeat for BombsiteB with group `bombsite_b`
## 7. Add Buy Zones
Buy zones mark areas where players can purchase equipment.
1. Drag `assets/prefabs/buy_zone.tscn` into the scene
2. Place one at the CT spawn area (group `buy_zone` stays)
3. Resize the yellow CollisionShape3D to cover the spawn room
4. Duplicate for T spawn — add subgroup `ct_buy` or `t_buy` on each
## 8. Check the Editor Validator
Open the **Output** (bottom dock). The `template_map.gd` script runs
automatically. You should see something like:
```
=== Map Template: Validate Scene ===
+ CT Spawn points (5 found)
+ T Spawn points (5 found)
+ Buy zones (2 found)
+ Bomb sites (2 found)
+ Cubemap capture origins (1 found)
+ Map boundary walls (8 found)
+ CSG Floor geometry (1 nodes)
+ CSG Wall geometry (4 walls)
+ LightmapGI configured ✓
+ LightmapGI: NOT YET BAKED
+ ReflectionProbe present
+ WorldEnvironment present
+ DirectionalLight3D (sun) present
```
The "NOT YET BAKED" warning is expected — you'll bake lighting after geometry
is finalised (see [Lighting guide](03-lighting-and-env.md)).
## 9. Save and Package
When your map is ready for testing:
1. Save the scene (`Ctrl+S`)
2. Read the [Validation guide](04-validation.md) for headless CI checks
3. Read the [Packaging guide](05-packaging-and-shipping.md) to ship it
## Next Steps
- [Building Geometry with CSG](02-building-geometry.md) — detailed prefab reference
- [Lighting & Baking](03-lighting-and-env.md) — how to light your map properly
- [Validator Reference](04-validation.md) — CLI validator exit codes and CI
- [Packaging & Shipping](05-packaging-and-shipping.md) — publishing as .pck
+245
View File
@@ -0,0 +1,245 @@
# Building Geometry with CSG
Tactical Shooter maps are built with Godot's **CSG** (Constructive Solid
Geometry) system — the same tool you'd use in a 3D modeller, but inside the
Godot editor. CSG nodes let you cut, join, and extrude geometry in real-time
with instant visual feedback.
## CSG Basics
### CSG Node Types
| Node | Use | Shape |
|------|-----|-------|
| `CSGBox3D` | Walls, floors, ceilings, pillars | Rectangular prism |
| `CSGSphere3D` | Domes, curved edges | Sphere |
| `CSGCylinder3D` | Columns, pipes | Cylinder |
| `CSGCombiner3D` | Group container, non-collidable parent | — |
| `CSGPolygon3D` | Extruded 2D path | Custom shape |
### CSG Operations
Each CSG node has an **Operation** property that determines how it combines
with sibling CSG nodes in the same parent:
| Value | Operation | Effect |
|-------|-----------|--------|
| 0 | **Union** | Adds to existing geometry (default) |
| 1 | **Subtraction** | Cuts a hole through parent |
| 2 | **Intersection** | Keeps only overlapping volume |
**Example — a window cutout:**
```
Wall (CSGBox3D, Union)
└── WindowCutout (CSGBox3D, Subtraction)
```
This subtracts the window shape from the wall. Move/resize the subtraction
child to reposition the window.
### Key Properties
- **`use_collision`** (bool) — Must be `true` on all floor/wall/ceiling CSG
nodes so players and bullets collide with them
- **`use_shadows`** (bool) — Enable shadow-casting on geometry
- **`material`** — Assign placeholder materials during construction; final
materials come from the main game project library
- **`snap_mode`** — Controls vertex snapping (useful for aligning geometry)
## Building a Room
A basic room needs 3 CSG layers:
```
Room (CSGCombiner3D)
├── Floor (CSGBox3D, Union, use_collision=true)
├── Walls (CSGCombiner3D)
│ ├── Wall_North (CSGBox3D, Union)
│ ├── Wall_South (CSGBox3D, Union)
│ ├── Wall_East (CSGBox3D, Union)
│ └── Wall_West (CSGBox3D, Union)
└── Ceiling (CSGBox3D, Union, use_collision=true)
```
### CSG Building Tips
1. **Overlap slightly** — Walls should extend past the floor edge by ~0.1 units
to prevent light leaks (thin gaps let light bleed through)
2. **Keep CSG simple** — Use as few CSG nodes as possible. A wall panel is one
CSGBox3D, not 4 extruded edges
3. **Convert to Mesh when done** — Once geometry is final, select all CSG
nodes → right-click → **Convert CSG to Mesh**. This bakes them into static
MeshInstance3D nodes that perform much better
4. **Avoid curved CSG for floor/walls** — CSGSphere3D and CSGCylinder3D are
expensive. Use them sparingly
## Prefab Reference
The template provides 6 drag-and-drop prefab nodes. Each is an `assets/prefabs/*.tscn`
file that you instantiate into your scene.
### ct_spawn.tscn
| Property | Value |
|----------|-------|
| Type | `Marker3D` (green pad + arrow) |
| Group | `ct_spawn` |
| Purpose | CT team spawn position |
| Quantity | 5 (one per player) |
The +Z arrow (blue axis) shows the direction players face when they spawn.
Position the pad at floor level — make it flush with the floor surface.
**Customisation:** Duplicate instances rather than editing the prefab. Adjust
only position and rotation. The green pad is cosmetic.
### t_spawn.tscn
| Property | Value |
|----------|-------|
| Type | `Marker3D` (red pad + arrow) |
| Group | `t_spawn` |
| Purpose | Terrorist team spawn position |
| Quantity | 5 |
Same usage as `ct_spawn` but red. Place at the opposite end of the map.
### buy_zone.tscn
| Property | Value |
|----------|-------|
| Type | `Area3D` with `CollisionShape3D` |
| Group | `buy_zone` |
| Purpose | Purchase-eligible zone |
| Quantity | 12 |
The game monitors `body_entered` / `body_exited` on these areas to show/hide
the buy menu. Resize the child CollisionShape3D to match the room shape.
**Additional groups (optional):**
- `ct_buy` — Only CT team can buy in this zone
- `t_buy` — Only T team can buy in this zone
If omitted, any player inside a `buy_zone` can buy (suitable for shared mid areas).
### bomb_site.tscn
| Property | Value |
|----------|-------|
| Type | `Area3D` with `CollisionShape3D` |
| Group | `bomb_site` |
| Purpose | Bomb plant zone |
| Quantity | 2 (A and B) |
Rename instances to `BombsiteA` and `BombsiteB`. Resize the CollisionShape3D
to cover the plantable area. Visual: semi-transparent orange floor panel.
**Additional groups (strongly recommended):**
- `bombsite_a` — Identifies this as site A
- `bombsite_b` — Identifies this as site B
### cubemap_origin.tscn
| Property | Value |
|----------|-------|
| Type | `Node3D` (blue sphere marker) |
| Group | `cubemap_origin` |
| Purpose | Marks ReflectionProbe capture position |
| Quantity | 1 |
Place at eye height ≈ 1.6 units above the floor in the most visually prominent
area (usually mid-map or the central choke point). The ReflectionProbe is a
separate node that should be placed at this marker's position after geometry
is finalised.
### map_bounds.tscn
| Property | Value |
|----------|-------|
| Type | `Area3D` with `CollisionShape3D` |
| Group | `map_bounds` |
| Purpose | Out-of-bounds kill wall |
| Quantity | 48 (enclose the perimeter) |
**Usage:**
- Place one instance per perimeter face (north, south, east, west)
- Resize each CollisionShape3D to form a closed box around the playable area
- Make the floor-bound wall tall enough that players can't jump over it
- The game detects `body_exited` from these areas and teleports/kills
out-of-bounds players
> **Performance tip:** `map_bounds` nodes should be as thin as practical —
> these are just detection triggers, not visual geometry. A thickness of 0.5
> units is sufficient.
## Advanced Geometry Techniques
### Corridors and Hallways
Build a corridor as a CSGCombiner3D with union floor + walls + ceiling and
subtraction nodes for doors/windows:
```
Corridor (CSGCombiner3D)
├── Floor (CSGBox3D, Union)
├── LeftWall (CSGBox3D, Union)
│ └── Door1 (CSGBox3D, Subtraction)
├── RightWall (CSGBox3D, Union)
│ └── Window1 (CSGBox3D, Subtraction)
└── Ceiling (CSGBox3D, Union)
```
### Ramps and Stairs
Use multiple CSGBox3D nodes at increasing heights. Stack them like steps and
wrap in a CSGCombiner3D for a clean hierarchy:
```
Stairs (CSGCombiner3D)
├── Step1 (CSGBox3D, Union, pos=(0, 0, 0))
├── Step2 (CSGBox3D, Union, pos=(0, 0.5, 1))
├── Step3 (CSGBox3D, Union, pos=(0, 1.0, 2))
└── Step4 (CSGBox3D, Union, pos=(0, 1.5, 3))
```
For ramps, rotate a CSGBox3D or use CSGPolygon3D with an extruded triangle shape.
### Map Scale Reference
| Element | Size (units) | Notes |
|---------|-------------|-------|
| Player height | 1.8 | Eye level ≈ 1.6 |
| Player width | 0.5 | Collision capsule radius |
| Doorway | 1.5 × 2.5 | Wide enough for 2 players |
| Corridor | 34 wide | Comfortable movement |
| Bombsite | 8×8 minimum | Playable site area |
| Spawn room | 6×8 | 5 players need space |
| Mid area | 68 wide | Main chokepoint |
## From CSG to MeshInstance3D (Baking)
When your geometry is **final** (no more changes expected):
1. Select all CSG nodes in the scene tree
2. Right-click → **Convert CSG to Mesh**
3. This replaces CSG nodes with static `MeshInstance3D` nodes
4. The mesh instances are much more performant
**Don't convert until you're done** — CSG is editable, MeshInstance3D is not.
Keep working in CSG mode; convert only for final optimisation before packaging.
## Performance Budget
See the [Validator guide](04-validation.md) for detailed numbers, but the key
geometry rules are:
- **≤ 50,000 triangles** total across all meshes
- **≤ 5,000 triangles** per individual mesh (recommended; enforced as warning)
- Use CSG for blockout only — convert to mesh before shipping
- Avoid CSGSphere3D in visible areas; use hand-modelled meshes
## Next Steps
- [Lighting & Environment Setup](03-lighting-and-env.md) — light your map
- [Map Validation](04-validation.md) — run the validator on your scene
+159
View File
@@ -0,0 +1,159 @@
# Lighting & Environment Setup
Proper lighting is the difference between a map that looks professional and one
that looks like a grey box. Tactical Shooter uses Godot's **LightmapGI** system
for baked global illumination: light bounces are pre-calculated and stored in
lightmaps, giving you realistic lighting at zero runtime cost.
## Lighting Architecture
```
DirectionalLight3D (sun) — main directional light, shadows
└─ OmniLight3D (fill) — ambient fill lights, no shadow
└─ LightmapGI — bakes all static lighting into textures
└─ baked lightmaps (zero runtime cost)
```
## 1. WorldEnvironment
Every map needs a `WorldEnvironment` node. This controls:
- **Sky** — Background appearance (skybox or solid colour)
- **Ambient light** — Base light level for unlit surfaces
- **Tonemapping** — Colour grading and exposure
**Setup:**
1. The template already includes a `WorldEnvironment` node
2. If creating from scratch, add one via the scene tree → + button → `WorldEnvironment`
3. In the Inspector, assign an `Environment` resource (New Environment)
4. Configure:
- **Background:** Sky (for outdoor) or Clear Color (for underground maps)
- **Ambient Light:** Sky (mimics sky colour) or Color (custom ambient)
- **Tonemap:** Reinhard or Filmic (prevents blown-out highlights)
> The validator warns if WorldEnvironment is missing — add it early.
## 2. DirectionalLight3D (Sun)
The main sun light. One per map.
**Recommended settings:**
| Property | Value | Reason |
|----------|-------|--------|
| `light_bake_mode` | Static (2) | Bakes into lightmap |
| `shadow_enabled` | true | Required for competitive visibility |
| `shadow_bias` | 0.1 | Prevents shadow acne on CSG |
| `angular_distance` | 0.01 | Sharp sunlight (competitive visual clarity) |
| `range` | 50100 | Large enough to cover the map |
**Positioning:**
- Rotate the sun to create interesting shadows through windows/doorways
- Avoid straight-down lighting (makes everything look flat)
- A 45° angle works well for most maps
## 3. Fill Lights (OmniLight3D)
Fill lights illuminate areas the sun doesn't reach (interiors, tunnels).
**Rules:**
- **Maximum 4 dynamic (non-baked) lights** — enforced by the validator
- Fill lights should use `light_bake_mode = Static (2)` so they bake into
the lightmap and don't count toward the dynamic budget
- Use low intensity (0.30.5) for ambient fill
**Performance tip:** Every omni/spot light with `shadow_enabled = true` is
expensive. The validator warns on dynamic lights with shadows. Baked lights
with shadows disabled are free — use them.
## 4. LightmapGI — Baked Global Illumination
LightmapGI is the backbone of Tactical Shooter's lighting. It pre-computes
light bouncing and stores the result as textures (lightmaps).
### Configuration
| Property | Recommended | Notes |
|----------|-------------|-------|
| `quality` | 2 (High) | Higher = better GI, longer bake |
| `bounces` | 3 | Minimum 2 for realistic indirect lighting |
| `texel_scale` | 1.02.0 | Lower = higher res, longer bake |
| `max_texture_size` | 1024 | Controls lightmap atlas size |
| `use_denoiser` | true | Removes noise from baked result |
| `environment_mode` | 0 (Baked Environment) | Uses WorldEnvironment for sky lighting |
### Baking
To bake: select the `LightmapGI` node → click **Bake Lightmap** in the top
toolbar (or `Ctrl+Shift+C`).
**Baking checklist:**
- [ ] All geometry is finalised (moving geometry after baking invalidates it)
- [ ] All lights have correct bake mode (Static for baked, Dynamic for real-time)
- [ ] CSG has `use_collision = true` (non-collidable CSG may not bake properly)
- [ ] WorldEnvironment is configured with a sky or ambient colour
- [ ] Save the scene before baking
**Baking time:** Varies by map complexity. A small map (10×14 units) bakes in
3060 seconds. A full competitive map (20×20) may take 510 minutes.
### After Baking
- The LightmapGI node's `light_data` property will show a `LightmapGIData` resource
- The validator checks for `light_data != null` to confirm the map is baked
- Baked lightmaps are stored in the `.tscn` file as embedded resources
- You can rebake at any time — it overwrites the previous lightmaps
**Warning:** Opening an unbaked map on a server will default to unlit rendering.
Always bake before packaging.
## 5. ReflectionProbe
ReflectionProbes add specular reflections to shiny surfaces (metal, glass, water).
- Place one `ReflectionProbe` near the `cubemap_origin` marker
- Set `max_distance` to cover the visible area (1030 units)
- Enable `box_projection` for interior spaces (corrects reflection distortion)
- Set `intensity` to 0.50.8 (realistic reflection strength)
**Tip:** You need reflection probes even if your map has no mirrors — they make
metal and plastic materials look correct.
## 6. Lighting Performance Budget
| Metric | Limit | Enforced |
|--------|-------|----------|
| Dynamic (non-baked) lights | ≤ 4 | Validator (error) |
| Total lights (baked + dynamic) | Unlimited | — |
| LightmapGI bounces | ≥ 2 | Validator (warning) |
| ReflectionProbes | ≥ 1 | Validator (warning) |
| Lightmap max texture | ≤ 2048 | Project settings |
If you exceed the dynamic light budget, set more lights to
`light_bake_mode = Static (2)` — baked lights cost nothing at runtime.
## 7. Quick Start Checklist
- [ ] WorldEnvironment with Environment resource assigned
- [ ] DirectionalLight3D (sun) with shadows
- [ ] 13 fill OmniLight3D with Static bake mode
- [ ] LightmapGI with quality=2, bounces=3, texel_scale=1.0
- [ ] ReflectionProbe at cubemap origin
- [ ] All geometry CSG has `use_collision = true`
- [ ] Save scene → Bake LightmapGI (Ctrl+Shift+C)
- [ ] Verify `light_data` is populated (not null)
## Troubleshooting
| Problem | Likely Cause | Fix |
|---------|-------------|-----|
| Light leaks at corners | CSG gap < 0.01 units | Overlap CSG edges by 0.1 |
| Baked lighting too dark | bounces too low | Set bounces ≥ 2 |
| Lightmap seams visible | texel_scale too high | Lower texel_scale to 1.0 |
| Shadow acne (striped shadows) | shadow_bias too low | Increase shadow_bias to 0.1 |
| ReflectionProbe visible as sphere | probe_mode wrong | Set to Box or disable visibility |
| Bake fails with error | Scene not saved | Save `.tscn` before baking |
## Next Steps
- [Map Validation](04-validation.md) — run all checks on your scene
- [Packaging & Shipping](05-packaging-and-shipping.md) — publish your map
+235
View File
@@ -0,0 +1,235 @@
# Map Validation
The validator catches structural problems, performance budget violations, and
lighting issues before your map ships. Two validation modes are available:
1. **Editor validation** — runs automatically when you open a map scene
2. **Headless CLI validation** — runs from the terminal, suitable for CI/CD
## Editor Validation (template_map.gd)
Every map created from the template includes `template_map.gd`, a `@tool`
script that runs automatically when the scene opens in the Godot editor.
**How to use it:**
1. Open your map's `.tscn` file in the Godot editor
2. Look at the **Output** panel (bottom dock)
3. The script prints a validation report like:
```
=== Map Template: Validate Scene ===
+ CT Spawn points (5 found)
+ T Spawn points (5 found)
+ Buy zones (2 found)
+ Bomb sites (2 found)
+ Cubemap capture origins (1 found)
+ Map boundary walls (8 found)
+ LightmapGI configured ✓
+ LightmapGI: NOT YET BAKED
+ DirectionalLight3D (sun) present
```
**Checks performed:**
- Required gameplay groups exist (`ct_spawn`, `t_spawn`, `buy_zone`, etc.)
- CSG floor and wall geometry present
- LightmapGI configured and baked
- ReflectionProbe and WorldEnvironment present
- DirectionalLight3D (sun) present
- Playable area extents estimated
## Headless CLI Validator (validate_map.gd)
The headless validator is a 4-module CLI tool at `client/tools/validate_map.gd`.
Use it for automated checking before packaging or in CI/CD pipelines.
### Usage
```bash
# From the tactical-shooter repository root
godot --path client --script tools/validate_map.gd -- res://maps/mymap.tscn
```
> The `--` separates Godot engine arguments from user arguments.
> The scene path must be a `res://`-prefixed Godot resource path.
### Exit Codes
| Code | Meaning | Description |
|------|---------|-------------|
| 0 | **PASS** | All checks passed, no warnings |
| 1 | **WARNINGS** | Passed with non-blocking suggestions |
| 2 | **ERRORS** | Failed — must-fix items present |
### CI/CD Integration
```bash
#!/bin/bash
# Gate map merges on validation
godot --path client --script tools/validate_map.gd -- res://maps/mymap.tscn
exit_code=$?
if [ $exit_code -eq 2 ]; then
echo "❌ Map validation FAILED — fix errors before submitting"
exit 1
elif [ $exit_code -eq 1 ]; then
echo "⚠️ Map passed with warnings — review suggestions"
fi
echo "✅ Map validated successfully"
exit 0
```
### Example Output
```
═══════════════════════════════════════════
MAP VALIDATOR
Scene: res://maps/de_dust2.tscn
═══════════════════════════════════════════
── [validate_scene] ────────────────────────────
✓ Root node: World (Node3D)
✓ WorldEnvironment (WorldEnvironment)
✓ LightmapGI (LightmapGI)
✓ Group "ct_spawn" → CT_SpawnPos (5 node(s))
✓ Group "t_spawn" → T_SpawnPos (5 node(s))
✓ Group "buy_zone" → BuyZone_A (2 node(s))
✓ Group "bomb_site" → BombsiteA (2 node(s))
✓ Group "cubemap_origin" → CubemapOrigin (1 node(s))
✓ pass
── [validate_polycount] ────────────────────────
Meshes scanned: 24
Total triangles: 12,842
Budget: 50,000
✓ Within budget (25.7% of 50000)
Top meshes by triangle count:
4200 Building_01 (MeshInstance3D)
2100 Wall_Detail (MeshInstance3D)
1042 Floor_Tiles (MeshInstance3D)
✓ pass
── [validate_textures] ─────────────────────────
Textures inspected: 18
✓ All textures within 1024×1024 limit
✓ pass
── [validate_lights] ───────────────────────────
Light breakdown:
DirectionalLight3D: 1
OmniLight3D: 2
SpotLight3D: 0
Dynamic (non-baked): 1
Baked: 2
✓ Dynamic light count: 1 (limit: 4)
✓ LightmapGI baked (light_data present)
Quality: 2 (0=Low, 1=Med, 2=High, 3=Ultra)
Bounces: 3
Texel Scale: 1.0
Max Texture Size: 1024
✓ WorldEnvironment configured
ReflectionProbes: 1
✓ pass
═══════════════════════════════════════════
SUMMARY
═══════════════════════════════════════════
[PASS] validate_scene
[PASS] validate_polycount
[PASS] validate_textures
[PASS] validate_lights
Errors: 0
Warnings: 0
Result: PASS
```
## Validator Modules
### 1. Scene Structure (`validate_scene.gd`)
**What it checks:**
- Scene root is a `Node3D` type
- `WorldEnvironment` node present and named correctly
- `LightmapGI` node present and named correctly
- All 5 required groups assigned (`ct_spawn`, `t_spawn`, `buy_zone`, `bomb_site`, `cubemap_origin`)
- Node names follow PascalCase convention (no spaces)
- No duplicate required groups on different nodes
**Typical errors:**
```
✖ Missing required group: "ct_spawn"
✖ Missing WorldEnvironment — map needs a WorldEnvironment node
✖ Node name contains spaces: "My Room Section"
```
### 2. Polygon Count (`validate_polycount.gd`)
**What it checks:**
- Total triangle count across all `MeshInstance3D` nodes ≤ **50,000**
- Per-mesh triangle breakdown printed (helps identify heavy assets)
- Warns on individual meshes > **5,000** triangles
- Warns if no `MeshInstance3D` nodes found at all
**Typical errors:**
```
✖ Map exceeds 50,000 triangle budget: 68,420 total (36.8% over budget)
```
### 3. Texture Sizes (`validate_textures.gd`)
**What it checks:**
- All material textures ≤ **1024×1024**
- Checks `StandardMaterial3D`, `ORMMaterial3D`, and `ShaderMaterial` texture params
- Warns on textures > **512×512** (suggested for secondary surfaces)
- LightmapGI textures checked separately (baked textures can be larger)
**Typical errors:**
```
✖ Texture exceeds 1024×1024: floor_tiles.png (albedo) — 2048×
```
### 4. Lights & Lightmap (`validate_lights.gd`)
**What it checks:**
- Dynamic (non-baked) OmniLight3D + SpotLight3D ≤ **4**
- DirectionalLight3D counts toward dynamic budget if not baked
- LightmapGI present and baked (`light_data != null`)
- Lightmap quality, bounces, texel scale, max texture size
- WorldEnvironment assigned with Environment resource
- ReflectionProbe presence and `max_distance` coverage
- Warns on dynamic lights with shadow enabled (performance cost)
**Typical errors:**
```
✖ Too many dynamic lights: 6 (max 4)
✖ No LightmapGI node found
✖ No WorldEnvironment node found
```
## Passing the Validator
The goal is exit code 0: **PASS**, no errors, no warnings.
| Code | Action |
|------|--------|
| 0 (PASS) | Your map is ready to package |
| 1 (WARNINGS) | Fix suggestions before shipping (non-blocking) |
| 2 (ERRORS) | Fix all errors — map must pass before packaging |
> **CI gate rule:** Treat exit code 2 as a hard block. Exit code 1 is advisory
> but should be reviewed. Only exit code 0 means "ship it."
## Adding a Custom Validator Module
Advanced users can extend the validator:
1. Create `client/tools/validate_map/validate_<name>.gd` extending `RefCounted`
2. Implement `func validate(scene_root: Node, scene_path: String) -> Dictionary`
3. Return `{"pass": bool, "errors": [String], "warnings": [String]}`
4. Add a `_run_module("<name>", instance, scene_path)` call in
`validate_map.gd:_ready()`
## Next Steps
- [Packaging & Shipping](05-packaging-and-shipping.md) — ship your validated map
+264
View File
@@ -0,0 +1,264 @@
# Packaging & Shipping
Once your map is built, validated, and baked, it's time to publish it to the
world. Tactical Shooter uses the **PCK** (Godot Resource Pack) format — the map
is packaged as a `.pck` file, uploaded to a registry server, and clients
download it automatically when they join a server running that map.
## Pipeline Overview
```
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐
│ pack_map │ → │ .pck + .json │ → │ Map Registry │ → │ Client │
│ .gd │ │ files │ → │ Server │ → │ Downloader │
└──────────┘ └──────────────┘ └──────────────┘ └────────────┘
(editor (artifacts) (HTTP serving) (autoload)
tool)
```
## Step 1: Package the Map
The `pack_map.gd` script (at `scripts/map_packaging/pack_map.gd`) is a Godot
editor tool that exports a `.tscn` scene as a standalone `.pck` resource pack.
### Usage
From the **map template project** (not the main game project):
```bash
cd maps/my_first_map/
godot --headless --script /path/to/scripts/map_packaging/pack_map.gd \
--map=res://de_my_map.tscn
```
> Replace `/path/to/` with the actual path to the tactical-shooter repository
> root, or copy `pack_map.gd` into your map project's `scripts/` directory.
### What It Does
1. Loads the specified `.tscn` scene
2. Recursively collects all dependencies (materials, textures, meshes, etc.)
3. Exports them to `user://packed_maps/de_my_map.pck`
4. Writes a metadata file `user://packed_maps/de_my_map.json`
### Output Files
```
user://packed_maps/
├── de_my_map.pck # Binary resource pack (~110 MB for a typical map)
└── de_my_map.json # JSON metadata (machine-readable)
```
**Metadata example** (`de_my_map.json`):
```json
{
"map_name": "de_my_map",
"source_scene": "res://de_my_map.tscn",
"godot_version": {
"major": 4,
"minor": 2,
"string": "4.2"
},
"packed_at": "2026-06-25T14:30:00"
}
```
### Requirements
- The map scene must use **local resources** only (relative `res://` paths)
- External dependencies (game shared assets like weapon models, player
models) should stay in the base game — the `.pck` is **additive** content
- The `.pck` contains only the map's unique assets, not the entire game
### From the Editor
You can also run `pack_map.gd` from the Godot editor:
1. Open your map project in Godot
2. Open your map scene (`de_my_map.tscn`)
3. **Project → Tools → Pack Current Map** (if configured)
4. The script detects the active scene and exports it
## Step 2: Set Up the Map Registry Server
The registry server is a lightweight Python HTTP server
(`scripts/map_packaging/map_registry_server.py`) that:
- Serves `.pck` files for download
- Provides a JSON `/maps` endpoint listing available maps
- Auto-scans the maps directory every 5 seconds
- Computes and serves SHA-256 checksums for integrity verification
### Starting the Server
```bash
cd /path/to/tactical-shooter
# Default: port 8090, maps from ./packed_maps/
python3 scripts/map_packaging/map_registry_server.py
# Custom configuration
python3 scripts/map_packaging/map_registry_server.py \
--port 8080 \
--maps-dir /data/tactical-shooter-maps \
--verbose
```
**Environment variables:**
| Variable | Overrides | Default |
|----------|-----------|---------|
| `MAP_REGISTRY_PORT` | `--port` / `-p` | 8090 |
| `MAP_REGISTRY_MAPS` | `--maps-dir` / `-d` | `./packed_maps/` |
### API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `GET /` | — | Server info and endpoint documentation |
| `GET /maps` | — | JSON list of all available maps with metadata |
| `GET /maps/<name>.pck` | — | Download map `.pck` binary |
| `GET /maps/<name>.json` | — | Download per-map metadata |
| `OPTIONS /maps` | — | CORS preflight |
**Example responses:**
```
GET /maps
```
```json
{
"maps": [
{
"name": "de_dust2",
"size": 5432100,
"version": 1,
"description": "Classic competitive map",
"checksum_sha256": "a1b2c3d4...",
"packed_at": "2026-06-25T14:30:00"
}
],
"server_name": "Tactical Shooter Map Registry",
"server_version": "1.0.0",
"map_count": 1
}
```
```
GET /maps/de_dust2.pck
```
→ Returns the `.pck` binary file as an octet-stream download.
### Deployment Options
- **Local dev:** Run on the same machine as the game (default `127.0.0.1:8090`)
- **Server host:** Run on a public server alongside the master server
- **Docker:** Wrap in a Docker container for easy deployment
- **Systemd service:** Create a service file for production hosting
**Systemd service example:**
```ini
[Unit]
Description=Tactical Shooter Map Registry
After=network.target
[Service]
Type=simple
User=tactical
WorkingDirectory=/opt/tactical-shooter
ExecStart=/usr/bin/python3 scripts/map_packaging/map_registry_server.py \
--port 8090 --maps-dir /data/tactical-maps
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
## Step 3: Upload Your Map
1. Copy the `.pck` and `.json` files to the registry server's maps directory:
```bash
cp user://packed_maps/de_my_map.pck /data/tactical-maps/
cp user://packed_maps/de_my_map.json /data/tactical-maps/
```
2. The server auto-detects new files within 5 seconds
3. Verify via `curl http://your-server:8090/maps`
## Step 4: Client Downloads
The client uses `MapDownloader` (autoload at `client/scripts/map_downloader.gd`)
to fetch and load maps at runtime.
### How It Works
1. Player joins a server that specifies a map name
2. The client checks its local cache (`user://maps/manifest.json`)
3. If the map is not cached, `MapDownloader.fetch_map_list()` polls
the registry server to verify the map exists
4. `MapDownloader.download_map(name)` downloads the `.pck` to `user://maps/`
5. `ProjectSettings.load_resource_pack()` loads it into the game
6. The server changes to the new level
### Map Caching
```
user://maps/
├── de_dust2.pck # Cached map binary
├── de_inferno.pck # Another cached map
└── manifest.json # Cache index
```
**Cache behaviour:**
- Maps are cached to disk after download (survives restarts)
- The manifest tracks version numbers for staleness checks
- `MapDownloader.remove_map(name)` clears a specific map
- `MapDownloader.clear_cache()` removes all cached maps
### Configuration
The `MapDownloader` autoload can be configured in-game:
```gdscript
# Set the registry server URL (override default http://127.0.0.1:8090)
MapDownloader.registry_url = "http://maps.myserver.com:8090"
```
The URL can also be set via the `MAP_REGISTRY_URL` environment variable,
which is read in `_ready()`.
### Signals
```gdscript
# Register for map lifecycle events
MapDownloader.map_list_loaded.connect(_on_map_list)
MapDownloader.map_download_progress.connect(_on_download_progress)
MapDownloader.map_download_complete.connect(_on_download_done)
MapDownloader.map_loaded.connect(_on_map_loaded)
```
## Full Pipeline Example
```bash
# 1. Package from the template project
cd maps/my_first_map
godot --headless --script ../../scripts/map_packaging/pack_map.gd \
--map=res://de_my_map.tscn
# 2. Start registry server
cd ../..
python3 scripts/map_packaging/map_registry_server.py --port 8090 &
# 3. Deploy map
cp ~/.local/share/godot/app_userdata/MapTemplate/packed_maps/de_my_map.pck \
scripts/map_packaging/packed_maps/
cp ~/.local/share/godot/app_userdata/MapTemplate/packed_maps/de_my_map.json \
scripts/map_packaging/packed_maps/
# 4. Verify
curl -s http://localhost:8090/maps | python3 -m json.tool
# 5. In-game: connect to server and play the map
```
## Next Steps
- [FAQ & Troubleshooting](06-faq-and-troubleshooting.md) — common issues
@@ -0,0 +1,238 @@
# FAQ & Troubleshooting
Common questions, issues, and solutions encountered while building maps.
## Template Project
### Q: Can I use the template project without the main game repo?
Yes. The template project at `client/map_template/` is a **standalone Godot
project**. Copy it anywhere, open it in Godot, and start building. You only
need the main repo for packaging (`pack_map.gd`) and the registry server
(`map_registry_server.py`).
### Q: Why does Godot say "Import" instead of "Open"?
The template has its own `project.godot`, making it a separate project. Use
**Import** to add it to your Godot project list. You're not opening a scene
within an existing project.
### Q: How do I add the main game's assets to my map?
The template uses placeholder materials. For final maps:
1. Copy your map's `.tscn` into the main project under `client/maps/<name>/`
2. Open it from the main project's scenes tab
3. The main game's modular kit and PBR materials will be available
4. Assign materials to your CSG/MeshInstance3D nodes from the main project's
asset library
### Q: Can I use external models from Blender/Maya?
Yes. Export as `.glb` or `.obj` and import into Godot. Keep triangle counts
within budget (≤ 50K total, ≤ 5K per mesh recommended). Place imported meshes
as children of the map scene root.
## Geometry & CSG
### Q: My map has light leaks at the corners
Thin gaps between CSG blocks let light bleed through. Fix by overlapping CSG
edges by **0.1 units** instead of butting them flush.
```
Bad: Wall ends at x=10.0, floor ends at x=10.0 → gap
Good: Wall extends to x=10.1, floor ends at x=10.0 → overlap
```
### Q: Players can walk through walls
Make sure `use_collision = true` is set on all CSG geometry nodes. CSG nodes
don't have collision by default — you must enable it.
### Q: My map renders as a grey box / no materials
Placeholder materials are normal during construction. For the final look, you
need to:
1. Copy the map to the main project (see above)
2. Assign materials from the game's asset library
3. Re-bake LightmapGI after materials are assigned
### Q: CSG performance is bad in the editor
This is normal. CSG is re-calculated every time you move a node. For testing,
convert stable geometry to MeshInstance3D:
- Right-click CSG parent → **Convert CSG to Mesh**
- Keep a backup copy of the CSG hierarchy in another scene
### Q: How do I make a skybox?
Add a `WorldEnvironment` node and assign an Environment resource:
1. Set **Background** mode to **Sky**
2. Create a new **Sky** resource
3. Assign a **PanoramaSky** or **ProceduralSky** material
4. The sky appears behind your geometry
### Q: My map has no collision after CSG → Mesh conversion
When you convert CSG to MeshInstance3D, collision is not preserved. Add a
`CollisionShape3D` as a child of each MeshInstance3D, or use Godot's
**Mesh → Create Trimesh Collision** (right-click the MeshInstance3D).
## Lighting
### Q: My lightmap bake takes forever
Baking time depends on:
- **texel_scale** — lower = higher resolution = longer bake (try 2.0 for testing)
- **bounces** — fewer bounces = faster bake (use 1 for quick tests, 3 for final)
- **Map size** — larger maps bake longer
- **Light count** — more lights = more calculations
For quick iteration, use Low quality (0) during development and High quality (2)
for the final bake.
### Q: Baked lighting is too dark
- Increase `bounces` to 3 (more light bounce = brighter interiors)
- Add a low-intensity ambient fill (OmniLight3D with Static bake mode)
- Lower the `texel_scale` for more lightmap resolution
- Check that the DirectionalLight3D intensity is sufficient
### Q: Lightmap has visible seams
Seams appear where lightmap texels don't align. Fixes:
- Lower `texel_scale` to 1.0 (higher resolution lightmaps)
- Ensure CSG edges overlap (no gaps)
- Convert CSG to MeshInstance3D before final bake
### Q: Shadows look blocky or jagged
This is Godot's shadow mapping at default resolution. For competitive clarity:
- Keep `shadow_bias` at 0.1 (higher = softer shadows, fewer artefacts)
- Directional shadow `max_distance` at 2030 (shadows beyond this use lower
resolution)
### Q: I see a sphere artifact on my ReflectionProbe
The ReflectionProbe has a `probe_mode` property — if set to **Reflection Probe**
(sphere mode), it renders as a visible sphere in reflections. Change to **Box
Probe** for interior spaces, or disable reflection visibility via the `visible`
property on the probe's MeshInstance3D child (Godot 4.x).
### Q: Can I have a day/night cycle?
No. LightmapGI bakes lighting statically. Dynamic lighting is limited to
≤ 4 real-time lights. Day/night cycles are outside the current scope.
## Validation
### Q: The validator says "No MeshInstance3D nodes found"
Your map is using CSG nodes that haven't been converted to meshes. The
validator counts mesh triangles — CSG nodes are scanned as MeshInstance3D
after conversion. Either:
1. Convert CSG → Mesh for the area you want validated (right-click → Convert CSG to Mesh)
2. This warning is expected during early construction stages
### Q: The validator fails with "Scene cannot be instantiated"
The scene depends on resources that don't exist in the template project. This
usually happens when the map references game assets from the main project.
Either:
1. Run the validator from the **main game project** (`--path client` with the
main `client/project.godot`), or
2. Strip external dependencies — the map should be self-contained
### Q: The validator reports texture errors but my textures are 1024×1024
Check for textures with different aspect ratios. A 2048×512 texture fails
because the larger dimension (2048) exceeds 1024. Resize or tile.
### Q: Can I skip certain validator checks?
Not directly. Run the validator, fix errors, then fix warnings. If a check
produces a false positive, the `@tool` scripts are in
`client/tools/validate_map/` — you can edit the constants (e.g.
`MAX_TOTAL_TRIANGLES`) for custom pipelines.
## Packaging
### Q: The .pck file is very large
The `.pck` includes all dependencies of the scene. Large files mean:
- High-resolution textures (use ≤ 1024×1024 per the budget)
- High-poly meshes (stay under 50K triangles)
- Embedded audio or other large assets
Run the validator first to check budgets.
### Q: I get "Failed to load pack" in-game
Possible causes:
- `.pck` file is corrupted (check SHA-256 checksum)
- The map scene or a dependency path changed between packaging and loading
- The `.pck` was built with a different Godot version than the game
- The resource path in the `.pck` conflicts with a path in the base game
**Solution:** Re-package the map from the same Godot version the game uses.
Verify with `curl` that the download matches the server's checksum.
### Q: Can I update a map without changing the client version?
Yes. The `.pck` is additive content. Update the `.pck` on the registry server
and increment the `version` field in the `.json` metadata. The client will
re-download when it notices the version mismatch.
### Q: Can I host the registry server on a different port?
Yes. Use `--port <port>` or the `MAP_REGISTRY_PORT` environment variable.
The client's `MapDownloader` has a `registry_url` property you can set.
### Q: Can I load more than one .pck at a time?
Yes. `ProjectSettings.load_resource_pack()` is additive. Multiple `.pck` files
can be loaded simultaneously — each adds its resources to the global namespace.
### Q: The registry server returns 404 for my map
- Check that the `.pck` file is in the maps directory (default `./packed_maps/`)
- Check the file extension is `.pck` (lowercase)
- The server auto-scans every 5 seconds — wait or send SIGHUP to force a scan
- Verify by listing: `curl http://localhost:8090/maps`
## General
### Q: Can I make a hostage rescue map?
The SDK is gameplay-agnostic. The groups `ct_spawn`, `t_spawn`, `bomb_site`,
and `buy_zone` are the default set. You can add your own groups (e.g.,
`hostage`, `hostage_zone`) — the game logic discovers them at runtime.
Check the game's plugin API documentation for custom group support.
### Q: Can I use this SDK for a different game?
The SDK is designed for Tactical Shooter's Godot project structure. The
packaging and validation scripts are reusable for any Godot map-authoring
workflow, but the group conventions and gameplay nodes are game-specific.
### Q: The editor validator doesn't run
The `template_map.gd` script uses `@tool` and checks `Engine.is_editor_hint()`.
Make sure:
1. The script is attached to the root node of the scene
2. You're editing the scene in the Godot editor (not running the game)
3. The Output panel is visible (Window → Output)
### Q: How do I report a bug in the SDK?
Open an issue on the project's Gitea repository. Include:
- SDK version (commit hash)
- Godot version
- Steps to reproduce
- Log output (editor or CLI validator)
### Q: Can I contribute validator modules?
Yes. See [Validation — Adding a Custom Validator Module](04-validation.md#adding-a-custom-validator-module).
Submit a pull request with your module and updated documentation.