diff --git a/.agents/skills/defold-api-fetch/SKILL.md b/.agents/skills/defold-api-fetch/SKILL.md new file mode 100644 index 0000000..52ae00c --- /dev/null +++ b/.agents/skills/defold-api-fetch/SKILL.md @@ -0,0 +1,160 @@ +--- +name: defold-api-fetch +description: "Fetches Defold API documentation. Use when working with Defold engine APIs, looking up Lua/C++ functions, or needing API reference for game development." +--- + +# Defold API Reference + +Fetch documentation from the links below (the URLs point to plain Markdown files). + +## Lua APIs (Most Common) + +| Namespace | URL | +|-----------|-----| +| go (Game object) | https://defold.com/llms/apis/go-lua.md | +| gui (GUI) | https://defold.com/llms/apis/gui-lua.md | +| msg (Message) | https://defold.com/llms/apis/msg-lua.md | +| vmath (Vector math) | https://defold.com/llms/apis/vmath-lua.md | +| sprite (Sprite) | https://defold.com/llms/apis/sprite-lua.md | +| factory (Factory) | https://defold.com/llms/apis/factory-lua.md | +| collectionfactory (Collection factory) | https://defold.com/llms/apis/collectionfactory-lua.md | +| collectionproxy (Collection proxy) | https://defold.com/llms/apis/collectionproxy-lua.md | +| physics (Collision object) | https://defold.com/llms/apis/physics-lua.md | +| sound (Sound) | https://defold.com/llms/apis/sound-lua.md | +| timer (Timer) | https://defold.com/llms/apis/timer-lua.md | +| sys (System) | https://defold.com/llms/apis/sys-lua.md | +| resource (Resource) | https://defold.com/llms/apis/resource-lua.md | +| render (Render) | https://defold.com/llms/apis/render-lua.md | +| particlefx (Particle effects) | https://defold.com/llms/apis/particlefx-lua.md | +| label (Label) | https://defold.com/llms/apis/label-lua.md | +| tilemap (Tilemap) | https://defold.com/llms/apis/tilemap-lua.md | +| model (Model) | https://defold.com/llms/apis/model-lua.md | +| camera (Camera) | https://defold.com/llms/apis/camera-lua.md | +| window (Window) | https://defold.com/llms/apis/window-lua.md | +| buffer (Buffer) | https://defold.com/llms/apis/buffer-lua.md | +| graphics (Graphics) | https://defold.com/llms/apis/graphics-lua.md | +| image (Image) | https://defold.com/llms/apis/image-lua.md | +| json (JSON) | https://defold.com/llms/apis/json-lua.md | +| http (HTTP) | https://defold.com/llms/apis/http-lua.md | +| html5 (HTML5) | https://defold.com/llms/apis/html5-lua.md | +| crash (Crash) | https://defold.com/llms/apis/crash-lua.md | +| profiler (Profiler) | https://defold.com/llms/apis/profiler-lua.md | +| liveupdate (LiveUpdate) | https://defold.com/llms/apis/liveupdate-lua.md | +| builtins (Built-ins) | https://defold.com/llms/apis/builtins-lua.md | +| types (Types) | https://defold.com/llms/apis/types-lua.md | + +## Lua Standard Library + +| Namespace | URL | +|-----------|-----| +| base (Base) | https://defold.com/llms/apis/base-lua.md | +| coroutine (Coroutine) | https://defold.com/llms/apis/coroutine-lua.md | +| debug (Debug) | https://defold.com/llms/apis/debug-lua.md | +| io (Io) | https://defold.com/llms/apis/io-lua.md | +| math (Math) | https://defold.com/llms/apis/math-lua.md | +| os (Os) | https://defold.com/llms/apis/os-lua.md | +| package (Package) | https://defold.com/llms/apis/package-lua.md | +| string (String) | https://defold.com/llms/apis/string-lua.md | +| table (Table) | https://defold.com/llms/apis/table-lua.md | +| bit (BitOp) | https://defold.com/llms/apis/bit-lua.md | +| socket (LuaSocket) | https://defold.com/llms/apis/socket-lua.md | +| zlib (Zlib) | https://defold.com/llms/apis/zlib-lua.md | + +## Box2D Physics + +| Namespace | URL | +|-----------|-----| +| b2d | https://defold.com/llms/apis/b2d-lua.md | +| b2d.body | https://defold.com/llms/apis/b2d.body-lua.md | + +## Editor Scripting + +| Namespace | URL | +|-----------|-----| +| editor (Editor) | https://defold.com/llms/apis/editor-lua.md | + +## Extension APIs + +> **Note:** Extension APIs (`extension-*`) require the corresponding extension to be added as a dependency in `game.project`. To learn how to install and configure a specific extension, use the `defold-docs-fetch` skill to look up its documentation page. + +| Extension | URL | +|-----------|-----| +| crypt (Crypt) | https://defold.com/llms/apis/extension-crypt_crypt.md | +| safearea (SafeArea) | https://defold.com/llms/apis/extension-safearea_safearea.md | +| poki_sdk (Poki SDK) | https://defold.com/llms/apis/extension-poki-sdk_poki_sdk.md | +| websocket (WebSocket) | https://defold.com/llms/apis/extension-websocket_websocket.md | +| webview (WebView) | https://defold.com/llms/apis/extension-webview_webview.md | +| iap (In-App Purchase) | https://defold.com/llms/apis/extension-iap_iap.md | +| push (Push) | https://defold.com/llms/apis/extension-push_push.md | +| facebook (Facebook) | https://defold.com/llms/apis/extension-facebook_facebook.md | +| firebase (Firebase) | https://defold.com/llms/apis/extension-firebase_firebase.md | +| firebase (Firebase Analytics) | https://defold.com/llms/apis/extension-firebase-analytics_firebase.md | +| firebase (Firebase RemoteConfig) | https://defold.com/llms/apis/extension-firebase-remoteconfig_firebase.md | +| admob (AdMob) | https://defold.com/llms/apis/extension-admob_admob.md | +| ironsource (IronSource) | https://defold.com/llms/apis/extension-ironsource_ironsource.md | +| gpgs (Google Play Games) | https://defold.com/llms/apis/extension-gpgs_gpgs.md | +| steam (Steam) | https://defold.com/llms/apis/extension-steam_steam.md | +| review (Review) | https://defold.com/llms/apis/extension-review_review.md | +| iac (Inter-App Communication) | https://defold.com/llms/apis/extension-iac_iac.md | +| adinfo (Ad Info) | https://defold.com/llms/apis/extension-adinfo_adinfo.md | +| permissions (Permissions) | https://defold.com/llms/apis/extension-permissions_permissions.md | +| camera (Camera Extension) | https://defold.com/llms/apis/extension-camera_camera.md | +| networkinfo (Network Info) | https://defold.com/llms/apis/extension-network-info_networkinfo.md | +| spine (Spine) | https://defold.com/llms/apis/extension-spine_spine.md | +| spine gui (Spine GUI) | https://defold.com/llms/apis/extension-spine_gui.md | +| rive (Rive) | https://defold.com/llms/apis/extension-rive_rive.md | +| fmod (FMOD) | https://defold.com/llms/apis/extension-fmod_fmod.md | +| fontgen (Font Gen) | https://defold.com/llms/apis/extension-fontgen_fontgen.md | +| crazygames (CrazyGames) | https://defold.com/llms/apis/extension-crazygames_crazygames.md | +| pad (Play Asset Delivery) | https://defold.com/llms/apis/extension-pad_pad.md | +| instantapp (Google Play Instant) | https://defold.com/llms/apis/extension-googleplayinstant_instantapp.md | +| siwa (Sign in with Apple) | https://defold.com/llms/apis/extension-siwa_siwa.md | +| zendesk (Zendesk) | https://defold.com/llms/apis/extension-zendesk_zendesk.md | +| xsolla (Xsolla) | https://defold.com/llms/apis/extension-xsolla_shop.md | +| realtime (Photon Realtime) | https://defold.com/llms/apis/extension-photon-realtime_realtime.md | +| odin (ODIN Voice) | https://defold.com/llms/apis/extension-odin_odin.md | +| adpf (Android ADPF) | https://defold.com/llms/apis/extension-adpf_adpf.md | + +## C++ Native Extension APIs + +| Namespace | URL | +|-----------|-----| +| dmExtension (Extension) | https://defold.com/llms/apis/engine-extension-src-dmsdk-extension-extension-h.md | +| dmScript (Script) | https://defold.com/llms/apis/engine-script-src-dmsdk-script-script-h.md | +| dmBuffer (Buffer) | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-buffer-h.md | +| dmGameObject | https://defold.com/llms/apis/engine-gameobject-src-dmsdk-gameobject-gameobject-h.md | +| dmGraphics | https://defold.com/llms/apis/engine-graphics-src-dmsdk-graphics-graphics-h.md | +| dmGui | https://defold.com/llms/apis/engine-gui-src-dmsdk-gui-gui-h.md | +| dmRender | https://defold.com/llms/apis/engine-render-src-dmsdk-render-render-h.md | +| dmResource | https://defold.com/llms/apis/engine-resource-src-dmsdk-resource-resource-h.md | +| dmSound | https://defold.com/llms/apis/engine-sound-src-dmsdk-sound-sound-h.md | +| dmPhysics | https://defold.com/llms/apis/engine-physics-src-dmsdk-physics-physics-h.md | +| dmVMath | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-vmath-h.md | +| Hash | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-hash-h.md | +| Log | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-log-h.md | +| dmMutex | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-mutex-h.md | +| dmThread | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-thread-h.md | +| dmSocket | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-socket-h.md | +| dmHttpClient | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-http_client-h.md | +| dmConfigFile | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-configfile-h.md | +| dmCrypt | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-crypt-h.md | +| dmImage | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-image-h.md | +| dmMessage | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-message-h.md | +| dmProfile | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-profile-h.md | +| dmSys | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-sys-h.md | +| dmTime | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-time-h.md | +| dmTransform | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-transform-h.md | +| dmURI | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-uri-h.md | +| dmAndroid | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-android-h.md | +| Array | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-array-h.md | +| Hashtable | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-hashtable-h.md | +| dmMemory | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-memory-h.md | +| dmWebServer | https://defold.com/llms/apis/engine-dlib-src-dmsdk-dlib-webserver-h.md | + +## Usage + +When you need API documentation: + +1. Identify the namespace (e.g., `go`, `gui`, `vmath`) +2. Fetch the corresponding URL (all URLs are plain Markdown — fetch and read the raw content) +3. Example: fetch `https://defold.com/llms/apis/go-lua.md` diff --git a/.agents/skills/defold-assets-search/.gitignore b/.agents/skills/defold-assets-search/.gitignore new file mode 100644 index 0000000..ee88966 --- /dev/null +++ b/.agents/skills/defold-assets-search/.gitignore @@ -0,0 +1 @@ +assets/ diff --git a/.agents/skills/defold-assets-search/SKILL.md b/.agents/skills/defold-assets-search/SKILL.md new file mode 100644 index 0000000..4c56e52 --- /dev/null +++ b/.agents/skills/defold-assets-search/SKILL.md @@ -0,0 +1,92 @@ +--- +name: defold-assets-search +description: "Searches the Defold Asset Store for community libraries and extensions. Use BEFORE writing custom modules for pathfinding, RNG, UI, save/load, localization, tweening, input handling, etc. Helps find, compare, and install Defold dependencies." +--- + +# Defold Asset Store Search + +Search the Defold community Asset Store to find existing libraries instead of writing custom code. + +## When to use + +**ALWAYS search the Asset Store first** when a task requires functionality like: +- Pathfinding (A*, navigation) +- Random number generation +- UI components / GUI frameworks +- Save/load systems +- Localization / i18n +- Tweening / easing +- Input handling / gestures +- Camera control +- Screen management +- Event systems +- Dialogue / narrative systems +- Physics helpers (AABB, raycasting) +- Any other reusable game module + +## Procedure + +### Step 1: Generate and search the index + +The index file is `.agents/skills/defold-assets-search/assets/dependencies_index.tsv`. If it already exists and is less than 24 hours old, use it directly. Otherwise, regenerate it by running `python .agents/skills/defold-assets-search/scripts/generate_index.py` from the project root. The TSV columns: + +``` +id title author description tags stars api example_code manifest_url latest_zip +``` + +Use `Grep` to search the generated TSV file by keyword with `literal: true` for single keywords, or Rust-style regex alternation `keyword1|keyword2` (no backslashes before `|`) for multiple keywords. Search not only the user's exact terms but also synonyms and related words (e.g., RNG → random, i18n → localization, tween → easing, pathfinding → A*). Entries are sorted by stars (descending). + +### Step 2: Research candidates in depth + +For each candidate found in Step 1 (up to top 5 by stars): +1. **Skip `scene3d`** — this module is deprecated and should NOT be suggested. +2. If the `example_code` column has a URL, fetch the URL to study the library's README, usage examples, and features. +3. If the `api` column has a URL, fetch it too for API details. +4. Use the gathered information to understand what each library actually does and how it compares to alternatives. +5. If you need more details (all available versions, sub-dependencies, etc.), fetch the `manifest_url` from the index. + +### Step 3: Present candidates to the user + +After studying all candidates, show the user **2-3 best candidates** with: +- Title, author, stars count +- Brief description based on your research (not just the short TSV description) +- Key features / pros / cons +- Your recommendation and reasoning + +Ask the user which one to use, or recommend the best one. + +### Step 4: Install the dependency + +1. The `latest_zip` column contains the dependency URL to add to `game.project`. +2. Open `game.project` and add the URL to the `[project] dependencies` field (comma-separated list). +3. Run the `defold-project-setup` skill to download the dependency into `.deps/`. +4. Tell the user: **"In the Defold editor, go to Project → Fetch Libraries to sync."** +5. After the dependency is downloaded, scan its folder in `.deps/` for `.script_api` files and `.lua` modules to learn the full API. Use this to show usage examples or suggest how to apply the library in the context of the user's original request. + +## Community defaults + +These libraries are the de facto standard choices in the Defold community: + +| Need | Library | Author | +|------|---------|--------| +| GUI framework | **Druid** | Insality | +| Screen management | **Monarch** | Björn Ritzl | +| General-purpose utilities (especially `flow` and `broadcast`) | **Ludobits** | Björn Ritzl | +| OS/window functions | **DefOS** | Brian Kramer | +| Ready-made render script with shadows & post-processing | **Light and Shadows** | Igor Suntsev | +| High-quality 2D downscale (UI, sprites) | **Sharp Sprite** | Indiesoft LLC | + +**Camera** — a separate camera library is NOT needed. The built-in Defold camera component and its API already cover all common use cases. + +Prefer these over alternatives unless the user has a specific reason to choose otherwise. + +## Notable authors and libraries + +| Author | Known for | Libraries | +|--------|-----------|-----------| +| **Insality** | Best-in-class Lua modules with detailed API docs | Druid (UI), Panthera (animation), Defold-Event, Defold-Saver, Defold-Tweener, Defold-Lang, Defold-Log, Defold-Token, Defold-Quest, Decore | +| **Björn Ritzl** | Defold core team, prolific contributor | Monarch (screens), Orthographic (camera), Gooey (GUI), Rich Text, Defold-Input, DefTest | +| **Selim Anaç** | High-performance native extensions | A* Pathfinding, DAABBCC (AABB tree), PCG Random, Graph Pathfinder, Tile Raycast | +| **Brian Kramer** | Practical game utilities | DefOS, DefSave, DefMath, DefGlot, DefBlend | +| **Roman Silin** | 3D game tools | Illumination, Kinematic Walker, Operator (camera), Narrator (Ink), TrenchFold | +| **Indiesoft LLC** | Visual effects and platform tools | Hyper Trails, Sharp Sprite, YaGames, ResZip, Dissolve FX, SplitMix64 | diff --git a/.agents/skills/defold-assets-search/scripts/generate_index.py b/.agents/skills/defold-assets-search/scripts/generate_index.py new file mode 100644 index 0000000..641bd9a --- /dev/null +++ b/.agents/skills/defold-assets-search/scripts/generate_index.py @@ -0,0 +1,69 @@ +"""Download Defold Asset Store JSON and generate a compact TSV index. + +Usage: + python .agents/skills/defold-assets-search/scripts/generate_index.py + +Output: + .agents/skills/defold-assets-search/assets/dependencies_index.tsv +""" + +import json +import os +import urllib.request + +SOURCE_URL = "https://insality.github.io/asset-store/dependencies_store.json" +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +OUTPUT_DIR = os.path.join(SCRIPT_DIR, os.pardir, "assets") +OUTPUT_FILE = os.path.join(OUTPUT_DIR, "dependencies_index.tsv") + + +def main() -> None: + print(f"Downloading {SOURCE_URL} ...") + with urllib.request.urlopen(SOURCE_URL) as resp: + raw = json.loads(resp.read().decode("utf-8")) + + # The JSON has an "items" key containing the list + data: list[dict] = raw.get("items", raw) if isinstance(raw, dict) else raw + + os.makedirs(OUTPUT_DIR, exist_ok=True) + + # Exclude unlisted entries + entries = [e for e in data if not e.get("unlisted", False)] + + # Sort by stars descending (None → 0) + entries.sort(key=lambda e: e.get("stars") or 0, reverse=True) + + header = "id\ttitle\tauthor\tdescription\ttags\tstars\tapi\texample_code\tmanifest_url\tlatest_zip" + lines: list[str] = [header] + + for e in entries: + latest_zip = "" + content = e.get("content") or [] + if content: + latest_zip = content[-1] + + tags_str = ", ".join(e.get("tags") or []) + desc = (e.get("description") or "").replace("\t", " ").replace("\n", " ") + + line = "\t".join([ + e.get("id") or "", + e.get("title") or "", + e.get("author") or "", + desc, + tags_str, + str(e.get("stars") or 0), + e.get("api") or "", + e.get("example_code") or "", + e.get("manifest_url") or "", + latest_zip, + ]) + lines.append(line) + + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + + print(f"Generated {OUTPUT_FILE} with {len(entries)} entries.") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/defold-docs-fetch/SKILL.md b/.agents/skills/defold-docs-fetch/SKILL.md new file mode 100644 index 0000000..f512441 --- /dev/null +++ b/.agents/skills/defold-docs-fetch/SKILL.md @@ -0,0 +1,287 @@ +--- +name: defold-docs-fetch +description: "Fetches Defold manuals and documentation. Use when looking up how Defold features work, understanding concepts, components, workflows, platform setup, or needing guidance beyond API reference." +--- + +# Defold Documentation Reference + +Fetch documentation from the links below (the URLs point to plain Markdown files). + +## Getting Started + +| Topic | URL | +|-------|-----| +| Introduction | https://defold.com/llms/manuals/introduction.md | +| Installing Defold | https://defold.com/llms/manuals/install.md | +| Glossary | https://defold.com/llms/manuals/glossary.md | +| Defold for Flash users | https://defold.com/llms/manuals/flash.md | +| Getting help | https://defold.com/llms/manuals/getting-help.md | + +## Project Setup + +| Topic | URL | +|-------|-----| +| Creating a project | https://defold.com/llms/manuals/project-setup.md | +| Project settings | https://defold.com/llms/manuals/project-settings.md | +| Sharing projects (Libraries) | https://defold.com/llms/manuals/libraries.md | +| Ignoring files | https://defold.com/llms/manuals/project-defignore.md | + +## Editor + +| Topic | URL | +|-------|-----| +| Overview | https://defold.com/llms/manuals/editor.md | +| Code editor | https://defold.com/llms/manuals/writing-code.md | +| Custom project templates | https://defold.com/llms/manuals/editor-templates.md | +| Editor scripts | https://defold.com/llms/manuals/editor-scripts.md | +| Editor scripts: UI | https://defold.com/llms/manuals/editor-scripts-ui.md | +| Debugger | https://defold.com/llms/manuals/debugging-game-logic.md | +| Preferences | https://defold.com/llms/manuals/editor-preferences.md | +| Refactoring | https://defold.com/llms/manuals/refactoring.md | +| Styling | https://defold.com/llms/manuals/editor-styling.md | + +## Core Concepts + +| Topic | URL | +|-------|-----| +| Building blocks | https://defold.com/llms/manuals/building-blocks.md | +| Addressing | https://defold.com/llms/manuals/addressing.md | +| Message passing | https://defold.com/llms/manuals/message-passing.md | +| Application lifecycle | https://defold.com/llms/manuals/application-lifecycle.md | + +## Assets and Resources + +| Topic | URL | +|-------|-----| +| Importing assets | https://defold.com/llms/manuals/importing-assets.md | +| Caching assets | https://defold.com/llms/manuals/caching-assets.md | +| Importing 2D graphics | https://defold.com/llms/manuals/importing-graphics.md | +| Importing 3D models | https://defold.com/llms/manuals/importing-models.md | +| Adapting to screen sizes | https://defold.com/llms/manuals/adapting-graphics-to-screen-size.md | +| Live update | https://defold.com/llms/manuals/live-update.md | +| Atlas | https://defold.com/llms/manuals/atlas.md | +| Buffer | https://defold.com/llms/manuals/buffer.md | +| Font | https://defold.com/llms/manuals/font.md | +| Resource management | https://defold.com/llms/manuals/resource.md | +| Tile source | https://defold.com/llms/manuals/tilesource.md | +| Texture filtering | https://defold.com/llms/manuals/texture-filtering.md | +| Texture compression | https://defold.com/llms/manuals/texture-profiles.md | + +## Animations + +| Topic | URL | +|-------|-----| +| Overview | https://defold.com/llms/manuals/animation.md | +| Flipbook Animation | https://defold.com/llms/manuals/flipbook-animation.md | +| Model animation | https://defold.com/llms/manuals/model-animation.md | +| Property animation (tweens) | https://defold.com/llms/manuals/property-animation.md | + +## Components + +| Topic | URL | +|-------|-----| +| Overview | https://defold.com/llms/manuals/components.md | +| Collection factory | https://defold.com/llms/manuals/collection-factory.md | +| Collection proxy | https://defold.com/llms/manuals/collection-proxy.md | +| Collision object | https://defold.com/llms/manuals/physics-objects.md | +| Camera | https://defold.com/llms/manuals/camera.md | +| Factory | https://defold.com/llms/manuals/factory.md | +| Label | https://defold.com/llms/manuals/label.md | +| Mesh | https://defold.com/llms/manuals/mesh.md | +| Model | https://defold.com/llms/manuals/model.md | +| Particle FX | https://defold.com/llms/manuals/particlefx.md | +| Sound | https://defold.com/llms/manuals/sound.md | +| Sprite | https://defold.com/llms/manuals/sprite.md | +| Tilemap | https://defold.com/llms/manuals/tilemap.md | + +## GUI + +| Topic | URL | +|-------|-----| +| GUI overview | https://defold.com/llms/manuals/gui.md | +| Box nodes | https://defold.com/llms/manuals/gui-box.md | +| Text nodes | https://defold.com/llms/manuals/gui-text.md | +| Pie nodes | https://defold.com/llms/manuals/gui-pie.md | +| ParticleFX nodes | https://defold.com/llms/manuals/gui-particlefx.md | +| Template nodes | https://defold.com/llms/manuals/gui-template.md | +| Scripts | https://defold.com/llms/manuals/gui-script.md | +| Clipping | https://defold.com/llms/manuals/gui-clipping.md | +| Layouts | https://defold.com/llms/manuals/gui-layouts.md | + +## Physics + +| Topic | URL | +|-------|-----| +| Physics overview | https://defold.com/llms/manuals/physics.md | +| Collision objects | https://defold.com/llms/manuals/physics-objects.md | +| Collision shapes | https://defold.com/llms/manuals/physics-shapes.md | +| Collision groups | https://defold.com/llms/manuals/physics-groups.md | +| Collision messages | https://defold.com/llms/manuals/physics-messages.md | +| Collision events listener | https://defold.com/llms/manuals/physics-events.md | +| Resolving collisions | https://defold.com/llms/manuals/physics-resolving-collisions.md | +| Ray casts | https://defold.com/llms/manuals/physics-ray-casts.md | +| Joints and constraints | https://defold.com/llms/manuals/physics-joints.md | + +## Sound + +| Topic | URL | +|-------|-----| +| Sound | https://defold.com/llms/manuals/sound.md | +| Sound Streaming | https://defold.com/llms/manuals/sound-streaming.md | + +## Input + +| Topic | URL | +|-------|-----| +| Overview | https://defold.com/llms/manuals/input.md | +| Key and text input | https://defold.com/llms/manuals/input-key-and-text.md | +| Mouse and touch | https://defold.com/llms/manuals/input-mouse-and-touch.md | +| Gamepads | https://defold.com/llms/manuals/input-gamepads.md | + +## Game Logic + +| Topic | URL | +|-------|-----| +| Scripts | https://defold.com/llms/manuals/script.md | +| Properties | https://defold.com/llms/manuals/properties.md | +| Script properties | https://defold.com/llms/manuals/script-properties.md | +| Lua in Defold | https://defold.com/llms/manuals/lua.md | +| Source code obfuscation | https://defold.com/llms/manuals/application-security.md | +| Modules | https://defold.com/llms/manuals/modules.md | +| Debugging | https://defold.com/llms/manuals/debugging-game-logic.md | +| Writing code | https://defold.com/llms/manuals/writing-code.md | + +## Files + +| Topic | URL | +|-------|-----| +| Working with files | https://defold.com/llms/manuals/file-access.md | + +## Network Connections + +| Topic | URL | +|-------|-----| +| Overview | https://defold.com/llms/manuals/networking.md | +| HTTP Requests | https://defold.com/llms/manuals/http-requests.md | +| Socket connections | https://defold.com/llms/manuals/socket-connections.md | +| Online services | https://defold.com/llms/manuals/online-services.md | + +## Rendering + +| Topic | URL | +|-------|-----| +| Render | https://defold.com/llms/manuals/render.md | +| Material | https://defold.com/llms/manuals/material.md | +| Compute | https://defold.com/llms/manuals/compute.md | +| Shader | https://defold.com/llms/manuals/shader.md | +| Texture filtering | https://defold.com/llms/manuals/texture-filtering.md | +| Physically Based Rendering | https://defold.com/llms/manuals/physically-based-rendering.md | + +## Workflow + +| Topic | URL | +|-------|-----| +| Application security | https://defold.com/llms/manuals/application-security.md | +| Bundling an application | https://defold.com/llms/manuals/bundling.md | +| Caching assets | https://defold.com/llms/manuals/caching-assets.md | +| Command line tools (bob) | https://defold.com/llms/manuals/bob.md | +| Hot reloading | https://defold.com/llms/manuals/hot-reload.md | +| Porting guidelines | https://defold.com/llms/manuals/porting-guidelines.md | +| Refactoring | https://defold.com/llms/manuals/refactoring.md | +| The mobile dev app | https://defold.com/llms/manuals/dev-app.md | +| Version control | https://defold.com/llms/manuals/version-control.md | +| Writing code | https://defold.com/llms/manuals/writing-code.md | +| Working offline | https://defold.com/llms/manuals/working-offline.md | + +## Debugging + +| Topic | URL | +|-------|-----| +| Debugging game logic | https://defold.com/llms/manuals/debugging-game-logic.md | +| Debugging native code | https://defold.com/llms/manuals/debugging-native-code.md | +| Debugging native code on Android | https://defold.com/llms/manuals/debugging-native-code-android.md | +| Debugging native code on iOS | https://defold.com/llms/manuals/debugging-native-code-ios.md | +| Reading game and system logs | https://defold.com/llms/manuals/debugging-game-and-system-logs.md | + +## Optimization + +| Topic | URL | +|-------|-----| +| Optimizing an application | https://defold.com/llms/manuals/optimization.md | +| Optimize game size | https://defold.com/llms/manuals/optimization-size.md | +| Optimize runtime performance | https://defold.com/llms/manuals/optimization-speed.md | +| Optimize battery usage | https://defold.com/llms/manuals/optimization-battery.md | +| Optimize memory usage | https://defold.com/llms/manuals/optimization-memory.md | +| Profiling | https://defold.com/llms/manuals/profiling.md | + +## Monetization + +| Topic | URL | +|-------|-----| +| Ads | https://defold.com/llms/manuals/ads.md | + +## Platforms + +### Android + +| Topic | URL | +|-------|-----| +| Introduction | https://defold.com/llms/manuals/android.md | +| Inter-app communication | https://defold.com/llms/manuals/iac.md | +| The mobile dev app | https://defold.com/llms/manuals/dev-app.md | + +### iOS + +| Topic | URL | +|-------|-----| +| Introduction | https://defold.com/llms/manuals/ios.md | +| Inter-app communication | https://defold.com/llms/manuals/iac.md | +| The mobile dev app | https://defold.com/llms/manuals/dev-app.md | + +### Consoles + +| Topic | URL | +|-------|-----| +| Nintendo Switch | https://defold.com/llms/manuals/nintendo-switch.md | +| PlayStation | https://defold.com/llms/manuals/sony-playstation.md | +| Microsoft Xbox | https://defold.com/llms/manuals/microsoft-xbox.md | + +### HTML5 + +| Topic | URL | +|-------|-----| +| Introduction | https://defold.com/llms/manuals/html5.md | + +### Desktop + +| Topic | URL | +|-------|-----| +| Linux | https://defold.com/llms/manuals/linux.md | +| macOS | https://defold.com/llms/manuals/macos.md | +| Windows | https://defold.com/llms/manuals/windows.md | + +## Engine Extensions + +| Topic | URL | +|-------|-----| +| Introduction | https://defold.com/llms/manuals/extensions.md | +| Defold SDK | https://defold.com/llms/manuals/extensions-defold-sdk.md | +| Gradle dependencies | https://defold.com/llms/manuals/extensions-gradle.md | +| Cocoapod dependencies | https://defold.com/llms/manuals/extensions-cocoapods.md | +| Adding auto-complete definition | https://defold.com/llms/manuals/extensions-script-api.md | +| Best Practices | https://defold.com/llms/manuals/extensions-best-practices.md | +| Debugging | https://defold.com/llms/manuals/debugging-native-code.md | +| Extension Manifests | https://defold.com/llms/manuals/extensions-ext-manifests.md | +| App Manifests | https://defold.com/llms/manuals/app-manifest.md | +| Manifest Merging | https://defold.com/llms/manuals/extensions-manifest-merge-tool.md | +| Setup local build server | https://defold.com/llms/manuals/extender-local-setup.md | +| Available Docker images | https://defold.com/llms/manuals/extender-docker-images.md | + +## Usage + +When you need documentation on a Defold feature or concept: + +1. Identify the topic (e.g., "collection proxy", "input handling", "physics") +2. Find the matching entry in the tables above +3. Fetch the corresponding URL (all URLs are plain Markdown — fetch and read the raw content) +4. Example: fetch `https://defold.com/llms/manuals/collection-proxy.md` diff --git a/.agents/skills/defold-examples-fetch/SKILL.md b/.agents/skills/defold-examples-fetch/SKILL.md new file mode 100644 index 0000000..65a429f --- /dev/null +++ b/.agents/skills/defold-examples-fetch/SKILL.md @@ -0,0 +1,221 @@ +--- +name: defold-examples-fetch +description: "Fetches Defold code examples by topic. Use when looking for practical implementation patterns, sample code, or how to do something specific in Defold." +--- + +# Defold Examples Reference + +Fetch example code from the links below (the URLs point to plain Markdown files). + +## Animation + +| Example | URL | +|---------|-----| +| Animation State Machine | https://defold.com/llms/examples/animation/animation_states.md | +| Cursor animation | https://defold.com/llms/examples/animation/cursor.md | +| Easing functions (tweens) | https://defold.com/llms/examples/animation/easing.md | +| Euler Rotation | https://defold.com/llms/examples/animation/euler_rotation.md | +| Flipbook animation | https://defold.com/llms/examples/animation/flipbook.md | +| Spine animation | https://defold.com/llms/examples/animation/spine.md | +| Spinner animation | https://defold.com/llms/examples/animation/spinner.md | +| Tween animation | https://defold.com/llms/examples/animation/basic_tween.md | +| Tween animations chain | https://defold.com/llms/examples/animation/chained_tween.md | + +## Basics + +| Example | URL | +|---------|-----| +| Message passing | https://defold.com/llms/examples/basics/message_passing.md | +| Parent/child | https://defold.com/llms/examples/basics/parent_child.md | +| Random numbers | https://defold.com/llms/examples/basics/random_numbers.md | +| Z-order | https://defold.com/llms/examples/basics/z_order.md | + +## Collection + +| Example | URL | +|---------|-----| +| Proxy | https://defold.com/llms/examples/collection/proxy.md | +| Splash | https://defold.com/llms/examples/collection/splash.md | +| Time-step | https://defold.com/llms/examples/collection/timestep.md | + +## Debug + +| Example | URL | +|---------|-----| +| Physics debug | https://defold.com/llms/examples/debug/physics.md | +| Visual profiler | https://defold.com/llms/examples/debug/profile.md | + +## Factory + +| Example | URL | +|---------|-----| +| Dynamic factories | https://defold.com/llms/examples/factory/dynamic.md | +| Shoot bullets | https://defold.com/llms/examples/factory/bullets.md | +| Spawn enemies with central management | https://defold.com/llms/examples/factory/spawn_manager.md | +| Spawn enemies with script properties | https://defold.com/llms/examples/factory/spawn_properties.md | +| Spawn game object | https://defold.com/llms/examples/factory/basic.md | + +## File + +| Example | URL | +|---------|-----| +| Load JSON data | https://defold.com/llms/examples/file/json_load.md | +| Save and Load | https://defold.com/llms/examples/file/sys_save_load.md | + +## GUI + +| Example | URL | +|---------|-----| +| Button | https://defold.com/llms/examples/gui/button.md | +| Drag | https://defold.com/llms/examples/gui/drag.md | +| Get and set a gui font resource | https://defold.com/llms/examples/gui/get_set_font.md | +| Get and set a gui material resource | https://defold.com/llms/examples/gui/get_set_material.md | +| Get and set a gui texture resource | https://defold.com/llms/examples/gui/get_set_texture.md | +| GUI color | https://defold.com/llms/examples/gui/color.md | +| GUI progress indicators | https://defold.com/llms/examples/gui/progress.md | +| Health Bar | https://defold.com/llms/examples/gui/healthbar.md | +| Layouts | https://defold.com/llms/examples/gui/layouts.md | +| Load texture | https://defold.com/llms/examples/gui/load_texture.md | +| Pointer over | https://defold.com/llms/examples/gui/pointer_over.md | +| Slice-9 | https://defold.com/llms/examples/gui/slice9.md | +| Stencil | https://defold.com/llms/examples/gui/stencil.md | + +## Input + +| Example | URL | +|---------|-----| +| 8 ways movement | https://defold.com/llms/examples/input/move.md | +| Down duration | https://defold.com/llms/examples/input/down_duration.md | +| Entity Picking | https://defold.com/llms/examples/input/entity_picking.md | +| Mouse and touch events | https://defold.com/llms/examples/input/mouse_and_touch.md | +| Text input | https://defold.com/llms/examples/input/text.md | + +## Material + +| Example | URL | +|---------|-----| +| Custom Sprite | https://defold.com/llms/examples/material/custom_sprite.md | +| Noise shader | https://defold.com/llms/examples/material/noise.md | +| Repeating Background | https://defold.com/llms/examples/material/repeating_background.md | +| Screenspace | https://defold.com/llms/examples/material/screenspace.md | +| Sprite local UV | https://defold.com/llms/examples/material/sprite_local_uv.md | +| Sprite Vertex Color Attribute | https://defold.com/llms/examples/material/vertexcolor.md | +| Unlit | https://defold.com/llms/examples/material/unlit.md | +| UV Gradient | https://defold.com/llms/examples/material/uvgradient.md | + +## Mesh + +| Example | URL | +|---------|-----| +| Mesh (triangle) | https://defold.com/llms/examples/mesh/triangle.md | +| Textured Mesh | https://defold.com/llms/examples/mesh/textured.md | + +## Model + +| Example | URL | +|---------|-----| +| AABB | https://defold.com/llms/examples/model/aabb.md | +| Character | https://defold.com/llms/examples/model/character.md | +| Cubemap Reflection | https://defold.com/llms/examples/model/cubemap.md | +| GLTF | https://defold.com/llms/examples/model/gltf.md | +| GPU Skinning | https://defold.com/llms/examples/model/skinning.md | +| Model Vertex Color | https://defold.com/llms/examples/model/modelvertexcolor.md | +| Skybox | https://defold.com/llms/examples/model/skybox.md | + +## Movement + +| Example | URL | +|---------|-----| +| First-person 3D camera and movement | https://defold.com/llms/examples/movement/3d_fps.md | +| Follow input | https://defold.com/llms/examples/movement/follow.md | +| Look at | https://defold.com/llms/examples/movement/look_at.md | +| Look rotation | https://defold.com/llms/examples/movement/look_rotation.md | +| Move forward | https://defold.com/llms/examples/movement/move_forward.md | +| Move to target | https://defold.com/llms/examples/movement/move_to.md | +| Movement speed | https://defold.com/llms/examples/movement/movement_speed.md | +| Moving game object | https://defold.com/llms/examples/movement/simple_move.md | + +## Particles + +| Example | URL | +|---------|-----| +| Modifiers | https://defold.com/llms/examples/particles/modifiers.md | +| Particle effect | https://defold.com/llms/examples/particles/particlefx.md | +| Particle Effect Emission Space | https://defold.com/llms/examples/particles/particlefx_emission_space.md | +| Confetti | https://defold.com/llms/examples/particles/confetti.md | +| Fire and smoke | https://defold.com/llms/examples/particles/fire_and_smoke.md | +| Fireworks | https://defold.com/llms/examples/particles/fireworks.md | + +## Physics + +| Example | URL | +|---------|-----| +| Dynamic physics | https://defold.com/llms/examples/physics/dynamic.md | +| Hinge joint physics | https://defold.com/llms/examples/physics/hinge_joint.md | +| Kinematic physics | https://defold.com/llms/examples/physics/kinematic.md | +| Knockback | https://defold.com/llms/examples/physics/knockback.md | +| Pendulum physics | https://defold.com/llms/examples/physics/pendulum.md | +| Raycast | https://defold.com/llms/examples/physics/raycast.md | +| Trigger | https://defold.com/llms/examples/physics/trigger.md | + +## Render + +| Example | URL | +|---------|-----| +| Camera | https://defold.com/llms/examples/render/camera.md | +| Orbit Camera | https://defold.com/llms/examples/render/orbit_camera.md | +| Post-processing | https://defold.com/llms/examples/render/post_processing.md | +| Screen to World | https://defold.com/llms/examples/render/screen_to_world.md | +| World to Screen | https://defold.com/llms/examples/render/world_to_screen.md | + +## Resource + +| Example | URL | +|---------|-----| +| Create atlas | https://defold.com/llms/examples/resource/create_atlas.md | +| Modify atlas | https://defold.com/llms/examples/resource/modify_atlas.md | + +## Sound + +| Example | URL | +|---------|-----| +| Fade In-Out | https://defold.com/llms/examples/sound/fade_in_out.md | +| Get and set sound | https://defold.com/llms/examples/sound/get_set_sound.md | +| Music | https://defold.com/llms/examples/sound/music.md | +| Panning | https://defold.com/llms/examples/sound/panning.md | + +## Sprite + +| Example | URL | +|---------|-----| +| Bunnymark | https://defold.com/llms/examples/sprite/bunnymark.md | +| Change sprite image | https://defold.com/llms/examples/sprite/changeimage.md | +| Flip | https://defold.com/llms/examples/sprite/flip.md | +| Multiple Sprite Samplers | https://defold.com/llms/examples/sprite/samplers.md | +| Sprite cursor | https://defold.com/llms/examples/sprite/cursor.md | +| Sprite size | https://defold.com/llms/examples/sprite/size.md | +| Sprite tint | https://defold.com/llms/examples/sprite/tint.md | + +## Tilemap + +| Example | URL | +|---------|-----| +| Get and set tiles | https://defold.com/llms/examples/tilemap/get_set_tile.md | +| Tilemap collisions | https://defold.com/llms/examples/tilemap/collisions.md | + +## Timer + +| Example | URL | +|---------|-----| +| Cancel timer | https://defold.com/llms/examples/timer/cancel_timer.md | +| Repeating timer | https://defold.com/llms/examples/timer/repeating_timer.md | +| Trigger timer | https://defold.com/llms/examples/timer/trigger_timer.md | + +## Usage + +When you need a practical example of how to implement something in Defold: + +1. Identify the topic (e.g., "factory spawning", "physics raycasts", "GUI buttons") +2. Find the matching entry in the tables above +3. Fetch the corresponding URL (all URLs are plain Markdown — fetch and read the raw content) +4. Example: fetch `https://defold.com/llms/examples/factory/basic.md` diff --git a/.agents/skills/defold-native-extension-editing/SKILL.md b/.agents/skills/defold-native-extension-editing/SKILL.md new file mode 100644 index 0000000..95f3b26 --- /dev/null +++ b/.agents/skills/defold-native-extension-editing/SKILL.md @@ -0,0 +1,275 @@ +--- +name: defold-native-extension-editing +description: "Defold native extension development. Use when creating or editing C/C++ (.c, .cpp, .h, .hpp), JavaScript (.js), or manifest files in native extension directories (src/, include/, lib/, api/)." +--- + +# Defold Native Extension Structure + +A native extension is a folder containing `ext.manifest` and native code for extending the Defold engine. + +## Required structure + +``` +my_extension/ +├── ext.manifest # Extension manifest (YAML) +├── src/ # Source code (C/C++/ObjC/JS) +│ ├── extension.cpp # Main extension entry point +│ └── ... +├── include/ # Public headers (optional) +├── lib/ # Platform libraries (optional) +│ ├── x86_64-win32/ +│ ├── x86_64-linux/ +│ ├── arm64-ios/ +│ └── ... +├── api/ # Script API definitions (optional) +│ └── my_extension.script_api +└── res/ # Platform resources (optional) + └── android/ +``` + +## ext.manifest format + +```yaml +name: "MyExtension" +platforms: + x86_64-win32: + context: + defines: ["MY_DEFINE"] + libs: ["user32"] + arm64-ios: + context: + frameworks: ["UIKit"] +``` + +## Extension entry point (C++) + +```cpp +#define EXTENSION_NAME MyExtension +#define LIB_NAME "MyExtension" +#define MODULE_NAME "myextension" + +#include + +static int MyFunction(lua_State* L) +{ + // Implementation + return 0; +} + +static const luaL_reg Module_methods[] = +{ + {"my_function", MyFunction}, + {0, 0} +}; + +static void LuaInit(lua_State* L) +{ + int top = lua_gettop(L); + luaL_register(L, MODULE_NAME, Module_methods); + lua_pop(L, 1); + assert(top == lua_gettop(L)); +} + +static dmExtension::Result AppInitializeMyExtension(dmExtension::AppParams* params) +{ + return dmExtension::RESULT_OK; +} + +static dmExtension::Result InitializeMyExtension(dmExtension::Params* params) +{ + LuaInit(params->m_L); + return dmExtension::RESULT_OK; +} + +static dmExtension::Result FinalizeMyExtension(dmExtension::Params* params) +{ + return dmExtension::RESULT_OK; +} + +DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, AppInitializeMyExtension, 0, InitializeMyExtension, 0, 0, FinalizeMyExtension) +``` + +## HTML5/JavaScript extension + +For web platform, use `.js` files in `src/`: + +```javascript +var MyExtension = { + MyFunction: function() { + // Implementation + } +}; +mergeInto(LibraryManager.library, MyExtension); +``` + +## Platform library paths (lib/) + +- `x86_64-win32/` - Windows 64-bit +- `x86-win32/` - Windows 32-bit +- `x86_64-linux/` - Linux 64-bit +- `arm64-osx/` - macOS ARM +- `x86_64-osx/` - macOS Intel +- `arm64-ios/` - iOS ARM64 +- `armv7-android/`, `arm64-android/` - Android +- `js-web/`, `wasm-web/` - HTML5 + +## Prefer dmSDK over Lua API calls + +**Always use direct C++ dmSDK functions instead of calling Lua API wrappers** (e.g. `lua_getglobal(L, "go")` + `lua_call`). The dmSDK provides efficient C++ equivalents for most Lua game object operations. + +### Key dmSDK patterns + +**Getting game object instances from Lua:** +```cpp +// Get the calling script's game object instance +dmGameObject::HInstance caller = dmScript::CheckGOInstance(L); + +// Get the collection from any instance +dmGameObject::HCollection collection = dmGameObject::GetCollection(caller); + +// Resolve a hash identifier (from Lua stack) to an instance +dmhash_t id = dmScript::CheckHash(L, index); +dmGameObject::HInstance target = dmGameObject::GetInstanceFromIdentifier(collection, id); +``` + +**Manipulating game objects directly in C++:** +```cpp +// Position (uses dmVMath::Point3) +dmVMath::Point3 pos = dmGameObject::GetPosition(instance); +dmGameObject::SetPosition(instance, dmVMath::Point3(x, y, z)); + +// Rotation (uses dmVMath::Quat) +dmVMath::Quat rot = dmGameObject::GetRotation(instance); +dmGameObject::SetRotation(instance, rot); + +// Scale +float scale = dmGameObject::GetUniformScale(instance); +dmGameObject::SetScale(instance, scale); +dmGameObject::SetScale(instance, dmVMath::Vector3(sx, sy, sz)); +``` + +**Math types — create once, reuse in loops:** +```cpp +dmVMath::Point3 pos(0.0f, 0.0f, 0.0f); +for (int i = 0; i < count; ++i) +{ + pos.setX(computed_x); + pos.setY(computed_y); + dmGameObject::SetPosition(instance, pos); +} +``` + +**Extracting Lua types via dmScript:** +```cpp +dmVMath::Vector3* v = dmScript::CheckVector3(L, index); +dmVMath::Vector4* v4 = dmScript::CheckVector4(L, index); +dmVMath::Quat* q = dmScript::CheckQuat(L, index); +dmVMath::Matrix4* m = dmScript::CheckMatrix4(L, index); +dmhash_t hash = dmScript::CheckHash(L, index); +dmhash_t hash = dmScript::CheckHashOrString(L, index); +``` + +### Async Lua callbacks via dmScript + +When a native extension needs to asynchronously invoke a user-provided Lua callback (e.g. after a platform event, timer, or async operation), use `dmScript::LuaCallbackInfo`. Reference: https://github.com/indiesoftby/defold-page-visibility/tree/main/page_visibility/src + +**Storing a callback from Lua:** +```cpp +static dmScript::LuaCallbackInfo* g_Callback = 0; + +static int SetCallback(lua_State* L) +{ + DM_LUA_STACK_CHECK(L, 0); + + // Destroy previous callback to avoid leaks + if (g_Callback) + { + dmScript::DestroyCallback(g_Callback); + g_Callback = 0; + } + + if (lua_isfunction(L, 1)) + { + g_Callback = dmScript::CreateCallback(L, 1); + } + + return 0; +} +``` + +**Invoking the callback later (from any native event):** +```cpp +static void OnAsyncEvent(int result_code) +{ + if (!g_Callback || !dmScript::IsCallbackValid(g_Callback)) + return; + + lua_State* L = dmScript::GetCallbackLuaContext(g_Callback); + DM_LUA_STACK_CHECK(L, 0); + + if (!dmScript::SetupCallback(g_Callback)) + { + dmScript::DestroyCallback(g_Callback); + g_Callback = 0; + return; + } + + // Push arguments after self (self is already on stack from SetupCallback) + lua_pushnumber(L, result_code); + + dmScript::PCall(L, 2, 0); // 2 = self + 1 user argument + + dmScript::TeardownCallback(g_Callback); +} +``` + +**Cleanup in Finalize (prevent leaks on extension unload):** +```cpp +static dmExtension::Result FinalizeMyExtension(dmExtension::Params* params) +{ + if (g_Callback) + { + dmScript::DestroyCallback(g_Callback); + g_Callback = 0; + } + return dmExtension::RESULT_OK; +} +``` + +**Lifecycle:** `CreateCallback` → `SetupCallback` → `PCall` → `TeardownCallback` → `DestroyCallback` (when done). + +For one-shot callbacks, call `DestroyCallback` right after `TeardownCallback`. For persistent listeners, keep the callback and only destroy on replacement or finalize. + +### When Lua API calls are acceptable + +Only fall back to `lua_getglobal`/`lua_call` patterns when: +- The functionality has **no dmSDK C++ equivalent** (e.g. `go.animate`, `msg.post` to arbitrary URLs) +- You need to call a **user-defined Lua callback** without the `dmScript::LuaCallbackInfo` pattern (rare) + +## Defold engine source as reference + +The Defold engine source at https://github.com/defold/defold/tree/dev/engine contains extensive dmSDK usage examples. When implementing native extensions, actively study the engine source to find correct API usage patterns, especially: + +- `engine/gameobject/` — game object manipulation (`dmGameObject::*`) +- `engine/gamesys/` — component systems (sprite, collision, factory, etc.) +- `engine/script/` — script bridge utilities (`dmScript::*`) +- `engine/dlib/` — math, hashing, logging, buffers (`dmVMath::*`, `dmLog*`, `dmBuffer::*`) +- `engine/render/` — render pipeline (`dmRender::*`) + +Additionally, the Spine extension at https://github.com/defold/extension-spine/tree/main/defold-spine is an excellent real-world reference for dmSDK usage — it demonstrates component registration, game object manipulation, rendering, resource management, and script bindings. + +Use the Librarian tool or `defold-api-fetch` skill to fetch specific API docs. Browse engine source and extension-spine on GitHub for real-world usage when the API docs are insufficient. + +## Code formatting + +When working with native extensions, ensure `.clang-format` exists in project root for consistent C/C++ formatting. If missing, fetch from Defold repository: + +``` +https://raw.githubusercontent.com/defold/defold/refs/heads/dev/.clang-format +``` + +**Important**: This `.clang-format` is from the official Defold repository and ensures code style consistency with the engine. + +## API reference + +For C++ SDK documentation, use `defold-api-fetch` skill with C++ Native Extension APIs section (dmExtension, dmScript, dmBuffer, etc.). diff --git a/.agents/skills/defold-project-build/SKILL.md b/.agents/skills/defold-project-build/SKILL.md new file mode 100644 index 0000000..97c2a22 --- /dev/null +++ b/.agents/skills/defold-project-build/SKILL.md @@ -0,0 +1,113 @@ +--- +name: defold-project-build +description: Builds the project using the running Defold editor, returns build errors, and launches the game if build succeeds. +--- + +# Build Defold Project via Editor HTTP API + +Build and run a Defold project by sending HTTP requests to the running Defold editor. + +## Prerequisites + +- The Defold editor must be running with the project open. + +## Reading the Editor Port + +The editor writes its HTTP port to `.internal/editor.port` in the project root. Read this file to get the port number. + +**Windows (PowerShell):** +```powershell +$port = Get-Content .internal/editor.port +``` + +**Linux/macOS:** +```bash +port=$(cat .internal/editor.port) +``` + +If the file does not exist, the editor is not running or the project is not open. + +## Building the Project + +Send a POST request to the `/command/build` endpoint. + +**Windows (PowerShell):** +```powershell +Invoke-RestMethod -Uri "http://127.0.0.1:$port/command/build" -Method Post +``` + +**Linux/macOS:** +```bash +curl -X POST "http://127.0.0.1:$port/command/build" --silent +``` + +The response is JSON with two fields: +- `success` (boolean) — whether the build succeeded. +- `issues` (array) — list of build issues (empty on success). + +If `success` is `true`, the build succeeded and the editor launches the game automatically. + +If `success` is `false`, each entry in `issues` contains: + +| Field | Description | +|-------|-------------| +| `severity` | `"error"` or `"warning"` | +| `message` | Human-readable description | +| `resource` | Absolute project path (e.g. `/main/logo.script`) | +| `range.start.line` | Start line (0-based) | +| `range.start.character` | Start column (0-based) | +| `range.end.line` | End line (0-based) | +| `range.end.character` | End column (0-based) | + +## Checking Console Output + +After a successful build and launch, read runtime logs from the editor console: + +**Windows (PowerShell):** +```powershell +Invoke-RestMethod -Uri "http://127.0.0.1:$port/console" -Method Get +``` + +**Linux/macOS:** +```bash +curl "http://127.0.0.1:$port/console" --silent +``` + +## Workflow + +1. Read the port from `.internal/editor.port`. +2. POST to `/command/build`. +3. If the build fails, report all issues with file paths, line numbers, and messages. +4. If the build succeeds, the game launches automatically. Check `/console` for runtime logs if needed. + +## Troubleshooting + +- **Connection refused** — the editor is not running or the port file is stale. Restart the editor and try again. + +## Example: Successful Build Response + +```json +{ + "success": true, + "issues": [] +} +``` + +## Example: Failed Build Response + +```json +{ + "success": false, + "issues": [ + { + "severity": "error", + "message": "go.property declaration should be a top-level statement", + "resource": "/main/logo.script", + "range": { + "start": { "line": 3, "character": 4 }, + "end": { "line": 3, "character": 35 } + } + } + ] +} +``` diff --git a/.agents/skills/defold-project-setup/SKILL.md b/.agents/skills/defold-project-setup/SKILL.md new file mode 100644 index 0000000..b39c42a --- /dev/null +++ b/.agents/skills/defold-project-setup/SKILL.md @@ -0,0 +1,88 @@ +--- +name: defold-project-setup +description: "Downloads Defold project dependencies into .deps/ folder. Also provides recommended game.project settings. Use FIRST before any other task when .deps/ folder is missing or empty, or after editing dependency URLs in game.project. Also use when creating a new project, configuring game.project, or asking about recommended project settings." +--- + +# Setup Defold Project + +Downloads and extracts Defold library dependencies and engine builtins into the `.deps/` directory, which is used as read-only context for resolving module references. Also provides recommended `game.project` settings for new projects. + +## When to Run + +- **Before any task** if the `.deps/` folder does not exist or is empty. +- **After editing dependencies** in `game.project` (any `[project] dependencies#N` entry was added, removed, or changed). + +## How to Run + +Execute the setup script from the project root: + +**Windows (PowerShell):** +```powershell +python .agents/skills/defold-project-setup/scripts/fetch_deps.py +``` + +**Linux/macOS:** +```bash +python3 .agents/skills/defold-project-setup/scripts/fetch_deps.py +``` + +Use `--dry-run` to print what would be done without downloading or extracting anything. + +## What It Does + +1. Reads `game.project` and parses all `[project] dependencies#N` URLs. +2. Downloads each dependency zip to a temporary directory. +3. Inspects each zip's `game.project` for `[library] include_dirs`. +4. Extracts only the declared include directories into `.deps/`. +5. Downloads Defold engine builtins into `.deps/builtins/` (from the latest stable release). + +## After Running + +- The `.deps/` folder is ready for use as a read-only include directory. + +## Recommended game.project settings + +When creating a new project or reviewing `game.project`, apply these recommended baseline settings. They provide sensible defaults for most Defold games. + +### [html5] + +```ini +[html5] +scale_mode = Stretch +heap_size = 64 +cssfile = /builtins/manifests/web/dark_theme.css +retry_count = 1000 +``` + +- `scale_mode = Stretch` — the HTML5 canvas fills the entire browser window. +- `heap_size = 64` — 64 MB is enough for most games; the engine will allocate more if needed. +- `cssfile = /builtins/manifests/web/dark_theme.css` — dark theme is a better default than white. +- `retry_count = 1000` — the engine retries loading game files up to 1000 times on network issues, preventing failures on slow connections. + +### [engine] + +```ini +[engine] +fixed_update_frequency = 60 +max_time_step = 0.05 +``` + +- `fixed_update_frequency = 60` — fixed update runs at 60 FPS (this is the Defold default). +- `max_time_step = 0.05` — the game runs without slowdowns down to 20 FPS. Below that, the game slows down instead of skipping large time chunks. + +### [physics] + +```ini +[physics] +gravity_y = -1000.0 +scale = 0.01 +velocity_threshold = 100.0 +use_fixed_timestep = 1 +max_fixed_timesteps = 0 +``` + +- `gravity_y = -1000.0` — this value is multiplied by `physics.scale`, so internally the physics engine sees -10 m/s² (Earth gravity). +- `scale = 0.01` — for 2D games this is typically `0.01` (1 pixel = 0.01 m). For 3D games use `1.0` (1 unit = 1 m). +- `velocity_threshold = 100.0` — (2D physics only) for stable physics this should be `1.0 / physics.scale`, so `100.0` when scale is `0.01`. +- `use_fixed_timestep = 1` — physics runs in fixed timestep mode. Move object interpolation between physics steps via the `object_interpolation` extension. +- `max_fixed_timesteps = 0` — (3D physics only) passes fixed dt directly from the Defold engine to Bullet3D and disables Bullet3D's internal accumulator. diff --git a/.agents/skills/defold-project-setup/scripts/fetch_deps.py b/.agents/skills/defold-project-setup/scripts/fetch_deps.py new file mode 100644 index 0000000..3821e42 --- /dev/null +++ b/.agents/skills/defold-project-setup/scripts/fetch_deps.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +"""Fetch Defold project dependencies from game.project.""" + +import io +import json +import os +import re +import shutil +import sys +import tempfile +import urllib.request +import zipfile +from pathlib import Path + + +def find_project_root(start_dir: Path) -> Path: + """Find the project root by looking for game.project.""" + dir_path = start_dir.resolve() + for _ in range(8): + candidate = dir_path / "game.project" + if candidate.exists(): + return dir_path + parent = dir_path.parent + if parent == dir_path: + break + dir_path = parent + raise RuntimeError("Failed to locate project root (game.project not found)") + + +def parse_ini(text: str) -> dict[str, dict[str, str]]: + """Minimal Defold-style INI parser.""" + out: dict[str, dict[str, str]] = {} + section = "" + + for raw_line in text.splitlines(): + line = raw_line.strip() + if not line or line.startswith(";") or line.startswith("#"): + continue + + match = re.match(r"^\[([^\]]+)\]$", line) + if match: + section = match.group(1).strip() + if section not in out: + out[section] = {} + continue + + eq_idx = line.find("=") + if eq_idx == -1: + continue + + key = line[:eq_idx].strip() + value = line[eq_idx + 1:].strip() + if section not in out: + out[section] = {} + out[section][key] = value + + return out + + +def parse_project_dependencies(project_text: str) -> list[str]: + """Parse project.dependencies#N entries from game.project.""" + ini = parse_ini(project_text) + project_section = ini.get("project", {}) + + indexed: list[tuple[int, str]] = [] + + for key, value in project_section.items(): + match = re.match(r"^dependencies#(\d+)$", key) + if match: + indexed.append((int(match.group(1)), value)) + + indexed.sort(key=lambda x: x[0]) + return [url for _, url in indexed] + + +def download_to_file(url: str, out_path: Path) -> None: + """Download URL to file with progress indication.""" + tmp_path = out_path.with_suffix(out_path.suffix + ".tmp") + out_path.parent.mkdir(parents=True, exist_ok=True) + + print(f" Downloading...") + request = urllib.request.Request(url, headers={"User-Agent": "sync-deps-py"}) + + with urllib.request.urlopen(request, timeout=600) as response: + total = response.headers.get("Content-Length") + total_size = int(total) if total else None + + with open(tmp_path, "wb") as f: + downloaded = 0 + block_size = 8192 + + while True: + chunk = response.read(block_size) + if not chunk: + break + f.write(chunk) + downloaded += len(chunk) + + if total_size: + pct = downloaded * 100 // total_size + print(f"\r Downloaded: {downloaded:,} / {total_size:,} bytes ({pct}%)", end="", flush=True) + else: + print(f"\r Downloaded: {downloaded:,} bytes", end="", flush=True) + + print() + + tmp_path.rename(out_path) + + +def find_game_project_in_zip(zip_path: Path) -> tuple[str, str]: + """Find game.project in zip, return (zip_root_prefix, project_text).""" + with zipfile.ZipFile(zip_path, "r") as zf: + for name in zf.namelist(): + if name.endswith("/"): + continue + if name.endswith("game.project"): + with zf.open(name) as f: + text = f.read().decode("utf-8") + idx = name.rfind("game.project") + prefix = name[:idx] if idx > 0 else "" + return prefix, text + + raise RuntimeError("No game.project found inside dependency zip") + + +def parse_library_include_dirs(project_text: str) -> list[str]: + """Parse [library] include_dirs from game.project.""" + ini = parse_ini(project_text) + library = ini.get("library", ini.get("Library", {})) + raw = library.get("include_dirs", "") + + if not raw: + raise RuntimeError("Missing [library] include_dirs in dependency game.project") + + return [s.strip() for s in raw.split(",") if s.strip()] + + +def assert_safe_include_dir(dir_name: str) -> None: + """Validate include_dir name for safety.""" + if not dir_name: + raise RuntimeError("Invalid include_dir (empty)") + if "/" in dir_name or "\\" in dir_name: + raise RuntimeError(f"Unsafe include_dir contains slash: {dir_name}") + if ".." in dir_name: + raise RuntimeError(f'Unsafe include_dir contains "..": {dir_name}') + if not re.match(r"^[A-Za-z0-9_-]+$", dir_name): + raise RuntimeError(f"Unsafe include_dir contains disallowed characters: {dir_name}") + + +def delete_local_include_dirs(deps_dir: Path, include_dirs: list[str]) -> None: + """Delete local include_dirs folders in .deps/.""" + for d in include_dirs: + target = deps_dir / d + if target.exists(): + print(f" Deleting: {target}") + shutil.rmtree(target) + + +def extract_selected_dirs(deps_dir: Path, zip_path: Path, zip_root_prefix: str, include_dirs: list[str]) -> None: + """Extract only include_dirs from zip to .deps/.""" + prefixes = [f"{zip_root_prefix}{d}/" for d in include_dirs] + deps_dir.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(zip_path, "r") as zf: + for entry in zf.infolist(): + if entry.filename.endswith("/"): + continue + + matched_prefix = None + for p in prefixes: + if entry.filename.startswith(p): + matched_prefix = p + break + + if not matched_prefix: + continue + + rel_zip_path = entry.filename[len(zip_root_prefix):] + out_path = deps_dir / rel_zip_path + + resolved = out_path.resolve() + if not str(resolved).startswith(str(deps_dir.resolve()) + os.sep): + raise RuntimeError(f"Zip-slip detected: {entry.filename}") + + out_path.parent.mkdir(parents=True, exist_ok=True) + + with zf.open(entry) as src, open(out_path, "wb") as dst: + shutil.copyfileobj(src, dst) + + print(f" Extracted {len(include_dirs)} dir(s)") + + +def sync_builtins(deps_dir: Path) -> None: + """Download and extract builtins from the stable Defold release.""" + builtins_dir = deps_dir / "builtins" + if builtins_dir.exists(): + print("Builtins already present, skipping.") + return + + print("Fetching Defold stable release info...") + request = urllib.request.Request( + "https://d.defold.com/stable/info.json", + headers={"User-Agent": "sync-deps-py"}, + ) + with urllib.request.urlopen(request, timeout=30) as response: + info = json.loads(response.read().decode("utf-8")) + + version = info["version"] + sha1 = info["sha1"] + print(f" Defold version: {version} (sha1: {sha1})") + + release_url = f"https://github.com/defold/defold/releases/download/{version}/Defold-x86_64-win32.zip" + jar_entry = f"Defold/packages/defold-{sha1}.jar" + + tmp_dir = Path(tempfile.mkdtemp(prefix="sync_builtins_")) + try: + release_zip_path = tmp_dir / "defold_release.zip" + download_to_file(release_url, release_zip_path) + + print(f" Extracting {jar_entry} from release zip...") + jar_path = tmp_dir / "defold.jar" + with zipfile.ZipFile(release_zip_path, "r") as zf: + with zf.open(jar_entry) as src, open(jar_path, "wb") as dst: + shutil.copyfileobj(src, dst) + + print(" Extracting builtins/ from jar...") + deps_dir.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(jar_path, "r") as jf: + for entry in jf.infolist(): + if not entry.filename.startswith("builtins/"): + continue + if entry.filename.endswith("/"): + continue + + out_path = deps_dir / entry.filename + resolved = out_path.resolve() + if not str(resolved).startswith(str(deps_dir.resolve()) + os.sep): + raise RuntimeError(f"Zip-slip detected: {entry.filename}") + + out_path.parent.mkdir(parents=True, exist_ok=True) + with jf.open(entry) as src, open(out_path, "wb") as dst: + shutil.copyfileobj(src, dst) + + print(" Builtins extracted.") + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + +def fix_gitignore_builtins(project_root: Path) -> None: + """Replace bare 'builtins' with '/builtins' in .gitignore. + + A bare 'builtins' pattern matches at any depth, which causes tools + to ignore .deps/builtins/. The anchored '/builtins' only matches + the top-level builtins/ directory. + """ + gitignore_path = project_root / ".gitignore" + if not gitignore_path.exists(): + return + + text = gitignore_path.read_text(encoding="utf-8") + lines = text.splitlines(keepends=True) + changed = False + + for i, line in enumerate(lines): + stripped = line.rstrip("\n\r") + if stripped == "builtins" or stripped == "builtins/": + lines[i] = "/" + stripped + line[len(stripped):] + changed = True + + if changed: + gitignore_path.write_text("".join(lines), encoding="utf-8") + print(" Fixed .gitignore: 'builtins' -> '/builtins'") + + +def main() -> None: + dry_run = "--dry-run" in sys.argv + + script_dir = Path(__file__).parent + project_root = find_project_root(script_dir) + game_project_path = project_root / "game.project" + deps_dir = project_root / ".deps" + + game_project_text = game_project_path.read_text(encoding="utf-8") + deps = parse_project_dependencies(game_project_text) + + print(f"Project root: {project_root}") + print(f"Dependencies: {len(deps)}") + print(f"Output dir: {deps_dir.relative_to(project_root)}") + + if dry_run: + print("DRY-RUN: Will not download/delete/extract.") + + deps_dir.mkdir(parents=True, exist_ok=True) + + print() + print("== .gitignore ==") + if not dry_run: + fix_gitignore_builtins(project_root) + else: + print(" Would fix 'builtins' -> '/builtins' in .gitignore") + + if deps: + tmp_dir = Path(tempfile.mkdtemp(prefix="sync_deps_")) + try: + for i, url in enumerate(deps): + print() + print(f"== Dependency {i + 1}/{len(deps)} ==") + print(url) + + if not dry_run: + zip_path = tmp_dir / f"dep_{i:02d}.zip" + download_to_file(url, zip_path) + + zip_root_prefix, project_text = find_game_project_in_zip(zip_path) + include_dirs = parse_library_include_dirs(project_text) + + for d in include_dirs: + assert_safe_include_dir(d) + + print(f" include_dirs: {', '.join(include_dirs)}") + + delete_local_include_dirs(deps_dir, include_dirs) + extract_selected_dirs(deps_dir, zip_path, zip_root_prefix, include_dirs) + else: + print(" Would download, inspect zip, read include_dirs, delete local folders, and extract.") + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + else: + print("\nNo [project] dependencies found in game.project, skipping library fetch.") + + print() + print("== Builtins ==") + if not dry_run: + sync_builtins(deps_dir) + else: + print(" Would download and extract builtins to .deps/builtins") + + print() + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/defold-proto-file-editing/SKILL.md b/.agents/skills/defold-proto-file-editing/SKILL.md new file mode 100644 index 0000000..2b07646 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/SKILL.md @@ -0,0 +1,234 @@ +--- +name: defold-proto-file-editing +description: "Creates and edits Defold resource and component files that use Protobuf Text Format (.collection, .go, .atlas, .sprite, .gui, .collisionobject, .convexshape, .label, .font, .material, .model, .mesh, .particlefx, .sound, .camera, .factory, .collectionfactory, .collectionproxy, .tilemap, .tilesource, .objectinterpolation). Use when asked to create, modify, or configure any Defold proto text format file." +--- + +# Editing Defold Proto Text Format Files + +Creates and edits Defold resource and component files that use Protobuf Text Format. + +## When to use + +This skill covers all Defold file types that are serialized as Protobuf Text Format. It does NOT cover Lua script files (`.script`, `.gui_script`, `.render_script`, `.editor_script`). + +## Supported file types + +For detailed field references, consult the per-type reference file in `references/`: + +- `references/collection.md` — `.collection` files (game levels, hierarchies) +- `references/gameobject.md` — `.go` files (game object prototypes) +- `references/gui.md` — `.gui` files (GUI scenes) +- `references/atlas.md` — `.atlas` files (texture atlases) +- `references/sprite.md` — `.sprite` files (sprite components) +- `references/collisionobject.md` — `.collisionobject` files (physics) +- `references/convexshape.md` — `.convexshape` files (external convex hull collision shapes) +- `references/label.md` — `.label` files (text in game space) +- `references/font.md` — `.font` files (font resources) +- `references/material.md` — `.material` files (render materials) +- `references/model.md` — `.model` files (3D models) +- `references/mesh.md` — `.mesh` files (custom 3D meshes from buffer data) +- `references/particlefx.md` — `.particlefx` files (particle effects) +- `references/sound.md` — `.sound` files (audio) +- `references/camera.md` — `.camera` files (camera components) +- `references/factory.md` — `.factory` files (object spawning) +- `references/collectionfactory.md` — `.collectionfactory` files (collection spawning) +- `references/collectionproxy.md` — `.collectionproxy` files (world loading) +- `references/tilemap.md` — `.tilemap` files (tile grids) +- `references/tilesource.md` — `.tilesource` files (tile source resources) +- `references/objectinterpolation.md` — `.objectinterpolation` files (extension: interpolation of fixed step movement) + +For skill maintenance tasks (updating references, fetching proto schemas), use the `defold-skill-maintain` skill. + +## Shaders and materials relationship + +Shaders (`.vp`, `.fp`, `.glsl`) are GLSL files and are NOT covered by this skill — use the `defold-shaders-editing` skill for shader files. However, shaders and materials are tightly coupled: + +### Data flow from material to shader + +1. **Constants** declared in `vertex_constants` / `fragment_constants` become `uniform` variables in shaders. Engine-provided constants (`CONSTANT_TYPE_VIEW`, `CONSTANT_TYPE_PROJECTION`, etc.) are automatically populated. User constants (`CONSTANT_TYPE_USER`) can be animated via `go.set()` / `go.animate()`. + +2. **Samplers** declared in `samplers` become `sampler2D` uniforms. The sampler `name` in the material must match the uniform name in the shader. + +3. **Attributes** declared in `attributes` become vertex `in` variables. Semantic types like `SEMANTIC_TYPE_POSITION`, `SEMANTIC_TYPE_TEXCOORD` provide engine-generated data. + +### Instancing with mtx_world and mtx_normal + +For instanced rendering, two special vertex attributes are available **without declaring them in the material's `attributes` section**: + +- `mtx_world` — `mat4` world transformation matrix (per-instance) +- `mtx_normal` — `mat4` normal transformation matrix (per-instance) + +When these are declared as vertex `in` attributes in the shader, Defold automatically enables instanced rendering: + +```glsl +// model_instanced.vp +in mediump mat4 mtx_world; +in mediump mat4 mtx_normal; + +void main() { + vec4 p = mtx_view * mtx_world * vec4(position.xyz, 1.0); + var_normal = normalize((mtx_normal * vec4(normal, 0.0)).xyz); + gl_Position = mtx_proj * p; +} +``` + +**Requirements for instancing**: +- Material must have `vertex_space: VERTEX_SPACE_LOCAL` +- Shader declares `mtx_world` and/or `mtx_normal` as `in` attributes +- No need to add these to the material's `attributes` list + +### Reference examples + +Built-in shader examples are in `.deps/builtins/materials/`: +- `sprite.vp` / `sprite.fp` — 2D sprite rendering (world space) +- `model.vp` / `model.fp` — 3D model rendering (local space, uniforms) +- `model_instanced.vp` — 3D model with instancing (uses `mtx_world`, `mtx_normal` as attributes) + +## Bundled scripts + +- `scripts/get_image_size.py` — Get image dimensions (width × height) from PNG/JPEG files. Pure Python, no external dependencies. Use this when creating collision object box shapes that should match sprite image sizes. See `references/collisionobject.md` → "Sizing box shapes from sprite images" for the full workflow. +- `scripts/gen_convexshape.py` — Generate a `.convexshape` file from a 2D image's non-transparent silhouette. Uses PIL/Pillow. Computes a convex hull, simplifies to ≤16 points (Box2D limit), centers at origin, and outputs Defold `.convexshape` format. See `references/convexshape.md` → "Generating from an image" for usage. +- `scripts/gen_silhouette_chain.py` — Generate a `.collisionobject` file with a chain of rotated TYPE_BOX shapes tracing the contour of any image silhouette (concave, with holes, multi-part). Uses PIL/Pillow. Extracts boundary contour loops, simplifies with RDP, and outputs a `.collisionobject` with thin rotated boxes along each edge. See `references/collisionobject.md` → "Silhouette chain from image contour" for usage. + +## Embedded component type names + +When embedding components in `.go` or `.collection` files, these are the `type` string values: + +- `"sprite"` — Sprite component +- `"label"` — Label component +- `"collisionobject"` — Collision object +- `"sound"` — Sound component +- `"particlefx"` — Particle effect +- `"model"` — 3D model +- `"mesh"` — Mesh +- `"camera"` — Camera +- `"factory"` — Factory +- `"collectionfactory"` — Collection factory +- `"collectionproxy"` — Collection proxy +- `"tilegrid"` — Tilemap (note: type is `"tilegrid"`, not `"tilemap"`) +- `"objectinterpolation"` — Object interpolation (extension) + +GUI components (`.gui`) CANNOT be embedded inline — they must be added as referenced components. + +## Protobuf Text Format rules + +These rules apply to ALL Defold proto text format files: + +1. **Default omission**: Omit fields that equal their proto default. This keeps files minimal and matches Defold editor output. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Integers**: No decimal point: `4`, not `4.0`. +5. **Strings**: Always double-quoted: `"text"`. +6. **Enums**: Use the constant name without quotes: `BLEND_MODE_ALPHA`. +7. **Booleans**: `true` or `false`, no quotes. +8. **Repeated fields**: Each scalar value gets its own line with the field name. +9. **Repeated messages**: Each entry gets its own `field_name { ... }` block. +10. **Field order**: Follow the proto field number order. +11. **No trailing commas or semicolons**. +12. **Indentation**: 2 spaces per nesting level inside message blocks. +13. **Newlines**: One empty line between the end of a message block `}` and the next field. No empty line between consecutive scalar fields. +14. **No blank lines** between fields or blocks within the same nesting level (match Defold editor output). + +## Vector and math type conventions + +These types from `ddf_math.proto` appear across many file types: + +### dmMath.Vector3 / dmMath.Point3 +Components: `x, y, z` (and `d`, rarely used). All default to `0.0`. + +### dmMath.Vector3One +Components: `x, y, z` (and `d`, rarely used). All default to `1.0`. + +### dmMath.Vector4 +Components: `x, y, z, w`. All default to `0.0`. + +### dmMath.Vector4One +Components: `x, y, z, w`. All default to `1.0`. + +### dmMath.Vector4WOne +Components: `x, y, z, w`. `x/y/z` default to `0.0`, `w` defaults to `1.0`. + +### dmMath.Quat +Components: `x, y, z, w`. `x/y/z` default to `0.0`, `w` defaults to `1.0`. + +### Omission rules for vector/math blocks + +- Only include components that differ from their default value. +- If all components are at defaults, omit the entire block (for optional fields). +- If the field is required, include the block but leave it empty: `position { }`. + +```protobuf +position { + x: 200.0 + y: 100.0 +} +rotation { + z: 0.7071068 + w: 0.7071068 +} +``` + +## Data string encoding rules + +Game objects embedded in `.collection` files and components embedded in `.go` files encode their content as multi-line strings in a `data` field. + +### Single nesting level (game object with referenced components) + +The content is escaped once: +- Quotes become `\"` +- Newlines become `\n` + +```protobuf +data: "components {\n" +" id: \"script\"\n" +" component: \"/main/main.script\"\n" +"}\n" +"" +``` + +### Double nesting level (game object with embedded components) + +The embedded component's `data` field requires double escaping: +- Outer quotes: `\"` +- Inner quotes (inside component data): `\\\"` +- Outer newlines: `\n` +- Inner newlines (inside component data): `\\n` + +```protobuf +data: "embedded_components {\n" +" id: \"camera\"\n" +" type: \"camera\"\n" +" data: \"aspect_ratio: 1.0\\n" +"fov: 0.7854\\n" +"near_z: -1.0\\n" +"far_z: 1.0\\n" +"orthographic_projection: 1\\n" +"\"\n" +"}\n" +"" +``` + +**Common mistake — closing quote of inner `data`**: The line that closes the inner `data` string (the `\"` that ends the embedded component's data value) must use single-escaped newline `"\"\n"`, NOT double-escaped `"\"\\n"`. The closing quote `\"` is the boundary between nesting levels — after it, you are back at the outer (game object) level, so the newline is single `\n`. Using `\\n` here corrupts the game object data and causes a load error. + +Note: After the opening `data: \"...\\n"` line inside double-nested data, subsequent lines of that inner data do NOT have leading whitespace (they start at column 0 of the quoted string). + +Multi-line string concatenation blocks end with an empty `""` terminator. A single-line `data: ""` does not need an additional terminator. + +## Workflow + +### Creating a new file + +1. Determine the file type and path. +2. Consult the relevant reference file in `references/` for the field structure. +3. Set all required fields. +4. Set optional fields only if they differ from defaults. +5. Follow proto field number order. +6. Apply all Protobuf Text Format rules above. + +### Editing an existing file + +1. Read the current file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. +5. When editing existing files, preserve the existing formatting style. diff --git a/.agents/skills/defold-proto-file-editing/references/atlas.md b/.agents/skills/defold-proto-file-editing/references/atlas.md new file mode 100644 index 0000000..d0fad52 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/atlas.md @@ -0,0 +1,168 @@ +# Atlas (.atlas) + +Proto message: `Atlas` from `atlas_ddf.proto`. + +An atlas combines multiple images into larger texture pages for efficient rendering. Used as image sources for Sprite, ParticleFX, and other visual components. + +## Canonical example + +```protobuf +images { + image: "/assets/images/logo.png" +} +margin: 4 +extrude_borders: 2 +``` + +## Top-level fields reference + +### images (repeated) — `AtlasImage` + +Individual images in the atlas. Each image is a separate `images { ... }` block. + +```protobuf +images { + image: "/assets/images/player.png" +} +images { + image: "/assets/images/enemy.png" +} +``` + +### animations (repeated) — `AtlasAnimation` + +Flipbook animation groups. Each animation is a separate `animations { ... }` block containing a sequence of images. + +```protobuf +animations { + id: "walk" + images { + image: "/assets/images/walk_01.png" + } + images { + image: "/assets/images/walk_02.png" + } + fps: 12 + playback: PLAYBACK_LOOP_FORWARD +} +``` + +### margin (optional) — `uint32` + +Pixels added between each image. Default: `0`. **Omit if `0`.** + +### extrude_borders (optional) — `uint32` + +Edge pixels repeatedly padded around each image. Prevents neighbor image bleeding. Default: `0`. **Omit if `0`.** + +### inner_padding (optional) — `uint32` + +Empty pixels padded around each image. Default: `0`. **Omit if `0`.** + +### max_page_width (optional) — `uint32` + +Maximum width of a page in a multi-page atlas (pixels). Default: `0` (no limit). **Omit if `0`.** + +### max_page_height (optional) — `uint32` + +Maximum height of a page in a multi-page atlas (pixels). Default: `0` (no limit). **Omit if `0`.** + +### rename_patterns (optional) — `string` + +Comma-separated `search=replace` patterns to rename animation IDs. **Omit if empty.** + +## AtlasImage fields + +### image (required) — `string` + +Absolute resource path to a `.png` image file. + +### sprite_trim_mode (optional) — enum `SpriteTrimmingMode` + +How the sprite geometry is generated. Default: `SPRITE_TRIM_MODE_OFF`. **Omit if `SPRITE_TRIM_MODE_OFF`.** + +Values: `SPRITE_TRIM_MODE_OFF`, `SPRITE_TRIM_MODE_4`, `SPRITE_TRIM_MODE_5`, `SPRITE_TRIM_MODE_6`, `SPRITE_TRIM_MODE_7`, `SPRITE_TRIM_MODE_8`, `SPRITE_TRIM_POLYGONS`. + +Note: sprite trimming does not work with slice-9 sprites. + +### pivot_x (optional) — `float` + +Horizontal pivot. Default: `0.5` (center). **Omit if `0.5`.** + +### pivot_y (optional) — `float` + +Vertical pivot. Default: `0.5` (center). **Omit if `0.5`.** + +## AtlasAnimation fields + +### id (required) — `string` + +Animation name. Used via `sprite.play_flipbook()`. + +### images (repeated) — `AtlasImage` + +Ordered list of frames. + +### playback (optional) — enum `Playback` + +Default: `PLAYBACK_ONCE_FORWARD`. **Omit if `PLAYBACK_ONCE_FORWARD`.** + +Values: `PLAYBACK_NONE`, `PLAYBACK_ONCE_FORWARD`, `PLAYBACK_ONCE_BACKWARD`, `PLAYBACK_ONCE_PINGPONG`, `PLAYBACK_LOOP_FORWARD`, `PLAYBACK_LOOP_BACKWARD`, `PLAYBACK_LOOP_PINGPONG`. + +### fps (optional) — `uint32` + +Frames per second. Default: `30`. **Omit if `30`.** + +### flip_horizontal (optional) — `uint32` + +`0` = no flip, `1` = flip. Default: `0`. **Omit if `0`.** + +### flip_vertical (optional) — `uint32` + +`0` = no flip, `1` = flip. Default: `0`. **Omit if `0`.** + +## Complete examples + +### Atlas with animation + +```protobuf +images { + image: "/assets/images/idle.png" +} +animations { + id: "walk" + images { + image: "/assets/images/walk_01.png" + } + images { + image: "/assets/images/walk_02.png" + } + images { + image: "/assets/images/walk_03.png" + } + images { + image: "/assets/images/walk_04.png" + } + playback: PLAYBACK_LOOP_FORWARD + fps: 12 +} +margin: 2 +extrude_borders: 2 +``` + +### Multi-page atlas with trimming + +```protobuf +images { + image: "/assets/images/large_bg.png" + sprite_trim_mode: SPRITE_TRIM_MODE_4 +} +images { + image: "/assets/images/ui_panel.png" + sprite_trim_mode: SPRITE_TRIM_MODE_6 +} +margin: 2 +extrude_borders: 2 +max_page_width: 2048 +max_page_height: 2048 +``` diff --git a/.agents/skills/defold-proto-file-editing/references/camera.md b/.agents/skills/defold-proto-file-editing/references/camera.md new file mode 100644 index 0000000..bcabaa8 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/camera.md @@ -0,0 +1,223 @@ +# Editing Cameras + +Creates and edits Defold `.camera` component files using Protobuf Text Format. + +## Overview + +A Camera component changes the viewport and projection of the game world. It provides view and projection matrices to the render script, supporting both perspective (3D) and orthographic (2D) projections. + +**Important**: Camera components are almost never created as separate `.camera` files. The standard practice is to embed them directly inside a game object within a `.collection` file as an `embedded_components` block. All examples in this project follow this pattern. + +## File format + +Camera files (`.camera`) use **Protobuf Text Format** based on the `CameraDesc` message from `gamesys/camera_ddf.proto`. + +### Canonical example (standalone `.camera` file) + +```protobuf +aspect_ratio: 1.0 +fov: 0.7854 +near_z: -1.0 +far_z: 1.0 +orthographic_projection: 1 +orthographic_mode: ORTHO_MODE_AUTO_COVER +``` + +### Embedded in a collection (typical usage) + +Camera components are typically embedded as part of a dedicated game object in a `.collection` file. This is the most common way to use cameras in Defold: + +```protobuf +embedded_instances { + id: "camera" + data: "embedded_components {\n" + " id: \"camera\"\n" + " type: \"camera\"\n" + " data: \"aspect_ratio: 1.0\\n" + "fov: 0.7854\\n" + "near_z: -1.0\\n" + "far_z: 1.0\\n" + "orthographic_projection: 1\\n" + "orthographic_mode: ORTHO_MODE_AUTO_COVER\\n" + "\"\n" + "}\n" + "" + position { + x: 640.0 + y: 360.0 + } +} +``` + +## Fields reference + +### aspect_ratio (required) — `float` + +The ratio between the frustum width and height. Used when calculating the projection of a perspective camera. `1.0` means a quadratic view, `1.33` for 4:3, `1.78` for 16:9. Ignored if `auto_aspect_ratio` is set. + +```protobuf +aspect_ratio: 1.0 +``` + +### fov (required) — `float` + +Vertical camera field of view expressed in radians. Only used for perspective projection. The wider the field of view, the more the camera sees. Common values: `0.7854` (45°), `1.0472` (60°). + +```protobuf +fov: 0.7854 +``` + +### near_z (required) — `float` + +Z-value of the near clipping plane. For orthographic 2D cameras, typically set to `-1.0`. + +```protobuf +near_z: -1.0 +``` + +### far_z (required) — `float` + +Z-value of the far clipping plane. For orthographic 2D cameras, typically set to `1.0`. For perspective 3D cameras, use larger values like `1000.0`. + +```protobuf +far_z: 1.0 +``` + +### auto_aspect_ratio (optional) — `uint32` + +Automatically calculate the aspect ratio based on the window dimensions. `0` = disabled (default), `1` = enabled. Only used for perspective cameras. + +**Omission rule**: Omit if `0`. + +```protobuf +auto_aspect_ratio: 1 +``` + +### orthographic_projection (optional) — `uint32` + +Switch the camera to orthographic projection. `0` = perspective (default), `1` = orthographic. For 2D games, this is almost always set to `1`. + +**Omission rule**: Omit if `0`. + +```protobuf +orthographic_projection: 1 +``` + +### orthographic_zoom (optional) — `float` + +Zoom level for orthographic projection. Default: `1.0`. Values `> 1.0` zoom in, values `< 1.0` zoom out. Only meaningful when `orthographic_projection` is `1`. + +**Omission rule**: Omit if `1.0`. + +```protobuf +orthographic_zoom: 2.0 +``` + +### orthographic_mode (optional) — `OrthoZoomMode` + +Controls how the orthographic camera determines zoom relative to window size and design resolution. Default: `ORTHO_MODE_FIXED`. Only meaningful when `orthographic_projection` is `1`. + +**Omission rule**: Omit if `ORTHO_MODE_FIXED`. + +```protobuf +orthographic_mode: ORTHO_MODE_AUTO_COVER +``` + +## Enum: OrthoZoomMode + +| Value | Description | +|-------|-------------| +| `ORTHO_MODE_FIXED` | Uses the current `orthographic_zoom` value as-is. | +| `ORTHO_MODE_AUTO_FIT` | Automatically adjusts zoom so the full design area fits inside the window (contain). May show extra content. | +| `ORTHO_MODE_AUTO_COVER` | Automatically adjusts zoom so the design area covers the entire window (cover). May crop content. | + +## Runtime properties + +These properties can be read/written at runtime via `go.get()` / `go.set()`: + +| Property | Type | Read/Write | +|----------|------|------------| +| `fov` | `float` | Read/Write | +| `near_z` | `float` | Read/Write | +| `far_z` | `float` | Read/Write | +| `orthographic_zoom` | `float` | Read/Write | +| `aspect_ratio` | `float` | Read/Write | +| `view` | `matrix4` | Read only | +| `projection` | `matrix4` | Read only | + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. +2. **Floats**: Always include decimal point: `1.0`, not `1`. +3. **Integers**: No decimal point: `1`, not `1.0`. +4. **Enums**: Use the constant name without quotes: `ORTHO_MODE_AUTO_COVER`. +5. **Field order**: Follow the proto field number order: `aspect_ratio`, `fov`, `near_z`, `far_z`, `auto_aspect_ratio`, `orthographic_projection`, `orthographic_zoom`, `orthographic_mode`. +6. **No trailing commas or semicolons**. +7. **No empty lines between fields** (all fields are scalar). + +## Common templates + +### 2D orthographic camera (auto cover) + +The most common setup for 2D games. Position the game object at the center of the design resolution. + +```protobuf +aspect_ratio: 1.0 +fov: 0.7854 +near_z: -1.0 +far_z: 1.0 +orthographic_projection: 1 +orthographic_mode: ORTHO_MODE_AUTO_COVER +``` + +### 2D orthographic camera (auto fit) + +Shows the full design area, may reveal extra content at edges. + +```protobuf +aspect_ratio: 1.0 +fov: 0.7854 +near_z: -1.0 +far_z: 1.0 +orthographic_projection: 1 +orthographic_mode: ORTHO_MODE_AUTO_FIT +``` + +### 2D orthographic camera (fixed zoom) + +Manual zoom control; `orthographic_mode` is omitted because `ORTHO_MODE_FIXED` is the default. + +```protobuf +aspect_ratio: 1.0 +fov: 0.7854 +near_z: -1.0 +far_z: 1.0 +orthographic_projection: 1 +``` + +### 3D perspective camera + +```protobuf +aspect_ratio: 1.78 +fov: 0.7854 +near_z: 0.1 +far_z: 1000.0 +auto_aspect_ratio: 1 +``` + +## Workflow + +### Creating a new camera + +1. Determine whether the camera should be a standalone `.camera` file or embedded in a collection (embedded is the standard practice). +2. Set the four required fields: `aspect_ratio`, `fov`, `near_z`, `far_z`. +3. For 2D games, set `orthographic_projection: 1` and choose an `orthographic_mode`. +4. Add optional fields only if they differ from defaults. +5. If embedded in a collection, position the game object at the center of the design resolution (e.g., `x: 640.0, y: 360.0` for 1280×720). + +### Editing an existing camera + +1. Read the current `.camera` file or the embedded camera data in the collection. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/collection.md b/.agents/skills/defold-proto-file-editing/references/collection.md new file mode 100644 index 0000000..36c5e00 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/collection.md @@ -0,0 +1,756 @@ +# Editing Collections + +Creates and edits Defold `.collection` files using Protobuf Text Format. + +## Overview + +A collection (`.collection`) is the primary structural unit in Defold. Collections organize game objects and other collections into tree hierarchies. They are used for game levels, groups of entities, UI screens, and any reusable structural grouping. The bootstrap collection (specified in `game.project`) is the entry point of the game. + +Collections can contain: +- **Referenced game objects** (`instances`) — pointing to external `.go` files +- **Embedded game objects** (`embedded_instances`) — inline game object definitions +- **Sub-collections** (`collection_instances`) — references to other `.collection` files + +Game objects within a collection can form parent-child hierarchies via the `children` field. + +## File format + +Collection files (`.collection`) use **Protobuf Text Format** based on the `CollectionDesc` message from `gameobject/gameobject_ddf.proto`. + +### Canonical example + +```protobuf +name: "main" +instances { + id: "player" + prototype: "/main/player.go" +} +instances { + id: "enemy" + prototype: "/main/enemy.go" + position { + x: 200.0 + y: 100.0 + } +} +scale_along_z: 0 +embedded_instances { + id: "go" + data: "components {\n" + " id: \"main\"\n" + " component: \"/main/main.script\"\n" + "}\n" + "" + position { + x: 640.0 + y: 360.0 + } +} +embedded_instances { + id: "camera" + data: "embedded_components {\n" + " id: \"camera\"\n" + " type: \"camera\"\n" + " data: \"aspect_ratio: 1.0\\n" + "fov: 0.7854\\n" + "near_z: -1.0\\n" + "far_z: 1.0\\n" + "orthographic_projection: 1\\n" + "orthographic_mode: ORTHO_MODE_AUTO_COVER\\n" + "\"\n" + "}\n" + "" + position { + x: 640.0 + y: 360.0 + } +} +``` + +## Top-level structure (`CollectionDesc`) + +| Field # | Field | Type | Required | Default | +|---------|-------|------|----------|---------| +| 1 | `name` | `string` | required | — | +| 2 | `instances` | repeated `InstanceDesc` | — | — | +| 3 | `collection_instances` | repeated `CollectionInstanceDesc` | — | — | +| 4 | `scale_along_z` | `uint32` | optional | `0` | +| 5 | `embedded_instances` | repeated `EmbeddedInstanceDesc` | — | — | +| 6 | `property_resources` | repeated `string` | — | — | +| 7 | `component_types` | repeated `ComponenTypeDesc` | — | — | + +Fields #6 and #7 are `runtime_only` — they are never written in `.collection` files and are only used internally by the engine. + +**Field order**: `name`, `instances`, `collection_instances`, `scale_along_z`, `embedded_instances`. + +## Fields reference + +### name (required) — `string` + +The name identifier of the collection. Typically matches the file name without extension. + +```protobuf +name: "main" +``` + +### instances (repeated) — `InstanceDesc` + +References to external game object files. Each referenced game object is a separate `instances { ... }` block. + +See the **InstanceDesc** section below. + +### collection_instances (repeated) — `CollectionInstanceDesc` + +References to external collection files (sub-collections). Each sub-collection is a separate `collection_instances { ... }` block. + +See the **CollectionInstanceDesc** section below. + +### scale_along_z (optional) — `uint32` + +Deprecated field. Default: `0`. The Defold editor always writes this field with value `0`. + +**Omission rule**: Although the proto default is `0`, the Defold editor always writes this field explicitly. Always include `scale_along_z: 0` to match editor output. + +```protobuf +scale_along_z: 0 +``` + +### embedded_instances (repeated) — `EmbeddedInstanceDesc` + +Inline game object definitions. Each embedded game object is a separate `embedded_instances { ... }` block. + +See the **EmbeddedInstanceDesc** section below. + +## InstanceDesc + +A reference to an external `.go` file placed in the collection. + +### InstanceDesc fields + +#### id (required) — `string` + +Unique identifier for this game object within the collection. Used for addressing (e.g., `go.get_position("player")`). + +```protobuf +id: "player" +``` + +#### prototype (required) — `string` + +Absolute resource path to the `.go` file. + +```protobuf +prototype: "/main/player.go" +``` + +#### children (repeated) — `string` + +IDs of game objects that are children of this instance (parent-child hierarchy). Each child is a separate `children:` line. + +**Omission rule**: Omit if no children. + +```protobuf +children: "child1" +children: "child2" +``` + +#### position (optional) — `dmMath.Point3` + +Position of the game object in the collection. Defaults: `x: 0.0`, `y: 0.0`, `z: 0.0`. + +**Omission rule**: Omit the entire block if all components are `0.0`. Only include components that differ from `0.0`. + +```protobuf +position { + x: 200.0 + y: 100.0 +} +``` + +#### rotation (optional) — `dmMath.Quat` + +Rotation as quaternion. Defaults: `x: 0.0`, `y: 0.0`, `z: 0.0`, `w: 1.0`. + +**Omission rule**: Omit the entire block if all components are at defaults. Only include components that differ. + +```protobuf +rotation { + z: 0.7071068 + w: 0.7071068 +} +``` + +#### component_properties (repeated) — `ComponentPropertyDesc` + +Overrides for script properties on components of this game object. Each override targets a specific component by its id. + +See the **ComponentPropertyDesc** section below. + +**Omission rule**: Omit if no property overrides. + +#### scale (optional) — `float` + +Uniform scale factor. Default: `1.0`. + +**Omission rule**: Omit if `1.0`. Note: this is a scalar float, not a message block. Deprecated in favor of `scale3`. + +```protobuf +scale: 2.0 +``` + +#### scale3 (optional) — `dmMath.Vector3One` + +Non-uniform scale. Defaults: `x: 1.0`, `y: 1.0`, `z: 1.0`. + +**Omission rule**: Omit the entire block if all components are `1.0`. Only include components that differ from `1.0`. + +```protobuf +scale3 { + x: 2.0 + y: 2.0 +} +``` + +### Full InstanceDesc example + +```protobuf +instances { + id: "enemy" + prototype: "/main/enemy.go" + children: "weapon" + position { + x: 200.0 + y: 100.0 + } + component_properties { + id: "script" + properties { + id: "speed" + value: "150.0" + type: PROPERTY_TYPE_NUMBER + } + } +} +``` + +## EmbeddedInstanceDesc + +An inline game object definition within the collection. The game object content (its `PrototypeDesc`) is encoded as a multi-line string in the `data` field. + +### EmbeddedInstanceDesc fields + +#### id (required) — `string` + +Unique identifier for this game object within the collection. + +```protobuf +id: "go" +``` + +#### children (repeated) — `string` + +IDs of child game objects. Same format as `InstanceDesc.children`. + +**Omission rule**: Omit if no children. + +#### data (required) — `string` + +The game object's `PrototypeDesc` content encoded as a multi-line string. This contains the same content that would be in a `.go` file (`components` and `embedded_components` blocks). + +**Encoding rules**: +- Each logical line of the game object's protobuf text becomes a separate quoted string literal +- Lines end with `\n` inside the quotes +- Inner quotes are escaped as `\"` +- Double-nested data (embedded components inside the game object) uses `\\n` for line breaks and `\\\"` for quotes +- The last entry is an empty string `""` + +**Empty game object**: Use `data: ""` for a game object with no components (position marker). This is a required field, so it must always be present even if empty. + +**Single-nesting example** (game object with referenced component): + +```protobuf +data: "components {\n" +" id: \"script\"\n" +" component: \"/main/main.script\"\n" +"}\n" +"" +``` + +**Double-nested example** (game object with embedded component): + +```protobuf +data: "embedded_components {\n" +" id: \"camera\"\n" +" type: \"camera\"\n" +" data: \"aspect_ratio: 1.0\\n" +"fov: 0.7854\\n" +"near_z: -1.0\\n" +"far_z: 1.0\\n" +"orthographic_projection: 1\\n" +"\"\n" +"}\n" +"" +``` + +Note: multi-line string concatenation blocks end with an empty `""` terminator. A single-line `data: ""` does not need an additional terminator. + +**Common mistake — closing quote of inner `data`**: The line that closes the inner `data` string (the `\"` that ends the embedded component's data value) must use single-escaped newline `"\"\n"`, NOT double-escaped `"\"\\n"`. The closing quote is the boundary between nesting levels — after it, you are back at the outer (game object) level, so the newline is single `\n`. Using `\\n` here corrupts the game object data and causes a load error. + +#### position (optional) — `dmMath.Point3` + +Position of the game object. Same defaults and omission rules as `InstanceDesc.position`. + +#### rotation (optional) — `dmMath.Quat` + +Rotation quaternion. Same defaults and omission rules as `InstanceDesc.rotation`. + +#### component_properties (repeated) — `ComponentPropertyDesc` + +Property overrides. Same as `InstanceDesc.component_properties`. + +#### scale (optional) — `float` + +Uniform scale. Default: `1.0`. Same omission rules as `InstanceDesc.scale`. + +#### scale3 (optional) — `dmMath.Vector3One` + +Non-uniform scale. Same defaults and omission rules as `InstanceDesc.scale3`. + +### Full EmbeddedInstanceDesc example + +```protobuf +embedded_instances { + id: "go" + data: "components {\n" + " id: \"main\"\n" + " component: \"/main/main.script\"\n" + "}\n" + "embedded_components {\n" + " id: \"logo\"\n" + " type: \"sprite\"\n" + " data: \"default_animation: \\\"logo\\\"\\n" + "material: \\\"/builtins/materials/sprite.material\\\"\\n" + "textures {\\n" + " sampler: \\\"texture_sampler\\\"\\n" + " texture: \\\"/main/main.atlas\\\"\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: 640.0 + y: 360.0 + } +} +``` + +## CollectionInstanceDesc + +A reference to an external `.collection` file (sub-collection). + +### CollectionInstanceDesc fields + +#### id (required) — `string` + +Unique identifier for this sub-collection within the parent collection. Used as a path prefix when addressing game objects inside (e.g., `"level/enemy"`). + +```protobuf +id: "level" +``` + +#### collection (required) — `string` + +Absolute resource path to the `.collection` file. + +```protobuf +collection: "/main/level1.collection" +``` + +#### position (optional) — `dmMath.Point3` + +Position of the sub-collection. Defaults: `x: 0.0`, `y: 0.0`, `z: 0.0`. + +**Omission rule**: Omit the entire block if all components are `0.0`. + +```protobuf +position { + x: 500.0 + y: 200.0 +} +``` + +#### rotation (optional) — `dmMath.Quat` + +Rotation quaternion. Defaults: `x: 0.0`, `y: 0.0`, `z: 0.0`, `w: 1.0`. + +**Omission rule**: Omit the entire block if all components are at defaults. + +#### scale (optional) — `float` + +Uniform scale. Default: `1.0`. + +**Omission rule**: Omit if `1.0`. + +#### instance_properties (repeated) — `InstancePropertyDesc` + +Property overrides for game objects inside the sub-collection. Each entry targets a specific game object by its id and overrides properties on its components. + +See the **InstancePropertyDesc** section below. + +**Omission rule**: Omit if no property overrides. + +#### scale3 (optional) — `dmMath.Vector3One` + +Non-uniform scale. Defaults: `x: 1.0`, `y: 1.0`, `z: 1.0`. + +**Omission rule**: Omit the entire block if all components are `1.0`. + +### Full CollectionInstanceDesc example + +```protobuf +collection_instances { + id: "level" + collection: "/main/level1.collection" + position { + x: 500.0 + y: 200.0 + } + instance_properties { + id: "enemy" + properties { + id: "script" + properties { + id: "speed" + value: "200.0" + type: PROPERTY_TYPE_NUMBER + } + } + } +} +``` + +## ComponentPropertyDesc + +Used to override script properties on a specific component of a game object. + +### id (required) — `string` + +The component id within the game object (e.g., `"script"`). + +### properties (repeated) — `PropertyDesc` + +Property overrides. Each is a `properties { ... }` block. + +```protobuf +component_properties { + id: "script" + properties { + id: "speed" + value: "200.0" + type: PROPERTY_TYPE_NUMBER + } +} +``` + +## InstancePropertyDesc + +Used within `CollectionInstanceDesc` to override properties on game objects inside a sub-collection. + +### id (required) — `string` + +The game object id inside the sub-collection. + +### properties (repeated) — `ComponentPropertyDesc` + +Component property overrides for that game object. Each is a `properties { ... }` block containing component id and property overrides. + +```protobuf +instance_properties { + id: "enemy" + properties { + id: "script" + properties { + id: "health" + value: "50.0" + type: PROPERTY_TYPE_NUMBER + } + } +} +``` + +## PropertyDesc + +### id (required) — `string` + +The script property name. + +### value (required) — `string` + +The property value as a string. + +### type (required) — enum `PropertyType` + +| Value | Description | +|-------|-------------| +| `PROPERTY_TYPE_NUMBER` | Numeric value | +| `PROPERTY_TYPE_HASH` | Hash value | +| `PROPERTY_TYPE_URL` | URL value | +| `PROPERTY_TYPE_VECTOR3` | Vector3 value (format: `"x, y, z"`) | +| `PROPERTY_TYPE_VECTOR4` | Vector4 value (format: `"x, y, z, w"`) | +| `PROPERTY_TYPE_QUAT` | Quaternion value | +| `PROPERTY_TYPE_BOOLEAN` | Boolean value (`"true"` or `"false"`) | +| `PROPERTY_TYPE_MATRIX4` | Matrix4 value | + +## Parent-child hierarchies + +Game objects within a collection can form parent-child hierarchies. The parent game object lists its children using the `children` field. This affects the transform hierarchy at runtime — child positions, rotations, and scales are relative to the parent. + +To create a hierarchy: +1. The parent game object (either `instances` or `embedded_instances`) includes `children: "child_id"` for each child. +2. The child game object must exist in the same collection (as `instances` or `embedded_instances`). +3. Multiple children are listed as separate `children:` lines. + +```protobuf +instances { + id: "parent" + prototype: "/main/parent.go" + children: "child1" + children: "child2" +} +instances { + id: "child1" + prototype: "/main/child.go" + position { + x: 50.0 + } +} +instances { + id: "child2" + prototype: "/main/child.go" + position { + x: -50.0 + } +} +``` + +## Data string encoding rules + +The `data` field in `EmbeddedInstanceDesc` contains a game object's `PrototypeDesc` as an escaped string. Understanding the encoding is essential: + +### Single nesting level (game object with referenced components) + +The game object content is escaped once: +- Quotes become `\"` +- Newlines become `\n` + +```protobuf +data: "components {\n" +" id: \"script\"\n" +" component: \"/main/main.script\"\n" +"}\n" +"" +``` + +### Double nesting level (game object with embedded components) + +The embedded component's `data` field requires double escaping: +- Outer quotes: `\"` +- Inner quotes (inside component data): `\\\"` +- Outer newlines: `\n` +- Inner newlines (inside component data): `\\n` + +```protobuf +data: "embedded_components {\n" +" id: \"camera\"\n" +" type: \"camera\"\n" +" data: \"aspect_ratio: 1.0\\n" +"fov: 0.7854\\n" +"near_z: -1.0\\n" +"far_z: 1.0\\n" +"orthographic_projection: 1\\n" +"\"\\n" +"}\n" +"" +``` + +Note: After the opening `data: \"...\\n"` line inside the double-nested data, subsequent lines of that inner data do NOT have leading whitespace (they start at column 0 of the quoted string). + +## Protobuf Text Format rules + +1. **Default omission**: Omit optional fields that equal their proto default (exception: `scale_along_z` is always written by the editor). +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Include decimal point for clarity: `1.0`, not `1`. Inside embedded `data` strings, Defold may write integer-like values for `uint32` fields (e.g., `orthographic_projection: 1`). +4. **Integers**: No decimal point: `0`, not `0.0`. +5. **Strings**: Always double-quoted. +6. **Enums**: Use the constant name without quotes. +7. **Booleans**: `true` or `false`, no quotes. +8. **Repeated fields**: Each value gets its own line with the field name. +9. **Repeated messages**: Each entry gets its own `field_name { ... }` block. +10. **Field order**: Follow the proto field number order. +11. **No trailing commas or semicolons**. +12. **Indentation**: 2 spaces per nesting level inside message blocks. +13. **No blank lines** between fields or blocks (match Defold editor output). +14. **Embedded data strings**: Each line is a separate quoted string. Escape inner quotes with `\"`. End lines with `\n`. Terminate multi-line concatenations with empty `""`. +15. **When editing existing files**, preserve the existing formatting style. + +## Common templates + +### Minimal collection (empty) + +```protobuf +name: "empty" +scale_along_z: 0 +``` + +### Collection with a single embedded game object + +```protobuf +name: "level" +scale_along_z: 0 +embedded_instances { + id: "go" + data: "components {\n" + " id: \"script\"\n" + " component: \"/main/level.script\"\n" + "}\n" + "" +} +``` + +### Collection with referenced game objects + +```protobuf +name: "level" +instances { + id: "player" + prototype: "/main/player.go" + position { + x: 320.0 + y: 240.0 + } +} +instances { + id: "enemy" + prototype: "/main/enemy.go" + position { + x: 600.0 + y: 240.0 + } +} +scale_along_z: 0 +``` + +### Collection with a sub-collection + +```protobuf +name: "main" +collection_instances { + id: "level" + collection: "/main/level1.collection" +} +scale_along_z: 0 +``` + +### Collection with parent-child hierarchy + +```protobuf +name: "level" +scale_along_z: 0 +embedded_instances { + id: "parent" + children: "child" + data: "components {\n" + " id: \"script\"\n" + " component: \"/main/parent.script\"\n" + "}\n" + "" + position { + x: 320.0 + y: 240.0 + } +} +embedded_instances { + id: "child" + data: "embedded_components {\n" + " id: \"sprite\"\n" + " type: \"sprite\"\n" + " data: \"default_animation: \\\"idle\\\"\\n" + "material: \\\"/builtins/materials/sprite.material\\\"\\n" + "textures {\\n" + " sampler: \\\"texture_sampler\\\"\\n" + " texture: \\\"/main/main.atlas\\\"\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: 50.0 + } +} +``` + +### Collection with a GUI component + +GUI components (`.gui`) CANNOT be embedded inline. They must always be added as **referenced components** using a `components` block pointing to a `.gui` file. + +```protobuf +name: "gameplay" +scale_along_z: 0 +embedded_instances { + id: "go" + data: "components {\n" + " id: \"gui\"\n" + " component: \"/screens/gameplay/gameplay.gui\"\n" + "}\n" + "" +} +``` + +### Collection with embedded camera (typical 2D setup) + +```protobuf +name: "main" +scale_along_z: 0 +embedded_instances { + id: "camera" + data: "embedded_components {\n" + " id: \"camera\"\n" + " type: \"camera\"\n" + " data: \"aspect_ratio: 1.0\\n" + "fov: 0.7854\\n" + "near_z: -1.0\\n" + "far_z: 1.0\\n" + "orthographic_projection: 1\\n" + "orthographic_mode: ORTHO_MODE_AUTO_COVER\\n" + "\"\n" + "}\n" + "" + position { + x: 640.0 + y: 360.0 + } +} +``` + +## Workflow + +### Creating a new collection + +1. Determine the file path (must end with `.collection`). +2. Set the `name` field (typically matching the file name without extension). +3. Add game objects as `instances` (file references) or `embedded_instances` (inline). +4. Add sub-collections as `collection_instances` if needed. +5. Set `scale_along_z: 0` (always include this field). +6. Set `position`, `rotation`, `scale3` on instances only if they differ from defaults. +7. Establish parent-child hierarchies with `children` fields if needed. +8. For embedded instances, encode the game object data following the data string encoding rules. +9. Write fields in proto field number order: `name`, `instances`, `collection_instances`, `scale_along_z`, `embedded_instances`. + +### Editing an existing collection + +1. Read the current `.collection` file. +2. Modify only the requested game objects, sub-collections, or properties. +3. Preserve existing instance order and field values. +4. When adding new instances, place them after existing instances of the same type. +5. Apply omission rules: remove fields that become equal to their defaults after editing. +6. Maintain parent-child relationships: if removing a game object, also remove it from any parent's `children` list. diff --git a/.agents/skills/defold-proto-file-editing/references/collectionfactory.md b/.agents/skills/defold-proto-file-editing/references/collectionfactory.md new file mode 100644 index 0000000..bd952b6 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/collectionfactory.md @@ -0,0 +1,280 @@ + +# Editing Collection Factory Files + +Defold collection factory component (`.collectionfactory`) — spawns entire collections (hierarchies of game objects) dynamically at runtime. + +## Overview + +A collection factory creates copies of a `.collection` file at runtime. Unlike a regular factory (which spawns a single `.go`), a collection factory spawns all game objects in the collection while preserving parent-child relationships. + +**Key differences from factory:** +- **Prototype**: references a `.collection` file (not a `.go`) +- **Return value**: `collectionfactory.create()` returns a table mapping collection-local ids to runtime ids +- **Properties**: passed per game object using `id`-`table` pairs +- **API namespace**: `collectionfactory.*` (not `factory.*`) + +**Key concepts:** +- **Prototype**: The `.collection` file used as template +- **Dynamic loading**: Defer resource loading until first spawn or explicit `collectionfactory.load()` +- **Dynamic prototype**: Allow changing the collection prototype at runtime with `collectionfactory.set_prototype()` + +## File format + +Collection factory files use **Protobuf Text Format** based on the `CollectionFactoryDesc` message from `gamesys/gamesys_ddf.proto`. + +## Canonical example + +Minimal collection factory (most common): +```protobuf +prototype: "/game/enemy_group.collection" +``` + +Full collection factory with all options: +```protobuf +prototype: "/game/level_chunk.collection" +load_dynamically: true +dynamic_prototype: true +``` + +## Fields reference + +### prototype +- **Required**: Yes +- **Type**: string (resource path) +- **Description**: Path to the `.collection` file used as template for spawned object hierarchies. Must be an absolute project path starting with `/`. +- **Omission rule**: Cannot be omitted (required field). + +```protobuf +prototype: "/game/enemy_group.collection" +``` + +### load_dynamically +- **Required**: No +- **Type**: bool +- **Default**: `false` +- **Description**: When `false`, prototype resources are loaded when the collection factory's parent collection loads. When `true`, resources are loaded on first `collectionfactory.create()` (synchronously) or via explicit `collectionfactory.load()` (asynchronously). +- **Omission rule**: Omit when `false`. + +```protobuf +prototype: "/game/enemy_group.collection" +load_dynamically: true +``` + +**Usage patterns**: +- `false` (default): Resources ready immediately, higher memory usage +- `true` with `collectionfactory.create()`: Synchronous load on first spawn (may cause hitch) +- `true` with `collectionfactory.load()`: Asynchronous pre-loading with callback + +### dynamic_prototype +- **Required**: No +- **Type**: bool +- **Default**: `false` +- **Description**: When `true`, allows changing the prototype at runtime using `collectionfactory.set_prototype()`. Disables component count optimization — the collection uses default counts from `game.project`. +- **Omission rule**: Omit when `false`. + +```protobuf +prototype: "/game/level_chunk.collection" +dynamic_prototype: true +``` + +## Common templates + +### Basic collection factory (static, pre-loaded) +```protobuf +prototype: "/game/enemy_group.collection" +``` + +### Lazy-loaded collection factory (load on demand) +```protobuf +prototype: "/game/popup.collection" +load_dynamically: true +``` + +### Dynamic prototype collection factory (switchable at runtime) +```protobuf +prototype: "/game/level1.collection" +load_dynamically: true +dynamic_prototype: true +``` + +## Embedded collection factory in game object + +Collection factories are typically embedded in `.go` or `.collection` files: + +Minimal embedded collection factory: +```protobuf +embedded_components { + id: "collectionfactory" + type: "collectionfactory" + data: "prototype: \"/game/enemy_group.collection\"\n" + "" +} +``` + +With load_dynamically: +```protobuf +embedded_components { + id: "collectionfactory" + type: "collectionfactory" + data: "prototype: \"/bubbles/popups/settings.collection\"\n" + "load_dynamically: true\n" + "" +} +``` + +With position and rotation: +```protobuf +embedded_components { + id: "spawner" + type: "collectionfactory" + data: "prototype: \"/game/enemy_group.collection\"\n" + "load_dynamically: true\n" + "" + position { + x: 0.0 + y: 0.0 + z: 0.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } +} +``` + +## Runtime API + +### Creating objects + +```lua +-- Basic spawn at collection factory position +local ids = collectionfactory.create("#enemy_factory") + +-- Spawn at specific position +local pos = vmath.vector3(100, 200, 0) +local ids = collectionfactory.create("#enemy_factory", pos) + +-- Spawn with rotation +local rot = vmath.quat_rotation_z(math.pi / 4) +local ids = collectionfactory.create("#enemy_factory", pos, rot) + +-- Spawn with per-object script properties +local props = {} +props[hash("/enemy")] = { speed = 100, health = 50 } +props[hash("/weapon")] = { damage = 10 } +local ids = collectionfactory.create("#enemy_factory", pos, nil, props) + +-- Spawn with scale (uniform) +local ids = collectionfactory.create("#enemy_factory", pos, nil, nil, 2.0) +``` + +### Accessing spawned objects + +`collectionfactory.create()` returns a table mapping collection-local hash ids to runtime ids: + +```lua +local ids = collectionfactory.create("#bean_factory") +-- ids = { +-- hash("/bean") = hash("/collection0/bean"), +-- hash("/shield") = hash("/collection0/shield"), +-- } + +-- Access a specific spawned object by its collection-local id +local bean_id = ids[hash("/bean")] +go.set_scale(0.5, bean_id) + +-- Send message to a spawned object +msg.post(ids[hash("/enemy")], "activate") + +-- Access component on a spawned object +local sprite_url = msg.url(nil, ids[hash("/enemy")], "sprite") +sprite.play_flipbook(sprite_url, hash("run")) +``` + +### Dynamic loading + +```lua +-- Asynchronous loading with callback +local function on_loaded(self, url, result) + if result then + local ids = collectionfactory.create(url) + end +end + +function init(self) + collectionfactory.load("#enemy_factory", on_loaded) +end + +function final(self) + -- Unload when done + collectionfactory.unload("#enemy_factory") +end +``` + +### Changing prototype (requires dynamic_prototype: true) + +```lua +-- Unload current resources +collectionfactory.unload("#factory") + +-- Set new prototype (uses .collectionc — compiled collection) +collectionfactory.set_prototype("#factory", "/main/levels/level1.collectionc") + +-- Create uses new prototype +local ids = collectionfactory.create("#factory") +``` + +### Tracking and deleting spawned collections + +```lua +function init(self) + self.spawned_groups = {} +end + +function spawn_enemy_group(self, pos) + local ids = collectionfactory.create("#enemy_factory", pos) + table.insert(self.spawned_groups, ids) + return ids +end + +function cleanup(self) + -- Delete all spawned groups + for _, ids in ipairs(self.spawned_groups) do + go.delete(ids) + end + self.spawned_groups = {} +end +``` + +## Instance limits + +The `max_instances` setting in `game.project` (Collection related settings) limits total game objects in a world. Each collection factory spawn creates multiple objects — all count against this limit. + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default +2. **Floats**: Always include decimal point: `1.0`, not `1` +3. **Strings**: Always double-quoted +4. **Booleans**: `true` or `false`, no quotes +5. **Field order**: `prototype`, `load_dynamically`, `dynamic_prototype` +6. **No trailing commas or semicolons** +7. **Embedded data**: Multi-line strings with escaped quotes and `\n` + +## Workflow + +### Creating a new collection factory + +1. Create the prototype `.collection` file first (with its game objects and hierarchy) +2. Create `.collectionfactory` file or embed in parent `.go` / `.collection` +3. Set `prototype` to the `.collection` path +4. Enable `load_dynamically` if resources should load on demand +5. Enable `dynamic_prototype` if prototype will change at runtime + +### Editing an existing collection factory + +1. Read the current file content +2. Modify fields as needed +3. Ensure required `prototype` field is present +4. Omit optional fields that equal defaults diff --git a/.agents/skills/defold-proto-file-editing/references/collectionproxy.md b/.agents/skills/defold-proto-file-editing/references/collectionproxy.md new file mode 100644 index 0000000..3ea55a2 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/collectionproxy.md @@ -0,0 +1,274 @@ + +# Editing Collection Proxy Files + +Defold collection proxy component (`.collectionproxy`) — dynamically loads and unloads separate game worlds from collection files. + +## Overview + +A collection proxy loads the contents of a `.collection` file into a **separate game world** with its own physics simulation. Unlike collection factories (which spawn objects into the current world), proxies create isolated worlds accessed through a named socket. + +**Use cases:** +- Level switching +- GUI screens / popup overlays +- Loading/unloading narrative scenes +- Mini-games within a game + +**Key behaviors:** +- Each loaded collection creates a separate physics world — no cross-world physics interactions +- The collection's `Name` property becomes the socket name for addressing (`"mylevel:/object"`) +- Socket names must be unique — loading two collections with the same name causes an error +- Input propagates through the proxy only if the parent game object has acquired input focus + +## File format + +Collection proxy files use **Protobuf Text Format** based on the `CollectionProxyDesc` message from `gamesys/gamesys_ddf.proto`. + +## Canonical example + +Minimal collection proxy (most common): +```protobuf +collection: "/levels/level1.collection" +``` + +With exclude for Live Update: +```protobuf +collection: "/levels/level1.collection" +exclude: true +``` + +## Fields reference + +### collection +- **Required**: Yes +- **Type**: string (resource path) +- **Description**: Path to the `.collection` file to load as a separate game world. Must be an absolute project path starting with `/`. +- **Omission rule**: Cannot be omitted (required field). + +```protobuf +collection: "/levels/level1.collection" +``` + +### exclude +- **Required**: No +- **Type**: bool +- **Default**: `false` +- **Description**: When `true`, excludes the collection's content from the game bundle. The content must then be downloaded separately using the Live Update feature. Used for on-demand content delivery. +- **Omission rule**: Omit when `false`. + +```protobuf +collection: "/levels/level1.collection" +exclude: true +``` + +## Common templates + +### Standard level proxy +```protobuf +collection: "/levels/level1.collection" +``` + +### Live Update proxy (excluded content) +```protobuf +collection: "/levels/bonus_level.collection" +exclude: true +``` + +## Embedded collection proxy in game object + +Collection proxies are typically embedded in `.go` or `.collection` files: + +Minimal embedded: +```protobuf +embedded_components { + id: "levelproxy" + type: "collectionproxy" + data: "collection: \"/levels/level1.collection\"\n" + "" +} +``` + +With exclude: +```protobuf +embedded_components { + id: "menuproxy" + type: "collectionproxy" + data: "collection: \"/example/menu.collection\"\n" + "exclude: false\n" + "" + position { + x: 0.0 + y: 0.0 + z: 0.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } +} +``` + +Multiple proxies on same game object (level switcher pattern): +```protobuf +embedded_components { + id: "menuproxy" + type: "collectionproxy" + data: "collection: \"/example/menu.collection\"\n" + "" +} +embedded_components { + id: "level1proxy" + type: "collectionproxy" + data: "collection: \"/example/level1.collection\"\n" + "" +} +embedded_components { + id: "level2proxy" + type: "collectionproxy" + data: "collection: \"/example/level2.collection\"\n" + "" +} +``` + +## Runtime API + +### Loading lifecycle + +```lua +-- Step 1: Load the collection (async) +msg.post("#levelproxy", "async_load") +-- or synchronous (blocks until loaded): +msg.post("#levelproxy", "load") + +-- Step 2: Handle proxy_loaded callback +function on_message(self, message_id, message, sender) + if message_id == hash("proxy_loaded") then + -- Step 3: Initialize and enable the world + msg.post(sender, "init") + msg.post(sender, "enable") + end +end +``` + +### Unloading lifecycle + +```lua +-- Full explicit unload +msg.post("#levelproxy", "disable") +msg.post("#levelproxy", "final") +msg.post("#levelproxy", "unload") + +-- Or simplified — unload auto-disables and finalizes +msg.post("#levelproxy", "unload") + +-- Handle unload completion +function on_message(self, message_id, message, sender) + if message_id == hash("proxy_unloaded") then + -- World fully unloaded + end +end +``` + +### Level switching pattern + +```lua +local function show(self, proxy) + if self.current_proxy then + msg.post(self.current_proxy, "unload") + self.current_proxy = nil + end + msg.post(proxy, "async_load") +end + +function init(self) + msg.post(".", "acquire_input_focus") + self.current_proxy = nil + show(self, "#menuproxy") +end + +function on_message(self, message_id, message, sender) + if message_id == hash("proxy_loaded") then + self.current_proxy = sender + msg.post(sender, "enable") + elseif message_id == hash("proxy_unloaded") then + print("Unloaded", sender) + end +end +``` + +### Time step control + +```lua +-- Pause the loaded world (factor=0) +msg.post("#levelproxy", "set_time_step", { factor = 0, mode = 0 }) + +-- Resume at normal speed +msg.post("#levelproxy", "set_time_step", { factor = 1, mode = 1 }) + +-- Slow motion (20% speed, discrete) +msg.post("#levelproxy", "set_time_step", { factor = 0.2, mode = 1 }) + +-- Double speed (continuous) +msg.post("#levelproxy", "set_time_step", { factor = 2, mode = 0 }) +``` + +Time step modes: +- `0` — `TIME_STEP_MODE_CONTINUOUS`: dt is scaled continuously +- `1` — `TIME_STEP_MODE_DISCRETE`: dt alternates between 0 and full frame dt (useful for factors < 1.0) + +### Cross-world addressing + +```lua +-- From loaded collection, address bootstrap world +msg.post("main:/loader#script", "load_level", { level_id = 2 }) + +-- From bootstrap world, address loaded collection +msg.post("mylevel:/myobject", "hello") +``` + +**Limitation**: `go.get_position()` and similar functions can only access objects within the same collection. + +### Input propagation + +The game object containing the proxy must acquire input focus for input to reach the loaded collection: + +```lua +function init(self) + msg.post(".", "acquire_input_focus") + msg.post("#levelproxy", "async_load") +end +``` + +## Caveats + +- **Separate physics worlds**: Objects in different proxied collections cannot physically interact (no collisions, triggers, or ray-casts across worlds) +- **Memory overhead**: Each loaded collection creates a full game world — avoid loading many simultaneously +- **Unique names**: Each loaded collection must have a unique `Name` property (set in the collection file) +- **For spawning objects**: Use collection factories instead of proxies when spawning many instances into the same world + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default +2. **Strings**: Always double-quoted +3. **Booleans**: `true` or `false`, no quotes +4. **Field order**: `collection`, `exclude` +5. **No trailing commas or semicolons** +6. **Embedded data**: Multi-line strings with escaped quotes and `\n` + +## Workflow + +### Creating a new collection proxy + +1. Create the target `.collection` file first +2. Ensure the collection's `Name` property is unique across all loaded worlds +3. Create `.collectionproxy` file or embed in parent `.go` / `.collection` +4. Set `collection` to the `.collection` path +5. Set `exclude: true` only if using Live Update + +### Editing an existing collection proxy + +1. Read the current file content +2. Modify fields as needed +3. Ensure required `collection` field is present +4. Omit optional fields that equal defaults diff --git a/.agents/skills/defold-proto-file-editing/references/collisionobject.md b/.agents/skills/defold-proto-file-editing/references/collisionobject.md new file mode 100644 index 0000000..7678d2b --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/collisionobject.md @@ -0,0 +1,594 @@ +# Editing Collision Objects + +Creates and edits Defold `.collisionobject` component files using Protobuf Text Format. + +## File format + +Collision object files (`.collisionobject`) use **Protobuf Text Format** based on the `CollisionObjectDesc` message from `physics_ddf.proto`. + +### Canonical example + +```protobuf +type: COLLISION_OBJECT_TYPE_STATIC +mass: 0.0 +friction: 0.1 +restitution: 0.5 +group: "ground" +mask: "entity" +mask: "world" +embedded_collision_shape { + shapes { + shape_type: TYPE_BOX + position { + y: 2.0 + } + rotation { + z: 0.25881904 + w: 0.9659258 + } + index: 0 + count: 3 + } + data: 2.5 + data: 2.5 + data: 2.5 +} +``` + +## Fields reference + +### collision_shape (optional) — `string` + +Resource path to an external collision shape file (`.convexshape` or tilemap). Use this **instead of** `embedded_collision_shape` when the shape is defined externally. + +**Omission rule**: Omit when using embedded shapes. + +```protobuf +collision_shape: "/main/level.tilemap" +``` + +### type (required) — enum `CollisionObjectType` + +| Value | Description | +|-------|-------------| +| `COLLISION_OBJECT_TYPE_DYNAMIC` | Simulated by the physics engine. Must have non-zero `mass`. | +| `COLLISION_OBJECT_TYPE_KINEMATIC` | Registers collisions but not simulated. Resolve collisions manually. | +| `COLLISION_OBJECT_TYPE_STATIC` | Never moves. Used for level geometry (ground, walls). Cannot be moved at runtime. | +| `COLLISION_OBJECT_TYPE_TRIGGER` | Detects overlaps but does not produce physical reactions. | + +```protobuf +type: COLLISION_OBJECT_TYPE_DYNAMIC +``` + +**Parent transform caveat**: Dynamic collision objects (`COLLISION_OBJECT_TYPE_DYNAMIC`) must NOT be placed in a child game object whose parent has non-zero position or rotation. The physics engine overwrites the game object's world transform every frame with the simulated body's position/rotation, and parent transforms are not taken into account — the object will ignore its parent hierarchy entirely. Static, kinematic, and trigger objects are not affected by this limitation because their transforms are not driven by the physics engine. + +### mass (required) — `float` + +Physical mass of the object. Must be `0.0` for static and kinematic objects. Must be non-zero for dynamic objects. + +```protobuf +mass: 1.0 +``` + +### friction (required) — `float` + +Friction coefficient. Usually `0.0` (slippery) to `1.0` (abrasive). Any positive value is valid. Combined between two shapes via geometric mean: `sqrt(F_A * F_B)`. + +```protobuf +friction: 0.1 +``` + +### restitution (required) — `float` + +Bounciness. `0.0` = inelastic (no bounce), `1.0` = perfectly elastic. Combined between two shapes via `max(R_A, R_B)`. + +```protobuf +restitution: 0.5 +``` + +### group (required) — `string` + +Collision group name this object belongs to. Up to 16 groups per project. Double-quoted. + +```protobuf +group: "enemy" +``` + +### mask (repeated) — `string` + +Other collision groups this object should collide with. Each group is a separate `mask:` line. Both objects must mutually list each other's group in their mask for collision to register. If empty, the object collides with nothing. + +```protobuf +mask: "player" +mask: "world" +``` + +### embedded_collision_shape (optional) — `CollisionShape` + +Inline collision shape definition containing one or more primitive shapes. Use this **instead of** `collision_shape` for embedded primitives. + +See the **Embedded collision shape** section below. + +### linear_damping (optional) — `float` + +Reduces linear velocity. Values between `0.0` and `1.0`. Default: `0.0`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +linear_damping: 0.1 +``` + +### angular_damping (optional) — `float` + +Reduces angular velocity. Values between `0.0` and `1.0`. Default: `0.0`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +angular_damping: 0.1 +``` + +### locked_rotation (optional) — `bool` + +Disables rotation entirely. Default: `false`. + +**Omission rule**: Omit if `false`. + +```protobuf +locked_rotation: true +``` + +### bullet (optional) — `bool` + +Enables continuous collision detection (CCD). Only applies when `type` is `COLLISION_OBJECT_TYPE_DYNAMIC`. Default: `false`. + +**Omission rule**: Omit if `false`. + +```protobuf +bullet: true +``` + +### event_collision (optional) — `bool` + +Generate collision events. Default: `true`. + +**Omission rule**: Omit if `true`. + +```protobuf +event_collision: false +``` + +### event_contact (optional) — `bool` + +Generate contact events. Default: `true`. + +**Omission rule**: Omit if `true`. + +```protobuf +event_contact: false +``` + +### event_trigger (optional) — `bool` + +Generate trigger events. Default: `true`. + +**Omission rule**: Omit if `true`. + +```protobuf +event_trigger: false +``` + +## Embedded collision shape + +The `embedded_collision_shape` block contains a `CollisionShape` message with `shapes` and `data`. + +### Structure + +```protobuf +embedded_collision_shape { + shapes { + shape_type: TYPE_BOX + position { + } + rotation { + } + index: 0 + count: 3 + } + data: 50.0 + data: 50.0 + data: 50.0 +} +``` + +### Shape message fields + +#### shape_type (required) — enum `Type` + +| Value | Description | Data count | +|-------|-------------|------------| +| `TYPE_SPHERE` | Sphere shape | 1 (`radius`) | +| `TYPE_BOX` | Box shape | 3 (`ext_x`, `ext_y`, `ext_z` — half-extents) | +| `TYPE_CAPSULE` | Capsule shape (3D physics only) | 2 (`radius`, `height`) | +| `TYPE_HULL` | Convex hull | N (`x0, y0, z0, x1, y1, z1, ...`) | + +#### position (required) — `dmMath.Point3` + +Local position offset of the shape. Components default to `0.0`. + +- `x` — (default: `0.0`) +- `y` — (default: `0.0`) +- `z` — (default: `0.0`) + +**Omission rule**: Include the `position` block always (it is required), but only include components that differ from `0.0`. + +```protobuf +position { + y: 25.0 +} +``` + +#### rotation (required) — `dmMath.Quat` + +Local rotation of the shape as a quaternion. Defaults: `x: 0.0`, `y: 0.0`, `z: 0.0`, `w: 1.0`. + +**Omission rule**: Include the `rotation` block always (it is required), but only include components that differ from their defaults. + +```protobuf +rotation { + z: 0.7071068 + w: 0.7071068 +} +``` + +#### index (required) — `uint32` + +Starting index into the `data` array for this shape's data. + +#### count (required) — `uint32` + +Number of `data` entries used by this shape. + +#### id (optional) — `string` + +Shape identifier for runtime shape manipulation via `physics.set_shape()`. + +```protobuf +id: "my_box" +``` + +### Data array + +The `data` entries are flat repeated floats at the `embedded_collision_shape` level. Each shape references its portion via `index` and `count`. + +**Sphere** — 1 float: `radius` +```protobuf +data: 25.0 +``` + +**Box** — 3 floats: `ext_x`, `ext_y`, `ext_z` (half-extents, so a box with data `50, 50, 50` is 100x100x100 in size) +```protobuf +data: 50.0 +data: 50.0 +data: 50.0 +``` + +**Capsule** — 2 floats: `radius`, `height` +```protobuf +data: 10.0 +data: 40.0 +``` + +**Hull** — N floats: `x0, y0, z0, x1, y1, z1, ...` (points in counter-clockwise order for 2D) +```protobuf +data: 0.0 +data: 0.0 +data: 0.0 +data: 100.0 +data: 0.0 +data: 0.0 +data: 100.0 +data: 100.0 +data: 0.0 +``` + +### Multiple shapes + +When embedding multiple shapes, each shape references its slice of the shared `data` array via `index` and `count`. + +```protobuf +embedded_collision_shape { + shapes { + shape_type: TYPE_SPHERE + position { + } + rotation { + } + index: 0 + count: 1 + } + shapes { + shape_type: TYPE_BOX + position { + x: 30.0 + } + rotation { + } + index: 1 + count: 3 + } + data: 15.0 + data: 20.0 + data: 20.0 + data: 20.0 +} +``` + +## Matching collision shape to sprite image + +When creating a collision object for a sprite, choose the shape type based on the sprite's visual form: + +| Approach | When to use | How | +|----------|-------------|-----| +| **Box shape** (embedded `TYPE_BOX`) | Rectangular sprites, platforms, walls, UI elements | Use `get_image_size.py` → half-extents → `TYPE_BOX` data | +| **Convex hull** (external `.convexshape`) | Characters, irregular convex shapes, sprites with transparency | Use `gen_convexshape.py` → `.convexshape` file → `collision_shape` property | +| **Silhouette chain** (embedded multi-box) | Concave shapes, race tracks, complex outlines, static level geometry | Use `gen_silhouette_chain.py` → `.collisionobject` file with rotated `TYPE_BOX` shapes along contour | + +Ask the user which approach they prefer when the choice is ambiguous. + +For convex hull generation from images, see `references/convexshape.md` → "Generating from an image". + +### Box shape from sprite image + +When creating a collision object that should match a sprite's visual size, determine the image dimensions first and use **half-extents** (half the pixel size) for the box shape data. + +### Workflow + +1. Find the image path from the atlas (`.atlas` file) referenced by the sprite. The atlas `images { image: "/assets/player.png" }` field gives the resource path. +2. Run the image size script (no external dependencies, pure Python stdlib): + +``` +python .agents/skills/defold-proto-file-editing/scripts/get_image_size.py / +``` + +The `` is the path from the atlas file **without** the leading `/`. The script outputs: ` `. + +Example: +``` +python .agents/skills/defold-proto-file-editing/scripts/get_image_size.py assets/images/player.png +# Output: assets/images/player.png 64 128 +``` + +3. Calculate half-extents: `ext_x = width / 2`, `ext_y = height / 2`, `ext_z = width / 2` (or `10.0` for 2D games where z doesn't matter). +4. Use the half-extents in the `data` fields of the box shape. + +### Example + +For a 64×128 pixel sprite image: + +```protobuf +embedded_collision_shape { + shapes { + shape_type: TYPE_BOX + position { + } + rotation { + } + index: 0 + count: 3 + } + data: 32.0 + data: 64.0 + data: 10.0 +} +``` + +### Multiple images in one command + +The script accepts multiple paths: + +``` +python .agents/skills/defold-proto-file-editing/scripts/get_image_size.py assets/a.png assets/b.jpg +# Output: +# assets/a.png 64 64 +# assets/b.jpg 128 256 +``` + +## Common templates + +### Static ground (box) + +```protobuf +type: COLLISION_OBJECT_TYPE_STATIC +mass: 0.0 +friction: 0.1 +restitution: 0.5 +group: "ground" +mask: "player" +mask: "enemy" +embedded_collision_shape { + shapes { + shape_type: TYPE_BOX + position { + } + rotation { + } + index: 0 + count: 3 + } + data: 500.0 + data: 10.0 + data: 10.0 +} +``` + +### Dynamic ball (sphere) + +```protobuf +type: COLLISION_OBJECT_TYPE_DYNAMIC +mass: 1.0 +friction: 0.3 +restitution: 0.8 +group: "ball" +mask: "ground" +mask: "wall" +embedded_collision_shape { + shapes { + shape_type: TYPE_SPHERE + position { + } + rotation { + } + index: 0 + count: 1 + } + data: 16.0 +} +``` + +### Kinematic player (box) + +```protobuf +type: COLLISION_OBJECT_TYPE_KINEMATIC +mass: 0.0 +friction: 0.0 +restitution: 0.0 +group: "player" +mask: "ground" +mask: "enemy" +mask: "pickup" +embedded_collision_shape { + shapes { + shape_type: TYPE_BOX + position { + } + rotation { + } + index: 0 + count: 3 + } + data: 8.0 + data: 16.0 + data: 8.0 +} +``` + +### Trigger zone (sphere) + +```protobuf +type: COLLISION_OBJECT_TYPE_TRIGGER +mass: 0.0 +friction: 0.0 +restitution: 0.0 +group: "trigger" +mask: "player" +embedded_collision_shape { + shapes { + shape_type: TYPE_SPHERE + position { + } + rotation { + } + index: 0 + count: 1 + } + data: 64.0 +} +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit optional fields that equal their proto default. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Strings**: Always double-quoted. +5. **Enums**: Use the constant name without quotes. +6. **Booleans**: `true` or `false`, no quotes. +7. **Repeated fields**: Each value gets its own line with the field name (e.g., `mask:`, `data:`). +8. **Field order**: Follow the proto field number order. +9. **No trailing commas or semicolons**. +10. **Indentation**: 2 spaces per nesting level inside message blocks. + +## Workflow + +### Creating a new collision object + +1. Determine object type (`STATIC`, `DYNAMIC`, `KINEMATIC`, `TRIGGER`). +2. Set `mass` (non-zero for dynamic, `0.0` for others). +3. Set `friction` and `restitution`. +4. Set `group` and `mask` entries. +5. Define shapes in `embedded_collision_shape` or reference an external `collision_shape`. +6. Add optional fields (`linear_damping`, `angular_damping`, etc.) only if they differ from defaults. + +### Editing an existing collision object + +1. Read the current `.collisionobject` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules for fields that become equal to their defaults. + +## Silhouette chain from image contour + +For static level geometry with complex, concave, or multi-part shapes (e.g., race tracks, terrain outlines, irregular platforms), use the `gen_silhouette_chain.py` script to generate a `.collisionobject` file with thin, rotated `TYPE_BOX` shapes that trace the contour polygon of the image silhouette. + +This simulates concave collision in Box2D (which only supports convex primitives) by placing narrow boxes along every edge of the simplified boundary polygon. + +### How it works + +1. Reads the image and extracts the alpha channel +2. Extracts directed boundary edges between opaque and transparent regions on the pixel grid +3. Chains edges into closed contour loops (handles shapes touching image edges, multiple components, and holes) +4. Simplifies each contour with Ramer-Douglas-Peucker (controlled by `--epsilon`) +5. For each edge of the simplified polygon, emits a thin `TYPE_BOX` shape: + - **position** = edge midpoint in image-centred, Y-up Defold coordinates + - **rotation** = quaternion aligning the box along the edge angle + - **half-extents** = `(half_edge_length, thickness, 10.0)` +6. Outputs a ready-to-use `.collisionobject` with `COLLISION_OBJECT_TYPE_STATIC` + +### Usage + +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_silhouette_chain.py [options] +``` + +Arguments: +- `image_path` — path to PNG or JPEG image +- `--output`, `-o` — output `.collisionobject` file path (default: prints to stdout) +- `--epsilon`, `-e` — RDP simplification tolerance in pixels (default: 2.0). Lower = more edges, higher fidelity +- `--thickness`, `-t` — half-thickness of wall boxes in pixels (default: 2.0) +- `--alpha-threshold`, `-a` — alpha threshold for "non-transparent" pixels, 0-255 (default: 1) +- `--group`, `-g` — collision group (default: `"geometry"`) +- `--mask` — collision mask group, repeatable (default: `"default"`) +- `--friction` — friction coefficient (default: 0.1) +- `--restitution` — restitution / bounciness (default: 0.5) + +### Examples + +Generate collision for a race track: +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_silhouette_chain.py assets/images/track.png -o main/track.collisionobject +``` + +Higher fidelity contour (smaller epsilon): +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_silhouette_chain.py assets/images/terrain.png -o main/terrain.collisionobject -e 1.0 +``` + +With custom physics properties: +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_silhouette_chain.py assets/images/wall.png -o main/wall.collisionobject --group "ground" --mask "player" --mask "enemy" --friction 0.3 +``` + +Thicker walls for more forgiving collision: +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_silhouette_chain.py assets/images/border.png -o main/border.collisionobject -t 4.0 +``` + +### Choosing the right epsilon + +| Epsilon | Result | +|---------|--------| +| 0.5–1.0 | High fidelity, many boxes. Use for small detailed sprites. | +| 2.0–4.0 | Good balance. Default is 2.0. | +| 8.0+ | Very simplified contour, few boxes. Use for large coarse shapes. | diff --git a/.agents/skills/defold-proto-file-editing/references/convexshape.md b/.agents/skills/defold-proto-file-editing/references/convexshape.md new file mode 100644 index 0000000..4428234 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/convexshape.md @@ -0,0 +1,250 @@ +# Convex Shape (.convexshape) + +Proto message: `CollisionShape` from `physics_ddf.proto`. + +A convex shape file defines an external collision shape that can be referenced by a collision object's `collision_shape` property. It uses the same `CollisionShape` message as the `embedded_collision_shape` block inside `.collisionobject` files, but as a standalone file. + +## When to use + +- When you need a convex hull collision shape (more than primitive box/sphere/capsule). +- When you want to share a collision shape across multiple collision objects. +- When you need a polygon-based collision boundary that matches a sprite's outline. + +## File format + +The file uses **Protobuf Text Format** with top-level `shape_type`, `data`, and optionally `shapes` blocks. + +### Canonical example — convex hull (2D rectangle) + +```protobuf +shape_type: TYPE_HULL +data: 200.0 +data: 100.0 +data: 0.0 +data: 400.0 +data: 100.0 +data: 0.0 +data: 400.0 +data: 300.0 +data: 0.0 +data: 200.0 +data: 300.0 +data: 0.0 +``` + +This defines a rectangle with corners at (200,100), (400,100), (400,300), (200,300): + +``` + 200x300 400x300 + 4---------3 + | | + | | + 1---------2 + 200x100 400x100 +``` + +## Fields reference + +### shape_type (required) — enum `Type` + +The shape type. For `.convexshape` files, typically `TYPE_HULL`. + +| Value | Description | Data format | +|-------|-------------|-------------| +| `TYPE_SPHERE` | Sphere shape | 1 float: `radius` | +| `TYPE_BOX` | Box shape | 3 floats: `ext_x`, `ext_y`, `ext_z` (half-extents) | +| `TYPE_CAPSULE` | Capsule shape (3D only) | 2 floats: `radius`, `height` | +| `TYPE_HULL` | Convex hull | N floats: `x0, y0, z0, x1, y1, z1, ...` (vertices) | + +```protobuf +shape_type: TYPE_HULL +``` + +### data (repeated) — `float` + +Flat array of floats defining the shape geometry. Each value is on its own `data:` line. + +For **TYPE_HULL**, data contains vertex positions as triplets `(x, y, z)`. In 2D physics, `z` is typically `0.0`. Points must be in **counter-clockwise order** for 2D physics. An abstract point cloud is used for 3D physics. + +```protobuf +data: 0.0 +data: 0.0 +data: 0.0 +data: 100.0 +data: 0.0 +data: 0.0 +data: 100.0 +data: 100.0 +data: 0.0 +data: 0.0 +data: 100.0 +data: 0.0 +``` + +### shapes (optional, repeated) — `Shape` + +When present, defines multiple shapes with position/rotation offsets. Each shape references a slice of the `data` array via `index` and `count`. This uses the same structure as `embedded_collision_shape.shapes` in `.collisionobject` files — see `collisionobject.md` for the full `Shape` field reference. + +For simple single-shape convex hulls, the `shapes` block is typically omitted. + +## Common templates + +### Simple convex hull — triangle (2D) + +```protobuf +shape_type: TYPE_HULL +data: 0.0 +data: 0.0 +data: 0.0 +data: 64.0 +data: 0.0 +data: 0.0 +data: 32.0 +data: 64.0 +data: 0.0 +``` + +### Convex hull — pentagon (2D) + +```protobuf +shape_type: TYPE_HULL +data: 32.0 +data: 0.0 +data: 0.0 +data: 64.0 +data: 24.0 +data: 0.0 +data: 50.0 +data: 64.0 +data: 0.0 +data: 14.0 +data: 64.0 +data: 0.0 +data: 0.0 +data: 24.0 +data: 0.0 +``` + +### Centered rectangle matching a sprite (2D) + +For a sprite of size `W × H`, center the hull at origin using half-extents. Points in counter-clockwise order: + +```protobuf +shape_type: TYPE_HULL +data: -32.0 +data: -32.0 +data: 0.0 +data: 32.0 +data: -32.0 +data: 0.0 +data: 32.0 +data: 32.0 +data: 0.0 +data: -32.0 +data: 32.0 +data: 0.0 +``` + +This defines a 64×64 hull centered at origin. + +## Usage with collision objects + +Reference the `.convexshape` file from a collision object's `collision_shape` field: + +```protobuf +collision_shape: "/main/player.convexshape" +type: COLLISION_OBJECT_TYPE_KINEMATIC +mass: 0.0 +friction: 0.1 +restitution: 0.5 +group: "player" +mask: "ground" +mask: "enemy" +``` + +When using `collision_shape`, do **not** include `embedded_collision_shape` — they are mutually exclusive. + +## Protobuf Text Format rules + +1. **Floats**: Always include decimal point: `1.0`, not `1`. +2. **Enums**: Use the constant name without quotes: `TYPE_HULL`. +3. **Repeated fields**: Each `data:` value gets its own line. +4. **No trailing commas or semicolons**. +5. **Field order**: `shape_type`, then `data` entries. + +## Workflow + +### Creating a new convex shape + +1. Determine the vertices of the convex hull. +2. For 2D physics, order points **counter-clockwise** and set `z` to `0.0`. +3. Set `shape_type: TYPE_HULL`. +4. Add each vertex coordinate as a separate `data:` line (x, y, z per vertex). +5. Reference the file from the collision object's `collision_shape` property. + +### Sizing from a sprite image (rectangular) + +To create a rectangular convex hull matching a sprite's dimensions: + +1. Get the image size using the bundled script: + ``` + python .agents/skills/defold-proto-file-editing/scripts/get_image_size.py + ``` +2. Calculate half-extents: `hw = width / 2`, `hh = height / 2`. +3. Define 4 vertices centered at origin (counter-clockwise): + - `(-hw, -hh, 0)`, `(hw, -hh, 0)`, `(hw, hh, 0)`, `(-hw, hh, 0)` + +## Generating from an image + +For sprites with non-rectangular shapes (characters, objects with transparency), use the `gen_convexshape.py` script to automatically generate a convex hull that tightly fits the visible (non-transparent) pixels. + +### How it works + +1. Reads the image and extracts the alpha channel +2. Finds boundary pixels of the non-transparent silhouette +3. Computes a convex hull via Graham scan (Andrew's monotone chain) +4. Simplifies to ≤16 points using Visvalingam-Whyatt area-based simplification (16 is the Box2D vertex limit in Defold) +5. Centers all points at the image origin (0,0) and flips Y axis to match Defold's coordinate system +6. Ensures counter-clockwise winding order (required by Defold 2D physics) +7. Outputs a ready-to-use `.convexshape` file + +### Usage + +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_convexshape.py [--output ] [--max-points N] [--alpha-threshold T] [--inset P] +``` + +Arguments: +- `image_path` — path to PNG or JPEG image (relative to project root) +- `--output`, `-o` — output `.convexshape` file path (default: prints to stdout) +- `--max-points`, `-m` — maximum hull vertices (default: 16) +- `--alpha-threshold`, `-a` — alpha value threshold for "non-transparent" pixels, 0-255 (default: 1) +- `--inset`, `-i` — inset percentage to shrink the shape toward its centroid, 0-100 (default: 0). Useful to make the collision shape slightly smaller than the sprite's visible outline. + +### Examples + +Generate and write directly to a file: +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_convexshape.py assets/images/player.png -o main/player.convexshape +``` + +Preview to stdout first: +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_convexshape.py assets/images/player.png +``` + +Use fewer points for simpler shapes: +``` +python .agents/skills/defold-proto-file-editing/scripts/gen_convexshape.py assets/images/coin.png -o main/coin.convexshape -m 8 +``` + +### Choosing between box shape and convex hull + +When creating collision shapes for sprites, choose the approach based on the sprite's shape: + +| Approach | When to use | How | +|----------|-------------|-----| +| **Box shape** (embedded in `.collisionobject`) | Simple rectangular sprites, UI elements, platforms, walls | Use `get_image_size.py` to get dimensions, calculate half-extents, set as `TYPE_BOX` data in the collision object | +| **Convex hull** (`.convexshape` file) | Characters, irregular objects, sprites with significant transparency around edges | Use `gen_convexshape.py` to generate a `.convexshape` file, reference it via `collision_shape` in the collision object | + +Ask the user which approach they prefer when the choice is ambiguous. diff --git a/.agents/skills/defold-proto-file-editing/references/factory.md b/.agents/skills/defold-proto-file-editing/references/factory.md new file mode 100644 index 0000000..60c9c23 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/factory.md @@ -0,0 +1,270 @@ + +# Editing Factory Files + +Defold factory component (`.factory`) — spawns game objects dynamically at runtime from a prototype. + +## Overview + +A factory component creates copies of a game object prototype during runtime. Each `factory.create()` call instantiates a new game object with optional position, rotation, scale, and script properties. + +**Key concepts**: +- **Prototype**: The `.go` file that serves as a template for spawned objects +- **Dynamic loading**: Optionally defer resource loading until first spawn or explicit `factory.load()` call +- **Dynamic prototype**: Allow changing the prototype at runtime with `factory.set_prototype()` + +## File format + +Factory files use **Protobuf Text Format** based on the `FactoryDesc` message from `gamesys/gamesys_ddf.proto`. + +## Canonical example + +Minimal factory (most common): +```protobuf +prototype: "/game/enemy.go" +``` + +Full factory with all options: +```protobuf +prototype: "/game/bullet.go" +load_dynamically: true +dynamic_prototype: true +``` + +## Fields reference + +### prototype +- **Required**: Yes +- **Type**: string (resource path) +- **Description**: Path to the `.go` file used as template for spawned objects. Must be an absolute project path starting with `/`. +- **Omission rule**: Cannot be omitted (required field). + +```protobuf +prototype: "/game/enemy.go" +``` + +### load_dynamically +- **Required**: No +- **Type**: bool +- **Default**: `false` +- **Description**: When `false`, prototype resources are loaded when the factory's parent collection loads. When `true`, resources are loaded on first `factory.create()` (synchronously) or via explicit `factory.load()` (asynchronously). +- **Omission rule**: Omit when `false`. + +```protobuf +prototype: "/game/enemy.go" +load_dynamically: true +``` + +**Usage patterns**: +- `false` (default): Resources ready immediately, slight memory overhead +- `true` with `factory.create()`: Synchronous load on first spawn (may cause hitch) +- `true` with `factory.load()`: Asynchronous pre-loading with callback + +### dynamic_prototype +- **Required**: No +- **Type**: bool +- **Default**: `false` +- **Description**: When `true`, allows changing the prototype at runtime using `factory.set_prototype()`. Disables component count optimization — the collection uses default counts from `game.project`. +- **Omission rule**: Omit when `false`. + +```protobuf +prototype: "/game/bullet.go" +dynamic_prototype: true +``` + +## Common templates + +### Basic factory (static, pre-loaded) +```protobuf +prototype: "/game/enemy.go" +``` + +### Lazy-loaded factory (load on demand) +```protobuf +prototype: "/game/powerup.go" +load_dynamically: true +``` + +### Dynamic prototype factory (switchable at runtime) +```protobuf +prototype: "/game/bullet_fire.go" +load_dynamically: true +dynamic_prototype: true +``` + +## Embedded factory in game object + +Factories are typically embedded in a `.go` file rather than as separate `.factory` files: + +```protobuf +embedded_components { + id: "enemy_factory" + type: "factory" + data: "prototype: \"/game/enemy.go\"\n" + position { + x: 0.0 + y: 0.0 + z: 0.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } +} +``` + +With load_dynamically: +```protobuf +embedded_components { + id: "bulletfactory" + type: "factory" + data: "prototype: \"/example/bullet.go\"\n" + "load_dynamically: false\n" + "dynamic_prototype: false\n" + "" + position { + x: 0.0 + y: 0.0 + z: 0.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } +} +``` + +Minimal embedded factory: +```protobuf +embedded_components { + id: "carrotfactory" + type: "factory" + data: "prototype: \"/example/debris.go\"\n" + "" +} +``` + +## Runtime API + +### Creating objects + +```lua +-- Basic spawn at factory position +local id = factory.create("#enemy_factory") + +-- Spawn at specific position +local pos = vmath.vector3(100, 200, 0) +local id = factory.create("#enemy_factory", pos) + +-- Spawn with rotation +local rot = vmath.quat_rotation_z(math.pi / 4) +local id = factory.create("#enemy_factory", pos, rot) + +-- Spawn with script properties +local id = factory.create("#enemy_factory", pos, nil, { speed = 100, health = 50 }) + +-- Spawn with scale (uniform) +local id = factory.create("#enemy_factory", pos, nil, nil, 2.0) + +-- Spawn with non-uniform scale +local id = factory.create("#enemy_factory", pos, nil, nil, vmath.vector3(1, 2, 1)) +``` + +### Dynamic loading + +```lua +-- Asynchronous loading with callback +local function on_loaded(self, url, result) + if result then + local id = factory.create(url) + end +end + +function init(self) + factory.load("#enemy_factory", on_loaded) +end + +function final(self) + -- Unload when done + factory.unload("#enemy_factory") +end +``` + +### Changing prototype (requires dynamic_prototype: true) + +```lua +-- Unload current resources +factory.unload("#bulletfactory") + +-- Set new prototype +factory.set_prototype("#bulletfactory", "/game/bullet_ice.goc") + +-- Create uses new prototype +local id = factory.create("#bulletfactory") +``` + +### Tracking spawned objects + +```lua +function init(self) + self.spawned = {} +end + +function spawn_enemy(self, pos) + local id = factory.create("#enemy_factory", pos) + table.insert(self.spawned, id) + return id +end + +function cleanup(self) + -- Delete all spawned objects + go.delete(self.spawned) + self.spawned = {} +end +``` + +### Addressing spawned objects + +```lua +-- Send message to spawned object +local id = factory.create("#enemy_factory") +msg.post(id, "set_target", { target = player_id }) + +-- Access component on spawned object +local sprite_url = msg.url(nil, id, "sprite") +sprite.play_flipbook(sprite_url, hash("run")) +``` + +## Instance limits + +The `max_instances` setting in `game.project` (Collection related settings) limits total game objects in a world. All placed and spawned objects count against this limit. Deleting objects frees slots for new spawns. + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default +2. **Floats**: Always include decimal point: `1.0`, not `1` +3. **Strings**: Always double-quoted +4. **Booleans**: `true` or `false`, no quotes +5. **Field order**: `prototype`, `load_dynamically`, `dynamic_prototype` +6. **No trailing commas or semicolons** +7. **Embedded data**: Multi-line strings with escaped quotes and `\n` + +## Workflow + +### Creating a new factory + +1. Create the prototype `.go` file first +2. Create `.factory` file or embed in parent `.go` +3. Set `prototype` to the `.go` path +4. Enable `load_dynamically` if resources should load on demand +5. Enable `dynamic_prototype` if prototype will change at runtime + +### Editing an existing factory + +1. Read the current file content +2. Modify fields as needed +3. Ensure required `prototype` field is present +4. Omit optional fields that equal defaults diff --git a/.agents/skills/defold-proto-file-editing/references/font.md b/.agents/skills/defold-proto-file-editing/references/font.md new file mode 100644 index 0000000..5f0eb25 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/font.md @@ -0,0 +1,361 @@ +# Editing Fonts + +Creates and edits Defold `.font` resource files using Protobuf Text Format. + +## Overview + +Font resources define how a font file (TTF, OTF, or BMFont `.fnt`) is rasterized into glyph textures for rendering text on Label components and GUI text nodes. Two output formats are available: bitmap (default) and distance field. + +## File format + +Font files (`.font`) use **Protobuf Text Format** based on the `FontDesc` message from `render/font_ddf.proto`. + +### Canonical example — Defold built-in default (`/builtins/fonts/default.font`) + +```protobuf +font: "/builtins/fonts/vera_mo_bd.ttf" +material: "/builtins/fonts/font-df.material" +size: 14 +antialias: 1 +alpha: 1.0 +shadow_alpha: 0.0 +shadow_blur: 0 +output_format: TYPE_DISTANCE_FIELD +``` + +Note: the Defold editor sometimes writes fields even when they equal the proto default (e.g. `antialias: 1`, `alpha: 1.0`). When **creating** files, omit default-valued fields. When **editing** existing files, preserve fields already present. + +### Additional examples + +Distance field font with outline, shadow, and multi-layer rendering: + +```protobuf +font: "/example/assets/fonts/Nunito-Black.ttf" +material: "/builtins/fonts/font-df.material" +size: 50 +outline_alpha: 1.0 +outline_width: 3.0 +shadow_alpha: 1.0 +shadow_blur: 8 +output_format: TYPE_DISTANCE_FIELD +render_mode: MODE_MULTI_LAYER +characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +``` + +Minimal font with only required characters (score display): + +```protobuf +font: "/example/assets/fonts/Nunito-Black.ttf" +material: "/builtins/fonts/font-df.material" +size: 50 +outline_alpha: 1.0 +outline_width: 3.0 +output_format: TYPE_DISTANCE_FIELD +render_mode: MODE_MULTI_LAYER +characters: " +0123456789" +``` + +Font with explicit cache width and shadow offset: + +```protobuf +font: "/example/assets/fonts/MPLUSRounded1c-Black.ttf" +material: "/builtins/fonts/font-df.material" +size: 50 +outline_alpha: 1.0 +outline_width: 4.0 +shadow_alpha: 1.0 +shadow_y: -2.5 +output_format: TYPE_DISTANCE_FIELD +cache_width: 2048 +render_mode: MODE_MULTI_LAYER +characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +``` + +## Fields reference + +### font (required) — `string` + +Absolute resource path to the source font file (`.ttf`, `.otf`, or `.fnt`). + +```protobuf +font: "/builtins/fonts/vera_mo_bd.ttf" +``` + +### material (required) — `string` + +Absolute resource path to the `.material` file used when rendering this font. + +Common built-in materials: +- `/builtins/fonts/font.material` — bitmap fonts +- `/builtins/fonts/font-df.material` — distance field fonts +- `/builtins/fonts/font-fnt.material` — BMFonts + +The material **must** match the `output_format`: +- `TYPE_BITMAP` → `font.material` +- `TYPE_DISTANCE_FIELD` → `font-df.material` +- BMFont (`.fnt`) → `font-fnt.material` + +```protobuf +material: "/builtins/fonts/font.material" +``` + +### size (required) — `uint32` + +Target glyph size in pixels. Integer, no decimal point. + +```protobuf +size: 24 +``` + +### antialias (optional) — `uint32` + +Antialiasing level. Default: `1`. Set to `0` for pixel-perfect font rendering. + +**Omission rule**: Omit if `1`. + +```protobuf +antialias: 0 +``` + +### alpha (optional) — `float` + +Transparency of the glyph face. Range: `0.0`–`1.0`. Default: `1.0` (opaque). + +**Omission rule**: Omit if `1.0`. + +```protobuf +alpha: 0.8 +``` + +### outline_alpha (optional) — `float` + +Transparency of the generated outline. Range: `0.0`–`1.0`. Default: `0.0` (transparent / no outline). + +**Omission rule**: Omit if `0.0`. + +```protobuf +outline_alpha: 1.0 +``` + +### outline_width (optional) — `float` + +Width of the generated outline in pixels. Default: `0.0` (no outline). + +**Omission rule**: Omit if `0.0`. + +```protobuf +outline_width: 2.0 +``` + +### shadow_alpha (optional) — `float` + +Transparency of the generated shadow. Range: `0.0`–`1.0`. Default: `0.0` (transparent / no shadow). + +Shadow support is enabled by built-in font material shaders. If you don't need shadow support, keep this at `0.0` to avoid unnecessary memory usage. + +**Omission rule**: Omit if `0.0`. + +```protobuf +shadow_alpha: 0.5 +``` + +### shadow_blur (optional) — `uint32` + +Shadow blur amount. Default: `0`. For bitmap fonts, this is the number of blur kernel passes. For distance field fonts, this is the pixel width of the blur. + +**Omission rule**: Omit if `0`. + +```protobuf +shadow_blur: 2 +``` + +### shadow_x (optional) — `float` + +Horizontal shadow offset in pixels. Default: `0.0`. Only affects rendering when `render_mode` is `MODE_MULTI_LAYER`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +shadow_x: 2.0 +``` + +### shadow_y (optional) — `float` + +Vertical shadow offset in pixels. Default: `0.0`. Only affects rendering when `render_mode` is `MODE_MULTI_LAYER`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +shadow_y: -2.0 +``` + +### extra_characters (deprecated) — `string` + +Deprecated field. Default: `""`. Do not use — use `characters` instead. + +**Omission rule**: Always omit. + +### output_format (optional) — enum `FontTextureFormat` + +Type of font data generated. Default: `TYPE_BITMAP`. + +| Value | Description | +|-------|-------------| +| `TYPE_BITMAP` | Bitmap texture (default). Color channels encode face, outline, and shadow. | +| `TYPE_DISTANCE_FIELD` | Distance field texture. Requires a DF material. Better for upscaling. | + +**Omission rule**: Omit if `TYPE_BITMAP`. + +```protobuf +output_format: TYPE_DISTANCE_FIELD +``` + +### all_chars (optional) — `bool` + +Include all glyphs available in the source font file. Default: `false`. + +**Omission rule**: Omit if `false`. + +```protobuf +all_chars: true +``` + +### cache_width (optional) — `uint32` + +Width of the glyph cache bitmap in pixels. Default: `0` (automatic, grows up to 2048). + +**Omission rule**: Omit if `0`. + +```protobuf +cache_width: 512 +``` + +### cache_height (optional) — `uint32` + +Height of the glyph cache bitmap in pixels. Default: `0` (automatic, grows up to 4096). + +**Omission rule**: Omit if `0`. + +```protobuf +cache_height: 512 +``` + +### render_mode (optional) — enum `FontRenderMode` + +Glyph rendering mode. Default: `MODE_SINGLE_LAYER`. + +| Value | Description | +|-------|-------------| +| `MODE_SINGLE_LAYER` | Single quad per character (default). | +| `MODE_MULTI_LAYER` | Separate quads for face, outline, and shadow. Prevents overlapping glyphs and enables proper shadow offset via `shadow_x`/`shadow_y`. | + +**Omission rule**: Omit if `MODE_SINGLE_LAYER`. + +```protobuf +render_mode: MODE_MULTI_LAYER +``` + +### characters (optional) — `string` + +Characters to include in the font. Default: `""`. Typically set to ASCII printable range (codes 32–126). Always wrap in double quotes. + +For runtime fonts, this text acts as a cache prewarming hint. + +```protobuf +characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. This keeps files minimal and matches Defold editor behavior. +2. **Floats**: Always include decimal point: `1.0`, not `1`. +3. **Integers**: No decimal point: `4`, not `4.0`. +4. **Strings**: Always double-quoted: `"text"`. +5. **Enums**: Use the enum constant name without quotes: `TYPE_DISTANCE_FIELD`. +6. **Booleans**: `true` or `false`, no quotes. +7. **Field order**: Follow the proto field number order: `font`, `material`, `size`, `antialias`, `alpha`, `outline_alpha`, `outline_width`, `shadow_alpha`, `shadow_blur`, `shadow_x`, `shadow_y`, `output_format`, `all_chars`, `cache_width`, `cache_height`, `render_mode`, `characters`. +8. **No trailing commas or semicolons**. +9. **No field number tags** — use field names only. +10. **No empty lines** between scalar fields (font files have no message blocks). + +## Best practices + +- **Use base size 50** for all fonts. This provides enough detail for distance field generation and crisp rendering at any display size. +- **Always use distance field** (`output_format: TYPE_DISTANCE_FIELD` with `material: "/builtins/fonts/font-df.material"`). Distance field fonts look sharp at any scale — both upscaled and downscaled — unlike bitmap fonts that become blocky when enlarged. +- **Scale labels via game object**, not font size. Keep `size: 50` in the `.font` file and use the game object's `scale` property to adjust the visual size of Label components on the scene. This way a single font resource works for all text sizes in the game. +- **Always include `~` and space in `characters`**. The space character is required for word spacing — without it, spaces won't render. The `~` character is used as a fallback substitute for missing glyphs. + +## Common templates + +### Recommended: distance field font (ASCII) + +```protobuf +font: "/assets/fonts/my_font.ttf" +material: "/builtins/fonts/font-df.material" +size: 50 +output_format: TYPE_DISTANCE_FIELD +characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +``` + +### Distance field font with outline and shadow (multi-layer) + +```protobuf +font: "/assets/fonts/my_font.ttf" +material: "/builtins/fonts/font-df.material" +size: 50 +outline_alpha: 1.0 +outline_width: 3.0 +shadow_alpha: 1.0 +shadow_blur: 8 +output_format: TYPE_DISTANCE_FIELD +render_mode: MODE_MULTI_LAYER +characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +``` + +### All characters included + +```protobuf +font: "/assets/fonts/my_font.ttf" +material: "/builtins/fonts/font-df.material" +size: 50 +output_format: TYPE_DISTANCE_FIELD +all_chars: true +``` + +### BMFont + +```protobuf +font: "/assets/fonts/my_bmfont.fnt" +material: "/builtins/fonts/font-fnt.material" +size: 50 +characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +``` + +### Bitmap font (no antialiasing, pixel-perfect) + +```protobuf +font: "/assets/fonts/pixel_font.ttf" +material: "/builtins/fonts/font.material" +size: 16 +antialias: 0 +characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +``` + +## Workflow + +### Creating a new font + +1. Determine the file path (must end with `.font`). +2. Set required fields: `font` (source font path), `material` (matching material), `size`. +3. Set `characters` with desired glyphs or enable `all_chars`. +4. Choose `output_format` and matching `material` if not using default bitmap. +5. Add optional fields (outline, shadow, cache) only if they differ from defaults. +6. Write the file using the field order from the reference above. + +### Editing an existing font + +1. Read the current `.font` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. +5. If changing `output_format`, ensure `material` is updated to match. diff --git a/.agents/skills/defold-proto-file-editing/references/gameobject.md b/.agents/skills/defold-proto-file-editing/references/gameobject.md new file mode 100644 index 0000000..25beb19 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/gameobject.md @@ -0,0 +1,362 @@ +# Editing Game Objects + +Creates and edits Defold `.go` game object files using Protobuf Text Format. + +## Overview + +A game object (`.go`) is a container with an id, position, rotation and scale. It holds **components** that give it visual, audible, and logic representation. Components can be added by **file reference** (`components` block) or **embedded inline** (`embedded_components` block). + +## File format + +Game object files (`.go`) use **Protobuf Text Format** based on the `PrototypeDesc` message from `gameobject_ddf.proto`. + +### Canonical example + +```protobuf +components { + id: "collisionobject" + component: "/main/example.collisionobject" +} +embedded_components { + id: "sprite" + type: "sprite" + data: "default_animation: \"logo\"\n" + "material: \"/builtins/materials/sprite.material\"\n" + "textures {\n" + " sampler: \"texture_sampler\"\n" + " texture: \"/main/main.atlas\"\n" + "}\n" + "" +} +``` + +## Top-level structure (`PrototypeDesc`) + +The `.go` file is a `PrototypeDesc` message with two repeated fields: + +- `components` — referenced component files (repeated `ComponentDesc`) +- `embedded_components` — inline component definitions (repeated `EmbeddedComponentDesc`) + +A `.go` file may contain any combination of referenced and embedded components, or be empty (a bare position marker). + +## Fields reference + +### components (repeated) — `ComponentDesc` + +A reference to an external component file. Each referenced component is a separate `components { ... }` block. + +#### ComponentDesc fields + +##### id (required) — `string` + +Unique identifier for this component within the game object. Used for addressing: `go_id#component_id`. + +```protobuf +id: "sprite" +``` + +##### component (required) — `string` + +Absolute resource path to the component file. Supports any component type (`.script`, `.collisionobject`, `.gui`, `.particlefx`, `.tilemap`, `.sound`, `.label`, `.sprite`, `.model`, `.mesh`, `.camera`, `.factory`, `.collectionfactory`, `.collectionproxy`, etc.). + +```protobuf +component: "/main/player.script" +``` + +##### position (optional) — `dmMath.Point3` + +Local position offset of the component. Defaults: `x: 0.0`, `y: 0.0`, `z: 0.0`. + +**Omission rule**: Omit the entire block if all components are at `0.0`. Only include components that differ from `0.0`. + +```protobuf +position { + x: 10.0 + y: 20.0 +} +``` + +##### rotation (optional) — `dmMath.Quat` + +Local rotation as quaternion. Defaults: `x: 0.0`, `y: 0.0`, `z: 0.0`, `w: 1.0`. + +**Omission rule**: Omit the entire block if all components are at defaults. Only include components that differ from defaults. + +```protobuf +rotation { + z: 0.7071068 + w: 0.7071068 +} +``` + +##### scale (optional) — `dmMath.Vector3One` + +Local scale. Defaults: `x: 1.0`, `y: 1.0`, `z: 1.0`. + +**Omission rule**: Omit the entire block if all components are `1.0`. Only include components that differ from `1.0`. + +```protobuf +scale { + x: 2.0 + y: 2.0 +} +``` + +##### properties (repeated) — `PropertyDesc` + +Overrides for script properties. Each override is a separate `properties { ... }` block. + +See the **PropertyDesc** section below. + +#### Full ComponentDesc example + +```protobuf +components { + id: "script" + component: "/main/player.script" + properties { + id: "speed" + value: "200.0" + type: PROPERTY_TYPE_NUMBER + } +} +``` + +### embedded_components (repeated) — `EmbeddedComponentDesc` + +An inline component definition. Each embedded component is a separate `embedded_components { ... }` block. + +#### EmbeddedComponentDesc fields + +##### id (required) — `string` + +Unique identifier for this component within the game object. + +```protobuf +id: "sprite" +``` + +##### type (required) — `string` + +Component type name. Common values: `"sprite"`, `"label"`, `"collisionobject"`, `"sound"`, `"particlefx"`, `"model"`, `"mesh"`, `"camera"`, `"factory"`, `"collectionfactory"`, `"collectionproxy"`, `"tilegrid"`. + +**Important**: GUI components (`.gui`) CANNOT be embedded inline. They must always be added as **referenced components** using a `components` block pointing to a `.gui` file. + +```protobuf +type: "sprite" +``` + +##### data (required) — `string` + +The component's Protobuf Text Format content, encoded as a multi-line string. Each line of the component data is a separate quoted string, terminated with `\n`. + +**Encoding rules**: +- Each logical line of the embedded component's protobuf text becomes a separate quoted string literal +- Lines end with `\n` inside the quotes +- Inner quotes are escaped as `\"` +- The last entry is an empty string `""` +- Indentation inside the data string reflects the component's own protobuf nesting (2 spaces per level) + +```protobuf +data: "default_animation: \"idle\"\n" +"material: \"/builtins/materials/sprite.material\"\n" +"textures {\n" +" sampler: \"texture_sampler\"\n" +" texture: \"/main/main.atlas\"\n" +"}\n" +"" +``` + +##### position (optional) — `dmMath.Point3` + +Local position offset. Same defaults and omission rules as `ComponentDesc.position`. + +##### rotation (optional) — `dmMath.Quat` + +Local rotation. Same defaults and omission rules as `ComponentDesc.rotation`. + +##### scale (optional) — `dmMath.Vector3One` + +Local scale. Same defaults and omission rules as `ComponentDesc.scale`. + +#### Full EmbeddedComponentDesc example + +```protobuf +embedded_components { + id: "label" + type: "label" + data: "size {\n" + " x: 128.0\n" + " y: 32.0\n" + "}\n" + "text: \"Hello World\"\n" + "font: \"/builtins/fonts/default.font\"\n" + "material: \"/builtins/fonts/label-df.material\"\n" + "" + position { + y: 50.0 + } +} +``` + +## PropertyDesc + +Used to override script properties on referenced components. + +### id (required) — `string` + +The script property name. + +### value (required) — `string` + +The property value as a string. Numbers, hashes, URLs, vectors, quaternions, and booleans are all represented as strings. + +### type (required) — enum `PropertyType` + +| Value | Description | +|-------|-------------| +| `PROPERTY_TYPE_NUMBER` | Numeric value | +| `PROPERTY_TYPE_HASH` | Hash value | +| `PROPERTY_TYPE_URL` | URL value | +| `PROPERTY_TYPE_VECTOR3` | Vector3 value (format: `"x, y, z"`) | +| `PROPERTY_TYPE_VECTOR4` | Vector4 value (format: `"x, y, z, w"`) | +| `PROPERTY_TYPE_QUAT` | Quaternion value | +| `PROPERTY_TYPE_BOOLEAN` | Boolean value (`"true"` or `"false"`) | + +```protobuf +properties { + id: "speed" + value: "200.0" + type: PROPERTY_TYPE_NUMBER +} +``` + +## Common templates + +### Empty game object (position marker) + +```protobuf +``` + +An empty file is valid — it creates a game object with no components. + +### Game object with a script reference + +```protobuf +components { + id: "script" + component: "/main/player.script" +} +``` + +### Game object with an embedded sprite + +```protobuf +embedded_components { + id: "sprite" + type: "sprite" + data: "default_animation: \"idle\"\n" + "material: \"/builtins/materials/sprite.material\"\n" + "textures {\n" + " sampler: \"texture_sampler\"\n" + " texture: \"/main/main.atlas\"\n" + "}\n" + "" +} +``` + +### Game object with a script and collision object references + +```protobuf +components { + id: "script" + component: "/main/enemy.script" +} +components { + id: "collisionobject" + component: "/main/enemy.collisionobject" +} +``` + +### Game object with referenced and embedded components + +```protobuf +components { + id: "script" + component: "/main/player.script" +} +embedded_components { + id: "sprite" + type: "sprite" + data: "default_animation: \"idle\"\n" + "material: \"/builtins/materials/sprite.material\"\n" + "textures {\n" + " sampler: \"texture_sampler\"\n" + " texture: \"/main/main.atlas\"\n" + "}\n" + "" +} +``` + +### Game object with a GUI reference + +```protobuf +components { + id: "gui" + component: "/screens/gameplay/gameplay.gui" +} +``` + +### Game object with script property overrides + +```protobuf +components { + id: "script" + component: "/main/player.script" + properties { + id: "speed" + value: "200.0" + type: PROPERTY_TYPE_NUMBER + } + properties { + id: "health" + value: "100.0" + type: PROPERTY_TYPE_NUMBER + } +} +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit optional fields that equal their proto default. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Integers**: No decimal point: `4`, not `4.0`. +5. **Strings**: Always double-quoted. +6. **Enums**: Use the constant name without quotes. +7. **Booleans**: `true` or `false`, no quotes. +8. **Repeated fields**: Each entry gets its own `field_name { ... }` block. +9. **Field order**: Follow the proto field number order: `components` before `embedded_components`. +10. **No trailing commas or semicolons**. +11. **Indentation**: 2 spaces per nesting level inside message blocks. +12. **Newlines**: One empty line between the end of a message block `}` and the next field. No empty line between consecutive scalar fields. +13. **Embedded data strings**: Each line is a separate quoted string. Escape inner quotes with `\"`. End lines with `\n`. Terminate with empty `""`. + +## Workflow + +### Creating a new game object + +1. Determine the file path (must end with `.go`). +2. Decide which components to include (referenced files vs. embedded). +3. For referenced components: set `id` and `component` path. +4. For embedded components: set `id`, `type`, and encode the component data as a multi-line escaped string. +5. Add optional `position`, `rotation`, `scale` only if they differ from defaults. +6. Add `properties` overrides for script components if needed. +7. Write the file with `components` blocks first, then `embedded_components` blocks. + +### Editing an existing game object + +1. Read the current `.go` file. +2. Modify only the requested fields or components. +3. Preserve existing component order and field values. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/gui.md b/.agents/skills/defold-proto-file-editing/references/gui.md new file mode 100644 index 0000000..73ee341 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/gui.md @@ -0,0 +1,836 @@ + +# Editing GUI Files + +Defold GUI scene (`.gui`) — defines a 2D user interface component with nodes, fonts, textures, materials, layers, and an optional script. + +## Overview + +A GUI component is attached to a game object and rendered independently of the game view (on top by default). It has its own coordinate space and layout system. A `.gui` file defines: + +- **Dependencies**: fonts, textures (atlases), materials, particle effects, resources +- **Layers**: control draw order for batching optimization +- **Nodes**: the visual elements (box, text, pie, template, particlefx, custom) +- **Layouts**: alternative node configurations for different screen sizes + +GUI nodes are rendered in list order (first = behind, last = in front). Parent-child hierarchies inherit transforms. Layers override default draw order. + +GUI scripts (`.gui_script`) have access to `gui` namespace but **not** `go` or `render`. + +A `.gui` is added to a game object as a **component** — either as a file reference in `.go` or embedded in `.collection`. It is rendered independently of the game view; the game object's position has no effect on the GUI. + +## File format + +GUI files use **Protobuf Text Format** based on the `SceneDesc` message from `gamesys/gui_ddf.proto`. + +## Canonical example + +From `main/example.gui` — minimal GUI with a single box node: +```protobuf +nodes { + size { + x: 200.0 + y: 100.0 + } + type: TYPE_BOX + id: "box" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT +``` + +Key observations from editor output: +- Fields at proto defaults are **omitted** (no `position`, `rotation`, `scale`, `color`, `blend_mode`, `alpha`, etc.) +- `background_color` is **not** output by the editor (deprecated) +- `max_nodes` is **not** output when at default (`512`) +- `material` and `adjust_reference` are always output + +## SceneDesc fields reference + +### script (optional) — `string` + +Path to the `.gui_script` file that controls this GUI's behavior. + +**Omission rule**: Omit if no script is attached. + +```protobuf +script: "/main/hud.gui_script" +``` + +### fonts (repeated) — `FontDesc` + +Font resources available to text nodes. Each entry maps a `name` (used in nodes) to a `font` resource path. + +```protobuf +fonts { + name: "system_font" + font: "/builtins/fonts/default.font" +} +fonts { + name: "title" + font: "/assets/fonts/title.font" +} +``` + +### textures (repeated) — `TextureDesc` + +Texture/atlas resources available to box and pie nodes. Each entry maps a `name` to a `texture` resource path. + +```protobuf +textures { + name: "main" + texture: "/main/main.atlas" +} +``` + +Node `texture` field references these as `"/"` (e.g., `"main/logo"`). + +### background_color (optional) — `dmMath.Vector4` + +**Deprecated**. The editor no longer outputs this field. Omit it. + +### nodes (repeated) — `NodeDesc` + +The GUI nodes. See the **NodeDesc** section below for full field reference. + +### layers (repeated) — `LayerDesc` + +Named layers that control draw order. Nodes assigned to a layer are drawn in layer order rather than list order. Layers reduce draw calls by grouping same-type nodes. + +```protobuf +layers { + name: "graphics" +} +layers { + name: "text" +} +``` + +### material (optional) — `string` + +Default material for the GUI. Default: `"/builtins/materials/gui.material"`. + +```protobuf +material: "/builtins/materials/gui.material" +``` + +### layouts (repeated) — `LayoutDesc` + +Alternative node configurations for different screen sizes/orientations. Each layout has a `name` and a list of `nodes` that override the default nodes. + +```protobuf +layouts { + name: "Landscape" + nodes { + position { + x: 568.0 + y: 320.0 + } + size { + x: 200.0 + y: 100.0 + } + type: TYPE_BOX + id: "box" + } +} +``` + +### adjust_reference (optional) — enum `AdjustReference` + +Controls how each node's adjust mode is calculated. Default: `ADJUST_REFERENCE_LEGACY`. + +| Value | Description | +|---|---| +| `ADJUST_REFERENCE_LEGACY` | Root-based (deprecated) | +| `ADJUST_REFERENCE_PARENT` | Per-node, adjusts against parent node or resized screen | +| `ADJUST_REFERENCE_DISABLED` | Turns off node adjust mode, all nodes keep set size | + +```protobuf +adjust_reference: ADJUST_REFERENCE_PARENT +``` + +### max_nodes (optional) — `uint32` + +Maximum number of nodes for this GUI. Default: `512`. + +```protobuf +max_nodes: 512 +``` + +### particlefxs (repeated) — `ParticleFXDesc` + +Particle effect resources available to particlefx nodes. + +```protobuf +particlefxs { + name: "explosion" + particlefx: "/effects/explosion.particlefx" +} +``` + +### resources (repeated) — `ResourceDesc` + +Generic resources (e.g., buffers, custom data). + +```protobuf +resources { + name: "my_buffer" + path: "/data/my_buffer.buffer" +} +``` + +### materials (repeated) — `MaterialDesc` + +Additional materials available to individual nodes (besides the default scene material). + +```protobuf +materials { + name: "glow" + material: "/materials/glow.material" +} +``` + +### max_dynamic_textures (optional) — `uint32` + +Maximum number of textures that can be created using `gui.new_texture()`. Default: `128`. + +### spine_scenes (repeated) — `SpineSceneDesc` + +**Deprecated**. Spine scenes for spine nodes. + +## NodeDesc fields reference + +Each node is a `nodes { ... }` block inside `SceneDesc`. Nodes are rendered in list order. + +### Common fields (all node types) + +#### position (optional) — `dmMath.Vector4` + +Position in pixels relative to parent (or scene origin). Components: `x`, `y`, `z`, `w`. Defaults: all `0.0`. + +```protobuf +position { + x: 320.0 + y: 568.0 +} +``` + +#### rotation (optional) — `dmMath.Vector4` + +Rotation as **Euler angles in degrees** (not quaternion). Components: `x`, `y`, `z`, `w`. Defaults: all `0.0`. Typically only `z` is used for 2D rotation. + +```protobuf +rotation { + z: 45.0 +} +``` + +#### scale (optional) — `dmMath.Vector4One` + +Scale factor. Components: `x`, `y`, `z`, `w`. Defaults: all `1.0`. + +```protobuf +scale { + x: 2.0 + y: 2.0 +} +``` + +#### size (optional) — `dmMath.Vector4` + +Node size in pixels. Components: `x`, `y`, `z`, `w`. Defaults: all `0.0`. Required for box and text nodes when `size_mode` is `SIZE_MODE_MANUAL`. + +```protobuf +size { + x: 200.0 + y: 100.0 +} +``` + +#### color (optional) — `dmMath.Vector4One` + +Tint color (RGBA). Components: `x` (R), `y` (G), `z` (B), `w` (A). Defaults: all `1.0` (white, fully opaque). + +```protobuf +color { + x: 1.0 + y: 0.0 + z: 0.0 +} +``` + +#### type (optional) — enum `Type` + +| Value | Description | +|---|---| +| `TYPE_BOX` | Rectangular node with color/texture | +| `TYPE_TEXT` | Text display node | +| `TYPE_PIE` | Circular/ellipsoid fill node | +| `TYPE_TEMPLATE` | Instance of another GUI scene | +| `TYPE_PARTICLEFX` | Particle effect node | +| `TYPE_CUSTOM` | Custom node type | + +#### blend_mode (optional) — enum `BlendMode` + +Default: `BLEND_MODE_ALPHA`. + +| Value | Description | +|---|---| +| `BLEND_MODE_ALPHA` | Normal alpha blending | +| `BLEND_MODE_ADD` | Additive blending (linear dodge) | +| `BLEND_MODE_ADD_ALPHA` | Deprecated | +| `BLEND_MODE_MULT` | Multiply blending | +| `BLEND_MODE_SCREEN` | Screen blending | + +#### id (optional) — `string` + +Unique identifier within the GUI scene. Used by `gui.get_node()` in scripts. + +```protobuf +id: "my_button" +``` + +#### xanchor (optional) — enum `XAnchor` + +Default: `XANCHOR_NONE`. + +| Value | Description | +|---|---| +| `XANCHOR_NONE` | Position relative to center of parent | +| `XANCHOR_LEFT` | Anchored to left edge | +| `XANCHOR_RIGHT` | Anchored to right edge | + +#### yanchor (optional) — enum `YAnchor` + +Default: `YANCHOR_NONE`. + +| Value | Description | +|---|---| +| `YANCHOR_NONE` | Position relative to center of parent | +| `YANCHOR_TOP` | Anchored to top edge | +| `YANCHOR_BOTTOM` | Anchored to bottom edge | + +#### pivot (optional) — enum `Pivot` + +Default: `PIVOT_CENTER`. The origin/anchor point of the node. Rotation, scaling, and size changes happen around this point. For text nodes, also controls text alignment (West = left, Center = center, East = right). + +| Value | Description | +|---|---| +| `PIVOT_CENTER` | Center | +| `PIVOT_N` | North (top center) | +| `PIVOT_NE` | North East | +| `PIVOT_E` | East (center right) | +| `PIVOT_SE` | South East | +| `PIVOT_S` | South (bottom center) | +| `PIVOT_SW` | South West | +| `PIVOT_W` | West (center left) | +| `PIVOT_NW` | North West | + +#### adjust_mode (optional) — enum `AdjustMode` + +Default: `ADJUST_MODE_FIT`. Controls how node content scales when scene boundaries are stretched. + +| Value | Description | +|---|---| +| `ADJUST_MODE_FIT` | Scale to fit inside stretched bounds (preserves aspect) | +| `ADJUST_MODE_ZOOM` | Scale to cover stretched bounds (preserves aspect) | +| `ADJUST_MODE_STRETCH` | Stretch to fill bounds (breaks aspect) | + +#### parent (optional) — `string` + +ID of the parent node. Empty or omitted means the node is a root node. + +```protobuf +parent: "panel" +``` + +#### layer (optional) — `string` + +Layer assignment. Must match a layer name defined in `layers`. Unset layer inherits from parent; root nodes with no layer go to the implicit "null" layer (drawn first). + +```protobuf +layer: "graphics" +``` + +#### inherit_alpha (optional) — `bool` + +Default: `false`. When `true`, node alpha is multiplied with parent's alpha. + +#### alpha (optional) — `float` + +Default: `1.0`. Node translucency (0.0 = transparent, 1.0 = opaque). Animatable. + +#### enabled (optional) — `bool` + +Default: `true`. When `false`, node is not rendered, not animated, and not pickable. + +#### visible (optional) — `bool` + +Default: `true`. When `false`, node is not rendered but can still be animated and picked. + +#### material (optional) — `string` + +Per-node material override. References a material name from the `materials` list, or empty to use the scene default. + +### Text node fields + +These fields apply when `type: TYPE_TEXT`. + +#### text (optional) — `string` + +The display text. + +```protobuf +text: "Score: 0" +``` + +#### font (optional) — `string` + +Font name from the `fonts` list. + +```protobuf +font: "system_font" +``` + +#### line_break (optional) — `bool` + +Default: `false`. When `true`, text wraps at node width. + +#### outline (optional) — `dmMath.Vector4WOne` + +Outline color (RGBA). Defaults: x/y/z: `0.0`, w: `1.0`. + +```protobuf +outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 +} +``` + +#### shadow (optional) — `dmMath.Vector4WOne` + +Shadow color (RGBA). Same defaults as `outline`. + +#### outline_alpha (optional) — `float` + +Default: `1.0`. Outline translucency. + +#### shadow_alpha (optional) — `float` + +Default: `1.0`. Shadow translucency. + +#### text_leading (optional) — `float` + +Default: `1.0`. Line spacing multiplier. `0` = no spacing, `1` = normal. + +#### text_tracking (optional) — `float` + +Default: `0.0`. Letter spacing adjustment. + +### Box node fields + +These fields apply when `type: TYPE_BOX`. + +#### texture (optional) — `string` + +Reference to a texture/animation in format `"/"` or empty for a solid color box. + +```protobuf +texture: "main/button" +``` + +#### slice9 (optional) — `dmMath.Vector4` + +9-slice margins in pixels. Components: `x` (left), `y` (top), `z` (right), `w` (bottom). Defaults: all `0.0`. Preserves edge pixel size when node is resized. + +```protobuf +slice9 { + x: 12.0 + y: 12.0 + z: 12.0 + w: 12.0 +} +``` + +#### size_mode (optional) — enum `SizeMode` + +Default: `SIZE_MODE_MANUAL`. + +| Value | Description | +|---|---| +| `SIZE_MODE_MANUAL` | Size set manually | +| `SIZE_MODE_AUTO` | Size determined automatically from texture | + +#### clipping_mode (optional) — enum `ClippingMode` + +Default: `CLIPPING_MODE_NONE`. + +| Value | Description | +|---|---| +| `CLIPPING_MODE_NONE` | No clipping | +| `CLIPPING_MODE_STENCIL` | Node acts as stencil mask for children | + +#### clipping_visible (optional) — `bool` + +Default: `true`. Whether the clipping node itself is rendered. + +#### clipping_inverted (optional) — `bool` + +Default: `false`. Inverts the stencil mask. + +### Pie node fields + +These fields apply when `type: TYPE_PIE`. Pie nodes also support `texture`, `size_mode`, and clipping fields (same as box). + +#### outerBounds (optional) — enum `PieBounds` + +Default: `PIEBOUNDS_ELLIPSE`. + +| Value | Description | +|---|---| +| `PIEBOUNDS_RECTANGLE` | Rectangular outer bounds | +| `PIEBOUNDS_ELLIPSE` | Elliptical outer bounds | + +#### innerRadius (optional) — `float` + +Default: `0`. Inner radius along X axis (creates a ring when > 0). + +#### perimeterVertices (optional) — `int32` + +Default: `32`. Number of segments building the shape. + +#### pieFillAngle (optional) — `float` + +Default: `360`. Fill angle in degrees (partial pie when < 360). + +### Template node fields + +These fields apply when `type: TYPE_TEMPLATE`. + +#### template (optional) — `string` + +Path to another `.gui` file used as template. + +```protobuf +template: "/gui/button.gui" +``` + +#### template_node_child (optional) — `bool` + +Internal flag — nodes that belong to a template instance have this set to `true`. + +#### overridden_fields (repeated) — `uint32` + +List of proto field numbers that are overridden from the template. Used internally by the editor to track which properties have been customized. + +### ParticleFX node fields + +These fields apply when `type: TYPE_PARTICLEFX`. + +#### particlefx (optional) — `string` + +Name from the `particlefxs` list. + +```protobuf +particlefx: "explosion" +``` + +### Custom node fields + +#### custom_type (optional) — `uint32` + +Default: `0`. The custom type identifier. + +## Templates (GUI composition) + +GUI scenes can include other GUI scenes as **template nodes** (`TYPE_TEMPLATE`). This is the primary way to build reusable UI components (buttons, dialogs, HUD elements). + +**Important caveat**: Only the **main GUI's** `.gui_script` executes. If a template GUI has its own `.gui_script` attached, that script is **ignored** — it does NOT run. All logic for template nodes must be handled by the parent GUI's script. + +Template node children are accessible via `gui.get_node()` with the template node ID as prefix: `gui.get_node("template_id/child_node_id")`. + +```protobuf +nodes { + type: TYPE_TEMPLATE + id: "play_button" + template: "/gui/button.gui" + inherit_alpha: true +} +``` + +To override properties of template child nodes, add them after the template node with `template_node_child: true` and the `overridden_fields` list indicating which fields are customized. + +## GUI as a component in .go / .collection + +A `.gui` file is referenced as a component in game objects, just like `.script` or `.sprite`: + +In a `.go` file: +```protobuf +components { + id: "example" + component: "/main/example.gui" +} +``` + +In a `.collection` embedded instance data: +```protobuf +embedded_instances { + id: "go" + data: "components {\n" + " id: \"gui\"\n" + " component: \"/main/hud.gui\"\n" + "}\n" + "" +} +``` + +The game object's position has **no effect** on the GUI — GUI rendering is independent of the game view. + +## Common templates + +### Empty GUI (placeholder) + +```protobuf +script: "/main/empty.gui_script" +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT +``` + +### GUI with a centered text label + +```protobuf +script: "/main/hud.gui_script" +fonts { + name: "system_font" + font: "/builtins/fonts/default.font" +} +nodes { + position { + x: 320.0 + y: 568.0 + } + size { + x: 400.0 + y: 40.0 + } + type: TYPE_TEXT + text: "Score: 0" + font: "system_font" + id: "score" + pivot: PIVOT_N + yanchor: YANCHOR_TOP + inherit_alpha: true +} + +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT +``` + +### GUI with layered button (box + text) + +```protobuf +script: "/main/menu.gui_script" +fonts { + name: "system_font" + font: "/builtins/fonts/default.font" +} +textures { + name: "ui" + texture: "/assets/ui.atlas" +} +nodes { + position { + x: 320.0 + y: 400.0 + } + size { + x: 200.0 + y: 60.0 + } + type: TYPE_BOX + texture: "ui/button" + id: "btn_play" + layer: "graphics" + inherit_alpha: true +} + +nodes { + position { + x: 320.0 + y: 400.0 + } + size { + x: 200.0 + y: 60.0 + } + type: TYPE_TEXT + text: "PLAY" + font: "system_font" + id: "btn_play_text" + parent: "btn_play" + layer: "text" + inherit_alpha: true +} + +layers { + name: "graphics" +} +layers { + name: "text" +} +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT +``` + +### Box node with 9-slice + +```protobuf +nodes { + position { + x: 320.0 + y: 300.0 + } + size { + x: 400.0 + y: 200.0 + } + type: TYPE_BOX + texture: "ui/panel" + id: "panel" + slice9 { + x: 16.0 + y: 16.0 + z: 16.0 + w: 16.0 + } + inherit_alpha: true +} +``` + +### Stencil clipping (mask + content) + +```protobuf +nodes { + position { + x: 320.0 + y: 400.0 + } + size { + x: 200.0 + y: 200.0 + } + type: TYPE_BOX + texture: "ui/circle_mask" + id: "mask" + clipping_mode: CLIPPING_MODE_STENCIL + clipping_visible: true + inherit_alpha: true +} + +nodes { + position { + x: 320.0 + y: 400.0 + } + size { + x: 300.0 + y: 300.0 + } + type: TYPE_BOX + texture: "ui/photo" + id: "content" + parent: "mask" + inherit_alpha: true +} +``` + +### Pie node (health ring) + +```protobuf +nodes { + position { + x: 80.0 + y: 80.0 + } + size { + x: 100.0 + y: 100.0 + } + color { + x: 0.0 + y: 1.0 + z: 0.0 + } + type: TYPE_PIE + id: "health_ring" + outerBounds: PIEBOUNDS_ELLIPSE + innerRadius: 40.0 + perimeterVertices: 64 + pieFillAngle: 270.0 + inherit_alpha: true +} +``` + +## Layout, anchoring, and adjust mode + +**Pivot + Anchor interaction**: The pivot sets the node's origin point. When an anchor is active, the pivot edge stays at a fixed percentage from the corresponding screen/parent edge. For edge-aligned elements, set the pivot to the same side as the anchor (e.g., `PIVOT_W` + `XANCHOR_LEFT`). + +**Adjust mode**: Controls how node content scales when the scene is stretched to fit the screen: +- `ADJUST_MODE_FIT` — content fits inside bounds (may leave empty space) +- `ADJUST_MODE_ZOOM` — content covers bounds (may crop) +- `ADJUST_MODE_STRETCH` — content fills bounds (may distort) + +**Layers and draw calls**: Nodes are batched into draw calls when they share the same type, atlas, blend mode, and font. Layers let you group same-type nodes to minimize draw calls. Without layers, alternating node types in the hierarchy breaks batching. + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Integers**: No decimal point: `32`, not `32.0`. +5. **Strings**: Always double-quoted. +6. **Enums**: Use the constant name without quotes. +7. **Booleans**: `true` or `false`, no quotes. +8. **Repeated fields**: Each entry gets its own `field_name { ... }` block. +9. **Field order**: Follow the proto field number order. +10. **No trailing commas or semicolons**. +11. **Indentation**: 2 spaces per nesting level inside message blocks. +12. **Newlines**: One empty line between the end of a message block `}` and the next field. No empty line between consecutive scalar fields. +13. **Vector blocks**: Only include components that differ from defaults. Omit the block entirely if all components are at defaults (for optional fields). + +## Best practices + +- **Font selection priority.** When choosing a font for GUI text nodes: + 1. Use the font specified by the user in the current request, if provided. + 2. Search the project for existing `.font` files (e.g., `assets/fonts/`). If found, use the project font. Pick the most appropriate one if there are several (e.g., prefer distance field fonts). + 3. Fall back to `/builtins/fonts/default.font` only if no project fonts exist and the user did not specify one. +- **Use distance field fonts with base size 50.** The `.font` resource should use `size: 50` and `output_format: TYPE_DISTANCE_FIELD`. This provides crisp text at any visual size. +- **Control text size via node scale, not font size.** Do not create separate `.font` files for different text sizes. Instead, set the `scale` of the text node. For example, to display text at visual size 25, keep `size: 50` in the `.font` and set the node's scale to `0.5`. + +## Workflow + +### Creating a new GUI + +1. Determine the file path (must end with `.gui`). +2. Add `fonts` entries for any fonts needed by text nodes. +3. Add `textures` entries for any atlases needed by box/pie nodes. +4. Add `layers` if you need draw order optimization. +5. Add `materials` if nodes need non-default materials. +6. Add `particlefxs` if using particle effect nodes. +7. Add `nodes` — each node needs at minimum: `type`, `id`, and type-specific fields (`text`+`font` for text, `texture` for textured box, etc.). +8. Set `parent` field on child nodes to establish hierarchy. +9. Set `script` to the `.gui_script` path. +10. Set `adjust_reference` (prefer `ADJUST_REFERENCE_PARENT`). +11. Set `material` (usually keep the default). + +### Editing an existing GUI + +1. Read the current `.gui` file. +2. When adding nodes, place them after existing nodes. +3. When adding dependencies (fonts, textures, etc.), check if the dependency already exists before adding a duplicate. +4. Preserve existing node IDs — they may be referenced in `.gui_script`. +5. When modifying template nodes, only override the fields you need to change. diff --git a/.agents/skills/defold-proto-file-editing/references/label.md b/.agents/skills/defold-proto-file-editing/references/label.md new file mode 100644 index 0000000..cab4a8a --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/label.md @@ -0,0 +1,256 @@ +# Editing Labels + +Creates and edits Defold `.label` component files using Protobuf Text Format. + +## File format + +Label files (`.label`) use **Protobuf Text Format** based on the `LabelDesc` message from `label_ddf.proto`. + +### Canonical example + +```protobuf +size { + x: 128.0 + y: 32.0 +} +color { + x: 0.93333334 + y: 0.93333334 + z: 0.93333334 +} +outline { + x: 0.2 + y: 0.2 + z: 0.2 +} +shadow { + x: 0.2 + y: 0.2 + z: 0.2 +} +pivot: PIVOT_N +line_break: true +text: "Label" +font: "/builtins/fonts/default.font" +material: "/builtins/fonts/label-df.material" +``` + +## Fields reference + +### size (required) — `dmMath.Vector4` + +Bounding box of the text area. If `line_break` is enabled, `x` controls the wrap width. + +- `x` — width (default: `0.0`) +- `y` — height (default: `0.0`) +- `z` — depth (default: `0.0`, rarely used) +- `w` — (default: `0.0`, rarely used) + +Only include components that differ from `0.0`. Typically only `x` and `y` are set. + +```protobuf +size { + x: 256.0 + y: 64.0 +} +``` + +### color (optional) — `dmMath.Vector4One` + +Text RGBA color. Components default to `1.0` (white, fully opaque). + +- `x` — red (default: `1.0`) +- `y` — green (default: `1.0`) +- `z` — blue (default: `1.0`) +- `w` — alpha (default: `1.0`) + +**Omission rule**: Only include components that differ from `1.0`. If color is fully white (`1.0, 1.0, 1.0, 1.0`), omit the entire block. + +```protobuf +color { + x: 0.0 + y: 0.5 + z: 1.0 +} +``` + +### outline (optional) — `dmMath.Vector4WOne` + +Outline RGBA color. `x`, `y`, `z` default to `0.0`; `w` defaults to `1.0`. + +**Omission rule**: Only include components that differ from their defaults (`x/y/z` from `0.0`, `w` from `1.0`). If all at defaults, omit the entire block. + +```protobuf +outline { + x: 0.2 + y: 0.2 + z: 0.2 +} +``` + +### shadow (optional) — `dmMath.Vector4WOne` + +Shadow RGBA color. Same defaults as `outline`. + +Note: default material has shadow rendering disabled for performance. To see shadows, use a material that supports them. + +```protobuf +shadow { + x: 0.2 + y: 0.2 + z: 0.2 +} +``` + +### leading (optional) — `float` + +Line spacing multiplier. Default: `1.0`. Value `0` gives no line spacing. + +**Omission rule**: Omit if `1.0`. + +```protobuf +leading: 1.5 +``` + +### tracking (optional) — `float` + +Letter spacing multiplier. Default: `0.0`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +tracking: 0.02 +``` + +### pivot (optional) — enum `Pivot` + +Text anchor point and alignment. Default: `PIVOT_CENTER`. + +Valid values: + +| Value | Meaning | +|-------|---------| +| `PIVOT_CENTER` | Center | +| `PIVOT_N` | North (top center) | +| `PIVOT_NE` | North East | +| `PIVOT_E` | East (center right) | +| `PIVOT_SE` | South East | +| `PIVOT_S` | South (bottom center) | +| `PIVOT_SW` | South West | +| `PIVOT_W` | West (center left) | +| `PIVOT_NW` | North West | + +**Omission rule**: Omit if `PIVOT_CENTER`. + +```protobuf +pivot: PIVOT_NW +``` + +### blend_mode (optional) — enum `BlendMode` + +Blending mode for rendering. Default: `BLEND_MODE_ALPHA`. + +| Value | Description | +|-------|-------------| +| `BLEND_MODE_ALPHA` | Normal alpha blending | +| `BLEND_MODE_ADD` | Additive blending (brightens) | +| `BLEND_MODE_MULT` | Multiply (darkens) | +| `BLEND_MODE_SCREEN` | Screen (inverse multiply) | + +**Omission rule**: Omit if `BLEND_MODE_ALPHA`. + +```protobuf +blend_mode: BLEND_MODE_ADD +``` + +### line_break (optional) — `bool` + +Enable multi-line text wrapping at the bounding box width. Default: `false`. + +**Omission rule**: Omit if `false`. + +```protobuf +line_break: true +``` + +### text (optional) — `string` + +Text content. Default: `""`. Always wrap in double quotes. + +```protobuf +text: "Hello World" +``` + +### font (required) — `string` + +Absolute resource path to a `.font` file. Must match the material type (bitmap, distance field, or BMFont). + +```protobuf +font: "/builtins/fonts/default.font" +``` + +### material (required) — `string` + +Absolute resource path to a `.material` file. Must match the font type. + +Common built-in materials: +- `/builtins/fonts/label.material` — bitmap fonts +- `/builtins/fonts/label-df.material` — distance field fonts +- `/builtins/fonts/label-fnt.material` — BMFonts + +```protobuf +material: "/builtins/fonts/label-df.material" +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. This keeps files minimal and matches Defold editor behavior. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. Use standard float formatting. +4. **Strings**: Always double-quoted: `"text"`. +5. **Enums**: Use the enum constant name without quotes: `PIVOT_N`. +6. **Booleans**: `true` or `false`, no quotes. +7. **Field order**: Follow the proto field number order: `size`, `color`, `outline`, `shadow`, `leading`, `tracking`, `pivot`, `blend_mode`, `line_break`, `text`, `font`, `material`. +8. **No trailing commas or semicolons**. +9. **No field number tags** — use field names only. +10. **Newlines**: One empty line between the end of a message block `}` and the next field. No empty line between scalar fields. +11. **Indentation**: 2 spaces inside message blocks. + +## Minimal label (only required fields + text) + +```protobuf +size { + x: 128.0 + y: 32.0 +} +text: "Hello" +font: "/builtins/fonts/default.font" +material: "/builtins/fonts/label-df.material" +``` + +## Best practices + +- **Font selection priority.** When choosing a `font` for a label: + 1. Use the font specified by the user in the current request, if provided. + 2. Search the project for existing `.font` files (e.g., `assets/fonts/`). If found, use the project font. Pick the most appropriate one if there are several (e.g., prefer distance field fonts). + 3. Fall back to `/builtins/fonts/default.font` only if no project fonts exist and the user did not specify one. + Always match the `material` to the font type (`label-df.material` for distance field, `label.material` for bitmap, `label-fnt.material` for BMFont). +- **Use distance field fonts with base size 50.** The `.font` resource should use `size: 50` and `output_format: TYPE_DISTANCE_FIELD`. This provides crisp text at any visual size. +- **Control text size via game object scale, not font size.** Do not create separate `.font` files for different text sizes. Instead, scale the game object that contains the Label component. For example, to display text at visual size 25, keep `size: 50` in the `.font` and set the game object's scale to `0.5`. + +## Workflow + +### Creating a new label + +1. Determine the file path (must end with `.label`). +2. Set required fields: `size`, `font`, `material`. +3. Set `text` with the desired content. +4. Add optional fields only if they differ from defaults. +5. Write the file using the field order from the reference above. + +### Editing an existing label + +1. Read the current `.label` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/material.md b/.agents/skills/defold-proto-file-editing/references/material.md new file mode 100644 index 0000000..f2de636 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/material.md @@ -0,0 +1,705 @@ +# Editing Materials + +Creates and edits Defold `.material` resource files using Protobuf Text Format. + +## Overview + +A Material defines how a graphical component (sprite, tilemap, font, GUI node, model, mesh, etc.) is rendered. It holds tags used by the render pipeline to select objects for rendering, references to vertex and fragment shader programs, shader constants (uniforms), texture samplers, and custom vertex attributes. Materials are referenced from components and listed in render resources. + +## File format + +Material files (`.material`) use **Protobuf Text Format** based on the `MaterialDesc` message from `render/material_ddf.proto`. + +### Canonical example — sprite material with user constant + +```protobuf +name: "sprite" +tags: "tile" +vertex_program: "/example/recolor.vp" +fragment_program: "/example/recolor.fp" +vertex_constants { + name: "view_proj" + type: CONSTANT_TYPE_VIEWPROJ +} +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +samplers { + name: "texture_sampler" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_DEFAULT + filter_mag: FILTER_MODE_MAG_DEFAULT +} +``` + +### Canonical example — 3D model material with local vertex space + +```protobuf +name: "unlit" +tags: "model" +vertex_program: "/example/unlit.vp" +fragment_program: "/example/unlit.fp" +vertex_space: VERTEX_SPACE_LOCAL +vertex_constants { + name: "mtx_view" + type: CONSTANT_TYPE_VIEW +} +vertex_constants { + name: "mtx_proj" + type: CONSTANT_TYPE_PROJECTION +} +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +samplers { + name: "texture0" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR + max_anisotropy: 0.0 +} +``` + +## Fields reference + +Fields are listed in proto field number order. + +### name (required) — `string` + +The identity of the material. This name is used in render resources and with `render.enable_material()`. Must be unique within the project. + +```protobuf +name: "my_material" +``` + +### tags (repeated) — `string` + +Tags used by `render.predicate()` to collect components for rendering. Each tag gets its own line. The maximum number of tags across a project is 32. Common tags: `"tile"` (sprites, tilemaps), `"model"` (3D models), `"gui"` (GUI), `"text"` (fonts/labels), `"particle"` (particle effects), `"debug"` (debug rendering). + +```protobuf +tags: "tile" +``` + +Multiple tags: + +```protobuf +tags: "model" +tags: "custom_pass" +``` + +**Omission rule**: Omit if no tags are needed (but materials almost always need at least one tag to be rendered). + +### vertex_program (required) — `string` + +Absolute resource path to a vertex shader program file (`.vp`). + +```protobuf +vertex_program: "/builtins/materials/sprite.vp" +``` + +### fragment_program (required) — `string` + +Absolute resource path to a fragment shader program file (`.fp`). + +```protobuf +fragment_program: "/builtins/materials/sprite.fp" +``` + +### vertex_space (optional) — enum `VertexSpace` + +Controls the coordinate space of vertex data passed to the vertex shader. Default: `VERTEX_SPACE_WORLD` (value `0`). + +| Value | Description | +|-------|-------------| +| `VERTEX_SPACE_WORLD` | Vertices are in world space (default). Used for 2D components like sprites that are batched in world space. | +| `VERTEX_SPACE_LOCAL` | Vertices are in local (object) space. Used for 3D models where vertex transform is done in the shader. Required for instancing. | + +**Omission rule**: Omit if `VERTEX_SPACE_WORLD`. + +```protobuf +vertex_space: VERTEX_SPACE_LOCAL +``` + +### vertex_constants (repeated) — `Constant` + +Shader uniforms passed to the vertex shader program. Each entry gets its own `vertex_constants { ... }` block. + +```protobuf +vertex_constants { + name: "view_proj" + type: CONSTANT_TYPE_VIEWPROJ +} +``` + +**Omission rule**: Omit if no vertex constants are needed. + +### fragment_constants (repeated) — `Constant` + +Shader uniforms passed to the fragment shader program. Each entry gets its own `fragment_constants { ... }` block. + +```protobuf +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +``` + +**Omission rule**: Omit if no fragment constants are needed. + +### textures (deprecated, repeated) — `string` + +Legacy texture list. **Do not use.** Use `samplers` with a `texture` field instead. + +**Omission rule**: Always omit. + +### samplers (repeated) — `Sampler` + +Texture sampler configurations. Each entry defines a sampler name (matching a `sampler2D` uniform in the shader), wrap modes, filter settings, optional max anisotropy, and an optional texture resource. + +Sprite, tilemap, GUI, and particle effect components automatically bind the first `sampler2D` to the component's image. For model components, samplers must be configured explicitly in the material to allow texture assignment in the editor. + +```protobuf +samplers { + name: "texture_sampler" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_DEFAULT + filter_mag: FILTER_MODE_MAG_DEFAULT +} +``` + +**Omission rule**: Omit if no samplers need explicit configuration (global graphics settings will apply). + +### max_page_count (optional) — `uint32` + +Maximum number of texture pages. Default: `0` (unlimited). + +**Omission rule**: Omit if `0`. + +```protobuf +max_page_count: 4 +``` + +### attributes (repeated) — `dmGraphics.VertexAttribute` + +Custom vertex attributes that provide additional per-vertex or per-instance data to the shader. Each entry defines an attribute name, optional semantic type, data type, vector type, step function, and default values. + +```protobuf +attributes { + name: "mycolor" + double_values { + v: 1.0 + v: 1.0 + v: 1.0 + v: 1.0 + } +} +``` + +**Omission rule**: Omit if no custom attributes are needed. + +## Nested message: Constant + +Each `vertex_constants` or `fragment_constants` entry contains: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | `string` | yes | The uniform name in the shader program | +| `type` | `ConstantType` | yes | The type of constant | +| `value` | repeated `dmMath.Vector4` | no | Initial values. Required for `CONSTANT_TYPE_USER` and `CONSTANT_TYPE_USER_MATRIX4`. Omit for engine-provided constants. | + +### ConstantType enum + +| Value | Description | +|-------|-------------| +| `CONSTANT_TYPE_USER` | A `vec4` constant for custom data. Mutable via `go.set()` / `go.animate()`. | +| `CONSTANT_TYPE_USER_MATRIX4` | A `mat4` constant for custom data. Mutable via `go.set()`. | +| `CONSTANT_TYPE_VIEWPROJ` | Combined view and projection matrix. | +| `CONSTANT_TYPE_WORLD` | World transform matrix. | +| `CONSTANT_TYPE_VIEW` | View (camera) matrix. | +| `CONSTANT_TYPE_PROJECTION` | Projection matrix. | +| `CONSTANT_TYPE_NORMAL` | Normal matrix (transpose inverse of world-view). | +| `CONSTANT_TYPE_WORLDVIEW` | Combined world and view matrix. | +| `CONSTANT_TYPE_WORLDVIEWPROJ` | Combined world, view, and projection matrix. | +| `CONSTANT_TYPE_TEXTURE` | Texture matrix. | + +### Constant with user value + +For `CONSTANT_TYPE_USER`, provide a `value` block with a `dmMath.Vector4` (components `x`, `y`, `z`, `w`, all default to `0.0`). Only include components that differ from `0.0`: + +```protobuf +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +``` + +For `CONSTANT_TYPE_USER_MATRIX4`, provide four `value` blocks (one per matrix row). + +### Constant with engine-provided value + +For engine-provided types (`VIEWPROJ`, `WORLD`, `VIEW`, `PROJECTION`, `NORMAL`, `WORLDVIEW`, `WORLDVIEWPROJ`, `TEXTURE`), omit the `value` field: + +```protobuf +vertex_constants { + name: "mtx_view" + type: CONSTANT_TYPE_VIEW +} +``` + +## Nested message: Sampler + +Each `samplers` entry contains: + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | `string` | yes | — | Sampler name matching the `sampler2D` uniform in the shader | +| `wrap_u` | `WrapMode` | yes | — | Horizontal wrap mode | +| `wrap_v` | `WrapMode` | yes | — | Vertical wrap mode | +| `filter_min` | `FilterModeMin` | yes | — | Minification filter | +| `filter_mag` | `FilterModeMag` | yes | — | Magnification filter | +| `max_anisotropy` | `float` | no | `1.0` | Anisotropic filtering level. Set to `0.0` to disable. | +| `texture` | `string` | no | `""` | Absolute resource path to a texture/image. Optional. | + +### WrapMode enum + +| Value | Description | +|-------|-------------| +| `WRAP_MODE_REPEAT` | Repeats texture data outside [0,1] range (default, value `0`) | +| `WRAP_MODE_MIRRORED_REPEAT` | Repeats with mirroring every second repetition | +| `WRAP_MODE_CLAMP_TO_EDGE` | Clamps to edge pixels | + +### FilterModeMin enum + +| Value | Description | +|-------|-------------| +| `FILTER_MODE_MIN_NEAREST` | Nearest texel (value `0`) | +| `FILTER_MODE_MIN_LINEAR` | Weighted linear average of 2×2 texels | +| `FILTER_MODE_MIN_NEAREST_MIPMAP_NEAREST` | Nearest texel within nearest mipmap | +| `FILTER_MODE_MIN_NEAREST_MIPMAP_LINEAR` | Nearest texel, linear between two mipmaps | +| `FILTER_MODE_MIN_LINEAR_MIPMAP_NEAREST` | Linear within nearest mipmap | +| `FILTER_MODE_MIN_LINEAR_MIPMAP_LINEAR` | Linear within and between mipmaps (trilinear) | +| `FILTER_MODE_MIN_DEFAULT` | Uses `game.project` Graphics settings | + +### FilterModeMag enum + +| Value | Description | +|-------|-------------| +| `FILTER_MODE_MAG_NEAREST` | Nearest texel (value `0`) | +| `FILTER_MODE_MAG_LINEAR` | Linear interpolation | +| `FILTER_MODE_MAG_DEFAULT` | Uses `game.project` Graphics settings | + +### Sampler with default filter (for sprites) + +```protobuf +samplers { + name: "texture_sampler" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_DEFAULT + filter_mag: FILTER_MODE_MAG_DEFAULT +} +``` + +### Sampler with linear filter (for 3D models) + +```protobuf +samplers { + name: "texture0" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR + max_anisotropy: 0.0 +} +``` + +### Sampler with repeating wrap (for tiled textures) + +```protobuf +samplers { + name: "texture0" + wrap_u: WRAP_MODE_REPEAT + wrap_v: WRAP_MODE_REPEAT + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR + max_anisotropy: 0.0 +} +``` + +## Nested message: VertexAttribute + +Each `attributes` entry contains: + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | `string` | yes | — | Attribute name matching the shader `attribute` declaration | +| `semantic_type` | `SemanticType` | no | `SEMANTIC_TYPE_NONE` | Semantic meaning of the attribute | +| `normalize` | `bool` | no | `false` | Whether GPU normalizes values | +| `data_type` | `DataType` | no | `TYPE_FLOAT` | Backing data type | +| `coordinate_space` | `CoordinateSpace` | no | `COORDINATE_SPACE_LOCAL` | Coordinate space for position/normal semantics | +| `step_function` | `VertexStepFunction` | no | `VERTEX_STEP_FUNCTION_VERTEX` | Per-vertex or per-instance stepping | +| `vector_type` | `VectorType` | no | `VECTOR_TYPE_VEC4` | Vector dimensionality | +| `double_values` | `DoubleValues` | no | — | Float attribute values (repeated `v` entries) | +| `long_values` | `LongValues` | no | — | Integer attribute values (repeated `v` entries) | + +**Omission rules for attribute sub-fields**: Omit `semantic_type` if `SEMANTIC_TYPE_NONE`. Omit `normalize` if `false`. Omit `data_type` if `TYPE_FLOAT`. Omit `coordinate_space` if `COORDINATE_SPACE_LOCAL`. Omit `step_function` if `VERTEX_STEP_FUNCTION_VERTEX`. Omit `vector_type` if `VECTOR_TYPE_VEC4`. Omit value block if no default values are needed. + +### SemanticType enum + +| Value | Description | +|-------|-------------| +| `SEMANTIC_TYPE_NONE` | No special semantic (default) | +| `SEMANTIC_TYPE_POSITION` | Per-vertex position data | +| `SEMANTIC_TYPE_TEXCOORD` | Per-vertex texture coordinates | +| `SEMANTIC_TYPE_PAGE_INDEX` | Per-vertex page indices | +| `SEMANTIC_TYPE_COLOR` | Color data (shows color picker in editor) | +| `SEMANTIC_TYPE_NORMAL` | Per-vertex normal data | +| `SEMANTIC_TYPE_TANGENT` | Per-vertex tangent data | +| `SEMANTIC_TYPE_WORLD_MATRIX` | Per-vertex world matrix | +| `SEMANTIC_TYPE_NORMAL_MATRIX` | Per-vertex normal matrix | + +### DataType enum + +| Value | Description | +|-------|-------------| +| `TYPE_BYTE` | Signed 8-bit | +| `TYPE_UNSIGNED_BYTE` | Unsigned 8-bit | +| `TYPE_SHORT` | Signed 16-bit | +| `TYPE_UNSIGNED_SHORT` | Unsigned 16-bit | +| `TYPE_INT` | Signed 32-bit | +| `TYPE_UNSIGNED_INT` | Unsigned 32-bit | +| `TYPE_FLOAT` | Floating point (default) | + +### VectorType enum + +| Value | Description | +|-------|-------------| +| `VECTOR_TYPE_SCALAR` | Single scalar | +| `VECTOR_TYPE_VEC2` | 2D vector | +| `VECTOR_TYPE_VEC3` | 3D vector | +| `VECTOR_TYPE_VEC4` | 4D vector (default) | +| `VECTOR_TYPE_MAT2` | 2×2 matrix | +| `VECTOR_TYPE_MAT3` | 3×3 matrix | +| `VECTOR_TYPE_MAT4` | 4×4 matrix | + +### Simple custom color attribute + +```protobuf +attributes { + name: "newcolor" + semantic_type: SEMANTIC_TYPE_COLOR + double_values { + v: 0.3882 + v: 0.6078 + v: 1.0 + v: 1.0 + } +} +``` + +### Position attribute with vec2 type + +```protobuf +attributes { + name: "position_local" + semantic_type: SEMANTIC_TYPE_POSITION + vector_type: VECTOR_TYPE_VEC2 +} +``` + +### Per-instance attribute (for instancing) + +```protobuf +attributes { + name: "instance_color" + step_function: VERTEX_STEP_FUNCTION_INSTANCE + double_values { + v: 1.0 + v: 1.0 + v: 1.0 + v: 1.0 + } +} +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. This keeps files minimal and matches Defold editor behavior. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Integers**: No decimal point: `4`, not `4.0`. +5. **Strings**: Always double-quoted: `"text"`. +6. **Enums**: Use the enum constant name without quotes. +7. **Booleans**: `true` or `false`, no quotes. +8. **Repeated fields**: Each value gets its own line with the field name. +9. **Repeated messages**: Each entry gets its own `field_name { ... }` block. +10. **Field order**: Follow the proto field number order: `name`, `tags`, `vertex_program`, `fragment_program`, `vertex_space`, `vertex_constants`, `fragment_constants`, `samplers`, `max_page_count`, `attributes`. +11. **No trailing commas or semicolons**. +12. **No field number tags** — use field names only. +13. **Indentation**: 2 spaces per nesting level inside message blocks. +14. **Newlines**: One empty line between the end of a message block `}` and the next field. No empty line between consecutive scalar fields. + +## Common templates + +### Sprite material (default-like) + +```protobuf +name: "sprite" +tags: "tile" +vertex_program: "/assets/materials/sprite.vp" +fragment_program: "/assets/materials/sprite.fp" +vertex_constants { + name: "view_proj" + type: CONSTANT_TYPE_VIEWPROJ +} +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +samplers { + name: "texture_sampler" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_DEFAULT + filter_mag: FILTER_MODE_MAG_DEFAULT +} +``` + +### Unlit 3D model material + +```protobuf +name: "unlit" +tags: "model" +vertex_program: "/assets/materials/unlit.vp" +fragment_program: "/assets/materials/unlit.fp" +vertex_space: VERTEX_SPACE_LOCAL +vertex_constants { + name: "mtx_view" + type: CONSTANT_TYPE_VIEW +} +vertex_constants { + name: "mtx_proj" + type: CONSTANT_TYPE_PROJECTION +} +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +samplers { + name: "texture0" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR + max_anisotropy: 0.0 +} +``` + +### Model material with multiple samplers + +```protobuf +name: "multi_tex" +tags: "model" +vertex_program: "/assets/materials/multi_tex.vp" +fragment_program: "/assets/materials/multi_tex.fp" +vertex_space: VERTEX_SPACE_LOCAL +vertex_constants { + name: "mtx_world" + type: CONSTANT_TYPE_WORLD +} +vertex_constants { + name: "mtx_view" + type: CONSTANT_TYPE_VIEW +} +vertex_constants { + name: "mtx_proj" + type: CONSTANT_TYPE_PROJECTION +} +samplers { + name: "texture0" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR + max_anisotropy: 0.0 +} +samplers { + name: "texture_pattern" + wrap_u: WRAP_MODE_REPEAT + wrap_v: WRAP_MODE_REPEAT + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR + max_anisotropy: 0.0 +} +``` + +### Sprite material with custom vertex attributes + +```protobuf +name: "sprite" +tags: "tile" +vertex_program: "/assets/materials/custom_sprite.vp" +fragment_program: "/assets/materials/custom_sprite.fp" +vertex_constants { + name: "view_proj" + type: CONSTANT_TYPE_VIEWPROJ +} +samplers { + name: "texture_sampler" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_DEFAULT + filter_mag: FILTER_MODE_MAG_DEFAULT +} +attributes { + name: "mycolor" + semantic_type: SEMANTIC_TYPE_COLOR + double_values { + v: 1.0 + v: 1.0 + v: 1.0 + v: 1.0 + } +} +``` + +### Sprite material with local UV coordinates + +```protobuf +name: "sprite" +tags: "tile" +vertex_program: "/assets/materials/sprite_local_uv.vp" +fragment_program: "/assets/materials/sprite_local_uv.fp" +vertex_constants { + name: "view_proj" + type: CONSTANT_TYPE_VIEWPROJ +} +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +samplers { + name: "texture_sampler" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_DEFAULT + filter_mag: FILTER_MODE_MAG_DEFAULT +} +attributes { + name: "position_local" + semantic_type: SEMANTIC_TYPE_POSITION + vector_type: VECTOR_TYPE_VEC2 +} +attributes { + name: "sprite_size" + double_values { + v: 64.0 + v: 64.0 + } + vector_type: VECTOR_TYPE_VEC2 +} +``` + +### Instanced 3D model material + +```protobuf +name: "instanced_model" +tags: "model" +vertex_program: "/assets/materials/instanced.vp" +fragment_program: "/assets/materials/instanced.fp" +vertex_space: VERTEX_SPACE_LOCAL +vertex_constants { + name: "mtx_view" + type: CONSTANT_TYPE_VIEW +} +vertex_constants { + name: "mtx_proj" + type: CONSTANT_TYPE_PROJECTION +} +samplers { + name: "texture0" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR + max_anisotropy: 0.0 +} +attributes { + name: "instance_color" + step_function: VERTEX_STEP_FUNCTION_INSTANCE + double_values { + v: 1.0 + v: 1.0 + v: 1.0 + v: 1.0 + } +} +``` + +## Workflow + +### Creating a new material + +1. Determine the file path (must end with `.material`). +2. Set the required `name` field — should be unique and descriptive. +3. Add at least one `tags` entry to ensure the material is included in a render predicate. +4. Set `vertex_program` and `fragment_program` to the shader file paths. +5. Set `vertex_space: VERTEX_SPACE_LOCAL` if creating a material for 3D models (required for instancing). Omit for 2D components (sprites, tilemaps) which use world space by default. +6. Add `vertex_constants` and/or `fragment_constants` for shader uniforms. Engine-provided constants need only `name` and `type`. User constants also need a `value` block. +7. Add `samplers` entries for texture configuration. Match sampler names to `sampler2D` uniforms in the shaders. +8. Add `attributes` if custom vertex attributes are needed. +9. Write the file using the field order from the reference above. + +### Editing an existing material + +1. Read the current `.material` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/mesh.md b/.agents/skills/defold-proto-file-editing/references/mesh.md new file mode 100644 index 0000000..9642354 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/mesh.md @@ -0,0 +1,155 @@ +# Editing Meshes + +Creates and edits Defold `.mesh` component files using Protobuf Text Format. + +## Overview + +A Mesh component renders custom 3D geometry defined by a buffer file (`.buffer`). Unlike the Model component which loads glTF files, Mesh works with raw vertex data streams and can be manipulated at runtime via `buffer.create()`, `buffer.get_stream()`, and `resource.set_buffer()`. Meshes are NOT automatically frustum-culled; AABB metadata must be set manually via `buffer.set_metadata()`. If the material's Vertex Space is set to World Space, the position and normal streams are needed for the engine to transform vertices correctly. + +## File format + +Mesh files (`.mesh`) use **Protobuf Text Format** based on the `MeshDesc` message from `gamesys/mesh_ddf.proto`. + +### Canonical example + +```protobuf +material: "/builtins/materials/debug.material" +vertices: "/assets/meshes/plane.buffer" +textures: "/assets/textures/diffuse.png" +``` + +## Fields reference + +Fields are listed in proto field number order. + +### material (required) — `string` + +Absolute resource path to the material used for rendering the mesh. + +```protobuf +material: "/builtins/materials/debug.material" +``` + +### vertices (required) — `string` + +Absolute resource path to a buffer file (`.buffer`) describing the mesh vertex data per stream. + +```protobuf +vertices: "/assets/meshes/plane.buffer" +``` + +### textures (repeated) — `string` + +Absolute resource paths to textures used by the mesh (tex0..tex7). Each texture gets its own `textures:` line. Up to 8 textures can be bound. + +**Omission rule**: Omit if the mesh does not use any textures. + +```protobuf +textures: "/assets/textures/diffuse.png" +textures: "/assets/textures/normal.png" +``` + +### primitive_type (optional) — `PrimitiveType` + +How the vertex data is interpreted for rendering. Default: `PRIMITIVE_TRIANGLES`. + +**Omission rule**: Omit if `PRIMITIVE_TRIANGLES`. + +```protobuf +primitive_type: PRIMITIVE_LINES +``` + +### position_stream (optional) — `string` + +Name of the position stream in the buffer. Automatically provided as input to the vertex shader. Required when the material's Vertex Space is World Space. + +**Omission rule**: Omit if not needed (empty string default). + +```protobuf +position_stream: "position" +``` + +### normal_stream (optional) — `string` + +Name of the normal stream in the buffer. Automatically provided as input to the vertex shader. Required when the material's Vertex Space is World Space. + +**Omission rule**: Omit if not needed (empty string default). + +```protobuf +normal_stream: "normal" +``` + +## Enum: PrimitiveType + +| Constant | Value | Description | +|----------|-------|-------------| +| `PRIMITIVE_LINES` | 1 | Vertices are interpreted as line segments | +| `PRIMITIVE_TRIANGLES` | 4 | Vertices are interpreted as triangles (default) | +| `PRIMITIVE_TRIANGLE_STRIP` | 5 | Vertices are interpreted as a triangle strip | + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. This keeps files minimal and matches Defold editor behavior. +2. **Floats**: Always include decimal point: `1.0`, not `1`. +3. **Integers**: No decimal point: `4`, not `4.0`. +4. **Strings**: Always double-quoted: `"text"`. +5. **Enums**: Use the enum constant name without quotes. +6. **Repeated strings**: Each value gets its own `field_name: "value"` line. +7. **Field order**: Follow the proto field number order: `material`, `vertices`, `textures`, `primitive_type`, `position_stream`, `normal_stream`. +8. **No trailing commas or semicolons**. +9. **No field number tags** — use field names only. +10. **No empty lines between fields** (all fields are scalar or repeated scalar). + +## Common templates + +### Basic mesh with texture + +```protobuf +material: "/builtins/materials/debug.material" +vertices: "/assets/meshes/plane.buffer" +textures: "/assets/textures/diffuse.png" +``` + +### Basic mesh without texture + +```protobuf +material: "/builtins/materials/debug.material" +vertices: "/assets/meshes/cube.buffer" +``` + +### Mesh with lines primitive type + +```protobuf +material: "/builtins/materials/debug.material" +vertices: "/assets/meshes/wireframe.buffer" +primitive_type: PRIMITIVE_LINES +``` + +### Mesh with explicit stream names + +```protobuf +material: "/assets/materials/custom_mesh.material" +vertices: "/assets/meshes/terrain.buffer" +textures: "/assets/textures/terrain.png" +position_stream: "position" +normal_stream: "normal" +``` + +## Workflow + +### Creating a new mesh + +1. Determine the file path (must end with `.mesh`). +2. Set the required `material` field to the material resource path. +3. Set the required `vertices` field to the buffer file resource path. +4. Add `textures` lines for each texture the mesh needs (up to 8). +5. Set `primitive_type` only if not using the default `PRIMITIVE_TRIANGLES`. +6. Set `position_stream` and `normal_stream` if the material's Vertex Space is World Space. +7. Write the file using the field order from the reference above. + +### Editing an existing mesh + +1. Read the current `.mesh` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/model.md b/.agents/skills/defold-proto-file-editing/references/model.md new file mode 100644 index 0000000..8481e87 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/model.md @@ -0,0 +1,326 @@ +# Editing Models + +Creates and edits Defold `.model` component files using Protobuf Text Format. + +## Overview + +A Model component displays a 3D mesh (glTF `.gltf` or `.glb`) with one or more materials and textures. It optionally supports skeletal animation through a skeleton file and animation set. Models can reference built-in or custom materials depending on whether they are static, instanced, skinned, or both. + +## File format + +Model files (`.model`) use **Protobuf Text Format** based on the `ModelDesc` message from `gamesys/model_ddf.proto`. + +### Canonical example — static model + +```protobuf +mesh: "/assets/models/crate.glb" +name: "{{NAME}}" +materials { + name: "colormap" + material: "/builtins/materials/model.material" + textures { + sampler: "tex0" + texture: "/assets/models/crate_texture.png" + } +} +``` + +### Canonical example — skinned model with animations + +```protobuf +mesh: "/assets/models/Knight.glb" +skeleton: "/assets/models/Knight.glb" +animations: "/assets/models/Knight.glb" +default_animation: "T-Pose" +name: "{{NAME}}" +materials { + name: "knight_texture" + material: "/builtins/materials/model_skinned.material" + textures { + sampler: "tex0" + texture: "/assets/models/knight_texture.png" + } +} +``` + +## Fields reference + +Fields are listed in proto field number order. Deprecated fields (`material` #3, `textures` #4, `name` #10) are documented but should not be used in new files — use `materials` instead. + +### mesh (required) — `string` + +Absolute resource path to the glTF mesh file (`.gltf` or `.glb`). If the file contains multiple meshes, only the first one is read. + +```protobuf +mesh: "/assets/models/character.glb" +``` + +### material (deprecated) — `string` + +Legacy single-material field. **Do not use in new files.** Use the `materials` repeated field instead. + +**Omission rule**: Always omit. + +### textures (deprecated, repeated) — `string` + +Legacy texture list for the single deprecated material. **Do not use in new files.** Use `textures` inside `materials` entries instead. + +**Omission rule**: Always omit. + +### skeleton (optional) — `string` + +Absolute resource path to the glTF file (`.gltf` or `.glb`) containing the skeleton for animation. The skeleton must have a single root bone. Typically the same file as `mesh`. + +**Omission rule**: Omit if the model has no skeleton/animation. + +```protobuf +skeleton: "/assets/models/character.glb" +``` + +### animations (optional) — `string` + +Absolute resource path to an Animation Set File that contains the animations for this model. Often the same file as `mesh` and `skeleton`. + +**Omission rule**: Omit if the model has no animations. + +```protobuf +animations: "/assets/models/character.glb" +``` + +### default_animation (optional) — `string` + +The animation id (from the animation set) that plays automatically when the model loads. + +**Omission rule**: Omit if no default animation is needed. + +```protobuf +default_animation: "idle" +``` + +### name (optional) — `string` + +Internal name for the model instance. Typically set to `"{{NAME}}"` (a Defold editor placeholder that resolves to the component name at build time) or a custom descriptive name. + +**Omission rule**: Omit if not needed. The Defold editor typically auto-generates this as `"{{NAME}}"`. + +```protobuf +name: "{{NAME}}" +``` + +### materials (repeated) — `Material` + +Material bindings for the model. Each entry maps a material name (from the glTF file) to a Defold `.material` resource and its textures. A model can have multiple materials if its mesh uses multiple material slots. + +```protobuf +materials { + name: "default" + material: "/builtins/materials/model.material" + textures { + sampler: "tex0" + texture: "/assets/textures/diffuse.png" + } +} +``` + +### create_go_bones (optional) — `bool` + +Whether to create a game object for every bone in the model skeleton. This allows attaching other game objects (e.g., weapons) to bone positions using `model.get_go()`. Default: `true`. + +**Omission rule**: Omit if `true` (the default). + +```protobuf +create_go_bones: false +``` + +## Nested message: Material + +Each `materials` entry contains the following fields: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | `string` | yes | The material name as defined in the glTF model file | +| `material` | `string` | yes | Absolute resource path to a `.material` file | +| `textures` | repeated `Texture` | no | Texture bindings for this material | +| `attributes` | repeated `dmGraphics.VertexAttribute` | no | Custom vertex attribute overrides | + +### Built-in materials + +| Material | Use case | +|----------|----------| +| `/builtins/materials/model.material` | Static non-instanced models | +| `/builtins/materials/model_instances.material` | Static instanced models | +| `/builtins/materials/model_skinned.material` | Skinned (animated) non-instanced models | +| `/builtins/materials/model_skinned_instances.material` | Skinned (animated) instanced models | + +## Nested message: Texture + +Each `textures` entry inside a `Material` contains: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `sampler` | `string` | yes | The sampler name from the material (e.g., `"tex0"`) | +| `texture` | `string` | yes | Absolute resource path to the texture image file | + +```protobuf +textures { + sampler: "tex0" + texture: "/assets/textures/diffuse.png" +} +``` + +## Nested message: VertexAttribute + +Custom vertex attributes can override values from the material. See the `dmGraphics.VertexAttribute` message in `graphics/graphics_ddf.proto` for all fields. Common usage: + +```protobuf +attributes { + name: "tint" + double_values { + v: 1.0 + v: 0.0 + v: 0.0 + v: 1.0 + } +} +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. This keeps files minimal and matches Defold editor behavior. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Integers**: No decimal point: `4`, not `4.0`. +5. **Strings**: Always double-quoted: `"text"`. +6. **Enums**: Use the enum constant name without quotes. +7. **Booleans**: `true` or `false`, no quotes. +8. **Repeated messages**: Each entry gets its own `field_name { ... }` block. +9. **Field order**: Follow the proto field number order: `mesh`, `skeleton`, `animations`, `default_animation`, `name`, `materials`, `create_go_bones`. +10. **No trailing commas or semicolons**. +11. **No field number tags** — use field names only. +12. **Indentation**: 2 spaces per nesting level inside message blocks. +13. **Newlines**: One empty line between the end of a message block `}` and the next field. No empty line between consecutive scalar fields. + +## Common templates + +### Static model (non-instanced) + +```protobuf +mesh: "/assets/models/crate.glb" +name: "{{NAME}}" +materials { + name: "default" + material: "/builtins/materials/model.material" + textures { + sampler: "tex0" + texture: "/assets/textures/crate.png" + } +} +``` + +### Static instanced model + +```protobuf +mesh: "/assets/models/tree.glb" +name: "{{NAME}}" +materials { + name: "default" + material: "/builtins/materials/model_instances.material" + textures { + sampler: "tex0" + texture: "/assets/textures/tree.png" + } +} +``` + +### Skinned model with animations + +```protobuf +mesh: "/assets/models/character.glb" +skeleton: "/assets/models/character.glb" +animations: "/assets/models/character.glb" +default_animation: "idle" +name: "{{NAME}}" +materials { + name: "body" + material: "/builtins/materials/model_skinned.material" + textures { + sampler: "tex0" + texture: "/assets/textures/character.png" + } +} +``` + +### Model without textures (vertex colors only) + +```protobuf +mesh: "/assets/models/shape.glb" +name: "{{NAME}}" +materials { + name: "default" + material: "/assets/materials/vertex_color.material" +} +``` + +### Multi-material model + +```protobuf +mesh: "/assets/models/vehicle.glb" +name: "{{NAME}}" +materials { + name: "body" + material: "/builtins/materials/model.material" + textures { + sampler: "tex0" + texture: "/assets/textures/vehicle_body.png" + } +} +materials { + name: "wheels" + material: "/builtins/materials/model.material" + textures { + sampler: "tex0" + texture: "/assets/textures/vehicle_wheels.png" + } +} +``` + +### Skinned instanced model without bone game objects + +```protobuf +mesh: "/assets/models/crowd_npc.glb" +skeleton: "/assets/models/crowd_npc.glb" +animations: "/assets/models/crowd_npc.glb" +default_animation: "walk" +name: "{{NAME}}" +materials { + name: "default" + material: "/builtins/materials/model_skinned_instances.material" + textures { + sampler: "tex0" + texture: "/assets/textures/npc.png" + } +} +create_go_bones: false +``` + +## Workflow + +### Creating a new model + +1. Determine the file path (must end with `.model`). +2. Set the required `mesh` field to the glTF resource path. +3. Add at least one `materials` entry with the material name from the glTF file, a Defold material path, and texture bindings. +4. Choose the correct built-in material: + - Static: `model.material` or `model_instances.material` + - Skinned: `model_skinned.material` or `model_skinned_instances.material` +5. If the model is animated, set `skeleton`, `animations`, and optionally `default_animation`. +6. Set `name` to `"{{NAME}}"` or a descriptive name. +7. Set `create_go_bones: false` only if bone game objects are not needed (the default is `true`). +8. Write the file using the field order from the reference above. + +### Editing an existing model + +1. Read the current `.model` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/objectinterpolation.md b/.agents/skills/defold-proto-file-editing/references/objectinterpolation.md new file mode 100644 index 0000000..01a2f10 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/objectinterpolation.md @@ -0,0 +1,178 @@ +# Editing Object Interpolations + +Creates and edits Defold `.objectinterpolation` component files using Protobuf Text Format. + +## Overview + +Object Interpolation is an **extension component** (not built-in). It interpolates position and rotation of a game object between fixed update steps. Typical use cases: smoothing movement of objects with `collisionobject` components in fixed time step mode, or smoothing movement driven by `fixed_update()`. + +### Prerequisite + +This component is only available when the `defold-object-interpolation` library is added as a dependency in `game.project`. Before creating `.objectinterpolation` files, verify the dependency exists: + +```ini +[project] +dependencies#N = https://github.com/indiesoftby/defold-object-interpolation/archive/...zip +``` + +If the dependency is missing, inform the user that they need to add it to `game.project` and fetch libraries before the component can be used. + +## File format + +Object Interpolation files (`.objectinterpolation`) use **Protobuf Text Format** based on the `ObjectInterpolationDesc` message from the extension's `objectinterpolation_ddf.proto`. + +### Canonical example + +```protobuf +apply_transform: APPLY_TRANSFORM_TARGET +target_object: "player" +``` + +### Minimal file (all defaults) + +An empty file is valid — `apply_transform` defaults to `APPLY_TRANSFORM_NONE` and `target_object` is optional. + +```protobuf +``` + +### Embedded in a game object + +When embedded in a `.go` file, the data appears inside an `embedded_components` block: + +```protobuf +embedded_components { + id: "objectinterpolation" + type: "objectinterpolation" + data: "apply_transform: APPLY_TRANSFORM_TARGET\n" + "target_object: \"visual\"\n" + "" +} +``` + +## Fields reference + +### apply_transform (required) — `ApplyTransform` enum + +The interpolation mode. Default: `APPLY_TRANSFORM_NONE`. + +- `APPLY_TRANSFORM_NONE` — only calculates interpolated values, does not apply them to any object. Values are readable via `go.get()`. +- `APPLY_TRANSFORM_TARGET` — applies interpolated position and rotation to the game object specified in `target_object`. + +**Omission rule**: Omit if `APPLY_TRANSFORM_NONE`. + +```protobuf +apply_transform: APPLY_TRANSFORM_TARGET +``` + +### target_object (optional) — `string` + +The game object identifier to apply interpolated transform to. Supports relative and absolute paths (like `go.get_id()`). Only meaningful when `apply_transform` is `APPLY_TRANSFORM_TARGET`. + +**Omission rule**: Omit if empty or not needed (when `apply_transform` is `APPLY_TRANSFORM_NONE`). + +```protobuf +target_object: "visual" +``` + +## Enum: ApplyTransform + +| Constant | Display Name | Description | +|----------|-------------|-------------| +| `APPLY_TRANSFORM_NONE` | None | Only interpolates values for reading via `go.get()` | +| `APPLY_TRANSFORM_TARGET` | Target Object | Applies interpolated transform to `target_object` | + +## Common templates + +### Passive interpolation (read-only values) + +Use when you want to read interpolated position/rotation from script but not automatically apply them. + +```protobuf +``` + +### Target object interpolation + +The most common setup — interpolate and apply to a visual representation object. + +```protobuf +apply_transform: APPLY_TRANSFORM_TARGET +target_object: "visual" +``` + +## Best practice: collection with physics + visual objects + +The recommended setup is a collection with two game objects: + +1. **Physics object** — has `collisionobject` and `objectinterpolation` components. The `objectinterpolation` targets the visual object using a relative ID within the collection. +2. **Visual object** — has a `sprite` or `model` component that displays the physics object on screen with smooth interpolated movement. + +The `target_object` field uses a **relative path** within the collection scope (e.g., `"visual"` refers to a sibling game object named `visual` in the same collection). + +### Example `.objectinterpolation` for this setup + +```protobuf +apply_transform: APPLY_TRANSFORM_TARGET +target_object: "visual" +``` + +Where `visual` is the ID of the sibling game object in the same collection that has the `sprite` or `model` component. + +## Runtime API reference + +### Properties (via `go.get()` / `go.set()`) + +| Property | Type | Access | Description | +|----------|------|--------|-------------| +| `apply_transform` | number | read | Current mode (`object_interpolation.APPLY_TRANSFORM_NONE` or `object_interpolation.APPLY_TRANSFORM_TARGET`) | +| `target_object` | hash | read | Target game object identifier | +| `position` | vector3 | read/write | Interpolated position. Setting resets interpolation. | +| `rotation` | quat | read/write | Interpolated rotation. Setting resets interpolation. | + +### Messages + +- `set_apply_transform` — change mode and target at runtime: + +```lua +msg.post("#objectinterpolation", "set_apply_transform", { + apply_transform = object_interpolation.APPLY_TRANSFORM_TARGET, + target_object = hash("/object_to_move") +}) +``` + +### Lua API + +- `object_interpolation.set_enabled(enabled)` — enable/disable interpolation globally. +- `object_interpolation.is_enabled()` — check if enabled. + +### game.project settings + +```ini +[object_interpolation] +max_count = 1024 +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. +2. **Strings**: Always double-quoted: `"text"`. +3. **Enums**: Use the constant name without quotes: `APPLY_TRANSFORM_TARGET`. +4. **Field order**: Follow proto field number order: `apply_transform`, `target_object`. +5. **No trailing commas or semicolons**. +6. **No empty lines between fields** (all fields are scalar). + +## Workflow + +### Creating a new object interpolation component + +1. **Verify dependency**: Check that `game.project` includes the `defold-object-interpolation` library URL in its `dependencies` list. If missing, inform the user. +2. Determine the file path (must end with `.objectinterpolation`). +3. Choose the mode: `APPLY_TRANSFORM_NONE` (default, omit field) or `APPLY_TRANSFORM_TARGET`. +4. If using `APPLY_TRANSFORM_TARGET`, set `target_object` to the target game object path. +5. Write the file using the field order from the reference above. + +### Editing an existing object interpolation component + +1. Read the current `.objectinterpolation` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/particlefx.md b/.agents/skills/defold-proto-file-editing/references/particlefx.md new file mode 100644 index 0000000..630abeb --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/particlefx.md @@ -0,0 +1,1025 @@ +# Editing Particle Effects + +Creates and edits Defold `.particlefx` files using Protobuf Text Format. + +## Overview + +A ParticleFX is a visual effect composed of one or more emitters and optional modifiers. Emitters are positioned shapes that emit particles with configurable spawn rate, life time, speed, size, color, rotation, and other properties animated via spline curves. Modifiers affect particle velocity through acceleration, drag, radial attraction/repulsion, or vortex forces. Modifiers can be placed at the effect level (affecting all emitters) or as children of a specific emitter. + +## File format + +ParticleFX files (`.particlefx`) use **Protobuf Text Format** based on the `ParticleFX` message from `particle_ddf.proto`. + +The top-level message contains: +- `emitters` (repeated) — one or more particle emitters +- `modifiers` (repeated) — effect-level modifiers affecting all emitters + +### Canonical example + +```protobuf +emitters { + id: "emitter" + mode: PLAY_MODE_ONCE + duration: 1.0 + space: EMISSION_SPACE_WORLD + tile_source: "/assets/particles/particles.atlas" + animation: "particle" + material: "/builtins/materials/particlefx.material" + max_particle_count: 64 + type: EMITTER_TYPE_CIRCLE + properties { + key: EMITTER_KEY_SPAWN_RATE + points { + y: 50.0 + } + } + properties { + key: EMITTER_KEY_SIZE_X + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_SIZE_Y + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_SIZE_Z + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_LIFE_TIME + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_SPEED + points { + y: 200.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_SIZE + points { + y: 20.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_RED + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_GREEN + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_BLUE + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ALPHA + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ROTATION + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_STRETCH_FACTOR_X + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_STRETCH_FACTOR_Y + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ANGULAR_VELOCITY + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_SCALE + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_RED + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_GREEN + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_BLUE + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_ALPHA + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_ROTATION + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_STRETCH_FACTOR_X + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_STRETCH_FACTOR_Y + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_ANGULAR_VELOCITY + points { + y: 0.0 + } + } +} +``` + +## Emitter fields reference + +Fields are listed in proto field number order. All emitter properties (`properties`) and particle properties (`particle_properties`) entries are required in practice — the Defold editor always writes all of them. + +### id (optional) — `string` + +Emitter identifier. Used when setting render constants for specific emitters. Default: `"emitter"`. + +**Omission rule**: Omit if `"emitter"`. + +```protobuf +id: "sparks" +``` + +### mode (required) — `PlayMode` + +Controls how the emitter plays. `PLAY_MODE_ONCE` stops after reaching duration. `PLAY_MODE_LOOP` restarts after reaching duration. + +```protobuf +mode: PLAY_MODE_LOOP +``` + +### duration (optional) — `float` + +Number of seconds the emitter emits particles. Default: `0.0`. A value of `0` with `PLAY_MODE_LOOP` means the emitter runs indefinitely. + +**Omission rule**: Omit if `0.0`. + +```protobuf +duration: 2.0 +``` + +### space (required) — `EmissionSpace` + +Which geometrical space the spawned particles exist in. `EMISSION_SPACE_WORLD` moves particles independently of the emitter. `EMISSION_SPACE_EMITTER` moves particles relative to the emitter. + +```protobuf +space: EMISSION_SPACE_WORLD +``` + +### position (optional) — `dmMath.Point3` + +Transform position of the emitter relative to the ParticleFX component. Components: `x, y, z`, all default `0.0`. + +**Omission rule**: Omit if all components are `0.0`. + +```protobuf +position { + x: 10.0 + y: 20.0 +} +``` + +### rotation (optional) — `dmMath.Quat` + +Transform rotation of the emitter relative to the ParticleFX component. Components: `x, y, z, w`, where `x/y/z` default to `0.0`, `w` defaults to `1.0`. + +**Omission rule**: Omit if at default (identity quaternion). + +```protobuf +rotation { + z: 0.7071068 + w: 0.7071068 +} +``` + +### tile_source (required) — `string` + +Absolute resource path to the image file (Atlas or Tile Source) used for texturing and animating particles. + +```protobuf +tile_source: "/assets/particles/particles.atlas" +``` + +### animation (required) — `string` + +Animation name from the `tile_source` to use on particles. + +```protobuf +animation: "spark" +``` + +### material (required) — `string` + +Absolute resource path to the material used for shading particles. The built-in particle material is `/builtins/materials/particlefx.material`. + +```protobuf +material: "/builtins/materials/particlefx.material" +``` + +### blend_mode (optional) — `BlendMode` + +Blending mode for particle rendering. Default: `BLEND_MODE_ALPHA`. + +**Omission rule**: Omit if `BLEND_MODE_ALPHA`. + +```protobuf +blend_mode: BLEND_MODE_ADD +``` + +### particle_orientation (optional) — `ParticleOrientation` + +How emitted particles are oriented. Default: `PARTICLE_ORIENTATION_DEFAULT`. + +**Omission rule**: Omit if `PARTICLE_ORIENTATION_DEFAULT`. + +```protobuf +particle_orientation: PARTICLE_ORIENTATION_MOVEMENT_DIRECTION +``` + +### inherit_velocity (optional) — `float` + +Scale value of how much emitter velocity particles inherit. Default: `0.0`. Only works when `space` is `EMISSION_SPACE_WORLD`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +inherit_velocity: 0.5 +``` + +### max_particle_count (required) — `uint32` + +Maximum number of particles from this emitter that can exist simultaneously. + +```protobuf +max_particle_count: 128 +``` + +### type (required) — `EmitterType` + +Shape of the emitter. Controls how particles are distributed and their initial direction. + +```protobuf +type: EMITTER_TYPE_BOX +``` + +### start_delay (optional) — `float` + +Number of seconds the emitter waits before emitting. Default: `0.0`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +start_delay: 0.5 +``` + +### properties (repeated) — `Emitter.Property` + +Keyed emitter properties animated over the emitter's play time. One entry per `EmitterKey` (except `EMITTER_KEY_COUNT`) is required. See [Emitter Property](#emitter-property) section. + +### particle_properties (repeated) — `Emitter.ParticleProperty` + +Keyed particle properties animated over each particle's life time. One entry per `ParticleKey` (except `PARTICLE_KEY_COUNT`) is required. See [Particle Property](#particle-property) section. + +### modifiers (repeated) — `Modifier` + +Emitter-level modifiers that affect only this emitter's particles. See [Modifier](#modifier) section. + +### size_mode (optional) — `SizeMode` + +Controls how flipbook animations are sized. `SIZE_MODE_MANUAL` uses the particle size property. `SIZE_MODE_AUTO` uses the source image frame size, ignoring the size property. Default: `SIZE_MODE_MANUAL`. + +**Omission rule**: Omit if `SIZE_MODE_MANUAL`. + +```protobuf +size_mode: SIZE_MODE_AUTO +``` + +### start_delay_spread (optional) — `float` + +Random variation for `start_delay`. Default: `0.0`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +start_delay_spread: 0.2 +``` + +### duration_spread (optional) — `float` + +Random variation for `duration`. Default: `0.0`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +duration_spread: 0.5 +``` + +### stretch_with_velocity (optional) — `bool` + +Whether to scale particle stretch in the direction of movement. Default: `false`. + +**Omission rule**: Omit if `false`. + +```protobuf +stretch_with_velocity: true +``` + +### start_offset (optional) — `float` + +Number of seconds to prewarm the simulation. The emitter starts as if it had been running for this duration. Default: `0.0`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +start_offset: 2.0 +``` + +### pivot (optional) — `dmMath.Point3` + +Pivot point for the emitter. Components: `x, y, z`, all default `0.0`. + +**Omission rule**: Omit if all components are `0.0`. + +```protobuf +pivot { + x: 0.5 + y: 0.5 +} +``` + +### attributes (repeated) — `dmGraphics.VertexAttribute` + +Custom vertex attribute overrides. See the `dmGraphics.VertexAttribute` message in `graphics/graphics_ddf.proto`. + +**Omission rule**: Omit if no custom attributes are needed. + +## Emitter Property + +Each `properties` entry inside an emitter represents a keyed property animated over the emitter's play time. + +```protobuf +properties { + key: EMITTER_KEY_SPAWN_RATE + points { + y: 100.0 + } + spread: 10.0 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `key` | `EmitterKey` | yes | Which property this entry controls | +| `points` | repeated `SplinePoint` | yes | Spline curve points defining the value over time | +| `spread` | `float` | no | Random variation (value ± spread). Default: `0.0` | + +All `EmitterKey` values (except `EMITTER_KEY_COUNT`) must have a corresponding `properties` entry. Order follows the enum value order. + +## Particle Property + +Each `particle_properties` entry represents a property animated over each particle's life time. + +```protobuf +particle_properties { + key: PARTICLE_KEY_ALPHA + points { + y: 1.0 + } + points { + x: 0.5 + y: 0.8 + t_x: 1.0 + } + points { + x: 1.0 + y: 0.0 + t_x: 1.0 + } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `key` | `ParticleKey` | yes | Which property this entry controls | +| `points` | repeated `SplinePoint` | yes | Spline curve points defining the value over particle life | + +All `ParticleKey` values (except `PARTICLE_KEY_COUNT`) must have a corresponding `particle_properties` entry. Order follows the enum value order. + +Note: `ParticleProperty` does not have a `spread` field (unlike `Emitter.Property`). + +## SplinePoint format + +Spline points define values over time using cubic Hermite spline curves. + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `x` | `float` | no | — | Time position along the curve (0.0 = start, 1.0 = end) | +| `y` | `float` | yes | — | Value at this point | +| `t_x` | `float` | no | `1.0` | Tangent X (controls curve shape) | +| `t_y` | `float` | no | `0.0` | Tangent Y (controls curve shape) | + +### Constant value (no curve) + +For a constant property, use a single point with only `y`: + +```protobuf +points { + y: 50.0 +} +``` + +### Curve with multiple points + +For values animated over time, use multiple points. `x` ranges from `0.0` (start) to `1.0` (end): + +```protobuf +points { + y: 1.0 +} +points { + x: 0.7 + y: 1.0 + t_x: 1.0 +} +points { + x: 1.0 + y: 0.0 + t_x: 1.0 +} +``` + +## Modifier + +Modifiers affect particle velocity. They can be placed at the effect level (inside the top-level `ParticleFX`) or as children of an emitter. + +```protobuf +modifiers { + type: MODIFIER_TYPE_ACCELERATION + properties { + key: MODIFIER_KEY_MAGNITUDE + points { + y: -100.0 + } + } +} +``` + +### Modifier fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `type` | `ModifierType` | yes | — | Type of velocity modification | +| `use_direction` | `uint32` | no | `0` | Whether the modifier uses a direction vector | +| `position` | `dmMath.Point3` | no | `0, 0, 0` | Position relative to parent | +| `rotation` | `dmMath.Quat` | no | identity | Rotation relative to parent | +| `properties` | repeated `Modifier.Property` | yes | — | Modifier properties (magnitude, max distance) | + +### Modifier.Property + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `key` | `ModifierKey` | yes | — | `MODIFIER_KEY_MAGNITUDE` or `MODIFIER_KEY_MAX_DISTANCE` | +| `points` | repeated `SplinePoint` | yes | — | Spline curve points | +| `spread` | `float` | no | `0.0` | Random variation | + +`MODIFIER_KEY_MAX_DISTANCE` is only used for `MODIFIER_TYPE_RADIAL` and `MODIFIER_TYPE_VORTEX`. + +## Enum: EmitterType + +| Constant | Value | Description | +|----------|-------|-------------| +| `EMITTER_TYPE_CIRCLE` | 0 | Emits from a circle; particles directed outward. Size X = diameter | +| `EMITTER_TYPE_2DCONE` | 1 | Emits from a flat cone (triangle); particles directed out the top. Size X = width, Y = height | +| `EMITTER_TYPE_BOX` | 2 | Emits from a box; particles directed up along local Y. Size X/Y/Z = width/height/depth | +| `EMITTER_TYPE_SPHERE` | 3 | Emits from a sphere; particles directed outward. Size X = diameter | +| `EMITTER_TYPE_CONE` | 4 | Emits from a 3D cone; particles directed out the top disc. Size X = diameter, Y = height | + +## Enum: PlayMode + +| Constant | Value | Description | +|----------|-------|-------------| +| `PLAY_MODE_ONCE` | 0 | Stops after reaching duration | +| `PLAY_MODE_LOOP` | 1 | Restarts after reaching duration | + +## Enum: EmissionSpace + +| Constant | Value | Description | +|----------|-------|-------------| +| `EMISSION_SPACE_WORLD` | 0 | Particles move independently of the emitter | +| `EMISSION_SPACE_EMITTER` | 1 | Particles move relative to the emitter | + +## Enum: BlendMode + +| Constant | Value | Description | +|----------|-------|-------------| +| `BLEND_MODE_ALPHA` | 0 | Normal alpha blending | +| `BLEND_MODE_ADD` | 1 | Additive blending (brighten) | +| `BLEND_MODE_ADD_ALPHA` | 2 | **Deprecated** — do not use | +| `BLEND_MODE_MULT` | 3 | Multiply blending (darken) | +| `BLEND_MODE_SCREEN` | 4 | Screen blending (brighten) | + +## Enum: SizeMode + +| Constant | Value | Description | +|----------|-------|-------------| +| `SIZE_MODE_MANUAL` | 0 | Particle size controlled by size property | +| `SIZE_MODE_AUTO` | 1 | Particle size matches source image frame size | + +## Enum: ParticleOrientation + +| Constant | Value | Description | +|----------|-------|-------------| +| `PARTICLE_ORIENTATION_DEFAULT` | 0 | Unit orientation | +| `PARTICLE_ORIENTATION_INITIAL_DIRECTION` | 1 | Keeps initial orientation | +| `PARTICLE_ORIENTATION_MOVEMENT_DIRECTION` | 2 | Oriented according to velocity | +| `PARTICLE_ORIENTATION_ANGULAR_VELOCITY` | 3 | Oriented by angular velocity | + +## Enum: EmitterKey + +| Constant | Value | Description | +|----------|-------|-------------| +| `EMITTER_KEY_SPAWN_RATE` | 0 | Particles emitted per second | +| `EMITTER_KEY_SIZE_X` | 1 | Emitter shape size X | +| `EMITTER_KEY_SIZE_Y` | 2 | Emitter shape size Y | +| `EMITTER_KEY_SIZE_Z` | 3 | Emitter shape size Z | +| `EMITTER_KEY_PARTICLE_LIFE_TIME` | 4 | Particle lifespan in seconds | +| `EMITTER_KEY_PARTICLE_SPEED` | 5 | Initial particle speed | +| `EMITTER_KEY_PARTICLE_SIZE` | 6 | Initial particle size | +| `EMITTER_KEY_PARTICLE_RED` | 7 | Initial red color component | +| `EMITTER_KEY_PARTICLE_GREEN` | 8 | Initial green color component | +| `EMITTER_KEY_PARTICLE_BLUE` | 9 | Initial blue color component | +| `EMITTER_KEY_PARTICLE_ALPHA` | 10 | Initial alpha component | +| `EMITTER_KEY_PARTICLE_ROTATION` | 11 | Initial rotation (degrees) | +| `EMITTER_KEY_PARTICLE_STRETCH_FACTOR_X` | 12 | Initial stretch X (units) | +| `EMITTER_KEY_PARTICLE_STRETCH_FACTOR_Y` | 13 | Initial stretch Y (units) | +| `EMITTER_KEY_PARTICLE_ANGULAR_VELOCITY` | 14 | Initial angular velocity (degrees/second) | + +## Enum: ParticleKey + +| Constant | Value | Description | +|----------|-------|-------------| +| `PARTICLE_KEY_SCALE` | 0 | Scale over particle life | +| `PARTICLE_KEY_RED` | 1 | Red tint over particle life | +| `PARTICLE_KEY_GREEN` | 2 | Green tint over particle life | +| `PARTICLE_KEY_BLUE` | 3 | Blue tint over particle life | +| `PARTICLE_KEY_ALPHA` | 4 | Alpha over particle life | +| `PARTICLE_KEY_ROTATION` | 5 | Rotation over particle life (degrees) | +| `PARTICLE_KEY_STRETCH_FACTOR_X` | 6 | Stretch X over particle life (units) | +| `PARTICLE_KEY_STRETCH_FACTOR_Y` | 7 | Stretch Y over particle life (units) | +| `PARTICLE_KEY_ANGULAR_VELOCITY` | 8 | Angular velocity over particle life (degrees/second) | + +## Enum: ModifierType + +| Constant | Value | Description | +|----------|-------|-------------| +| `MODIFIER_TYPE_ACCELERATION` | 0 | Acceleration in a general direction | +| `MODIFIER_TYPE_DRAG` | 1 | Reduces acceleration proportional to velocity | +| `MODIFIER_TYPE_RADIAL` | 2 | Attracts or repels from a position | +| `MODIFIER_TYPE_VORTEX` | 3 | Circular/spiral movement around a position | + +## Enum: ModifierKey + +| Constant | Value | Description | +|----------|-------|-------------| +| `MODIFIER_KEY_MAGNITUDE` | 0 | Amount of effect on particles | +| `MODIFIER_KEY_MAX_DISTANCE` | 1 | Maximum distance for effect (Radial and Vortex only) | + +## Protobuf Text Format rules + +1. **Default omission**: Omit optional scalar fields that equal their proto default. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Integers**: No decimal point: `64`, not `64.0`. +5. **Strings**: Always double-quoted: `"text"`. +6. **Enums**: Use the enum constant name without quotes. +7. **Booleans**: `true` or `false`, no quotes. +8. **Repeated messages**: Each entry gets its own `field_name { ... }` block. +9. **Field order**: Follow the proto field number order. +10. **No trailing commas or semicolons**. +11. **No field number tags** — use field names only. +12. **Indentation**: 2 spaces per nesting level inside message blocks. +13. **All `properties` and `particle_properties` entries are required** — include one for each enum key (except `_COUNT` values), even if the value is `0.0` or `1.0`. + +## Common templates + +### Simple one-shot burst + +```protobuf +emitters { + id: "burst" + mode: PLAY_MODE_ONCE + duration: 0.2 + space: EMISSION_SPACE_WORLD + tile_source: "/assets/particles/particles.atlas" + animation: "particle" + material: "/builtins/materials/particlefx.material" + blend_mode: BLEND_MODE_ADD + max_particle_count: 32 + type: EMITTER_TYPE_CIRCLE + properties { + key: EMITTER_KEY_SPAWN_RATE + points { + y: 200.0 + } + } + properties { + key: EMITTER_KEY_SIZE_X + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_SIZE_Y + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_SIZE_Z + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_LIFE_TIME + points { + y: 0.5 + } + } + properties { + key: EMITTER_KEY_PARTICLE_SPEED + points { + y: 300.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_SIZE + points { + y: 15.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_RED + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_GREEN + points { + y: 0.8 + } + } + properties { + key: EMITTER_KEY_PARTICLE_BLUE + points { + y: 0.2 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ALPHA + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ROTATION + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_STRETCH_FACTOR_X + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_STRETCH_FACTOR_Y + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ANGULAR_VELOCITY + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_SCALE + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_RED + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_GREEN + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_BLUE + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_ALPHA + points { + y: 1.0 + } + points { + x: 1.0 + y: 0.0 + t_x: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_ROTATION + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_STRETCH_FACTOR_X + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_STRETCH_FACTOR_Y + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_ANGULAR_VELOCITY + points { + y: 0.0 + } + } +} +``` + +### Looping emitter with gravity modifier + +```protobuf +emitters { + id: "fire" + mode: PLAY_MODE_LOOP + space: EMISSION_SPACE_WORLD + tile_source: "/assets/particles/particles.atlas" + animation: "flame" + material: "/builtins/materials/particlefx.material" + blend_mode: BLEND_MODE_ADD + max_particle_count: 100 + type: EMITTER_TYPE_BOX + properties { + key: EMITTER_KEY_SPAWN_RATE + points { + y: 30.0 + } + } + properties { + key: EMITTER_KEY_SIZE_X + points { + y: 20.0 + } + } + properties { + key: EMITTER_KEY_SIZE_Y + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_SIZE_Z + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_LIFE_TIME + points { + y: 1.5 + } + } + properties { + key: EMITTER_KEY_PARTICLE_SPEED + points { + y: 50.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_SIZE + points { + y: 30.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_RED + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_GREEN + points { + y: 0.6 + } + } + properties { + key: EMITTER_KEY_PARTICLE_BLUE + points { + y: 0.1 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ALPHA + points { + y: 1.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ROTATION + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_STRETCH_FACTOR_X + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_STRETCH_FACTOR_Y + points { + y: 0.0 + } + } + properties { + key: EMITTER_KEY_PARTICLE_ANGULAR_VELOCITY + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_SCALE + points { + y: 1.0 + } + points { + x: 1.0 + y: 0.5 + t_x: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_RED + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_GREEN + points { + y: 1.0 + } + points { + x: 1.0 + y: 0.3 + t_x: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_BLUE + points { + y: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_ALPHA + points { + y: 1.0 + } + points { + x: 0.8 + y: 0.5 + t_x: 1.0 + } + points { + x: 1.0 + y: 0.0 + t_x: 1.0 + } + } + particle_properties { + key: PARTICLE_KEY_ROTATION + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_STRETCH_FACTOR_X + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_STRETCH_FACTOR_Y + points { + y: 0.0 + } + } + particle_properties { + key: PARTICLE_KEY_ANGULAR_VELOCITY + points { + y: 0.0 + } + } + modifiers { + type: MODIFIER_TYPE_ACCELERATION + properties { + key: MODIFIER_KEY_MAGNITUDE + points { + y: 50.0 + } + } + } +} +``` + +## Workflow + +### Creating a new particle effect + +1. Determine the file path (must end with `.particlefx`). +2. Add at least one `emitters` entry with all required fields: `mode`, `space`, `tile_source`, `animation`, `material`, `max_particle_count`, `type`. +3. Include all 15 `properties` entries (one per `EmitterKey`, excluding `EMITTER_KEY_COUNT`), in enum value order. +4. Include all 9 `particle_properties` entries (one per `ParticleKey`, excluding `PARTICLE_KEY_COUNT`), in enum value order. +5. Set optional emitter fields (`blend_mode`, `duration`, `start_delay`, `size_mode`, etc.) only if they differ from defaults. +6. Add modifiers as needed, either at the emitter level or effect level. +7. Follow proto field number order for all fields. + +### Editing an existing particle effect + +1. Read the current `.particlefx` file. +2. Modify only the requested fields or property values. +3. Preserve existing field values, order, and all `properties`/`particle_properties` entries. +4. Apply omission rules for optional scalar fields only — never omit `properties` or `particle_properties` entries. diff --git a/.agents/skills/defold-proto-file-editing/references/sound.md b/.agents/skills/defold-proto-file-editing/references/sound.md new file mode 100644 index 0000000..0797775 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/sound.md @@ -0,0 +1,169 @@ +# Editing Sounds + +Creates and edits Defold `.sound` component files using Protobuf Text Format. + +## Overview + +A Sound component plays back audio from a Wave (`.wav`), Ogg Vorbis (`.ogg`), or Ogg Opus (`.opus`) file. It supports looping, gain, panning, speed control, and sound group assignment for mixer-level volume management. + +## File format + +Sound files (`.sound`) use **Protobuf Text Format** based on the `SoundDesc` message from `gamesys/sound_ddf.proto`. + +### Canonical example + +```protobuf +sound: "/assets/Duck Quacks 1.wav" +group: "sfx" +gain: 0.5 +speed: 0.75 +``` + +### Embedded in a game object + +When embedded in a `.go` file, the sound data appears inside an `embedded_components` block. The `data` field contains the same Protobuf Text Format content as a standalone `.sound` file, but serialized as a quoted string: + +```protobuf +embedded_components { + id: "sound" + type: "sound" + data: "sound: \"/assets/Duck Quacks 1.opus\"\n" + "looping: 1\n" + "" +} +``` + +## Fields reference + +### sound (required) — `string` + +Absolute resource path to the audio file. Supported formats: Wave (`.wav`), Ogg Vorbis (`.ogg`), Ogg Opus (`.opus`). Defold supports 16-bit bit depth. + +```protobuf +sound: "/assets/effects/explosion.wav" +``` + +### looping (optional) — `int32` + +Whether the sound loops. `0` = no looping (default), `1` = looping. When enabled, the sound plays `loopcount` times (or indefinitely if `loopcount` is `0`). + +**Omission rule**: Omit if `0`. + +```protobuf +looping: 1 +``` + +### group (optional) — `string` + +Sound mixer group name. Default: `"master"`. All sounds in the same group can have their gain controlled together via `sound.set_group_gain()`. + +**Omission rule**: Omit if `"master"`. + +```protobuf +group: "sfx" +``` + +### gain (optional) — `float` + +Component gain in linear scale. Default: `1.0`. Range: `0.0` to `1.0`. The final output gain is the product of: component gain × play gain × group gain × master gain. + +**Omission rule**: Omit if `1.0`. + +```protobuf +gain: 0.5 +``` + +### pan (optional) — `float` + +Stereo panning. Default: `0.0`. Range: `-1.0` (45° left) to `1.0` (45° right). At `0.0`, channels are balanced at 71%/71% (constant power panning). + +**Omission rule**: Omit if `0.0`. + +```protobuf +pan: -0.5 +``` + +### speed (optional) — `float` + +Playback speed multiplier. Default: `1.0`. `0.5` = half speed, `2.0` = double speed. + +**Omission rule**: Omit if `1.0`. + +```protobuf +speed: 1.5 +``` + +### loopcount (optional) — `int32` + +Number of times a looping sound plays before stopping. Default: `0` (loop indefinitely). Only meaningful when `looping` is `1`. + +**Omission rule**: Omit if `0`. + +```protobuf +loopcount: 3 +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. This keeps files minimal and matches Defold editor behavior. +2. **Floats**: Always include decimal point: `1.0`, not `1`. +3. **Integers**: No decimal point: `1`, not `1.0`. +4. **Strings**: Always double-quoted: `"text"`. +5. **Field order**: Follow the proto field number order: `sound`, `looping`, `group`, `gain`, `pan`, `speed`, `loopcount`. +6. **No trailing commas or semicolons**. +7. **No field number tags** — use field names only. +8. **No empty lines between fields** (all fields are scalar). + +## Common templates + +### Simple one-shot sound effect + +```protobuf +sound: "/assets/sfx/click.wav" +group: "sfx" +``` + +### Looping background music + +```protobuf +sound: "/assets/music/theme.ogg" +looping: 1 +group: "music" +gain: 0.8 +``` + +### Looping ambient sound with limited repeats + +```protobuf +sound: "/assets/ambient/rain.ogg" +looping: 1 +group: "ambient" +gain: 0.6 +loopcount: 5 +``` + +### Panned sound effect + +```protobuf +sound: "/assets/sfx/engine_left.wav" +group: "sfx" +pan: -0.75 +``` + +## Workflow + +### Creating a new sound + +1. Determine the file path (must end with `.sound`). +2. Set the required `sound` field to the audio file resource path. +3. Set `group` if not using the default `"master"` group. +4. Set `looping: 1` if the sound should loop. +5. Add optional fields (`gain`, `pan`, `speed`, `loopcount`) only if they differ from defaults. +6. Write the file using the field order from the reference above. + +### Editing an existing sound + +1. Read the current `.sound` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/sprite.md b/.agents/skills/defold-proto-file-editing/references/sprite.md new file mode 100644 index 0000000..875c9a6 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/sprite.md @@ -0,0 +1,279 @@ +# Editing Sprites + +Creates and edits Defold `.sprite` component files using Protobuf Text Format. + +## Overview + +A Sprite component displays a simple image or flipbook animation on screen. It uses an Atlas or Tile Source for its graphics and supports slice-9 texturing, multiple texture samplers, blend modes, and custom material attributes. + +## File format + +Sprite files (`.sprite`) use **Protobuf Text Format** based on the `SpriteDesc` message from `gamesys/sprite_ddf.proto`. + +### Canonical example + +```protobuf +default_animation: "logo" +material: "/builtins/materials/sprite.material" +size { + x: 100.0 + y: 100.0 +} +size_mode: SIZE_MODE_MANUAL +textures { + sampler: "texture_sampler" + texture: "/main/example.atlas" +} +``` + +## Fields reference + +### tile_set (deprecated) — `string` + +Legacy field for single-texture sprites. **Do not use**. Use the `textures` repeated field instead. + +**Omission rule**: Always omit. + +### default_animation (required) — `string` + +The animation (or image) id to display. The animation data is taken from the first atlas or tilesource. + +```protobuf +default_animation: "idle" +``` + +### material (optional) — `string` + +Absolute resource path to a `.material` file. Default: `"/builtins/materials/sprite.material"`. + +**Omission rule**: Omit if `"/builtins/materials/sprite.material"`. + +```protobuf +material: "/builtins/materials/sprite.material" +``` + +### blend_mode (optional) — enum `BlendMode` + +Blending mode for rendering. Default: `BLEND_MODE_ALPHA`. + +| Value | Description | +|-------|-------------| +| `BLEND_MODE_ALPHA` | Normal alpha blending | +| `BLEND_MODE_ADD` | Additive blending (brightens) | +| `BLEND_MODE_ADD_ALPHA` | Add Alpha (Deprecated) | +| `BLEND_MODE_MULT` | Multiply (darkens) | +| `BLEND_MODE_SCREEN` | Screen (inverse multiply) | + +**Omission rule**: Omit if `BLEND_MODE_ALPHA`. + +```protobuf +blend_mode: BLEND_MODE_ADD +``` + +### slice9 (optional) — `dmMath.Vector4` + +Slice-9 margins in pixels to preserve edges when the sprite is resized. Components default to `0.0`. Values are set clockwise: left (`x`), top (`y`), right (`z`), bottom (`w`). + +- `x` — left margin (default: `0.0`) +- `y` — top margin (default: `0.0`) +- `z` — right margin (default: `0.0`) +- `w` — bottom margin (default: `0.0`) + +When using slice-9, the Sprite Trim Mode in the atlas must be set to Off. + +**Omission rule**: Omit entire block if all components are `0.0`. + +```protobuf +slice9 { + x: 16.0 + y: 16.0 + z: 16.0 + w: 16.0 +} +``` + +### size (optional) — `dmMath.Vector4` + +Sprite size in pixels. **Only has effect when `size_mode` is `SIZE_MODE_MANUAL`**. When `size_mode` is `SIZE_MODE_AUTO` (the default), the engine determines the size automatically from the image — do NOT set `size` and do NOT look up the image dimensions. Components default to `0.0`. + +- `x` — width (default: `0.0`) +- `y` — height (default: `0.0`) +- `z` — depth (default: `0.0`, rarely used) +- `w` — (default: `0.0`, rarely used) + +**Omission rule**: Omit entire block if `size_mode` is `SIZE_MODE_AUTO` (default) or if all components are `0.0`. + +```protobuf +size { + x: 128.0 + y: 64.0 +} +``` + +### size_mode (optional) — enum `SizeMode` + +Controls whether the sprite size is set automatically from the image or manually. Default: `SIZE_MODE_AUTO`. + +| Value | Description | +|-------|-------------| +| `SIZE_MODE_MANUAL` | Size is set manually via the `size` field | +| `SIZE_MODE_AUTO` | Size is determined automatically from the image | + +**Omission rule**: Omit if `SIZE_MODE_AUTO`. + +```protobuf +size_mode: SIZE_MODE_MANUAL +``` + +### offset (optional) — `float` + +The normalized initial value of the animation cursor when the animation starts playing. Default: `0.0`. Range: `0.0` to `1.0`. + +**Omission rule**: Omit if `0.0`. + +```protobuf +offset: 0.5 +``` + +### playback_rate (optional) — `float` + +The rate at which the animation plays. Must be positive. Default: `1.0`. + +**Omission rule**: Omit if `1.0`. + +```protobuf +playback_rate: 2.0 +``` + +### attributes (optional, repeated) — `dmGraphics.VertexAttribute` + +Custom vertex attributes that override values from the material. Each entry specifies an attribute name and its values. + +```protobuf +attributes { + name: "tint" + double_values { + v: 1.0 + v: 0.0 + v: 0.0 + v: 1.0 + } +} +``` + +### textures (repeated) — `SpriteTexture` + +Texture bindings. Each entry maps a material sampler name to an atlas or tilesource resource. For the default sprite material, the sampler name is `"texture_sampler"`. + +When using multiple textures, the animation frame ids must match across all textures. Use the atlas `Rename patterns` to ensure matching. + +```protobuf +textures { + sampler: "texture_sampler" + texture: "/main/my_atlas.atlas" +} +``` + +#### SpriteTexture fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `sampler` | `string` | yes | The sampler name from the material | +| `texture` | `string` | yes | Absolute resource path to `.atlas` or `.tilesource` | + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. This keeps files minimal and matches Defold editor behavior. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. Use standard float formatting. +4. **Strings**: Always double-quoted: `"text"`. +5. **Enums**: Use the enum constant name without quotes: `SIZE_MODE_MANUAL`. +6. **Booleans**: `true` or `false`, no quotes. +7. **Field order**: Follow the proto field number order: `default_animation`, `material`, `blend_mode`, `slice9`, `size`, `size_mode`, `offset`, `playback_rate`, `attributes`, `textures`. +8. **No trailing commas or semicolons**. +9. **No field number tags** — use field names only. +10. **Newlines**: One empty line between the end of a message block `}` and the next field. No empty line between consecutive scalar fields. +11. **Indentation**: 2 spaces inside message blocks. +12. **Repeated messages**: Each entry gets its own `field_name { ... }` block. + +## Common templates + +### Simple sprite (auto-sized) + +```protobuf +default_animation: "idle" +textures { + sampler: "texture_sampler" + texture: "/assets/sprites.atlas" +} +``` + +### Manual-sized sprite + +```protobuf +default_animation: "background" +size { + x: 320.0 + y: 240.0 +} +size_mode: SIZE_MODE_MANUAL +textures { + sampler: "texture_sampler" + texture: "/assets/backgrounds.atlas" +} +``` + +### Slice-9 sprite (for UI panels) + +```protobuf +default_animation: "panel" +slice9 { + x: 12.0 + y: 12.0 + z: 12.0 + w: 12.0 +} +size { + x: 200.0 + y: 100.0 +} +size_mode: SIZE_MODE_MANUAL +textures { + sampler: "texture_sampler" + texture: "/assets/ui.atlas" +} +``` + +### Multi-texture sprite + +```protobuf +default_animation: "hero_idle" +material: "/assets/materials/multi_tex.material" +textures { + sampler: "diffuse" + texture: "/assets/hero_diffuse.atlas" +} +textures { + sampler: "normal" + texture: "/assets/hero_normal.atlas" +} +``` + +## Workflow + +### Creating a new sprite + +1. Determine the file path (must end with `.sprite`). +2. Set the required `default_animation` field. +3. Add at least one `textures` entry with the sampler name and atlas/tilesource path. +4. Set `material` only if not using the default sprite material. +5. If manual sizing is needed, set `size_mode: SIZE_MODE_MANUAL` and provide `size`. Otherwise, do NOT set `size` and do NOT look up image dimensions — the engine handles sizing automatically. +6. Add optional fields only if they differ from defaults. +7. Write the file using the field order from the reference above. + +### Editing an existing sprite + +1. Read the current `.sprite` file. +2. Modify only the requested fields. +3. Preserve existing field values and order. +4. Apply omission rules: remove fields that become equal to their defaults after editing. diff --git a/.agents/skills/defold-proto-file-editing/references/tilemap.md b/.agents/skills/defold-proto-file-editing/references/tilemap.md new file mode 100644 index 0000000..597543a --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/tilemap.md @@ -0,0 +1,410 @@ + +# Editing Tilemaps + +Creates and edits Defold `.tilemap` component files using Protobuf Text Format. + +## Overview + +A Tilemap is a component that allows you to paint tiles from a Tile Source onto a large grid area. Tilemaps are commonly used to build game level environments. They support multiple layers, collision shapes from the tile source, and runtime manipulation via script. + +## File format + +Tilemap files (`.tilemap`) use **Protobuf Text Format** based on the `TileGrid` message from `tile_ddf.proto`. + +### Canonical example + +```protobuf +tile_set: "/assets/tiles/tileset.tilesource" +layers { + id: "ground" + z: 0.0 + is_visible: 1 + cell { + x: 0 + y: 0 + tile: 1 + h_flip: 0 + v_flip: 0 + } + cell { + x: 1 + y: 0 + tile: 2 + h_flip: 0 + v_flip: 0 + } +} +material: "/builtins/materials/tile_map.material" +blend_mode: BLEND_MODE_ALPHA +``` + +## Top-level fields reference + +### tile_set (required) — `string` + +Absolute resource path to the tile source (`.tilesource`) used by this tilemap. + +```protobuf +tile_set: "/assets/tiles/tileset.tilesource" +``` + +### layers (repeated) — `TileLayer` + +Tilemap layers. Each layer is a separate `layers { ... }` block containing cells. Layers are rendered in order, with later layers on top. See **TileLayer fields** below. + +```protobuf +layers { + id: "background" + z: 0.0 + is_visible: 1 + cell { + x: 0 + y: 0 + tile: 1 + h_flip: 0 + v_flip: 0 + } +} +``` + +### material (optional) — `string` + +Absolute resource path to the material used for rendering. Default: `"/builtins/materials/tile_map.material"`. + +**Omission rule**: Omit if using default material. + +```protobuf +material: "/builtins/materials/tile_map.material" +``` + +### blend_mode (optional) — enum `BlendMode` + +How the tilemap graphics blend with background. Default: `BLEND_MODE_ALPHA`. + +| Value | Description | Formula | +|-------|-------------|---------| +| `BLEND_MODE_ALPHA` | Normal blending (default) | `src.a * src.rgb + (1 - src.a) * dst.rgb` | +| `BLEND_MODE_ADD` | Additive blending | `src.rgb + dst.rgb` | +| `BLEND_MODE_ADD_ALPHA` | Add Alpha (Deprecated) | — | +| `BLEND_MODE_MULT` | Multiply | `src.rgb * dst.rgb` | +| `BLEND_MODE_SCREEN` | Screen | `src.rgb - dst.rgb * dst.rgb` | + +**Omission rule**: Omit if `BLEND_MODE_ALPHA`. + +```protobuf +blend_mode: BLEND_MODE_ADD +``` + +## TileLayer fields + +### id (required) — `string` + +Layer identifier. Used to reference the layer in scripts (e.g., `tilemap.get_tile()`, `tilemap.set_tile()`). Default: `"layer1"`. + +```protobuf +id: "ground" +``` + +### z (required) — `float` + +Z-order offset for this layer. Layers with higher z values render on top. Default: `0.0`. + +```protobuf +z: 0.0 +``` + +### is_visible (optional) — `uint32` + +Layer visibility. `1` = visible, `0` = hidden. Default: `1`. + +**Omission rule**: Omit if `1`. + +```protobuf +is_visible: 1 +``` + +### cell (repeated) — `TileCell` + +Individual tile placements on this layer. Each cell is a `cell { ... }` block. See **TileCell fields** below. + +## TileCell fields + +### x (required) — `int32` + +Horizontal grid position of the cell. Can be negative. Default: `0`. + +```protobuf +x: 5 +``` + +### y (required) — `int32` + +Vertical grid position of the cell. Can be negative. Y increases upward. Default: `0`. + +```protobuf +y: 3 +``` + +### tile (required) — `uint32` + +Tile index from the tile source. Tile `0` is typically empty/transparent. Tiles are numbered starting from 0 in the top-left of the tilesource, proceeding left-to-right, row-by-row. Default: `0`. + +```protobuf +tile: 42 +``` + +### h_flip (optional) — `uint32` + +Horizontal flip. `0` = normal, `1` = flipped. Default: `0`. + +**Omission rule**: Omit if `0`. + +```protobuf +h_flip: 1 +``` + +### v_flip (optional) — `uint32` + +Vertical flip. `0` = normal, `1` = flipped. Default: `0`. + +**Omission rule**: Omit if `0`. + +```protobuf +v_flip: 1 +``` + +### rotate90 (optional) — `uint32` + +90-degree clockwise rotation. `0` = no rotation, `1` = rotated. Default: `0`. + +**Omission rule**: Omit if `0`. + +```protobuf +rotate90: 1 +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Integers**: No decimal point: `4`, not `4.0`. +5. **Strings**: Always double-quoted. +6. **Enums**: Use the constant name without quotes. +7. **Repeated messages**: Each entry gets its own `field_name { ... }` block. +8. **Field order**: Follow the proto field number order. +9. **No trailing commas or semicolons**. +10. **Indentation**: 2 spaces per nesting level inside message blocks. + +## Complete examples + +### Simple single-layer tilemap + +```protobuf +tile_set: "/assets/tiles/tileset.tilesource" +layers { + id: "layer1" + z: 0.0 + is_visible: 1 + cell { + x: 0 + y: 0 + tile: 5 + h_flip: 0 + v_flip: 0 + } + cell { + x: 1 + y: 0 + tile: 6 + h_flip: 0 + v_flip: 0 + } + cell { + x: 2 + y: 0 + tile: 7 + h_flip: 0 + v_flip: 0 + } +} +material: "/builtins/materials/tile_map.material" +blend_mode: BLEND_MODE_ALPHA +``` + +### Multi-layer tilemap + +```protobuf +tile_set: "/assets/tiles/platformer.tilesource" +layers { + id: "background" + z: 0.0 + is_visible: 1 + cell { + x: 0 + y: 0 + tile: 10 + h_flip: 0 + v_flip: 0 + } + cell { + x: 1 + y: 0 + tile: 10 + h_flip: 0 + v_flip: 0 + } +} +layers { + id: "ground" + z: 0.1 + is_visible: 1 + cell { + x: 0 + y: 0 + tile: 1 + h_flip: 0 + v_flip: 0 + } + cell { + x: 1 + y: 0 + tile: 2 + h_flip: 0 + v_flip: 0 + } + cell { + x: 2 + y: 0 + tile: 3 + h_flip: 0 + v_flip: 0 + } +} +layers { + id: "decorations" + z: 0.2 + is_visible: 1 + cell { + x: 1 + y: 1 + tile: 44 + h_flip: 0 + v_flip: 0 + } +} +material: "/builtins/materials/tile_map.material" +blend_mode: BLEND_MODE_ALPHA +``` + +### Tilemap with flipped tiles + +```protobuf +tile_set: "/assets/tiles/tileset.tilesource" +layers { + id: "layer1" + z: 0.0 + is_visible: 1 + cell { + x: 0 + y: 0 + tile: 5 + h_flip: 0 + v_flip: 0 + } + cell { + x: 1 + y: 0 + tile: 5 + h_flip: 1 + v_flip: 0 + } + cell { + x: 0 + y: 1 + tile: 5 + h_flip: 0 + v_flip: 1 + } + cell { + x: 1 + y: 1 + tile: 5 + h_flip: 1 + v_flip: 1 + } +} +material: "/builtins/materials/tile_map.material" +blend_mode: BLEND_MODE_ALPHA +``` + +## Runtime manipulation + +Tilemaps can be manipulated at runtime using the `tilemap` module: + +```lua +-- Get a tile +local tile = tilemap.get_tile("/level#map", "ground", x, y) + +-- Set a tile +tilemap.set_tile("/level#map", "ground", x, y, 4) + +-- Set tile with transformations +tilemap.set_tile("/level#map", "ground", x, y, 4, true, false) -- h_flip, v_flip +``` + +### Runtime properties + +These properties can be changed with `go.get()` and `go.set()`: + +- `tile_source` — The tile source resource (`hash`) +- `material` — The material resource (`hash`) + +### Material constants + +The default tilemap material supports: + +- `tint` — Color tint (`vector4`, RGBA) + +```lua +go.set("#tilemap", "tint", vmath.vector4(1, 0, 0, 1)) +go.animate("#tilemap", "tint", go.PLAYBACK_LOOP_PINGPONG, vmath.vector4(1, 0, 0, 1), go.EASING_LINEAR, 2) +``` + +## Workflow + +### Creating a new tilemap + +1. Determine the file path (must end with `.tilemap`). +2. Set `tile_set` to the tile source path. +3. Add at least one `layers` block with an `id`. +4. Add `cell` entries for each tile placement. +5. Set `material` if using a custom material (otherwise use default). +6. Set `blend_mode` if not using alpha blending. +7. Omit fields at their default values. + +### Editing an existing tilemap + +1. Read the current `.tilemap` file. +2. Modify only the requested changes (add/remove cells, change layers). +3. Preserve existing field values and order. +4. Apply omission rules for fields that become equal to their defaults. + +### Tile coordinate system + +- Origin (0, 0) is at the bottom-left of the tilemap grid. +- X increases to the right. +- Y increases upward. +- Coordinates can be negative for tiles placed to the left or below origin. + +### Tile numbering (in tilesource) + +Tiles in the tilesource are numbered starting from 0: +- Tile `0` is typically empty/transparent +- Numbering proceeds left-to-right, top-to-bottom in the source image + +``` +| 0 | 1 | 2 | 3 | +| 4 | 5 | 6 | 7 | +| 8 | 9 | 10 | 11 | +``` diff --git a/.agents/skills/defold-proto-file-editing/references/tilesource.md b/.agents/skills/defold-proto-file-editing/references/tilesource.md new file mode 100644 index 0000000..03b3854 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/references/tilesource.md @@ -0,0 +1,400 @@ + +# Editing Tile Sources + +Creates and edits Defold `.tilesource` resource files using Protobuf Text Format. + +## Overview + +A Tile Source defines a grid of uniformly-sized tiles from a single image. Tile sources are used by Tilemap components to paint tiles onto a grid, or as image sources for Sprite and ParticleFX components. They support flipbook animations and collision shape definitions for physics. + +## File format + +Tile source files (`.tilesource`) use **Protobuf Text Format** based on the `TileSet` message from `tile_ddf.proto`. + +### Canonical example + +```protobuf +image: "/assets/images/tileset.png" +tile_width: 32 +tile_height: 32 +animations { + id: "walk" + start_tile: 1 + end_tile: 4 + playback: PLAYBACK_LOOP_FORWARD + fps: 12 +} +extrude_borders: 2 +``` + +## Top-level fields reference + +### image (required) — `string` + +Absolute resource path to the source image containing the tile grid. The image must contain tiles arranged in a uniform grid. + +```protobuf +image: "/assets/images/tileset.png" +``` + +### tile_width (required) — `uint32` + +Width of each tile in pixels. Default: `0`. + +```protobuf +tile_width: 32 +``` + +### tile_height (required) — `uint32` + +Height of each tile in pixels. Default: `0`. + +```protobuf +tile_height: 32 +``` + +### tile_margin (optional) — `uint32` + +Number of pixels surrounding each tile in the source image (margin around the tile). Default: `0`. + +**Omission rule**: Omit if `0`. + +```protobuf +tile_margin: 1 +``` + +### tile_spacing (optional) — `uint32` + +Number of pixels between each tile in the source image. Default: `0`. + +**Omission rule**: Omit if `0`. + +```protobuf +tile_spacing: 2 +``` + +### collision (optional) — `string` + +Absolute resource path to an image used to automatically generate collision shapes for tiles. Often the same image as the main tile image. When specified, the engine generates convex hull collision shapes from non-transparent pixels. + +**Omission rule**: Omit if empty/unused. + +```protobuf +collision: "/assets/images/tileset.png" +``` + +### material_tag (optional) — `string` + +Tag used in render scripts to identify this tile source's material. Default: `"tile"`. + +**Omission rule**: Omit if `"tile"`. + +```protobuf +material_tag: "tile" +``` + +### convex_hulls (repeated) — `ConvexHull` + +Collision shapes for tiles. Each entry defines a convex hull for one tile. See **ConvexHull fields** below. These are typically auto-generated from the collision image. + +```protobuf +convex_hulls { + index: 0 + count: 4 + collision_group: "ground" +} +``` + +### collision_groups (repeated) — `string` + +List of collision group names used by tiles. Each name appears once in this list and is referenced by `ConvexHull.collision_group`. + +```protobuf +collision_groups: "ground" +collision_groups: "danger" +``` + +### animations (repeated) — `Animation` + +Flipbook animations defined from consecutive tiles. See **Animation fields** below. + +```protobuf +animations { + id: "run" + start_tile: 1 + end_tile: 8 + playback: PLAYBACK_LOOP_FORWARD + fps: 12 +} +``` + +### extrude_borders (optional) — `uint32` + +Number of times edge pixels are replicated around each tile in the compiled texture. Prevents texture bleeding at tile edges. Default: `0`. + +**Omission rule**: Omit if `0`. + +```protobuf +extrude_borders: 2 +``` + +### inner_padding (optional) — `uint32` + +Empty pixels added around each tile in the compiled texture. Default: `0`. + +**Omission rule**: Omit if `0`. + +```protobuf +inner_padding: 1 +``` + +### sprite_trim_mode (optional) — enum `SpriteTrimmingMode` + +How sprite geometry is generated when used with Sprite components. Trimming transparent pixels can reduce overdraw. Default: `SPRITE_TRIM_MODE_OFF`. + +| Value | Description | +|-------|-------------| +| `SPRITE_TRIM_MODE_OFF` | Rectangular quad (default) | +| `SPRITE_TRIM_MODE_4` | 4 vertices | +| `SPRITE_TRIM_MODE_5` | 5 vertices | +| `SPRITE_TRIM_MODE_6` | 6 vertices | +| `SPRITE_TRIM_MODE_7` | 7 vertices | +| `SPRITE_TRIM_MODE_8` | 8 vertices | +| `SPRITE_TRIM_POLYGONS` | Polygon-based trimming | + +**Omission rule**: Omit if `SPRITE_TRIM_MODE_OFF`. + +```protobuf +sprite_trim_mode: SPRITE_TRIM_MODE_6 +``` + +## Animation fields + +### id (required) — `string` + +Animation name. Must be unique within the tile source. Used to reference the animation in code via `sprite.play_flipbook()`. + +```protobuf +id: "walk" +``` + +### start_tile (required) — `uint32` + +First tile of the animation. Numbering starts at 1 in the top-left corner and proceeds left-to-right, row-by-row. + +```protobuf +start_tile: 1 +``` + +### end_tile (required) — `uint32` + +Last tile of the animation (inclusive). + +```protobuf +end_tile: 8 +``` + +### playback (optional) — enum `Playback` + +Animation playback mode. Default: `PLAYBACK_ONCE_FORWARD`. + +| Value | Description | +|-------|-------------| +| `PLAYBACK_NONE` | No playback, shows first frame | +| `PLAYBACK_ONCE_FORWARD` | Play once, first to last | +| `PLAYBACK_ONCE_BACKWARD` | Play once, last to first | +| `PLAYBACK_ONCE_PINGPONG` | Play once forward then backward | +| `PLAYBACK_LOOP_FORWARD` | Loop, first to last | +| `PLAYBACK_LOOP_BACKWARD` | Loop, last to first | +| `PLAYBACK_LOOP_PINGPONG` | Loop forward then backward | + +**Omission rule**: Omit if `PLAYBACK_ONCE_FORWARD`. + +```protobuf +playback: PLAYBACK_LOOP_FORWARD +``` + +### fps (optional) — `uint32` + +Playback speed in frames per second. Default: `30`. + +**Omission rule**: Omit if `30`. + +```protobuf +fps: 12 +``` + +### flip_horizontal (optional) — `uint32` + +Flip animation horizontally. `0` = no flip, `1` = flip. Default: `0`. + +**Omission rule**: Omit if `0`. + +### flip_vertical (optional) — `uint32` + +Flip animation vertically. `0` = no flip, `1` = flip. Default: `0`. + +**Omission rule**: Omit if `0`. + +### cues (repeated) — `Cue` + +Animation cues/events triggered at specific frames. See **Cue fields** below. + +## Cue fields + +### id (required) — `string` + +Cue identifier. + +### frame (required) — `uint32` + +Frame number when the cue triggers (0-indexed within animation). + +### value (optional) — `float` + +Optional value associated with the cue. Default: `0.0`. + +**Omission rule**: Omit if `0.0`. + +## ConvexHull fields + +### index (required) — `uint32` + +Index into the convex hull points array. Default: `0`. + +### count (required) — `uint32` + +Number of points in this convex hull. Default: `0`. + +### collision_group (required) — `string` + +Collision group name for this tile's shape. Must match an entry in `collision_groups`. Default: `"tile"`. Use empty string `""` for tiles without collision. + +```protobuf +convex_hulls { + index: 0 + count: 4 + collision_group: "ground" +} +``` + +## Protobuf Text Format rules + +1. **Default omission**: Omit fields that equal their proto default. +2. **Message blocks**: Use `field_name { ... }` with nested `key: value` pairs. +3. **Floats**: Always include decimal point: `1.0`, not `1`. +4. **Integers**: No decimal point: `4`, not `4.0`. +5. **Strings**: Always double-quoted. +6. **Enums**: Use the constant name without quotes. +7. **Repeated messages**: Each entry gets its own `field_name { ... }` block. +8. **Repeated scalars**: Each value gets its own line with the field name. +9. **Field order**: Follow the proto field number order. +10. **No trailing commas or semicolons**. +11. **Indentation**: 2 spaces per nesting level inside message blocks. + +## Complete examples + +### Simple tile source for sprites + +```protobuf +image: "/assets/images/character_sheet.png" +tile_width: 96 +tile_height: 128 +animations { + id: "run" + start_tile: 37 + end_tile: 44 + playback: PLAYBACK_LOOP_FORWARD + fps: 10 +} +extrude_borders: 2 +``` + +### Tile source for tilemap with collisions + +```protobuf +image: "/assets/images/platformer_tiles.png" +tile_width: 64 +tile_height: 64 +collision: "/assets/images/platformer_tiles.png" +convex_hulls { + index: 0 + count: 4 + collision_group: "ground" +} +convex_hulls { + index: 4 + count: 4 + collision_group: "ground" +} +convex_hulls { + index: 8 + count: 6 + collision_group: "danger" +} +collision_groups: "ground" +collision_groups: "danger" +extrude_borders: 1 +``` + +### Tile source with multiple animations + +```protobuf +image: "/assets/images/character.png" +tile_width: 32 +tile_height: 32 +animations { + id: "idle" + start_tile: 1 + end_tile: 4 + playback: PLAYBACK_LOOP_FORWARD + fps: 8 +} +animations { + id: "walk" + start_tile: 5 + end_tile: 12 + playback: PLAYBACK_LOOP_FORWARD + fps: 12 +} +animations { + id: "jump" + start_tile: 13 + end_tile: 16 + fps: 15 +} +extrude_borders: 2 +``` + +## Workflow + +### Creating a new tile source + +1. Determine the file path (must end with `.tilesource`). +2. Set `image` to the source image path. +3. Set `tile_width` and `tile_height` to match the tile grid. +4. Set `tile_margin` and `tile_spacing` if the source image has gaps between tiles. +5. Add `animations` blocks for flipbook animations if needed. +6. If using for tilemaps with physics: + - Set `collision` to the collision mask image. + - Add `collision_groups` for each collision group used. + - Convex hulls are typically auto-generated by the editor. +7. Set `extrude_borders` (recommended: 1-2) to prevent edge bleeding. +8. Omit fields at their default values. + +### Editing an existing tile source + +1. Read the current `.tilesource` file. +2. Modify only the requested fields (add/remove animations, change properties). +3. Preserve existing field values and order. +4. Apply omission rules for fields that become equal to their defaults. + +### Tile numbering + +Tiles are numbered starting from 1 in the top-left corner, proceeding left-to-right, then top-to-bottom: + +``` +| 1 | 2 | 3 | 4 | +| 5 | 6 | 7 | 8 | +| 9 | 10 | 11 | 12 | +``` diff --git a/.agents/skills/defold-proto-file-editing/scripts/gen_convexshape.py b/.agents/skills/defold-proto-file-editing/scripts/gen_convexshape.py new file mode 100644 index 0000000..5036ffa --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/scripts/gen_convexshape.py @@ -0,0 +1,243 @@ +# SPDX-License-Identifier: CC0-1.0 + +"""Generate a Defold .convexshape file from a 2D image's non-transparent silhouette. + +Uses PIL/Pillow (bundled with most Python installations) to read images. +Computes a convex hull via Graham scan, simplifies to ≤16 points +using Visvalingam-Whyatt area-based simplification, centers points +at image origin, and outputs Defold-compatible .convexshape format. + +Usage: + python gen_convexshape.py [--output ] [--max-points N] [--alpha-threshold T] + +Arguments: + image_path Path to a PNG or JPEG image file + --output, -o Output .convexshape file path (default: prints to stdout) + --max-points, -m Maximum number of hull points (default: 16, Box2D limit in Defold) + --alpha-threshold Alpha value threshold for "non-transparent" (0-255, default: 1) + --force-png-py Force using bundled png.py instead of PIL (PNG only) + +Environment: + FORCE_PNG_PY=1 Same as --force-png-py + +Output: + Protobuf Text Format .convexshape with TYPE_HULL shape, points centered at image origin. + +Exit code 0 on success, 1 on error. +""" + +import argparse +import math +import os +import sys +from typing import TextIO + +# image_loader is in the same directory; adjust sys.path so it's importable +# both when invoked directly and from the editor script. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from image_loader import load_alpha_mask + + +def extract_boundary_pixels(coords: list[tuple[int, int]], width: int, height: int) -> list[tuple[int, int]]: + """Extract only boundary pixels from the silhouette to reduce point count before hull.""" + pixel_set = set(coords) + boundary = [] + for x, y in coords: + is_boundary = False + for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + nx, ny = x + dx, y + dy + if nx < 0 or nx >= width or ny < 0 or ny >= height or (nx, ny) not in pixel_set: + is_boundary = True + break + if is_boundary: + boundary.append((x, y)) + return boundary + + +def cross(o: tuple[float, float], a: tuple[float, float], b: tuple[float, float]) -> float: + """2D cross product of vectors OA and OB.""" + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) + + +def graham_scan(points: list[tuple[float, float]]) -> list[tuple[float, float]]: + """Compute convex hull using Andrew's monotone chain (a Graham scan variant). + + Returns points in counter-clockwise order. + """ + pts = sorted(set(points)) + if len(pts) <= 2: + return pts + + # Build lower hull + lower: list[tuple[float, float]] = [] + for p in pts: + while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0: + lower.pop() + lower.append(p) + + # Build upper hull + upper: list[tuple[float, float]] = [] + for p in reversed(pts): + while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0: + upper.pop() + upper.append(p) + + # Remove last point of each half because it's repeated + return lower[:-1] + upper[:-1] + + +def triangle_area(a: tuple[float, float], b: tuple[float, float], c: tuple[float, float]) -> float: + """Area of triangle formed by three points.""" + return abs(cross(a, b, c)) / 2.0 + + +def simplify_hull(hull: list[tuple[float, float]], max_points: int) -> list[tuple[float, float]]: + """Reduce hull to max_points using Visvalingam-Whyatt area-based simplification. + + Iteratively removes the vertex that contributes the least area + to the polygon until we have at most max_points vertices. + Preserves counter-clockwise winding order. + """ + if len(hull) <= max_points: + return hull + + pts = list(hull) + + while len(pts) > max_points: + n = len(pts) + min_area = float("inf") + min_idx = -1 + + for i in range(n): + prev_pt = pts[(i - 1) % n] + curr_pt = pts[i] + next_pt = pts[(i + 1) % n] + area = triangle_area(prev_pt, curr_pt, next_pt) + if area < min_area: + min_area = area + min_idx = i + + pts.pop(min_idx) + + return pts + + +def ensure_ccw(hull: list[tuple[float, float]]) -> list[tuple[float, float]]: + """Ensure points are in counter-clockwise order (positive signed area).""" + signed_area = 0.0 + n = len(hull) + for i in range(n): + x1, y1 = hull[i] + x2, y2 = hull[(i + 1) % n] + signed_area += (x2 - x1) * (y2 + y1) + if signed_area > 0: + hull.reverse() + return hull + + +def write_convexshape(hull: list[tuple[float, float]], out: TextIO) -> None: + """Write hull points as Defold .convexshape format (TYPE_HULL with z=0).""" + out.write("shape_type: TYPE_HULL\n") + for x, y in hull: + out.write(f"data: {x:.1f}\n") + out.write(f"data: {y:.1f}\n") + out.write("data: 0.0\n") + + +def format_float(v: float) -> str: + """Format float for Defold: always has decimal point, no unnecessary trailing zeros.""" + if v == int(v): + return f"{int(v)}.0" + # Use enough precision but strip trailing zeros + s = f"{v:.6f}".rstrip("0") + if s.endswith("."): + s += "0" + return s + + +def write_convexshape_formatted(hull: list[tuple[float, float]], out: TextIO) -> None: + """Write hull points as Defold .convexshape format with proper float formatting.""" + out.write("shape_type: TYPE_HULL\n") + for x, y in hull: + out.write(f"data: {format_float(x)}\n") + out.write(f"data: {format_float(y)}\n") + out.write("data: 0.0\n") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Generate a Defold .convexshape file from an image's non-transparent silhouette." + ) + parser.add_argument("image_path", help="Path to a PNG or JPEG image file") + parser.add_argument("--output", "-o", help="Output .convexshape file path (default: stdout)") + parser.add_argument("--max-points", "-m", type=int, default=16, + help="Maximum number of hull points (default: 16)") + parser.add_argument("--alpha-threshold", "-a", type=int, default=1, + help="Alpha threshold for non-transparent pixels (0-255, default: 1)") + parser.add_argument("--inset", "-i", type=float, default=0.0, + help="Inset percentage to shrink the shape (0-100, default: 0)") + parser.add_argument("--force-png-py", action="store_true", + help="Force using bundled png.py instead of PIL (PNG only)") + args = parser.parse_args() + + # Load image and get non-transparent pixels + try: + coords, width, height = load_alpha_mask( + args.image_path, args.alpha_threshold, + force_png_py=args.force_png_py, + ) + except Exception as e: + print(f"ERROR: Failed to read image: {e}", file=sys.stderr) + return 1 + + if len(coords) < 3: + print("ERROR: Not enough non-transparent pixels to form a convex hull (need at least 3)", file=sys.stderr) + return 1 + + print(f"Image: {args.image_path} ({width}x{height})", file=sys.stderr) + print(f"Non-transparent pixels: {len(coords)}", file=sys.stderr) + + # Extract boundary pixels to speed up hull computation + boundary = extract_boundary_pixels(coords, width, height) + print(f"Boundary pixels: {len(boundary)}", file=sys.stderr) + + # Compute convex hull (in pixel coordinates, Y grows down) + hull = graham_scan([(float(x), float(y)) for x, y in boundary]) + print(f"Convex hull vertices: {len(hull)}", file=sys.stderr) + + # Simplify to max points + if len(hull) > args.max_points: + hull = simplify_hull(hull, args.max_points) + print(f"Simplified to: {len(hull)} vertices", file=sys.stderr) + + # Center at image origin and flip Y axis (pixel Y grows down, Defold Y grows up) + cx = width / 2.0 + cy = height / 2.0 + centered_hull = [(x - cx, cy - y) for x, y in hull] + + # Apply inset: shrink each point toward centroid by percentage + if args.inset > 0.0: + centroid_x = sum(x for x, y in centered_hull) / len(centered_hull) + centroid_y = sum(y for x, y in centered_hull) / len(centered_hull) + scale = 1.0 - args.inset / 100.0 + centered_hull = [ + (centroid_x + (x - centroid_x) * scale, centroid_y + (y - centroid_y) * scale) + for x, y in centered_hull + ] + + # Ensure counter-clockwise winding (required by Defold 2D physics) + centered_hull = ensure_ccw(centered_hull) + + # Write output + if args.output: + with open(args.output, "w", newline="\n") as f: + write_convexshape_formatted(centered_hull, f) + print(f"Written: {args.output}", file=sys.stderr) + else: + write_convexshape_formatted(centered_hull, sys.stdout) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.agents/skills/defold-proto-file-editing/scripts/gen_silhouette_chain.py b/.agents/skills/defold-proto-file-editing/scripts/gen_silhouette_chain.py new file mode 100644 index 0000000..7f0c4aa --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/scripts/gen_silhouette_chain.py @@ -0,0 +1,459 @@ +# SPDX-License-Identifier: CC0-1.0 + +"""Generate a Defold .collisionobject that approximates a concave silhouette +using a chain of thin, rotated TYPE_BOX shapes along the contour edges. + +This lets you simulate concave collision in Box2D (which only supports convex +primitives) by lining up narrow boxes along every edge of the simplified +boundary polygon — ideal for race tracks, terrain outlines, and other static +level geometry. + +Algorithm: + 1. Load the image and build a binary alpha mask. + 2. Extract directed boundary edges between opaque and transparent regions. + 3. Chain edges into closed contour loops (handles multiple disconnected + components and holes). + 4. Simplify each contour with Ramer-Douglas-Peucker. + 5. For every edge of the simplified polygon, emit a thin TYPE_BOX whose + position is the edge midpoint (in image-centred, Y-up Defold coords) + and whose rotation quaternion aligns the box along the edge. + +Usage: + python gen_silhouette_chain.py [options] + +Arguments: + image_path Path to a PNG or JPEG image file + --output, -o Output .collisionobject file path (default: stdout) + --epsilon, -e RDP simplification tolerance in pixels (default: 2.0) + --thickness, -t Half-thickness of each wall box in pixels (default: 2.0) + --alpha-threshold, -a Alpha threshold for "non-transparent" (0-255, default: 1) + --force-png-py Force using bundled png.py instead of PIL (PNG only) + --group, -g Collision group (default: "default") + --mask Collision mask group (repeatable, default: "default") + --friction Friction coefficient (default: 0.1) + --restitution Restitution / bounciness (default: 0.5) + +Output: + Protobuf Text Format .collisionobject with COLLISION_OBJECT_TYPE_STATIC + and embedded TYPE_BOX shapes. + +Environment: + FORCE_PNG_PY=1 Same as --force-png-py + +Exit code 0 on success, 1 on error. +""" + +import argparse +import math +import os +import sys +from typing import TextIO + +# image_loader is in the same directory; adjust sys.path so it's importable +# both when invoked directly and from the editor script. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from image_loader import load_binary_mask + + +# --------------------------------------------------------------------------- +# Contour extraction (directed boundary edges → closed loops) +# --------------------------------------------------------------------------- + +def extract_contours( + mask: list[list[bool]], width: int, height: int +) -> list[list[tuple[float, float]]]: + """Extract ordered contour loops from the binary mask. + + Uses directed boundary edges on the pixel grid. Each edge runs between + two grid vertices (integer coordinates 0..W, 0..H). Edges are chained + into closed loops by following start→end links. + + Returns a list of contours, each a list of (x, y) grid-vertex coords + forming a closed polygon (first point == last point). + """ + + def is_opaque(px: int, py: int) -> bool: + if 0 <= px < width and 0 <= py < height: + return mask[py][px] + return False + + # Pad the mask with a 1-pixel transparent border so shapes touching + # image edges always produce closed contour loops. + pad_w = width + 2 + pad_h = height + 2 + padded: list[list[bool]] = [[False] * pad_w for _ in range(pad_h)] + for py in range(height): + for px in range(width): + padded[py + 1][px + 1] = mask[py][px] + + def is_opaque_padded(px: int, py: int) -> bool: + if 0 <= px < pad_w and 0 <= py < pad_h: + return padded[py][px] + return False + + # Collect directed edges as a list, then index by start vertex. + edge_list: list[tuple[tuple[int, int], tuple[int, int]]] = [] + + # Direction convention: walk CW around opaque region in pixel space + # (Y-down), which becomes CCW in Defold's Y-up space. + # + # Vertical edge at column c between rows r and r+1: + # opaque on left (c-1, r) → edge goes DOWN: (c,r)→(c,r+1) + # opaque on right (c, r) → edge goes UP: (c,r+1)→(c,r) + for c in range(pad_w + 1): + for r in range(pad_h): + left_opaque = is_opaque_padded(c - 1, r) + right_opaque = is_opaque_padded(c, r) + if left_opaque == right_opaque: + continue + if left_opaque: + edge_list.append(((c, r), (c, r + 1))) + else: + edge_list.append(((c, r + 1), (c, r))) + + # Horizontal edge at row r between columns c and c+1: + # opaque below (c, r) → edge goes RIGHT: (c,r)→(c+1,r) + # opaque above (c, r-1) → edge goes LEFT: (c+1,r)→(c,r) + for r in range(pad_h + 1): + for c in range(pad_w): + top_opaque = is_opaque_padded(c, r - 1) + bot_opaque = is_opaque_padded(c, r) + if top_opaque == bot_opaque: + continue + if bot_opaque: + edge_list.append(((c, r), (c + 1, r))) + else: + edge_list.append(((c + 1, r), (c, r))) + + # Build adjacency: start_vertex → list of edge indices + adj: dict[tuple[int, int], list[int]] = {} + for idx, (s, _e) in enumerate(edge_list): + adj.setdefault(s, []).append(idx) + + used = [False] * len(edge_list) + + # Chain edges into closed loops + contours: list[list[tuple[float, float]]] = [] + for start_idx in range(len(edge_list)): + if used[start_idx]: + continue + used[start_idx] = True + start_vertex = edge_list[start_idx][0] + loop: list[tuple[float, float]] = [start_vertex, edge_list[start_idx][1]] + current = edge_list[start_idx][1] + + while current != start_vertex: + found = False + if current in adj: + for ei in adj[current]: + if not used[ei]: + used[ei] = True + nxt = edge_list[ei][1] + loop.append(nxt) + current = nxt + found = True + break + if not found: + break + + if current == start_vertex and len(loop) >= 4: + # Shift coordinates back by the padding offset (1, 1) + contours.append([(x - 1.0, y - 1.0) for x, y in loop]) + + return contours + + +# --------------------------------------------------------------------------- +# Ramer-Douglas-Peucker simplification (for closed polygons) +# --------------------------------------------------------------------------- + +def _perpendicular_distance( + px: float, py: float, ax: float, ay: float, bx: float, by: float +) -> float: + """Perpendicular distance from point P to line segment A-B.""" + dx = bx - ax + dy = by - ay + length_sq = dx * dx + dy * dy + if length_sq == 0.0: + return math.hypot(px - ax, py - ay) + t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / length_sq)) + proj_x = ax + t * dx + proj_y = ay + t * dy + return math.hypot(px - proj_x, py - proj_y) + + +def _rdp_reduce( + points: list[tuple[float, float]], epsilon: float +) -> list[tuple[float, float]]: + """Ramer-Douglas-Peucker on an open polyline.""" + if len(points) <= 2: + return points + + ax, ay = points[0] + bx, by = points[-1] + + max_dist = 0.0 + max_idx = 0 + for i in range(1, len(points) - 1): + d = _perpendicular_distance(points[i][0], points[i][1], ax, ay, bx, by) + if d > max_dist: + max_dist = d + max_idx = i + + if max_dist > epsilon: + left = _rdp_reduce(points[: max_idx + 1], epsilon) + right = _rdp_reduce(points[max_idx:], epsilon) + return left[:-1] + right + else: + return [points[0], points[-1]] + + +def simplify_contour( + contour: list[tuple[float, float]], epsilon: float +) -> list[tuple[float, float]]: + """Simplify a closed contour using RDP. + + The input contour has first == last point. Returns a simplified closed + polygon (first == last) with at least 3 unique vertices. + """ + # Remove closing duplicate for processing + pts = contour[:-1] + if len(pts) < 3: + return contour + + # For a closed polygon, we split at the point farthest from the + # line between its neighbours to avoid arbitrary split artefacts. + # Simple approach: split the ring into two halves and RDP each. + n = len(pts) + half = n // 2 + first_half = pts[: half + 1] + second_half = pts[half:] + [pts[0]] + + simplified_first = _rdp_reduce(first_half, epsilon) + simplified_second = _rdp_reduce(second_half, epsilon) + + # Merge (remove duplicate junction points) + merged = simplified_first[:-1] + simplified_second[:-1] + + # Close the polygon + if len(merged) < 3: + merged = pts[:3] + merged.append(merged[0]) + + return merged + + +# --------------------------------------------------------------------------- +# Box generation from polygon edges +# --------------------------------------------------------------------------- + +def format_float(v: float) -> str: + """Format float for Defold: always has decimal point, no unnecessary trailing zeros.""" + if v == int(v): + return f"{int(v)}.0" + s = f"{v:.6f}".rstrip("0") + if s.endswith("."): + s += "0" + return s + + +def angle_to_quat_z(angle: float) -> tuple[float, float]: + """Convert a Z-axis rotation angle (radians) to quaternion (z, w) components. + + Full quaternion is (0, 0, z, w). + """ + half = angle / 2.0 + return (math.sin(half), math.cos(half)) + + +def write_collisionobject( + contours: list[list[tuple[float, float]]], + thickness: float, + img_width: int, + img_height: int, + group: str, + masks: list[str], + friction: float, + restitution: float, + out: TextIO, +) -> int: + """Write a .collisionobject with rotated TYPE_BOX shapes along contour edges. + + Returns the number of boxes written. + """ + cx = img_width / 2.0 + cy = img_height / 2.0 + ext_z = 10.0 + + # Collect all boxes: (pos_x, pos_y, qz, qw, ext_x, ext_y) + boxes: list[tuple[float, float, float, float, float, float]] = [] + + for contour in contours: + for i in range(len(contour) - 1): + x1, y1 = contour[i] + x2, y2 = contour[i + 1] + + # Edge vector and length + dx = x2 - x1 + dy = y2 - y1 + length = math.hypot(dx, dy) + if length < 1e-6: + continue + + # Inward normal: left perpendicular of the CW edge direction + nx = -dy / length + ny = dx / length + + # Edge midpoint offset inward by thickness (inner stroke) + mid_px = (x1 + x2) / 2.0 + nx * thickness + mid_py = (y1 + y2) / 2.0 + ny * thickness + + # Convert to Defold coordinates (origin at image centre, Y up) + pos_x = mid_px - cx + pos_y = cy - mid_py + + # Angle of edge in pixel space (Y down), then negate for Defold (Y up) + angle_pixel = math.atan2(dy, dx) + angle_defold = -angle_pixel + + qz, qw = angle_to_quat_z(angle_defold) + + ext_x = length / 2.0 + ext_y = thickness + + boxes.append((pos_x, pos_y, qz, qw, ext_x, ext_y)) + + # Write header + out.write("type: COLLISION_OBJECT_TYPE_STATIC\n") + out.write("mass: 0.0\n") + out.write(f"friction: {format_float(friction)}\n") + out.write(f"restitution: {format_float(restitution)}\n") + out.write(f'group: "{group}"\n') + for m in masks: + out.write(f'mask: "{m}"\n') + + out.write("embedded_collision_shape {\n") + + # Write shapes + data_index = 0 + for pos_x, pos_y, qz, qw, ext_x, ext_y in boxes: + out.write(" shapes {\n") + out.write(" shape_type: TYPE_BOX\n") + out.write(" position {\n") + if pos_x != 0.0: + out.write(f" x: {format_float(pos_x)}\n") + if pos_y != 0.0: + out.write(f" y: {format_float(pos_y)}\n") + out.write(" }\n") + out.write(" rotation {\n") + if abs(qz) > 1e-7: + out.write(f" z: {format_float(qz)}\n") + if abs(qw - 1.0) > 1e-7: + out.write(f" w: {format_float(qw)}\n") + out.write(" }\n") + out.write(f" index: {data_index}\n") + out.write(" count: 3\n") + out.write(" }\n") + data_index += 3 + + # Write data array + for _pos_x, _pos_y, _qz, _qw, ext_x, ext_y in boxes: + out.write(f" data: {format_float(ext_x)}\n") + out.write(f" data: {format_float(ext_y)}\n") + out.write(f" data: {format_float(ext_z)}\n") + + out.write("}\n") + + return len(boxes) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Generate a Defold .collisionobject that approximates a concave " + "silhouette using a chain of rotated TYPE_BOX shapes along the " + "contour polygon." + ) + ) + parser.add_argument("image_path", help="Path to a PNG or JPEG image file") + parser.add_argument("--output", "-o", + help="Output .collisionobject file path (default: stdout)") + parser.add_argument("--epsilon", "-e", type=float, default=2.0, + help="RDP simplification tolerance in pixels (default: 2.0)") + parser.add_argument("--thickness", "-t", type=float, default=2.0, + help="Half-thickness of wall boxes in pixels (default: 2.0)") + parser.add_argument("--alpha-threshold", "-a", type=int, default=1, + help="Alpha threshold for non-transparent pixels (0-255, default: 1)") + parser.add_argument("--force-png-py", action="store_true", + help="Force using bundled png.py instead of PIL (PNG only)") + parser.add_argument("--group", "-g", default="default", + help='Collision group (default: "default")') + parser.add_argument("--mask", action="append", default=None, + help='Collision mask group (repeatable, default: "default")') + parser.add_argument("--friction", type=float, default=0.1, + help="Friction coefficient (default: 0.1)") + parser.add_argument("--restitution", type=float, default=0.5, + help="Restitution / bounciness (default: 0.5)") + args = parser.parse_args() + + masks = args.mask if args.mask else ["default"] + + # Load image + try: + mask, width, height = load_binary_mask( + args.image_path, args.alpha_threshold, + force_png_py=args.force_png_py, + ) + except Exception as e: + print(f"ERROR: Failed to read image: {e}", file=sys.stderr) + return 1 + + print(f"Image: {args.image_path} ({width}x{height})", file=sys.stderr) + + # Extract contours + contours = extract_contours(mask, width, height) + if not contours: + print("ERROR: No contours found in image", file=sys.stderr) + return 1 + + total_verts = sum(len(c) - 1 for c in contours) + print(f"Contours: {len(contours)} loops, {total_verts} vertices total", file=sys.stderr) + + # Simplify contours + simplified: list[list[tuple[float, float]]] = [] + for contour in contours: + s = simplify_contour(contour, args.epsilon) + simplified.append(s) + + total_simplified = sum(len(c) - 1 for c in simplified) + print( + f"Simplified: {total_simplified} vertices " + f"(epsilon={args.epsilon})", + file=sys.stderr, + ) + + # Write output + if args.output: + with open(args.output, "w", newline="\n") as f: + n = write_collisionobject( + simplified, args.thickness, width, height, + args.group, masks, args.friction, args.restitution, f, + ) + print(f"Box shapes: {n}", file=sys.stderr) + print(f"Written: {args.output}", file=sys.stderr) + else: + n = write_collisionobject( + simplified, args.thickness, width, height, + args.group, masks, args.friction, args.restitution, sys.stdout, + ) + print(f"Box shapes: {n}", file=sys.stderr) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.agents/skills/defold-proto-file-editing/scripts/get_image_size.py b/.agents/skills/defold-proto-file-editing/scripts/get_image_size.py new file mode 100644 index 0000000..eb1cc90 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/scripts/get_image_size.py @@ -0,0 +1,77 @@ +"""Get image dimensions (width, height) from PNG or JPEG files. + +No external dependencies — uses only Python stdlib (struct). + +Usage: + python get_image_size.py [ ...] + +Output (per image): + + +Exit code 0 on success, 1 if any file fails. +""" + +import struct +import sys +from pathlib import Path + + +def get_png_size(path: str) -> tuple[int, int] | None: + with open(path, "rb") as f: + sig = f.read(8) + if sig[:4] != b"\x89PNG": + return None + f.read(4) # IHDR chunk length + f.read(4) # IHDR chunk type + w, h = struct.unpack(">II", f.read(8)) + return w, h + + +def get_jpeg_size(path: str) -> tuple[int, int] | None: + with open(path, "rb") as f: + soi = f.read(2) + if soi != b"\xff\xd8": + return None + while True: + marker = f.read(2) + if len(marker) < 2 or marker[0] != 0xFF: + return None + m = marker[1] + # SOF0 (baseline) or SOF2 (progressive) + if m in (0xC0, 0xC2): + f.read(3) # length (2) + precision (1) + h, w = struct.unpack(">HH", f.read(4)) + return w, h + else: + length = struct.unpack(">H", f.read(2))[0] + f.read(length - 2) + + +def get_image_size(path: str) -> tuple[int, int] | None: + ext = Path(path).suffix.lower() + if ext == ".png": + return get_png_size(path) + elif ext in (".jpg", ".jpeg"): + return get_jpeg_size(path) + else: + return None + + +def main() -> int: + if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} [ ...]", file=sys.stderr) + return 1 + + ok = True + for path in sys.argv[1:]: + size = get_image_size(path) + if size is None: + print(f"ERROR: Could not read dimensions from: {path}", file=sys.stderr) + ok = False + else: + print(f"{path} {size[0]} {size[1]}") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.agents/skills/defold-proto-file-editing/scripts/image_loader.py b/.agents/skills/defold-proto-file-editing/scripts/image_loader.py new file mode 100644 index 0000000..cac49c9 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/scripts/image_loader.py @@ -0,0 +1,231 @@ +# SPDX-License-Identifier: CC0-1.0 + +"""Unified image loading with PIL/Pillow and pure-Python png.py fallback. + +Provides two functions used by gen_convexshape.py and gen_silhouette_chain.py: + +- load_alpha_mask() — returns list of non-transparent pixel coords + dimensions +- load_binary_mask() — returns 2D boolean mask + dimensions + +Backend selection order: +1. PIL/Pillow (supports PNG, JPEG, and other formats). +2. Bundled png.py (PNG only — raises an error for non-PNG files). + +Set the environment variable FORCE_PNG_PY=1 or pass force_png_py=True +to bypass PIL and use only the bundled png.py reader. +""" + +import os +import sys +from pathlib import Path + + +def _is_png(path: str) -> bool: + """Check if file has a .png extension (case-insensitive).""" + return Path(path).suffix.lower() == ".png" + + +def _try_import_pil(force_png_py: bool) -> type | None: + """Try to import PIL.Image, return the class or None.""" + if force_png_py: + return None + try: + from PIL import Image + return Image + except ImportError: + return None + + +# --------------------------------------------------------------------------- +# PIL backend +# --------------------------------------------------------------------------- + +def _load_alpha_mask_pil( + path: str, threshold: int +) -> tuple[list[tuple[int, int]], int, int]: + """Load image via PIL and return non-transparent pixel coordinates.""" + from PIL import Image + + img = Image.open(path) + width, height = img.size + + if img.mode == "RGBA": + pixels = img.load() + coords = [] + for y in range(height): + for x in range(width): + if pixels[x, y][3] >= threshold: + coords.append((x, y)) + elif img.mode == "LA": + pixels = img.load() + coords = [] + for y in range(height): + for x in range(width): + if pixels[x, y][1] >= threshold: + coords.append((x, y)) + elif img.mode == "P": + img_rgba = img.convert("RGBA") + pixels = img_rgba.load() + coords = [] + for y in range(height): + for x in range(width): + if pixels[x, y][3] >= threshold: + coords.append((x, y)) + else: + # No alpha channel (RGB, L, etc.) — all pixels are opaque + coords = [(x, y) for y in range(height) for x in range(width)] + + return coords, width, height + + +def _load_binary_mask_pil( + path: str, threshold: int +) -> tuple[list[list[bool]], int, int]: + """Load image via PIL and return a 2D boolean mask.""" + from PIL import Image + + img = Image.open(path) + width, height = img.size + + if img.mode != "RGBA": + img = img.convert("RGBA") + pixels = img.load() + + mask: list[list[bool]] = [] + for y in range(height): + row: list[bool] = [] + for x in range(width): + row.append(pixels[x, y][3] >= threshold) + mask.append(row) + + return mask, width, height + + +# --------------------------------------------------------------------------- +# png.py fallback backend +# --------------------------------------------------------------------------- + +def _load_png_reader(path: str): + """Create a png.Reader and read the image as direct RGBA/LA/RGB/L data.""" + # Import the bundled png.py from the same directory + script_dir = os.path.dirname(os.path.abspath(__file__)) + png_module_path = os.path.join(script_dir, "png.py") + if not os.path.isfile(png_module_path): + raise ImportError(f"Bundled png.py not found at {png_module_path}") + + import importlib.util + spec = importlib.util.spec_from_file_location("_bundled_png", png_module_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + reader = mod.Reader(filename=path) + width, height, rows, info = reader.asDirect() + # Materialize rows (they may be a generator) + rows = [list(row) for row in rows] + return width, height, rows, info + + +def _load_alpha_mask_png( + path: str, threshold: int +) -> tuple[list[tuple[int, int]], int, int]: + """Load a PNG image via bundled png.py and return non-transparent pixel coordinates.""" + width, height, rows, info = _load_png_reader(path) + planes = info["planes"] + has_alpha = info["alpha"] + + coords: list[tuple[int, int]] = [] + for y, row in enumerate(rows): + for x in range(width): + if has_alpha: + # Alpha is the last plane value for this pixel + alpha = row[x * planes + (planes - 1)] + if alpha >= threshold: + coords.append((x, y)) + else: + # No alpha channel — all pixels are opaque + coords.append((x, y)) + + return coords, width, height + + +def _load_binary_mask_png( + path: str, threshold: int +) -> tuple[list[list[bool]], int, int]: + """Load a PNG image via bundled png.py and return a 2D boolean mask.""" + width, height, rows, info = _load_png_reader(path) + planes = info["planes"] + has_alpha = info["alpha"] + + mask: list[list[bool]] = [] + for row in rows: + mask_row: list[bool] = [] + for x in range(width): + if has_alpha: + alpha = row[x * planes + (planes - 1)] + mask_row.append(alpha >= threshold) + else: + mask_row.append(True) + mask.append(mask_row) + + return mask, width, height + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def _resolve_force_flag(force_png_py: bool | None) -> bool: + """Resolve the force flag from argument and environment variable.""" + if force_png_py is not None: + return force_png_py + return os.environ.get("FORCE_PNG_PY", "").strip() in ("1", "true", "yes") + + +def load_alpha_mask( + path: str, threshold: int, *, force_png_py: bool | None = None +) -> tuple[list[tuple[int, int]], int, int]: + """Load image and return list of non-transparent pixel coordinates, width, height. + + Tries PIL first, falls back to bundled png.py for PNG files. + Set force_png_py=True or env FORCE_PNG_PY=1 to skip PIL. + """ + force = _resolve_force_flag(force_png_py) + pil = _try_import_pil(force) + + if pil is not None: + return _load_alpha_mask_pil(path, threshold) + + # Fallback to png.py + if not _is_png(path): + raise ImportError( + "Pillow (PIL) is required for non-PNG images (JPEG, etc.).\n" + "Install it from your terminal:\n" + " pip install pillow" + ) + print("INFO: PIL not available, using bundled png.py reader.", file=sys.stderr) + return _load_alpha_mask_png(path, threshold) + + +def load_binary_mask( + path: str, threshold: int, *, force_png_py: bool | None = None +) -> tuple[list[list[bool]], int, int]: + """Load image and return a 2D boolean mask (True = opaque), width, height. + + Tries PIL first, falls back to bundled png.py for PNG files. + Set force_png_py=True or env FORCE_PNG_PY=1 to skip PIL. + """ + force = _resolve_force_flag(force_png_py) + pil = _try_import_pil(force) + + if pil is not None: + return _load_binary_mask_pil(path, threshold) + + # Fallback to png.py + if not _is_png(path): + raise ImportError( + "Pillow (PIL) is required for non-PNG images (JPEG, etc.).\n" + "Install it from your terminal:\n" + " pip install pillow" + ) + print("INFO: PIL not available, using bundled png.py reader.", file=sys.stderr) + return _load_binary_mask_png(path, threshold) diff --git a/.agents/skills/defold-proto-file-editing/scripts/png.py b/.agents/skills/defold-proto-file-editing/scripts/png.py new file mode 100644 index 0000000..cfd5de1 --- /dev/null +++ b/.agents/skills/defold-proto-file-editing/scripts/png.py @@ -0,0 +1,2257 @@ +#!/usr/bin/env python + +# png.py - PNG encoder/decoder in pure Python +# +# Copyright (C) 2006 Johann C. Rocholl +# Portions Copyright (C) 2009 David Jones +# And probably portions Copyright (C) 2006 Nicko van Someren +# +# Original concept by Johann C. Rocholl. +# +# LICENCE (MIT) +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +The ``png`` module can read and write PNG files. + +Installation and Overview +------------------------- + +``pip install pypng`` + +For help, type ``import png; help(png)`` in your python interpreter. + +A good place to start is the :class:`Reader` and :class:`Writer` classes. + +Coverage of PNG formats is fairly complete; +all allowable bit depths (1/2/4/8/16/24/32/48/64 bits per pixel) and +colour combinations are supported: + +- greyscale (1/2/4/8/16 bit); +- RGB, RGBA, LA (greyscale with alpha) with 8/16 bits per channel; +- colour mapped images (1/2/4/8 bit). + +Interlaced images, +originally intended for progressive display when downloading, +are supported for reading (but not for writing). + +A number of optional chunks can be specified (when writing) +and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``. + +The ``sBIT`` chunk can be used to specify precision for +non-native bit depths. + +Requires Python 3.5 or higher. +Installation is trivial, +but see the ``README.txt`` file (with the source distribution) for details. + +Full use of all features will need some reading of the PNG specification +https://www.w3.org/TR/2003/REC-PNG-20031110/. + +The package also comes with command line utilities. + +- ``pripamtopng`` converts + `Netpbm `_ PAM/PNM files to PNG; +- ``pripngtopam`` converts PNG to file PAM/PNM. + +There are a few more for simple PNG manipulations. + +Spelling and Terminology +------------------------ + +Generally British English spelling is used in the documentation. +So that's "greyscale" and "colour". +This not only matches the author's native language, +it's also used by the PNG specification. + +Colour Models +------------- + +The major colour models supported by PNG (and hence by PyPNG) are: + +- greyscale; +- greyscale--alpha; +- RGB; +- RGB--alpha. + +Also referred to using the abbreviations: L, LA, RGB, RGBA. +Each letter codes a single channel: +*L* is for Luminance or Luma or Lightness (greyscale images); +*A* stands for Alpha, the opacity channel +(used for transparency effects, but higher values are more opaque, +so it makes sense to call it opacity); +*R*, *G*, *B* stand for Red, Green, Blue (colour image). + +Lists, arrays, sequences, and so on +----------------------------------- + +When getting pixel data out of this module (reading) and +presenting data to this module (writing) there are +a number of ways the data could be represented as a Python value. + +The preferred format is a sequence of *rows*, +which each row being a sequence of *values*. +In this format, the values are in pixel order, +with all the values from all the pixels in a row +being concatenated into a single sequence for that row. + +Consider an image that is 3 pixels wide by 2 pixels high, and each pixel +has RGB components: + +Sequence of rows:: + + list([R,G,B, R,G,B, R,G,B], + [R,G,B, R,G,B, R,G,B]) + +Each row appears as its own list, +but the pixels are flattened so that three values for one pixel +simply follow the three values for the previous pixel. + +This is the preferred because +it provides a good compromise between space and convenience. +PyPNG regards itself as at liberty to replace any sequence type with +any sufficiently compatible other sequence type; +in practice each row is an array (``bytearray`` or ``array.array``). + +To allow streaming the outer list is sometimes +an iterator rather than an explicit list. + +An alternative format is a single array holding all the values. + +Array of values:: + + [R,G,B, R,G,B, R,G,B, + R,G,B, R,G,B, R,G,B] + +The entire image is one single giant sequence of colour values. +Generally an array will be used (to save space), not a list. + +The top row comes first, +and within each row the pixels are ordered from left-to-right. +Within a pixel the values appear in the order R-G-B-A +(or L-A for greyscale--alpha). + +There is another format, which should only be used with caution. +It is mentioned because it is used internally, +is close to what lies inside a PNG file itself, +and has some support from the public API. +This format is called *packed*. +When packed, each row is a sequence of bytes (integers from 0 to 255), +just as it is before PNG scanline filtering is applied. +When the bit depth is 8 this is the same as a sequence of rows; +when the bit depth is less than 8 (1, 2 and 4), +several pixels are packed into each byte; +when the bit depth is 16 each pixel value is decomposed into 2 bytes +(and *packed* is a misnomer). +This format is used by the `Writer.write_packed` method. +It isn't usually a convenient format, +but may be just right if the source data for +the PNG image comes from something that uses a similar format +(for example, 1-bit BMPs, or another PNG file). +""" + +__version__ = "0.20250521.0" + +import collections +import io # For io.BytesIO +import itertools +import math +import re +import struct +import sys + +# https://docs.python.org/3.5/library/warnings.html +import warnings +import zlib + +from array import array + + +__all__ = ["ProtocolError", "Image", "Reader", "Writer", "write_chunks", "from_array"] + + +# The PNG signature. +# https://www.w3.org/TR/PNG/#5PNG-file-signature +signature = struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10) + +# The xstart, ystart, xstep, ystep for the Adam7 interlace passes. +adam7 = ( + (0, 0, 8, 8), + (4, 0, 8, 8), + (0, 4, 4, 8), + (2, 0, 4, 4), + (0, 2, 2, 4), + (1, 0, 2, 2), + (0, 1, 1, 2), +) + + +def adam7_generate(width, height): + """ + Generate the coordinates for the reduced scanlines + of an Adam7 interlaced image + of size `width` by `height` pixels. + + Yields a generator for each pass, + and each pass generator yields a series of (x, y, xstep) triples, + each one identifying a reduced scanline consisting of + pixels starting at (x, y) and taking every xstep pixel to the right. + """ + + for xstart, ystart, xstep, ystep in adam7: + if xstart >= width: + continue + yield ((xstart, y, xstep) for y in range(ystart, height, ystep)) + + +# Models the 'pHYs' chunk (used by the Reader) +Resolution = collections.namedtuple("_Resolution", "x y unit_is_meter") + + +def group(s, n): + return list(zip(*[iter(s)] * n)) + + +def isarray(x): + return isinstance(x, array) + + +def check_palette(palette): + """ + Check a palette argument (to the :class:`Writer` class) for validity. + Returns the palette as a list if okay; + raises an exception otherwise. + """ + + # None is the default and is allowed. + if palette is None: + return None + + p = list(palette) + if not (0 < len(p) <= 256): + raise ProtocolError( + "a palette must have between 1 and 256 entries," + " see https://www.w3.org/TR/PNG/#11PLTE" + ) + seen_triple = False + for i, t in enumerate(p): + if len(t) not in (3, 4): + raise ProtocolError("palette entry %d: entries must be 3- or 4-tuples." % i) + if len(t) == 3: + seen_triple = True + if seen_triple and len(t) == 4: + raise ProtocolError( + "palette entry %d: all 4-tuples must precede all 3-tuples" % i + ) + for x in t: + if int(x) != x or not (0 <= x <= 255): + raise ProtocolError( + "palette entry %d: " "values must be integer: 0 <= x <= 255" % i + ) + return p + + +def check_sizes(size, width, height): + """ + Check that these arguments, if supplied, are consistent. + Return a (width, height) pair. + """ + + if not size: + return width, height + + if len(size) != 2: + raise ProtocolError( + "size argument should be a pair (width, height) instead is %r" % (size,) + ) + if width is not None and width != size[0]: + raise ProtocolError( + "size[0] (%r) and width (%r) should match when both are used." + % (size[0], width) + ) + if height is not None and height != size[1]: + raise ProtocolError( + "size[1] (%r) and height (%r) should match when both are used." + % (size[1], height) + ) + return size + + +def check_color(c, greyscale, which): + """ + Checks that a colour argument for transparent or background options + is the right form. + Returns the colour + (which, if it's a bare integer, is "corrected" to a 1-tuple). + """ + + if c is None: + return c + if greyscale: + try: + len(c) + except TypeError: + c = (c,) + if len(c) != 1: + raise ProtocolError("%s for greyscale must be 1-tuple" % which) + if not is_natural(c[0]): + raise ProtocolError("%s colour for greyscale must be integer" % which) + else: + if not ( + len(c) == 3 and is_natural(c[0]) and is_natural(c[1]) and is_natural(c[2]) + ): + raise ProtocolError("%s colour must be a triple of integers" % which) + return c + + +class Error(Exception): + def __str__(self): + return self.__class__.__name__ + ": " + " ".join(self.args) + + +class FormatError(Error): + """ + Problem with input file format. + In other words, PNG file does not conform to + the specification in some way and is invalid. + """ + + +class ProtocolError(Error): + """ + Problem with the way the programming interface has been used, + or the data presented to it. + """ + + +class ChunkError(FormatError): + pass + + +class Default: + """The default for the greyscale parameter.""" + + +class Writer: + """ + PNG encoder in pure Python. + """ + + def __init__( + self, + width=None, + height=None, + size=None, + greyscale=Default, + alpha=False, + bitdepth=8, + palette=None, + transparent=None, + background=None, + gamma=None, + compression=None, + planes=None, + colormap=None, + maxval=None, + chunk_limit=2 ** 20, + physical=tuple(), + x_pixels_per_unit=None, + y_pixels_per_unit=None, + unit_is_meter=False, + ): + """ + Create a PNG encoder object. + + Arguments: + + width, height + Image size in pixels, as two separate arguments. + size + Image size (w,h) in pixels, as single argument. + greyscale + Pixels are greyscale, not RGB. + alpha + Input data has alpha channel (RGBA or LA). + bitdepth + Bit depth: from 1 to 16 (for each channel). + palette + Create a palette (PLTE chunk); enable colormap if it + is not specified. + transparent + Specify a transparent colour (create a ``tRNS`` chunk). + background + Specify a default background colour (create a ``bKGD`` chunk). + gamma + Specify a gamma value (create a ``gAMA`` chunk). + compression + zlib compression level: in range 0 to 9, or -1, or None; + planes + Number of planes (values per pixel) + colormap + True for type 3 PNG (a palette is required) + chunk_limit + Write multiple ``IDAT`` chunks to save memory. + physical + Write ``pHYs`` chunk using 3 values in a list. + x_pixels_per_unit + Use *physical* argument instead. + y_pixels_per_unit + Use *physical* argument instead. + unit_is_meter + Use *physical* argument instead. + + The image size (in pixels) can be specified either by using the + *width* and *height* arguments, or with the single *size* + argument. + If *size* is used it should be a pair (*width*, *height*). + + The *greyscale* argument indicates whether input pixels + are greyscale (when true), or colour (when false). + The default is true unless *palette* is used. + + The *alpha* argument (a boolean) specifies + whether input pixels have an alpha channel (or not). + + *bitdepth* specifies the bit depth of the source pixel values. + Each channel may have a different bit depth. + Each source pixel must have values that are + an integer between 0 and ``2**bitdepth-1``, where + *bitdepth* is the bit depth for the corresponding channel. + For example, 8-bit images have values between 0 and 255. + PNG only stores images with bit depths of + 1,2,4,8, or 16 (the same for all channels). + When *bitdepth* is not one of these values or where + channels have different bit depths, + the next highest valid bit depth is selected, + and an ``sBIT`` (significant bits) chunk is generated + that specifies the original precision of the source image. + In this case the supplied pixel values will be rescaled to + fit the range of the selected bit depth. + + The PNG file format supports many bit depth / colour model + combinations, but not all. + The details are somewhat arcane + (refer to the PNG specification for full details). + Briefly: + Bit depths < 8 (1,2,4) are only allowed with greyscale and + colour mapped images; + colour mapped images cannot have bit depth 16. + + For colour mapped images + (when the *colormap* argument is true, + or has been implicitly made true via the *palette* + argument) + the *bitdepth* argument must match one of + the valid PNG bit depths: 1, 2, 4, or 8. + (It is valid to have a PNG image with a palette and + an ``sBIT`` chunk, but the meaning is slightly different; + it would be awkward to use the *bitdepth* argument for this.) + + The *colormap* option, when true, + the PNG colour type is set to 3; + *greyscale* must not be true; *alpha* must not be true; + *transparent* must not be set. + The bit depth must be 1,2,4, or 8. + When a colour mapped image is created, + the pixel values are palette indexes and + the *bitdepth* argument specifies the size of these indexes + (not the size of the colour values in the palette). + + The *palette* argument adds a palette. + It also implicitly sets the *colormap* (to True) if + the *colormap* option is defaulted + (thus making a colour type 3 PNG). + + The palette argument value should be a sequence of 3- or + 4-tuples. + 3-tuples specify RGB palette entries; + 4-tuples specify RGBA palette entries. + All the 4-tuples (if present) must come before all the 3-tuples. + A ``PLTE`` chunk is created; + if there are 4-tuples then a ``tRNS`` chunk is created as well. + The ``PLTE`` chunk will contain all the RGB triples in the same + sequence; + the ``tRNS`` chunk will contain the alpha channel for + all the 4-tuples, in the same sequence. + Palette entries are always 8-bit. + + If specified, the *transparent* and *background* parameters must be + a tuple with one element for each channel in the image. + Either a 3-tuple of integer (RGB) values for a colour image, or + a 1-tuple of a single integer for a greyscale image. + + If specified, the *gamma* parameter must be a positive number + (generally, a `float`). + A ``gAMA`` chunk will be created. + Note that this will not change the values of the pixels as + they appear in the PNG file, + they are assumed to have already + been converted appropriately for the gamma specified. + + The *compression* argument specifies the compression level. + It is passed to the ``zlib`` module (unless it is `None`, + in which case nothing is passed, and ``zlib`` defaults are used). + Values from 1 to 9 (highest) specify compression. + 0 means no compression. + -1 is the ``zlib`` default (and so will also be used + when this argument is `None`) and indicates + the default level of compression (which is generally acceptable). + + *chunk_limit* is used to limit the amount of memory used whilst + compressing the image. + In order to avoid using large amounts of memory, + multiple ``IDAT`` chunks may be created. + + *physical* should be a list of up to 3 items: [xpp, ypp, ism]. + *xpp* is x-pixels-per-unit; *ypp* is y-pixels-per-unit + (defaults to xpp if not present); *ism* is is-meter, + ``True`` when the x- and y-resolutions are specified per meter + (defaults to ``False`` if not present). + + *x_pixels_per_unit* + *y_pixels_per_unit* + *unit_is_meter* + alternative to using *physical* keyword. *physical* will + override these values. + + """ + + # At the moment the `planes` argument is ignored; + # its purpose is to act as a placeholder so that + # ``Writer(x, y, **info)`` works, where `info` is a dictionary + # returned by Reader.read and friends. + + width, height = check_sizes(size, width, height) + del size + + if not is_natural(width) or not is_natural(height): + raise ProtocolError("width and height must be integers") + if width <= 0 or height <= 0: + raise ProtocolError("width and height must be greater than zero") + # https://www.w3.org/TR/PNG/#7Integers-and-byte-order + if width > 2 ** 31 - 1 or height > 2 ** 31 - 1: + raise ProtocolError("width and height cannot exceed 2**31-1") + + if alpha and transparent is not None: + raise ProtocolError("transparent colour not allowed with alpha channel") + + # bitdepth is either single integer, or tuple of integers. + # Convert to tuple. + try: + len(bitdepth) + except TypeError: + bitdepth = (bitdepth,) + for b in bitdepth: + valid = is_natural(b) and 1 <= b <= 16 + if not valid: + raise ProtocolError( + "each bitdepth %r must be a positive integer <= 16" % (bitdepth,) + ) + + # Check palette, and coerce to list + palette = check_palette(palette) + + # palette sets colormap only if colormap has been defaulted + if colormap is None: + colormap = bool(palette) + + # Check palette is available when colormap is true + if colormap and not palette: + raise ProtocolError("palette must be present when colormap is true") + + # Calculate channels, and + # expand bitdepth to be one element per channel. + alpha = bool(alpha) + colormap = bool(colormap) + if greyscale is Default and palette: + greyscale = False + greyscale = bool(greyscale) + if colormap: + color_planes = 1 + planes = 1 + else: + color_planes = (3, 1)[greyscale] + planes = color_planes + alpha + if len(bitdepth) == 1: + bitdepth *= planes + + bitdepth, self.rescale = check_bitdepth_rescale( + colormap, bitdepth, transparent, alpha, greyscale + ) + + # These are assertions, because above logic should have + # corrected or raised all problematic cases. + if bitdepth < 8: + assert greyscale or colormap + assert not alpha + if bitdepth > 8: + assert not colormap + + transparent = check_color(transparent, greyscale, "transparent") + background = check_color(background, greyscale, "background") + + if len(physical) == 0: + pass + elif len(physical) == 1: + (x_pixels_per_unit,) = physical + (y_pixels_per_unit,) = physical + elif len(physical) == 2: + x_pixels_per_unit, y_pixels_per_unit = physical + elif len(physical) == 3: + x_pixels_per_unit, y_pixels_per_unit, unit_is_meter = physical + else: + raise ProtocolError( + "Too many items in `physical` parameter %r" % (physical,) + ) + + # It's important that the true boolean values + # (greyscale, alpha, colormap) are converted + # to bool because Iverson's convention is relied upon later on. + self.width = width + self.height = height + self.transparent = transparent + self.background = background + self.gamma = gamma + self.greyscale = greyscale + self.alpha = alpha + self.colormap = colormap + self.bitdepth = int(bitdepth) + self.compression = compression + self.chunk_limit = chunk_limit + self.palette = palette + self.x_pixels_per_unit = x_pixels_per_unit + self.y_pixels_per_unit = y_pixels_per_unit + self.unit_is_meter = bool(unit_is_meter) + + self.color_type = 4 * self.alpha + 2 * (not greyscale) + 1 * self.colormap + assert self.color_type in (0, 2, 3, 4, 6) + + self.color_planes = color_planes + self.planes = planes + # :todo: fix for bitdepth < 8 + self.psize = (self.bitdepth / 8) * self.planes + + def write(self, outfile, rows): + """ + Write a PNG image to the output file. + *rows* should be an iterable that yields each row + (each row is a sequence of values). + + This method only consumes sufficient rows for the PNG + file (``self.height`` rows). + Extra rows are left unconsumed, but insufficient rows + will raise a `ProtocolError`. + Each row should have ``self.width * self.planes`` values. + """ + + # Values per row + vpr = self.width * self.planes + + def check_rows(rows): + """ + Yield each row in rows, + but check each row first (for correct width). + """ + for i, row in enumerate(rows): + try: + wrong_length = len(row) != vpr + except TypeError: + # When using an itertools.ichain object or + # other generator not supporting __len__, + # we set this to False to skip the check. + wrong_length = False + if wrong_length: + # Note: row numbers start at 0. + raise ProtocolError( + "Expected %d values but got %d values, in row %d" + % (vpr, len(row), i) + ) + yield row + + return self.write_passes(outfile, check_rows(rows)) + + def write_passes(self, outfile, rows): + """ + Write a PNG image to the output file. + + Most users are expected to find the `write` or + `write_array` method more convenient. + + The rows should be given to this method in the order that + they appear in the output file. + For straightlaced images, this is the usual top to bottom ordering. + For interlaced images the rows should have been interlaced before + passing them to this function (though PyPNG no longer + writes interlaced images). + + *rows* should be an iterable that yields each row + (each row being a sequence of values). + """ + + # Ensure rows are scaled (to 4-/8-/16-bit), + # and packed into bytes. + + if self.rescale: + rows = rescale_rows(rows, self.rescale) + + if self.bitdepth < 8: + rows = pack_rows(rows, self.bitdepth) + elif self.bitdepth == 16: + rows = unpack_rows(rows) + + return self.write_packed(outfile, rows) + + def write_packed(self, outfile, rows): + """ + Write PNG file to *outfile*. + *rows* should be an iterator that yields each packed row; + a packed row being a sequence of packed bytes. + + The rows have a filter byte prefixed and + are then compressed into one or more ``IDAT`` chunks. + They are not processed any further, + so if bitdepth is other than 1, 2, 4, 8, 16, + the pixel values should have been scaled + before passing them to this method. + + For interlaced images (no longer written by PyPNG), + the rows should be + presented in the order that they appear in the file. + """ + + self.write_preamble(outfile) + + # https://www.w3.org/TR/PNG/#11IDAT + if self.compression is not None: + compressor = zlib.compressobj(self.compression) + else: + compressor = zlib.compressobj() + + # data accumulates bytes to be compressed for the IDAT chunk; + # it's compressed when sufficiently large. + data = array("B") + + # raise i scope out of the for loop. set to -1, because the for loop + irows = iter(rows) + for i in range(self.height): + try: + row = next(irows) + except StopIteration: + raise ProtocolError("Not enough rows: %d supplied; %d required" % (i, self.height)) + # Add "None" filter type. + # Currently, it's essential that this filter type be used + # for every scanline as + # we do not mark the first row of a reduced pass image; + # that means we could accidentally compute + # the wrong filtered scanline if we used + # "up", "average", or "paeth" on such a line. + data.append(0) + data.extend(row) + if len(data) > self.chunk_limit: + compressed = compressor.compress(data) + if len(compressed): + write_chunk(outfile, b"IDAT", compressed) + data = bytearray() + + compressed = compressor.compress(bytes(data)) + flushed = compressor.flush() + if len(compressed) or len(flushed): + write_chunk(outfile, b"IDAT", compressed + flushed) + # https://www.w3.org/TR/PNG/#11IEND + write_chunk(outfile, b"IEND") + + def write_preamble(self, outfile): + # https://www.w3.org/TR/PNG/#5PNG-file-signature + + # This is the first write that is made when + # writing a PNG file. + # This one, and only this one, is checked for TypeError, + # which generally indicates that we are writing bytes + # into a text stream. + try: + outfile.write(signature) + except TypeError as e: + raise ProtocolError("PNG must be written to a binary stream") from e + + # https://www.w3.org/TR/PNG/#11IHDR + interlace = 0 + write_chunk( + outfile, + b"IHDR", + struct.pack( + "!2I5B", + self.width, + self.height, + self.bitdepth, + self.color_type, + 0, + 0, + interlace, + ), + ) + + # See :chunk:order + # https://www.w3.org/TR/PNG/#11gAMA + if self.gamma is not None: + write_chunk( + outfile, b"gAMA", struct.pack("!L", int(round(self.gamma * 1e5))) + ) + + # See :chunk:order + # https://www.w3.org/TR/PNG/#11sBIT + if self.rescale: + write_chunk( + outfile, + b"sBIT", + struct.pack("%dB" % self.planes, *[s[0] for s in self.rescale]), + ) + + # :chunk:order: Without a palette (PLTE chunk), + # ordering is relatively relaxed. + # With one, gAMA chunk must precede PLTE chunk + # which must precede tRNS and bKGD. + # See https://www.w3.org/TR/PNG/#5ChunkOrdering + if self.palette: + p, t = make_palette_chunks(self.palette) + write_chunk(outfile, b"PLTE", p) + if t: + # tRNS chunk is optional; + # Only needed if palette entries have alpha. + write_chunk(outfile, b"tRNS", t) + + # https://www.w3.org/TR/PNG/#11tRNS + if self.transparent is not None: + if self.greyscale: + fmt = "!1H" + else: + fmt = "!3H" + write_chunk(outfile, b"tRNS", struct.pack(fmt, *self.transparent)) + + # https://www.w3.org/TR/PNG/#11bKGD + if self.background is not None: + if self.greyscale: + fmt = "!1H" + else: + fmt = "!3H" + write_chunk(outfile, b"bKGD", struct.pack(fmt, *self.background)) + + # https://www.w3.org/TR/PNG/#11pHYs + if self.x_pixels_per_unit is not None and self.y_pixels_per_unit is not None: + tup = ( + self.x_pixels_per_unit, + self.y_pixels_per_unit, + int(self.unit_is_meter), + ) + write_chunk(outfile, b"pHYs", struct.pack("!LLB", *tup)) + + def write_array(self, outfile, pixels): + """ + Write an array that holds all the image values + as a PNG file on the output file. + See also :meth:`write` method. + """ + + return self.write_passes(outfile, self.array_scanlines(pixels)) + + def array_scanlines(self, pixels): + """ + Generates rows (each a sequence of values) from + a single array of values. + """ + + # Values per row + vpr = self.width * self.planes + stop = 0 + for y in range(self.height): + start = stop + stop = start + vpr + yield pixels[start:stop] + + +def write_chunk(outfile, tag, data=b""): + """ + Write a PNG chunk to the output file, including length and + checksum. + """ + + data = bytes(data) + # https://www.w3.org/TR/PNG/#5Chunk-layout + outfile.write(struct.pack("!I", len(data))) + outfile.write(tag) + outfile.write(data) + checksum = zlib.crc32(tag) + checksum = zlib.crc32(data, checksum) + checksum &= 2 ** 32 - 1 + outfile.write(struct.pack("!I", checksum)) + + +def write_chunks(out, chunks): + """Create a PNG file by writing out the chunks.""" + + out.write(signature) + for chunk in chunks: + write_chunk(out, *chunk) + + +def rescale_rows(rows, rescale): + """ + Take each row in rows (an iterator) and yield + a fresh row with the pixels scaled according to + the rescale parameters in the list `rescale`. + Each element of `rescale` is a tuple of + (source_bitdepth, target_bitdepth), + with one element per channel. + """ + + # One factor for each channel + fs = [float(2 ** s[1] - 1) / float(2 ** s[0] - 1) for s in rescale] + + # Assume all target_bitdepths are the same + target_bitdepths = set(s[1] for s in rescale) + assert len(target_bitdepths) == 1 + (target_bitdepth,) = target_bitdepths + typecode = "BH"[target_bitdepth > 8] + + # Number of channels + n_chans = len(rescale) + + for row in rows: + rescaled_row = array(typecode, iter(row)) + for i in range(n_chans): + channel = array(typecode, (int(round(fs[i] * x)) for x in row[i::n_chans])) + rescaled_row[i::n_chans] = channel + yield rescaled_row + + +def pack_rows(rows, bitdepth): + """Yield packed rows that are a byte array. + Each byte is packed with the values from several pixels. + """ + + assert bitdepth < 8 + assert 8 % bitdepth == 0 + + # samples per byte + spb = int(8 / bitdepth) + + def make_byte(block): + """Take a block of (2, 4, or 8) values, + and pack them into a single byte. + """ + + res = 0 + for v in block: + res = (res << bitdepth) + v + return res + + for row in rows: + a = bytearray(row) + # Adding padding bytes so we can group into a whole + # number of spb-tuples. + n = float(len(a)) + extra = math.ceil(n / spb) * spb - n + a.extend([0] * int(extra)) + # Pack into bytes. + # Each block is the samples for one byte. + blocks = group(a, spb) + yield bytearray(make_byte(block) for block in blocks) + + +def unpack_rows(rows): + """Unpack each row from being 16-bits per value, + to being a sequence of bytes. + """ + for row in rows: + fmt = "!%dH" % len(row) + yield bytearray(struct.pack(fmt, *row)) + + +def make_palette_chunks(palette): + """ + Create the byte sequences for a ``PLTE`` and + if necessary a ``tRNS`` chunk. + Returned as a pair (*p*, *t*). + *t* will be ``None`` if no ``tRNS`` chunk is necessary. + """ + + p = bytearray() + t = bytearray() + + for x in palette: + p.extend(x[0:3]) + if len(x) > 3: + t.append(x[3]) + if t: + return p, t + return p, None + + +def check_bitdepth_rescale(colormap, bitdepth, transparent, alpha, greyscale): + """ + Returns (bitdepth, rescale) pair. + """ + + if colormap: + if len(bitdepth) != 1: + raise ProtocolError("with colormap, only a single bitdepth may be used") + (bitdepth,) = bitdepth + if bitdepth not in (1, 2, 4, 8): + raise ProtocolError("with colormap, bitdepth must be 1, 2, 4, or 8") + if transparent is not None: + raise ProtocolError("transparent and colormap not compatible") + if alpha: + raise ProtocolError("alpha and colormap not compatible") + if greyscale: + raise ProtocolError("greyscale and colormap not compatible") + return bitdepth, None + + # No colormap, check for sBIT chunk generation. + + if greyscale and not alpha: + # Single channel, L. + (bitdepth,) = bitdepth + if bitdepth in (1, 2, 4, 8, 16): + return bitdepth, None + if bitdepth > 8: + targetbitdepth = 16 + elif bitdepth == 3: + targetbitdepth = 4 + else: + assert bitdepth in (5, 6, 7) + targetbitdepth = 8 + return targetbitdepth, [(bitdepth, targetbitdepth)] + + assert alpha or not greyscale + + depth_set = tuple(set(bitdepth)) + if depth_set in [(8,), (16,)]: + # No sBIT required. + (bitdepth,) = depth_set + return bitdepth, None + + targetbitdepth = (8, 16)[max(bitdepth) > 8] + return targetbitdepth, [(b, targetbitdepth) for b in bitdepth] + + +# Regex for decoding mode string +RegexModeDecode = re.compile("(LA?|RGBA?);?([0-9]*)", flags=re.IGNORECASE) + + +def from_array(a, mode=None, info={}): + """ + Create a PNG `Image` object from a 2-dimensional array. + One application of this function is easy PIL-style saving: + ``png.from_array(pixels, 'L').save('foo.png')``. + + Python doesn't really have 2-dimensional arrays, + a sequence of sequences should be supplied (list of array, + list of list, or similar). + + Unless they are specified using the *info* parameter, + the PNG's height and width are taken from the array size. + The height is the length of the sequence *a*; + the width is the length of the first row divided by the + number of channels. + + The argument *a* is assumed to be a sequence of rows, + each row being a sequence of values (``width*channels`` in number). + So an RGB image that is 20 pixels high and 32 wide will + occupy a 2-dimensional array that is 20x96 + (each row will be 32*3 = 96 sample values). + + *mode* is a string that specifies the image colour format in a + PIL-style mode. It can be: + + ``'L'`` + greyscale (1 channel) + ``'LA'`` + greyscale with alpha (2 channel) + ``'RGB'`` + colour image (3 channel) + ``'RGBA'`` + colour image with alpha (4 channel) + + The bit depth defaults to 8, but can be changed by + appending ``';16'`` to *mode*; + any decimal from 1 to 16 can be used to specify the bit depth. + + *mode* determines how many channels the image has, and + so allows the width to be derived from the row length + (the second array dimension). + + Canonically the argument *a* is a list of lists: + ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``. + Other forms may be suitable, particular if they are made + from Python Standard Library types. + The exact rules are: ``len(a)`` gives the first dimension, height; + ``len(a[0])`` gives the second dimension. + It's slightly more complicated than that because + an iterator of rows can be used, and it all still works. + Using an iterator allows data to be streamed efficiently. + + The *info* parameter is a dictionary that can + be used to specify metadata (in the same style as + the arguments to the `png.Writer` class). + For this function the keys that are useful are: + + height + overrides the height derived from the array dimensions and + allows *a* to be an iterable. + width + overrides the width derived from the array dimensions. + bitdepth + select bit depth + (must match *mode* if that also specifies a bit depth). + + Generally anything specified in the *info* dictionary will + override any implicit choices that this function would otherwise make, + but must match any explicit ones. + For example, if the *info* dictionary has a ``greyscale`` key then + this must be true when mode is ``'L'`` or ``'LA'`` and + false when mode is ``'RGB'`` or ``'RGBA'``. + """ + + # We abuse the *info* parameter by modifying it. Take a copy here. + # (Also typechecks *info* to some extent). + info = dict(info) + + # Syntax check mode string. + match = RegexModeDecode.match(mode) + if not match: + raise Error("mode string should be 'RGB' or 'L;16' or similar.") + + mode, bitdepth = match.groups() + if bitdepth: + bitdepth = int(bitdepth) + + # Colour format. + if "greyscale" in info: + if bool(info["greyscale"]) != ("L" in mode): + raise ProtocolError("info['greyscale'] should match mode.") + info["greyscale"] = "L" in mode + + alpha = "A" in mode + if "alpha" in info: + if bool(info["alpha"]) != alpha: + raise ProtocolError("info['alpha'] should match mode.") + info["alpha"] = alpha + + # Get bitdepth from *mode* if possible. + if bitdepth: + if info.get("bitdepth") and bitdepth != info["bitdepth"]: + raise ProtocolError( + "bitdepth (%d) should match bitdepth of info (%d)." + % (bitdepth, info["bitdepth"]) + ) + info["bitdepth"] = bitdepth + + # Fill in and/or check entries in *info*. + # Dimensions. + width = info.get("width") + + if "height" not in info: + try: + info["height"] = len(a) + except TypeError: + raise ProtocolError("len(a) does not work, supply info['height'] instead.") + height = info["height"] + + planes = len(mode) + if "planes" in info: + if info["planes"] != planes: + raise Error("info['planes'] should match mode.") + + # The first row is required to derive width and bitdepth. + # Which is why we need a copy of its iterator. + a, t = itertools.tee(a) + row = next(t) + del t + + if "width" not in info: + width = len(row) // planes + info["width"] = width + + if "bitdepth" not in info: + bitdepth = 8 + info["bitdepth"] = bitdepth + + for thing in ["width", "height", "bitdepth", "greyscale", "alpha"]: + assert thing in info + + return Image(a, info) + + +# So that refugee's from PIL feel more at home. Not documented. +fromarray = from_array + + +class Image: + """A PNG image. You can create an :class:`Image` object from + an array of pixels by calling :meth:`png.from_array`. It can be + saved to disk with the :meth:`save` method. + """ + + def __init__(self, rows, info): + """ + .. note :: + + The constructor is not public. Please do not call it. + """ + + self.rows = rows + self.info = info + + def save(self, file): + """Save the image to the named *file*. + + See `.write()` if you already have an open file object. + + In general, you can only call this method once; + after it has been called the first time the PNG image is written, + the source data will have been streamed, and + cannot be streamed again. + """ + + w = Writer(**self.info) + + with open(file, "wb") as fd: + w.write(fd, self.rows) + + def stream(self): + """Stream the rows into a list, so that the rows object + can be accessed multiple times, or randomly. + """ + + self.rows = list(self.rows) + + def write(self, file): + """Write the image to the open file object. + + See `.save()` if you have a filename. + + In general, you can only call this method once; + after it has been called the first time the PNG image is written, + the source data will have been streamed, and + cannot be streamed again. + """ + + w = Writer(**self.info) + w.write(file, self.rows) + + +class Reader: + """ + Pure Python PNG decoder in pure Python. + """ + + def __init__(self, _guess=None, filename=None, file=None, bytes=None): + """ + The constructor expects exactly one keyword argument. + If you supply a positional argument instead, + it will guess the input type. + Choose from the following keyword arguments: + + filename + Name of input file (a PNG file). + file + A file-like object (object with a read() method). + bytes + ``bytes`` or ``bytearray`` with PNG data. + + """ + keywords_supplied = ( + (_guess is not None) + + (filename is not None) + + (file is not None) + + (bytes is not None) + ) + if keywords_supplied != 1: + raise TypeError("Reader() takes exactly 1 argument") + + # Will be the first 8 bytes, later on. See validate_signature. + self.signature = None + self.transparent = None + # A pair of (len,type) if a chunk has been read but its data and + # checksum have not (in other words the file position is just + # past the 4 bytes that specify the chunk type). + # See preamble method for how this is used. + self.atchunk = None + + if _guess is not None: + if isarray(_guess): + bytes = _guess + elif isinstance(_guess, str): + filename = _guess + elif hasattr(_guess, "read"): + file = _guess + + if bytes is not None: + self.file = io.BytesIO(bytes) + elif filename is not None: + self.file = open(filename, "rb") + elif file is not None: + self.file = file + else: + raise ProtocolError("expecting filename, file or bytes array") + + def chunk(self): + """ + Read the next PNG chunk from the input file; + returns a (*type*, *data*) tuple. + *type* is the chunk's type as a byte string + (all PNG chunk types are 4 bytes long). + *data* is the chunk's data content, as a byte string. + """ + + self.validate_signature() + + # https://www.w3.org/TR/PNG/#5Chunk-layout + if not self.atchunk: + self.atchunk = self._chunk_len_type() + if not self.atchunk: + raise ChunkError("No more chunks.") + length, type = self.atchunk + self.atchunk = None + + data = self.file.read(length) + if len(data) != length: + raise ChunkError( + "Chunk %s too short for required %i octets." % (type, length) + ) + + checksum = self.file.read(4) + if len(checksum) != 4: + raise ChunkError("Chunk %s too short for checksum." % type) + verify = zlib.crc32(type) + verify = zlib.crc32(data, verify) + verify = struct.pack("!I", verify) + + if checksum != verify: + (a,) = struct.unpack("!I", checksum) + (b,) = struct.unpack("!I", verify) + message = "Checksum error in %s chunk: 0x%08X != 0x%08X." % ( + type.decode("ascii"), + a, + b, + ) + warnings.warn(message, RuntimeWarning) + + return type, data + + def chunks(self): + """Return an iterator that will yield each chunk as a + (*chunktype*, *content*) pair. + """ + + while True: + t, v = self.chunk() + yield t, v + if t == b"IEND": + break + + def chunk_of_type(self, type): + """Return the next chunk of the given type, which is a 4 + character ASCII string. + Raises an error if the chunk is not found. + """ + + target = bytes(type, "ascii") + + while True: + t, v = self.chunk() + if t == target: + return t, v + + def undo_filter(self, filter_type, scanline, previous): + """ + Undo the filter for a scanline. + *scanline* is a sequence of bytes that + does not include the initial filter type byte. + *previous* is decoded previous scanline + (for straightlaced images this is the previous pixel row, + but for interlaced images, it is + the previous scanline in the reduced image, + which in general is not the previous pixel row in the final image). + When there is no previous scanline + (the first row of a straightlaced image, + or the first row in one of the passes in an interlaced image), + then this argument should be ``None``. + + The scanline will have the effects of filtering removed; + the result will be returned as a fresh sequence of bytes. + """ + + # :todo: Would it be better to update scanline in place? + result = scanline + + if filter_type == 0: + return result + + if filter_type not in (1, 2, 3, 4): + raise FormatError( + "Invalid PNG Filter Type. " + "See https://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." + ) + + # Filter unit. The stride from one pixel to the corresponding + # byte from the previous pixel. Normally this is the pixel + # size in bytes, but when this is smaller than 1, the previous + # byte is used instead. + fu = max(1, self.psize) + + # For the first line of a pass, synthesize a placeholder previous + # line. An alternative approach would be to observe that on the + # first line 'up' is the same as 'null', 'paeth' is the same + # as 'sub', with only 'average' requiring any special case. + if not previous: + previous = bytearray([0] * len(scanline)) + + # Call appropriate filter algorithm. Note that 0 has already + # been dealt with. + fn = ( + None, + undo_filter_sub, + undo_filter_up, + undo_filter_average, + undo_filter_paeth, + )[filter_type] + fn(fu, scanline, previous, result) + return result + + def _deinterlace(self, raw): + """ + Read raw pixel data, undo filters, deinterlace, and flatten. + Return a single array of values. + """ + + # Values per row (of the target image) + vpr = self.width * self.planes + + # Values per image + vpi = vpr * self.height + # Interleaving writes to the output array randomly + # (well, not quite), so the entire output array must be in memory. + # Make a result array, and make it big enough. + if self.bitdepth > 8: + a = array("H", [0] * vpi) + else: + a = bytearray([0] * vpi) + source_offset = 0 + + for lines in adam7_generate(self.width, self.height): + # The previous (reconstructed) scanline. + # `None` at the beginning of a pass + # to indicate that there is no previous line. + recon = None + for x, y, xstep in lines: + # Pixels per row (reduced pass image) + ppr = int(math.ceil((self.width - x) / float(xstep))) + # Row size in bytes for this pass. + row_size = int(math.ceil(self.psize * ppr)) + + filter_type = raw[source_offset] + source_offset += 1 + scanline = raw[source_offset : source_offset + row_size] + source_offset += row_size + recon = self.undo_filter(filter_type, scanline, recon) + # Convert so that there is one element per pixel value + flat = self._bytes_to_values(recon, width=ppr) + if xstep == 1: + assert x == 0 + offset = y * vpr + a[offset : offset + vpr] = flat + else: + offset = y * vpr + x * self.planes + end_offset = (y + 1) * vpr + skip = self.planes * xstep + for i in range(self.planes): + a[offset + i : end_offset : skip] = flat[i :: self.planes] + + return a + + def _iter_bytes_to_values(self, byte_rows): + """ + Iterator that yields each scanline; + each scanline being a sequence of values. + `byte_rows` should be an iterator that yields + the bytes of each row in turn. + """ + + for row in byte_rows: + yield self._bytes_to_values(row) + + def _bytes_to_values(self, bs, width=None): + """Convert a packed row of bytes into a row of values. + Result will be a freshly allocated object, + not shared with the argument. + """ + + if self.bitdepth == 8: + return bytearray(bs) + if self.bitdepth == 16: + return array("H", struct.unpack("!%dH" % (len(bs) // 2), bs)) + + assert self.bitdepth < 8 + if width is None: + width = self.width + # Samples per byte + spb = 8 // self.bitdepth + out = bytearray() + mask = 2 ** self.bitdepth - 1 + shifts = [self.bitdepth * i for i in reversed(range(spb))] + for o in bs: + out.extend([mask & (o >> i) for i in shifts]) + return out[:width] + + def _iter_straight_packed(self, byte_blocks): + """Iterator that undoes the effect of filtering; + yields each row as a sequence of packed bytes. + Assumes input is straightlaced. + `byte_blocks` should be an iterable that yields the raw bytes + in blocks of arbitrary size. + """ + + # length of row, in bytes + rb = self.row_bytes + a = bytearray() + # The previous (reconstructed) scanline. + # None indicates first line of image. + recon = None + for some_bytes in byte_blocks: + a.extend(some_bytes) + while len(a) >= rb + 1: + filter_type = a[0] + scanline = a[1 : rb + 1] + del a[: rb + 1] + recon = self.undo_filter(filter_type, scanline, recon) + yield recon + if len(a) != 0: + # :file:format We get here with a file format error: + # when the available bytes (after decompressing) do not + # pack into exact rows. + raise FormatError("Wrong size for decompressed IDAT chunk.") + assert len(a) == 0 + + def validate_signature(self): + """ + If signature (header) has not been read then read and + validate it; otherwise do nothing. + No signature (empty read()) will raise EOFError; + An invalid signature will raise FormatError. + EOFError is raised to make possible the case where + a program can read multiple PNG files from the same stream. + The end of the stream can be distinguished from non-PNG files + or corrupted PNG files. + """ + + if self.signature: + return + self.signature = self.file.read(8) + if len(self.signature) == 0: + raise EOFError("End of PNG stream.") + if self.signature != signature: + raise FormatError("PNG file has invalid signature %r." % self.signature) + + def preamble(self): + """ + Extract the image metadata by reading + the initial part of the PNG file up to + the start of the ``IDAT`` chunk. + All the chunks that precede the ``IDAT`` chunk are + read and either processed for metadata or discarded. + """ + + self.validate_signature() + + while True: + if not self.atchunk: + self.atchunk = self._chunk_len_type() + if self.atchunk is None: + raise FormatError("This PNG file has no IDAT chunks.") + if self.atchunk[1] == b"IDAT": + return + self.process_chunk() + + def _chunk_len_type(self): + """ + Reads just enough of the input to + determine the next chunk's length and type; + return a (*length*, *type*) pair where *type* is a byte sequence. + If there are no more chunks, ``None`` is returned. + """ + + x = self.file.read(8) + if not x: + return None + if len(x) != 8: + raise FormatError("End of file whilst reading chunk length and type.") + length, type = struct.unpack("!I4s", x) + if length > 2 ** 31 - 1: + raise FormatError("Chunk %s is too large: %d." % (type, length)) + # Check that all bytes are in valid ASCII range. + # https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-layout + type_bytes = set(bytearray(type)) + if not (type_bytes <= set(range(65, 91)) | set(range(97, 123))): + raise FormatError("Chunk %r has invalid Chunk Type." % list(type)) + return length, type + + def process_chunk(self): + """ + Process the next chunk and its data. + This only processes the following chunk types: + ``IHDR``, ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``, ``pHYs``. + All other chunk types are ignored. + """ + + type, data = self.chunk() + method = "_process_" + type.decode("ascii") + m = getattr(self, method, None) + if m: + m(data) + + def _process_IHDR(self, data): + # https://www.w3.org/TR/PNG/#11IHDR + if len(data) != 13: + raise FormatError("IHDR chunk has incorrect length.") + ( + self.width, + self.height, + self.bitdepth, + self.color_type, + self.compression, + self.filter, + self.interlace, + ) = struct.unpack("!2I5B", data) + + check_bitdepth_colortype(self.bitdepth, self.color_type) + + if self.compression != 0: + raise FormatError("Unknown compression method %d" % self.compression) + if self.filter != 0: + raise FormatError( + "Unknown filter method %d," + " see https://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." + % self.filter + ) + if self.interlace not in (0, 1): + raise FormatError( + "Unknown interlace method %d, see " + "https://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods" + " ." % self.interlace + ) + + # Derived values + # https://www.w3.org/TR/PNG/#6Colour-values + colormap = bool(self.color_type & 1) + greyscale = not (self.color_type & 2) + alpha = bool(self.color_type & 4) + color_planes = (3, 1)[greyscale or colormap] + planes = color_planes + alpha + + self.colormap = colormap + self.greyscale = greyscale + self.alpha = alpha + self.color_planes = color_planes + self.planes = planes + self.psize = float(self.bitdepth) / float(8) * planes + if int(self.psize) == self.psize: + self.psize = int(self.psize) + self.row_bytes = int(math.ceil(self.width * self.psize)) + # Stores PLTE chunk if present, and is used to check + # chunk ordering constraints. + self.plte = None + # Stores tRNS chunk if present, and is used to check chunk + # ordering constraints. + self.trns = None + # Stores sBIT chunk if present. + self.sbit = None + + def _process_PLTE(self, data): + # https://www.w3.org/TR/PNG/#11PLTE + if self.plte: + warnings.warn("Multiple PLTE chunks present.") + self.plte = data + if len(data) % 3 != 0: + raise FormatError("PLTE chunk's length should be a multiple of 3.") + if len(data) > (2 ** self.bitdepth) * 3: + raise FormatError("PLTE chunk is too long.") + if len(data) == 0: + raise FormatError("Empty PLTE is not allowed.") + + def _process_bKGD(self, data): + try: + if self.colormap: + if not self.plte: + warnings.warn("PLTE chunk is required before bKGD chunk.") + self.background = struct.unpack("B", data) + else: + self.background = struct.unpack("!%dH" % self.color_planes, data) + except struct.error: + raise FormatError("bKGD chunk has incorrect length.") + + def _process_tRNS(self, data): + # https://www.w3.org/TR/PNG/#11tRNS + self.trns = data + if self.colormap: + if not self.plte: + warnings.warn("PLTE chunk is required before tRNS chunk.") + else: + if len(data) > len(self.plte) / 3: + # Was warning, but promoted to Error as it + # would otherwise cause pain later on. + raise FormatError("tRNS chunk is too long.") + else: + if self.alpha: + raise FormatError( + "tRNS chunk is not valid with colour type %d." % self.color_type + ) + try: + self.transparent = struct.unpack("!%dH" % self.color_planes, data) + except struct.error: + raise FormatError("tRNS chunk has incorrect length.") + + def _process_gAMA(self, data): + try: + self.gamma = struct.unpack("!L", data)[0] / 100000.0 + except struct.error: + raise FormatError("gAMA chunk has incorrect length.") + + def _process_sBIT(self, data): + self.sbit = data + if ( + self.colormap + and len(data) != 3 + or not self.colormap + and len(data) != self.planes + ): + raise FormatError("sBIT chunk has incorrect length.") + + def _process_pHYs(self, data): + # https://www.w3.org/TR/PNG/#11pHYs + self.phys = data + fmt = "!LLB" + if len(data) != struct.calcsize(fmt): + raise FormatError("pHYs chunk has incorrect length.") + self.x_pixels_per_unit, self.y_pixels_per_unit, unit = struct.unpack(fmt, data) + self.unit_is_meter = bool(unit) + + def read(self): + """ + Read the PNG file and decode it. + Returns (*width*, *height*, *rows*, *info*). + + May use excessive memory. + + *rows* is a sequence of rows; + each row is a sequence of values. + """ + + def iteridat(): + """Iterator that yields all the ``IDAT`` chunks as strings.""" + while True: + type, data = self.chunk() + if type == b"IEND": + # https://www.w3.org/TR/PNG/#11IEND + break + if type != b"IDAT": + continue + # type == b'IDAT' + # https://www.w3.org/TR/PNG/#11IDAT + if self.colormap and not self.plte: + warnings.warn("PLTE chunk is required before IDAT chunk") + yield data + + self.preamble() + raw = decompress(iteridat()) + + if self.interlace: + + def rows_from_interlace(): + """Yield each row from an interlaced PNG.""" + # It's important that this iterator doesn't read + # IDAT chunks until it yields the first row. + bs = bytearray(itertools.chain(*raw)) + arraycode = "BH"[self.bitdepth > 8] + # Like :meth:`group` but + # producing an array.array object for each row. + values = self._deinterlace(bs) + vpr = self.width * self.planes + for i in range(0, len(values), vpr): + row = array(arraycode, values[i : i + vpr]) + yield row + + rows = rows_from_interlace() + else: + rows = self._iter_bytes_to_values(self._iter_straight_packed(raw)) + info = dict() + for attr in "greyscale alpha planes bitdepth".split(): + info[attr] = getattr(self, attr) + info["size"] = (self.width, self.height) + for attr in "gamma transparent background".split(): + a = getattr(self, attr, None) + if a is not None: + info[attr] = a + if getattr(self, "x_pixels_per_unit", None): + info["physical"] = Resolution( + self.x_pixels_per_unit, self.y_pixels_per_unit, self.unit_is_meter + ) + if self.plte: + info["palette"] = self.palette() + return self.width, self.height, rows, info + + def read_flat(self): + """ + Read a PNG file and decode it into a single array of values. + Returns (*width*, *height*, *values*, *info*). + + May use excessive memory. + + *values* is a single array. + + The `read` method is more stream-friendly than this, + because it returns a sequence of rows. + """ + + x, y, pixel, info = self.read() + arraycode = "BH"[info["bitdepth"] > 8] + pixel = array(arraycode, itertools.chain(*pixel)) + return x, y, pixel, info + + def palette(self, alpha="natural"): + """ + Returns a palette that is a sequence of 3-tuples or 4-tuples, + synthesizing it from the ``PLTE`` and ``tRNS`` chunks. + These chunks should have already been processed (for example, + by calling the `preamble` method). + All the tuples are the same size: + 3-tuples if there is no ``tRNS`` chunk, + 4-tuples when there is a ``tRNS`` chunk. + + Assumes that the image is colour type + 3 and therefore a ``PLTE`` chunk is required. + + If the *alpha* argument is ``'force'`` then an alpha channel is + always added, forcing the result to be a sequence of 4-tuples. + """ + + if not self.plte: + raise FormatError("Required PLTE chunk is missing in colour type 3 image.") + plte = group(array("B", self.plte), 3) + if self.trns or alpha == "force": + trns = array("B", self.trns or []) + trns.extend([255] * (len(plte) - len(trns))) + plte = [pal + (a,) for pal, a in zip(plte, trns)] + return plte + + def asDirect(self): + """ + Returns the image data as a direct representation of + an ``x * y * planes`` array. + This removes the need for callers to deal with + palettes and transparency themselves. + Images with a palette (colour type 3) are converted to RGB or RGBA; + images with transparency (a ``tRNS`` chunk) are converted to + LA or RGBA as appropriate. + When returned in this format the pixel values represent + the colour value directly without needing to refer + to palettes or transparency information. + + Like the `read` method this method returns a 4-tuple: + + (*width*, *height*, *rows*, *info*) + + This method returns pixel values with + the bit depth they have in the source image. + + The *info* dictionary that is returned reflects + the *direct* format and not the original source image. + For example, an RGB source image with a ``tRNS`` chunk + to represent a transparent colour, + will start with ``planes=3`` and ``alpha=False`` for the + source image, + but the *info* dictionary returned by this method + will have ``planes=4`` and ``alpha=True`` because + an alpha channel is synthesized and added. + + *rows* is a sequence of rows; + each row being a sequence of values + (like the :meth:`read` method). + + All the other aspects of the image data are not changed. + """ + + self.preamble() + + # Simple case, no conversion necessary. + if not self.colormap and not self.trns and not self.sbit: + return self.read() + + x, y, pixels, info = self.read() + + if self.colormap: + info["colormap"] = False + info["alpha"] = bool(self.trns) + info["bitdepth"] = 8 + info["planes"] = 3 + bool(self.trns) + palette = self.palette() + + def iterpal(pixels): + for row in pixels: + row = [palette[x] for x in row] + yield array("B", itertools.chain(*row)) + + pixels = iterpal(pixels) + elif self.trns: + # It would be nice if there was some reasonable way + # of doing this without generating a whole load of + # intermediate tuples. But tuples does seem like the + # easiest way, with no other way clearly much simpler or + # much faster. (Actually, the L to LA conversion could + # perhaps go faster (all those 1-tuples!), but I still + # wonder whether the code proliferation is worth it) + it = self.transparent + maxval = 2 ** info["bitdepth"] - 1 + planes = info["planes"] + info["alpha"] = True + info["planes"] += 1 + del info["transparent"] + typecode = "BH"[info["bitdepth"] > 8] + + def itertrns(pixels): + for row in pixels: + # For each row we group it into pixels, then form a + # characterisation vector that says whether each + # pixel is opaque or not. Then we convert + # True/False to 0/maxval (by multiplication), + # and add it as the extra channel. + row = group(row, planes) + opa = [maxval * (pix != it) for pix in row] + yield array( + typecode, + itertools.chain(*[pix + (a,) for pix, a in zip(row, opa)]), + ) + + pixels = itertrns(pixels) + + return x, y, pixels, info + + def asRGB(self): + """ + Return image as RGB pixels. + RGB colour images are passed through unchanged; + greyscales are expanded into RGB triplets + (there is a small speed overhead for doing this). + + An alpha channel in the source image will raise an exception. + + The return values are as for the :meth:`read` method except that + the *info* reflect the returned pixels, not the source image. + In particular, + for this method ``info['greyscale']`` will be ``False``. + """ + + width, height, pixels, info = self.asDirect() + if info["alpha"]: + raise Error("will not convert image with alpha channel to RGB") + if not info["greyscale"]: + return width, height, pixels, info + info["greyscale"] = False + info["planes"] = 3 + + if info["bitdepth"] > 8: + + def newarray(): + return array("H", [0]) + + else: + + def newarray(): + return bytearray([0]) + + def iterrgb(): + for row in pixels: + a = newarray() * 3 * width + for i in range(3): + a[i::3] = row + yield a + + return width, height, iterrgb(), info + + def asRGBA(self): + """ + Return image as RGBA pixels. + Greyscales are expanded into RGB triplets; + an alpha channel is synthesized if necessary. + The return values are as for the :meth:`read` method except that + the *info* reflect the returned pixels, not the source image. + In particular, for this method + ``info['greyscale']`` will be ``False``, and + ``info['alpha']`` will be ``True``. + """ + + width, height, pixels, info = self.asDirect() + if info["alpha"] and not info["greyscale"]: + return width, height, pixels, info + typecode = "BH"[info["bitdepth"] > 8] + maxval = 2 ** info["bitdepth"] - 1 + maxbuffer = struct.pack("=" + typecode, maxval) * 4 * width + + if info["bitdepth"] > 8: + + def newarray(): + return array("H", maxbuffer) + + else: + + def newarray(): + return bytearray(maxbuffer) + + if info["alpha"] and info["greyscale"]: + # LA to RGBA + def convert(): + for row in pixels: + # Create a fresh target row, then copy L channel + # into first three target channels, and A channel + # into fourth channel. + a = newarray() + convert_la_to_rgba(row, a) + yield a + + elif info["greyscale"]: + # L to RGBA + def convert(): + for row in pixels: + a = newarray() + convert_l_to_rgba(row, a) + yield a + + else: + assert not info["alpha"] and not info["greyscale"] + # RGB to RGBA + + def convert(): + for row in pixels: + a = newarray() + convert_rgb_to_rgba(row, a) + yield a + + info["alpha"] = True + info["greyscale"] = False + info["planes"] = 4 + return width, height, convert(), info + + +def decompress(data_blocks): + """ + `data_blocks` should be an iterable that + yields the compressed data (from the ``IDAT`` chunks). + This yields decompressed byte strings. + """ + + # Currently, with no max_length parameter to decompress, + # this routine will do one yield per IDAT chunk: Not very + # incremental. + d = zlib.decompressobj() + # Each IDAT chunk is passed to the decompressor, then any + # remaining state is decompressed out. + for data in data_blocks: + # :todo: add a max_length argument here to limit output size. + yield bytearray(d.decompress(data)) + yield bytearray(d.flush()) + + +def check_bitdepth_colortype(bitdepth, colortype): + """ + Check that `bitdepth` and `colortype` are both valid, + and specified in a valid combination. + Returns (None) if valid, raise an Exception if not valid. + """ + + if bitdepth not in (1, 2, 4, 8, 16): + raise FormatError("invalid bit depth %d" % bitdepth) + if colortype not in (0, 2, 3, 4, 6): + raise FormatError("invalid colour type %d" % colortype) + # Check indexed (palettized) images have 8 or fewer bits + # per pixel; check only indexed or greyscale images have + # fewer than 8 bits per pixel. + if colortype & 1 and bitdepth > 8: + raise FormatError( + "Indexed images (colour type %d) cannot" + " have bitdepth > 8 (bit depth %d)." + " See https://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." + % (bitdepth, colortype) + ) + if bitdepth < 8 and colortype not in (0, 3): + raise FormatError( + "Illegal combination of bit depth (%d)" + " and colour type (%d)." + " See https://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." + % (bitdepth, colortype) + ) + + +def is_natural(x): + """A non-negative integer.""" + try: + is_integer = int(x) == x + except (TypeError, ValueError): + return False + return is_integer and x >= 0 + + +def undo_filter_sub(filter_unit, scanline, previous, result): + """Undo sub filter.""" + + ai = 0 + # Loops starts at index fu. Observe that the initial part + # of the result is already filled in correctly with + # scanline. + for i in range(filter_unit, len(result)): + x = scanline[i] + a = result[ai] + result[i] = (x + a) & 0xFF + ai += 1 + + +def undo_filter_up(filter_unit, scanline, previous, result): + """Undo up filter.""" + + for i in range(len(result)): + x = scanline[i] + b = previous[i] + result[i] = (x + b) & 0xFF + + +def undo_filter_average(filter_unit, scanline, previous, result): + """Undo up filter.""" + + ai = -filter_unit + for i in range(len(result)): + x = scanline[i] + if ai < 0: + a = 0 + else: + a = result[ai] + b = previous[i] + result[i] = (x + ((a + b) >> 1)) & 0xFF + ai += 1 + + +def undo_filter_paeth(filter_unit, scanline, previous, result): + """Undo Paeth filter.""" + + # Also used for ci. + ai = -filter_unit + for i in range(len(result)): + x = scanline[i] + if ai < 0: + a = c = 0 + else: + a = result[ai] + c = previous[ai] + b = previous[i] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + result[i] = (x + pr) & 0xFF + ai += 1 + + +def convert_la_to_rgba(row, result): + for i in range(3): + result[i::4] = row[0::2] + result[3::4] = row[1::2] + + +def convert_l_to_rgba(row, result): + """ + Convert a grayscale image to RGBA. + This method assumes the alpha channel in result is + already correctly initialized. + """ + for i in range(3): + result[i::4] = row + + +def convert_rgb_to_rgba(row, result): + """ + Convert an RGB image to RGBA. + This method assumes the alpha channel in result is + already correctly initialized. + """ + for i in range(3): + result[i::4] = row[i::3] + + +# Only reason to include this in this module is that +# several utilities need it, and it is small. +def binary_stdin(): + """ + A sys.stdin that returns bytes. + """ + + return sys.stdin.buffer + + +def binary_stdout(): + """ + A sys.stdout that accepts bytes. + """ + + stdout = sys.stdout.buffer + + # On Windows the C runtime file orientation needs changing. + if sys.platform == "win32": + import msvcrt + import os + + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + + return stdout + + +def cli_open(path): + if path == "-": + return binary_stdin() + return open(path, "rb") + + +def main(argv): + """ + Run command line PNG. + Which reports version. + """ + print(__version__, __file__) + + +if __name__ == "__main__": + try: + main(sys.argv) + except Error as e: + print(e, file=sys.stderr) diff --git a/.agents/skills/defold-scripts-editing/SKILL.md b/.agents/skills/defold-scripts-editing/SKILL.md new file mode 100644 index 0000000..0904769 --- /dev/null +++ b/.agents/skills/defold-scripts-editing/SKILL.md @@ -0,0 +1,155 @@ +--- +name: defold-scripts-editing +description: "Creates and edits Defold Lua script files (.script, .gui_script, .render_script, .editor_script) and plain Lua modules (.lua). Use when asked to create, modify, or configure any Defold script or Lua module." +--- + +# Editing Defold Script Files and Lua Modules + +Defold has four Lua script types (each running in a different context with different APIs) and plain `.lua` modules for reusable logic. + +**For API details** use `defold-api-fetch` skill. **For conceptual manuals** use `defold-docs-fetch` skill. This skill covers script structure, constraints, and templates. + +## Lua modules (.lua) + +Plain `.lua` files are Lua modules used to encapsulate reusable logic. **Extract frequently used universal logic into `.lua` modules** — avoid duplication and keep scripts lean. Modules are required via `require("path.to.module")` (dots as path separators). + +Defold projects typically use **Lua shared state** (`shared_state` in `game.project`). When enabled, all scripts, GUI scripts, and the render script run in the **same Lua context**. A Lua module required from any script has the **same context and state** within that single Lua interpreter instance — module-level locals and `package.loaded` are shared across all users of the module. Stateful modules behave like singletons. + +## Lua module structure (.lua) + +Encapsulate data and functions in a local table, return it: + +```lua +local M = {} + +function M.hello() + print("Hello") +end + +return M +``` + +Avoid globals in modules. For stateful modules, internal state is shared between all callers (singleton-like). For stateless logic, pass state explicitly or use constructors that return new state tables. See [Modules manual](https://defold.com/llms/manuals/modules.md). + +## Script types + +| Extension | Context | Lua runtime | `go.property()` | Key namespaces | +|---|---|---|---|---| +| `.script` | Game object component | LuaJIT | Yes | `go`, `msg`, `vmath`, `timer`, `sys`, component namespaces (`sprite`, `label`, `factory`, `collectionfactory`, `collectionproxy`, `tilemap`, `model`, `sound`, `particlefx`, `buffer`, `resource`, `physics`) | +| `.gui_script` | GUI component | LuaJIT | No | `gui`, `msg`, `vmath`, `sys` | +| `.render_script` | Render pipeline (one per project) | LuaJIT | No | `render`, `graphics`, `camera`, `msg`, `vmath`, `sys` | +| `.editor_script` | Defold editor | Lua 5.2 (luaj) | No | `editor`, `editor.ui`, `editor.prefs`, `editor.tx`, `http`, `json`, `zip`, `zlib` | + +## File format + +All script types and `.lua` modules are plain Lua files (not Protobuf Text Format). Scripts use specific extensions (`.script`, `.gui_script`, etc.); modules use `.lua`. + +## Runtime environment notes (.script, .gui_script, .render_script) + +**Platform-specific**: `html5` module is only available on the **HTML5** platform. + +**App Manifest exclusions**: Some built-in modules (`liveupdate`, `image`, `types`, `profiler`, `sound`, `physics`, etc.) can be excluded via the App Manifest to reduce binary size. If excluded, their APIs will not be available at runtime. + +**No `utf8` module**: For working with non-ASCII strings, use the defold-utf8 dependency (`https://github.com/d954mas/defold-utf8/archive/refs/heads/master.zip`). API reference: [utf8.script_api](https://github.com/d954mas/defold-utf8/blob/master/utf8/api/utf8.script_api). + +## Common runtime script patterns (.script, .gui_script, .render_script) + +All runtime lifecycle callbacks receive `self` as the first parameter — a userdata that acts like a table for storing instance state. + +### Script structure + +1. **Module requires** (optional) — `local M = require("module")` +2. **Local helper functions** (optional) — must be at module scope, never inside other functions +3. **Declarations** (`.script` only: `go.property()` at top level) +4. **Lifecycle callbacks** + +### Lifecycle callback availability + +| Callback | `.script` | `.gui_script` | `.render_script` | +|---|---|---|---| +| `init(self)` | ✓ | ✓ | ✓ | +| `final(self)` | ✓ | ✓ | — | +| `fixed_update(self, dt)` | ✓ | — | — | +| `update(self, dt)` / `update(self)` | ✓ (dt) | ✓ (dt) | ✓ (no dt) | +| `late_update(self, dt)` | ✓ | — | — | +| `on_message(self, message_id, message, sender)` | ✓ | ✓ | ✓ (no sender) | +| `on_input(self, action_id, action)` | ✓ | ✓ | — | +| `on_reload(self)` | ✓ | ✓ | — | + +### Key rules + +- Store per-instance state in `self`, not in module-level locals (module-level locals are shared across all instances). +- Omit unused callbacks — especially `update()` and `fixed_update()` which cost a call per frame even if empty. +- Keep local helper functions at module scope, never inside other functions. + +## Native extensions + +Native extensions (C/C++/ObjC/Java) register their Lua functions into the global scope via the Lua C API (`lua_register`, `luaL_openlib`, etc.). This means extensions typically add a new global table (e.g., `myext`) with functions and constants accessible from any `.script` or `.gui_script`. + +Extensions describe their API for editor auto-complete via `*.script_api` files (YAML format) located in their `api/` directory. The format: + +```yaml +- name: extension_name + type: table + desc: Extension description + members: + - name: function_name + type: function + desc: Function description + parameters: + - name: param_name + type: string + desc: Parameter description + returns: + - name: return_name + type: number + desc: Return value description +``` + +Types: `table`, `string`, `boolean`, `number`, `function`. Multiple types: `[type1, type2]`. + +## Lua preprocessing + +Defold supports conditional compilation via the [Lua preprocessor extension](https://github.com/defold/extension-lua-preprocessor) (applies to all Lua files including `.lua` modules): + +```lua +--#IF DEBUG +local lives = 999 +--#ELSE +local lives = 3 +--#ENDIF +``` + +Keywords: `RELEASE`, `DEBUG`, `HEADLESS`. + +## Type-specific references + +Consult the `references/` directory for constraints, templates, and patterns specific to each script type: + +- `references/script.md` — `go.property()` reference (types, constraints, overrides), templates +- `references/gui_script.md` — GUI script constraints, template node access, templates +- `references/render_script.md` — render pipeline architecture, system messages, full working templates +- `references/editor_script.md` — module structure, commands, lifecycle hooks, execution modes, templates + +## Workflow + +### Creating a new Lua module (.lua) + +1. Use when logic is reusable across multiple scripts or screens. +2. Create a local table, add functions, return it. +3. Keep modules stateless when possible; if stateful, document that state is shared. +4. Require with dot notation: `require("main.utils")`, `require("screens.game.helpers")`. + +### Creating a new script + +1. Determine the correct extension for the script type. +2. Read the corresponding type-specific reference in `references/` for constraints and templates. +3. Follow the structure pattern: requires → helpers → declarations → callbacks. +4. Only add lifecycle callbacks you actually need. + +### Editing an existing script + +1. Read the current file. +2. Preserve existing declarations (e.g., `go.property()` names) and callback signatures. +3. Add/modify callbacks as needed. +4. Changing a `go.property()` name or type may break overrides in `.go` and `.collection` files. diff --git a/.agents/skills/defold-scripts-editing/references/editor_script.md b/.agents/skills/defold-scripts-editing/references/editor_script.md new file mode 100644 index 0000000..37a2981 --- /dev/null +++ b/.agents/skills/defold-scripts-editing/references/editor_script.md @@ -0,0 +1,211 @@ +# .editor_script Reference + +Editor script — Lua file that extends the Defold editor with custom menu commands, build lifecycle hooks, language servers, HTTP server routes, and preferences. + +## Available APIs + +`editor`, `editor.ui`, `editor.prefs`, `editor.tx`, `http` (editor version), `json`, `localization`, `tilemap.tiles`, `zip`, `zlib`, `io`, `os` (restricted), `string`, `table`, `math`, `coroutine`, `print`, `pprint` + +**Not available**: all game engine namespaces (`go`, `gui`, `render`, `vmath`, `msg`, `timer`, `sprite`, `label`, `factory`, `collectionfactory`, `collectionproxy`, `physics`, `sound`, `particlefx`, `tilemap` engine API, `model`, `camera`, `buffer`, `resource`, `window`, `graphics`, `hash`, `crash`, `profiler`) + +For `editor` namespace API details use `defold-api-fetch`. For editor scripting concepts and UI components use `defold-docs-fetch` (editor-scripts and editor-scripts-ui manuals). + +## Key constraints + +- Runs in the **editor**, not the game runtime +- Uses **Lua 5.2** (via luaj JVM runtime), not LuaJIT +- **No game engine APIs** — `go`, `gui`, `render`, `vmath`, `msg`, `timer`, etc. are not available +- **No lifecycle callbacks** (`init`, `update`, `on_message`, etc.) — uses module functions instead +- **No `go.property()`** or `self` table — no instance concept +- Only `/hooks.editor_script` at the project root receives lifecycle events +- All editor scripts reload via **Project → Reload Editor Scripts** + +### Lua 5.2 runtime restrictions + +- No `debug` package +- No `os.execute` — use `editor.execute()` instead +- No `os.tmpname`, `io.tmpfile` — scripts can only access files inside the project directory +- No `os.rename`, `os.exit`, `os.setlocale` + +## Module structure + +Every `.editor_script` **must return a module table**: + +```lua +local M = {} + +function M.get_commands() + -- return array of command definitions +end + +function M.get_language_servers() + -- return array of language server definitions +end + +function M.get_prefs_schema() + -- return table of preference schemas +end + +function M.get_http_server_routes() + -- return array of HTTP route definitions +end + +return M +``` + +All module functions are optional. + +## Node properties (`editor.get` / `editor.tx.set`) + +Use `editor.get(node, property)` to read and `editor.tx.set(node, property, value)` to write properties. Always check availability with `editor.can_get()` / `editor.can_set()` first. + +### Property naming convention + +Outline properties use **snake_case** names matching the proto field names (not hyphenated). To verify a property's editor script name, hover over its label in the Properties panel — the tooltip shows the exact name. + +Examples: `"default_animation"`, `"blend_mode"`, `"size_mode"`, `"playback_rate"`, `"material"`. + +**Sprite texture samplers** use a special naming pattern: `"__sampler____"`. For the default sprite material, the atlas property is `"__sampler__texture_sampler__0"`. For multi-texture sprites, subsequent samplers use `__1`, `__2`, etc. + +### Common properties + +- `"path"` — resource path for file-backed resources (e.g. `"/main/game.script"`) +- `"text"` — text content of text-editable resources (scripts, JSON); reflects unsaved edits +- `"children"` — child resource paths for directory resources + +### Component-type-specific list properties + +- **Atlas**: `"images"` (list of image nodes), `"animations"` (list of animation nodes) +- **Atlas animation**: `"images"` (list of image nodes), `"id"` (animation name) +- **Atlas image**: `"image"` (resource path to PNG) +- **Tilemap**: `"layers"` (list of layer nodes) +- **Tilemap layer**: `"tiles"` (2D grid, see `tilemap.tiles.*`) +- **ParticleFX**: `"emitters"`, `"modifiers"` +- **ParticleFX emitter**: `"modifiers"` +- **Collision object**: `"shapes"` (list of shape nodes) +- **GUI**: `"layers"`, `"materials"`, `"fonts"`, `"textures"`, `"particlefxs"`, `"nodes"` +- **Game object** (`.go`): `"components"` (list of component nodes) +- **Collection**: `"children"` (list of game object / collection nodes) + +### Outline property types supported + +`strings`, `booleans`, `numbers`, `vec2`/`vec3`/`vec4`, `resources`, `curves` + +Set a resource property to `nil` by passing `""`. + +## Commands + +Define custom menu items via `get_commands()`. Each command table: + +- `label` (required) — text shown in the menu +- `locations` (required) — array of: `"Edit"`, `"View"`, `"Project"`, `"Debug"`, `"Assets"`, `"Bundle"`, `"Scene"`, `"Outline"` +- `query` (optional) — `{selection = {type = "resource"|"outline"|"scene", cardinality = "one"|"many"}}` or `{argument = ...}` (Bundle only) +- `id` (optional) — identifier for shortcut assignment (e.g., `"my-ext.do-stuff"`) +- `active` (optional) — function returning boolean. Runs in **immediate** mode — must be fast +- `run` (optional) — function executed when selected. Runs in **long-running** mode + +```lua +function M.get_commands() + return { + { + label = "Remove Comments", + locations = {"Edit", "Assets"}, + query = { + selection = {type = "resource", cardinality = "one"} + }, + active = function(opts) + local path = editor.get(opts.selection, "path") + return path:match("%.lua$") ~= nil or path:match("%.script$") ~= nil + end, + run = function(opts) + local text = editor.get(opts.selection, "text") + local new_text = text:gsub("%-%-[^\n]*", "") + editor.transact({ + editor.tx.set(opts.selection, "text", new_text) + }) + end + } + } +end +``` + +Alternatively, use `editor.command({...})` at the top level of the module. + +## Execution modes + +**Immediate mode** — `active` callbacks and top-level script code. Must be fast. + +**Long-running mode** — `run` callbacks and lifecycle hooks. Can take time. + +Long-running-only functions (error if called in immediate mode): +- `editor.create_directory()`, `editor.create_resources()`, `editor.delete_directory()` +- `editor.save()`, `os.remove()`, `file:write()` +- `editor.execute()` +- `editor.transact()` + +## Lifecycle hooks + +**Only** `/hooks.editor_script` (at the project root, next to `game.project`) receives lifecycle events. Other editor scripts do NOT receive them. Hooks are editor-only — NOT executed by Bob. + +```lua +local M = {} + +function M.on_build_started(opts) + -- opts.platform — e.g., "x86_64-win32" + -- Raising error aborts the build +end + +function M.on_build_finished(opts) + -- opts.platform, opts.success (boolean) +end + +function M.on_bundle_started(opts) + -- opts.output_directory, opts.platform, opts.variant ("debug"|"release"|"headless") + -- Raising error aborts the bundle +end + +function M.on_bundle_finished(opts) + -- opts.output_directory, opts.platform, opts.variant, opts.success +end + +function M.on_target_launched(opts) + -- opts.url — e.g., "http://127.0.0.1:35405" +end + +function M.on_target_terminated(opts) + -- opts.url +end + +return M +``` + +## Language servers + +Define LSP-compatible language servers via `get_language_servers()`: + +```lua +function M.get_language_servers() + local command = "build/plugins/my-ext/plugins/bin/" .. editor.platform .. "/lua-lsp" + if editor.platform == "x86_64-win32" then + command = command .. ".exe" + end + return { + { + languages = {"lua"}, + watched_files = { + {pattern = "**/.luacheckrc"} + }, + command = {command, "--stdio"} + } + } +end +``` + +- `languages` (required) — array of language identifiers +- `command` (required) — array of command and arguments +- `watched_files` (optional) — array of `{pattern = "glob"}` + +## Editor scripts in libraries + +Editor scripts in libraries are automatically picked up. Lifecycle hooks cannot be in libraries — they must be in `/hooks.editor_script`. Library hooks should be exposed as Lua functions for the user to `require` in their own `/hooks.editor_script`. + diff --git a/.agents/skills/defold-scripts-editing/references/gui_script.md b/.agents/skills/defold-scripts-editing/references/gui_script.md new file mode 100644 index 0000000..3b53705 --- /dev/null +++ b/.agents/skills/defold-scripts-editing/references/gui_script.md @@ -0,0 +1,46 @@ +# .gui_script Reference + +GUI script — Lua file attached to a `.gui` component via its `script` field. Controls UI behavior, node manipulation, and input handling. + +## Available APIs + +`gui`, `msg`, `vmath`, `sys`, `http`, `socket`, `json`, `zlib`, `html5`, `image`, `timer`, `sound`, `resource`, `camera` + Lua standard library + native extension globals + +**Not available**: `go`, `render`, `sprite`, `label`, `tilemap`, `factory`, `collectionfactory`, `collectionproxy`, `model`, `particlefx`, `buffer`, `graphics`, `physics` + +For `gui` namespace API details use `defold-api-fetch`. + +## Key constraints (differences from .script) + +- Has access to `gui` namespace, but **not** `go` or `render` +- **No `go.property()`** — gui scripts cannot declare exposed properties +- **No `fixed_update()` or `late_update()`** callbacks +- **No raw gamepad input** — `on_input` does not receive raw gamepad data (mapped button actions still work) +- **One script per GUI** — each `.gui` component has exactly one `.gui_script` (or none) +- **Template scripts are ignored** — if a GUI is included as a template in another GUI, only the parent GUI's script runs +- Prefer reactive/message-driven logic over `update()` polling +- Cache node references in `init()` — calling `gui.get_node()` every frame is wasteful + +## Template node access + +For nodes inside a template, prefix with the template node ID: + +```lua +local btn_bg = gui.get_node("play_button/background") +local btn_text = gui.get_node("play_button/label") +``` + +## Addressing the GUI component + +From game object scripts: + +```lua +msg.post("hud_go#gui", "set_score", { score = 100 }) +``` + +From the GUI script, address game objects: + +```lua +msg.post("/player#script", "heal", { amount = 10 }) +``` + diff --git a/.agents/skills/defold-scripts-editing/references/render_script.md b/.agents/skills/defold-scripts-editing/references/render_script.md new file mode 100644 index 0000000..19a0d87 --- /dev/null +++ b/.agents/skills/defold-scripts-editing/references/render_script.md @@ -0,0 +1,40 @@ +# .render_script Reference + +Render script — Lua file that controls the rendering pipeline: what is drawn, when, and where. One active render script per project. + +## Available APIs + +`render`, `graphics`, `camera`, `msg`, `vmath`, `sys`, `hash`, `pprint`, `print`, `socket`, `json`, `zlib`, `http`, `html5`, `image`, `window`, `timer` + Lua standard library + +**Not available**: `go`, `gui`, `sprite`, `label`, `tilemap`, `factory`, `collectionfactory`, `collectionproxy`, `model`, `particlefx`, `sound`, `physics`, `buffer`, `resource`, `crash`, `profiler` + +For `render`, `graphics`, and `camera` namespace API details use `defold-api-fetch`. For rendering concepts use `defold-docs-fetch` (render manual). + +## Key constraints + +- Has access to `render`, `graphics`, `camera` namespaces, but **not** `go`, `gui`, or component-specific namespaces +- **No `go.property()`** +- **Only three callbacks**: `init()`, `update()`, `on_message()` — no `final`, `fixed_update`, `late_update`, `on_input`, `on_reload` +- **`update()` runs at the end of the frame** — after all game object updates and transforms are complete +- **`on_message` has no `sender` parameter** — only `(self, message_id, message)` +- Receives messages via the **`@render:` socket** (e.g., `msg.post("@render:", "clear_color", {...})`) + +## Render file connection + +A render script must be referenced by a `.render` file: + +``` +script: "/render/my.render_script" +render_resources { + name: "my_material" + path: "/materials/my.material" +} +``` + +The `.render` file is set in `game.project`: + +```ini +[bootstrap] +render = /render/my.render +``` + diff --git a/.agents/skills/defold-scripts-editing/references/script.md b/.agents/skills/defold-scripts-editing/references/script.md new file mode 100644 index 0000000..449fe20 --- /dev/null +++ b/.agents/skills/defold-scripts-editing/references/script.md @@ -0,0 +1,131 @@ +# .script Reference + +Game object script — Lua file attached to a game object as a component. Defines behavior through lifecycle callbacks and exposes editable properties via `go.property()`. + +## Available APIs + +`go`, `msg`, `vmath`, `timer`, `sys`, `http`, `socket`, `json`, `zlib`, `html5`, `sound`, `sprite`, `label`, `particlefx`, `tilemap`, `factory`, `collectionfactory`, `collectionproxy`, `model`, `buffer`, `resource`, `crash`, `profiler`, `window`, `image`, `physics`, `b2d`, `camera`, `graphics` + Lua standard library + native extension globals + +**Not available**: `gui`, `render` + +For API details of specific namespaces use `defold-api-fetch`. + +## Game object addressing + +When referencing other game objects from a script (via `go.set_position`, `go.get_position`, `msg.post`, etc.), use absolute paths with a leading `/`: `hash("/sprite1")`, NOT `hash("sprite1")`. Without `/` the path is relative and only resolves to children of the current game object. + +## go.property() reference + +`go.property(name, default_value)` declares a script property that is: +- Visible and editable in the Defold editor Properties panel +- Accessible at runtime as `self.` +- Overridable per-instance in `.go` files (via `PropertyDesc`) and `.collection` files (via `ComponentPropertyDesc`) +- Settable at spawn time via `factory.create()` / `collectionfactory.create()` +- Readable/writable externally via `go.get()` / `go.set()` / `go.animate()` + +### Supported property types + +| Lua constructor | Property type | Example declaration | Override type in `.go` | +|---|---|---|---| +| `number` literal | number | `go.property("speed", 200)` | `PROPERTY_TYPE_NUMBER` | +| `boolean` literal | boolean | `go.property("active", true)` | `PROPERTY_TYPE_BOOLEAN` | +| `hash("...")` | hash | `go.property("type", hash("enemy"))` | `PROPERTY_TYPE_HASH` | +| `msg.url()` | URL | `go.property("target", msg.url())` | `PROPERTY_TYPE_URL` | +| `vmath.vector3()` | vector3 | `go.property("dir", vmath.vector3(1, 0, 0))` | `PROPERTY_TYPE_VECTOR3` | +| `vmath.vector4()` | vector4 | `go.property("color", vmath.vector4(1, 1, 1, 1))` | `PROPERTY_TYPE_VECTOR4` | +| `vmath.quat()` | quaternion | `go.property("rot", vmath.quat())` | `PROPERTY_TYPE_QUAT` | +| `resource.atlas()` | resource | `go.property("my_atlas", resource.atlas("/main.atlas"))` | — | +| `resource.font()` | resource | `go.property("my_font", resource.font("/main.font"))` | — | +| `resource.material()` | resource | `go.property("my_mat", resource.material("/mat.material"))` | — | +| `resource.texture()` | resource | `go.property("my_tex", resource.texture("/tex.png"))` | — | +| `resource.tile_source()` | resource | `go.property("my_ts", resource.tile_source("/main.tilesource"))` | — | + +### Unsupported types + +- **`string`** — use `hash` instead +- **`table`** / **`array`** — not supported +- **`integer`** — use `number` (Lua has no integer type) +- **`nil`** — not a valid default + +### No expression evaluation + +`go.property()` default values are parsed statically at build time. **Expressions are not evaluated**. Only literal values and constructor calls are allowed. + +```lua +-- CORRECT — literal values only +go.property("frame_time", 0.00833333) +go.property("half_pi", 1.5707963) +go.property("max_count", 10) + +-- WRONG — expressions are NOT evaluated +go.property("frame_time", 1 / 120) -- will NOT work +go.property("half_pi", math.pi / 2) -- will NOT work +go.property("max_count", 5 + 5) -- will NOT work +go.property("name", "enemy") -- will NOT work (string not supported) +``` + +### Resource properties + +Resource properties allow swapping assets per-instance in the editor. They show up as file browser fields in the Properties panel. + +```lua +go.property("my_atlas", resource.atlas("/gfx/main.atlas")) +go.property("my_font", resource.font("/fonts/main.font")) +go.property("my_material", resource.material("/materials/sprite.material")) +go.property("my_texture", resource.texture("/textures/bg.png")) +go.property("my_tile_source", resource.tile_source("/tiles/main.tilesource")) + +function init(self) + go.set("#sprite", "image", self.my_atlas) + go.set("#label", "font", self.my_font) + go.set("#sprite", "material", self.my_material) + go.set("#model", "texture0", self.my_texture) + go.set("#tilemap", "tile_source", self.my_tile_source) +end +``` + +## Physics caveats + +- **Raycast from inside an object**: `physics.raycast()` and `physics.raycast_async()` ignore any collision object that contains the ray's starting point. If the ray originates from within the object's own collision shape, that object will never appear in the results — no special filtering is needed. +- **Triggers vs raycasts**: Rays only intersect with dynamic, kinematic, and static collision objects. Trigger objects are invisible to raycasts. + +## Property overrides in .go files + +When a `.script` is referenced in a `.go` file, its `go.property` values can be overridden using `PropertyDesc` entries: + +```protobuf +components { + id: "script" + component: "/main/enemy.script" + properties { + id: "speed" + value: "200.0" + type: PROPERTY_TYPE_NUMBER + } + properties { + id: "type" + value: "boss" + type: PROPERTY_TYPE_HASH + } +} +``` + +### Override value formats by type + +| Property type | value format | Example | +|---|---|---| +| `PROPERTY_TYPE_NUMBER` | decimal string | `"200.0"` | +| `PROPERTY_TYPE_BOOLEAN` | `"true"` or `"false"` | `"true"` | +| `PROPERTY_TYPE_HASH` | bare string (hashed automatically) | `"enemy"` | +| `PROPERTY_TYPE_URL` | URL string | `"/level/spawner#script"` | +| `PROPERTY_TYPE_VECTOR3` | `"x, y, z"` | `"1.0, 0.0, 0.0"` | +| `PROPERTY_TYPE_VECTOR4` | `"x, y, z, w"` | `"1.0, 1.0, 1.0, 1.0"` | +| `PROPERTY_TYPE_QUAT` | `"x, y, z, w"` | `"0.0, 0.0, 0.0, 1.0"` | + +### Override priority + +1. Script default (`go.property("speed", 100)`) — lowest +2. `.go` file override (`PropertyDesc` in `ComponentDesc`) — medium +3. `.collection` file override (`ComponentPropertyDesc` in `InstanceDesc` / `EmbeddedInstanceDesc`) — high +4. `factory.create()` / `collectionfactory.create()` properties table — highest + diff --git a/.agents/skills/defold-shaders-editing/SKILL.md b/.agents/skills/defold-shaders-editing/SKILL.md new file mode 100644 index 0000000..36e65e9 --- /dev/null +++ b/.agents/skills/defold-shaders-editing/SKILL.md @@ -0,0 +1,366 @@ +--- +name: defold-shaders-editing +description: "Creates and edits Defold shader files (.vp, .fp, .glsl). Use when asked to create, modify, or configure any Defold vertex shader, fragment shader, or GLSL include file." +--- + +# Editing Defold Shaders + +Creates and edits Defold shader files: vertex programs (`.vp`), fragment programs (`.fp`), and GLSL include snippets (`.glsl`). + +## When to use + +This skill covers GLSL shader files used in Defold's rendering pipeline. It does NOT cover `.material` files — for those, use the `defold-proto-file-editing` skill (see `references/material.md`). + +## Shader pipeline and GLSL version + +Defold supports two shader pipelines: + +1. **Legacy pipeline** — shaders written in OpenGL ES 2.0 compatible GLSL (no `#version` directive). **Deprecated since Defold 1.9.2.** +2. **Modern pipeline** — shaders written in SPIR-V compatible GLSL with `#version 140` or higher. **This is the current standard.** + +**Always write shaders using `#version 140`** (OpenGL 3.1) at the top of the file. This directive selects the modern pipeline during the build process. If no `#version` is found, Defold falls back to the legacy pipeline. + +```glsl +#version 140 +``` + +### Cross-compilation and platform targets + +Defold compiles shaders for multiple graphics APIs from a single GLSL source: + +- **OpenGL 3.x / 4.x** (desktop: Windows, macOS, Linux) +- **OpenGL ES 2.0 / 3.0** (mobile: Android, iOS) +- **WebGL 1.0 / 2.0** (HTML5) +- **SPIR-V** (Vulkan on Android, desktop) +- **Metal** (iOS, macOS — via SPIR-V cross-compilation) + +Because of this cross-compilation, **not all GLSL features are available everywhere**. Some functions (e.g., `dFdx`, `dFdy`, `fwidth`) require extensions on ES 2.0 / WebGL 1.0 targets but are built-in on ES 3.0+ / desktop GL. Use `#extension` and preprocessor guards for conditional features: + +```glsl +#ifdef GL_OES_standard_derivatives +#extension GL_OES_standard_derivatives : enable +#endif + +#if !defined(GL_ES) || __VERSION__ >= 300 || defined(GL_OES_standard_derivatives) + // Use derivative functions +#else + // Provide fallback +#endif +``` + +**Key limitations to keep in mind:** +- Dynamic loops with variable bounds may not work on ES 2.0 / WebGL 1.0 +- Integer operations are limited on ES 2.0 +- `sampler2DArray` requires ES 3.0+ / WebGL 2.0+ +- Storage buffers and compute shaders require Vulkan / Metal + +## File types + +### Vertex program (`.vp`) + +Runs once per vertex. Transforms vertex positions from model/world space to screen space. Outputs `gl_Position` and passes data to the fragment shader via `out` variables. + +### Fragment program (`.fp`) + +Runs once per fragment (pixel). Computes the final color. Outputs to a user-defined `out vec4` variable (not `gl_FragColor`, which is deprecated in `#version 140`). + +### GLSL include snippet (`.glsl`) + +Reusable GLSL code included by `.vp` or `.fp` files via `#include`. Does not run standalone. Use header guards to prevent double-inclusion. + +## Modern GLSL syntax rules (#version 140) + +### Attributes → `in` + +In vertex shaders, use `in` instead of `attribute`: + +```glsl +in highp vec4 position; +in mediump vec2 texcoord0; +``` + +Fragment shaders do NOT have vertex attributes. + +### Varyings → `out` / `in` + +In vertex shaders, use `out` instead of `varying`. In fragment shaders, use `in`: + +```glsl +// vertex shader +out mediump vec2 var_texcoord0; + +// fragment shader +in mediump vec2 var_texcoord0; +``` + +### Fragment output → `out vec4` + +Use a declared `out` variable instead of the deprecated `gl_FragColor`: + +```glsl +out vec4 out_fragColor; + +void main() +{ + out_fragColor = vec4(1.0, 0.0, 0.0, 1.0); +} +``` + +### Uniform blocks + +Non-opaque uniforms (matrices, vectors, floats) must be placed in a **uniform block**. Use `vs_uniforms` for vertex shaders and `fs_uniforms` for fragment shaders (by convention): + +```glsl +uniform vs_uniforms +{ + highp mat4 view_proj; +}; +``` + +Opaque uniforms (samplers, images) remain standalone: + +```glsl +uniform mediump sampler2D texture_sampler; +``` + +Members of the uniform block are accessed directly by name (no block prefix): + +```glsl +gl_Position = view_proj * vec4(position.xyz, 1.0); +``` + +### Texture sampling → `texture()` + +Use `texture()` instead of the deprecated `texture2D()` / `texture2DArray()`: + +```glsl +vec4 color = texture(texture_sampler, var_texcoord0.xy); +``` + +### Precision qualifiers + +Explicit precision (`lowp`, `mediump`, `highp`) is **optional** in `#version 140` — the pipeline sets precision automatically for platforms that need it. However, you may still use them for clarity or to match existing code style in the project. Follow the convention of surrounding files. + +## Editor-specific code (`EDITOR` define) + +When shaders are rendered in the Defold Editor viewport, the preprocessor define `EDITOR` is available. Use `#ifdef EDITOR` to write code that behaves differently in the editor vs the game: + +```glsl +#ifdef EDITOR + // Simplified rendering for editor preview + out_fragColor = texture(texture_sampler, var_texcoord0.xy); +#else + // Full rendering with effects for the game + out_fragColor = apply_effects(texture(texture_sampler, var_texcoord0.xy)); +#endif +``` + +Common use cases: +- Disable expensive effects (RGSS, post-processing) in editor for performance +- Show debug/fallback visuals for materials that don't preview well +- Skip features that depend on runtime-only data (e.g., skinning) + +## Including snippets (`#include`) + +Shader files can include `.glsl` snippets using `#include` with project-relative or file-relative paths: + +```glsl +// Absolute (project-relative) path +#include "/main/my-snippet.glsl" + +// Relative to current file +#include "my-snippet.glsl" +#include "../shared/utils.glsl" +``` + +**Rules:** +- Only `.glsl` files can be included +- Paths must be within the project (or library dependencies) +- Absolute paths start with `/` +- `#include` cannot be used inline within a statement + +### Header guards + +Use header guards in `.glsl` snippets to prevent double-inclusion: + +```glsl +#ifndef MY_SNIPPET_GLSL +#define MY_SNIPPET_GLSL + +// ... snippet code ... + +#endif // MY_SNIPPET_GLSL +``` + +## Relationship with materials + +Shaders and materials (`.material`) are tightly coupled. The material file defines **what data** the shader receives. For material file editing, use the `defold-proto-file-editing` skill (reference: `references/material.md`). + +### Constants (uniforms) + +Constants declared in the material's `vertex_constants` / `fragment_constants` become uniform variables in the shader. Place them inside the uniform block: + +| Material constant type | Shader uniform type | Description | +|------------------------|---------------------|-------------| +| `CONSTANT_TYPE_VIEWPROJ` | `mat4` | Combined view × projection matrix | +| `CONSTANT_TYPE_WORLD` | `mat4` | World transform matrix | +| `CONSTANT_TYPE_VIEW` | `mat4` | View (camera) matrix | +| `CONSTANT_TYPE_PROJECTION` | `mat4` | Projection matrix | +| `CONSTANT_TYPE_WORLDVIEW` | `mat4` | World × view matrix | +| `CONSTANT_TYPE_WORLDVIEWPROJ` | `mat4` | World × view × projection matrix | +| `CONSTANT_TYPE_NORMAL` | `mat4` | Normal matrix — `transpose(inverse(view * world))`. **Produces view-space normals, not world-space.** | +| `CONSTANT_TYPE_USER` | `vec4` | Custom data, mutable via `go.set()` / `go.animate()` | +| `CONSTANT_TYPE_USER_MATRIX4` | `mat4` | Custom matrix data, mutable via `go.set()` | + +The `name` in the material must match the variable name in the uniform block. + +### Samplers + +Samplers declared in the material's `samplers` section become `sampler2D` uniforms. The sampler `name` in the material must match the uniform name in the shader: + +```glsl +uniform mediump sampler2D texture_sampler; // Matches samplers { name: "texture_sampler" ... } +``` + +For sprites, tilemaps, GUI, and particles — the first `sampler2D` is automatically bound to the component's image. + +### Vertex attributes + +Attributes declared in the material's `attributes` section (or default attributes provided by the engine) become `in` variables in the vertex shader. + +**Default attributes by component type:** + +| Component | Attributes | +|-----------|------------| +| Sprite | `position`, `texcoord0` | +| Tilemap | `position`, `texcoord0` | +| GUI node | `position`, `texcoord0`, `color` | +| ParticleFX | `position`, `texcoord0`, `color` | +| Model | `position`, `texcoord0`, `normal` | +| Font | `position`, `texcoord0`, `face_color`, `outline_color`, `shadow_color` | + +### Vertex space + +The material's `vertex_space` setting affects how position data arrives: + +- `VERTEX_SPACE_WORLD` (default) — positions are pre-transformed to world space. Used for 2D components (sprites, tilemaps). Use `view_proj` (or `CONSTANT_TYPE_VIEWPROJ`) to go directly to screen space. +- `VERTEX_SPACE_LOCAL` — positions are in local/object space. Used for 3D models. You must transform through `world → view → projection` in the shader. + +### Instancing + +For instanced rendering (Model components), declare `mtx_world` and `mtx_normal` as `in` attributes (not uniforms): + +```glsl +in mediump mat4 mtx_world; +in mediump mat4 mtx_normal; +``` + +The material must have `vertex_space: VERTEX_SPACE_LOCAL`. These attributes are automatically configured for per-instance step function. + +### Normal matrix (`mtx_normal`) — view-space vs world-space + +The built-in `CONSTANT_TYPE_NORMAL` computes `transpose(inverse(view * world))` on the CPU. This produces normals in **view-space** (camera-space), not world-space. + +**View-space normals are fine when:** +- All lighting calculations are done in view-space +- Camera position is implicitly at origin `(0,0,0)` (simplifies specular) + +**World-space normals are needed for:** +- Cubemap reflections, environment mapping +- World-space lighting, world-space effects + +To get world-space normals, compute the normal matrix from `mtx_world` in the vertex shader using the adjugate matrix trick (cheaper than full `inverse()` — 3 cross products instead of cofactor expansion): + +```glsl +// transpose(adjugate(M)) for upper-left 3x3 of mat4. +// Equivalent to transpose(inverse(M)) up to a uniform scale factor, +// which is eliminated by normalize(). +mat3 adjoint(mat4 m) +{ + return mat3( + cross(m[1].xyz, m[2].xyz), + cross(m[2].xyz, m[0].xyz), + cross(m[0].xyz, m[1].xyz) + ); +} +``` + +Usage in vertex shader: + +```glsl +var_world_normal = normalize(adjoint(mtx_world) * normal.xyz); +``` + +| Goal | Method | +|------|--------| +| View-space normals | Use built-in `mtx_normal` (`CONSTANT_TYPE_NORMAL`) | +| World-space normals | Compute `normalize(adjoint(mtx_world) * normal.xyz)` in shader | + +**Key rule:** Never mix coordinate spaces — if light direction is in world-space, normals must also be in world-space. + +## Canonical examples + +### Builtin reference shaders + +Use `.deps/builtins/materials/` as reference for standard shaders: + +- `sprite.vp` / `sprite.fp` — 2D sprite (world space, `view_proj`, `tint`) +- `model.vp` / `model.fp` — 3D model with lighting (local space, `mtx_worldview`, `mtx_normal`, `light`) +- `model_instanced.vp` — 3D model with instancing (`mtx_world`, `mtx_normal` as `in` attributes) +- `gui.vp` / `gui.fp` — GUI nodes (world space, `color` attribute, premultiplied alpha) +- `particlefx.vp` / `particlefx.fp` — Particle effects +- `tile_map.vp` / `tile_map.fp` — Tilemaps +- `skinning.glsl` — Skeletal animation include (uses `#ifdef EDITOR` for fallback) + +### GLSL include snippet (with header guards and extension) + +```glsl +#ifndef MY_UTILS_GLSL +#define MY_UTILS_GLSL + +#ifdef GL_OES_standard_derivatives +#extension GL_OES_standard_derivatives : enable +#endif + +// Utility function available on platforms that support derivatives +#if !defined(GL_ES) || __VERSION__ >= 300 || defined(GL_OES_standard_derivatives) +mediump float edge_smoothing(mediump float dist) +{ + return smoothstep(0.0, fwidth(dist), dist); +} +#else +mediump float edge_smoothing(mediump float dist) +{ + return step(0.0, dist); +} +#endif + +#endif // MY_UTILS_GLSL +``` + +## Workflow + +### Creating a new shader pair (.vp + .fp) + +1. Determine the component type (sprite, model, GUI, etc.) to know which attributes and vertex space to use. +2. Check the corresponding `.material` file (or plan one) to know which constants, samplers, and attributes the shader will receive. Use the `defold-proto-file-editing` skill for material editing. +3. Start both files with `#version 140`. +4. In the `.vp`: declare `in` attributes, `out` varyings, uniform block with constants, and compute `gl_Position`. +5. In the `.fp`: declare `in` varyings, `out vec4` for color output, samplers, uniform block, and compute the output color. +6. Ensure uniform names match the material's constant names exactly. +7. Ensure sampler names match the material's sampler names exactly. + +### Creating a GLSL include snippet + +1. Create a `.glsl` file. +2. Add header guards (`#ifndef` / `#define` / `#endif`). +3. Add `#extension` directives with preprocessor guards if using features not available on all targets. +4. Write reusable functions or constants. + +### Editing an existing shader + +1. Read the current shader file and its corresponding `.material` file. +2. Modify only the requested parts. +3. Keep the existing code style (precision qualifiers, naming, spacing). +4. Ensure any new uniforms are also added to the material file (use `defold-proto-file-editing` skill). +5. Ensure any removed uniforms are also removed from the material file. diff --git a/.agents/skills/defold-skill-maintain/SKILL.md b/.agents/skills/defold-skill-maintain/SKILL.md new file mode 100644 index 0000000..b983fc2 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/SKILL.md @@ -0,0 +1,49 @@ +--- +name: defold-skill-maintain +description: "Maintains Defold agent skills. Use when asked to update link lists in api-fetch/docs-fetch/examples-fetch skills, create or update proto file references, or fetch proto schemas." +--- + +# Defold Skill Maintenance + +Handles maintenance tasks for other Defold skills: updating link indexes and managing proto file references. + +Read guide first about the best practices for Agent Skills: `references/The-Complete-Guide-to-Building-Skill-for-Claude.md`. + +## Capability 1: Updating link lists in fetch skills + +The `defold-api-fetch`, `defold-docs-fetch`, and `defold-examples-fetch` skills contain hardcoded link tables. These tables should be kept in sync with the official Defold index files. + +### Index sources + +- **API index**: `https://defold.com/llms/apis.md` — links to per-namespace API Markdown files +- **Manuals index**: `https://defold.com/llms/manuals.md` — links to per-manual Markdown files +- **Examples index**: `https://defold.com/llms/examples.md` — links to per-example Markdown files + +### Procedure: update link lists + +1. Fetch the relevant index page(s) by downloading the URL content. +2. Parse the Markdown content. Each index page contains links in the form `[Title](url)` grouped by sections. +3. Compare parsed links against the current SKILL.md of the target skill. +4. Update the SKILL.md link tables to match the index, preserving the table structure and the `## Usage` footer section. + +### Target skill files + +- `.agents/skills/defold-api-fetch/SKILL.md` — updated from `apis.md` +- `.agents/skills/defold-docs-fetch/SKILL.md` — updated from `manuals.md` +- `.agents/skills/defold-examples-fetch/SKILL.md` — updated from `examples.md` + +### Rules + +- Keep the YAML frontmatter (`---` block) unchanged. +- Keep the intro line `Fetch documentation from the links below (the URLs point to plain Markdown files).` unchanged. +- Keep the `## Usage` section at the bottom unchanged. +- Replace the link tables between frontmatter and Usage section with the parsed content from the index. +- Preserve section grouping and table formatting style that already exists in each skill. + +## Capability 2: Managing proto file references + +For detailed instructions on creating, updating, and maintaining proto file references for the `defold-proto-file-editing` skill, see `references/proto-reference-guide.md`. + +## Scripts + +- `scripts/fetch_proto.py` — downloads proto schemas from the stable Defold SDK into `.agents/skills/defold-skill-maintain/assets/proto/`. Run when proto schemas are missing or need updating: `python .agents/skills/defold-skill-maintain/scripts/fetch_proto.py` diff --git a/.agents/skills/defold-skill-maintain/assets/proto/ddf/ddf_extensions.proto b/.agents/skills/defold-skill-maintain/assets/proto/ddf/ddf_extensions.proto new file mode 100644 index 0000000..f906cf0 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/ddf/ddf_extensions.proto @@ -0,0 +1,30 @@ +syntax = "proto2"; + +import "google/protobuf/descriptor.proto"; + +option java_package = "com.dynamo.proto"; +option java_outer_classname = "DdfExtensions"; + +extend google.protobuf.MessageOptions +{ + optional string alias = 50000; + optional bool struct_align = 50003; +} + +extend google.protobuf.FieldOptions +{ + optional bool resource = 50100; + optional bool runtime_only = 50101; + optional bool field_align = 50004; +} + +extend google.protobuf.EnumValueOptions +{ + optional string displayName = 50200; +} + +extend google.protobuf.FileOptions +{ + optional string ddf_namespace = 50001; + optional string ddf_includes = 50002; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/ddf/ddf_math.proto b/.agents/skills/defold-skill-maintain/assets/proto/ddf/ddf_math.proto new file mode 100644 index 0000000..8efad17 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/ddf/ddf_math.proto @@ -0,0 +1,111 @@ +syntax = "proto2"; + +package dmMath; + +import "ddf/ddf_extensions.proto"; + +option java_package = "com.dynamo.proto"; +option java_outer_classname = "DdfMath"; + +option (ddf_includes) = "dmsdk/dlib/vmath.h dmsdk/dlib/transform.h"; + +message Point3 +{ + option (alias) = "dmVMath::Point3"; + + optional float x = 1 [default = 0.0]; + optional float y = 2 [default = 0.0]; + optional float z = 3 [default = 0.0]; + optional float d = 4 [default = 0.0]; +} + +message Vector3 +{ + option (alias) = "dmVMath::Vector3"; + + optional float x = 1 [default = 0.0]; + optional float y = 2 [default = 0.0]; + optional float z = 3 [default = 0.0]; + optional float d = 4 [default = 0.0]; +} + +message Vector3One +{ + option (alias) = "dmVMath::Vector3"; + + optional float x = 1 [default = 1.0]; + optional float y = 2 [default = 1.0]; + optional float z = 3 [default = 1.0]; + optional float d = 4 [default = 1.0]; +} + +message Vector4 +{ + option (alias) = "dmVMath::Vector4"; + + optional float x = 1 [default = 0.0]; + optional float y = 2 [default = 0.0]; + optional float z = 3 [default = 0.0]; + optional float w = 4 [default = 0.0]; +} + +message Vector4One +{ + option (alias) = "dmVMath::Vector4"; + + optional float x = 1 [default = 1.0]; + optional float y = 2 [default = 1.0]; + optional float z = 3 [default = 1.0]; + optional float w = 4 [default = 1.0]; +} + +message Vector4WOne +{ + option (alias) = "dmVMath::Vector4"; + + optional float x = 1 [default = 0.0]; + optional float y = 2 [default = 0.0]; + optional float z = 3 [default = 0.0]; + optional float w = 4 [default = 1.0]; +} + +message Quat +{ + option (alias) = "dmVMath::Quat"; + + optional float x = 1 [default = 0.0]; + optional float y = 2 [default = 0.0]; + optional float z = 3 [default = 0.0]; + optional float w = 4 [default = 1.0]; +} + +message Transform +{ + option (alias) = "dmTransform::Transform"; + + optional Quat rotation = 1; + optional Vector3 translation = 2; + optional Vector3 scale = 3; +} + +message Matrix4 +{ + option (alias) = "dmVMath::Matrix4"; + + optional float m00 = 1 [default = 1.0]; + optional float m01 = 2 [default = 0.0]; + optional float m02 = 3 [default = 0.0]; + optional float m03 = 4 [default = 0.0]; + optional float m10 = 5 [default = 0.0]; + optional float m11 = 6 [default = 1.0]; + optional float m12 = 7 [default = 0.0]; + optional float m13 = 8 [default = 0.0]; + optional float m20 = 9 [default = 0.0]; + optional float m21 = 10 [default = 0.0]; + optional float m22 = 11 [default = 1.0]; + optional float m23 = 12 [default = 0.0]; + optional float m30 = 13 [default = 0.0]; + optional float m31 = 14 [default = 0.0]; + optional float m32 = 15 [default = 0.0]; + optional float m33 = 16 [default = 1.0]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/engine/engine_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/engine/engine_ddf.proto new file mode 100644 index 0000000..be5d660 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/engine/engine_ddf.proto @@ -0,0 +1,27 @@ +syntax = "proto2"; +package dmEngineDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +import "gameobject/lua_ddf.proto"; + +option java_package = "com.dynamo.engine.proto"; +option java_outer_classname = "Engine"; + +// (Hidden) iconifies the application +/* This command asks the window manager to minimize/iconify the application + * + * This message can only be sent to the designated `@system` socket. + */ +message HideApp {} + +// (Hidden) run a Lua script +/* Runs a Lua script on the initialized Lua contexts + * + * This message can only be sent to the designated `@system` socket. + */ +message RunScript +{ + required dmLuaDDF.LuaModule module = 1; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gameobject/gameobject_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gameobject/gameobject_ddf.proto new file mode 100644 index 0000000..9630f18 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gameobject/gameobject_ddf.proto @@ -0,0 +1,304 @@ +syntax = "proto2"; +package dmGameObjectDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +import "gameobject/properties_ddf.proto"; + +option java_package = "com.dynamo.gameobject.proto"; +option java_outer_classname = "GameObject"; + +/* This must match with enum PropertyType in gameobject.h */ +enum PropertyType +{ + PROPERTY_TYPE_NUMBER = 0; + PROPERTY_TYPE_HASH = 1; + PROPERTY_TYPE_URL = 2; + PROPERTY_TYPE_VECTOR3 = 3; + PROPERTY_TYPE_VECTOR4 = 4; + PROPERTY_TYPE_QUAT = 5; + PROPERTY_TYPE_BOOLEAN = 6; + PROPERTY_TYPE_MATRIX4 = 7; + PROPERTY_TYPE_COUNT = 8; +} + +message PropertyDesc +{ + required string id = 1; + required string value = 2; + required PropertyType type = 3; +} + +message ComponentDesc +{ + required string id = 1; + required string component = 2 [(resource) = true]; + optional dmMath.Point3 position = 3; + optional dmMath.Quat rotation = 4; + repeated PropertyDesc properties = 5; + optional dmPropertiesDDF.PropertyDeclarations property_decls = 6 [(runtime_only) = true]; + optional dmMath.Vector3One scale = 7; +} + +message EmbeddedComponentDesc +{ + required string id = 1; + required string type = 2; + required string data = 3; + optional dmMath.Point3 position = 4; + optional dmMath.Quat rotation = 5; + optional dmMath.Vector3One scale = 6; +} + +message PrototypeDesc +{ + repeated ComponentDesc components = 1; + repeated EmbeddedComponentDesc embedded_components = 2; + repeated string property_resources = 3 [(runtime_only) = true, (resource) = true]; +} + +message ComponentPropertyDesc +{ + required string id = 1; + repeated PropertyDesc properties = 2; + optional dmPropertiesDDF.PropertyDeclarations property_decls = 3 [(runtime_only) = true]; +} + +message ComponenTypeDesc +{ + required uint64 name_hash = 1; + required uint32 max_count = 2; +} + +message InstanceDesc +{ + required string id = 1; + required string prototype = 2 [(resource) = true]; + repeated string children = 3; + optional dmMath.Point3 position = 4; + optional dmMath.Quat rotation = 5; + repeated ComponentPropertyDesc component_properties = 6; + optional float scale = 7 [default = 1.0]; + optional dmMath.Vector3One scale3 = 8; +} + +message EmbeddedInstanceDesc +{ + required string id = 1; + repeated string children = 2; + required string data = 3; + optional dmMath.Point3 position = 4; + optional dmMath.Quat rotation = 5; + repeated ComponentPropertyDesc component_properties = 6; + optional float scale = 7 [default = 1.0]; + optional dmMath.Vector3One scale3 = 8; +} + +message InstancePropertyDesc +{ + required string id = 1; + repeated ComponentPropertyDesc properties = 2; +} + +message CollectionInstanceDesc +{ + required string id = 1; + required string collection = 2 [(resource) = true]; + optional dmMath.Point3 position = 3; + optional dmMath.Quat rotation = 4; + optional float scale = 5 [default = 1.0]; + optional dmMath.Vector3One scale3 = 7; + repeated InstancePropertyDesc instance_properties = 6; +} + +message CollectionDesc +{ + required string name = 1; + repeated InstanceDesc instances = 2; + repeated CollectionInstanceDesc collection_instances = 3; + optional uint32 scale_along_z = 4 [default = 0]; // Deprecated + repeated EmbeddedInstanceDesc embedded_instances = 5; + repeated string property_resources = 6 [(runtime_only) = true, (resource) = true]; + repeated ComponenTypeDesc component_types = 7 [(runtime_only) = true]; +} + +/*# Game object API documentation + * + * @document + * @name Game object + * @namespace go + * @language Lua + */ + +/*# acquires the user input focus + * + * Post this message to a game object instance to make that instance acquire the user input focus. + * + * User input is distributed by the engine to every instance that has + * requested it. The last instance to request focus will receive it first. + * This means that the scripts in the instance will have first-hand-chance + * at reacting on user input, possibly consuming it (by returning + * true from on_input) so that no other instances + * can react on it. The most common case is for a script to send this message + * to itself when it needs to respond to user input. + * + * A script belonging to an instance which has the user input focus will + * receive the input actions in its on_input callback function. + * See [ref:on_input] for more information on how user input can be + * handled. + * + * @message + * @name acquire_input_focus + * @examples + * + * This example demonstrates how to acquire and act on user input. + * + * ```lua + * function init(self) + * -- acquire input focus as soon as the instance has been initialized + * msg.post(".", "acquire_input_focus") + * end + * + * function on_input(self, action_id, action) + * -- check which input we received + * if action_id == hash("my_action") then + * -- act on the input + * self.my_action_amount = action.value + * end + * end + * ``` + */ +message AcquireInputFocus {} + +/*# releases the user input focus + * Post this message to an instance to make that instance release the user input focus. + * See [ref:acquire_input_focus] for more information on how the user input handling + * works. + * + * @message + * @name release_input_focus + * @examples + * How to make a game object stop receiving input: + * + * ```lua + * msg.post(".", "release_input_focus") + * ``` + */ +message ReleaseInputFocus {} + +/*# sets the parent of the receiving instance + * When this message is sent to an instance, it sets the parent of that instance. This means that the instance will exist + * in the geometrical space of its parent, like a basic transformation hierarchy or scene graph. If no parent is specified, + * the instance will be detached from any parent and exist in world space. A script can send this message to itself to set + * the parent of its instance. + * + * @message + * @name set_parent + * @param parent_id [type:hash] the id of the new parent + * @param keep_world_transform [type:number] if the world transform of the instance should be preserved when changing spaces, 0 for false and 1 for true. The default value is 1. + * @examples + * + * Attach myself to another instance "my_parent": + * + * ```lua + * msg.post(".", "set_parent", {parent_id = go.get_id("my_parent")}) + * ``` + * + * Attach an instance "my_instance" to another instance "my_parent": + * + * ```lua + * msg.post("my_instance", "set_parent", {parent_id = go.get_id("my_parent")}) + * ``` + * + * Detach an instance "my_instance" from its parent (if any): + * + * ```lua + * msg.post("my_instance", "set_parent") + * ``` + */ +message SetParent +{ + optional uint64 parent_id = 1 [default = 0]; + optional uint32 keep_world_transform = 2 [default = 1]; +} + +/*# enables the receiving component + * + * This message enables the receiving component. All components are enabled by default, which means they will receive input, updates + * and be a part of the simulation. A component is disabled when it receives the disable message. + * + * [icon:alert] Components that currently supports this message are: + * + * - Camera + * - Collection Proxy + * - Collision Object + * - Gui + * - Label + * - Spine Model + * - Sprite + * - Tile Grid + * - Model + * - Mesh + * + * @message + * @name enable + * @examples + * + * Enable the component "my_component": + * + * ```lua + * msg.post("#my_component", "enable") + * ``` + */ +message Enable +{ +} + +/*# disables the receiving component + * + * This message disables the receiving component. All components are enabled by default, which means they will receive input, updates + * and be a part of the simulation. A component is disabled when it receives the disable message. + * + * [icon:alert] Components that currently supports this message are: + * + * - Camera + * - Collection Proxy + * - Collision Object + * - Gui + * - Label + * - Spine Model + * - Sprite + * - Tile Grid + * - Model + * - Mesh + * + * @message + * @name disable + * @examples + * + * Disable the component "my_component": + * + * ```lua + * msg.post("#my_component", "disable") + * ``` + */ +message Disable +{ +} + + +// Internal engine script message wrapper for added typesafety +message ScriptMessage +{ + required uint64 descriptor_hash = 1; // The descriptor name hash of the message + required uint32 payload_size = 2; // The payload ddf message. The payload will begin directly after this message + optional uint32 function = 3; // If 0, it will call the "on_message" function + optional bool unref_function = 4; // unreference function after call +} + +// Internal engine message for removing script references (typically used for callbacks) +message ScriptUnrefMessage +{ + required uint32 reference = 1; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gameobject/lua_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gameobject/lua_ddf.proto new file mode 100644 index 0000000..86a0bcc --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gameobject/lua_ddf.proto @@ -0,0 +1,25 @@ +syntax = "proto2"; +package dmLuaDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +import "script/lua_source_ddf.proto"; + +import "gameobject/properties_ddf.proto"; + +option java_package = "com.dynamo.lua.proto"; +option java_outer_classname = "Lua"; + +message LuaModule +{ + // For now provide both script and bytecode to support the engine build option + // of compiling with vanilla lua. + required LuaSource source = 1; + + // NOTE: The following arrays must be equal in size + repeated string modules = 2; // required modules + repeated string resources = 3 [(resource)=true]; // required resources. same as modules but on the form /x/y/z.luac instead of x.y.z + optional dmPropertiesDDF.PropertyDeclarations properties = 4; + repeated string property_resources = 5 [(resource)=true]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gameobject/properties_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gameobject/properties_ddf.proto new file mode 100644 index 0000000..f17585a --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gameobject/properties_ddf.proto @@ -0,0 +1,30 @@ +syntax = "proto2"; +package dmPropertiesDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.properties.proto"; +option java_outer_classname = "PropertiesProto"; + +message PropertyDeclarationEntry +{ + required string key = 1; + required uint64 id = 2; + required uint32 index = 3; + repeated uint64 element_ids = 4; +} + +message PropertyDeclarations +{ + repeated PropertyDeclarationEntry number_entries = 1; + repeated PropertyDeclarationEntry hash_entries = 2; + repeated PropertyDeclarationEntry url_entries = 3; + repeated PropertyDeclarationEntry vector3_entries = 4; + repeated PropertyDeclarationEntry vector4_entries = 5; + repeated PropertyDeclarationEntry quat_entries = 6; + repeated PropertyDeclarationEntry bool_entries = 7; + repeated float float_values = 8; + repeated uint64 hash_values = 9; + repeated string string_values = 10; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/atlas_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/atlas_ddf.proto new file mode 100644 index 0000000..31ba271 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/atlas_ddf.proto @@ -0,0 +1,42 @@ +syntax = "proto2"; +package dmGameSystemDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +import "gamesys/tile_ddf.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "AtlasProto"; + +message AtlasImage +{ + required string image = 1 [(resource) = true]; + optional SpriteTrimmingMode sprite_trim_mode = 2 [default = SPRITE_TRIM_MODE_OFF]; + optional float pivot_x = 3 [default = 0.5]; + optional float pivot_y = 4 [default = 0.5]; +} + +message AtlasAnimation +{ + required string id = 1; + repeated AtlasImage images = 2; + optional Playback playback = 3 [default = PLAYBACK_ONCE_FORWARD]; + optional uint32 fps = 4 [default = 30]; + optional uint32 flip_horizontal = 5 [default = 0]; + optional uint32 flip_vertical = 6 [default = 0]; +} + +// Editor format. Engine format is in texture_set_ddf.proto +message Atlas +{ + repeated AtlasImage images = 1; + repeated AtlasAnimation animations = 2; + optional uint32 margin = 3 [default = 0]; + optional uint32 extrude_borders = 4 [default = 0]; + optional uint32 inner_padding = 5 [default = 0]; + optional uint32 max_page_width = 6 [default = 0]; + optional uint32 max_page_height = 7 [default = 0]; + // A list of comma separated patterns to rename animations: E.g. "_nrm=,_normal=" + optional string rename_patterns = 8; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/buffer_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/buffer_ddf.proto new file mode 100644 index 0000000..4d8e5b1 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/buffer_ddf.proto @@ -0,0 +1,40 @@ +syntax = "proto2"; +package dmBufferDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "BufferProto"; + +// NOTE: Must match ValueType in dmsdk/buffer.h +enum ValueType +{ + VALUE_TYPE_UINT8 = 0; + VALUE_TYPE_UINT16 = 1; + VALUE_TYPE_UINT32 = 2; + VALUE_TYPE_UINT64 = 3; + VALUE_TYPE_INT8 = 4; + VALUE_TYPE_INT16 = 5; + VALUE_TYPE_INT32 = 6; + VALUE_TYPE_INT64 = 7; + VALUE_TYPE_FLOAT32 = 8; +} + +message StreamDesc +{ + required string name = 1; + required ValueType value_type = 2; + required uint32 value_count = 3; + repeated uint32 ui = 4; + repeated int32 i = 5; + repeated uint64 ui64 = 6; + repeated int64 i64 = 7; + repeated float f = 8; + required uint64 name_hash = 9; +} + +message BufferDesc +{ + repeated StreamDesc streams = 1; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/camera_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/camera_ddf.proto new file mode 100644 index 0000000..77cc17e --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/camera_ddf.proto @@ -0,0 +1,239 @@ +syntax = "proto2"; +package dmGamesysDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "Camera"; + +/*# Camera API documentation + * + * Messages to control camera components and camera focus. + * + * @document + * @name Camera + * @namespace camera + * @language Lua + */ + +// NOTE: Enum values must correspond to the enum values in dmRender::OrthoZoomMode +// Don't forget to change engine/render/src/render/render.h if you change here +// Orthographic camera zoom modes +enum OrthoZoomMode +{ + ORTHO_MODE_FIXED = 0[(displayName) = "Fixed"]; // Use orthographic_zoom as-is + ORTHO_MODE_AUTO_FIT = 1[(displayName) = "Auto Fit"]; // Fit original display area (fit) + ORTHO_MODE_AUTO_COVER = 2[(displayName) = "Auto Cover"]; // Fill original display area (cover) +} + +message CameraDesc +{ + required float aspect_ratio = 1; + required float fov = 2; + required float near_z = 3; + required float far_z = 4; + optional uint32 auto_aspect_ratio = 5 [default = 0]; + optional uint32 orthographic_projection = 6 [default = 0]; + optional float orthographic_zoom = 7 [default = 1.0]; + optional OrthoZoomMode orthographic_mode = 8 [default = ORTHO_MODE_FIXED]; +} + +/*# sets camera properties + *

+ * Post this message to a camera-component to set its properties at run-time. + *

+ * + * @message + * @name set_camera + * @param aspect_ratio [type:number] aspect ratio of the screen (width divided by height) + * @param fov [type:number] field of view of the lens, measured as the angle in radians between the right and left edge + * @param near_z [type:number] position of the near clipping plane (distance from camera along relative z) + * @param far_z [type:number] position of the far clipping plane (distance from camera along relative z) + * @param orthographic_projection [type:boolean] set to use an orthographic projection + * @param orthographic_zoom [type:number] zoom level when the camera is using an orthographic projection + * @param orthographic_mode [type:number] orthographic zoom behavior when orthographic_projection is enabled + * @examples + * + * In the examples, it is assumed that the instance of the script has a camera-component with id "camera". + * + * ```lua + * msg.post("#camera", "set_camera", {aspect_ratio = 16/9, fov = math.pi * 0.5, near_z = 0.1, far_z = 500}) + * ``` + */ +message SetCamera +{ + required float aspect_ratio = 1; + required float fov = 2; + required float near_z = 3; + required float far_z = 4; + optional uint32 orthographic_projection = 5 [default = 0]; + optional float orthographic_zoom = 6 [default = 1.0]; + optional OrthoZoomMode orthographic_mode = 7 [default = ORTHO_MODE_FIXED]; +} + +/** DEPRECATED! makes the receiving camera become the active camera + * + * Post this message to a camera-component to activate it. + * + * Several cameras can be active at the same time, but only the camera that was last activated will be used for rendering. + * When the camera is deactivated (see release_camera_focus), the previously activated camera will again be used for rendering automatically. + * + * The reason it is called "camera focus" is the similarity to how acquiring input focus works (see acquire_input_focus). + * + * @message + * @name acquire_camera_focus + * @examples + * + * In the examples, it is assumed that the instance of the script has a camera-component with id "camera". + * + * ```lua + * msg.post("#camera", "acquire_camera_focus") + * ``` + */ +message AcquireCameraFocus {} + +/** DEPRECATED! deactivates the receiving camera + *

+ * Post this message to a camera-component to deactivate it. The camera is then removed from the active cameras. + * See acquire_camera_focus for more information how the active cameras are used in rendering. + *

+ * + * @message + * @name release_camera_focus + * @examples + * + * In the examples, it is assumed that the instance of the script has a camera-component with id "camera". + * + * ```lua + * msg.post("#camera", "release_camera_focus") + * ``` + */ +message ReleaseCameraFocus {} + +/*# [type:float] camera fov + * + * Vertical field of view of the camera. + * The type of the property is float. + * + * @name fov + * @property + * + * @examples + * + * ```lua + * function init(self) + * local fov = go.get("#camera", "fov") + * go.set("#camera", "fov", fov + 0.1) + * go.animate("#camera", "fov", go.PLAYBACK_ONCE_PINGPONG, 1.2, go.EASING_LINEAR, 1) + * end + * ``` + */ + + /*# [type:float] camera near_z + * + * Camera frustum near plane. + * The type of the property is float. + * + * @name near_z + * @property + * + * @examples + * + * ```lua + * function init(self) + * local near_z = go.get("#camera", "near_z") + * go.set("#camera", "near_z", 10) + * end + * ``` + */ + +/*# [type:float] camera far_z + * + * Camera frustum far plane. + * The type of the property is float. + * + * @name far_z + * @property + * + * @examples + * + * ```lua + * function init(self) + * local far_z = go.get("#camera", "far_z") + * go.set("#camera", "far_z", 10) + * end + * ``` + */ + +/*# [type:float] camera orthographic_zoom + * + * Zoom level when using an orthographic projection. + * The type of the property is float. + * + * @name orthographic_zoom + * @property + * + * @examples + * + * ```lua + * function init(self) + * local orthographic_zoom = go.get("#camera", "orthographic_zoom") + * go.set("#camera", "orthographic_zoom", 2.0) + * go.animate("#camera", "orthographic_zoom", go.PLAYBACK_ONCE_PINGPONG, 0.5, go.EASING_INOUTQUAD, 2) + * end + * ``` + */ + +/*# [type:float] camera projection + * + * [mark:READ ONLY] The calculated projection matrix of the camera. + * The type of the property is matrix4. + * + * @name projection + * @property + * + * @examples + * + * ```lua + * function init(self) + * local projection = go.get("#camera", "projection") + * end + * ``` + */ + +/*# [type:float] camera view + * + * [mark:READ ONLY] The calculated view matrix of the camera. + * The type of the property is matrix4. + * + * @name view + * @property + * + * @examples + * + * ```lua + * function init(self) + * local view = go.get("#camera", "view") + * end + * ``` + */ + +/*# [type:float] camera aspect ratio + * + * The ratio between the frustum width and height. Used when calculating the + * projection of a perspective camera. + * The type of the property is number. + * + * @name aspect_ratio + * @property + * + * @examples + * + * ```lua + * function init(self) + * local aspect_ratio = go.get("#camera", "aspect_ratio") + * go.set("#camera", "aspect_ratio", 1.2) + * end + * ``` + */ diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/gamesys_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/gamesys_ddf.proto new file mode 100644 index 0000000..0a7dc1d --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/gamesys_ddf.proto @@ -0,0 +1,166 @@ +syntax = "proto2"; +package dmGameSystemDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "GameSystem"; + +message FactoryDesc +{ + required string prototype = 1 [(resource)=true]; + optional bool load_dynamically = 2 [default=false]; + optional bool dynamic_prototype = 3 [default=false]; +} + +message CollectionFactoryDesc +{ + required string prototype = 1 [(resource)=true]; + optional bool load_dynamically = 2 [default=false]; + optional bool dynamic_prototype = 3 [default=false]; +} + +message Create +{ + required dmMath.Point3 position = 1 [(field_align)=true]; + required dmMath.Quat rotation = 2 [(field_align)=true]; + optional uint64 id = 3 [default=0]; + optional float scale = 4 [default=1]; + optional dmMath.Vector3 scale3 = 5 [(field_align)=true]; // if zero, 'scale' is used instead +} + +message CollectionProxyDesc +{ + required string collection = 1 [(resource)=true]; + optional bool exclude = 2 [default=false]; +} + +enum TimeStepMode +{ + TIME_STEP_MODE_CONTINUOUS = 0; + TIME_STEP_MODE_DISCRETE = 1; +} + +/* Documented in comp_collecion_proxy.cpp */ +message SetTimeStep +{ + required float factor = 1; + required TimeStepMode mode = 2; +} + +enum LightType +{ + POINT = 0; + SPOT = 1; +} + +message LightDesc +{ + required string id = 1; + required LightType type = 2; + required float intensity = 3; + required dmMath.Vector3 color = 4; + required float range = 5; + required float decay = 6; + // Only applicable for spot-lights + optional float cone_angle = 7; + optional float penumbra_angle = 8; + optional float drop_off = 9; +} + +message SetLight +{ + required dmMath.Point3 position = 1; + required dmMath.Quat rotation = 2; + required LightDesc light = 3; +} + +message SetViewProjection +{ + required uint64 id = 1; + required dmMath.Matrix4 view = 2; + required dmMath.Matrix4 projection = 3; +} + +/* Documented in comp_sound.cpp */ +message PlaySound +{ + optional float delay = 1 [default=0.0]; + optional float gain = 2 [default=1.0]; + optional float pan = 3 [default=0.0]; + optional float speed = 4 [default=1.0]; + optional uint32 play_id = 5 [default=0xffffffff]; // Must be same as dmSound::INVALID_PLAY_ID + // Start playback from an offset. Only one of these should be set. + // If both are set (e.g. via manual message), start_frame takes precedence. + optional float start_time = 6 [default=0.0]; // seconds + optional uint32 start_frame = 7 [default=0]; // frames (samples per channel) +} + +message StopSound +{ + optional uint32 play_id = 1 [default=0xffffffff]; // Must be same as dmSound::INVALID_PLAY_ID +} + +message PauseSound +{ + optional bool pause = 1 [default=true]; +} + +message SoundEvent +{ + optional int32 play_id = 1 [default = 0]; +} + +message SetGain +{ + optional float gain = 1 [default=1.0]; +} + +message SetPan +{ + optional float pan = 1 [default=0.0]; +} + +message SetSpeed +{ + optional float speed = 1 [default=1.0]; +} + +/* Documented in scripts/script_particlefx.cpp */ +message PlayParticleFX {} +message StopParticleFX { + optional bool clear_particles = 1 [default=false]; +} +message SetConstantParticleFX +{ + required uint64 emitter_id = 1; + required uint64 name_hash = 2; + required dmMath.Matrix4 value = 3 [(field_align)=true]; + optional bool is_matrix4 = 4; +} +message ResetConstantParticleFX +{ + required uint64 emitter_id = 1; + required uint64 name_hash = 2; +} + +/* Function wrapper documented in gamesys_script.cpp */ +message SetConstant +{ + required uint64 name_hash = 1; + required dmMath.Vector4 value = 2 [(field_align)=true]; + optional int32 index = 3 [default=0]; +} + +/* Function wrapper documented in gamesys_script.cpp */ +message ResetConstant +{ + required uint64 name_hash = 1; +} + +/* Function wrapper documented in gamesys_script.cpp */ +message SetScale +{ + required dmMath.Vector3 scale = 1 [(field_align)=true]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/gui_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/gui_ddf.proto new file mode 100644 index 0000000..96975b7 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/gui_ddf.proto @@ -0,0 +1,264 @@ +syntax = "proto2"; +package dmGuiDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "Gui"; + +/*# GUI API documentation + * + * @document + * @name GUI + * @namespace gui + * @language Lua + */ + +message NodeDesc +{ + // NOTE: Enum values must correspond to the enum values in dmGui + enum Type + { + TYPE_BOX = 0 [(displayName) = "Box"]; + TYPE_TEXT = 1 [(displayName) = "Text"]; + TYPE_PIE = 2 [(displayName) = "Pie"]; + TYPE_TEMPLATE = 3 [(displayName) = "Template"]; + TYPE_SPINE = 4 [(displayName) = "Spine"]; // Deprecated + TYPE_PARTICLEFX = 5 [(displayName) = "ParticleFX"]; + TYPE_CUSTOM = 6 [(displayName) = "Custom"]; + } + + // NOTE: Enum values must correspond to the enum values in dmGui + enum BlendMode + { + BLEND_MODE_ALPHA = 0 [(displayName) = "Alpha"]; + BLEND_MODE_ADD = 1 [(displayName) = "Add"]; + BLEND_MODE_ADD_ALPHA = 2 [(displayName) = "Add Alpha (Deprecated)"]; + BLEND_MODE_MULT = 3 [(displayName) = "Multiply"]; + BLEND_MODE_SCREEN = 4 [(displayName) = "Screen"]; + } + + // NOTE: Enum values must correspond to the enum values in dmGui + enum ClippingMode + { + CLIPPING_MODE_NONE = 0 [(displayName) = "None"]; + CLIPPING_MODE_STENCIL = 2 [(displayName) = "Stencil"]; + } + + // NOTE: Enum values must correspond to the enum values in dmGui + enum XAnchor + { + XANCHOR_NONE = 0 [(displayName) = "None"]; + XANCHOR_LEFT = 1 [(displayName) = "Left"]; + XANCHOR_RIGHT = 2 [(displayName) = "Right"]; + }; + + // NOTE: Enum values must correspond to the enum values in dmGui + enum YAnchor + { + YANCHOR_NONE = 0 [(displayName) = "None"]; + YANCHOR_TOP = 1 [(displayName) = "Top"]; + YANCHOR_BOTTOM = 2 [(displayName) = "Bottom"]; + }; + + enum Pivot + { + PIVOT_CENTER = 0 [(displayName) = "Center"]; + PIVOT_N = 1 [(displayName) = "North"]; + PIVOT_NE = 2 [(displayName) = "North East"]; + PIVOT_E = 3 [(displayName) = "East"]; + PIVOT_SE = 4 [(displayName) = "South East"]; + PIVOT_S = 5 [(displayName) = "South"]; + PIVOT_SW = 6 [(displayName) = "South West"]; + PIVOT_W = 7 [(displayName) = "West"]; + PIVOT_NW = 8 [(displayName) = "North West"]; + }; + + enum AdjustMode + { + ADJUST_MODE_FIT = 0 [(displayName) = "Fit"]; + ADJUST_MODE_ZOOM = 1 [(displayName) = "Zoom"]; + ADJUST_MODE_STRETCH = 2 [(displayName) = "Stretch"]; + }; + + // NOTE: Enum values must correspond to the enum values in dmGui + enum SizeMode + { + SIZE_MODE_MANUAL = 0 [(displayName) = "Manual"]; + SIZE_MODE_AUTO = 1 [(displayName) = "Auto"]; + } + + enum PieBounds + { + PIEBOUNDS_RECTANGLE = 0 [(displayName) = "Rectangle"]; + PIEBOUNDS_ELLIPSE = 1 [(displayName) = "Ellipse"]; + }; + + optional dmMath.Vector4 position = 1; + optional dmMath.Vector4 rotation = 2; + optional dmMath.Vector4One scale = 3; + optional dmMath.Vector4 size = 4; + optional dmMath.Vector4One color = 5; + optional Type type = 6; + optional BlendMode blend_mode = 7 [default = BLEND_MODE_ALPHA]; + optional string text = 8; + optional string texture = 9; + optional string font = 10; + optional string id = 11; + optional XAnchor xanchor = 12 [default = XANCHOR_NONE]; + optional YAnchor yanchor = 13 [default = YANCHOR_NONE]; + optional Pivot pivot = 14 [default = PIVOT_CENTER]; + optional dmMath.Vector4WOne outline = 15; + optional dmMath.Vector4WOne shadow = 16; + optional AdjustMode adjust_mode = 17 [default = ADJUST_MODE_FIT]; + optional bool line_break = 18 [default = false]; + optional string parent = 19; + optional string layer = 20; + optional bool inherit_alpha = 21 [default = false]; + optional dmMath.Vector4 slice9 = 22; + + // These are pie specific options + optional PieBounds outerBounds = 23 [default = PIEBOUNDS_ELLIPSE]; + optional float innerRadius = 24 [default = 0]; + optional int32 perimeterVertices = 25 [default = 32]; + optional float pieFillAngle = 26 [default = 360]; + + optional ClippingMode clipping_mode = 27 [default = CLIPPING_MODE_NONE]; + optional bool clipping_visible = 28 [default = true]; + optional bool clipping_inverted = 29 [default = false]; + + optional float alpha = 30 [default = 1.0]; + optional float outline_alpha = 31 [default = 1.0]; + optional float shadow_alpha = 32 [default = 1.0]; + + repeated uint32 overridden_fields = 33; + + optional string template = 34 [(resource)=true]; + optional bool template_node_child = 35; + + optional float text_leading = 36 [default = 1.0]; + optional float text_tracking = 37 [default = 0.0]; + + optional SizeMode size_mode = 38 [default = SIZE_MODE_MANUAL]; + + // Spine specific options (deprecated) + optional string spine_scene = 39; + optional string spine_default_animation = 40; + optional string spine_skin = 41; + optional bool spine_node_child = 42 [default = false]; + + // ParticleFX specific options + optional string particlefx = 43; + + optional uint32 custom_type = 44 [default = 0]; // for custom types + + optional bool enabled = 45 [default = true]; + optional bool visible = 46 [default = true]; + + optional string material = 47; + + // Spine GUI options + // Controls whether GUI bone nodes are created for Spine nodes. + optional bool spine_create_bones = 48 [default = false]; + +} + +message SceneDesc +{ + enum AdjustReference + { + ADJUST_REFERENCE_LEGACY = 0 [(displayName) = "Root (Deprecated)"]; + ADJUST_REFERENCE_PARENT = 1 [(displayName) = "Per Node"]; + ADJUST_REFERENCE_DISABLED = 2 [(displayName) = "Disabled"]; + }; + + message FontDesc + { + required string name = 1; + required string font = 2 [(resource)=true]; + } + + message TextureDesc + { + required string name = 1; + required string texture = 2 [(resource)=true]; + } + + message LayerDesc + { + required string name = 1; + } + + message LayoutDesc + { + required string name = 1; + repeated NodeDesc nodes = 2; + } + + message MaterialDesc + { + required string name = 1; + required string material = 2 [(resource)=true]; + } + + message SpineSceneDesc + { + required string name = 1; + required string spine_scene = 2 [(resource)=true]; + } + + message ResourceDesc + { + required string name = 1; + required string path = 2 [(resource)=true]; + } + + message ParticleFXDesc + { + required string name = 1; + required string particlefx = 2 [(resource)=true]; + } + + optional string script = 1 [(resource)=true]; + repeated FontDesc fonts = 2; + repeated TextureDesc textures = 3; + optional dmMath.Vector4 background_color = 4; // Deprecated + repeated NodeDesc nodes = 6; + repeated LayerDesc layers = 7; + optional string material = 8 [(resource)=true, default="/builtins/materials/gui.material"]; + repeated LayoutDesc layouts = 9; + optional AdjustReference adjust_reference = 10 [default = ADJUST_REFERENCE_LEGACY]; + optional uint32 max_nodes = 11 [default = 512]; + repeated SpineSceneDesc spine_scenes = 12; + repeated ParticleFXDesc particlefxs = 13; + repeated ResourceDesc resources = 14; + repeated MaterialDesc materials = 15; + optional uint32 max_dynamic_textures = 16 [default = 128]; +} + +/*# reports a layout change + * + * This message is broadcast to every GUI component when a layout change has been initiated + * on device. + * + * @message + * @name layout_changed + * @param id [type:hash] the id of the layout the engine is changing to + * @param previous_id [type:hash] the id of the layout the engine is changing from + * @examples + * + * ```lua + * function on_message(self, message_id, message, sender) + * if message_id == hash("layout_changed") and message.id == hash("Landscape") then + * -- switching layout to "Landscape"... + * ... + * end + * end + * ``` + */ +message LayoutChanged +{ + required uint64 id = 1; + required uint64 previous_id = 2; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/label_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/label_ddf.proto new file mode 100644 index 0000000..a6b788c --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/label_ddf.proto @@ -0,0 +1,64 @@ +syntax = "proto2"; +package dmGameSystemDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "Label"; + +/*# Label API documentation + * + * @document + * @name Label + * @namespace label + * @language Lua + */ + +message LabelDesc +{ + enum BlendMode + { + BLEND_MODE_ALPHA = 0 [(displayName) = "Alpha"]; + BLEND_MODE_ADD = 1 [(displayName) = "Add"]; + BLEND_MODE_MULT = 3 [(displayName) = "Multiply"]; + BLEND_MODE_SCREEN = 4 [(displayName) = "Screen"]; + } + + enum Pivot + { + PIVOT_CENTER = 0 [(displayName) = "Center"]; + PIVOT_N = 1 [(displayName) = "North"]; + PIVOT_NE = 2 [(displayName) = "North East"]; + PIVOT_E = 3 [(displayName) = "East"]; + PIVOT_SE = 4 [(displayName) = "South East"]; + PIVOT_S = 5 [(displayName) = "South"]; + PIVOT_SW = 6 [(displayName) = "South West"]; + PIVOT_W = 7 [(displayName) = "West"]; + PIVOT_NW = 8 [(displayName) = "North West"]; + }; + + required dmMath.Vector4 size = 1; + optional dmMath.Vector4One scale = 2; // Deprecated + optional dmMath.Vector4One color = 3; + optional dmMath.Vector4WOne outline = 4; + optional dmMath.Vector4WOne shadow = 5; + + optional float leading = 6 [default = 1.0]; + optional float tracking = 7; + optional Pivot pivot = 8 [default = PIVOT_CENTER]; + optional BlendMode blend_mode = 9 [default = BLEND_MODE_ALPHA]; + optional bool line_break = 10 [default = false]; + + optional string text = 11; + required string font = 12 [(resource)=true]; + required string material = 13 [(resource)=true]; +} + + +message SetText +{ + required string text = 1; +} + + diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/mesh_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/mesh_ddf.proto new file mode 100644 index 0000000..93152db --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/mesh_ddf.proto @@ -0,0 +1,31 @@ +syntax = "proto2"; + +package dmMeshDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "MeshProto"; + +message MeshDesc +{ + // NOTE: Needs to match PrimitiveType in graphics.h + enum PrimitiveType + { + // PRIMITIVE_POINTS = 0 [(displayName) = "Points"]; + PRIMITIVE_LINES = 1 [(displayName) = "Lines"]; + // PRIMITIVE_LINE_LOOP = 2 [(displayName) = "Line Loop"]; + // PRIMITIVE_LINE_STRIP = 3 [(displayName) = "Line Strip"]; + PRIMITIVE_TRIANGLES = 4 [(displayName) = "Triangles"]; + PRIMITIVE_TRIANGLE_STRIP = 5 [(displayName) = "Triangle Strip"]; + // PRIMITIVE_TRIANGLE_FAN = 6 [(displayName) = "Triangle Fan"]; + } + + required string material = 1 [(resource)=true]; + required string vertices = 2 [(resource)=true]; + repeated string textures = 3 [(resource)=true]; + optional PrimitiveType primitive_type = 4 [default = PRIMITIVE_TRIANGLES]; + optional string position_stream = 5; + optional string normal_stream = 6; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/model_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/model_ddf.proto new file mode 100644 index 0000000..7cb576d --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/model_ddf.proto @@ -0,0 +1,120 @@ +syntax = "proto2"; +package dmModelDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; +import "graphics/graphics_ddf.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "ModelProto"; + +// TODO: Add documentation for messages, see https://github.com/defold/extension-spine/blob/main/defold-spine/commonsrc/spine_ddf.proto + +message Texture +{ + required string sampler = 1; // the name of the sampler + required string texture = 2 [(resource)=true]; +} + +message Material +{ + required string name = 1; // the name of the material in the model file + required string material = 2 [(resource)=true]; + repeated Texture textures = 3; + repeated dmGraphics.VertexAttribute attributes = 4; +} + +// The source format (.model) +message ModelDesc +{ + required string mesh = 2 [(resource)=true]; + optional string material = 3 [(resource)=true]; // Deprecated + repeated string textures = 4 [(resource)=true]; // Deprecated + optional string skeleton = 5 [(resource)=true]; + optional string animations = 6 [(resource)=true]; + optional string default_animation = 7; + + optional string name = 10; // Deprecated + repeated Material materials = 11; + optional bool create_go_bones = 12 [default=true]; +} + +// The engine format (.modelc) +message Model +{ + required string rig_scene = 1 [(resource)=true]; + optional string default_animation = 2; + repeated Material materials = 3; + optional bool create_go_bones = 4; +} + +message ResetConstant +{ + required uint64 name_hash = 1; +} + +message SetTexture +{ + required uint64 texture_hash = 1; + required uint32 texture_unit = 2; +} + +message ModelPlayAnimation +{ + required uint64 animation_id = 1; + // matches dmGameObject::Playback in gameobject.h + required uint32 playback = 2; + optional float blend_duration = 3 [default = 0.0]; + optional float offset = 4 [default = 0.0]; + optional float playback_rate = 5 [default = 1.0]; +} + +message ModelCancelAnimation +{ +} + +/*# Model API documentation + * + * @document + * @name Model + * @namespace model + * @language Lua + */ + +/*# reports the completion of a Model animation + * + * This message is sent when a Model animation has finished playing back to the script + * that started the animation. + * + * [icon:attention] No message is sent if a completion callback function was supplied + * when the animation was started. No message is sent if the animation is cancelled with + * model.cancel(). This message is sent only for animations that play with + * the following playback modes: + * + * - `go.PLAYBACK_ONCE_FORWARD` + * - `go.PLAYBACK_ONCE_BACKWARD` + * - `go.PLAYBACK_ONCE_PINGPONG` + * + * @message + * @name model_animation_done + * @param animation_id [type:hash] the id of the completed animation + * @param playback [type:constant] the playback mode of the completed animation + * @examples + * + * ```lua + * function on_message(self, message_id, message, sender) + * if message_id == hash("model_animation_done") then + * if message.animation_id == hash("run") and message.playback == go.PLAYBACK_ONCE_FORWARD then + * -- The animation "run" has finished running forward. + * end + * end + * end + * ``` + */ + +message ModelAnimationDone +{ + required uint64 animation_id = 1; + // matches dmGameObject::Playback in gameobject.h + required uint32 playback = 2; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/physics_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/physics_ddf.proto new file mode 100644 index 0000000..41f047f --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/physics_ddf.proto @@ -0,0 +1,602 @@ +syntax = "proto2"; +package dmPhysicsDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "Physics"; + +message ConvexShape +{ + /* NOTE: These values must match CollisionShape.Type below */ + enum Type + { + TYPE_SPHERE = 0 [(displayName) = "Sphere"]; + TYPE_BOX = 1 [(displayName) = "Box"]; + TYPE_CAPSULE = 2 [(displayName) = "Capsule"]; + TYPE_HULL = 3 [(displayName) = "Hull"]; + } + + required Type shape_type = 1; + + /* + ShapeType == SPHERE + Data = [radius] + + ShapeType == BOX + Data = [ext_x, ext_y, ext_z] + + ShapeType == CAPSULE + Data = [radius, height] + + ShapeType == HULL + Data = [x0, y0, z0, x1, ...] + */ + repeated float data = 2; +} + +message CollisionShape +{ + /* NOTE: These values must match ConvexShape.Type above */ + enum Type + { + TYPE_SPHERE = 0; + TYPE_BOX = 1; + TYPE_CAPSULE = 2; + TYPE_HULL = 3; + } + + /* + ShapeType == SPHERE + Data = [radius] + + ShapeType == BOX + Data = [ext_x, ext_y, ext_z] + + ShapeType == CAPSULE + Data = [radius, height] + + ShapeType == HULL + Data = [x0, y0, z0, x1, ...] + */ + + message Shape + { + required Type shape_type = 1; + required dmMath.Point3 position = 2; + required dmMath.Quat rotation = 3; + required uint32 index = 4; + required uint32 count = 5; + optional string id = 6; + optional uint64 id_hash = 7 [(runtime_only) = true]; + } + + repeated Shape shapes = 1; + repeated float data = 2; +} + +/* This should coincide with CollisionObjectType in physics-lib */ +enum CollisionObjectType +{ + COLLISION_OBJECT_TYPE_DYNAMIC = 0 [(displayName) = "Dynamic"]; + COLLISION_OBJECT_TYPE_KINEMATIC = 1 [(displayName) = "Kinematic"]; + COLLISION_OBJECT_TYPE_STATIC = 2 [(displayName) = "Static"]; + COLLISION_OBJECT_TYPE_TRIGGER = 3 [(displayName) = "Trigger"]; +} + +message CollisionObjectDesc +{ + optional string collision_shape = 1 [(resource)=true]; + required CollisionObjectType type = 2; + required float mass = 3; + required float friction = 4; + required float restitution = 5; + required string group = 6; + repeated string mask = 7; + optional CollisionShape embedded_collision_shape = 8; + optional float linear_damping = 9 [default=0]; + optional float angular_damping = 10 [default=0]; + optional bool locked_rotation = 11 [default=false]; + optional bool bullet = 12 [default=false]; + // Should the component generate events + optional bool event_collision = 13 [default=true]; + optional bool event_contact = 14 [default=true]; + optional bool event_trigger = 15 [default=true]; +} + +/*# Collision object physics API documentation + * + * @document + * @name Collision object + * @namespace physics + * @language Lua + */ + +/*# applies a force on a collision object + * Post this message to a collision-object-component to apply the specified force on the collision object. + * The collision object must be dynamic. + * + * @message + * @name apply_force + * @param force [type:vector3] the force to be applied on the collision object, measured in Newton + * @param position [type:vector3] the position where the force should be applied + * @examples + * + * Assuming the instance of the script has a collision-object-component with id "co": + * + * ```lua + * -- apply a force of 1 Newton towards world-x at the center of the game object instance + * msg.post("#co", "apply_force", {force = vmath.vector3(1, 0, 0), position = go.get_world_position()}) + * ``` + */ +message ApplyForce +{ + required dmMath.Vector3 force = 1; + required dmMath.Point3 position = 2; +} + +/*# reports a collision between two collision objects + * + * This message is broadcasted to every component of an instance that has a collision object, + * when the collision object collides with another collision object. For a script to take action + * when such a collision happens, it should check for this message in its `on_message` callback + * function. + * + * This message only reports that a collision actually happened and will only be sent once per + * colliding pair and frame. + * To retrieve more detailed information, check for the `contact_point_response` instead. + * + * @message + * @name collision_response + * @param other_id [type:hash] the id of the instance the collision object collided with + * @param other_position [type:vector3] the world position of the instance the collision object collided with + * @param other_group [type:hash] the collision group of the other collision object + * @param own_group [type:hash] the collision group of the own collision object + * @examples + * + * How to take action when a collision occurs: + * + * ```lua + * function on_message(self, message_id, message, sender) + * -- check for the message + * if message_id == hash("collision_response") then + * -- take action + * end + * end + * ``` + */ +message CollisionResponse +{ + required uint64 other_id = 1; + required uint64 group = 2; + required dmMath.Point3 other_position = 3; + required uint64 other_group = 4; + required uint64 own_group = 5; +} + +/*# reports a contact point between two collision objects + * + * This message is broadcasted to every component of an instance that has a collision object, + * when the collision object has contact points with respect to another collision object. + * For a script to take action when such contact points occur, it should check for this message + * in its `on_message` callback function. + * + * Since multiple contact points can occur for two colliding objects, this message can be sent + * multiple times in the same frame for the same two colliding objects. To only be notified once + * when the collision occurs, check for the `collision_response` message instead. + * + * @message + * @name contact_point_response + * @param position [type:vector3] world position of the contact point + * @param normal [type:vector3] normal in world space of the contact point, which points from the other object towards the current object + * @param relative_velocity [type:vector3] the relative velocity of the collision object as observed from the other object + * @param distance [type:number] the penetration distance between the objects, which is always positive + * @param applied_impulse [type:number] the impulse the contact resulted in + * @param life_time [type:number] life time of the contact, **not currently used** + * @param mass [type:number] the mass of the current collision object in kg + * @param other_mass [type:number] the mass of the other collision object in kg + * @param other_id [type:hash] the id of the instance the collision object is in contact with + * @param other_position [type:vector3] the world position of the other collision object + * @param other_group [type:hash] the collision group of the other collision object + * @param own_group [type:hash] the collision group of the own collision object + * @examples + * + * How to take action when a contact point occurs: + * + * ```lua + * function on_message(self, message_id, message, sender) + * -- check for the message + * if message_id == hash("contact_point_response") then + * -- take action + * end + * end + * ``` + */ +message ContactPointResponse +{ + required dmMath.Point3 position = 1; + required dmMath.Vector3 normal = 2; + required dmMath.Vector3 relative_velocity = 3; + required float distance = 4; + required float applied_impulse = 5; + required float life_time = 6; + required float mass = 7; + required float other_mass = 8; + required uint64 other_id = 9; + required dmMath.Point3 other_position = 10; + required uint64 group = 11; + required uint64 other_group = 12; + required uint64 own_group = 13; +} + +/*# reports interaction (enter/exit) between a trigger collision object and another collision object + * + * This message is broadcasted to every component of an instance that has a collision object, + * when the collision object interacts with another collision object and one of them is a trigger. + * For a script to take action when such an interaction happens, it should check for this message + * in its `on_message` callback function. + * + * This message only reports that an interaction actually happened and will only be sent once per + * colliding pair and frame. To retrieve more detailed information, check for the + * `contact_point_response` instead. + * + * @message + * @name trigger_response + * @param other_id [type:hash] the id of the instance the collision object collided with + * @param enter [type:boolean] if the interaction was an entry or not + * @param other_group [type:hash] the collision group of the triggering collision object + * @param own_group [type:hash] the collision group of the own collision object + * @examples + * + * How to take action when a trigger interaction occurs: + * + * ```lua + * function on_message(self, message_id, message, sender) + * -- check for the message + * if message_id == hash("trigger_response") then + * if message.enter then + * -- take action for entry + * else + * -- take action for exit + * end + * end + * end + * ``` + */ +message TriggerResponse +{ + required uint64 other_id = 1; + required bool enter = 2; + required uint64 group = 3; + required uint64 other_group = 4; + required uint64 own_group = 5; +} + +// Runtime only (not public) +message RequestRayCast +{ + required dmMath.Point3 from = 1; + required dmMath.Point3 to = 2; + required uint32 mask = 3; + required uint32 request_id = 4; +} + +/*# reports a ray cast hit + * + * This message is sent back to the sender of a [ref:ray_cast_request], or to the physics world listener + * if it is set (see [ref:physics.set_event_listener]), if the ray hits a collision object. + * See [ref:physics.raycast_async] for examples of how to use it. + * + * @message + * @name ray_cast_response + * @param fraction [type:number] the fraction of the hit measured along the ray, where 0 is the start of the ray and 1 is the end + * @param position [type:vector3] the world position of the hit + * @param normal [type:vector3] the normal of the surface of the collision object where it was hit + * @param id [type:hash] the instance id of the hit collision object + * @param group [type:hash] the collision group of the hit collision object as a hashed name + * @param request_id [type:number] id supplied when the ray cast was requested + */ +message RayCastResponse +{ + required float fraction = 1; + required dmMath.Point3 position = 2; + required dmMath.Vector3 normal = 3; + required uint64 id = 4; + required uint64 group = 5; + required uint32 request_id = 6; +} + +/*# reports a ray cast miss + * + * This message is sent back to the sender of a [ref:ray_cast_request], or to the physics world listener + * if it is set (see [ref:physics.set_event_listener]), if the ray didn't hit any collision object. + * See [ref:physics.raycast_async] for examples of how to use it. + * + * @message + * @name ray_cast_missed + * @param request_id [type:number] id supplied when the ray cast was requested + */ +message RayCastMissed +{ + required uint32 request_id = 1; +} + +message RequestVelocity {} + +message VelocityResponse +{ + required dmMath.Vector3 linear_velocity = 1; + required dmMath.Vector3 angular_velocity = 2; +} + +// System message (TileGrid=>CollisionObject) +message SetGridShapeHull +{ + required uint32 shape = 1; + required uint32 row = 2; + required uint32 column = 3; + required uint32 hull = 4; + required uint32 flip_horizontal = 5; + required uint32 flip_vertical = 6; + required uint32 rotate90 = 7; +} + +// System message (TileGrid=>CollisionObject) +message EnableGridShapeLayer +{ + required uint32 shape = 1; + required uint32 enable = 2; +} + +// CAUTION: If you change this, you need to change the Push-function in script_physics.cpp! +message ContactPoint +{ + required dmMath.Point3 position = 1; // World position of contact point + required dmMath.Point3 instance_position = 2; // World position of instance point + required dmMath.Vector3 normal = 3; + required dmMath.Vector3 relative_velocity = 4; + required float mass = 5; + required uint64 id = 6; + required uint64 group = 7; +} + +/*# reports a contact point between two collision objects in cases where a listener is specified. + * See [ref:physics.set_event_listener]. + * + * This message is sent to a function specified in [ref:physics.set_event_listener] when + * a collision object has contact points with another collision object. + * + * Since multiple contact points can occur for two colliding objects, this event can be sent + * multiple times in the same frame for the same two colliding objects. To only be notified once + * when the collision occurs, check for the [ref:collision_event] event instead. + * + * @message + * @name contact_point_event + * @param applied_impulse [type:number] the impulse the contact resulted in + * @param distance [type:number] the penetration distance between the objects, which is always positive + * @param a [type:table] contact point information for object A + * + * `position` + * : [type:vector3] The world position of object A + * + * `id` + * : [type:hash] The ID of object A + * + * `group` + * : [type:hash] The collision group of object A + * + * `relative_velocity` + * : [type:vector3] The relative velocity of the collision object A as observed from B object + * + * `mass` + * : [type:number] The mass of the collision object A in kg + * + * `normal` + * : [type:vector3] normal in world space of the contact point, which points from B object towards A object + * + * @param b [type:table] contact point information for object B + * + * `position` + * : [type:vector3] The world position of object B + * + * `id` + * : [type:hash] The ID of object B + * + * `group` + * : [type:hash] The collision group of object B + * + * `relative_velocity` + * : [type:vector3] The relative velocity of the collision object B as observed from A object + * + * `mass` + * : [type:number] The mass of the collision object B in kg + * + * `normal` + * : [type:vector3] normal in world space of the contact point, which points from A object towards B object + * + * @examples + * + * How to take action when a contact point occurs: + * + * ```lua + * physics.set_event_listener(function(self, events) + * for _,event in ipairs(events): + * if event['type'] == hash("contact_point_event") then + * pprint(event) + * -- { + * -- applied_impulse = 310.00769042969, + * -- distance = 0.0714111328125, + * -- a = { + * -- position = vmath.vector3(446, 371, 0), + * -- relative_velocity = vmath.vector3(1.1722083854693e-06, -20.667181015015, -0), + * -- mass = 0, + * -- group = hash: [default], + * -- id = hash: [/flat], + * -- normal = vmath.vector3(-0, -1, -0) + * -- }, + * -- b = { + * -- position = vmath.vector3(185, 657.92858886719, 0), + * -- relative_velocity = vmath.vector3(-1.1722083854693e-06, 20.667181015015, 0), + * -- mass = 10, + * -- group = hash: [default], + * -- id = hash: [/go2], + * -- normal = vmath.vector3(0, 1, 0) + * -- }, + * -- type = hash: [contact_point_event] + * -- } + * end + * end + * end) + * ``` + */ +// CAUTION: If you change this, you need to change the Push-function in script_physics.cpp! +message ContactPointEvent +{ + required ContactPoint a = 1; + required ContactPoint b = 2; + required float distance = 3; + required float applied_impulse = 4; +} + +// CAUTION: If you change this, you need to change the Push-function in script_physics.cpp! +message Collision +{ + required dmMath.Point3 position = 1; + required uint64 id = 2; + required uint64 group = 3; +} + +/*# reports a collision between two collision objects in cases where a listener is specified. + * See [ref:physics.set_event_listener]. + * + * This message is sent to a function specified in [ref:physics.set_event_listener] + * when two collision objects collide. + * + * This message only reports that a collision has occurred and will be sent once per frame and per colliding pair. + * For more detailed information, check for the [ref:contact_point_event]. + * + * @message + * @name collision_event + * @param a [type:table] collision information for object A + * + * `position` + * : [type:vector3] The world position of object A + * + * `id` + * : [type:hash] The ID of object A + * + * `group` + * : [type:hash] The collision group of object A + * + * @param b [type:table] collision information for object B + * + * `position` + * : [type:vector3] The world position of object B + * + * `id` + * : [type:hash] The ID of object B + * + * `group` + * : [type:hash] The collision group of object B + * + * @examples + * + * How to take action when a collision occurs: + * + * ```lua + * physics.set_event_listener(function(self, event, data) + * if event == hash("collision_event") then + * pprint(data) + * -- { + * -- a = { + * -- group = hash: [default], + * -- position = vmath.vector3(183, 666, 0), + * -- id = hash: [/go1] + * -- }, + * -- b = { + * -- group = hash: [default], + * -- position = vmath.vector3(185, 704.05865478516, 0), + * -- id = hash: [/go2] + * -- } + * -- } + * end + * end) + * ``` + */ +// CAUTION: If you change this, you need to change the Push-function in script_physics.cpp! +message CollisionEvent +{ + required Collision a = 1; + required Collision b = 2; +} + +// CAUTION: If you change this, you need to change the Push-function in script_physics.cpp! +message Trigger +{ + required uint64 id = 1; + required uint64 group = 2; +} + +/*# reports interaction (enter/exit) between a trigger collision object and another collision object + * See [ref:physics.set_event_listener]. + * + * This message is sent to a function specified in [ref:physics.set_event_listener] + * when a collision object interacts with another collision object and one of them is a trigger. + * + * This message only reports that an interaction actually happened and will be sent once per colliding pair and frame. + * For more detailed information, check for the [ref:contact_point_event]. + * + * @message + * @name trigger_event + * @param enter [type:boolean] if the interaction was an entry or not + * @param a [type:table] interaction information for object A + * `id` + * : [type:hash] The ID of object A + * + * `group` + * : [type:hash] The collision group of object A + * + * @param b [type:table] collision information for object B + * + * `id` + * : [type:hash] The ID of object B + * + * `group` + * : [type:hash] The collision group of object B + * + * @examples + * + * How to take action when a trigger interaction occurs: + * + * ```lua + * physics.set_event_listener(function(self, event, data) + * if event == hash("trigger_event") then + * if data.enter then + * -- take action for entry + * else + * -- take action for exit + * end + * pprint(data) + * -- { + * -- enter = true, + * -- b = { + * -- group = hash: [default], + * -- id = hash: [/go2] + * -- }, + * -- a = { + * -- group = hash: [default], + * -- id = hash: [/go1] + * -- } + * -- }, + * end + * end) + * ``` + */ +// CAUTION: If you change this, you need to change the Push-function in script_physics.cpp! +message TriggerEvent +{ + required bool enter = 1; + required Trigger a = 2; + required Trigger b = 3; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/sound_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/sound_ddf.proto new file mode 100644 index 0000000..0c2a006 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/sound_ddf.proto @@ -0,0 +1,19 @@ +syntax = "proto2"; +package dmSoundDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "Sound"; + +message SoundDesc +{ + required string sound = 1 [(resource)=true]; + optional int32 looping = 2 [default = 0]; + optional string group = 3 [default = "master"]; + optional float gain = 4 [default = 1.0]; + optional float pan = 5 [default = 0.0]; + optional float speed = 6 [default = 1.0]; + optional int32 loopcount = 7 [default = 0]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/sprite_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/sprite_ddf.proto new file mode 100644 index 0000000..506c8d7 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/sprite_ddf.proto @@ -0,0 +1,137 @@ +syntax = "proto2"; +package dmGameSystemDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; +import "graphics/graphics_ddf.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "Sprite"; + +message SpriteTexture +{ + required string sampler = 1; + required string texture = 2 [(resource)=true]; +} + +message SpriteDesc +{ + enum BlendMode + { + BLEND_MODE_ALPHA = 0 [(displayName) = "Alpha"]; + BLEND_MODE_ADD = 1 [(displayName) = "Add"]; + BLEND_MODE_ADD_ALPHA = 2 [(displayName) = "Add Alpha (Deprecated)"]; + BLEND_MODE_MULT = 3 [(displayName) = "Multiply"]; + BLEND_MODE_SCREEN = 4 [(displayName) = "Screen"]; + } + + enum SizeMode + { + SIZE_MODE_MANUAL = 0 [(displayName) = "Manual"]; + SIZE_MODE_AUTO = 1 [(displayName) = "Auto"]; + } + + optional string tile_set = 1 [(resource)=true]; // Deprecated + required string default_animation = 2; + optional string material = 3 [(resource)=true, default="/builtins/materials/sprite.material"]; + optional BlendMode blend_mode = 4 [default = BLEND_MODE_ALPHA]; + optional dmMath.Vector4 slice9 = 5; + optional dmMath.Vector4 size = 6; + optional SizeMode size_mode = 7 [default = SIZE_MODE_AUTO]; + optional float offset = 8 [default = 0.0]; + optional float playback_rate = 9 [default = 1.0]; + + repeated dmGraphics.VertexAttribute attributes = 10; + + repeated SpriteTexture textures = 11; +} + +/*# Sprite API documentation + * + * @document + * @name Sprite + * @namespace sprite + * @language Lua + */ + +/*# play a sprite animation + * + * Post this message to a sprite component to make it play an animation from its tile set. + * + * @message + * @name play_animation + * @param id [type:hash] the id of the animation to play + * @param offset [type:number] the normalized initial value of the animation cursor when the animation starts playing + * @param playback_rate [type:number] the rate with which the animation will be played. Must be positive + * @examples + * + * In the example, it is assumed that the instance of the script has a sprite-component with id "sprite". The sprite itself is assumed to be bound to a tile set with animations "walk" and "jump". + * + * ```lua + * msg.post("#sprite", "play_animation", {id = hash("jump")}) + * ``` + */ +message PlayAnimation +{ + required uint64 id = 1; + + optional float offset = 2 [default = 0.0]; + optional float playback_rate = 3 [default = 1.0]; +} + +/*# reports that an animation has completed + * + * This message is sent to the sender of a play_animation message when the + * animation has completed. + * + * Note that this message is sent only for animations that play with the following + * playback modes: + * + * - Once Forward + * - Once Backward + * - Once Ping Pong + * + * See [ref:play_animation] for more information and examples of how to use + * this message. + * + * @message + * @name animation_done + * @param current_tile [type:number] the current tile of the sprite + * @param id [type:hash] id of the animation that was completed + * @examples + * + * How to sequence two animations together. + * + * ```lua + * function init(self) + * -- play jump animation at init + * msg.post("#sprite", "play_animation", {id = hash("jump")}) + * end + * + * function on_message(self, message_id, message, sender) + * -- check for animation done response + * if message_id == hash("animation_done") then + * -- start the walk animation + * msg.post("#sprite", "play_animation", { id = hash("walk") }) + * end + * end + * ``` + */ +message AnimationDone +{ + required uint32 current_tile = 1; + required uint64 id = 2; +} + +/* Function wrapper documented in gamesys_script.cpp */ +message SetFlipHorizontal +{ + required uint32 flip = 1; +} + +/* Function wrapper documented in gamesys_script.cpp */ +message SetFlipVertical +{ + required uint32 flip = 1; +} + diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/texture_set_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/texture_set_ddf.proto new file mode 100644 index 0000000..483bdd9 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/texture_set_ddf.proto @@ -0,0 +1,115 @@ +syntax = "proto2"; +package dmGameSystemDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; +import "gamesys/tile_ddf.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "TextureSetProto"; + +// Engine formats. Editor formats are either atlas_ddf.proto or tile_ddf.proto + +message TextureSetAnimation +{ + required string id = 1; + required uint32 width = 2; + required uint32 height = 3; + required uint32 start = 4; // Frame index. Absolute offset into TextureSet.frame_indices/TextureSet.image_name_hashes. + required uint32 end = 5; // Frame index (not included in animation) + + optional uint32 fps = 6 [default = 30]; + optional Playback playback = 7 [default = PLAYBACK_ONCE_FORWARD]; + optional uint32 flip_horizontal = 8 [default = 0]; + optional uint32 flip_vertical = 9 [default = 0]; +} + +// * Vertices are relative to the center of the sprite +// * Polygon may be concave +// * Indices for a triangle list (i.e. 3 indices per triangle) +message SpriteGeometry +{ + // The width and height of the image this geometry was generated from + required uint32 width = 1; + required uint32 height = 2; + + // The center location of the image + required float center_x = 3; + required float center_y = 4; + + required bool rotated = 5; // If true, then the image is rotated 90 degrees (ccw) + + // From the atlas image + required SpriteTrimmingMode trim_mode = 6 [default = SPRITE_TRIM_MODE_OFF]; + + // A list of 2-tuples, each making up a point in a hull: [p0.x, p0.y, p1.x, p1.y, ... pN.x, pN.y] where N is (convex_hull_size-1) + // Coords are in local UV space [-0.5, 0.5], origin is at the sprite center + // The vertices are not rotated + repeated float vertices= 7; + // A list of 2-tuples, corresponding directly to the vertices (deprecated) + repeated float uvs = 8; + // list of 3-tuples, each defining a triangle in the vertex/uv list + repeated uint32 indices = 9; + + // In unit coords [-0.5, 0.5]. Default [0, 0] + optional float pivot_x = 10 [default = 0]; + optional float pivot_y = 11 [default = 0]; +} + +message TextureSet +{ + required string texture = 1 [(resource)=true]; + + required uint32 width = 2; + required uint32 height = 3; + + // A hash of the texture name/path. Not written at compile time, but instead used at runtime + optional uint64 texture_hash = 4; + + repeated TextureSetAnimation animations = 5; + + // Only used when the source is a tile-source + // tile_width and tile_height should only be used in tilemap + optional uint32 tile_width = 6; + optional uint32 tile_height = 7; + // This is the number of rects stored before the animation frames (atlas and tile source) + optional uint32 tile_count = 8; // This is the actual number of unique frames in the set + + repeated float collision_hull_points = 9; + repeated string collision_groups = 10; + repeated ConvexHull convex_hulls = 11; + + // Hash mapping from image name to animation index + // { hash("animA/image_name0", hash("animA/image_name1"), hash("animB/image_name0", ...} + // Usage: Find the hash in this array. Use the index and get the frame index: frame_indices[ find_hash(hash, image_name_hashes) ] + repeated uint64 image_name_hashes = 12; // (length == tile_count + sum([num_frames(anim) for anim in animations]) + + // Maps animation frames to frame index + // It is a flattened array, where each animation has a start and end number of frames + // [ tile0, tile1, tile2, ..., animation0_frame_start, ..., animation0_frame_end-1, ] + repeated uint32 frame_indices = 13; // length = tile_count + sum([num_frames(anim) for anim in animations]) + + // A series of four float pairs of UV coords, representing quad texture coordinates and + // allowing for rotation on texture atlases. + // For unrotated quads, the order is: [(minU,maxV),(minU,minV),(maxU,minV),(maxU,maxV)] + // For rotated quads, the order is: [(minU,minV),(maxU,minV),(maxU,maxV),(minU,maxV)] + // (See TextureSetGenerator.java) + required bytes tex_coords = 14; // (length = (tile_count * 4 * 2)) + + // A series of two float pairs of dimensions representing quad texture width and height in texels. + optional bytes tex_dims = 15; // (length = len(frame_indices) * 2) + + // One geometry struct per image + repeated SpriteGeometry geometries = 16; // (length == tile_count) + + // If false, uses the legacy code path + optional uint32 use_geometries = 17; + + // Maps animation frames to atlas page index + repeated uint32 page_indices = 18; // (length == tile_count) + + // Number of pages the texture contains. If the texture is non-paged, this value will be zero + // Note: We currently only need this for validation in bob, the engine can get this + // value from the number of images in the texture resource + optional uint32 page_count = 19 [default = 0]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/gamesys/tile_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/tile_ddf.proto new file mode 100644 index 0000000..6bfd5e7 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/gamesys/tile_ddf.proto @@ -0,0 +1,125 @@ +syntax = "proto2"; +package dmGameSystemDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.gamesys.proto"; +option java_outer_classname = "Tile"; + +message ConvexHull +{ + // index into an array of points (x0, y0, x1, y1, ...) + // in “points” unit, ie effectively divided by 2 + required uint32 index = 1 [default = 0]; + required uint32 count = 2 [default = 0]; + required string collision_group = 3 [default = "tile"]; +} + +message Cue +{ + required string id = 1; + required uint32 frame = 2; + optional float value = 3 [default = 0.0]; +} + +enum Playback +{ + PLAYBACK_NONE = 0 [(displayName) = "None"]; + PLAYBACK_ONCE_FORWARD = 1 [(displayName) = "Once Forward"]; + PLAYBACK_ONCE_BACKWARD = 2 [(displayName) = "Once Backward"]; + PLAYBACK_ONCE_PINGPONG = 6 [(displayName) = "Once Ping Pong"]; + PLAYBACK_LOOP_FORWARD = 3 [(displayName) = "Loop Forward"]; + PLAYBACK_LOOP_BACKWARD = 4 [(displayName) = "Loop Backward"]; + PLAYBACK_LOOP_PINGPONG = 5 [(displayName) = "Loop Ping Pong"]; +} + +enum SpriteTrimmingMode +{ + SPRITE_TRIM_MODE_OFF = 0 [(displayName) = "Off"]; + SPRITE_TRIM_MODE_4 = 4 [(displayName) = "4 Vertices"]; + SPRITE_TRIM_MODE_5 = 5 [(displayName) = "5 Vertices"]; + SPRITE_TRIM_MODE_6 = 6 [(displayName) = "6 Vertices"]; + SPRITE_TRIM_MODE_7 = 7 [(displayName) = "7 Vertices"]; + SPRITE_TRIM_MODE_8 = 8 [(displayName) = "8 Vertices"]; + SPRITE_TRIM_POLYGONS = 9 [(displayName) = "Polygons"]; +} + +message Animation +{ + required string id = 1; + required uint32 start_tile = 2; + required uint32 end_tile = 3; + optional Playback playback = 4 [default = PLAYBACK_ONCE_FORWARD]; + optional uint32 fps = 5 [default = 30]; + optional uint32 flip_horizontal = 6 [default = 0]; + optional uint32 flip_vertical = 7 [default = 0]; + repeated Cue cues = 8; +} + +message TileSet +{ + required string image = 1 [(resource)=true]; + required uint32 tile_width = 2 [default = 0]; + required uint32 tile_height = 3 [default = 0]; + optional uint32 tile_margin = 4 [default = 0]; + optional uint32 tile_spacing = 5 [default = 0]; + optional string collision = 6 [(resource)=true]; + optional string material_tag = 7 [default = "tile"]; + repeated ConvexHull convex_hulls = 8; + repeated float convex_hull_points = 9 [(runtime_only)=true]; + repeated string collision_groups = 10; + repeated Animation animations = 11; + optional uint32 extrude_borders = 12 [default = 0]; + optional uint32 inner_padding = 13 [default = 0]; + optional SpriteTrimmingMode sprite_trim_mode = 14 [default = SPRITE_TRIM_MODE_OFF]; +} + +message TileCell +{ + required int32 x = 1 [default = 0]; + required int32 y = 2 [default = 0]; + required uint32 tile = 3 [default = 0]; + optional uint32 h_flip = 4 [default = 0]; + optional uint32 v_flip = 5 [default = 0]; + optional uint32 rotate90 = 6 [default = 0]; +} + +message TileLayer +{ + required string id = 1 [default = "layer1"]; + required float z = 2 [default = 0.0]; + optional uint32 is_visible = 3 [default = 1]; + optional uint64 id_hash = 4 [default = 0, (runtime_only)=true]; + repeated TileCell cell = 6; +} + +message TileGrid +{ + enum BlendMode + { + BLEND_MODE_ALPHA = 0 [(displayName) = "Alpha"]; + BLEND_MODE_ADD = 1 [(displayName) = "Add"]; + BLEND_MODE_ADD_ALPHA = 2 [(displayName) = "Add Alpha (Deprecated)"]; + BLEND_MODE_MULT = 3 [(displayName) = "Multiply"]; + BLEND_MODE_SCREEN = 4 [(displayName) = "Screen"]; + } + + required string tile_set = 1 [(resource)=true]; + repeated TileLayer layers = 2; + optional string material = 3 [(resource)=true, default="/builtins/materials/tile_map.material"]; + optional BlendMode blend_mode = 4 [default = BLEND_MODE_ALPHA]; +} + +/* Function wrapper documented in script_tilemap.cpp */ +message SetConstantTileMap +{ + required uint64 name_hash = 1; + required dmMath.Vector4 value = 2 [(field_align)=true]; +} + +/* Function wrapper documented in script_tilemap.cpp */ +message ResetConstantTileMap +{ + required uint64 name_hash = 1; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/graphics/graphics_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/graphics/graphics_ddf.proto new file mode 100644 index 0000000..0e536cf --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/graphics/graphics_ddf.proto @@ -0,0 +1,454 @@ +syntax = "proto2"; +package dmGraphics; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.graphics.proto"; +option java_outer_classname = "Graphics"; + +message Cubemap +{ + required string right = 1 [(resource)=true]; + required string left = 2 [(resource)=true]; + required string top = 3 [(resource)=true]; + required string bottom = 4 [(resource)=true]; + required string front = 5 [(resource)=true]; + required string back = 6 [(resource)=true]; +} + +enum CoordinateSpace +{ + // Used when building the default "manufactured" attributes. + // This setting mean that we use the vertex coordinate system setting + // from the material (in case of models). + COORDINATE_SPACE_DEFAULT = 0; + COORDINATE_SPACE_WORLD = 1; + COORDINATE_SPACE_LOCAL = 2; +} + +enum VertexStepFunction +{ + VERTEX_STEP_FUNCTION_VERTEX = 0; + VERTEX_STEP_FUNCTION_INSTANCE = 1; +} + +message VertexAttribute +{ + enum DataType + { + TYPE_BYTE = 1; + TYPE_UNSIGNED_BYTE = 2; + TYPE_SHORT = 3; + TYPE_UNSIGNED_SHORT = 4; + TYPE_INT = 5; + TYPE_UNSIGNED_INT = 6; + TYPE_FLOAT = 7; + } + + enum VectorType + { + VECTOR_TYPE_SCALAR = 1; + VECTOR_TYPE_VEC2 = 2; + VECTOR_TYPE_VEC3 = 3; + VECTOR_TYPE_VEC4 = 4; + VECTOR_TYPE_MAT2 = 5; + VECTOR_TYPE_MAT3 = 6; + VECTOR_TYPE_MAT4 = 7; + } + + enum SemanticType + { + SEMANTIC_TYPE_NONE = 1; + SEMANTIC_TYPE_POSITION = 2; + SEMANTIC_TYPE_TEXCOORD = 3; + SEMANTIC_TYPE_PAGE_INDEX = 4; + SEMANTIC_TYPE_COLOR = 5; + SEMANTIC_TYPE_NORMAL = 6; + SEMANTIC_TYPE_TANGENT = 7; + SEMANTIC_TYPE_WORLD_MATRIX = 8; + SEMANTIC_TYPE_NORMAL_MATRIX = 9; + SEMANTIC_TYPE_BONE_WEIGHTS = 10; + SEMANTIC_TYPE_BONE_INDICES = 11; + } + + message LongValues + { + repeated int64 v = 1 [packed = true]; // We use int64 so we can represent the entire signed and unsigned int32 range as human-readable integers in the project files. + } + + message DoubleValues + { + repeated double v = 1 [packed = true]; + } + + required string name = 1; + optional uint64 name_hash = 2 [(runtime_only) = true]; + optional SemanticType semantic_type = 3 [default = SEMANTIC_TYPE_NONE]; + optional int32 element_count = 4 [default = 0]; // Deprecated + optional bool normalize = 5 [default = false]; + optional DataType data_type = 6 [default = TYPE_FLOAT]; + optional CoordinateSpace coordinate_space = 7 [default = COORDINATE_SPACE_LOCAL]; + optional VertexStepFunction step_function = 11 [default = VERTEX_STEP_FUNCTION_VERTEX]; + optional VectorType vector_type = 12 [default = VECTOR_TYPE_VEC4]; + + // Note: Add a channel field here for identifying a semantic "channel", i.e a second UV set + + oneof values + { + LongValues long_values = 8; // Saved integer values (project files only) + DoubleValues double_values = 9; // Saved floating point values (project files only) + bytes binary_values = 10 [(runtime_only) = true]; // Packed binary representation of the input values (engine only) + } +} + +enum DepthStencilFormat +{ + DEPTH_STENCIL_FORMAT_D32F = 1; + DEPTH_STENCIL_FORMAT_D32F_S8U = 2; + DEPTH_STENCIL_FORMAT_D16U_S8U = 3; + DEPTH_STENCIL_FORMAT_D24U_S8U = 4; + DEPTH_STENCIL_FORMAT_S8U = 5; +} + +// Engine only, values corresponds to TextureUsageHint in graphics.h +enum TextureUsageFlag +{ + TEXTURE_USAGE_FLAG_SAMPLE = 1; + TEXTURE_USAGE_FLAG_MEMORYLESS = 2; + TEXTURE_USAGE_FLAG_STORAGE = 4; + TEXTURE_USAGE_FLAG_INPUT = 8; + TEXTURE_USAGE_FLAG_COLOR = 16; +} + +message TextureImage +{ + enum Type + { + TYPE_2D = 1; + TYPE_CUBEMAP = 2; + TYPE_2D_ARRAY = 3; + TYPE_2D_IMAGE = 4; + TYPE_3D = 5; + TYPE_3D_IMAGE = 6; + } + + enum CompressionType + { + // Not compressed + COMPRESSION_TYPE_DEFAULT = 0; + // WebP encoded (Deprecated, converts to Default) + COMPRESSION_TYPE_WEBP = 1; + // WebP lossy encoded (Deprecated, converts to UASTC) + COMPRESSION_TYPE_WEBP_LOSSY = 2; + // Basis UASTC + COMPRESSION_TYPE_BASIS_UASTC = 3; + // Basis ETC1S + COMPRESSION_TYPE_BASIS_ETC1S = 4; + // ASTC + COMPRESSION_TYPE_ASTC = 5; + } + + enum CompressionFlags + { + // RGB to be cleared when A is zero + COMPRESSION_FLAG_ALPHA_CLEAN = 1; + } + + enum TextureFormat + { + option allow_alias = true; + + TEXTURE_FORMAT_LUMINANCE = 0; + TEXTURE_FORMAT_RGB = 1; + TEXTURE_FORMAT_RGBA = 2; + TEXTURE_FORMAT_RGB_PVRTC_2BPPV1 = 3; + TEXTURE_FORMAT_RGB_PVRTC_4BPPV1 = 4; + TEXTURE_FORMAT_RGBA_PVRTC_2BPPV1 = 5; + TEXTURE_FORMAT_RGBA_PVRTC_4BPPV1 = 6; + TEXTURE_FORMAT_RGB_ETC1 = 7; + + TEXTURE_FORMAT_RGB_16BPP = 8; // 565 + TEXTURE_FORMAT_RGBA_16BPP = 9; // 4444 + + TEXTURE_FORMAT_LUMINANCE_ALPHA = 10; + + TEXTURE_FORMAT_RGBA_ETC2 = 11; + + TEXTURE_FORMAT_RGBA_ASTC_4X4 = 12; + TEXTURE_FORMAT_RGBA_ASTC_4x4 = 12; // Deprecated! + + TEXTURE_FORMAT_RGB_BC1 = 13; + TEXTURE_FORMAT_RGBA_BC3 = 14; + TEXTURE_FORMAT_R_BC4 = 15; + TEXTURE_FORMAT_RG_BC5 = 16; + TEXTURE_FORMAT_RGBA_BC7 = 17; + + TEXTURE_FORMAT_RGB16F = 18; + TEXTURE_FORMAT_RGB32F = 19; + TEXTURE_FORMAT_RGBA16F = 20; + TEXTURE_FORMAT_RGBA32F = 21; + TEXTURE_FORMAT_R16F = 22; + TEXTURE_FORMAT_RG16F = 23; + TEXTURE_FORMAT_R32F = 24; + TEXTURE_FORMAT_RG32F = 25; + + // ASTC Formats + TEXTURE_FORMAT_RGBA_ASTC_5X4 = 26; + TEXTURE_FORMAT_RGBA_ASTC_5X5 = 27; + TEXTURE_FORMAT_RGBA_ASTC_6X5 = 28; + TEXTURE_FORMAT_RGBA_ASTC_6X6 = 29; + TEXTURE_FORMAT_RGBA_ASTC_8X5 = 30; + TEXTURE_FORMAT_RGBA_ASTC_8X6 = 31; + TEXTURE_FORMAT_RGBA_ASTC_8X8 = 32; + TEXTURE_FORMAT_RGBA_ASTC_10X5 = 33; + TEXTURE_FORMAT_RGBA_ASTC_10X6 = 34; + TEXTURE_FORMAT_RGBA_ASTC_10X8 = 35; + TEXTURE_FORMAT_RGBA_ASTC_10X10 = 36; + TEXTURE_FORMAT_RGBA_ASTC_12X10 = 37; + TEXTURE_FORMAT_RGBA_ASTC_12X12 = 38; + } + + message Image + { + required uint32 width = 1; + required uint32 height = 2; + optional uint32 depth = 3 [default = 1]; + required uint32 original_width = 4; + required uint32 original_height = 5; + optional uint32 original_depth = 6 [default = 1]; + required TextureFormat format = 7; + repeated uint32 mip_map_offset = 8; + repeated uint32 mip_map_size = 9; // always uncompressed (native) size + optional CompressionType compression_type = 10 [default = COMPRESSION_TYPE_DEFAULT]; + repeated uint32 mip_map_size_compressed = 11; + repeated uint32 mip_map_dimensions = 12; // w0, h0, w1, h1, ... + optional uint32 data_size = 13; + } + + repeated Image alternatives = 1; + required Type type = 2; + // When count > 1 count mipmaps are laid out contiguously in memory and + // the mip_map_offset should reflect that, e.g. times 6 for cubemaps + required uint32 count = 3; + + // Runtime only, used for specifying how the texture should be used. + // Possible values are specified in dmsdk/graphics.h + optional uint32 usage_flags = 4 [(runtime_only) = true, default = 1]; + // Runtime only, when creating textures in runtime we want to avoid + // making a copy of the texture data just to create the resource. + // This field can instead be used to store a pointer to the actual data. + optional uint64 image_data_address = 5 [(runtime_only) = true]; +} + +// We encapsulate the texture format in its own message due +// to we cant have repeated enums. +message TextureFormatAlternative +{ + enum CompressionLevel + { + FAST = 0; + NORMAL = 1; + HIGH = 2; + BEST = 3; + } + + required TextureImage.TextureFormat format = 1; + optional CompressionLevel compression_level = 2; // DEPRECATED, use compressor + compressor_preset instead + optional TextureImage.CompressionType compression_type = 3 [default = COMPRESSION_TYPE_DEFAULT]; // DEPRECATED, use compressor + compressor_preset instead + optional string compressor = 4; + optional string compressor_preset = 5; +} + +message PathSettings +{ + required string path = 1; + required string profile = 2; +} + +message PlatformProfile +{ + enum OS + { + OS_ID_GENERIC = 0; + OS_ID_WINDOWS = 1; + OS_ID_OSX = 2; + OS_ID_LINUX = 3; + OS_ID_IOS = 4; + OS_ID_ANDROID = 5; + OS_ID_WEB = 6; + OS_ID_SWITCH = 7; + OS_ID_PS4 = 8; + OS_ID_PS5 = 9; + OS_ID_XBOX = 10; + } + + required OS os = 1; + repeated TextureFormatAlternative formats = 2; + required bool mipmaps = 3; + optional uint32 max_texture_size = 4; + optional bool premultiply_alpha = 5 [default = true]; +} + +message TextureProfile +{ + required string name = 1; + repeated PlatformProfile platforms = 2; +} + +message TextureProfiles +{ + + repeated PathSettings path_settings = 1; + repeated TextureProfile profiles = 2; +} + + +message ShaderDesc +{ + enum Language + { + LANGUAGE_GLSL_SM120 = 1; // OpenGL 2 compatible + LANGUAGE_GLES_SM100 = 2; // OpenGLES 2 / WebGL 1 + LANGUAGE_GLES_SM300 = 3; // OpenGLES 3 / WebGL 2 + LANGUAGE_SPIRV = 4; // Vulkan / MoltenVK + LANGUAGE_PSSL = 5; // Playstation + LANGUAGE_GLSL_SM430 = 6; // OpenGL 4.3+ compatible + LANGUAGE_GLSL_SM330 = 7; // OpenGL 3.3 (used for desktop platforms) + LANGUAGE_WGSL = 8; // WebGPU + LANGUAGE_HLSL_50 = 9; // Windows / XBox compatible (also used for cross compiling) + LANGUAGE_HLSL_51 = 10; // Windows / XBox compatible (also used for cross compiling) + LANGUAGE_MSL_22 = 11; // MacOS / iOS compatible + } + + enum ShaderType + { + SHADER_TYPE_VERTEX = 0; + SHADER_TYPE_FRAGMENT = 1; + SHADER_TYPE_COMPUTE = 2; + } + + enum ShaderDataType + { + SHADER_TYPE_UNKNOWN = 0; + SHADER_TYPE_INT = 1; + SHADER_TYPE_UINT = 2; + SHADER_TYPE_FLOAT = 3; + SHADER_TYPE_VEC2 = 4; + SHADER_TYPE_VEC3 = 5; + SHADER_TYPE_VEC4 = 6; + SHADER_TYPE_MAT2 = 7; + SHADER_TYPE_MAT3 = 8; + SHADER_TYPE_MAT4 = 9; + SHADER_TYPE_SAMPLER2D = 10; + SHADER_TYPE_SAMPLER3D = 11; + SHADER_TYPE_SAMPLER_CUBE = 12; + SHADER_TYPE_SAMPLER2D_ARRAY = 13; + SHADER_TYPE_UNIFORM_BUFFER = 14; + + // Extended types (not universally supported) + SHADER_TYPE_UVEC2 = 15; + SHADER_TYPE_UVEC3 = 16; + SHADER_TYPE_UVEC4 = 17; + SHADER_TYPE_TEXTURE2D = 18; + SHADER_TYPE_UTEXTURE2D = 19; + SHADER_TYPE_RENDER_PASS_INPUT = 20; + SHADER_TYPE_UIMAGE2D = 21; + SHADER_TYPE_IMAGE2D = 22; + SHADER_TYPE_SAMPLER = 23; + SHADER_TYPE_STORAGE_BUFFER = 24; + SHADER_TYPE_TEXTURE2D_ARRAY = 25; + SHADER_TYPE_TEXTURE_CUBE = 26; + + SHADER_TYPE_TEXTURE3D = 27; + SHADER_TYPE_UTEXTURE3D = 28; + SHADER_TYPE_UIMAGE3D = 29; + SHADER_TYPE_IMAGE3D = 30; + SHADER_TYPE_SAMPLER3D_ARRAY = 31; + SHADER_TYPE_TEXTURE3D_ARRAY = 32; + } + + message ResourceType + { + oneof Type + { + ShaderDataType shader_type = 1; + int32 type_index = 2; + } + + // The engine doesn't know which of the oneofs has been set + // (we should at some point support that for oneofs) + optional bool use_type_index = 3; + } + + message ResourceMember + { + required string name = 1; + required uint64 name_hash = 2; + required ResourceType type = 3; + optional uint32 element_count = 4; + optional uint32 offset = 5; + } + + message ResourceTypeInfo + { + required string name = 1; + required uint64 name_hash = 2; + repeated ResourceMember members = 3; + } + + message ResourceBinding + { + required string name = 1; + required uint64 name_hash = 2; + required ResourceType type = 3; + optional uint32 id = 4; + optional string instance_name = 5; + optional uint64 instance_name_hash = 6; + optional uint32 set = 7; + optional uint32 binding = 8; + optional uint32 element_count = 9; + optional uint32 stage_flags = 10; // 0x1=vert, 0x2=frag, 0x4=compute + + oneof BindingInfo + { + // Uniform buffer block size + uint32 block_size = 11; + // Sampler object index + // Since we split combined samplers into + // a texture + sampler object we need to + // keep track of which of the textures + // the sampler originally is pointing to. + uint32 sampler_texture_index = 12; + } + } + + message ShaderReflection + { + repeated ResourceBinding uniform_buffers = 1; + repeated ResourceBinding storage_buffers = 2; + repeated ResourceBinding textures = 3; + repeated ResourceBinding inputs = 4; + repeated ResourceBinding outputs = 5; + repeated ResourceTypeInfo types = 6; + } + + message HLSLResourceMapping + { + optional uint64 name_hash = 1; + optional uint32 binding = 2; + optional uint32 set = 3; + } + + message Shader + { + required bytes source = 1; + required ShaderType shader_type = 2; + required Language language = 3; + optional bool variant_texture_array = 4 [default = false]; + repeated HLSLResourceMapping hlsl_resource_mapping = 5; + } + + repeated Shader shaders = 1; + optional ShaderReflection reflection = 2; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/particle_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/particle_ddf.proto new file mode 100644 index 0000000..29b7a22 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/particle_ddf.proto @@ -0,0 +1,186 @@ +syntax = "proto2"; +package dmParticleDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; +import "graphics/graphics_ddf.proto"; + +option java_package = "com.dynamo.particle.proto"; +option java_outer_classname = "Particle"; + +enum EmitterType +{ + EMITTER_TYPE_CIRCLE = 0 [(displayName) = "Circle"]; + EMITTER_TYPE_2DCONE = 1 [(displayName) = "2D Cone"]; + EMITTER_TYPE_BOX = 2 [(displayName) = "Box"]; + EMITTER_TYPE_SPHERE = 3 [(displayName) = "Sphere"]; + EMITTER_TYPE_CONE = 4 [(displayName) = "Cone"]; +}; + +enum PlayMode +{ + PLAY_MODE_ONCE = 0 [(displayName) = "Once"]; + PLAY_MODE_LOOP = 1 [(displayName) = "Loop"]; +} + +enum EmissionSpace +{ + EMISSION_SPACE_WORLD = 0 [(displayName) = "World"]; + EMISSION_SPACE_EMITTER = 1 [(displayName) = "Emitter"]; +} + +message SplinePoint +{ + optional float x = 1; + required float y = 2; // Required since it holds the value for non-curve property values. + optional float t_x = 3 [default = 1.0]; + optional float t_y = 4; +} + +enum EmitterKey +{ + EMITTER_KEY_SPAWN_RATE = 0 [(displayName) = "Spawn Rate"]; + EMITTER_KEY_SIZE_X = 1 [(displayName) = "Emitter Size X"]; + EMITTER_KEY_SIZE_Y = 2 [(displayName) = "Emitter Size Y"]; + EMITTER_KEY_SIZE_Z = 3 [(displayName) = "Emitter Size Z"]; + EMITTER_KEY_PARTICLE_LIFE_TIME = 4 [(displayName) = "Particle Life Time"]; + EMITTER_KEY_PARTICLE_SPEED = 5 [(displayName) = "Initial Speed"]; + EMITTER_KEY_PARTICLE_SIZE = 6 [(displayName) = "Initial Size"]; + EMITTER_KEY_PARTICLE_RED = 7 [(displayName) = "Initial Red"]; + EMITTER_KEY_PARTICLE_GREEN = 8 [(displayName) = "Initial Green"]; + EMITTER_KEY_PARTICLE_BLUE = 9 [(displayName) = "Initial Blue"]; + EMITTER_KEY_PARTICLE_ALPHA = 10 [(displayName) = "Initial Alpha"]; + EMITTER_KEY_PARTICLE_ROTATION = 11 [(displayName) = "Initial Rotation"]; + EMITTER_KEY_PARTICLE_STRETCH_FACTOR_X = 12 [(displayName) = "Initial Stretch X"]; + EMITTER_KEY_PARTICLE_STRETCH_FACTOR_Y = 13 [(displayName) = "Initial Stretch Y"]; + EMITTER_KEY_PARTICLE_ANGULAR_VELOCITY = 14 [(displayName) = "Initial Angular Velocity"]; + EMITTER_KEY_COUNT = 15; +} + +enum ParticleKey +{ + PARTICLE_KEY_SCALE = 0 [(displayName) = "Life Scale"]; + PARTICLE_KEY_RED = 1 [(displayName) = "Life Red"]; + PARTICLE_KEY_GREEN = 2 [(displayName) = "Life Green"]; + PARTICLE_KEY_BLUE = 3 [(displayName) = "Life Blue"]; + PARTICLE_KEY_ALPHA = 4 [(displayName) = "Life Alpha"]; + PARTICLE_KEY_ROTATION = 5 [(displayName) = "Life Rotation"]; + PARTICLE_KEY_STRETCH_FACTOR_X = 6 [(displayName) = "Life Stretch X"]; + PARTICLE_KEY_STRETCH_FACTOR_Y = 7 [(displayName) = "Life Stretch Y"]; + PARTICLE_KEY_ANGULAR_VELOCITY = 8 [(displayName) = "Life Angular Velocity"]; + PARTICLE_KEY_COUNT = 9; +} + +enum ModifierType +{ + MODIFIER_TYPE_ACCELERATION = 0 [(displayName) = "Acceleration"]; + MODIFIER_TYPE_DRAG = 1 [(displayName) = "Drag"]; + MODIFIER_TYPE_RADIAL = 2 [(displayName) = "Radial"]; + MODIFIER_TYPE_VORTEX = 3 [(displayName) = "Vortex"]; +} + +enum ModifierKey +{ + MODIFIER_KEY_MAGNITUDE = 0 [(displayName) = "Magnitude"]; + MODIFIER_KEY_MAX_DISTANCE = 1 [(displayName) = "Max Distance"]; + MODIFIER_KEY_COUNT = 2; +} + +message Modifier +{ + required ModifierType type = 1; + optional uint32 use_direction = 2 [default = 0]; + optional dmMath.Point3 position = 3; + optional dmMath.Quat rotation = 4; + + message Property + { + required ModifierKey key = 1; + repeated SplinePoint points = 2; + optional float spread = 3 [default = 0.0]; + } + repeated Property properties = 5; +} + +// NOTE: Enum values must correspond to the enum values in XXX.cpp +enum BlendMode +{ + BLEND_MODE_ALPHA = 0 [(displayName) = "Alpha"]; + BLEND_MODE_ADD = 1 [(displayName) = "Add"]; + BLEND_MODE_ADD_ALPHA = 2 [(displayName) = "Add Alpha (Deprecated)"]; + BLEND_MODE_MULT = 3 [(displayName) = "Multiply"]; + BLEND_MODE_SCREEN = 4 [(displayName) = "Screen"]; + +} + +enum SizeMode +{ + SIZE_MODE_MANUAL = 0 [(displayName) = "Manual"]; + SIZE_MODE_AUTO = 1 [(displayName) = "Auto"]; +} + +enum ParticleOrientation +{ + PARTICLE_ORIENTATION_DEFAULT = 0 [(displayName) = "Default"]; + PARTICLE_ORIENTATION_INITIAL_DIRECTION = 1 [(displayName) = "Initial Direction"]; + PARTICLE_ORIENTATION_MOVEMENT_DIRECTION = 2 [(displayName) = "Movement direction"]; + PARTICLE_ORIENTATION_ANGULAR_VELOCITY = 3 [(displayName) = "Angular Velocity"]; +} + +message Emitter +{ + optional string id = 1 [default = "emitter"]; + required PlayMode mode = 2; + optional float duration = 3 [default = 0]; + + required EmissionSpace space = 4; + optional dmMath.Point3 position = 5; + optional dmMath.Quat rotation = 6; + + required string tile_source = 7 [(resource)=true]; + required string animation = 8; + required string material = 9 [(resource)=true]; + optional BlendMode blend_mode = 10 [default = BLEND_MODE_ALPHA]; + optional ParticleOrientation particle_orientation = 11 [default = PARTICLE_ORIENTATION_DEFAULT]; + optional float inherit_velocity = 12 [default = 0.0]; + + required uint32 max_particle_count = 13; + + required EmitterType type = 14; + optional float start_delay = 15 [default = 0.0]; + + message Property + { + required EmitterKey key = 1; + repeated SplinePoint points = 2; + optional float spread = 3 [default = 0.0]; + } + repeated Property properties = 16; + + message ParticleProperty + { + required ParticleKey key = 1; + repeated SplinePoint points = 2; + } + repeated ParticleProperty particle_properties = 17; + + repeated Modifier modifiers = 18; + + optional SizeMode size_mode = 19 [default = SIZE_MODE_MANUAL]; + + optional float start_delay_spread = 20 [default = 0.0]; + optional float duration_spread = 21 [default = 0.0]; + + optional bool stretch_with_velocity = 22 [default = false]; + + optional float start_offset = 23 [default = 0.0]; + optional dmMath.Point3 pivot = 24; + + repeated dmGraphics.VertexAttribute attributes = 25; +} + +message ParticleFX +{ + repeated Emitter emitters = 1; + repeated Modifier modifiers = 2; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/render/font_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/render/font_ddf.proto new file mode 100644 index 0000000..506033e --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/render/font_ddf.proto @@ -0,0 +1,129 @@ +syntax = "proto2"; +package dmRenderDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.render.proto"; +option java_outer_classname = "Font"; + +// Public API +enum FontTextureFormat +{ + TYPE_BITMAP = 0 [(displayName) = "Bitmap"]; + TYPE_DISTANCE_FIELD = 1 [(displayName) = "Distance Field"]; +} + +// Public API +enum FontRenderMode +{ + MODE_SINGLE_LAYER = 0 [(displayName) = "Single Layer"]; + MODE_MULTI_LAYER = 1 [(displayName) = "Multi Layer"]; +} + +// Source format only +message FontDesc +{ + required string font = 1 [(resource)=true]; + required string material = 2 [(resource)=true]; + required uint32 size = 3; + optional uint32 antialias = 4 [default = 1]; + optional float alpha = 5 [default = 1.0]; + optional float outline_alpha = 6 [default = 0.0]; + optional float outline_width = 7 [default = 0.0]; + optional float shadow_alpha = 8 [default = 0.0]; + optional uint32 shadow_blur = 9 [default = 0]; + optional float shadow_x = 10 [default = 0.0]; + optional float shadow_y = 11 [default = 0.0]; + optional string extra_characters = 12 [default = ""]; // Deprecated + optional FontTextureFormat output_format = 13 [default = TYPE_BITMAP]; + + optional bool all_chars = 14 [default = false]; + optional uint32 cache_width = 15 [default = 0]; + optional uint32 cache_height = 16 [default = 0]; + optional FontRenderMode render_mode = 17 [default = MODE_SINGLE_LAYER]; + optional string characters = 18 [default = ""]; +} + +// Used at runtime +message GlyphBank +{ + +// TODO: Bob shouldn't write down X/Y, in order to slim down content! + message Glyph + { + required uint32 character = 1; // utf32 codepoint + optional float width = 2 [default = 0]; // glyph width + optional float advance = 3 [default = 0.0]; + optional float left_bearing = 4 [default = 0.0]; + optional int32 ascent = 5 [default = 0]; // WHY 32 bits??? + optional int32 descent = 6 [default = 0]; // WHY 32 bits??? + optional int32 x = 7 [default = 0]; // only used in editor preview + optional int32 y = 8 [default = 0]; // only used in editor preview + optional uint64 glyph_data_offset = 9; // WHY 64 bits??? + optional uint64 glyph_data_size = 10; // 32 bits should suffice? + } + + repeated Glyph glyphs = 1; + optional uint64 glyph_padding = 2 [default = 0]; + optional uint32 glyph_channels = 3 [default = 0]; + optional bytes glyph_data = 4; // glyph data may be compressed + + optional float max_ascent = 5 [default = 0.0]; + optional float max_descent = 6 [default = 0.0]; + optional float max_advance = 7 [default = 0.0]; + optional float max_width = 8 [default = 0.0]; + optional float max_height = 9 [default = 0.0]; + optional FontTextureFormat image_format = 10 [default = TYPE_BITMAP]; + + // TODO: Move to fontDesc + optional float sdf_spread = 11 [default = 1.0]; + optional float sdf_outline = 12 [default = 0.0]; + optional float sdf_shadow = 13 [default = 0.0]; + + // TODO: Move to fontDesc + optional uint32 cache_width = 14 [default = 0]; + optional uint32 cache_height = 15 [default = 0]; + + optional uint32 cache_cell_width = 16 [default = 0]; + optional uint32 cache_cell_height = 17 [default = 0]; + optional uint32 cache_cell_max_ascent = 18 [default = 0]; + + optional uint32 padding = 19 [default = 0]; + optional bool is_monospaced = 20 [default = false]; +} + +// Used at runtime. +message FontMap +{ + optional string material = 1 [default = "", (resource)=true]; + optional string glyph_bank = 2 [default = "", (resource)=true]; + optional string font = 3 [default = "", (resource)=true]; + + optional uint32 size = 4 [default = 0]; + optional uint32 antialias = 5 [default = 1]; + optional float shadow_x = 6 [default = 0.0]; + optional float shadow_y = 7 [default = 0.0]; + optional uint32 shadow_blur = 8 [default = 0]; + optional float shadow_alpha = 9 [default = 1.0]; + optional float alpha = 10 [default = 1.0]; + optional float outline_alpha = 11 [default = 1.0]; + optional float outline_width = 12 [default = 0.0]; + optional uint32 layer_mask = 13 [default = 1]; + + optional FontTextureFormat output_format = 14 [default = TYPE_BITMAP]; + optional FontRenderMode render_mode = 15 [default = MODE_SINGLE_LAYER]; + + optional bool all_chars = 16 [default = false]; // 0x000000 - 0x10FFFF + optional string characters = 17 [default = ""]; + + // If zero, it'll grow dynamically to our max size (2k x 4k) + optional uint32 cache_width = 18 [default = 0]; + optional uint32 cache_height = 19 [default = 0]; + + optional float sdf_spread = 20 [default = 1.0]; + optional float sdf_outline = 21 [default = 0.0]; + optional float sdf_shadow = 22 [default = 0.0]; + optional uint32 padding = 23 [default = 0]; +} + diff --git a/.agents/skills/defold-skill-maintain/assets/proto/render/material_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/render/material_ddf.proto new file mode 100644 index 0000000..38df5ba --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/render/material_ddf.proto @@ -0,0 +1,106 @@ +syntax = "proto2"; +package dmRenderDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; +import "graphics/graphics_ddf.proto"; + +option java_package = "com.dynamo.render.proto"; +option java_outer_classname = "Material"; + +message MaterialDesc +{ + enum ConstantType + { + CONSTANT_TYPE_USER = 0; + CONSTANT_TYPE_VIEWPROJ = 1; + CONSTANT_TYPE_WORLD = 2; + CONSTANT_TYPE_TEXTURE = 3; + CONSTANT_TYPE_VIEW = 4; + CONSTANT_TYPE_PROJECTION = 5; + CONSTANT_TYPE_NORMAL = 6; + CONSTANT_TYPE_WORLDVIEW = 7; + CONSTANT_TYPE_WORLDVIEWPROJ = 8; + CONSTANT_TYPE_USER_MATRIX4 = 9; + } + + message Constant + { + required string name = 1; + required ConstantType type = 2; + repeated dmMath.Vector4 value = 3; + } + + enum VertexSpace + { + VERTEX_SPACE_WORLD = 0; + VERTEX_SPACE_LOCAL = 1; + } + + enum WrapMode + { + WRAP_MODE_REPEAT = 0; + WRAP_MODE_MIRRORED_REPEAT = 1; + WRAP_MODE_CLAMP_TO_EDGE = 2; + } + + enum FilterModeMin + { + FILTER_MODE_MIN_NEAREST = 0; + FILTER_MODE_MIN_LINEAR = 1; + FILTER_MODE_MIN_NEAREST_MIPMAP_NEAREST = 2; + FILTER_MODE_MIN_NEAREST_MIPMAP_LINEAR = 3; + FILTER_MODE_MIN_LINEAR_MIPMAP_NEAREST = 4; + FILTER_MODE_MIN_LINEAR_MIPMAP_LINEAR = 5; + FILTER_MODE_MIN_DEFAULT = 6; + } + + enum FilterModeMag + { + FILTER_MODE_MAG_NEAREST = 0; + FILTER_MODE_MAG_LINEAR = 1; + FILTER_MODE_MAG_DEFAULT = 2; + } + + message PbrParameters + { + optional bool has_parameters = 1; + optional bool has_metallic_roughness = 2; + optional bool has_specular_glossiness = 3; + optional bool has_clearcoat = 4; + optional bool has_transmission = 5; + optional bool has_ior = 6; + optional bool has_specular = 7; + optional bool has_volume = 8; + optional bool has_sheen = 9; + optional bool has_emissive_strength = 10; + optional bool has_iridescence = 11; + } + + message Sampler + { + required string name = 1; // uniform name + required WrapMode wrap_u = 2; + required WrapMode wrap_v = 3; + required FilterModeMin filter_min = 4; + required FilterModeMag filter_mag = 5; + optional float max_anisotropy = 6 [default = 1.0]; + repeated uint64 name_indirections = 7 [(runtime_only)=true]; // sampler name -> list of shader uniform hashes + optional string texture = 8 [(resource)=true]; + optional uint64 name_hash = 9 [(runtime_only)=true]; // Internal, for faster lookups + } + + required string name = 1; + repeated string tags = 2; + required string vertex_program = 3 [(resource)=true]; + required string fragment_program = 4 [(resource)=true]; + optional VertexSpace vertex_space = 5; + repeated Constant vertex_constants = 6; + repeated Constant fragment_constants = 7; + repeated string textures = 8; // deprecated + repeated Sampler samplers = 9; + optional uint32 max_page_count = 10 [default = 0]; + repeated dmGraphics.VertexAttribute attributes = 11; + optional string program = 12 [(resource)=true, (runtime_only)=true]; + optional PbrParameters pbr_parameters = 13 [(runtime_only)=true]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/render/render_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/render/render_ddf.proto new file mode 100644 index 0000000..346e109 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/render/render_ddf.proto @@ -0,0 +1,165 @@ +syntax = "proto2"; +package dmRenderDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.render.proto"; +option java_outer_classname = "Render"; + +message RenderPrototypeDesc +{ + // Deprecated + message MaterialDesc + { + required string name = 1; + required string material = 2 [(resource) = true]; + } + + message RenderResourceDesc + { + required string name = 1; + required string path = 2 [(resource) = true]; + } + + required string script = 1 [(resource) = true]; + repeated MaterialDesc materials = 2; // Deprecated + repeated RenderResourceDesc render_resources = 3; +} + +/*# Rendering API documentation + * + * @document + * @name Render + * @namespace render + * @language Lua + */ + +message DrawText +{ + required dmMath.Point3 position = 1; + required string text = 2; +} + +/*# draw a text on the screen + * Draw a text on the screen. This should be used for debugging purposes only. + * + * @message + * @name draw_debug_text + * @param position [type:vector3] position of the text + * @param text [type:string] the text to draw + * @param color [type:vector4] color of the text + * @examples + * + * ```lua + * msg.post("@render:", "draw_debug_text", { text = "Hello world!", position = vmath.vector3(200, 200, 0), color = vmath.vector4(1, 0, 0, 1) } ) + * ``` + */ +message DrawDebugText +{ + required dmMath.Point3 position = 1; + required string text = 2; + required dmMath.Vector4 color = 3; +} + +/*# draw a line on the screen + * Draw a line on the screen. This should mostly be used for debugging purposes. + * + * @message + * @name draw_line + * @param start_point [type:vector3] start point of the line + * @param end_point [type:vector3] end point of the line + * @param color [type:vector4] color of the line + * @examples + * + * ```lua + * -- draw a white line from (200, 200) to (200, 300) + * msg.post("@render:", "draw_line", { start_point = vmath.vector3(200, 200, 0), end_point = vmath.vector3(200, 300, 0), color = vmath.vector4(1, 1, 1, 1) } ) + * ``` + */ +message DrawLine +{ + required dmMath.Point3 start_point = 1; + required dmMath.Point3 end_point = 2; + required dmMath.Vector4 color = 3; +} + +/*# reports a window size change + * Reports a change in window size. This is initiated on window resize on desktop or by orientation changes + * on mobile devices. + * + * @message + * @name window_resized + * @param height [type:number] the new window height + * @param width [type:number] the new window width + * @examples + * + * ```lua + * function on_message(self, message_id, message) + * -- check for the message + * if message_id == hash("window_resized") then + * -- the window was resized. + * end + * end + * ``` + */ +message WindowResized +{ + required uint32 width = 1; + required uint32 height = 2; +} + +/*# resizes the window + * Set the size of the game window. Only works on desktop platforms. + * + * @message + * @name resize + * @param height [type:number] the new window height + * @param width [type:number] the new window width + * @examples + * + * ```lua + * msg.post("@render:", "resize", { width = 1024, height = 768 } ) + * ``` + */ +message Resize +{ + required uint32 width = 1; + required uint32 height = 2; +} + +/*# set clear color + * Set render clear color. This is the color that appears on the screen where nothing is rendered, i.e. background. + * + * @message + * @name clear_color + * @param color [type:vector4] color to use as clear color + * @examples + * + * ```lua + * msg.post("@render:", "clear_color", { color = vmath.vector4(1, 0, 0, 0) } ) + * ``` + */ +message ClearColor +{ + required dmMath.Vector4 color = 1; +} + +message DisplayProfileQualifier +{ + required uint32 width = 1; + required uint32 height = 2; + repeated string device_models = 3; +} + +message DisplayProfile +{ + required string name = 1; + repeated DisplayProfileQualifier qualifiers = 2; +} + +message DisplayProfiles +{ + repeated DisplayProfile profiles = 1; + optional bool auto_layout_selection = 2 [default = true]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/resource/liveupdate_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/resource/liveupdate_ddf.proto new file mode 100644 index 0000000..ca2827f --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/resource/liveupdate_ddf.proto @@ -0,0 +1,141 @@ +syntax = "proto2"; +package dmLiveUpdateDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.liveupdate.proto"; +option java_outer_classname = "Manifest"; + +/* + * This enum specifies the supported hashing algorithms both for resource + * verification and signature generation. + * + * The HASH_UNKNOWN value is used as a default that cannot be used to create + * a manifest. This forces every call to explicitly specify the hashing + * algorithm in order to avoid accidentally downgrading the strength of a hash + * or unnecessarily using a too expensive hash. + */ +enum HashAlgorithm { + HASH_UNKNOWN = 0; + HASH_MD5 = 1; + HASH_SHA1 = 2; + HASH_SHA256 = 3; + HASH_SHA512 = 4; +} + +/* + * This enum specifies the supported encryption algorithms used for signature + * generation. + * + * The SIGN_UNKNOWN value is used as a default that cannot be used to create + * a manifest. This forces every call to explicitly specify the encryption + * algorithm. + */ +enum SignAlgorithm { + SIGN_UNKNOWN = 0; + SIGN_RSA = 1; +} + +/* + * Enum flag on manifest resource entry + */ +enum ResourceEntryFlag { + BUNDLED = 1; + EXCLUDED = 2; + ENCRYPTED = 4; + COMPRESSED = 8; +} + +/* + * Stores a hashdigest + */ +message HashDigest { + required bytes data = 1; +} + +/* + * The manifest header specifies general information about the manifest. + * + * - resource_hash_algorithm : The algorithm that should be used when hashing + * resources + * - signature_hash_algorithm : The algorithm that should be used when hashing + * content for signature verification + * - signature_sign_algorithm : The algorithm that should be used for + * encryption and decryption for signature + * verification + * - project_identifier : An identifier meant to uniquely identify a + * project to avoid loading a manifest for a + * different project. This is implemented as the + * SHA-1 hash of the project title + */ +message ManifestHeader { + required HashAlgorithm resource_hash_algorithm = 1 [default = HASH_SHA256]; + required HashAlgorithm signature_hash_algorithm = 2 [default = HASH_SHA256]; + required SignAlgorithm signature_sign_algorithm = 3 [default = SIGN_RSA]; + required HashDigest project_identifier = 4; +} + +/* + * An entry that is produced for each resource that is part of the manifest. + * + * - hash : The hash of the resource data. This is used to + * index each resource in the archive with their + * actual hash. + * - url : The URL that is used by the engine to identify a resource + * - url_hash : The URL in hashed for, for faster lookups and efficient storage + * - size : The size of the uncompressed resource + * - compressed_size : The size of the compressed resource. 0xFFFFFFFF if it's uncompressed + * - flags : A set of bit flasg or type ResourceEntryFlag. E.g. Is used for manifest + * verification to determine if a resource is expected + * to be in the bundle or not. + * - dependants : A list of resources (url hashes) that are required + * to load the current resource. A Collection that + * is childed to a CollectionProxy is not + * considered a dependant since it is not required + * to load the parent Collection of the + * CollectionProxy. + */ +message ResourceEntry { + required HashDigest hash = 1; + required string url = 2; + required uint64 url_hash = 3; + required uint32 size = 4; + required uint32 compressed_size = 5; + required uint32 flags = 6 [default = 0]; // ResourceEntryFlag + repeated uint64 dependants = 7; +} + +/* + * The manifest data that contains all information about the project. + * + * - header : The manifest header + * - engine_versions : A list of engine versions (specified by their + * hash and same as sys.engine_info) that are able + * to support the manifest. An engine should only + * attempt to initialize with a manifest that has + * that version of the engine listed as a + * supported engine. + * - resources : The resources that are part of the manifest. + */ +message ManifestData { + required ManifestHeader header = 1; + repeated HashDigest engine_versions = 2; + repeated ResourceEntry resources = 3; +} + +/* + * The Manifest. This is separate from ManifestData to easily create a + * signature of the manifest content. Nothing other than a single ManifestData + * entry and a single signature should be part of this entity. + * - data : A data blob, which is in fact a ManifestData message + * - signature : A cryptographic signature of the data byte array + * - version : A version number to quickly identify if we can read/support the file + * - archive_identifier : (deprecated) a hash of the corresponding archive index + */ +message ManifestFile { + required bytes data = 1; + required bytes signature = 2; + optional bytes archive_identifier = 3; // deprecated + required uint32 version = 4 [default = 0]; // dmResource::MANIFEST_VERSION +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/resource/resource_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/resource/resource_ddf.proto new file mode 100644 index 0000000..c34c794 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/resource/resource_ddf.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +package dmResourceDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.resource.proto"; +option java_outer_classname = "Resource"; + +message Reload +{ + repeated string resources = 1; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/rig_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/rig_ddf.proto new file mode 100644 index 0000000..aa3c1b9 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/rig_ddf.proto @@ -0,0 +1,323 @@ +syntax = "proto2"; +package dmRigDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.rig.proto"; +option java_outer_classname = "Rig"; + + +enum AlphaMode { + ALPHA_MODE_OPAQUE = 0; + ALPHA_MODE_MASK = 1; + ALPHA_MODE_BLEND = 2; + ALPHA_MODE_MAX_ENUM= 3; +} + +// We're not really using the Image concept at runtime +// message Image +// { +// optional string name = 1; +// optional string uri = 2; +// optional string mimetype = 3; +// optional uint32 target = 4; +// optional uint32 index = 5; +// repeated bytes buffer = 6; +// } + +message Sampler +{ + optional string name = 1; + optional uint32 index = 2; // Index into the scene samplers list + optional uint32 magFilter = 3 [default = 9728]; // Required=No, No default, NEAREST=9728, LINEAR=9729 + optional uint32 minFilter = 4 [default = 9728]; // Required=No, No default, NEAREST=9728, LINEAR=9729 + optional uint32 wrapS = 5 [default = 10497]; // Required=No, Default=10497 (REPEAT) + optional uint32 wrapT = 6 [default = 10497]; // Required=No, Default=10497 (REPEAT) +} + +message Texture +{ + optional string name = 1; + optional int32 index = 2 [default = -1]; // Index into the scene textures, -1 if not set + optional string path = 3; // Technically an Image struct + optional Sampler sampler = 4; +} + +// KHR_texture_transform: https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_transform/README.md +message TextureTransform +{ + optional float offset_x = 1 [default = 0]; + optional float offset_y = 2 [default = 0]; + optional float scale_x = 3 [default = 1]; + optional float scale_y = 4 [default = 1]; + optional float rotation = 5 [default = 0]; + optional int32 texcoord = 6 [default = -1]; // -1 if not set +} + +message TextureView +{ + optional Texture texture = 1; + optional int32 texcoord = 2 [default = -1]; // It should be set + optional float scale = 3 [default = 1]; + optional TextureTransform transform = 4; +} + +// https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-material-pbrmetallicroughness +message PbrMetallicRoughness +{ + optional TextureView baseColorTexture = 1; + optional TextureView metallicRoughnessTexture= 2; + optional dmMath.Vector4One baseColorFactor = 3; + optional float metallicFactor = 4 [default = 1]; + optional float roughnessFactor = 5 [default = 1]; +} + +// https://kcoley.github.io/glTF/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness/ +message PbrSpecularGlossiness +{ + optional TextureView diffuseTexture = 1; + optional TextureView specularGlossinessTexture = 2; + optional dmMath.Vector4One diffuseFactor = 3; + optional dmMath.Vector3One specularFactor = 4; + optional float glossinessFactor = 5 [default = 1]; +} + +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md +message Clearcoat +{ + optional TextureView clearcoatTexture = 1; + optional TextureView clearcoatRoughnessTexture = 2; + optional TextureView clearcoatNormalTexture = 3; + optional float clearcoatFactor = 4 [default = 0]; + optional float clearcoatRoughnessFactor = 5 [default = 0]; +} + +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_transmission/README.md +message Transmission +{ + optional TextureView transmissionTexture = 1; + optional float transmissionFactor = 2 [default = 0]; +} + +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_ior/README.md +message Ior +{ + optional float ior = 1 [default = 0]; +} + +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_specular/README.md#extending-materials +message Specular +{ + optional TextureView specularTexture = 1; + optional TextureView specularColorTexture = 2; + optional dmMath.Vector3One specularColorFactor = 3; + optional float specularFactor = 4 [default = 1]; +} + +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_volume/README.md#properties +message Volume +{ + optional TextureView thicknessTexture = 1; + optional float thicknessFactor = 2 [default = 0]; + optional dmMath.Vector3One attenuationColor = 3; + optional float attenuationDistance = 4 [default = -1.0]; // +Infinity as default +}; + +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_sheen/README.md#sheen +message Sheen +{ + optional TextureView sheenColorTexture = 1; + optional TextureView sheenRoughnessTexture = 2; + optional dmMath.Vector3 sheenColorFactor = 3; + optional float sheenRoughnessFactor = 4 [default = 0]; +}; + +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_emissive_strength/README.md#parameters +message EmissiveStrength +{ + optional float emissiveStrength = 1 [default = 1]; +} + +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_iridescence/README.md#properties +message Iridescence +{ + optional float iridescenceFactor = 1 [default = 0]; + optional TextureView iridescenceTexture = 2; + optional float iridescenceIor = 3 [default = 1.3]; + optional float iridescenceThicknessMin = 4 [default = 100.0]; + optional float iridescenceThicknessMax = 5 [default = 400.0]; + optional TextureView iridescenceThicknessTexture = 6; +}; + +// https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-material +message Material +{ + optional string name = 1; + optional uint32 index = 2; // index into the scene materials + optional bool isSkinned = 3; // set if the material is skinned + optional PbrMetallicRoughness pbrMetallicRoughness = 4; + optional PbrSpecularGlossiness pbrSpecularGlossiness = 5; + optional Clearcoat clearcoat = 6; + optional Ior ior = 7; + optional Specular specular = 8; + optional Sheen sheen = 9; + optional Transmission transmission = 10; + optional Volume volume = 11; + optional EmissiveStrength emissiveStrength = 12; + optional Iridescence iridescence = 13; + optional TextureView normalTexture = 14; + optional TextureView occlusionTexture = 15; + optional TextureView emissiveTexture = 16; + optional dmMath.Vector3 emissiveFactor = 17; + optional float alphaCutoff = 18 [default = 0.5]; + optional AlphaMode alphaMode = 19 [default = ALPHA_MODE_OPAQUE]; + optional bool doubleSided = 20 [default = false]; + optional bool unlit = 21 [default = false]; + optional uint64 materialHash = 22; +} + +message Bone +{ + option (struct_align) = true; + + // 0xFFFFFFFF means no parent + required uint32 parent = 1; + required uint64 id = 2; // the bone name hash + required string name = 3; // For easier debugging at runtime + required dmMath.Transform local = 4 [(field_align)=true]; // Deprecated/unused (only in unit test=) + required dmMath.Transform world = 5 [(field_align)=true]; // Deprecated/unused + required dmMath.Transform inverse_bind_pose = 6 [(field_align)=true]; + + optional float length = 7 [default = 0]; +} + +message IK +{ + required uint64 id = 1; + required uint32 parent = 2; + required uint32 child = 3; + required uint32 target = 4; + optional bool positive = 5 [default = true]; + optional float mix = 6 [default = 1.0]; +} + +message Skeleton +{ + repeated Bone bones = 1; + repeated IK iks = 2; +} + +message AnimationTrack +{ + required uint64 bone_id = 1; // the bone name hash + // x0, y0, z0, ... + repeated float positions = 2; + // x0, x0, z0, w0, … + repeated float rotations = 3; + // x0, y0, z0, … + repeated float scale = 4; +} + +message EventKey +{ + required float t = 1; + optional int32 integer = 2 [default = 0]; + optional float float = 3 [default = 0.0]; + optional uint64 string = 4 [default = 0]; +} + +message EventTrack +{ + required uint64 event_id = 1; + repeated EventKey keys = 2; +} + +message RigAnimation +{ + required uint64 id = 1; + required float duration = 2; + required float sample_rate = 3; + repeated AnimationTrack tracks = 4; + repeated EventTrack event_tracks = 5; +} + +message AnimationSet +{ + repeated RigAnimation animations = 1; +} + +message AnimationInstanceDesc +{ + required string animation = 1 [(resource)=true]; +} + +message AnimationSetDesc +{ + repeated AnimationInstanceDesc animations = 1; + optional string skeleton = 2; +} + +enum IndexBufferFormat +{ + INDEXBUFFER_FORMAT_16 = 0; + INDEXBUFFER_FORMAT_32 = 1; +} + +message Mesh +{ + required dmMath.Vector3 aabb_min = 1; + required dmMath.Vector3 aabb_max = 2; + + repeated float positions = 3; + repeated float normals = 4; + repeated float tangents = 5; + repeated float colors = 6; + repeated float texcoord0 = 7; + optional uint32 num_texcoord0_components = 8; // max 3 + repeated float texcoord1 = 9; + optional uint32 num_texcoord1_components = 10; // max 3 + + optional bytes indices = 11; // indices for interleaved vertex buffer + optional IndexBufferFormat indices_format = 12; // format of values in indices + + // w00, w01, w02, w03, w10, … (only specified for skinned meshes) + repeated float weights = 13; + // i00, i01, i02, i03, i10, … (only specified for skinned meshes) + repeated uint32 bone_indices = 14; + + optional uint32 material_index = 15; // index into the mesh set material list + +} + +message Model // E.g. the Node in the Scene +{ + option (struct_align) = true; + + required dmMath.Transform local = 1 [(field_align)=true]; + required uint64 id = 2; // E.g. "torso", "head". Or simply "character" + repeated Mesh meshes = 3; + // If set, then this model should be transformed as a child of the bone + optional uint64 bone_id = 4 [default = 0]; // hash of bone id +} + +message MeshSet +{ + repeated Model models = 1; // There may be more than one object in a scene + repeated Material materials = 2; + + // List of bone names that represent the order of the bone influences. + // Not used for Spine rigs since they don't have support for external skeletons. + repeated uint64 bone_list = 3; + // Max number of bones used in any of the meshes (in the bone_indices list) + optional uint32 max_bone_count = 4; +} + +// Public api (dmSDK) +message RigScene +{ + optional string skeleton = 1 [(resource)=true]; + optional string animation_set = 2 [(resource)=true]; + required string mesh_set = 3 [(resource)=true]; + optional string texture_set = 4 [(resource)=true]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/script/ddf_script.proto b/.agents/skills/defold-skill-maintain/assets/proto/script/ddf_script.proto new file mode 100644 index 0000000..238abf3 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/script/ddf_script.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; +package dmScriptDDF; + +import "ddf/ddf_extensions.proto"; + +option java_package = "com.dynamo.ddfscript.proto"; +option java_outer_classname = "DdfScript"; + +// Field 'ref' is a local reference into lua table 'context_table_ref'. See usage in script_ddf.cpp. +// Currently used for passing gui node reference in spine event message +message LuaRef +{ + required int32 ref = 1 [default = 0]; + required int32 context_table_ref = 2 [default = 0]; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/script/lua_source_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/script/lua_source_ddf.proto new file mode 100644 index 0000000..408d0de --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/script/lua_source_ddf.proto @@ -0,0 +1,27 @@ +syntax = "proto2"; +package dmLuaDDF; + +option java_package = "com.dynamo.script.proto"; +option java_outer_classname = "Lua"; + +message LuaSource +{ + // HTML platforms uses script (vanilla lua), all + // other platforms uses bytecode instead. + optional bytes script = 1; + + // Path to the original file; used for debugging. + // Note that if bytecode is supplied from LuaJIT it will contain embedded + // chunk names that override this file name and this field is not used. + required string filename = 2; + + // Used when we bundle bytecode+delta AND when we bundle + // only bytecode for a single architecture + optional bytes bytecode = 3; + optional bytes delta = 5; + + // These two are used when we bundle without a bytecode + // delta and for more than one architecture + optional bytes bytecode_32 = 6; + optional bytes bytecode_64 = 4; +} diff --git a/.agents/skills/defold-skill-maintain/assets/proto/script/sys_ddf.proto b/.agents/skills/defold-skill-maintain/assets/proto/script/sys_ddf.proto new file mode 100644 index 0000000..ab920e5 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/assets/proto/script/sys_ddf.proto @@ -0,0 +1,251 @@ +syntax = "proto2"; +package dmSystemDDF; + +import "ddf/ddf_extensions.proto"; +import "ddf/ddf_math.proto"; + +option java_package = "com.dynamo.system.proto"; +option java_outer_classname = "System"; + +/*# System API documentation + * + * Functions and messages for using system resources, controlling the engine, + * error handling and debugging. + * + * @document + * @name System + * @namespace sys + * @language Lua + */ + +/*# exits application + * Terminates the game application and reports the specified code to the OS. + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name exit + * @param code [type:number] exit code to report to the OS, 0 means clean exit + * @examples + * + * This examples demonstrates how to exit the application when some kind of quit messages is received (maybe from gui or similar): + * + * ```lua + * function on_message(self, message_id, message, sender) + * if message_id == hash("quit") then + * msg.post("@system:", "exit", {code = 0}) + * end + * end + * ``` + */ +message Exit +{ + required int32 code = 1; +} + +/*# shows/hides the on-screen profiler + * Toggles the on-screen profiler. + * The profiler is a real-time tool that shows the numbers of milliseconds spent + * in each scope per frame as well as counters. The profiler is very useful for + * tracking down performance and resource problems. + * + * In addition to the on-screen profiler, Defold includes a web-based profiler that + * allows you to sample a series of data points and then analyze them in detail. + * The web profiler is available at `http://:8002` where is + * the IP address of the device you are running your game on. + * + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name toggle_profile + * @examples + * + * ```lua + * msg.post("@system:", "toggle_profile") + * ``` + */ +message ToggleProfile {} + +/*# shows/hides the on-screen physics visual debugging + * Toggles the on-screen physics visual debugging mode which is very useful for + * tracking down issues related to physics. This mode visualizes + * all collision object shapes and normals at detected contact points. Toggling + * this mode on is equal to setting `physics.debug` in the "game.project" settings, + * but set in run-time. + * + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name toggle_physics_debug + * @examples + * @examples + * + * ```lua + * msg.post("@system:", "toggle_physics_debug") + * ``` + */ +message TogglePhysicsDebug {} + +/*# starts video recording + * Starts video recording of the game frame-buffer to file. Current video format is the + * open vp8 codec in the ivf container. It's possible to upload this format directly + * to YouTube. The VLC video player has native support but with the known issue that + * not the entire file is played back. It's probably an issue with VLC. + * The Miro Video Converter has support for vp8/ivf. + * + * [icon:macos] [icon:windows] [icon:linux] Video recording is only supported on desktop platforms. + * + * [icon:attention] Audio is currently not supported + * + * [icon:attention] Window width and height must be a multiple of 8 to be able to record video. + * + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name start_record + * @param file_name [type:string] file name to write the video to + * @param frame_period [type:number] frame period to record, ie write every nth frame. Default value is `2` + * @param fps [type:number] frames per second. Playback speed for the video. Default value is `30`. The fps value doens't affect the recording. It's only meta-data in the written video file. + * @examples + * + * Record a video in 30 fps given that the native game fps is 60: + * + * ```lua + * msg.post("@system:", "start_record", { file_name = "test_rec.ivf" } ) + * ``` + * + * To write a video in 60 fps given that the native game fps is 60: + * + * ```lua + * msg.post("@system:", "start_record", { file_name = "test_rec.ivf", frame_period = 1, fps = 60 } ) + * ``` + */ +message StartRecord +{ + required string file_name = 1; + optional int32 frame_period = 2 [default = 2]; + optional int32 fps = 3 [ default = 30 ]; +} + +/*# stop current video recording + * Stops the currently active video recording. + * + * [icon:macos] [icon:windows] [icon:linux] Video recording is only supported on desktop platforms. + * + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name stop_record + * @examples + * + * ```lua + * msg.post("@system:", "stop_record") + * ``` + */ +message StopRecord +{ +} + + +/*# reboot engine with arguments + * Reboots the game engine with a specified set of arguments. + * Arguments will be translated into command line arguments. Sending the reboot + * command is equivalent to starting the engine with the same arguments. + * + * On startup the engine reads configuration from "game.project" in the + * project root. + * + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name reboot + * @param arg1 [type:string] argument 1 + * @param arg2 [type:string] argument 2 + * @param arg3 [type:string] argument 3 + * @param arg4 [type:string] argument 4 + * @param arg5 [type:string] argument 5 + * @param arg6 [type:string] argument 6 + * @examples + * + * How to reboot engine with a specific bootstrap collection. + * + * ```lua + * local arg1 = '--config=bootstrap.main_collection=/my.collectionc' + * local arg2 = 'build/game.projectc' + * msg.post("@system:", "reboot", {arg1 = arg1, arg2 = arg2}) + * ``` + */ +message Reboot +{ + // We don't support repeated value in script_ddf. Probably for a good reason. + optional string arg1 = 1; + optional string arg2 = 2; + optional string arg3 = 3; + optional string arg4 = 4; + optional string arg5 = 5; + optional string arg6 = 6; +} + +/*# set vsync swap interval + * Set the vsync swap interval. The interval with which to swap the front and back buffers + * in sync with vertical blanks (v-blank), the hardware event where the screen image is updated + * with data from the front buffer. A value of 1 swaps the buffers at every v-blank, a value of + * 2 swaps the buffers every other v-blank and so on. A value of 0 disables waiting for v-blank + * before swapping the buffers. Default value is 1. + * + * When setting the swap interval to 0 and having `vsync` disabled in + * "game.project", the engine will try to respect the set frame cap value from + * "game.project" in software instead. + * + * This setting may be overridden by driver settings. + * + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name set_vsync + * @param swap_interval [type:number] target swap interval. + * @examples + *
+ * msg.post("@system:", "set_vsync", { swap_interval = 1 } )
+ * 
+ */ +message SetVsync +{ + required int32 swap_interval = 1 [default = 1]; +} + +/*# set update frequency + * Set game update-frequency (frame cap). This option is equivalent to `display.update_frequency` in + * the "game.project" settings but set in run-time. If `Vsync` checked in "game.project", the rate will + * be clamped to a swap interval that matches any detected main monitor refresh rate. If `Vsync` is + * unchecked the engine will try to respect the rate in software using timers. There is no + * guarantee that the frame cap will be achieved depending on platform specifics and hardware settings. + * + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name set_update_frequency + * @param frequency [type:number] target frequency. 60 for 60 fps + * @examples + *
+ * msg.post("@system:", "set_update_frequency", { frequency = 60 } )
+ * 
+ */ +message SetUpdateFrequency +{ + required int32 frequency = 1; +} + +/*# resume rendering + * Resume rendering. + * This message can only be sent to the designated `@system` socket. + * + * @message + * @name resume_rendering + * @examples + *
+ * msg.post("@system:", "resume_rendering")
+ * 
+ */ +message ResumeRendering +{ +} diff --git a/.agents/skills/defold-skill-maintain/references/The-Complete-Guide-to-Building-Skill-for-Claude.md b/.agents/skills/defold-skill-maintain/references/The-Complete-Guide-to-Building-Skill-for-Claude.md new file mode 100644 index 0000000..96ea6d7 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/references/The-Complete-Guide-to-Building-Skill-for-Claude.md @@ -0,0 +1,1250 @@ +> [A complete guide to building skills for Claude | Claude Blog](https://claude.com/blog/complete-guide-to-building-skills-for-claude)에서 공개한 [PDF](https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf)를 마크다운 기반으로 변경한 파일입니다. + +---- + +# The Complete Guide to Building Skills for Claude + +--- + +## Contents + +- [Introduction](#introduction) +- [Chapter 1: Fundamentals](#chapter-1-fundamentals) +- [Chapter 2: Planning and Design](#chapter-2-planning-and-design) +- [Chapter 3: Testing and Iteration](#chapter-3-testing-and-iteration) +- [Chapter 4: Distribution and Sharing](#chapter-4-distribution-and-sharing) +- [Chapter 5: Patterns and Troubleshooting](#chapter-5-patterns-and-troubleshooting) +- [Chapter 6: Resources and References](#chapter-6-resources-and-references) +- [Reference A: Quick Checklist](#reference-a-quick-checklist) +- [Reference B: YAML Frontmatter](#reference-b-yaml-frontmatter) +- [Reference C: Complete Skill Examples](#reference-c-complete-skill-examples) + +--- + +## Introduction + +A skill is a set of instructions - packaged as a simple folder - that teaches Claude how to handle specific tasks or workflows. Skills are one of the most powerful ways to customize Claude for your specific needs. Instead of re-explaining your preferences, processes, and domain expertise in every conversation, skills let you teach Claude once and benefit every time. + +Skills are powerful when you have repeatable workflows: generating frontend designs from specs, conducting research with consistent methodology, creating documents that follow your team's style guide, or orchestrating multi-step processes. They work well with Claude's built-in capabilities like code execution and document creation. For those building MCP integrations, skills add another powerful layer helping turn raw tool access into reliable, optimized workflows. + +This guide covers everything you need to know to build effective skills - from planning and structure to testing and distribution. Whether you're building a skill for yourself, your team, or for the community, you'll find practical patterns and real-world examples throughout. + +**What you'll learn:** + +- Technical requirements and best practices for skill structure +- Patterns for standalone skills and MCP-enhanced workflows +- Patterns we've seen work well across different use cases +- How to test, iterate, and distribute your skills + +**Who this is for:** + +- Developers who want Claude to follow specific workflows consistently +- Power users who want Claude to follow specific workflows +- Teams looking to standardize how Claude works across their organization + +### Two Paths Through This Guide + +Building standalone skills? Focus on Fundamentals, Planning and Design, and category 1-2. Enhancing an MCP integration? The "Skills + MCP" section and category 3 are for you. Both paths share the same technical requirements, but you choose what's relevant to your use case. + +**What you'll get out of this guide:** By the end, you'll be able to build a functional skill in a single sitting. Expect about 15-30 minutes to build and test your first working skill using the skill-creator. + +Let's get started. + +--- + +## Chapter 1: Fundamentals + +### What is a skill? + +A skill is a folder containing: + +- **SKILL.md** (required): Instructions in Markdown with YAML frontmatter +- **scripts/** (optional): Executable code (Python, Bash, etc.) +- **references/** (optional): Documentation loaded as needed +- **assets/** (optional): Templates, fonts, icons used in output + +### Core design principles + +#### Progressive Disclosure + +Skills use a three-level system: + +- **First level (YAML frontmatter):** Always loaded in Claude's system prompt. Provides just enough information for Claude to know when each skill should be used without loading all of it into context. +- **Second level (SKILL.md body):** Loaded when Claude thinks the skill is relevant to the current task. Contains the full instructions and guidance. +- **Third level (Linked files):** Additional files bundled within the skill directory that Claude can choose to navigate and discover only as needed. + +This progressive disclosure minimizes token usage while maintaining specialized expertise. + +#### Composability + +Claude can load multiple skills simultaneously. Your skill should work well alongside others, not assume it's the only capability available. + +#### Portability + +Skills work identically across Claude.ai, Claude Code, and API. Create a skill once and it works across all surfaces without modification, provided the environment supports any dependencies the skill requires. + +### For MCP Builders: Skills + Connectors + +> 💡 Building standalone skills without MCP? Skip to [Planning and Design](#chapter-2-planning-and-design) - you can always return here later. + +If you already have a working MCP server, you've done the hard part. Skills are the knowledge layer on top - capturing the workflows and best practices you already know, so Claude can apply them consistently. + +#### The kitchen analogy + +MCP provides the professional kitchen: access to tools, ingredients, and equipment. + +Skills provide the recipes: step-by-step instructions on how to create something valuable. + +Together, they enable users to accomplish complex tasks without needing to figure out every step themselves. + +#### How they work together: + +| MCP (Connectivity) | Skills (Knowledge) | +|---|---| +| Connects Claude to your service (Notion, Asana, Linear, etc.) | Teaches Claude how to use your service effectively | +| Provides real-time data access and tool invocation | Captures workflows and best practices | +| What Claude can do | How Claude should do it | + +#### Why this matters for your MCP users + +**Without skills:** + +- Users connect your MCP but don't know what to do next +- Support tickets asking "how do I do X with your integration" +- Each conversation starts from scratch +- Inconsistent results because users prompt differently each time +- Users blame your connector when the real issue is workflow guidance + +**With skills:** + +- Pre-built workflows activate automatically when needed +- Consistent, reliable tool usage +- Best practices embedded in every interaction +- Lower learning curve for your integration + +--- + +## Chapter 2: Planning and Design + +### Start with use cases + +Before writing any code, identify 2-3 concrete use cases your skill should enable. + +**Good use case definition:** + +``` +Use Case: Project Sprint Planning +Trigger: User says "help me plan this sprint" or "create sprint tasks" +Steps: +1. Fetch current project status from Linear (via MCP) +2. Analyze team velocity and capacity +3. Suggest task prioritization +4. Create tasks in Linear with proper labels and estimates +Result: Fully planned sprint with tasks created +``` + +**Ask yourself:** + +- What does a user want to accomplish? +- What multi-step workflows does this require? +- Which tools are needed (built-in or MCP?) +- What domain knowledge or best practices should be embedded? + +### Common skill use case categories + +At Anthropic, we've observed three common use cases: + +#### Category 1: Document & Asset Creation + +**Used for:** Creating consistent, high-quality output including documents, presentations, apps, designs, code, etc. + +**Real example:** frontend-design skill (also see skills for docx, pptx, xlsx, and ppt) + +> "Create distinctive, production-grade frontend interfaces with high design quality. Use when building web components, pages, artifacts, posters, or applications." + +**Key techniques:** + +- Embedded style guides and brand standards +- Template structures for consistent output +- Quality checklists before finalizing +- No external tools required - uses Claude's built-in capabilities + +#### Category 2: Workflow Automation + +**Used for:** Multi-step processes that benefit from consistent methodology, including coordination across multiple MCP servers. + +**Real example:** skill-creator skill + +> "Interactive guide for creating new skills. Walks the user through use case definition, frontmatter generation, instruction writing, and validation." + +**Key techniques:** + +- Step-by-step workflow with validation gates +- Templates for common structures +- Built-in review and improvement suggestions +- Iterative refinement loops + +#### Category 3: MCP Enhancement + +**Used for:** Workflow guidance to enhance the tool access an MCP server provides. + +**Real example:** sentry-code-review skill (from Sentry) + +> "Automatically analyzes and fixes detected bugs in GitHub Pull Requests using Sentry's error monitoring data via their MCP server." + +**Key techniques:** + +- Coordinates multiple MCP calls in sequence +- Embeds domain expertise +- Provides context users would otherwise need to specify +- Error handling for common MCP issues + +### Define success criteria + +How will you know your skill is working? + +These are aspirational targets - rough benchmarks rather than precise thresholds. Aim for rigor but accept that there will be an element of vibes-based assessment. We are actively developing more robust measurement guidance and tooling. + +**Quantitative metrics:** + +- **Skill triggers on 90% of relevant queries** + - How to measure: Run 10-20 test queries that should trigger your skill. Track how many times it loads automatically vs. requires explicit invocation. +- **Completes workflow in X tool calls** + - How to measure: Compare the same task with and without the skill enabled. Count tool calls and total tokens consumed. +- **0 failed API calls per workflow** + - How to measure: Monitor MCP server logs during test runs. Track retry rates and error codes. + +**Qualitative metrics:** + +- **Users don't need to prompt Claude about next steps** + - How to assess: During testing, note how often you need to redirect or clarify. Ask beta users for feedback. +- **Workflows complete without user correction** + - How to assess: Run the same request 3-5 times. Compare outputs for structural consistency and quality. +- **Consistent results across sessions** + - How to assess: Can a new user accomplish the task on first try with minimal guidance? + +### Technical requirements + +#### File structure + +``` +your-skill-name/ +├── SKILL.md # Required - main skill file +├── scripts/ # Optional - executable code +│ ├── process_data.py # Example +│ └── validate.sh # Example +├── references/ # Optional - documentation +│ ├── api-guide.md # Example +│ └── examples/ # Example +└── assets/ # Optional - templates, etc. + └── report-template.md # Example +``` + +#### Critical rules + +**SKILL.md naming:** + +- Must be exactly `SKILL.md` (case-sensitive) +- No variations accepted (`SKILL.MD`, `skill.md`, etc.) + +**Skill folder naming:** + +- Use kebab-case: `notion-project-setup` ✅ +- No spaces: `Notion Project Setup` ❌ +- No underscores: `notion_project_setup` ❌ +- No capitals: `NotionProjectSetup` ❌ + +**No README.md:** + +- Don't include `README.md` inside your skill folder +- All documentation goes in `SKILL.md` or `references/` +- Note: when distributing via GitHub, you'll still want a repo-level README for human users — see [Distribution and Sharing](#chapter-4-distribution-and-sharing). + +### YAML frontmatter: The most important part + +The YAML frontmatter is how Claude decides whether to load your skill. Get this right. + +#### Minimal required format + +```yaml +--- +name: your-skill-name +description: What it does. Use when user asks to [specific phrases]. +--- +``` + +That's all you need to start. + +#### Field requirements + +**name** (required): + +- kebab-case only +- No spaces or capitals +- Should match folder name + +**description** (required): + +- MUST include BOTH: + - What the skill does + - When to use it (trigger conditions) +- Under 1024 characters +- No XML tags (`<` or `>`) +- Include specific tasks users might say +- Mention file types if relevant + +**license** (optional): + +- Use if making skill open source +- Common: MIT, Apache-2.0 + +**compatibility** (optional): + +- 1-500 characters +- Indicates environment requirements: e.g. intended product, required system packages, network access needs, etc. + +**metadata** (optional): + +- Any custom key-value pairs +- Suggested: author, version, mcp-server +- Example: + +```yaml +metadata: + author: ProjectHub + version: 1.0.0 + mcp-server: projecthub +``` + +#### Security restrictions + +**Forbidden in frontmatter:** + +- XML angle brackets (`<` `>`) +- Skills with "claude" or "anthropic" in name (reserved) + +**Why:** Frontmatter appears in Claude's system prompt. Malicious content could inject instructions. + +### Writing effective skills + +#### The description field + +According to Anthropic's engineering blog: "This metadata...provides just enough information for Claude to know when each skill should be used without loading all of it into context." This is the first level of progressive disclosure. + +**Structure:** + +``` +[What it does] + [When to use it] + [Key capabilities] +``` + +**Examples of good descriptions:** + +```yaml +# Good - specific and actionable +description: Analyzes Figma design files and generates developer handoff documentation. Use when user uploads .fig files, asks for "design specs", "component documentation", or "design-to-code handoff". + +# Good - includes trigger phrases +description: Manages Linear project workflows including sprint planning, task creation, and status tracking. Use when user mentions "sprint", "Linear tasks", "project planning", or asks to "create tickets". + +# Good - clear value proposition +description: End-to-end customer onboarding workflow for PayFlow. Handles account creation, payment setup, and subscription management. Use when user says "onboard new customer", "set up subscription", or "create PayFlow account". +``` + +**Examples of bad descriptions:** + +```yaml +# Too vague +description: Helps with projects. + +# Missing triggers +description: Creates sophisticated multi-page documentation systems. + +# Too technical, no user triggers +description: Implements the Project entity model with hierarchical relationships. +``` + +#### Writing the main instructions + +After the frontmatter, write the actual instructions in Markdown. + +**Recommended structure:** + +Adapt this template for your skill. Replace bracketed sections with your specific content. + +```markdown +--- +name: your-skill +description: [.] +--- + +# Your Skill Name + +# Instructions + +# Step 1: [First Major Step] +Clear explanation of what happens. + +Example: +```bash +python scripts/fetch_data.py --project-id PROJECT_ID +``` +Expected output: [describe what success looks like] + +(Add more steps as needed) + +# Examples + +## Example 1: [common scenario] +User says: "Set up a new marketing campaign" +Actions: +1. Fetch existing campaigns via MCP +2. Create new campaign with provided parameters +Result: Campaign created with confirmation link + +(Add more examples as needed) + +# Troubleshooting + +## Error: [Common error message] +Cause: [Why it happens] +Solution: [How to fix] + +(Add more error cases as needed) +``` + +#### Best Practices for Instructions + +**Be Specific and Actionable** + +✅ Good: + +``` +Run `python scripts/validate.py --input {filename}` to check data format. +If validation fails, common issues include: +- Missing required fields (add them to the CSV) +- Invalid date formats (use YYYY-MM-DD) +``` + +❌ Bad: + +``` +Validate the data before proceeding. +``` + +**Include error handling** + +```markdown +# Common Issues + +# MCP Connection Failed +If you see "Connection refused": +1. Verify MCP server is running: Check Settings > Extensions +2. Confirm API key is valid +3. Try reconnecting: Settings > Extensions > [Your Service] > Reconnect +``` + +**Reference bundled resources clearly** + +``` +Before writing queries, consult `references/api-patterns.md` for: +- Rate limiting guidance +- Pagination patterns +- Error codes and handling +``` + +**Use progressive disclosure** + +Keep SKILL.md focused on core instructions. Move detailed documentation to `references/` and link to it. (See [Core Design Principles](#core-design-principles) for how the three-level system works.) + +--- + +## Chapter 3: Testing and Iteration + +Skills can be tested at varying levels of rigor depending on your needs: + +- **Manual testing in Claude.ai** - Run queries directly and observe behavior. Fast iteration, no setup required. +- **Scripted testing in Claude Code** - Automate test cases for repeatable validation across changes. +- **Programmatic testing via skills API** - Build evaluation suites that run systematically against defined test sets. + +Choose the approach that matches your quality requirements and the visibility of your skill. A skill used internally by a small team has different testing needs than one deployed to thousands of enterprise users. + +> **Pro Tip: Iterate on a single task before expanding** +> +> We've found that the most effective skill creators iterate on a single challenging task until Claude succeeds, then extract the winning approach into a skill. This leverages Claude's in-context learning and provides faster signal than broad testing. Once you have a working foundation, expand to multiple test cases for coverage. + +### Recommended Testing Approach + +Based on early experience, effective skills testing typically covers three areas: + +#### 1. Triggering tests + +**Goal:** Ensure your skill loads at the right times. + +**Test cases:** + +- ✅ Triggers on obvious tasks +- ✅ Triggers on paraphrased requests +- ❌ Doesn't trigger on unrelated topics + +**Example test suite:** + +``` +Should trigger: +- "Help me set up a new ProjectHub workspace" +- "I need to create a project in ProjectHub" +- "Initialize a ProjectHub project for Q4 planning" + +Should NOT trigger: +- "What's the weather in San Francisco?" +- "Help me write Python code" +- "Create a spreadsheet" (unless ProjectHub skill handles sheets) +``` + +#### 2. Functional tests + +**Goal:** Verify the skill produces correct outputs. + +**Test cases:** + +- Valid outputs generated +- API calls succeed +- Error handling works +- Edge cases covered + +**Example:** + +``` +Test: Create project with 5 tasks +Given: Project name "Q4 Planning", 5 task descriptions +When: Skill executes workflow +Then: + - Project created in ProjectHub + - 5 tasks created with correct properties + - All tasks linked to project + - No API errors +``` + +#### 3. Performance comparison + +**Goal:** Prove the skill improves results vs. baseline. + +Use the metrics from [Define Success Criteria](#define-success-criteria). Here's what a comparison might look like. + +**Baseline comparison:** + +``` +Without skill: +- User provides instructions each time +- 15 back-and-forth messages +- 3 failed API calls requiring retry +- 12,000 tokens consumed + +With skill: +- Automatic workflow execution +- 2 clarifying questions only +- 0 failed API calls +- 6,000 tokens consumed +``` + +### Using the skill-creator skill + +The skill-creator skill - available in Claude.ai via plugin directory or download for Claude Code - can help you build and iterate on skills. If you have an MCP server and know your top 2–3 workflows, you can build and test a functional skill in a single sitting - often in 15–30 minutes. + +**Creating skills:** + +- Generate skills from natural language descriptions +- Produce properly formatted SKILL.md with frontmatter +- Suggest trigger phrases and structure + +**Reviewing skills:** + +- Flag common issues (vague descriptions, missing triggers, structural problems) +- Identify potential over/under-triggering risks +- Suggest test cases based on the skill's stated purpose + +**Iterative improvement:** + +- After using your skill and encountering edge cases or failures, bring those examples back to skill-creator +- Example: "Use the issues & solution identified in this chat to improve how the skill handles [specific edge case]" + +**To use:** + +``` +"Use the skill-creator skill to help me build a skill for [your use case]" +``` + +> **Note:** skill-creator helps you design and refine skills but does not execute automated test suites or produce quantitative evaluation results. + +### Iteration based on feedback + +Skills are living documents. Plan to iterate based on: + +**Undertriggering signals:** + +- Skill doesn't load when it should +- Users manually enabling it +- Support questions about when to use it + +**Solution:** Add more detail and nuance to the description - this may include keywords particularly for technical terms + +**Overtriggering signals:** + +- Skill loads for irrelevant queries +- Users disabling it +- Confusion about purpose + +**Solution:** Add negative triggers, be more specific + +**Execution issues:** + +- Inconsistent results +- API call failures +- User corrections needed + +**Solution:** Improve instructions, add error handling + +--- + +## Chapter 4: Distribution and Sharing + +Skills make your MCP integration more complete. As users compare connectors, those with skills offer a faster path to value, giving you an edge over MCP-only alternatives. + +### Current distribution model (January 2026) + +**How individual users get skills:** + +1. Download the skill folder +2. Zip the folder (if needed) +3. Upload to Claude.ai via Settings > Capabilities > Skills +4. Or place in Claude Code skills directory + +**Organization-level skills:** + +- Admins can deploy skills workspace-wide (shipped December 18, 2025) +- Automatic updates +- Centralized management + +### An open standard + +We've published Agent Skills as an open standard. Like MCP, we believe skills should be portable across tools and platforms - the same skill should work whether you're using Claude or other AI platforms. That said, some skills are designed to take full advantage of a specific platform's capabilities; authors can note this in the skill's compatibility field. We've been collaborating with members of the ecosystem on the standard, and we're excited by early adoption. + +### Using skills via API + +For programmatic use cases - such as building applications, agents, or automated workflows that leverage skills - the API provides direct control over skill management and execution. + +**Key capabilities:** + +- `/v1/skills` endpoint for listing and managing skills +- Add skills to Messages API requests via the `container.skills` parameter +- Version control and management through the Claude Console +- Works with the Claude Agent SDK for building custom agents + +**When to use skills via the API vs. Claude.ai:** + +| Use Case | Best Surface | +|---|---| +| End users interacting with skills directly | Claude.ai / Claude Code | +| Manual testing and iteration during development | Claude.ai / Claude Code | +| Individual, ad-hoc workflows | Claude.ai / Claude Code | +| Applications using skills programmatically | API | +| Production deployments at scale | API | +| Automated pipelines and agent systems | API | + +> **Note:** Skills in the API require the Code Execution Tool beta, which provides the secure environment skills need to run. + +**For implementation details, see:** + +- Skills API Quickstart +- Create Custom skills +- Skills in the Agent SDK + +### Recommended approach today + +Start by hosting your skill on GitHub with a public repo, clear README (for human visitors — this is separate from your skill folder, which should not contain a README.md), and example usage with screenshots. Then add a section to your MCP documentation that links to the skill, explains why using both together is valuable, and provides a quick-start guide. + +1. **Host on GitHub** + - Public repo for open-source skills + - Clear README with installation instructions + - Example usage and screenshots + +2. **Document in Your MCP Repo** + - Link to skills from MCP documentation + - Explain the value of using both together + - Provide quick-start guide + +3. **Create an Installation Guide** + +```markdown +# Installing the [Your Service] skill + +1. Download the skill: + - Clone repo: `git clone https://github.com/yourcompany/skills` + - Or download ZIP from Releases + +2. Install in Claude: + - Open Claude.ai > Settings > Skills + - Click "Upload skill" + - Select the skill folder (zipped) + +3. Enable the skill: + - Toggle on the [Your Service] skill + - Ensure your MCP server is connected + +4. Test: + - Ask Claude: "Set up a new project in [Your Service]" +``` + +### Positioning your skill + +How you describe your skill determines whether users understand its value and actually try it. When writing about your skill—in your README, documentation, or marketing—keep these principles in mind. + +**Focus on outcomes, not features:** + +✅ Good: + +``` +"The ProjectHub skill enables teams to set up complete project workspaces in seconds — including pages, databases, and templates — instead of spending 30 minutes on manual setup." +``` + +❌ Bad: + +``` +"The ProjectHub skill is a folder containing YAML frontmatter and Markdown instructions that calls our MCP server tools." +``` + +**Highlight the MCP + skills story:** + +``` +"Our MCP server gives Claude access to your Linear projects. Our skills teach Claude your team's sprint planning workflow. Together, they enable AI-powered project management." +``` + +--- + +## Chapter 5: Patterns and Troubleshooting + +These patterns emerged from skills created by early adopters and internal teams. They represent common approaches we've seen work well, not prescriptive templates. + +### Choosing your approach: Problem-first vs. tool-first + +Think of it like Home Depot. You might walk in with a problem - "I need to fix a kitchen cabinet" - and an employee points you to the right tools. Or you might pick out a new drill and ask how to use it for your specific job. + +Skills work the same way: + +- **Problem-first:** "I need to set up a project workspace" → Your skill orchestrates the right MCP calls in the right sequence. Users describe outcomes; the skill handles the tools. +- **Tool-first:** "I have Notion MCP connected" → Your skill teaches Claude the optimal workflows and best practices. Users have access; the skill provides expertise. + +Most skills lean one direction. Knowing which framing fits your use case helps you choose the right pattern below. + +### Pattern 1: Sequential workflow orchestration + +**Use when:** Your users need multi-step processes in a specific order. + +**Example structure:** + +```markdown +# Workflow: Onboard New Customer + +# Step 1: Create Account +Call MCP tool: `create_customer` +Parameters: name, email, company + +# Step 2: Setup Payment +Call MCP tool: `setup_payment_method` +Wait for: payment method verification + +# Step 3: Create Subscription +Call MCP tool: `create_subscription` +Parameters: plan_id, customer_id (from Step 1) + +# Step 4: Send Welcome Email +Call MCP tool: `send_email` +Template: welcome_email_template +``` + +**Key techniques:** + +- Explicit step ordering +- Dependencies between steps +- Validation at each stage +- Rollback instructions for failures + +### Pattern 2: Multi-MCP coordination + +**Use when:** Workflows span multiple services. + +**Example: Design-to-development handoff** + +```markdown +# Phase 1: Design Export (Figma MCP) +1. Export design assets from Figma +2. Generate design specifications +3. Create asset manifest + +# Phase 2: Asset Storage (Drive MCP) +1. Create project folder in Drive +2. Upload all assets +3. Generate shareable links + +# Phase 3: Task Creation (Linear MCP) +1. Create development tasks +2. Attach asset links to tasks +3. Assign to engineering team + +# Phase 4: Notification (Slack MCP) +1. Post handoff summary to #engineering +2. Include asset links and task references +``` + +**Key techniques:** + +- Clear phase separation +- Data passing between MCPs +- Validation before moving to next phase +- Centralized error handling + +### Pattern 3: Iterative refinement + +**Use when:** Output quality improves with iteration. + +**Example: Report generation** + +```markdown +# Iterative Report Creation + +# Initial Draft +1. Fetch data via MCP +2. Generate first draft report +3. Save to temporary file + +# Quality Check +1. Run validation script: `scripts/check_report.py` +2. Identify issues: + - Missing sections + - Inconsistent formatting + - Data validation errors + +# Refinement Loop +1. Address each identified issue +2. Regenerate affected sections +3. Re-validate +4. Repeat until quality threshold met + +# Finalization +1. Apply final formatting +2. Generate summary +3. Save final version +``` + +**Key techniques:** + +- Explicit quality criteria +- Iterative improvement +- Validation scripts +- Know when to stop iterating + +### Pattern 4: Context-aware tool selection + +**Use when:** Same outcome, different tools depending on context. + +**Example: File storage** + +```markdown +# Smart File Storage + +# Decision Tree +1. Check file type and size +2. Determine best storage location: + - Large files (>10MB): Use cloud storage MCP + - Collaborative docs: Use Notion/Docs MCP + - Code files: Use GitHub MCP + - Temporary files: Use local storage + +# Execute Storage +Based on decision: +- Call appropriate MCP tool +- Apply service-specific metadata +- Generate access link + +# Provide Context to User +Explain why that storage was chosen +``` + +**Key techniques:** + +- Clear decision criteria +- Fallback options +- Transparency about choices + +### Pattern 5: Domain-specific intelligence + +**Use when:** Your skill adds specialized knowledge beyond tool access. + +**Example: Financial compliance** + +```markdown +# Payment Processing with Compliance + +# Before Processing (Compliance Check) +1. Fetch transaction details via MCP +2. Apply compliance rules: + - Check sanctions lists + - Verify jurisdiction allowances + - Assess risk level +3. Document compliance decision + +# Processing +IF compliance passed: + - Call payment processing MCP tool + - Apply appropriate fraud checks + - Process transaction +ELSE: + - Flag for review + - Create compliance case + +# Audit Trail +- Log all compliance checks +- Record processing decisions +- Generate audit report +``` + +**Key techniques:** + +- Domain expertise embedded in logic +- Compliance before action +- Comprehensive documentation +- Clear governance + +### Troubleshooting + +#### Skill won't upload + +**Error: "Could not find SKILL.md in uploaded folder"** + +Cause: File not named exactly `SKILL.md` + +Solution: + +- Rename to `SKILL.md` (case-sensitive) +- Verify with: `ls -la` should show `SKILL.md` + +**Error: "Invalid frontmatter"** + +Cause: YAML formatting issue + +Common mistakes: + +```yaml +# Wrong - missing delimiters +name: my-skill +description: Does things + +# Wrong - unclosed quotes +name: my-skill +description: "Does things + +# Correct +--- +name: my-skill +description: Does things +--- +``` + +**Error: "Invalid skill name"** + +Cause: Name has spaces or capitals + +```yaml +# Wrong +name: My Cool Skill + +# Correct +name: my-cool-skill +``` + +#### Skill doesn't trigger + +**Symptom:** Skill never loads automatically + +**Fix:** Revise your description field. See [The Description Field](#the-description-field) for good/bad examples. + +Quick checklist: + +- Is it too generic? ("Helps with projects" won't work) +- Does it include trigger phrases users would actually say? +- Does it mention relevant file types if applicable? + +**Debugging approach:** + +Ask Claude: "When would you use the [skill name] skill?" Claude will quote the description back. Adjust based on what's missing. + +#### Skill triggers too often + +**Symptom:** Skill loads for unrelated queries + +**Solutions:** + +1. **Add negative triggers** + +```yaml +description: Advanced data analysis for CSV files. Use for statistical modeling, regression, clustering. Do NOT use for simple data exploration (use data-viz skill instead). +``` + +2. **Be more specific** + +```yaml +# Too broad +description: Processes documents + +# More specific +description: Processes PDF legal documents for contract review +``` + +3. **Clarify scope** + +```yaml +description: PayFlow payment processing for e-commerce. Use specifically for online payment workflows, not for general financial queries. +``` + +#### MCP connection issues + +**Symptom:** Skill loads but MCP calls fail + +**Checklist:** + +1. **Verify MCP server is connected** + - Claude.ai: Settings > Extensions > [Your Service] + - Should show "Connected" status + +2. **Check authentication** + - API keys valid and not expired + - Proper permissions/scopes granted + - OAuth tokens refreshed + +3. **Test MCP independently** + - Ask Claude to call MCP directly (without skill) + - "Use [Service] MCP to fetch my projects" + - If this fails, issue is MCP not skill + +4. **Verify tool names** + - Skill references correct MCP tool names + - Check MCP server documentation + - Tool names are case-sensitive + +#### Instructions not followed + +**Symptom:** Skill loads but Claude doesn't follow instructions + +**Common causes:** + +1. **Instructions too verbose** + - Keep instructions concise + - Use bullet points and numbered lists + - Move detailed reference to separate files + +2. **Instructions buried** + - Put critical instructions at the top + - Use `## Important` or `## Critical` headers + - Repeat key points if needed + +3. **Ambiguous language** + +```markdown +# Bad +Make sure to validate things properly + +# Good +CRITICAL: Before calling create_project, verify: +- Project name is non-empty +- At least one team member assigned +- Start date is not in the past +``` + +> **Advanced technique:** For critical validations, consider bundling a script that performs the checks programmatically rather than relying on language instructions. Code is deterministic; language interpretation isn't. See the Office skills for examples of this pattern. + +4. **Model "laziness"** — Add explicit encouragement: + +```markdown +# Performance Notes +- Take your time to do this thoroughly +- Quality is more important than speed +- Do not skip validation steps +``` + +> **Note:** Adding this to user prompts is more effective than in SKILL.md + +#### Large context issues + +**Symptom:** Skill seems slow or responses degraded + +**Causes:** + +- Skill content too large +- Too many skills enabled simultaneously +- All content loaded instead of progressive disclosure + +**Solutions:** + +1. **Optimize SKILL.md size** + - Move detailed docs to `references/` + - Link to references instead of inline + - Keep SKILL.md under 5,000 words + +2. **Reduce enabled skills** + - Evaluate if you have more than 20-50 skills enabled simultaneously + - Recommend selective enablement + - Consider skill "packs" for related capabilities + +--- + +## Chapter 6: Resources and References + +If you're building your first skill, start with the Best Practices Guide, then reference the API docs as needed. + +### Official Documentation + +**Anthropic Resources:** + +- Best Practices Guide +- Skills Documentation +- API Reference +- MCP Documentation + +**Blog Posts:** + +- Introducing Agent Skills +- Engineering Blog: Equipping Agents for the Real World +- Skills Explained +- How to Create Skills for Claude +- Building Skills for Claude Code +- Improving Frontend Design through Skills + +### Example skills + +**Public skills repository:** + +- GitHub: [anthropics/skills](https://github.com/anthropics/skills) +- Contains Anthropic-created skills you can customize + +### Tools and Utilities + +**skill-creator skill:** + +- Built into Claude.ai and available for Claude Code +- Can generate skills from descriptions +- Reviews and provides recommendations +- Use: "Help me build a skill using skill-creator" + +**Validation:** + +- skill-creator can assess your skills +- Ask: "Review this skill and suggest improvements" + +### Getting Support + +**For Technical Questions:** + +- General questions: Community forums at the Claude Developers Discord + +**For Bug Reports:** + +- GitHub Issues: anthropics/skills/issues +- Include: Skill name, error message, steps to reproduce + +--- + +## Reference A: Quick Checklist + +Use this checklist to validate your skill before and after upload. If you want a faster start, use the skill-creator skill to generate your first draft, then run through this list to make sure you haven't missed anything. + +### Before you start + +- [ ] Identified 2-3 concrete use cases +- [ ] Tools identified (built-in or MCP) +- [ ] Reviewed this guide and example skills +- [ ] Planned folder structure + +### During development + +- [ ] Folder named in kebab-case +- [ ] SKILL.md file exists (exact spelling) +- [ ] YAML frontmatter has `---` delimiters +- [ ] name field: kebab-case, no spaces, no capitals +- [ ] description includes WHAT and WHEN +- [ ] No XML tags (`<` `>`) anywhere +- [ ] Instructions are clear and actionable +- [ ] Error handling included +- [ ] Examples provided +- [ ] References clearly linked + +### Before upload + +- [ ] Tested triggering on obvious tasks +- [ ] Tested triggering on paraphrased requests +- [ ] Verified doesn't trigger on unrelated topics +- [ ] Functional tests pass +- [ ] Tool integration works (if applicable) +- [ ] Compressed as .zip file + +### After upload + +- [ ] Test in real conversations +- [ ] Monitor for under/over-triggering +- [ ] Collect user feedback +- [ ] Iterate on description and instructions +- [ ] Update version in metadata + +--- + +## Reference B: YAML Frontmatter + +### Required fields + +```yaml +--- +name: skill-name-in-kebab-case +description: What it does and when to use it. Include specific trigger phrases. +--- +``` + +### All optional fields + +```yaml +name: skill-name +description: [required description] +license: MIT # Optional: License for open-source +allowed-tools: "Bash(python:*) Bash(npm:*) WebFetch" # Optional: Restrict tool access +metadata: # Optional: Custom fields + author: Company Name + version: 1.0.0 + mcp-server: server-name + category: productivity + tags: [project-management, automation] + documentation: https://example.com/docs + support: support@example.com +``` + +### Security notes + +**Allowed:** + +- Any standard YAML types (strings, numbers, booleans, lists, objects) +- Custom metadata fields +- Long descriptions (up to 1024 characters) + +**Forbidden:** + +- XML angle brackets (`<` `>`) - security restriction +- Code execution in YAML (uses safe YAML parsing) +- Skills named with "claude" or "anthropic" prefix (reserved) + +--- + +## Reference C: Complete Skill Examples + +For full, production-ready skills demonstrating the patterns in this guide: + +- **Document Skills** - PDF, DOCX, PPTX, XLSX creation +- **Example Skills** - Various workflow patterns +- **Partner Skills Directory** - View skills from various partners such as Asana, Atlassian, Canva, Figma, Sentry, Zapier, and more + +These repositories stay up-to-date and include additional examples beyond what's covered here. Clone them, modify them for your use case, and use them as templates. \ No newline at end of file diff --git a/.agents/skills/defold-skill-maintain/references/proto-reference-guide.md b/.agents/skills/defold-skill-maintain/references/proto-reference-guide.md new file mode 100644 index 0000000..a3003af --- /dev/null +++ b/.agents/skills/defold-skill-maintain/references/proto-reference-guide.md @@ -0,0 +1,146 @@ +# Proto Reference Maintenance Guide + +Guide for updating the `defold-proto-file-editing` skill: adding new file type references, updating existing ones, and maintaining proto schemas. + +## Where to find information + +### 1. Proto schema (primary source of truth) + +Location: `.agents/skills/defold-skill-maintain/assets/proto/` + +Proto files define the exact message structure, field types, defaults, enums, and required/optional status for each Defold file format. This is the **authoritative source** for field definitions. + +Key directories: +- `.agents/skills/defold-skill-maintain/assets/proto/gamesys/` — component and resource formats (`label_ddf.proto`, `physics_ddf.proto`, `atlas_ddf.proto`, `sprite_ddf.proto`, etc.) +- `.agents/skills/defold-skill-maintain/assets/proto/ddf/ddf_math.proto` — shared math types (`Vector3`, `Vector4`, `Vector4One`, `Vector4WOne`, `Point3`, `Quat`) +- `.agents/skills/defold-skill-maintain/assets/proto/ddf/ddf_extensions.proto` — proto extensions (`resource`, `displayName`) +- `.agents/skills/defold-skill-maintain/assets/proto/render/` — render and material formats +- `.agents/skills/defold-skill-maintain/assets/proto/gameobject/` — game object and properties formats + +**How to use**: Read the proto file for the target format. Identify the main message (e.g., `LabelDesc`, `CollisionObjectDesc`, `Atlas`), all its fields, types, defaults, and referenced enums. Follow `import` statements to find shared types and enums in other proto files. + +### 2. Defold documentation (context and semantics) + +Use the `defold-docs-fetch` skill to find the relevant manual page. It provides tables of topics with URLs — fetch the page content from the URL. + +Key pages for editing skills: +- Component manuals explain what each property does, valid value ranges, and how properties interact +- The manual gives context that proto files alone cannot (e.g., "mass must be non-zero for dynamic objects") + +### 3. Defold API reference (runtime properties) + +Use the `defold-api-fetch` skill to find the API page for the component namespace. Fetch the page content from the URL. + +API docs reveal: +- Runtime-readable/writable properties and their types +- Available functions (e.g., `label.set_text()`, `sprite.play_flipbook()`) +- Constants and enum values used in code + +### 4. Example files (canonical output format) + +Location: `main/example.*` + +Each component type should have a corresponding example file that shows the **exact text format** the Defold editor produces. These files are the ground truth for: +- Field ordering +- Indentation style +- Which default-valued fields the editor omits vs. includes +- How message blocks, enums, and repeated fields are formatted + +**Before creating a reference**, verify an example file exists. If not, create one in the Defold editor first and save it to `main/example.`. + +## Proto schemas prerequisite + +Before creating or updating a reference, ensure proto schemas are available: + +1. Check if `.agents/skills/defold-skill-maintain/assets/proto/` directory exists +2. If it does NOT exist, run: `python .agents/skills/defold-skill-maintain/scripts/fetch_proto.py` +3. This downloads `defoldsdk.zip` from the stable Defold release and extracts `defoldsdk/share/proto/` into `.agents/skills/defold-skill-maintain/assets/proto/` + +## Reference file structure template + +Every reference file in `references/` follows this structure: + +### Frontmatter-style header + +Start with a title and one-line summary of what the file type is. + +### Sections (in order) + +1. **Overview** (optional) — brief explanation of what the component does, only if the component concept is non-obvious +2. **File format** — state that it uses Protobuf Text Format, reference the proto message +3. **Canonical example** — full example from `main/example.` +4. **Fields reference** — every field from the proto message, in proto field number order: + - Field name, required/optional, type + - Description of what it does (from docs) + - Default value (from proto) + - **Omission rule**: when to omit the field (when it equals its default) + - Code example showing the field in Protobuf Text Format +5. **Nested message sections** — if the format has nested messages (like `CollisionShape` inside `CollisionObjectDesc`), document them in dedicated sections after the top-level fields +6. **Enum tables** — all enums with their constant names and descriptions +7. **Common templates** (optional) — pre-built configurations for frequent use cases + +## Step-by-step: adding a new file type reference + +1. **Identify the file extension** (e.g., `.sprite`, `.sound`, `.gui`). + +2. **Ensure proto schemas exist**: + - If `.agents/skills/defold-skill-maintain/assets/proto/` is missing, run `python .agents/skills/defold-skill-maintain/scripts/fetch_proto.py` first + +3. **Find the proto schema**: + - Search in `.agents/skills/defold-skill-maintain/assets/proto/` for the relevant `*_ddf.proto` file + - Read it to identify the main message, all fields, types, enums, and imports + - Follow imports to resolve shared types (especially `ddf_math.proto`) + +4. **Find or create an example file**: + - Check `main/example.` for an existing example + - If none exists, note that one should be created in the Defold editor + +5. **Fetch Defold documentation**: + - Load `defold-docs-fetch` skill, find the relevant manual page + - Fetch the page content from the URL + - Extract property descriptions, valid ranges, and behavioral notes + +6. **Fetch Defold API** (optional, for runtime context): + - Load `defold-api-fetch` skill, find the component's API page + - Fetch the page content from the URL + - Note runtime-accessible properties and functions + +7. **Create the reference file**: + - Path: `references/.md` + - Follow the section structure template above + - Document every field from the proto in field number order + - Include omission rules for each optional field + - Add the canonical example from the example file + - Add common templates if applicable + +8. **Update SKILL.md**: + - Add the new reference to the "Supported file types" list + - Add the embedded component type name if applicable + +9. **Verify**: + - Cross-check all field names, types, and defaults against the proto + - Ensure enum values match exactly + - Verify the canonical example matches the example file + +## Existing references and their proto sources + +- `references/label.md` — `.label` — `LabelDesc` — `gamesys/label_ddf.proto` +- `references/collisionobject.md` — `.collisionobject` — `CollisionObjectDesc` — `gamesys/physics_ddf.proto` +- `references/atlas.md` — `.atlas` — `Atlas` — `gamesys/atlas_ddf.proto` +- `references/sprite.md` — `.sprite` — `SpriteDesc` — `gamesys/sprite_ddf.proto` +- `references/sound.md` — `.sound` — `SoundDesc` — `gamesys/sound_ddf.proto` +- `references/font.md` — `.font` — `FontDesc` — `render/font_ddf.proto` +- `references/camera.md` — `.camera` — `CameraDesc` — `gamesys/camera_ddf.proto` +- `references/gameobject.md` — `.go` — `PrototypeDesc` — `gameobject/gameobject_ddf.proto` +- `references/collection.md` — `.collection` — `CollectionDesc` — `gameobject/gameobject_ddf.proto` +- `references/objectinterpolation.md` — `.objectinterpolation` — `ObjectInterpolationDesc` — extension: `objectinterpolation_ddf.proto` +- `references/model.md` — `.model` — `ModelDesc` — `gamesys/model_ddf.proto` +- `references/material.md` — `.material` — `MaterialDesc` — `render/material_ddf.proto` +- `references/tilesource.md` — `.tilesource` — `TileSet` — `gamesys/tile_ddf.proto` +- `references/tilemap.md` — `.tilemap` — `TileGrid` — `gamesys/tile_ddf.proto` +- `references/factory.md` — `.factory` — `FactoryDesc` — `gamesys/gamesys_ddf.proto` +- `references/collectionfactory.md` — `.collectionfactory` — `CollectionFactoryDesc` — `gamesys/gamesys_ddf.proto` +- `references/collectionproxy.md` — `.collectionproxy` — `CollectionProxyDesc` — `gamesys/gamesys_ddf.proto` +- `references/gui.md` — `.gui` — `SceneDesc` — `gamesys/gui_ddf.proto` +- `references/mesh.md` — `.mesh` — `MeshDesc` — `gamesys/mesh_ddf.proto` +- `references/particlefx.md` — `.particlefx` — `ParticleFX` — `particle_ddf.proto` diff --git a/.agents/skills/defold-skill-maintain/scripts/fetch_proto.py b/.agents/skills/defold-skill-maintain/scripts/fetch_proto.py new file mode 100644 index 0000000..3d61861 --- /dev/null +++ b/.agents/skills/defold-skill-maintain/scripts/fetch_proto.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Download defoldsdk.zip and extract share/proto into skill assets.""" + +import json +import os +import shutil +import sys +import tempfile +import urllib.request +import zipfile +from pathlib import Path + + +def find_project_root(start_dir: Path) -> Path: + """Find the project root by looking for game.project.""" + dir_path = start_dir.resolve() + for _ in range(8): + candidate = dir_path / "game.project" + if candidate.exists(): + return dir_path + parent = dir_path.parent + if parent == dir_path: + break + dir_path = parent + raise RuntimeError("Failed to locate project root (game.project not found)") + + +def download_to_file(url: str, out_path: Path) -> None: + """Download URL to file with progress indication.""" + tmp_path = out_path.with_suffix(out_path.suffix + ".tmp") + out_path.parent.mkdir(parents=True, exist_ok=True) + + print(f" Downloading...") + request = urllib.request.Request(url, headers={"User-Agent": "sync-proto-py"}) + + with urllib.request.urlopen(request, timeout=600) as response: + total = response.headers.get("Content-Length") + total_size = int(total) if total else None + + with open(tmp_path, "wb") as f: + downloaded = 0 + block_size = 8192 + + while True: + chunk = response.read(block_size) + if not chunk: + break + f.write(chunk) + downloaded += len(chunk) + + if total_size: + pct = downloaded * 100 // total_size + print(f"\r Downloaded: {downloaded:,} / {total_size:,} bytes ({pct}%)", end="", flush=True) + else: + print(f"\r Downloaded: {downloaded:,} bytes", end="", flush=True) + + print() + + tmp_path.rename(out_path) + + +def extract_share_proto(zip_path: Path, output_dir: Path) -> None: + """Extract share/proto/ entries from defoldsdk.zip into output_dir.""" + prefix = "defoldsdk/share/proto/" + + if output_dir.exists(): + print(f" Removing existing {output_dir}...") + shutil.rmtree(output_dir) + + output_dir.mkdir(parents=True, exist_ok=True) + + extracted = 0 + with zipfile.ZipFile(zip_path, "r") as zf: + for entry in zf.infolist(): + if entry.filename.endswith("/"): + continue + if not entry.filename.startswith(prefix): + continue + + rel_path = entry.filename[len(prefix):] + out_path = output_dir / rel_path + + resolved = out_path.resolve() + if not str(resolved).startswith(str(output_dir.resolve()) + os.sep): + raise RuntimeError(f"Zip-slip detected: {entry.filename}") + + out_path.parent.mkdir(parents=True, exist_ok=True) + with zf.open(entry) as src, open(out_path, "wb") as dst: + shutil.copyfileobj(src, dst) + extracted += 1 + + if extracted == 0: + raise RuntimeError(f"No files found under '{prefix}' in the SDK zip") + + print(f" Extracted {extracted} proto file(s)") + + +def main() -> None: + script_dir = Path(__file__).parent + project_root = find_project_root(script_dir) + proto_dir = Path(__file__).resolve().parent.parent / "assets" / "proto" + + print("Fetching Defold stable release info...") + request = urllib.request.Request( + "https://d.defold.com/stable/info.json", + headers={"User-Agent": "sync-proto-py"}, + ) + with urllib.request.urlopen(request, timeout=30) as response: + info = json.loads(response.read().decode("utf-8")) + + version = info["version"] + sha1 = info["sha1"] + print(f" Defold version: {version} (sha1: {sha1})") + + sdk_url = f"https://github.com/defold/defold/releases/download/{version}/defoldsdk.zip" + print(f" SDK URL: {sdk_url}") + + tmp_dir = Path(tempfile.mkdtemp(prefix="sync_proto_")) + try: + zip_path = tmp_dir / "defoldsdk.zip" + download_to_file(sdk_url, zip_path) + + print(f" Extracting share/proto/ -> {proto_dir.relative_to(project_root)}") + extract_share_proto(zip_path, proto_dir) + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + print() + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/monarch-screen-setup/SKILL.md b/.agents/skills/monarch-screen-setup/SKILL.md new file mode 100644 index 0000000..db0899c --- /dev/null +++ b/.agents/skills/monarch-screen-setup/SKILL.md @@ -0,0 +1,226 @@ +--- +name: monarch-screen-setup +description: "Organizes screens and popups in a Defold game using Monarch screen manager. Use when creating new screens, popups, or setting up navigation between them." +--- + +# Organizing Screens and Popups with Monarch + +## Prerequisite: Verify Monarch Dependency + +Before applying any guidance from this skill, you MUST confirm that the project actually uses Monarch. Check the `game.project` file in the project root for a dependency URL containing `britzl/monarch` (e.g. `dependencies#N = https://github.com/britzl/monarch/archive/...`). Alternatively, check for the presence of a `monarch/monarch.lua` file in the project tree. + +If neither a Monarch dependency in `game.project` nor a local `monarch/monarch.lua` module is found, **do NOT apply this skill**. Inform the user that the project does not appear to use Monarch and suggest adding the dependency to `game.project` if they want to use it: +``` +[project] +dependencies#N = https://github.com/britzl/monarch/archive/refs/tags/5.2.0.zip +``` + +--- + +This skill describes the conventions for creating and managing screens (via collectionproxy) and popups (via collectionfactory) in a Defold project using the Monarch library. + +## Naming Rules + +- All screen and popup names use `snake_case`. +- Screen and popup names MUST be unique across the entire project because Monarch registers them in a single shared registry (`screens` table in `monarch.lua`). Duplicate ids cause an assertion error. +- Recommended pattern: prefix popup names distinctly (e.g. `settings_popup`, `reward_popup`) so they never collide with screen names (e.g. `main_menu`, `gameplay`). + +## Directory Layout + +``` +project/ +├── main/ +│ ├── main.collection -- bootstrap collection (set in game.project [bootstrap]) +│ └── main.script -- main controller script: waits for registration, shows first screen +├── screens/ +│ └── / +│ ├── .collection -- IMPORTANT: collection name MUST be unique (see below) +│ ├── .gui +│ └── .gui_script +├── popups/ +│ └── / +│ ├── .collection +│ ├── .gui +│ └── .gui_script +``` + +## Creating a Screen (collectionproxy) + +A screen is a full-screen view loaded via `collectionproxy`. Only ONE screen should be active at a time. When switching screens, always use `{ clear = true }` to avoid stacking multiple screens. + +### Required files for a screen (e.g. `gameplay`) + +**1. `screens/gameplay/gameplay.collection`** +- The collection `name` property MUST be unique across the project (Defold uses it for URL addressing within the loaded world). +- Contains a game object with a camera component. + +```protobuf +name: "gameplay" +scale_along_z: 0 +embedded_instances { + id: "camera" + data: "embedded_components {\n" + " id: \"camera\"\n" + " type: \"camera\"\n" + " data: \"aspect_ratio: 1.0\\n" + "fov: 0.7854\\n" + "near_z: -1.0\\n" + "far_z: 1.0\\n" + "orthographic_projection: 1\\n" + "orthographic_mode: ORTHO_MODE_AUTO_COVER\\n" + "\"\n" + "}\n" + "" +} +``` + +### Registering a screen in `main.collection` + +In `main.collection`, for each screen create a game object (e.g. id `gameplay`) with: + +1. A **component** `screen_proxy` referencing `/monarch/screen_proxy.script` with properties: + - `screen_id` = `gameplay` (hash, must match the id you use in `monarch.show()`) + - `popup` = `false` + - `popup_on_popup` = `false` +2. An **embedded component** `collectionproxy` of type `collectionproxy` pointing to `/screens/gameplay/gameplay.collection`. + +The `screen_proxy.script` will call `monarch.register_proxy()` in its `init()`, registering the screen automatically. + +### Showing a screen (single-screen stack pattern) + +ALWAYS use `{ clear = true }` when showing a screen so the stack is cleared down to any existing instance and then replaced. This ensures only 1 screen is ever in the stack: + +```lua +monarch.show("gameplay", { clear = true }) +``` + +Do NOT call `monarch.show("gameplay")` without `clear = true` for screens -- that would push onto the stack and create confusing multi-screen stacks. + +To navigate between screens: +```lua +-- from main_menu to gameplay: +monarch.show("gameplay", { clear = true }) + +-- from gameplay to main_menu: +monarch.show("main_menu", { clear = true }) +``` + +Do NOT use `monarch.back()` for screen-to-screen navigation. `back()` is for closing popups. + +## Creating a Popup (collectionfactory) + +A popup is an overlay that pauses/dims screens beneath it. Popups are created via `collectionfactory` so multiple popup instances can coexist (popup on popup). + +### Required files for a popup (e.g. `settings_popup`) + +**1. `popups/settings_popup/settings_popup.collection`** +- Contains a game object with a **referenced** GUI component. + +```protobuf +name: "settings_popup" +scale_along_z: 0 +embedded_instances { + id: "go" + data: "components {\n" + " id: \"gui\"\n" + " component: \"/popups/settings_popup/settings_popup.gui\"\n" + "}\n" + "" +} +``` + +**2. `popups/settings_popup/settings_popup.gui`** +- Standard Defold GUI file with the popup's UI nodes. + +**3. `popups/settings_popup/settings_popup.gui_script`** +- To close itself, call `monarch.back()`. + +```lua +local monarch = require("monarch.monarch") + +function init(self) +end + +function final(self) +end + +function on_message(self, message_id, message, sender) +end + +function on_input(self, action_id, action) + if action_id == hash("touch") and action.pressed then + -- example: close popup on a button press + -- (check node picking for your close button here) + monarch.back() + end +end +``` + +### Registering a popup in `main.collection` + +In `main.collection`, for each popup create a game object (e.g. id `settings_popup`) with: + +1. A **component** `screen_factory` referencing `/monarch/screen_factory.script` with properties: + - `screen_id` = `settings_popup` (hash) + - `popup` = `true` + - `popup_on_popup` = `true` (set to `true` if this popup can appear on top of another popup) + - `screen_factory` = URL to the collectionfactory component (see step 2) +2. An **embedded component** `collectionfactory` of type `collectionfactory` pointing to `/popups/settings_popup/settings_popup.collection`. + +The `screen_factory.script` will call `monarch.register_factory()` in its `init()`, registering the popup automatically. + +### Showing and closing a popup + +```lua +-- Open a popup (it stacks on top of the current screen): +monarch.show("settings_popup") + +-- Open a popup on top of another popup (requires popup_on_popup = true): +monarch.show("confirm_popup") + +-- Close the topmost popup: +monarch.back() +``` + +### Popup timestep behavior + +When a popup is shown on top of a screen, Monarch automatically sets `timestep_below_popup` on the underlying screen's proxy. The default value is `1` (normal speed). Set it to `0` in the screen's `screen_proxy.script` properties to pause the screen beneath a popup: +- `timestep_below_popup` = `0` + +## Waiting for All Screens to Register Before Starting + +Do NOT call `monarch.show()` directly in `init()` of `main.script` -- at that point other scripts' `init()` has not yet run and screens are not registered. This causes an assertion error. + +The solution: post a message to self from `init()`. By the time the message is processed, all `init()` functions in the collection have completed and all screens/popups are registered. + +### Example: `main/main.script` + +```lua +local monarch = require("monarch.monarch") + +function init(self) + msg.post(".", "acquire_input_focus") + msg.post("#", "start") +end + +function on_message(self, message_id, message, sender) + if message_id == hash("start") then + monarch.show("main_menu", { clear = true }) + end +end +``` + +## Quick Reference + +| Action | Code | +|---|---| +| Show a screen (clear stack) | `monarch.show("screen_name", { clear = true })` | +| Show a popup | `monarch.show("popup_name")` | +| Close topmost popup | `monarch.back()` | +| Check screen registered | `monarch.screen_exists("screen_name")` | +| Check if busy (transitioning) | `monarch.is_busy()` | +| Get screen data | `monarch.data("screen_name")` | +| Pass data to screen | `monarch.show("screen_name", { clear = true }, { key = "value" })` | +| Pass data to popup | `monarch.show("popup_name", nil, { key = "value" })` | +| Check if popup | `monarch.is_popup("popup_name")` | +| Get current top screen | `monarch.top()` | diff --git a/.agents/skills/xmath-usage/SKILL.md b/.agents/skills/xmath-usage/SKILL.md new file mode 100644 index 0000000..ca5b999 --- /dev/null +++ b/.agents/skills/xmath-usage/SKILL.md @@ -0,0 +1,134 @@ +--- +name: xmath-usage +description: "Provides xmath API reference and in-place math optimization patterns for Defold. Use when writing performance-critical math code, optimizing vector/quaternion/matrix operations, or when the user mentions xmath, zero-allocation math, or reducing Lua GC pressure." +--- + +# Using xmath for Zero-Allocation Math in Defold + +## Prerequisite: Verify xmath Dependency + +Before applying any guidance from this skill, you MUST confirm that the project uses xmath. Check the `game.project` file for a dependency URL containing `thejustinwalsh/defold-xmath` (e.g. `dependencies#N = https://github.com/thejustinwalsh/defold-xmath/archive/...`). Alternatively, check for the presence of `xmath/` in the `.deps/` directory. + +If neither an xmath dependency in `game.project` nor a local xmath module is found, **do NOT apply this skill**. Inform the user that the project does not use xmath and suggest adding the dependency: +``` +[project] +dependencies#N = https://github.com/thejustinwalsh/defold-xmath/archive/refs/heads/main.zip +``` + +--- + +## Core Concept: In-Place Mutation to Eliminate Heap Allocations + +Standard `vmath` creates a **new Lua object on every operation**, causing constant GC pressure in hot loops: + +```lua +-- BAD: vmath allocates 3 new objects every frame +function update(self, dt) + local v = self.dir * 5 * dt -- alloc #1 + local pos = go.get_position() -- alloc #2 + local result = pos + v -- alloc #3 + go.set_position(result) +end +``` + +xmath **mutates an existing variable in place** — the result is written into the first argument. You allocate once, reuse forever: + +```lua +-- GOOD: xmath reuses pre-allocated variables, zero allocations per frame +go.property("dir", vmath.vector3(0, 1, 0)) + +local v = vmath.vector3() -- allocate ONCE at module scope + +function update(self, dt) + local pos = go.get_position() + xmath.mul(v, self.dir, 5 * dt) -- writes into v + xmath.add(v, pos, v) -- writes into v + go.set_position(v) +end +``` + +## Key Rules + +1. **Pre-allocate scratch variables at module scope or in `init()`** — never inside `update()` or `on_message()`. +2. **The output variable is always the first argument** — this is the fundamental calling convention difference from `vmath`. +3. **Functions return nothing** — you cannot chain calls. Use a scratch variable at each step. +4. **Use `vmath` to create initial objects** — `vmath.vector3()`, `vmath.vector4()`, `vmath.quat()`, `vmath.matrix4()` to allocate scratch buffers, then use `xmath` to operate on them. +5. **Type polymorphism** — functions like `xmath.lerp` work for `vector3`, `vector4`, and `quaternion` based on the output argument type. + +## Optimization Pattern + +```lua +-- Scratch variables — allocated once +local temp_v = vmath.vector3() +local temp_q = vmath.quat() + +function update(self, dt) + -- Instead of: local dir = vmath.normalize(target - pos) + xmath.sub(temp_v, self.target, self.pos) + xmath.normalize(temp_v, temp_v) -- can use same variable as both input and output + + -- Instead of: local rot = vmath.quat_rotation_z(angle) + xmath.quat_rotation_z(temp_q, self.angle) + + -- Instead of: local rotated = vmath.rotate(rot, dir) + xmath.rotate(temp_v, temp_q, temp_v) +end +``` + +--- + +## Full API Reference + +All functions write the result into the first argument. No return values. + +### Vector Operations (vector3 / vector4) + +| Function | Equivalent | Description | +|---|---|---| +| `xmath.add(out, v1, v2)` | `out = v1 + v2` | Add two vectors | +| `xmath.sub(out, v1, v2)` | `out = v1 - v2` | Subtract two vectors | +| `xmath.mul(out, v, n)` | `out = v * n` | Multiply vector by scalar | +| `xmath.div(out, v, n)` | `out = v / n` | Divide vector by scalar | +| `xmath.cross(out, v1, v2)` | `out = cross(v1, v2)` | Cross product (vector3 only) | +| `xmath.mul_per_elem(out, v1, v2)` | `out.x = v1.x * v2.x, ...` | Element-wise multiplication | +| `xmath.normalize(out, v)` | `out = normalize(v)` | Normalize vector | +| `xmath.rotate(out, q, v)` | `out = rotate(q, v)` | Rotate vector3 by quaternion | +| `xmath.vector(out)` | `out = (0,0,0)` | Reset to zero vector | + +### Interpolation (vector3 / vector4 / quaternion) + +| Function | Equivalent | Description | +|---|---|---| +| `xmath.lerp(out, t, v1, v2)` | `out = lerp(t, v1, v2)` | Linear interpolation | +| `xmath.slerp(out, t, v1, v2)` | `out = slerp(t, v1, v2)` | Spherical interpolation | + +### Quaternion Operations + +| Function | Description | +|---|---| +| `xmath.quat(out)` | Reset to identity `(0, 0, 0, 1)` | +| `xmath.conj(out, q)` | Conjugate of quaternion | +| `xmath.quat_axis_angle(out, axis, angle)` | Quaternion from axis + angle | +| `xmath.quat_basis(out, x, y, z)` | Quaternion from 3 basis vectors (vector3) | +| `xmath.quat_from_to(out, v1, v2)` | Rotation quaternion from v1 to v2 | +| `xmath.quat_rotation_x(out, angle)` | Rotation around X axis | +| `xmath.quat_rotation_y(out, angle)` | Rotation around Y axis | +| `xmath.quat_rotation_z(out, angle)` | Rotation around Z axis | + +### Matrix Operations (matrix4) + +| Function | Description | +|---|---| +| `xmath.matrix(out [, m1])` | Reset to identity or copy from m1 | +| `xmath.matrix_axis_angle(out, axis, angle)` | Rotation matrix from axis + angle | +| `xmath.matrix_from_quat(out, q)` | Matrix from quaternion | +| `xmath.matrix_frustum(out, left, right, bottom, top, near, far)` | Frustum projection matrix | +| `xmath.matrix_inv(out, m)` | Matrix inverse | +| `xmath.matrix_look_at(out, eye, look_at, up)` | View matrix | +| `xmath.matrix4_orthographic(out, left, right, bottom, top, near, far)` | Orthographic projection | +| `xmath.matrix_ortho_inv(out, m)` | Orthographic inverse | +| `xmath.matrix4_perspective(out, fov, aspect, near, far)` | Perspective projection | +| `xmath.matrix_rotation_x(out, angle)` | Rotation around X axis | +| `xmath.matrix_rotation_y(out, angle)` | Rotation around Y axis | +| `xmath.matrix_rotation_z(out, angle)` | Rotation around Z axis | +| `xmath.matrix_translation(out, position)` | Translation matrix from vector3/vector4 | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..73774d4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# Agent Instructions + +This repository is a **Defold** game project. The project root is the folder containing `game.project`. + +## Project map + +- **Root config**: `game.project` +- **Main game content**: `main/` (collections, game objects, scripts) +- **Assets**: `assets/` (app icons, images) +- **Dependencies (read-only context)**: `.deps/` +- **Screens**: `screens//` +- **Popups**: `popups//` + +Key Defold settings from `game.project`: + +- **Bootstrap collection**: `/main/main.collection` + +**Resource paths in `game.project`**: Values like `main_collection`, `game_binding`, `app_icon` use Defold resource identifiers. A trailing `c` suffix denotes compiled resources and is expected — do not treat it as a typo. + +## Include directories + +- Use `.deps/` as an include directory for resolving module references and understanding dependency APIs. +- **NEVER modify any files inside `.deps/`** - these are downloaded dependencies provided strictly as read-only context. + +## Defold file formats + +- **Lua scripts**: `.lua`, `.script`, `.gui_script`, `.render_script`, `.editor_script`. +- **Metadata assets** (Protocol Buffer Text Format): `.collection`, `.go`, `.sprite`, `.tilemap`, `.tilesource`, `.atlas`, `.font`, `.particlefx`, `.sound`, `.label`, `.gui`, `.model`, `.mesh`, `.material`, `.collisionobject`, `.texture_profiles`, `.display_profiles`. +- **Manifests** (YAML): `.appmanifest`, `.manifest` - platform-specific libraries and build flags. +- **Buffers** (JSON): `.buffer` - streams of data (positions, colors, etc.) used as input for Mesh components. +- **Shaders** (GLSL): `.vp`, `.fp`, `.glsl`. +- **Project config** (INI): `game.project`. +- **Properties** (INI): `game.properties`, `ext.properties` - parameters available in `game.project`. +- **2D assets**: `.png`, `.jpg`. +- **3D assets** (GTLF): `.gltf`, `.glb`. +- **Sound assets**: `.ogg`, `.wav`, `.opus` (OPUS requires modification of the appmanifest). + +## Sharp Sprite materials + +When the project includes the `defold-sharp-sprite` dependency, use RGSS materials from `/sharp_sprite/rgss/` instead of builtins for all supported component types: Sprite, Spine, GUI, ParticleFX, Tilemap, Font, Label. + +## Editing Defold assets + +When creating or editing Defold asset files, use the corresponding `defold-*-editing` skill to get the correct file format and structure. Always load the skill **before** writing or modifying the file. + +When creating new screens, popups, or setting up navigation between them, load the `monarch-screen-setup` skill first. + +When writing performance-critical math code or optimizing vector/quaternion/matrix operations, load the `xmath-usage` skill first. + +## Code style guidelines + +### Lua scripts (.lua, .script, .gui_script, .render_script, .editor_script) + +- **Indentation**: 1 tab (4 spaces). +- **Naming**: `snake_case` for variables, functions, files, and folders. Keep resource paths absolute (`/assets/...`) where Defold expects them. +- **Comments**: + - Use **LuaCATS** (`---@...`) annotations for types, module/public API docs. +- **Whitespace**: + - Empty lines must be truly empty (no spaces/tabs). + - Avoid trailing whitespace. +- **Defold API**: strictly follow the Defold API - always verify against the official documentation using the `defold-api-fetch` skill. There are no hidden or undocumented APIs - only use functions, messages, and properties that are explicitly described in the docs. For conceptual guidance on how Defold features work (components, physics, rendering, input, etc.), use the `defold-docs-fetch` skill. For practical implementation patterns and sample code, use the `defold-examples-fetch` skill. +- **Defensive checks**: Do NOT assume data is missing or constantly re-check field existence in tables. If YOU set a field, it EXISTS. Similarly, do NOT check for standard Lua API availability (e.g., `io` and `io.open` always exist in standard Lua). Avoid unnecessary defensive programming. +- **Paradigm**: do not use metatables or imitate classes. Use functional, data-based structures only. +- **Logging**: use `print()` to look at the game state. Add logs for transactions, initializations, important events. +- **GUI and game state separation**: GUI scripts (`.gui_script`) should NOT directly access game logic modules. All communication between game logic and UI must be message-based (`msg.post()`) to maintain clear separation of concerns. GUI should be purely data-driven, receiving all necessary data through messages and updating its display accordingly. This ensures UI remains decoupled from game implementation details. +- **Script instance state**: In `.script`, `.gui_script`, `.render_script` files, store instance-specific state in the `self` table, NOT in local module variables. Local variables at the module level are shared across ALL instances of the script, which causes bugs when multiple instances exist. Use `self.my_variable` instead of `local my_variable`. Not applicable for local functions - keep them local. If you need to call local function that it's defined below, to use forward declarations or reorganize the functions. +- **Local functions**: NEVER create local functions inside other functions. Local functions are only allowed at module scope. Anonymous lambda functions (inline callbacks) are acceptable. +- **require**: + - Always call `require` with parentheses: `require("module")`, NOT `require "module"`. + - Use dot notation for module paths: `require("screens.flappy_bird.gameplay")`, NOT `require("/screens/flappy_bird/gameplay")`. + - Module paths are relative to the project root and use dots (`.`) instead of slashes (`/`) as separators. + - Do NOT use leading slashes in require paths. + - Examples: `require("monarch.monarch")`, `require("screens.flappy_bird.gameplay")`, `require("main.utils")`. +- **Hash values**: `hash("...")` can be left inline without premature optimization. It's acceptable to use `message_id == hash("trigger_response")` directly. If you need to reuse a hash value multiple times, you can declare it as a module-level constant in `UPPER_CASE` format: `local TRIGGER_RESPONSE = hash("trigger_response")`. +- **Constants**: Module-level constants can be declared as local variables in `UPPER_CASE` format: `local TRIGGER_RESPONSE = hash("trigger_response")`, `local MAX_HEALTH = 100`. +- **msg.url format**: Always remember the format `[socket:][path][#fragment]`: + - `socket` - collection name (world) + - `path` - game object instance id (can be relative or global) + - `fragment` - component id + - Shorthands: `"."` for current game object, `"#"` for current component + - Examples: `msg.url("#my_component")`, `msg.url("collection:/path/to/go#component")`, `msg.url(socket, path, fragment)`, `msg.url(nil, hash("id"), hash("script"))`, `msg.url(nil, go.get_id("physics"), "collisionobject")` + +### Python + +- Write for Python 3.11. Do NOT write code to support earlier versions of Python. Always use modern Python practices appropriate for Python 3.11. Always use full type annotations, generics, and other modern practices. + +## Shell + +- **Windows**: use PowerShell. +- **Linux**: use bash. +- **macOS**: use zsh. + +## Commands + +All commands run from the project root (the folder with `game.project`). + +- **Build & Run via editor** - use the `defold-project-build` skill. Requires the Defold editor to be running with the project open. Builds the project, returns compilation errors, and launches the game if the build succeeds. + +## Validation checklist + +- Build via the running editor succeeds (`defold-project-build` skill). + +## Important repo-specific caveats + +- **Git commit messages**: use the following format: `Short description` in English language ONLY. diff --git a/NAVMESH_API.md b/NAVMESH_API.md index a4776f3..fde5c25 100644 --- a/NAVMESH_API.md +++ b/NAVMESH_API.md @@ -1,6 +1,6 @@ # Navmesh Pathfinding API Documentation -Defold Graph Pathfinder Extension - Navigation mesh pathfinding with polygon-based A* and funnel algorithm for smooth, optimal paths through walkable areas. +Defold Graph Pathfinder Extension - Navigation mesh pathfinding with triangle-based A* and funnel algorithm for smooth, optimal paths through walkable areas. ## Table of Contents @@ -18,25 +18,20 @@ Defold Graph Pathfinder Extension - Navigation mesh pathfinding with polygon-bas ## Introduction -The navmesh pathfinding system provides polygon-based pathfinding for games requiring smooth character movement through complex environments. It implements: +The navmesh pathfinding system provides triangle-based pathfinding for games requiring smooth character movement through complex environments. It implements: -- **Polygon-based A\*** algorithm for finding cell corridors through the navigation mesh +- **Triangle-based A\*** algorithm for finding cell corridors through the navigation mesh - **Simple Stupid Funnel Algorithm (SSFA)** for extracting optimal smooth paths - **Agent radius support** for runtime collision avoidance via portal offsetting - **Spatial grid indexing** for fast cell lookup at arbitrary positions - **LRU path caching** with automatic invalidation on mesh changes +- **Multiple simultaneous navmeshes** — each `navmesh_init()` call returns a unique `navmesh_id`; all operations are context-based -**Key Features:** -- Handles arbitrary convex polygon cells (not just triangles) -- Automatic adjacency detection via edge hashing -- Fallback positioning for clicks outside walkable areas -- Cache provides 10-100× speedup for repeated queries -- Zero runtime allocation after initialization -**Performance:** -- Pathfinding: O((C + E) log C + P) where C=cells, E=edges, P=portals -- Cell lookup: O(1) average with spatial index -- Memory: O(max_cells × (vertices + neighbors) + spatial_grid_size) + +> [!WARNING] +> **TRIANGLE-ONLY:** Only triangle cells (`vertex_count == 3`) are supported. Cells with a different vertex count are silently skipped. Each triangle has at most 3 neighbors (one per edge), matching most real-world NavMesh data. + --- @@ -44,16 +39,15 @@ The navmesh pathfinding system provides polygon-based pathfinding for games requ ### pathfinder.navmesh_init() -Initialize the navigation mesh pathfinding system. Must be called before any other navmesh operations. +Initialize a navigation mesh pathfinding context. Returns a unique `navmesh_id` required by all other navmesh functions. Multiple navmesh contexts can be active simultaneously. **Syntax:** ```lua -pathfinder.navmesh_init(max_cells, max_edges_per_cell, pool_block_size, cache_size, max_cache_path_length, min_cell_size, max_cell_size, max_grid_dim, debug) +local navmesh_id = pathfinder.navmesh_init(max_cells, pool_block_size, cache_size, max_cache_path_length, min_cell_size, max_cell_size, max_grid_dim, debug) ``` **Parameters:** -- `max_cells` (number): Maximum number of polygon cells in the navigation mesh -- `max_edges_per_cell` (number): Maximum edges/neighbors per cell (typically 3-8) +- `max_cells` (number): Maximum number of triangle cells in the navigation mesh - `pool_block_size` (number): Heap pool block size for A* algorithm (default: 32) - `cache_size` (number): Number of paths to cache (0 to disable, recommended: 16-128) - `max_cache_path_length` (number): Maximum length of cached paths in cells (default: 256) @@ -62,6 +56,9 @@ pathfinder.navmesh_init(max_cells, max_edges_per_cell, pool_block_size, cache_si - `max_grid_dim` (number)[optional, default: 1000]: Maximum spatial index grid dimension - `debug` (boolean)[optional, default: false]: Enable debug output (requires NAVMESH_DEBUG=1 at compile time) +**Returns:** +- `navmesh_id` (number): Unique context identifier. Pass this to all other `navmesh_*` functions. + > [!IMPORTANT] > The heap pool capacity equals `max_cells`. If `pool_block_size > max_cells`, it will be automatically clamped to `max_cells` to prevent heap allocation failures. > Recommended: Use `pool_block_size = 32` (default) for most navigation meshes. @@ -75,10 +72,9 @@ pathfinder.navmesh_init(max_cells, max_edges_per_cell, pool_block_size, cache_si **Example:** ```lua function init(self) - -- Initialize navmesh with 600 cells, 6 edges per cell - pathfinder.navmesh_init( + -- Initialize a navmesh context; store the returned id for all subsequent calls + self.navmesh_id = pathfinder.navmesh_init( 600, -- max_cells - 6, -- max_edges_per_cell 32, -- pool_block_size 16, -- cache_size 256, -- max_cache_path_length @@ -92,7 +88,7 @@ end ### pathfinder.navmesh_shutdown() -Shutdown and cleanup the navigation mesh system. Deallocates all memory and resets version counters. +Shutdown and cleanup all navigation mesh contexts. Deallocates all memory and resets version counters. **Syntax:** ```lua @@ -106,6 +102,27 @@ function final(self) end ``` +### pathfinder.navmesh_remove() + +Remove a single navigation mesh context and free its resources. Use this to release individual navmeshes while keeping others active. + +**Syntax:** +```lua +pathfinder.navmesh_remove(navmesh_id) +``` + +**Parameters:** +- `navmesh_id` (number): Navmesh context identifier returned by `navmesh_init()` + +**Example:** +```lua +function on_level_unload(self) + -- Remove only this level's navmesh; other navmeshes remain active + pathfinder.navmesh_remove(self.level_navmesh_id) + self.level_navmesh_id = nil +end +``` + --- ## Configuration @@ -116,10 +133,11 @@ Configure the funnel algorithm tolerances for path smoothing. Must be called AFT **Syntax:** ```lua -pathfinder.navmesh_set_funnel(portal_vertex_tolerance, portal_collapse_threshold, waypoint_duplicate_tolerance) +pathfinder.navmesh_set_funnel(navmesh_id, portal_vertex_tolerance, portal_collapse_threshold, waypoint_duplicate_tolerance) ``` **Parameters:** +- `navmesh_id` (number): Navmesh context identifier returned by `navmesh_init()` - `portal_vertex_tolerance` (number): Tolerance for vertex matching in portal extraction (default: 0.002) - `portal_collapse_threshold` (number): Threshold for collapsing narrow portals (default: 0.1) - `waypoint_duplicate_tolerance` (number): Tolerance for duplicate waypoint filtering (default: 0.001) @@ -133,11 +151,11 @@ pathfinder.navmesh_set_funnel(portal_vertex_tolerance, portal_collapse_threshold **Example:** ```lua function init(self) - -- Initialize navmesh first - pathfinder.navmesh_init(600, 6, 32, 16, 256) - + self.navmesh_id = pathfinder.navmesh_init(600, 32, 16, 256) + -- Customize funnel tolerances for large world pathfinder.navmesh_set_funnel( + self.navmesh_id, 0.005, -- portal_vertex_tolerance (increased for large scale) 0.2, -- portal_collapse_threshold 0.01 -- waypoint_duplicate_tolerance @@ -151,18 +169,22 @@ end ### pathfinder.navmesh_set_buffer() -Load navigation mesh data from a Defold buffer. The buffer must contain vertex positions defining the polygon cells of the navigation mesh. +Load navigation mesh data from a Defold buffer into the specified navmesh context. **Syntax:** ```lua -pathfinder.navmesh_set_buffer(buffer) +pathfinder.navmesh_set_buffer(navmesh_id, buffer) ``` **Parameters:** +- `navmesh_id` (number): Navmesh context identifier returned by `navmesh_init()` - `buffer` (buffer): Defold buffer containing navigation mesh vertex data +> [!WARNING] +> **TRIANGLE-ONLY:** Only triangle cells (`vertex_count == 3`) are supported. Cells with a different vertex count are silently skipped. Each triangle has at most 3 neighbors (one per edge), matching most real-world NavMesh data. + **Buffer Format:** -The buffer must have a stream named `"position"` with 3 float32 values per vertex (x, y, z). Vertices are grouped into polygons, with each polygon's vertices stored consecutively. +The buffer must have a stream named `"position"` with 3 float32 values per vertex (x, y, z). Vertices are grouped into triangles, with each triangle's 3 vertices stored consecutively. **Example:** ```lua @@ -170,13 +192,13 @@ The buffer must have a stream named `"position"` with 3 float32 values per verte go.property("navmesh_buffer", resource.buffer("/assets/navmesh.buffer")) function init(self) - -- Initialize navmesh system - pathfinder.navmesh_init(600, 6, 32, 16, 256) - - -- Load buffer from resource + -- Initialize navmesh context + self.navmesh_id = pathfinder.navmesh_init(600, 32, 16, 256) + + -- Load buffer into context local buffer = resource.get_buffer(self.navmesh_buffer) - pathfinder.navmesh_set_buffer(buffer) - + pathfinder.navmesh_set_buffer(self.navmesh_id, buffer) + -- Navmesh is now ready for pathfinding end ``` @@ -193,14 +215,15 @@ Navigation mesh buffers are typically generated from 3D modeling tools or navmes ### pathfinder.navmesh_find_path() -Find a smoothed path through the navigation mesh from start to goal position using Polygon A* and Funnel algorithm. +Find a smoothed path through the navigation mesh from start to goal position using Triangle A* and Funnel algorithm. **Syntax:** ```lua -local path_length, status, status_text, path = pathfinder.navmesh_find_path(start_x, start_y, goal_x, goal_y, max_path_length, agent_radius, enable_fallback) +local path_length, status, status_text, path = pathfinder.navmesh_find_path(navmesh_id, start_x, start_y, goal_x, goal_y, max_path_length, agent_radius, enable_fallback) ``` **Parameters:** +- `navmesh_id` (number): Navmesh context identifier returned by `navmesh_init()` - `start_x` (number): X coordinate of start position - `start_y` (number): Y coordinate of start position (typically Z in 3D) - `goal_x` (number): X coordinate of goal position @@ -217,7 +240,7 @@ local path_length, status, status_text, path = pathfinder.navmesh_find_path(star **Algorithm Pipeline:** 1. **Cell Lookup**: Find cells containing start and goal positions using spatial index -2. **Polygon A\***: Find corridor of adjacent cells from start cell to goal cell +2. **Triangle A\***: Find corridor of adjacent triangle cells from start cell to goal cell 3. **Portal Extraction**: Extract shared edges (portals) between consecutive cells 4. **Portal Offsetting**: If `agent_radius > 0`, offset portals inward for collision avoidance 5. **Funnel Algorithm**: Apply SSFA to find optimal shortest path through portals @@ -235,27 +258,25 @@ local path_length, status, status_text, path = pathfinder.navmesh_find_path(star ```lua function on_input(self, action_id, action) if action_id == hash("mouse_click") and action.pressed then - -- Convert screen to world position local world_pos = screen_to_world(action.x, action.y) - - -- Find path from player to clicked position + local path_length, status, status_text, path = pathfinder.navmesh_find_path( + self.navmesh_id, -- navmesh context self.player_pos.x, -- start_x self.player_pos.z, -- start_y (Z in 3D) world_pos.x, -- goal_x world_pos.z, -- goal_y 128, -- max_path_length - 0.5, -- agent_radius (0.5 units for collision) + 0.5, -- agent_radius true -- enable_fallback ) - + if status == pathfinder.PathStatus.SUCCESS then print("Path found with", path_length, "waypoints") self.current_path = path self.path_index = 1 elseif status == pathfinder.PathStatus.SUCCESS_START_FALLBACK then print("Path found, but start was outside navmesh") - -- Move player to first waypoint first self.current_path = path self.path_index = 1 elseif status == pathfinder.PathStatus.ERROR_NO_PATH then @@ -267,16 +288,13 @@ function on_input(self, action_id, action) end function update(self, dt) - -- Follow path if self.current_path and self.path_index <= #self.current_path then local waypoint = self.current_path[self.path_index] local target = vmath.vector3(waypoint.x, 0, waypoint.y) - - -- Move towards waypoint + local dir = vmath.normalize(target - self.player_pos) self.player_pos = self.player_pos + dir * self.speed * dt - - -- Check if reached waypoint + if vmath.length(target - self.player_pos) < 0.5 then self.path_index = self.path_index + 1 end @@ -295,10 +313,11 @@ Find which navigation mesh cell contains a given position. **Syntax:** ```lua -local cell_id, center_x, center_y = pathfinder.navmesh_cell_at_position(x, y) +local cell_id, center_x, center_y = pathfinder.navmesh_cell_at_position(navmesh_id, x, y) ``` **Parameters:** +- `navmesh_id` (number): Navmesh context identifier returned by `navmesh_init()` - `x` (number): X coordinate of position to query - `y` (number): Y coordinate of position to query (typically Z in 3D) @@ -317,8 +336,8 @@ Uses the spatial grid index for O(1) average lookup, then performs point-in-poly **Example:** ```lua function validate_spawn_point(self, x, z) - local cell_id, center_x, center_y = pathfinder.navmesh_cell_at_position(x, z) - + local cell_id, center_x, center_y = pathfinder.navmesh_cell_at_position(self.navmesh_id, x, z) + if cell_id ~= pathfinder.INVALID_ID then print("Position is walkable, in cell", cell_id) print("Cell center:", center_x, center_y) @@ -340,9 +359,12 @@ Get spatial index grid data for debug visualization. The spatial index is a grid **Syntax:** ```lua -local grid = pathfinder.navmesh_get_spatial_index() +local grid = pathfinder.navmesh_get_spatial_index(navmesh_id) ``` +**Parameters:** +- `navmesh_id` (number): Navmesh context identifier returned by `navmesh_init()` + **Returns:** - `grid` (table): Table with `vertical` and `horizontal` arrays, each containing line data @@ -355,38 +377,36 @@ local grid = pathfinder.navmesh_get_spatial_index() **Description:** -Returns the spatial index grid structure for rendering debug overlays. This visualizes how the navigation mesh is spatially partitioned for efficient queries. The grid cell size is automatically calculated from the polygon sizes and clamped to min/max values specified in `navmesh_init()`. +Returns the spatial index grid structure for rendering debug overlays. This visualizes how the navigation mesh is spatially partitioned for efficient queries. The grid cell size is automatically calculated from the triangle cell sizes and clamped to min/max values specified in `navmesh_init()`. **Example:** ```lua function init(self) - pathfinder.navmesh_init(600, 6, 32, 16, 256) - + self.navmesh_id = pathfinder.navmesh_init(600, 32, 16, 256) + local buffer = resource.get_buffer(self.navmesh_buffer) - pathfinder.navmesh_set_buffer(buffer) - - -- Get spatial index for debug rendering - self.grid = pathfinder.navmesh_get_spatial_index() + pathfinder.navmesh_set_buffer(self.navmesh_id, buffer) + + self.grid = pathfinder.navmesh_get_spatial_index(self.navmesh_id) end function update(self, dt) - -- Draw spatial index grid if self.grid.vertical then for _, line in ipairs(self.grid.vertical) do msg.post("@render:", "draw_line", { start_point = line.start_position, end_point = line.end_position, - color = vmath.vector4(1, 0, 0, 0.3) -- Red, semi-transparent + color = vmath.vector4(1, 0, 0, 0.3) }) end end - + if self.grid.horizontal then for _, line in ipairs(self.grid.horizontal) do msg.post("@render:", "draw_line", { start_point = line.start_position, end_point = line.end_position, - color = vmath.vector4(1, 0, 0, 0.3) -- Red, semi-transparent + color = vmath.vector4(1, 0, 0, 0.3) }) end end @@ -403,9 +423,12 @@ Get comprehensive statistics about navmesh pathfinding caches for performance mo **Syntax:** ```lua -local stats = pathfinder.navmesh_get_stats() +local stats = pathfinder.navmesh_get_stats(navmesh_id) ``` +**Parameters:** +- `navmesh_id` (number): Navmesh context identifier returned by `navmesh_init()` + **Returns:** - `stats` (table): Table containing cache statistics @@ -420,38 +443,26 @@ local stats = pathfinder.navmesh_get_stats() - `miss_count` (number): Number of cache misses - `hit_rate` (number): Cache hit rate percentage (0-100) -**Description:** - -Provides detailed performance metrics for monitoring navmesh pathfinding efficiency. Use this data to: -- Tune cache sizes for optimal performance -- Monitor cache effectiveness -- Identify performance bottlenecks -- Validate optimization strategies - **Example:** ```lua function update(self, dt) - -- Update stats every second self.stats_timer = (self.stats_timer or 0) + dt if self.stats_timer >= 1.0 then self.stats_timer = 0 - - local stats = pathfinder.navmesh_get_stats() - - -- Log path cache stats + + local stats = pathfinder.navmesh_get_stats(self.navmesh_id) + print(string.format("Path Cache: %d/%d entries, Hit Rate: %d%%", stats.path_cache.cache_entries, stats.path_cache.cache_capacity, stats.path_cache.cache_hit_rate )) - - -- Log distance cache stats + print(string.format("Distance Cache: %d entries, Hit Rate: %d%%", stats.distance_cache.current_size, stats.distance_cache.hit_rate )) - - -- Warn if cache efficiency is low + if stats.path_cache.cache_hit_rate < 20 then print("WARNING: Low path cache hit rate, consider increasing cache_size") end @@ -494,26 +505,19 @@ When `enable_fallback = false`: **Usage:** ```lua local path_length, status, status_text, path = pathfinder.navmesh_find_path( - start_x, start_y, goal_x, goal_y, 128, 0.5, true + self.navmesh_id, start_x, start_y, goal_x, goal_y, 128, 0.5, true ) if status == pathfinder.PathStatus.SUCCESS then - -- Normal path, both positions were in cells follow_path(path) elseif status == pathfinder.PathStatus.SUCCESS_START_FALLBACK then - -- Start was outside navmesh, moved to nearest cell - -- First waypoint is the corrected start position move_to_position(path[1]) follow_path(path) elseif status == pathfinder.PathStatus.SUCCESS_GOAL_FALLBACK then - -- Goal was outside navmesh, moved to nearest cell - -- Agent will reach nearest valid position follow_path(path) elseif status == pathfinder.PathStatus.ERROR_START_NOT_IN_CELL then - -- Start position invalid and fallback disabled show_error("Cannot start from this position") elseif status == pathfinder.PathStatus.ERROR_NO_PATH then - -- No path exists between cells show_error("No path to target") else print("Pathfinding failed:", status_text) @@ -528,9 +532,8 @@ end **For Tower Defense / RTS:** ```lua -pathfinder.navmesh_init( +self.navmesh_id = pathfinder.navmesh_init( 600, -- max_cells - 6, -- max_edges_per_cell 32, -- pool_block_size 128, -- cache_size (high for repeated queries) 256 -- max_cache_path_length @@ -539,9 +542,8 @@ pathfinder.navmesh_init( **For Action / Adventure:** ```lua -pathfinder.navmesh_init( +self.navmesh_id = pathfinder.navmesh_init( 600, -- max_cells - 6, -- max_edges_per_cell 32, -- pool_block_size 16, -- cache_size (lower for varied paths) 256 -- max_cache_path_length @@ -554,8 +556,6 @@ pathfinder.navmesh_init( - **Medium meshes** (200-500 cells): `pool_block_size = 64` - **Large meshes** (>500 cells): `pool_block_size = 128` - - ### Fallback Strategy - **Permissive**: `enable_fallback = true` - Always finds a path @@ -563,3 +563,4 @@ pathfinder.navmesh_init( - Check status codes to handle fallback cases appropriately --- + diff --git a/example/navmesh_basic.collection b/example/navmesh_basic.collection new file mode 100644 index 0000000..28d9a88 --- /dev/null +++ b/example/navmesh_basic.collection @@ -0,0 +1,55 @@ +name: "basic" +scale_along_z: 0 +embedded_instances { + id: "scripts" + data: "components {\n" + " id: \"basic\"\n" + " component: \"/example/scripts/navmesh_basic.script\"\n" + "}\n" + "" +} +embedded_instances { + id: "path_end" + data: "embedded_components {\n" + " id: \"model\"\n" + " type: \"model\"\n" + " data: \"mesh: \\\"/builtins/assets/meshes/sphere.dae\\\"\\n" + "name: \\\"{{NAME}}\\\"\\n" + "materials {\\n" + " name: \\\"default\\\"\\n" + " material: \\\"/example/components/materials/unlit/model_unlit_instanced.material\\\"\\n" + " textures {\\n" + " sampler: \\\"tex0\\\"\\n" + " texture: \\\"/example/assets/img/red.png\\\"\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + z: 8.216292 + } +} +embedded_instances { + id: "path_start" + data: "embedded_components {\n" + " id: \"model\"\n" + " type: \"model\"\n" + " data: \"mesh: \\\"/builtins/assets/meshes/sphere.dae\\\"\\n" + "name: \\\"{{NAME}}\\\"\\n" + "materials {\\n" + " name: \\\"default\\\"\\n" + " material: \\\"/example/components/materials/unlit/model_unlit_instanced.material\\\"\\n" + " textures {\\n" + " sampler: \\\"tex0\\\"\\n" + " texture: \\\"/example/assets/img/green.png\\\"\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" + position { + x: -2.335069 + z: -6.027575 + } +} diff --git a/example/scripts/agents.lua b/example/scripts/agents.lua index 5bc8c3c..fe76190 100644 --- a/example/scripts/agents.lua +++ b/example/scripts/agents.lua @@ -1,11 +1,13 @@ -local data = require("example.scripts.data") -local const = require("example.scripts.const") -local draw = require("example.scripts.draw") +local data = require("example.scripts.data") +local const = require("example.scripts.const") +local draw = require("example.scripts.draw") -- ================================= -- MODULE -- ================================= -local agents = {} +local agents = {} + +local arrival_threshold = 0.1 --========================================================== -- FUNCTIONS @@ -19,16 +21,17 @@ local function get_current_waypoint_position(agent) return vmath.vector3(node.x, 0, node.y) end -function agents.add(start_position, goal_position) +function agents.add(navmesh_id, start_position, goal_position) local path_size = 0 local path_status = 0 local path_status_text = "" local path = {} local goal_node_id = 0 - path_size, path_status, path_status_text, path = pathfinder.navmesh_find_path(start_position.x, start_position.z, goal_position.x, goal_position.z, 128, 0.0, false) + path_size, path_status, path_status_text, path = pathfinder.navmesh_find_path(navmesh_id, start_position.x, start_position.z, goal_position.x, goal_position.z, 128, 0.0, false) if path_status ~= pathfinder.PathStatus.SUCCESS and path_status ~= pathfinder.PathStatus.SUCCESS_START_FALLBACK and path_status ~= pathfinder.PathStatus.SUCCESS_GOAL_FALLBACK then + pprint(path_status_text) return end @@ -75,8 +78,8 @@ local function check_waypoint_arrival(agent) local waypoint_position = get_current_waypoint_position(agent) local waypoint_distance = vmath.length(agent.position - waypoint_position) - -- Simple arrival threshold - very tight for point-to-point movement - local arrival_threshold = 0.1 + -- Simple arrival threshold + if waypoint_distance <= arrival_threshold then -- Reached waypoint, advance to next @@ -114,31 +117,18 @@ function agents.update(dt) local distance = vmath.length(to_target) if distance >= const.EPSILON then - -- Calculate direction unit vector - local direction = to_target * (1.0 / distance) - - -- Calculate target rotation angle - local target_rotation = math.atan2(direction.x, direction.z) - local target_quat = vmath.quat_rotation_y(target_rotation) - - -- Get current rotation quaternion - local current_quat = agent.rotation --go.get_rotation(agent.instance) - - -- Smooth rotation using slerp - -- The third parameter (t) controls interpolation speed (0.0 to 1.0) - -- Lower values = smoother/slower rotation, higher values = faster rotation - local t = math.min(1.0, agent.rotation_speed * dt) - agent.rotation = vmath.slerp(t, current_quat, target_quat) - - -- Update agent.rotation for reference (optional, if you need the angle) - agent.rotation_angle = target_rotation - - -- Calculate movement for this frame and clamp + local direction = to_target * (1.0 / distance) + local target_rotation = math.atan2(direction.x, direction.z) + local target_quat = vmath.quat_rotation_y(target_rotation) + local current_quat = agent.rotation + local t = math.min(1.0, agent.rotation_speed * dt) local movement_distance = math.min(agent.max_speed * dt, distance) - -- Update position and rotation - agent.position = agent.position + (direction * movement_distance) - agent.position.y = 0.0 + agent.rotation = vmath.slerp(t, current_quat, target_quat) + agent.rotation_angle = target_rotation + agent.position = agent.position + (direction * movement_distance) + agent.position.y = 0.0 + go.set_position(agent.position, agent.instance) go.set_rotation(agent.rotation, agent.instance) -- Use smoothed quaternion @@ -147,11 +137,8 @@ function agents.update(dt) else agent.state = const.AGENT_STATES.ARRIVED - -- model.play_anim(agent.model, hash("Idle_Loop"), go.PLAYBACK_LOOP_FORWARD) - go.delete(agent.instance) table.remove(data.agents, agent_id) - print("ARRIVED", agent.state) end else diff --git a/example/scripts/navmesh.script b/example/scripts/navmesh.script index f6f8724..6da29de 100644 --- a/example/scripts/navmesh.script +++ b/example/scripts/navmesh.script @@ -15,12 +15,13 @@ local goal_position = vmath.vector3() local cam = msg.url() local spatial_index_grid = {} local shift_pressed = false +local navmesh_id = 0 --========================================================== -- FUNCTIONS --========================================================== local function validate_spawn_point(x, y) - local cell_id, center_x, center_y = pathfinder.navmesh_cell_at_position(x, y) + local cell_id, center_x, center_y = pathfinder.navmesh_cell_at_position(navmesh_id, x, y) if cell_id ~= pathfinder.INVALID_ID then print("Position is walkable, in cell", cell_id) @@ -39,15 +40,15 @@ function init(self) cam = msg.url("/camera#camera") data.path_smoothing_id = pathfinder.add_path_smoothing(const.SMOOTHING_CONFIG) - pathfinder.navmesh_init(300, 6, 32, 32, 64, 5, 10, 500, true) - pathfinder.navmesh_set_buffer(navmesh_buffer) - spatial_index_grid = pathfinder.navmesh_get_spatial_index() + navmesh_id = pathfinder.navmesh_init(300, 32, 32, 128, 5, 10, 1000, true) + + pathfinder.navmesh_set_buffer(navmesh_id, navmesh_buffer) + spatial_index_grid = pathfinder.navmesh_get_spatial_index(navmesh_id) start_position = go.get_position("/path_start") goal_position = go.get_position("/path_end") - - -- Example 8: Validate Cell at position + -- Validate Cell at position validate_spawn_point(start_position.x, start_position.z) validate_spawn_point(100, 100) end @@ -56,7 +57,7 @@ function update(self, dt) draw.grid(spatial_index_grid) agents.update(dt) - local stats = pathfinder.navmesh_get_stats() + local stats = pathfinder.navmesh_get_stats(navmesh_id) -- pprint(stats) end @@ -80,17 +81,22 @@ function on_input(self, action_id, action) if action_id == const.TRIGGERS.MOUSE_BUTTON_LEFT then -- LEFT CLICK for start position start_position = utils.screen_to_plane(cam, action.screen_x, action.screen_y) - go.set_position(start_position, "/path_start") - validate_spawn_point(start_position.x, start_position.z) + if start_position then + go.set_position(start_position, "/path_start") + validate_spawn_point(start_position.x, start_position.z) + end elseif action_id == const.TRIGGERS.MOUSE_BUTTON_RIGHT then -- RIGHT CLICK for goal position goal_position = utils.screen_to_plane(cam, action.screen_x, action.screen_y) - go.set_position(goal_position, "/path_end") - validate_spawn_point(goal_position.x, goal_position.z) + + if goal_position then + go.set_position(goal_position, "/path_end") + validate_spawn_point(goal_position.x, goal_position.z) + end end -- Press SPACE to add agent if action_id == const.TRIGGERS.SPACE and action.pressed then - agents.add(start_position, goal_position) + agents.add(navmesh_id, start_position, goal_position) end end end diff --git a/example/scripts/navmesh_basic.script b/example/scripts/navmesh_basic.script new file mode 100644 index 0000000..432297d --- /dev/null +++ b/example/scripts/navmesh_basic.script @@ -0,0 +1,43 @@ +go.property("navmesh_buffer", resource.buffer("/example/assets/navmesh.buffer")) + +local navmesh_id = 0 +local function validate_spawn_point(x, y) + local cell_id, center_x, center_y = pathfinder.navmesh_cell_at_position(navmesh_id, x, y) + + if cell_id ~= pathfinder.INVALID_ID then + print("Position is walkable, in cell", cell_id) + print("Cell center:", center_x, center_y) + return true + else + print("Position is not walkable (outside navmesh): ", x, y) + return false + end +end + + +function init(self) + profiler.enable_ui(true) + profiler.set_ui_view_mode(profiler.VIEW_MODE_MINIMIZED) + + + navmesh_id = pathfinder.navmesh_init( + 600, -- max_cells + 32, -- pool_block_size + 16, -- cache_size + 256, -- max_cache_path_length + 5, -- min_cell_size + 10, -- max_cell_size + 1000, -- max_grid_dim + false -- debug + ) + + + local navmesh_buffer = resource.get_buffer(self.navmesh_buffer) + pathfinder.navmesh_set_buffer(navmesh_id, navmesh_buffer) + + start_position = go.get_position("/path_start") + goal_position = go.get_position("/path_end") + + validate_spawn_point(start_position.x, start_position.z) + validate_spawn_point(100, 100) +end diff --git a/graph_pathfinder/annotations.lua b/graph_pathfinder/annotations.lua index 60ed1a6..564fee3 100644 --- a/graph_pathfinder/annotations.lua +++ b/graph_pathfinder/annotations.lua @@ -2,8 +2,8 @@ ---High-performance A* pathfinding library for real-time games and simulations. ---@class pathfinder pathfinder = { - INVALID_ID = 0xFFFFFFFF, -- Invalid ID constant for cells and nodes (UINT32_MAX) - + INVALID_ID = 0xFFFFFFFF, -- Invalid ID constant for cells and nodes (UINT32_MAX) + ---PathStatus enum - Status codes for pathfinding operations ---@enum PathStatus PathStatus = { @@ -25,7 +25,7 @@ pathfinder = { ERROR_NO_PROJECTION = -9, -- Cannot project point onto graph (no edges exist) ERROR_VIRTUAL_NODE_FAILED = -10 -- Failed to create or connect virtual node }, - + ---PathSmoothStyle enum - Path smoothing algorithms ---@enum PathSmoothStyle PathSmoothStyle = { @@ -251,9 +251,9 @@ function pathfinder.get_spatial_index() end ---@return boolean is_initialized True if spatial index is active, false otherwise function pathfinder.spatial_index_initialized() end ----Initialize the navigation mesh pathfinding system. Must be called before any other navmesh operations. ----@param max_cells number Maximum number of polygon cells in the navigation mesh ----@param max_edges_per_cell number Maximum edges/neighbors per cell (typically 3-8) +---Initialize a navigation mesh pathfinding context. Must be called before any other navmesh operations. +---Multiple navmeshes can be active simultaneously; each call returns a unique navmesh_id. +---@param max_cells number Maximum number of triangle cells in the navigation mesh ---@param pool_block_size number Heap pool block size for A* algorithm (default: 32) ---@param cache_size number Number of paths to cache (0 to disable, recommended: 16-128) ---@param max_cache_path_length number Maximum length of cached paths in cells (default: 256) @@ -261,22 +261,32 @@ function pathfinder.spatial_index_initialized() end ---@param max_cell_size? number Maximum spatial index grid cell size (default: 10.0) ---@param max_grid_dim? number Maximum spatial index grid dimension (default: 1000) ---@param debug? boolean Enable debug output (default: false, requires NAVMESH_DEBUG=1 at compile time) -function pathfinder.navmesh_init(max_cells, max_edges_per_cell, pool_block_size, cache_size, max_cache_path_length, min_cell_size, max_cell_size, max_grid_dim, debug) end +---@return number navmesh_id Unique identifier for this navmesh context, required by all other navmesh functions +function pathfinder.navmesh_init(max_cells, pool_block_size, cache_size, max_cache_path_length, min_cell_size, max_cell_size, max_grid_dim, debug) end ----Shutdown and cleanup the navigation mesh system. +---Shutdown and cleanup all navigation mesh contexts. function pathfinder.navmesh_shutdown() end +---Remove a single navigation mesh context and free its resources. +---@param navmesh_id number Navmesh context identifier returned by navmesh_init() +function pathfinder.navmesh_remove(navmesh_id) end + ---Configure the funnel algorithm tolerances for path smoothing. Must be called AFTER navmesh_init(). +---@param navmesh_id number Navmesh context identifier returned by navmesh_init() ---@param portal_vertex_tolerance number Tolerance for vertex matching in portal extraction (default: 0.002) ---@param portal_collapse_threshold number Threshold for collapsing narrow portals (default: 0.1) ---@param waypoint_duplicate_tolerance number Tolerance for duplicate waypoint filtering (default: 0.001) -function pathfinder.navmesh_set_funnel(portal_vertex_tolerance, portal_collapse_threshold, waypoint_duplicate_tolerance) end +function pathfinder.navmesh_set_funnel(navmesh_id, portal_vertex_tolerance, portal_collapse_threshold, waypoint_duplicate_tolerance) end ----Load navigation mesh data from a Defold buffer. ----@param buffer buffer Defold buffer containing navigation mesh vertex data -function pathfinder.navmesh_set_buffer(buffer) end +---Load navigation mesh data from a Defold buffer into the given navmesh context. +---WARNING: Only triangle cells (vertex_count == 3) are supported. Cells with a different vertex count are silently skipped. +---Each triangle has at most 3 neighbors (one per edge), matching most real-world NavMesh data. +---@param navmesh_id number Navmesh context identifier returned by navmesh_init() +---@param buffer buffer Defold buffer containing navigation mesh vertex data (position stream, float32 x3) +function pathfinder.navmesh_set_buffer(navmesh_id, buffer) end ----Find a smoothed path through the navigation mesh using Polygon A* and Funnel algorithm. +---Find a smoothed path through the navigation mesh using Triangle A* and Funnel algorithm. +---@param navmesh_id number Navmesh context identifier returned by navmesh_init() ---@param start_x number X coordinate of start position ---@param start_y number Y coordinate of start position (typically Z in 3D) ---@param goal_x number X coordinate of goal position @@ -288,20 +298,23 @@ function pathfinder.navmesh_set_buffer(buffer) end ---@return number status PathStatus code indicating success or error ---@return string status_text Human-readable status message ---@return PathNode[] path Array of waypoint positions with x and y coordinates -function pathfinder.navmesh_find_path(start_x, start_y, goal_x, goal_y, max_path_length, agent_radius, enable_fallback) end +function pathfinder.navmesh_find_path(navmesh_id, start_x, start_y, goal_x, goal_y, max_path_length, agent_radius, enable_fallback) end ---Find which navigation mesh cell contains a given position. +---@param navmesh_id number Navmesh context identifier returned by navmesh_init() ---@param x number X coordinate of position to query ---@param y number Y coordinate of position to query (typically Z in 3D) ---@return number cell_id ID of cell containing position, or special value if not found ---@return number center_x X coordinate of cell center ---@return number center_y Y coordinate of cell center -function pathfinder.navmesh_cell_at_position(x, y) end +function pathfinder.navmesh_cell_at_position(navmesh_id, x, y) end ---Get spatial index grid data for debug visualization (navmesh). +---@param navmesh_id number Navmesh context identifier returned by navmesh_init() ---@return table grid Table with vertical and horizontal arrays, each containing line data with start_position and end_position (vector3) -function pathfinder.navmesh_get_spatial_index() end +function pathfinder.navmesh_get_spatial_index(navmesh_id) end ---Get comprehensive statistics about navmesh pathfinding caches. +---@param navmesh_id number Navmesh context identifier returned by navmesh_init() ---@return table stats Table containing cache statistics with fields: path_cache, distance_cache -function pathfinder.navmesh_get_stats() end +function pathfinder.navmesh_get_stats(navmesh_id) end diff --git a/graph_pathfinder/include/pathfinder_extension.h b/graph_pathfinder/include/pathfinder_extension.h index 8eb14fd..33ccc4b 100644 --- a/graph_pathfinder/include/pathfinder_extension.h +++ b/graph_pathfinder/include/pathfinder_extension.h @@ -5,6 +5,7 @@ #include "dmsdk/dlib/buffer.h" #include "dmsdk/dlib/vmath.h" #include "dmsdk/gameobject/gameobject.h" +#include "pathfinder_navmesh_types.h" #include "pathfinder_types.h" namespace pathfinder @@ -46,16 +47,36 @@ namespace pathfinder void smooth_path(uint32_t smooth_id, dmArray& path, dmArray& smoothed_path); void smooth_path_waypoint(uint32_t smooth_id, dmArray& waypoints, dmArray& smoothed_path); - // Navmesh - void navmesh_set_buffer(dmBuffer::HBuffer& buffer); + // ------------------------------ + // NAVMESH + // ------------------------------ + uint8_t navmesh_init(pathfinder::navmesh::NavMeshContext* ctx); + void navmesh_remove(uint8_t navmesh_id); + void navmesh_shutdown(); + void navmesh_set_buffer(uint8_t navmesh_id, dmBuffer::HBuffer& buffer); + void navmesh_get_stats(uint8_t navmesh_id, + uint32_t& cache_entries, + uint32_t& cache_capacity, + uint32_t& cache_hit_rate, + uint32_t& dist_cache_size, + uint32_t& dist_cache_hits, + uint32_t& dist_cache_misses, + uint32_t& dist_cache_hit_rate); - void navmesh_get_stats(uint32_t& cache_entries, - uint32_t& cache_capacity, - uint32_t& cache_hit_rate, - uint32_t& dist_cache_size, - uint32_t& dist_cache_hits, - uint32_t& dist_cache_misses, - uint32_t& dist_cache_hit_rate); + void navmesh_find_path(uint8_t navmesh_id, + uint32_t* path_length, + pathfinder::Vec2 start_position, + pathfinder::Vec2 goal_position, + dmArray* smooth_path, + uint32_t max_path, + float agent_radius, + bool enable_fallback, + PathStatus* status); + + void navmesh_cell_at_position(uint8_t navmesh_id, pathfinder::Vec2 position, uint32_t* cell_id, pathfinder::Vec2* center); + + navmesh::NavMeshSpatialIndex* navmesh_get_spatial_index(uint8_t navmesh_id); + void navmesh_set_funnel(uint8_t navmesh_id, float portal_vertex_tolerance, float portal_collapse_threshold, float waypoint_duplicate_tolerance); } // namespace extension } // namespace pathfinder diff --git a/graph_pathfinder/include/pathfinder_navmesh.h b/graph_pathfinder/include/pathfinder_navmesh.h index 53231e3..46c393e 100644 --- a/graph_pathfinder/include/pathfinder_navmesh.h +++ b/graph_pathfinder/include/pathfinder_navmesh.h @@ -10,6 +10,11 @@ * - LRU path caching with version-tracked invalidation * - Runtime agent radius collision avoidance via portal offsetting * + * Context-Based API: + * All operations take a NavMeshContext* as the first parameter. + * Use create_context() to allocate and initialize state, destroy_context() to free it. + * Multiple independent NavMesh instances are supported. + * * Algorithm Pipeline: * 1. Build NavMesh: Add cells → Build adjacency graph * 2. Pathfinding: Polygon A* (find corridor) → Funnel (extract shortest path) @@ -29,9 +34,6 @@ #include "pathfinder_constants.h" #include "pathfinder_navmesh_types.h" #include "pathfinder_types.h" -#include "pathfinder_heap.h" -#include "pathfinder_cache.h" -#include "pathfinder_distance_cache.h" #include namespace pathfinder @@ -39,13 +41,12 @@ namespace pathfinder namespace navmesh { /*******************************************/ - // INITIALIZATION & SHUTDOWN + // CONTEXT CREATION & DESTRUCTION /*******************************************/ /** - * @brief Initialize the NavMesh pathfinding system + * @brief Create a new NavMesh context * @param max_cells Maximum number of polygon cells in the navigation mesh - * @param max_edges_per_cell Maximum edges/neighbors per cell * @param pool_block_size Heap pool block size for A* algorithm (default: 32, automatically clamped to max_cells if larger) * @param min_cell_size Minimum spatial index grid cell size (default: 1.0f) * @param max_cell_size Maximum spatial index grid cell size (default: 2.0f) @@ -53,10 +54,14 @@ namespace pathfinder * @param cache_size Number of paths to cache (default: 32, set to 0 to disable caching) * @param max_cache_path_length Maximum length of cached paths in cells (default: 64) * @param debug Enable debug output (default: false, only works if NAVMESH_DEBUG is enabled at compile time) + * @return Pointer to the created context, or NULL on allocation failure * * Allocates all memory upfront for cells, adjacency, spatial index, and pathfinding state. * Also initializes heap pool, path cache, and distance cache subsystems. * + * NOTE: This NavMesh system supports triangle cells only (vertex_count == 3). + * build_adjacency() and the internal polygon graph are optimized for triangles. + * * IMPORTANT: The heap pool capacity equals max_cells. If pool_block_size > max_cells, * it will be automatically clamped to max_cells to prevent heap allocation failures. * Recommended: Use pool_block_size = 32 (default) for most navigation meshes. @@ -69,32 +74,31 @@ namespace pathfinder * * DEBUG: Set debug=true to enable diagnostic output (requires NAVMESH_DEBUG=1 at compile time). * - When NAVMESH_DEBUG=0: debug parameter is ignored, no overhead - * - When NAVMESH_DEBUG=1: debug parameter controls runtime output + * - When NAVMESH_DEBUG=1: debug parameter controls runtime output stored in NavMeshContext * * Time Complexity: O(max_cells + spatial_grid_size) * Memory: O(max_cells * (vertices + neighbors) + spatial_grid_size) - * - * Must be called before any other navmesh operations. */ - void init(uint32_t max_cells, - uint32_t max_edges_per_cell, - uint32_t pool_block_size = 32, - float min_cell_size = 1.0f, - float max_cell_size = 2.0f, - uint32_t max_grid_dim = 1000, - uint32_t cache_size = 32, - uint32_t max_cache_path_length = 64, - bool debug = false); + NavMeshContext* create_context(uint32_t max_cells, + uint32_t pool_block_size = 32, + float min_cell_size = 1.0f, + float max_cell_size = 2.0f, + uint32_t max_grid_dim = 1000, + uint32_t cache_size = 32, + uint32_t max_cache_path_length = 64, + bool debug = false); /** - * @brief Shutdown and cleanup the NavMesh system + * @brief Destroy a NavMesh context and free all associated memory + * @param ctx Context to destroy (may be NULL, which is a no-op) * - * Deallocates all memory and resets version counters. + * Deallocates all memory including cells, spatial index, heap, cache, and distance cache. * All cell IDs become invalid after this call. + * Does nothing if ctx is NULL. * - * Time Complexity: O(1) + * Time Complexity: O(cell_count) to free per-cell vertex and neighbor arrays */ - void shutdown(); + void destroy_context(NavMeshContext* ctx); /*******************************************/ // FUNNEL ALGORITHM CONFIGURATION @@ -102,11 +106,12 @@ namespace pathfinder /** * @brief Initialize funnel algorithm configuration with custom tolerances + * @param ctx NavMesh context * @param portal_vertex_tolerance Tolerance for vertex matching in portal extraction (default: 0.002) * @param portal_collapse_threshold Threshold for collapsing narrow portals (default: 0.1) * @param waypoint_duplicate_tolerance Tolerance for duplicate waypoint filtering (default: 0.001) * - * Must be called AFTER navmesh::init() to customize funnel algorithm tolerances. + * Must be called AFTER create_context() to customize funnel algorithm tolerances. * If not called, default values are used. * * WHEN TO CUSTOMIZE: @@ -119,13 +124,14 @@ namespace pathfinder * * Example: * @code - * navmesh::init(100, 8, 32); - * navmesh::funnel_init(0.005f, 0.2f, 0.01f); // Custom tolerances for large worlds + * NavMeshContext* ctx = navmesh::create_context(100, 8, 32); + * navmesh::funnel_init(ctx, 0.005f, 0.2f, 0.01f); // Custom tolerances for large worlds * @endcode */ - void funnel_init(float portal_vertex_tolerance = 0.002f, - float portal_collapse_threshold = 0.1f, - float waypoint_duplicate_tolerance = 0.001f); + void funnel_init(NavMeshContext* ctx, + float portal_vertex_tolerance = 0.002f, + float portal_collapse_threshold = 0.1f, + float waypoint_duplicate_tolerance = 0.001f); /*******************************************/ // CELL MANAGEMENT @@ -133,14 +139,19 @@ namespace pathfinder /** * @brief Add a polygon cell to the navigation mesh + * @param ctx NavMesh context * @param vertices Array of polygon vertices (must be counter-clockwise or clockwise) - * @param vertex_count Number of vertices (typically 3 for triangles, but supports general convex polygons) + * @param vertex_count Number of vertices (must be 3; only triangle cells are supported) * @param status Output parameter for operation status (optional) - * @return Cell ID (0 to max_cells-1) on success, ERROR (UINT32_MAX) on failure + * @return Cell ID (0 to max_cells-1) on success, INVALID_ID on failure * - * Creates a new walkable polygon cell with computed centroid. + * Creates a new walkable triangle cell with computed centroid. * Cell IDs are assigned sequentially and remain stable until removed. * + * TRIANGLE-ONLY: Only triangle cells (vertex_count == 3) are supported. + * build_adjacency() silently skips cells with vertex_count != 3. + * Most navigation meshes are built from triangles; n-gons are not supported. + * * Time Complexity: O(vertex_count) for centroid calculation * Success: status = SUCCESS, returns valid cell ID * Failure: status = ERROR_NODE_FULL if no slots available @@ -148,23 +159,28 @@ namespace pathfinder * Note: Does not automatically create adjacency edges. Use build_adjacency() or * add_edge_manual() to connect cells after adding all polygons. */ - uint32_t add_cell(Vec2* vertices, uint32_t vertex_count, PathStatus* status); + uint32_t add_cell(NavMeshContext* ctx, + Vec2* vertices, + uint32_t vertex_count, + PathStatus* status); /** * @brief Remove a cell from the navigation mesh + * @param ctx NavMesh context * @param cell_id ID of cell to remove * * Marks cell as unwalkable and removes all edges connected to/from this cell. * Invalidates cached paths containing this cell. * Cell ID slot becomes available for reuse via add_cell(). * - * Time Complexity: O(max_cells * max_edges_per_cell) - must scan all edges + * Time Complexity: O(max_cells * 3) - must scan all triangle neighbor lists * Does nothing if cell ID is invalid or already removed. */ - void remove_cell(uint32_t cell_id); + void remove_cell(NavMeshContext* ctx, uint32_t cell_id); /** * @brief Get the center position of a cell + * @param ctx NavMesh context * @param cell_id Cell ID to query * @return Vec2 centroid position of the cell * @@ -173,7 +189,7 @@ namespace pathfinder * Time Complexity: O(1) * Warning: No bounds checking. Ensure cell_id is valid. */ - Vec2 get_cell_center(uint32_t cell_id); + Vec2 get_cell_center(NavMeshContext* ctx, uint32_t cell_id); /*******************************************/ // ADJACENCY & GRAPH BUILDING @@ -181,24 +197,30 @@ namespace pathfinder /** * @brief Build adjacency graph from cell edges using edge hashing + * @param ctx NavMesh context * * Automatically detects shared edges between cells by hashing edge vertex pairs. * Creates bidirectional adjacency links for all pairs of cells sharing an edge. * + * TRIANGLE-ONLY: Only triangle cells (vertex_count == 3) are processed. + * Cells with a different vertex count are silently skipped. Each triangle has + * at most 3 neighbors (one per edge), matching most real-world NavMesh data. + * * Algorithm: - * 1. For each cell, hash all edges (vertex pairs) + * 1. For each triangle cell, hash all 3 edges (vertex pairs) * 2. Match edges with identical hash values * 3. Create bidirectional neighbor links for matches * - * Time Complexity: O(max_cells * avg_vertices_per_cell) + * Time Complexity: O(max_cells * 3) * Must be called after adding all cells but before pathfinding. * * Note: Replaces any existing adjacency data. */ - void build_adjacency(); + void build_adjacency(NavMeshContext* ctx); /** * @brief Manually add edge between two cells + * @param ctx NavMesh context * @param cell1_id First cell ID * @param cell2_id Second cell ID * @@ -208,20 +230,21 @@ namespace pathfinder * Time Complexity: O(1) * Does nothing if either cell is invalid or edge already exists. */ - void add_edge_manual(uint32_t cell1_id, uint32_t cell2_id); + void add_edge_manual(NavMeshContext* ctx, uint32_t cell1_id, uint32_t cell2_id); /** * @brief Remove edge between two cells + * @param ctx NavMesh context * @param cell1_id First cell ID * @param cell2_id Second cell ID * * Removes bidirectional adjacency link between cells. * Invalidates cached paths using this edge. * - * Time Complexity: O(max_edges_per_cell) - must search neighbors + * Time Complexity: O(3) - must search up to 3 triangle neighbors * Does nothing if edge doesn't exist or cells are invalid. */ - void remove_edge(uint32_t cell1_id, uint32_t cell2_id); + void remove_edge(NavMeshContext* ctx, uint32_t cell1_id, uint32_t cell2_id); /*******************************************/ // PATHFINDING OPERATIONS @@ -229,6 +252,7 @@ namespace pathfinder /** * @brief Find smoothed path through navigation mesh (Polygon A* + Funnel Algorithm) + * @param ctx NavMesh context * @param start_cell Starting cell ID * @param goal_cell Goal cell ID * @param start_pos Starting position within start_cell @@ -261,24 +285,20 @@ namespace pathfinder * - status = ERROR_GOAL_NODE_INVALID: goal_cell invalid or unwalkable * - status = ERROR_NO_PATH: no cell corridor exists between cells * - status = ERROR_HEAP_FULL: heap pool exhausted during A* - * - * Notes: - * - Agent radius offset provides runtime collision avoidance - * - Narrow portals (< agent_radius * collapse_threshold) collapse to midpoint - * - out_smooth_path array grows automatically if needed - * - max_length is advisory, not strictly enforced */ - uint32_t find_path_smoothed(uint32_t start_cell, - uint32_t goal_cell, - Vec2 start_pos, - Vec2 goal_pos, - dmArray* out_smooth_path, - uint32_t max_length, - float agent_radius, - PathStatus* status); + uint32_t find_path_smoothed(NavMeshContext* ctx, + uint32_t start_cell, + uint32_t goal_cell, + Vec2 start_pos, + Vec2 goal_pos, + dmArray* out_smooth_path, + uint32_t max_length, + float agent_radius, + PathStatus* status); /** * @brief Find cell containing a given position using spatial index + * @param ctx NavMesh context * @param position Position to query * @param enable_fallback If true, find nearest cell when position not in any cell (default: true) * @param out_used_fallback Output parameter indicating if fallback was used (optional, can be NULL) @@ -294,16 +314,15 @@ namespace pathfinder * Fallback Behavior: * - enable_fallback=true (default): Returns nearest cell, sets *out_used_fallback=true * - enable_fallback=false: Returns INVALID_ID, sets *out_used_fallback=false - * - * Use Cases: - * 1. enable_fallback=true: User can click anywhere, always get a path (original behavior) - * 2. enable_fallback=false: Reject clicks on walls/obstacles (prevent invalid paths) - * 3. Check out_used_fallback: Know if agent should move to nearest cell vs target position */ - uint32_t find_cell_at_position(Vec2 position, bool enable_fallback = true, bool* out_used_fallback = NULL); + uint32_t find_cell_at_position(NavMeshContext* ctx, + Vec2 position, + bool enable_fallback = true, + bool* out_used_fallback = NULL); /** * @brief Find smoothed path from arbitrary positions (convenience wrapper) + * @param ctx NavMesh context * @param start_pos Starting position (any world position) * @param goal_pos Goal position (any world position) * @param out_smooth_path Output array for smoothed waypoints (Vec2 positions) @@ -316,11 +335,6 @@ namespace pathfinder * High-level convenience function that combines cell lookup and pathfinding. * Automatically finds cells for start/goal positions and computes smooth path. * - * Algorithm: - * 1. Find cell containing start_pos (with optional fallback to nearest) - * 2. Find cell containing goal_pos (with optional fallback to nearest) - * 3. If cells found, call find_path_smoothed() to compute path - * * Status Codes: * - SUCCESS: Path found, positions were inside cells * - SUCCESS_START_FALLBACK: Path found, start_pos used fallback to nearest cell @@ -330,45 +344,20 @@ namespace pathfinder * - ERROR_NO_PATH: Cells found but no path exists between them * - Other error codes: From underlying pathfinding (heap full, etc.) * - * Use Cases: - * 1. enable_fallback=true, status=SUCCESS: Normal path, both positions in cells - * 2. enable_fallback=true, status=SUCCESS_START_FALLBACK: Move agent to out_smooth_path[0] - * 3. enable_fallback=true, status=SUCCESS_GOAL_FALLBACK: Agent reaches nearest valid position - * 4. enable_fallback=false: Reject invalid clicks on walls/obstacles - * - * Example - Handle fallback for player movement: - * @code - * PathStatus status; - * dmArray path; - * uint32_t waypoints = find_path_from_positions( - * player_pos, click_pos, &path, 64, agent_radius, true, &status - * ); - * - * if (waypoints > 0) { - * if (status == SUCCESS_START_FALLBACK || status == SUCCESS_GOAL_FALLBACK) { - * // Position was outside navmesh, move to nearest valid position - * move_player_to(path[0]); // Start from first waypoint (nearest cell) - * } else { - * // Normal path, follow waypoints - * follow_path(path); - * } - * } - * @endcode - * * Time Complexity: O(find_cell × 2 + pathfinding) - * - Cell lookup: O(1) typical, O(N) with fallback - * - Pathfinding: O((C + E) log C + P) */ - uint32_t find_path_from_positions(Vec2 start_pos, - Vec2 goal_pos, - dmArray* out_smooth_path, - uint32_t max_length, - float agent_radius, - bool enable_fallback, - PathStatus* status); + uint32_t find_path_from_positions(NavMeshContext* ctx, + Vec2 start_pos, + Vec2 goal_pos, + dmArray* out_smooth_path, + uint32_t max_length, + float agent_radius, + bool enable_fallback, + PathStatus* status); /** * @brief Find raw cell corridor path without smoothing (Polygon A* only) + * @param ctx NavMesh context * @param start_cell Starting cell ID * @param goal_cell Goal cell ID * @param start_pos Starting position within start_cell (used for heuristic) @@ -388,10 +377,9 @@ namespace pathfinder * * Success: status = SUCCESS, returns cell count > 0 * Failure: Same error codes as find_path_smoothed() - * - * Note: Path is cached and can be reused by find_path_smoothed() */ - uint32_t find_path_raw(uint32_t start_cell, + uint32_t find_path_raw(NavMeshContext* ctx, + uint32_t start_cell, uint32_t goal_cell, Vec2 start_pos, Vec2 goal_pos, @@ -405,14 +393,16 @@ namespace pathfinder /** * @brief Get number of cells in the navigation mesh + * @param ctx NavMesh context * @return Number of active cells * * Time Complexity: O(1) */ - uint32_t get_cell_count(); + uint32_t get_cell_count(NavMeshContext* ctx); /** * @brief Get direct access to cell array for visualization (read-only) + * @param ctx NavMesh context * @return Pointer to internal cell array (do not modify!) * * Provides read-only access to cell data for debug rendering. @@ -420,57 +410,62 @@ namespace pathfinder * * Time Complexity: O(1) */ - Cell* get_cells(); + Cell* get_cells(NavMeshContext* ctx); /** * @brief Get heap context for advanced usage (read-only) + * @param ctx NavMesh context * @return Pointer to internal heap context (do not modify!) * * Useful for debugging heap allocation and performance profiling. * * Time Complexity: O(1) */ - heap::HeapContext* get_heap_context(); + heap::HeapContext* get_heap_context(NavMeshContext* ctx); /** * @brief Get spatial index for visualization (read-only) + * @param ctx NavMesh context * @return Pointer to internal spatial index (do not modify!), NULL if not initialized * * Useful for debug rendering of spatial grid cells. * * Time Complexity: O(1) */ - NavMeshSpatialIndex* get_spatial_index(); + NavMeshSpatialIndex* get_spatial_index(NavMeshContext* ctx); /** * @brief Get cache context for statistics/testing (read-only) + * @param ctx NavMesh context * @return Pointer to internal cache context (do not modify!), NULL if caching disabled * * Use cache::get_hit_rate() and related functions to query cache performance. * * Time Complexity: O(1) */ - cache::CacheContext* get_cache_context(); + cache::CacheContext* get_cache_context(NavMeshContext* ctx); /** * @brief Get distance cache context for statistics/testing (read-only) + * @param ctx NavMesh context * @return Pointer to internal distance cache context (do not modify!), NULL if not initialized * * Use distance_cache functions to query cache statistics. * * Time Complexity: O(1) */ - distance_cache::DistanceCacheContext* get_distance_cache_context(); + distance_cache::DistanceCacheContext* get_distance_cache_context(NavMeshContext* ctx); /** * @brief Get funnel algorithm configuration (read-only) + * @param ctx NavMesh context * @return Const pointer to internal funnel configuration * * Returns the current funnel tolerances (set via funnel_init() or defaults). * * Time Complexity: O(1) */ - const FunnelConfig* get_funnel_config(); + const FunnelConfig* get_funnel_config(NavMeshContext* ctx); } // namespace navmesh } // namespace pathfinder diff --git a/graph_pathfinder/include/pathfinder_navmesh_astar.h b/graph_pathfinder/include/pathfinder_navmesh_astar.h index 2e38a7b..8285ecd 100644 --- a/graph_pathfinder/include/pathfinder_navmesh_astar.h +++ b/graph_pathfinder/include/pathfinder_navmesh_astar.h @@ -86,6 +86,10 @@ namespace pathfinder * @param heap_ctx Optional heap context (NULL = use global default context) * @param dist_cache_ctx Optional distance cache context (NULL = no caching) * @param cache_ctx Optional path cache context (NULL = no caching) + * @param astar_scratch Optional pre-allocated scratch node array sized to max_cells. + * When provided with astar_generation, avoids a per-call heap allocation. + * @param astar_generation Pointer to the caller's generation counter. Incremented + * on every call so that stale scratch entries are detected without a full reset. * @return Number of cells in corridor, 0 on failure * * A* Algorithm on Polygon Graph: @@ -127,7 +131,9 @@ namespace pathfinder PathStatus* status, heap::HeapContext* heap_ctx = NULL, distance_cache::DistanceCacheContext* dist_cache_ctx = NULL, - cache::CacheContext* cache_ctx = NULL); + cache::CacheContext* cache_ctx = NULL, + PolygonAStarNode* astar_scratch = NULL, + uint32_t* astar_generation = NULL); } // namespace astar } // namespace navmesh diff --git a/graph_pathfinder/include/pathfinder_navmesh_debug.h b/graph_pathfinder/include/pathfinder_navmesh_debug.h index 5aa6fcf..dc8cc99 100644 --- a/graph_pathfinder/include/pathfinder_navmesh_debug.h +++ b/graph_pathfinder/include/pathfinder_navmesh_debug.h @@ -2,14 +2,14 @@ * @file pathfinder_navmesh_debug.h * @brief Shared debug configuration for NavMesh subsystems * - * This header provides centralized debug macros and flag management for all - * NavMesh-related modules (navmesh, spatial, astar, funnel). It eliminates - * duplicate debug definitions across multiple files. + * This header provides centralized debug macros for all NavMesh-related + * modules (navmesh, spatial, astar, funnel). It eliminates duplicate debug + * definitions across multiple files. * * Usage: * 1. Include this header in NavMesh implementation files - * 2. Use NAVMESH_LOG() macro for debug output - * 3. Set debug mode via navmesh::init() or spatial::set_debug_mode() + * 2. Use NAVMESH_LOG(flag, ...) for debug output, passing the context's + * m_DebugMode flag (or a local bool debug parameter) explicitly */ #ifndef PATHFINDER_NAVMESH_DEBUG_H @@ -22,25 +22,10 @@ #if NAVMESH_DEBUG #include -// Runtime debug flags (only active when NAVMESH_DEBUG is enabled) -// These are defined in pathfinder_navmesh.cpp and can be set via init() -namespace pathfinder -{ - namespace navmesh - { - extern bool g_DebugMode; // Main NavMesh debug flag - - namespace spatial - { - extern bool g_SpatialDebugMode; // Spatial index debug flag - } - } // namespace navmesh -} // namespace pathfinder - // Centralized debug logging macro // Usage: NAVMESH_LOG(debug_flag, "format string", args...) -// Example: NAVMESH_LOG(pathfinder::navmesh::g_DebugMode, "Initialized: max_cells=%u", max_cells) -// Note: Typically use the convenience macros NAVMESH_LOG_MAIN() or NAVMESH_LOG_SPATIAL() instead +// Pass the context's m_DebugMode (or a local bool debug parameter) explicitly. +// Example: NAVMESH_LOG(ctx->m_DebugMode, "Initialized: max_cells=%u", max_cells) #define NAVMESH_LOG(flag, format, ...) \ do \ { \ @@ -48,15 +33,9 @@ namespace pathfinder printf("[NavMesh] " format "\n", ##__VA_ARGS__); \ } while (0) -// Convenience macros for specific subsystems -#define NAVMESH_LOG_MAIN(format, ...) NAVMESH_LOG(pathfinder::navmesh::g_DebugMode, format, ##__VA_ARGS__) -#define NAVMESH_LOG_SPATIAL(format, ...) NAVMESH_LOG(pathfinder::navmesh::spatial::g_SpatialDebugMode, format, ##__VA_ARGS__) - #else // Debug disabled at compile time - all macros become no-ops #define NAVMESH_LOG(flag, format, ...) ((void)0) -#define NAVMESH_LOG_MAIN(format, ...) ((void)0) -#define NAVMESH_LOG_SPATIAL(format, ...) ((void)0) #endif // NAVMESH_DEBUG #endif // PATHFINDER_NAVMESH_DEBUG_H diff --git a/graph_pathfinder/include/pathfinder_navmesh_spatial.h b/graph_pathfinder/include/pathfinder_navmesh_spatial.h index c3f3543..d6d2105 100644 --- a/graph_pathfinder/include/pathfinder_navmesh_spatial.h +++ b/graph_pathfinder/include/pathfinder_navmesh_spatial.h @@ -44,22 +44,6 @@ namespace pathfinder const float SPATIAL_INDEX_MAX_CELL_SIZE = 2.0f; const uint32_t SPATIAL_INDEX_MAX_GRID_DIM = 1000; - /*******************************************/ - // DEBUG CONFIGURATION - /*******************************************/ - - /** - * @brief Set debug mode for spatial index operations - * @param enable Enable (true) or disable (false) debug output - * - * Controls whether spatial index operations log diagnostic messages. - * Only has effect when NAVMESH_DEBUG is enabled at compile time. - * Called automatically by navmesh::init() with the debug parameter. - * - * Time Complexity: O(1) - */ - void set_debug_mode(bool enable); - /*******************************************/ // SPATIAL INDEX MANAGEMENT /*******************************************/ @@ -67,6 +51,7 @@ namespace pathfinder /** * @brief Build spatial index for fast cell lookup * @param navmesh NavMesh to build index for + * @param debug Pass ctx->m_DebugMode to enable diagnostic output * @return Pointer to newly created spatial index, NULL on failure * * Algorithm: @@ -85,7 +70,7 @@ namespace pathfinder * Caller is responsible for calling destroy_spatial_index() when done. * Returns NULL if NavMesh has no cells or memory allocation fails. */ - NavMeshSpatialIndex* build_spatial_index(PolygonNavMesh* navmesh); + NavMeshSpatialIndex* build_spatial_index(PolygonNavMesh* navmesh, bool debug = false); /** * @brief Destroy spatial index and free all resources @@ -108,6 +93,7 @@ namespace pathfinder * @param position Position to query * @param enable_fallback If true, find nearest cell when position not in any cell (default: true) * @param out_used_fallback Output parameter indicating if fallback was used (optional, can be NULL) + * @param debug Pass ctx->m_DebugMode to enable diagnostic output (default: false) * @return Cell index containing position, -1 if not found or fallback disabled * * Algorithm: @@ -136,7 +122,11 @@ namespace pathfinder * 2. enable_fallback=false: Reject clicks on walls/obstacles (prevent invalid paths) * 3. Check out_used_fallback: Move agent to nearest cell vs target position */ - int find_cell_at_position(PolygonNavMesh* navmesh, Vec2 position, bool enable_fallback = true, bool* out_used_fallback = NULL); + int find_cell_at_position(PolygonNavMesh* navmesh, + Vec2 position, + bool enable_fallback = true, + bool* out_used_fallback = NULL, + bool debug = false); /*******************************************/ // UTILITY FUNCTIONS diff --git a/graph_pathfinder/include/pathfinder_navmesh_types.h b/graph_pathfinder/include/pathfinder_navmesh_types.h index 249c067..0acbb20 100644 --- a/graph_pathfinder/include/pathfinder_navmesh_types.h +++ b/graph_pathfinder/include/pathfinder_navmesh_types.h @@ -19,6 +19,9 @@ #include "dmarray_include.h" #include "dmhashtable_include.h" +#include "pathfinder_cache.h" +#include "pathfinder_distance_cache.h" +#include "pathfinder_heap.h" #include "pathfinder_types.h" #include #include @@ -53,6 +56,35 @@ namespace pathfinder bool m_Walkable; // Is walkable? (false = removed/obstacle) } Cell; + /*******************************************/ + // POLYGON A* SEARCH NODE (for A* scratch buffer) + /*******************************************/ + + /** + * @brief Internal A* search node for polygon pathfinding + * + * Stores per-cell state during a single A* search. A pre-allocated array + * of these nodes (sized to m_MaxCells) is stored in NavMeshContext to avoid + * a heap allocation on every find_polygon_path() call. + * + * Generation Counter: + * A generation counter replaces a full-array reset between searches. + * If m_Generation does not match the context's current generation, the node + * is treated as unvisited (stale values are ignored). This keeps reset cost + * proportional to the number of cells actually visited rather than the total + * cell count. + */ + typedef struct PolygonAStarNode + { + uint32_t m_CellIdx; // Index into cell array + float m_GScore; // Cost from start + float m_FScore; // Estimated total cost (g + h) + uint32_t m_CameFrom; // Previous cell in path (INVALID_ID if none) + bool m_InOpen; // In open set + bool m_InClosed; // In closed set + uint32_t m_Generation; // Generation stamp — 0 means uninitialized + } PolygonAStarNode; + /*******************************************/ // POLYGON GRAPH NODE (for Recast-style A*) /*******************************************/ @@ -239,6 +271,49 @@ namespace pathfinder SpatialConfig m_SpatialConfig; // Configuration for spatial index grid sizing } PolygonNavMesh; + /*******************************************/ + // NAVMESH CONTEXT (user-managed lifetime) + /*******************************************/ + + /** + * @brief User-managed NavMesh context + * + * Encapsulates all state required for polygon-based pathfinding. + * Created via create_context(), destroyed via destroy_context(). + * + * Memory Ownership: + * - User creates and owns this context (malloc'd in create_context) + * - All sub-contexts (heap, cache, distance_cache) are owned by this context + * - User must call destroy_context() to free all memory + * + * Multiple instances are supported by creating separate contexts. + * + * Cache Efficiency: + * - Hot data (NavMesh pointer, sub-contexts) placed first for cache locality + * - Configuration (FunnelConfig) placed last as it is rarely accessed + */ + typedef struct NavMeshContext + { + // Hot path: frequently accessed during pathfinding + PolygonNavMesh* m_NavMesh; // Main navmesh container + heap::HeapContext* m_HeapContext; // Heap for A* priority queue + distance_cache::DistanceCacheContext* m_DistanceCacheContext; // Distance heuristic cache + cache::CacheContext* m_CacheContext; // Path result cache (LRU) + + // Pre-allocated A* scratch buffer (avoids per-call heap allocation) + // Sized to m_MaxCells at creation time. Reused across find_polygon_path() + // calls via the generation counter m_AStarGeneration. + PolygonAStarNode* m_AStarScratch; // Flat array [0..m_MaxCells-1] + uint32_t m_AStarGeneration; // Incremented each search; 0 is reserved + + // Capacity limits + uint32_t m_MaxCells; // Maximum cell capacity + + // Cold path: algorithm configuration + FunnelConfig m_FunnelConfig; // Funnel algorithm tolerances + bool m_DebugMode; // Runtime debug flag + } NavMeshContext; + } // namespace navmesh } // namespace pathfinder diff --git a/graph_pathfinder/lib/arm64-android/libGraphPathfinder.a b/graph_pathfinder/lib/arm64-android/libGraphPathfinder.a index f3bc4d2..2e2389e 100644 Binary files a/graph_pathfinder/lib/arm64-android/libGraphPathfinder.a and b/graph_pathfinder/lib/arm64-android/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/arm64-ios/libGraphPathfinder.a b/graph_pathfinder/lib/arm64-ios/libGraphPathfinder.a index a329159..67dd6d1 100644 Binary files a/graph_pathfinder/lib/arm64-ios/libGraphPathfinder.a and b/graph_pathfinder/lib/arm64-ios/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/arm64-linux/libGraphPathfinder.a b/graph_pathfinder/lib/arm64-linux/libGraphPathfinder.a index b15479e..61214be 100644 Binary files a/graph_pathfinder/lib/arm64-linux/libGraphPathfinder.a and b/graph_pathfinder/lib/arm64-linux/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/arm64-osx/libGraphPathfinder.a b/graph_pathfinder/lib/arm64-osx/libGraphPathfinder.a index 2d06123..e4f42ed 100644 Binary files a/graph_pathfinder/lib/arm64-osx/libGraphPathfinder.a and b/graph_pathfinder/lib/arm64-osx/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/armv7-android/libGraphPathfinder.a b/graph_pathfinder/lib/armv7-android/libGraphPathfinder.a index 6073ccd..5f03468 100644 Binary files a/graph_pathfinder/lib/armv7-android/libGraphPathfinder.a and b/graph_pathfinder/lib/armv7-android/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/js-web/libGraphPathfinder.a b/graph_pathfinder/lib/js-web/libGraphPathfinder.a index 91dcfa2..df69aec 100644 Binary files a/graph_pathfinder/lib/js-web/libGraphPathfinder.a and b/graph_pathfinder/lib/js-web/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/wasm-web/libGraphPathfinder.a b/graph_pathfinder/lib/wasm-web/libGraphPathfinder.a index 91dcfa2..df69aec 100644 Binary files a/graph_pathfinder/lib/wasm-web/libGraphPathfinder.a and b/graph_pathfinder/lib/wasm-web/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/x86_64-ios/libGraphPathfinder.a b/graph_pathfinder/lib/x86_64-ios/libGraphPathfinder.a index 2499434..d02b8db 100644 Binary files a/graph_pathfinder/lib/x86_64-ios/libGraphPathfinder.a and b/graph_pathfinder/lib/x86_64-ios/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/x86_64-linux/libGraphPathfinder.a b/graph_pathfinder/lib/x86_64-linux/libGraphPathfinder.a index 0fab377..52ed69b 100644 Binary files a/graph_pathfinder/lib/x86_64-linux/libGraphPathfinder.a and b/graph_pathfinder/lib/x86_64-linux/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/x86_64-osx/libGraphPathfinder.a b/graph_pathfinder/lib/x86_64-osx/libGraphPathfinder.a index 76d4dfd..fa68419 100644 Binary files a/graph_pathfinder/lib/x86_64-osx/libGraphPathfinder.a and b/graph_pathfinder/lib/x86_64-osx/libGraphPathfinder.a differ diff --git a/graph_pathfinder/lib/x86_64-win32/GraphPathfinder.lib b/graph_pathfinder/lib/x86_64-win32/GraphPathfinder.lib index 87d324f..fe1d6f3 100644 Binary files a/graph_pathfinder/lib/x86_64-win32/GraphPathfinder.lib and b/graph_pathfinder/lib/x86_64-win32/GraphPathfinder.lib differ diff --git a/graph_pathfinder/src/pathfinder.cpp b/graph_pathfinder/src/pathfinder.cpp index 3449119..3621b04 100644 --- a/graph_pathfinder/src/pathfinder.cpp +++ b/graph_pathfinder/src/pathfinder.cpp @@ -115,28 +115,41 @@ static inline void push_smoothed_path_table(lua_State* L, const dmArray smooth_path; smooth_path.SetCapacity(max_path); - uint32_t path_length = pathfinder::navmesh::find_path_from_positions( - start_position, goal_position, &smooth_path, max_path, agent_radius, enable_fallback, &status); + pathfinder::extension::navmesh_find_path(navmesh_id, &path_length, start_position, goal_position, &smooth_path, max_path, agent_radius, enable_fallback, &status); // OUT -> lua_pushinteger(L, path_length); @@ -210,7 +227,10 @@ static int pathfinder_navmesh_find_raw(lua_State* L) static int pathfinder_navmesh_get_spatial_index(lua_State* L) { DM_LUA_STACK_CHECK(L, 1); - pathfinder::navmesh::NavMeshSpatialIndex* spatial_index = pathfinder::navmesh::get_spatial_index(); + + uint8_t navmesh_id = luaL_checkint(L, 1); + + pathfinder::navmesh::NavMeshSpatialIndex* spatial_index = pathfinder::extension::navmesh_get_spatial_index(navmesh_id); // grid table lua_createtable(L, 0, 2); @@ -288,10 +308,13 @@ static int pathfinder_navmesh_get_spatial_index(lua_State* L) static int pathfinder_navmesh_set_funnel(lua_State* L) { DM_LUA_STACK_CHECK(L, 0); - float portal_vertex_tolerance = luaL_checknumber(L, 1); - float portal_collapse_threshold = luaL_checknumber(L, 2); - float waypoint_duplicate_tolerance = luaL_checknumber(L, 3); - pathfinder::navmesh::funnel_init(portal_vertex_tolerance, portal_collapse_threshold, waypoint_duplicate_tolerance); + + uint8_t navmesh_id = luaL_checkint(L, 1); + float portal_vertex_tolerance = luaL_checknumber(L, 2); + float portal_collapse_threshold = luaL_checknumber(L, 3); + float waypoint_duplicate_tolerance = luaL_checknumber(L, 4); + + pathfinder::extension::navmesh_set_funnel(navmesh_id, portal_vertex_tolerance, portal_collapse_threshold, waypoint_duplicate_tolerance); return 0; } @@ -300,6 +323,8 @@ static int pathfinder_navmesh_get_stats(lua_State* L) { DM_LUA_STACK_CHECK(L, 1); + uint8_t navmesh_id = luaL_checkint(L, 1); + uint32_t cache_entries; uint32_t cache_capacity; uint32_t cache_hit_rate; @@ -308,7 +333,7 @@ static int pathfinder_navmesh_get_stats(lua_State* L) uint32_t dist_cache_misses; uint32_t dist_cache_hit_rate; - pathfinder::extension::navmesh_get_stats(cache_entries, cache_capacity, cache_hit_rate, dist_cache_size, dist_cache_hits, dist_cache_misses, dist_cache_hit_rate); + pathfinder::extension::navmesh_get_stats(navmesh_id, cache_entries, cache_capacity, cache_hit_rate, dist_cache_size, dist_cache_hits, dist_cache_misses, dist_cache_hit_rate); // ============================================================================ // CREATE RESULT TABLE @@ -348,13 +373,15 @@ static int pathfinder_navmesh_find_cell_at_position(lua_State* L) { DM_LUA_STACK_CHECK(L, 3); - float x = luaL_checknumber(L, 1); - float y = luaL_checknumber(L, 2); + uint8_t navmesh_id = luaL_checkint(L, 1); + float x = luaL_checknumber(L, 2); + float y = luaL_checknumber(L, 3); pathfinder::Vec2 position = pathfinder::Vec2(x, y); + uint32_t cell_id = 0; + pathfinder::Vec2 center = pathfinder::Vec2(0, 0); - uint32_t cell_id = pathfinder::navmesh::find_cell_at_position(position, false); - pathfinder::Vec2 center = pathfinder::navmesh::get_cell_center(cell_id); + pathfinder::extension::navmesh_cell_at_position(navmesh_id, position, &cell_id, ¢er); lua_pushinteger(L, cell_id); lua_pushinteger(L, center.x); @@ -1436,11 +1463,12 @@ static const luaL_reg Module_methods[] = { // Navmesh { "navmesh_init", pathfinder_navmesh_init }, + { "navmesh_remove", pathfinder_navmesh_remove }, { "navmesh_shutdown", pathfinder_navmesh_shutdown }, { "navmesh_set_buffer", pathfinder_navmesh_set_buffer }, + { "navmesh_find_path", pathfinder_navmesh_find_smoothed }, - //{ "navmesh_find_path_raw", pathfinder_navmesh_find_raw }, // TODO -> No good use - { "navmesh_cell_at_position", pathfinder_navmesh_find_cell_at_position }, // No good use + { "navmesh_cell_at_position", pathfinder_navmesh_find_cell_at_position }, { "navmesh_get_stats", pathfinder_navmesh_get_stats }, { "navmesh_get_spatial_index", pathfinder_navmesh_get_spatial_index }, { "navmesh_set_funnel", pathfinder_navmesh_set_funnel }, @@ -1576,7 +1604,7 @@ static dmExtension::Result AppFinalizeGraphPathfinder(dmExtension::AppParams* pa dmLogInfo("AppFinalizeGraphPathfinder"); pathfinder::extension::shutdown(); pathfinder::path::shutdown(); - pathfinder::navmesh::shutdown(); + pathfinder::extension::navmesh_shutdown(); return dmExtension::RESULT_OK; } diff --git a/graph_pathfinder/src/pathfinder_extension.cpp b/graph_pathfinder/src/pathfinder_extension.cpp index 75fb3b2..ad167be 100644 --- a/graph_pathfinder/src/pathfinder_extension.cpp +++ b/graph_pathfinder/src/pathfinder_extension.cpp @@ -51,6 +51,12 @@ namespace pathfinder static dmHashTable32 m_Gameobjects; + //========================================================== + // Navmeshs + //========================================================== + static dmHashTable16 m_NavmeshContext; + static uint8_t m_NavmeshId = 0; + //========================================================== // Update //========================================================== @@ -358,8 +364,53 @@ namespace pathfinder } } - void navmesh_set_buffer(dmBuffer::HBuffer& buffer) + uint8_t navmesh_init(pathfinder::navmesh::NavMeshContext* ctx) + { + if (m_NavmeshContext.Full()) + { + m_NavmeshContext.SetCapacity(m_NavmeshContext.Size() + 1); + } + m_NavmeshId++; + m_NavmeshContext.Put(m_NavmeshId, ctx); + return m_NavmeshId; + } + + static inline pathfinder::navmesh::NavMeshContext* get_navmesh_ctx(uint8_t id) + { + pathfinder::navmesh::NavMeshContext** ctx = m_NavmeshContext.Get(id); + if (!ctx) + return 0; + return *ctx; + } + + static inline void navmesh_iterate_callback(void* /*context*/, const uint16_t* /*key*/, pathfinder::navmesh::NavMeshContext** ctx) { + pathfinder::navmesh::destroy_context(*ctx); + } + + void navmesh_shutdown() + { + m_NavmeshContext.Iterate(navmesh_iterate_callback, (void*)0x0); + m_NavmeshContext.Clear(); + m_NavmeshId = 0; + } + + void navmesh_remove(uint8_t navmesh_id) + { + pathfinder::navmesh::NavMeshContext* ctx = get_navmesh_ctx(navmesh_id); + if (!ctx) + return; + + pathfinder::navmesh::destroy_context(ctx); + m_NavmeshContext.Erase(navmesh_id); + } + + void navmesh_set_buffer(uint8_t navmesh_id, dmBuffer::HBuffer& buffer) + { + pathfinder::navmesh::NavMeshContext* ctx = get_navmesh_ctx(navmesh_id); + if (!ctx) + return; + void* data = 0; uint32_t count = 0; uint32_t components = 0; @@ -408,20 +459,73 @@ namespace pathfinder // dmLogInfo("t: %u - x %f - y: % f", t, vertices[v].x, vertices[v].y); } - // I'm are not doing anything with cell_id yet - uint32_t cell_id = pathfinder::navmesh::add_cell(vertices, 3, &status); + // not doing anything with cell_id yet + uint32_t cell_id = pathfinder::navmesh::add_cell(ctx, vertices, 3, &status); if (status != pathfinder::SUCCESS) { dmLogError("Failed to add cell %u (status: %d)", t, status); return; } } - pathfinder::navmesh::build_adjacency(); + pathfinder::navmesh::build_adjacency(ctx); dmLogInfo("Successfully built navmesh with %u triangles", tri_count); } - void navmesh_get_stats(uint32_t& cache_entries, + void navmesh_find_path(uint8_t navmesh_id, + uint32_t* path_length, + pathfinder::Vec2 start_position, + pathfinder::Vec2 goal_position, + dmArray* smooth_path, + uint32_t max_path, + float agent_radius, + bool enable_fallback, + PathStatus* status) + { + pathfinder::navmesh::NavMeshContext* ctx = get_navmesh_ctx(navmesh_id); + if (!ctx) + return; + + *path_length = pathfinder::navmesh::find_path_from_positions(ctx, + start_position, + goal_position, + smooth_path, + max_path, + agent_radius, + enable_fallback, + status); + } + + void navmesh_cell_at_position(uint8_t navmesh_id, pathfinder::Vec2 position, uint32_t* cell_id, pathfinder::Vec2* center) + { + pathfinder::navmesh::NavMeshContext* ctx = get_navmesh_ctx(navmesh_id); + if (!ctx) + return; + + *cell_id = pathfinder::navmesh::find_cell_at_position(ctx, position, false); + *center = pathfinder::navmesh::get_cell_center(ctx, *cell_id); + } + + navmesh::NavMeshSpatialIndex* navmesh_get_spatial_index(uint8_t navmesh_id) + { + pathfinder::navmesh::NavMeshContext* ctx = get_navmesh_ctx(navmesh_id); + if (!ctx) + return 0; + + return pathfinder::navmesh::get_spatial_index(ctx); + } + + void navmesh_set_funnel(uint8_t navmesh_id, float portal_vertex_tolerance, float portal_collapse_threshold, float waypoint_duplicate_tolerance) + { + pathfinder::navmesh::NavMeshContext* ctx = get_navmesh_ctx(navmesh_id); + if (!ctx) + return; + + pathfinder::navmesh::funnel_init(ctx, portal_vertex_tolerance, portal_collapse_threshold, waypoint_duplicate_tolerance); + } + + void navmesh_get_stats(uint8_t navmesh_id, + uint32_t& cache_entries, uint32_t& cache_capacity, uint32_t& cache_hit_rate, uint32_t& dist_cache_size, @@ -429,8 +533,12 @@ namespace pathfinder uint32_t& dist_cache_misses, uint32_t& dist_cache_hit_rate) { + pathfinder::navmesh::NavMeshContext* ctx = get_navmesh_ctx(navmesh_id); + if (!ctx) + return; + // Path cache statistics - pathfinder::cache::CacheContext* cache_ctx = pathfinder::navmesh::get_cache_context(); + pathfinder::cache::CacheContext* cache_ctx = pathfinder::navmesh::get_cache_context(ctx); if (cache_ctx) { @@ -438,7 +546,7 @@ namespace pathfinder } // Distance cache statistics - pathfinder::distance_cache::DistanceCacheContext* dist_cache_ctx = pathfinder::navmesh::get_distance_cache_context(); + pathfinder::distance_cache::DistanceCacheContext* dist_cache_ctx = pathfinder::navmesh::get_distance_cache_context(ctx); if (dist_cache_ctx) { pathfinder::distance_cache::get_stats(dist_cache_ctx, &dist_cache_size, &dist_cache_hits, &dist_cache_misses, &dist_cache_hit_rate);